Added goto line command

This commit is contained in:
Joshua Barretto 2025-06-12 00:57:22 +01:00
parent d352d04030
commit 846cc31174
8 changed files with 117 additions and 48 deletions

2
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "anstream" name = "anstream"

View file

@ -30,6 +30,8 @@ pub enum Action {
OpenOpener(PathBuf), // Open the file opener OpenOpener(PathBuf), // Open the file opener
SwitchBuffer(BufferId), // Switch the current pane to the given buffer SwitchBuffer(BufferId), // Switch the current pane to the given buffer
OpenFile(PathBuf), // Open the file and switch the current pane to it OpenFile(PathBuf), // Open the file and switch the current pane to it
CommandStart(&'static str), // Start a new command
GotoLine(isize), // Go to the specified file line
} }
#[derive(Debug)] #[derive(Debug)]
@ -220,6 +222,22 @@ impl RawEvent {
} }
} }
pub fn to_command_start(&self) -> Option<Action> {
if matches!(
&self.0,
TerminalEvent::Key(KeyEvent {
code: KeyCode::Char('l'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
})
) {
Some(Action::CommandStart("goto_line"))
} else {
None
}
}
pub fn to_go(&self) -> Option<Action> { pub fn to_go(&self) -> Option<Action> {
if matches!( if matches!(
&self.0, &self.0,

View file

@ -123,7 +123,12 @@ impl Buffer {
} }
// If the file doesn't exist, create a new file // If the file doesn't exist, create a new file
Err(err) if err.kind() == io::ErrorKind::NotFound => { Err(err) if err.kind() == io::ErrorKind::NotFound => {
(path.parent().map(Path::to_owned), Vec::new()) let dir = path
.parent()
.filter(|p| p.to_str() != Some(""))
.map(Path::to_owned)
.or_else(|| std::env::current_dir().ok());
(dir, Vec::new())
} }
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
@ -152,6 +157,15 @@ impl Buffer {
}); });
} }
pub fn goto_line_cursor(&mut self, cursor_id: CursorId, line: isize) {
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return;
};
cursor.pos = self.text.to_pos([0, line]);
cursor.reset_desired_col(&self.text);
cursor.base = cursor.pos;
}
pub fn move_cursor( pub fn move_cursor(
&mut self, &mut self,
cursor_id: CursorId, cursor_id: CursorId,

View file

@ -185,12 +185,8 @@ impl<'a> Rect<'a> {
self.rect([0, 0], self.size()) self.rect([0, 0], self.size())
} }
pub fn text<C: Borrow<char>>( pub fn text(&mut self, origin: [isize; 2], text: &str) -> Rect {
&mut self, for (idx, c) in text.chars().enumerate() {
origin: [isize; 2],
text: impl IntoIterator<Item = C>,
) -> Rect {
for (idx, c) in text.into_iter().enumerate() {
if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0 if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0
{ {
let cell = Cell { let cell = Cell {

View file

@ -77,6 +77,11 @@ impl Input {
self.refocus(buffer, cursor_id); self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::GotoLine(line)) => {
buffer.goto_line_cursor(cursor_id, line);
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
_ => Err(event), _ => Err(event),
} }
} }
@ -136,13 +141,13 @@ impl Input {
.with_bg(state.theme.margin_bg) .with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num) .with_fg(state.theme.margin_line_num)
.fill(' ') .fill(' ')
.text([0, 0], ">".chars()), .text([0, 0], ">"),
Mode::Doc => frame Mode::Doc => frame
.rect([0, i], [margin_w, 1]) .rect([0, i], [margin_w, 1])
.with_bg(state.theme.margin_bg) .with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num) .with_fg(state.theme.margin_line_num)
.fill(' ') .fill(' ')
.text([1, 0], format!("{:>line_num_w$}", line_num + 1).chars()), .text([1, 0], &format!("{:>line_num_w$}", line_num + 1)),
}; };
// Line // Line
@ -166,7 +171,7 @@ impl Input {
state.theme.unfocus_select_bg state.theme.unfocus_select_bg
}) })
.with_fg(fg) .with_fg(fg)
.text([i as isize, 0], &[c]); .text([i as isize, 0], c.encode_utf8(&mut [0; 4]));
} }
} }

