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.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "anstream"

View file

@ -30,6 +30,8 @@ pub enum Action {
OpenOpener(PathBuf), // Open the file opener
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
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)]
@ -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> {
if matches!(
&self.0,

View file

@ -123,7 +123,12 @@ impl Buffer {
}
// If the file doesn't exist, create a new file
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()),
};
@ -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(
&mut self,
cursor_id: CursorId,

View file

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

View file

@ -77,6 +77,11 @@ impl Input {
self.refocus(buffer, cursor_id);
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),
}
}
@ -136,13 +141,13 @@ impl Input {
.with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num)
.fill(' ')
.text([0, 0], ">".chars()),
.text([0, 0], ">"),
Mode::Doc => frame
.rect([0, i], [margin_w, 1])
.with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num)
.fill(' ')
.text([1, 0], format!("{:>line_num_w$}", line_num + 1).chars()),
.text([1, 0], &format!("{:>line_num_w$}", line_num + 1)),
};
// Line
@ -166,7 +171,7 @@ impl Input {
state.theme.unfocus_select_bg
})
.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) {
frame.with(|frame| {
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 {
pub fn new() -> Self {
pub fn new(init: &str) -> Self {
let mut buffer = Buffer::default();
let cursor_id = buffer.start_session();
buffer.enter(cursor_id, init.chars());
Self {
cursor_id: buffer.start_session(),
buffer,
cursor_id,
input: Input::prompt(),
}
}
pub fn get_action(&self) -> Option<Action> {
match self.buffer.text.to_string().as_str() {
pub fn parse_action(&self) -> Result<Action, String> {
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
"q" | "quit" => Some(Action::Cancel),
"version" => Some(Action::Show(
Some("q" | "quit") => Ok(Action::Cancel),
Some("version") => Ok(Action::Show(
Some(format!("Version")),
format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
)),
"?" | "help" => Some(Action::Show(
Some("?" | "help") => Ok(Action::Show(
Some(format!("Help")),
format!(
"Temporary help info:\n\
@ -37,9 +42,20 @@ impl Prompt {
- help"
),
)),
"pane_move_left" => Some(Action::PaneMove(Dir::Left)),
"pane_move_right" => Some(Action::PaneMove(Dir::Right)),
_ => None,
Some("pane_move_left") => Ok(Action::PaneMove(Dir::Left)),
Some("pane_move_right") => Ok(Action::PaneMove(Dir::Right)),
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 {
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::Go) => {
if let Some(action) = self.get_action() {
Some(Action::Go) => match self.parse_action() {
Ok(action) => {
self.buffer.clear();
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
.input
.handle(&mut self.buffer, self.cursor_id, event)
@ -248,7 +262,7 @@ impl Visual for BufferId {
let Some(buffer) = state.buffers.get(*self) else {
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(),
})
})
.chain([FileOption {
path: [dir, &filter].into_iter().collect(),
kind: FileKind::New,
is_link: false,
}]);
.chain(if filter != "" {
Some(FileOption {
path: [dir, &filter].into_iter().collect(),
kind: FileKind::New,
is_link: false,
})
} else {
None
});
// TODO
self.options.set_options(options, |e| {
let name = e.path.file_name()?.to_str()?.to_lowercase();
@ -395,9 +413,15 @@ pub struct FileOption {
impl Visual for FileOption {
fn render(&mut self, state: &State, frame: &mut Rect) {
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) => name.to_string(),
None => format!("<unknown>"),
Some(name) if matches!(self.kind, FileKind::Dir) => format!("{name}/"),
Some(name) => format!("{name}"),
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
.with_fg(match self.kind {
@ -405,7 +429,10 @@ impl Visual for FileOption {
FileKind::File | FileKind::Unknown => state.theme.option_file,
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
if let Some(action) =
action.and_then(|e| e.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel())))
{
if let Some(action) = action.and_then(|e| {
e.to_action(|e| {
e.to_open_prompt()
.or_else(|| e.to_cancel())
.or_else(|| e.to_command_start())
})
}) {
match action {
Action::OpenPrompt => {
self.tasks.clear(); // Prompt overrides all
self.tasks.push(Task::Prompt(Prompt::new()));
self.tasks.push(Task::Prompt(Prompt::new("")));
}
Action::OpenSwitcher => {
self.tasks.clear(); // Overrides all
@ -94,6 +98,11 @@ impl Element<()> for Root {
self.tasks.clear(); // Overrides all
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 {
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
action: Action::Quit,