View file

@ -96,7 +96,7 @@ impl Visual for Label {
fn render(&mut self, state: &State, frame: &mut Rect) { fn render(&mut self, state: &State, frame: &mut Rect) {
frame.with(|frame| { frame.with(|frame| {
for (idx, line) in self.lines().enumerate() { for (idx, line) in self.lines().enumerate() {
frame.text([0, idx as isize], line.chars()); frame.text([0, idx as isize], &line);
} }
}); });
} }

View file

@ -9,24 +9,29 @@ pub struct Prompt {
} }
impl Prompt { impl Prompt {
pub fn new() -> Self { pub fn new(init: &str) -> Self {
let mut buffer = Buffer::default(); let mut buffer = Buffer::default();
let cursor_id = buffer.start_session();
buffer.enter(cursor_id, init.chars());
Self { Self {
cursor_id: buffer.start_session(),
buffer, buffer,
cursor_id,
input: Input::prompt(), input: Input::prompt(),
} }
} }
pub fn get_action(&self) -> Option<Action> { pub fn parse_action(&self) -> Result<Action, String> {
match self.buffer.text.to_string().as_str() { let cmd = self.buffer.text.to_string();
let mut args = cmd.as_str().split_whitespace();
match args.next() {
// The root sees 'cancel' as an initiator for quitting // The root sees 'cancel' as an initiator for quitting
"q" | "quit" => Some(Action::Cancel), Some("q" | "quit") => Ok(Action::Cancel),
"version" => Some(Action::Show( Some("version") => Ok(Action::Show(
Some(format!("Version")), Some(format!("Version")),
format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
)), )),
"?" | "help" => Some(Action::Show( Some("?" | "help") => Ok(Action::Show(
Some(format!("Help")), Some(format!("Help")),
format!( format!(
"Temporary help info:\n\ "Temporary help info:\n\
@ -37,9 +42,20 @@ impl Prompt {
- help" - help"
), ),
)), )),
"pane_move_left" => Some(Action::PaneMove(Dir::Left)), Some("pane_move_left") => Ok(Action::PaneMove(Dir::Left)),
"pane_move_right" => Some(Action::PaneMove(Dir::Right)), Some("pane_move_right") => Ok(Action::PaneMove(Dir::Right)),
_ => None, Some("goto_line") => {
// Subtract 1 due to zero indexing
let line = args
.next()
.ok_or_else(|| "Expected argument".to_string())?
.parse::<isize>()
.map_err(|_| "Expected integer".to_string())?
- 1;
Ok(Action::GotoLine(line))
}
Some(cmd) => Err(format!("Unknown command `{cmd}`")),
None => Err(format!("No command entered")),
} }
} }
@ -50,22 +66,20 @@ impl Prompt {
impl Element<()> for Prompt { impl Element<()> for Prompt {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
match event.to_action(|e| e.to_go().or_else(|| e.to_cancel())) { match event.to_action(|e| {
e.to_go()
.or_else(|| e.to_cancel().or_else(|| e.to_command_start()))
}) {
Some(Action::Cancel) => Ok(Resp::end(None)), Some(Action::Cancel) => Ok(Resp::end(None)),
Some(Action::Go) => { Some(Action::Go) => match self.parse_action() {
if let Some(action) = self.get_action() { Ok(action) => {
self.buffer.clear(); self.buffer.clear();
Ok(Resp::end(Some(action.into()))) Ok(Resp::end(Some(action.into())))
} else {
Ok(Resp::handled(Some(
Action::Show(
Some(format!("Error")),
format!("unknown command `{}`", self.buffer.text.to_string()),
)
.into(),
)))
} }
} Err(err) => Ok(Resp::handled(Some(
Action::Show(Some(format!("Error")), err).into(),
))),
},
_ => self _ => self
.input .input
.handle(&mut self.buffer, self.cursor_id, event) .handle(&mut self.buffer, self.cursor_id, event)
@ -248,7 +262,7 @@ impl Visual for BufferId {
let Some(buffer) = state.buffers.get(*self) else { let Some(buffer) = state.buffers.get(*self) else {
return; return;
}; };
frame.text([0, 0], buffer.name().unwrap_or("<unknown>").chars()); frame.text([0, 0], buffer.name().unwrap_or("<unknown>"));
} }
} }
@ -313,11 +327,15 @@ impl Opener {
is_link: entry.file_type().ok()?.is_symlink(), is_link: entry.file_type().ok()?.is_symlink(),
}) })
}) })
.chain([FileOption { .chain(if filter != "" {
path: [dir, &filter].into_iter().collect(), Some(FileOption {
kind: FileKind::New, path: [dir, &filter].into_iter().collect(),
is_link: false, kind: FileKind::New,
}]); is_link: false,
})
} else {
None
});
// TODO // TODO
self.options.set_options(options, |e| { self.options.set_options(options, |e| {
let name = e.path.file_name()?.to_str()?.to_lowercase(); let name = e.path.file_name()?.to_str()?.to_lowercase();
@ -395,9 +413,15 @@ pub struct FileOption {
impl Visual for FileOption { impl Visual for FileOption {
fn render(&mut self, state: &State, frame: &mut Rect) { fn render(&mut self, state: &State, frame: &mut Rect) {
let name = match self.path.file_name().and_then(|n| n.to_str()) { let name = match self.path.file_name().and_then(|n| n.to_str()) {
Some(name) if matches!(self.kind, FileKind::Dir) => format!("{}/", name), Some(name) if matches!(self.kind, FileKind::Dir) => format!("{name}/"),
Some(name) => name.to_string(), Some(name) => format!("{name}"),
None => format!("<unknown>"), None => format!("Unknown"),
};
let desc = match self.kind {
FileKind::Dir => "Directory",
FileKind::Unknown => "Unknown filesystem item",
FileKind::File => "File",
FileKind::New => "Create new file",
}; };
frame frame
.with_fg(match self.kind { .with_fg(match self.kind {
@ -405,7 +429,10 @@ impl Visual for FileOption {
FileKind::File | FileKind::Unknown => state.theme.option_file, FileKind::File | FileKind::Unknown => state.theme.option_file,
FileKind::New => state.theme.option_new, FileKind::New => state.theme.option_new,
}) })
.text([0, 0], name.chars()); .text([0, 0], &name);
frame.with_fg(state.theme.margin_line_num).with(|f| {
f.text([f.size()[0] as isize / 2, 0], &desc);
});
} }
} }

View file

@ -77,13 +77,17 @@ impl Element<()> for Root {
}; };
// Handle 'top-level' actions // Handle 'top-level' actions
if let Some(action) = if let Some(action) = action.and_then(|e| {
action.and_then(|e| e.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel()))) e.to_action(|e| {
{ e.to_open_prompt()
.or_else(|| e.to_cancel())
.or_else(|| e.to_command_start())
})
}) {
match action { match action {
Action::OpenPrompt => { Action::OpenPrompt => {
self.tasks.clear(); // Prompt overrides all self.tasks.clear(); // Prompt overrides all
self.tasks.push(Task::Prompt(Prompt::new())); self.tasks.push(Task::Prompt(Prompt::new("")));
} }
Action::OpenSwitcher => { Action::OpenSwitcher => {
self.tasks.clear(); // Overrides all self.tasks.clear(); // Overrides all
@ -94,6 +98,11 @@ impl Element<()> for Root {
self.tasks.clear(); // Overrides all self.tasks.clear(); // Overrides all
self.tasks.push(Task::Opener(Opener::new(path))); self.tasks.push(Task::Opener(Opener::new(path)));
} }
Action::CommandStart(cmd) => {
self.tasks.clear(); // Prompt overrides all
self.tasks
.push(Task::Prompt(Prompt::new(&format!("{cmd} "))));
}
Action::Cancel => self.tasks.push(Task::Confirm(Confirm { Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
label: Label("Are you sure you wish to quit? (y/n)".to_string()), label: Label("Are you sure you wish to quit? (y/n)".to_string()),
action: Action::Quit, action: Action::Quit,