aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2025-04-28 15:25:37 +0100
committerGitHub <noreply@github.com>2025-04-28 15:25:37 +0100
commitcd5d337b52ad16a834cf8909b48598366e9a6efa (patch)
treeb4c1fb66ac8106dde6a3bdb4532de05e602df368
parentfeat: sort `atuin store status` output (#2719) (diff)
downloadatuin-cd5d337b52ad16a834cf8909b48598366e9a6efa.zip
fix: selection vs render issue (#2706)
* fix: selection vs render issue * render on continue too * clippy * fmt
Diffstat (limited to '')
-rw-r--r--crates/atuin/src/command/client/search/interactive.rs96
1 files changed, 66 insertions, 30 deletions
diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs
index 0fd7cbb6..adbb73e0 100644
--- a/crates/atuin/src/command/client/search/interactive.rs
+++ b/crates/atuin/src/command/client/search/interactive.rs
@@ -49,7 +49,7 @@ use ratatui::{
const TAB_TITLES: [&str; 2] = ["Search", "Inspect"];
pub enum InputAction {
- Accept(usize),
+ Accept(History),
Copy(usize),
Delete(usize),
ReturnOriginal,
@@ -110,13 +110,14 @@ impl State {
settings: &Settings,
input: &Event,
w: &mut W,
+ results: &[History],
) -> Result<InputAction>
where
W: Write,
{
execute!(w, EnableMouseCapture)?;
let r = match input {
- Event::Key(k) => self.handle_key_input(settings, k),
+ Event::Key(k) => self.handle_key_input(settings, k, results),
Event::Mouse(m) => self.handle_mouse_input(*m),
Event::Paste(d) => self.handle_paste_input(d),
_ => InputAction::Continue,
@@ -198,7 +199,12 @@ impl State {
}
}
- fn handle_key_input(&mut self, settings: &Settings, input: &KeyEvent) -> InputAction {
+ fn handle_key_input(
+ &mut self,
+ settings: &Settings,
+ input: &KeyEvent,
+ results: &[History],
+ ) -> InputAction {
if input.kind == event::KeyEventKind::Release {
return InputAction::Continue;
}
@@ -223,13 +229,23 @@ impl State {
KeyCode::Char('c' | 'g') if ctrl => Some(InputAction::ReturnOriginal),
KeyCode::Esc if esc_allow_exit => Some(Self::handle_key_exit(settings)),
KeyCode::Char('[') if ctrl && esc_allow_exit => Some(Self::handle_key_exit(settings)),
- KeyCode::Tab => Some(InputAction::Accept(self.results_state.selected())),
- KeyCode::Right if cursor_at_end_of_line && settings.keys.accept_past_line_end => {
- Some(InputAction::Accept(self.results_state.selected()))
+ KeyCode::Tab => {
+ let selected = self.results_state.selected();
+ if !results.is_empty() && selected < results.len() {
+ Some(InputAction::Accept(results[selected].clone()))
+ } else {
+ Some(InputAction::ReturnQuery)
+ }
}
- KeyCode::Left if cursor_at_start_of_line && settings.keys.exit_past_line_start => {
- Some(Self::handle_key_exit(settings))
+ KeyCode::Right if cursor_at_end_of_line => {
+ let selected = self.results_state.selected();
+ if !results.is_empty() && selected < results.len() {
+ Some(InputAction::Accept(results[selected].clone()))
+ } else {
+ Some(InputAction::ReturnQuery)
+ }
}
+ KeyCode::Left if cursor_at_start_of_line => Some(Self::handle_key_exit(settings)),
KeyCode::Char('o') if ctrl => {
self.tab_index = (self.tab_index + 1) % TAB_TITLES.len();
Some(InputAction::Continue)
@@ -245,7 +261,7 @@ impl State {
// handle tab-specific input
let action = match self.tab_index {
- 0 => self.handle_search_input(settings, input),
+ 0 => self.handle_search_input(settings, input, results),
1 => super::inspector::input(self, settings, self.results_state.selected(), input),
@@ -282,16 +298,26 @@ impl State {
self.handle_search_scroll_one_line(settings, enable_exit, !settings.invert)
}
- fn handle_search_accept(&mut self, settings: &Settings) -> InputAction {
+ fn handle_search_accept(&mut self, settings: &Settings, results: &[History]) -> InputAction {
if settings.enter_accept {
self.accept = true;
}
- InputAction::Accept(self.results_state.selected())
+ let selected = self.results_state.selected();
+ if !results.is_empty() && selected < results.len() {
+ InputAction::Accept(results[selected].clone())
+ } else {
+ InputAction::ReturnQuery
+ }
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
- fn handle_search_input(&mut self, settings: &Settings, input: &KeyEvent) -> InputAction {
+ fn handle_search_input(
+ &mut self,
+ settings: &Settings,
+ input: &KeyEvent,
+ results: &[History],
+ ) -> InputAction {
let ctrl = input.modifiers.contains(KeyModifiers::CONTROL);
let alt = input.modifiers.contains(KeyModifiers::ALT);
@@ -382,14 +408,20 @@ impl State {
}
match input.code {
- KeyCode::Enter => return self.handle_search_accept(settings),
- KeyCode::Char('m') if ctrl => return self.handle_search_accept(settings),
+ KeyCode::Enter => return self.handle_search_accept(settings, results),
+ KeyCode::Char('m') if ctrl => return self.handle_search_accept(settings, results),
KeyCode::Char('y') if ctrl => {
return InputAction::Copy(self.results_state.selected());
}
KeyCode::Char(c @ '1'..='9') if modfr => {
+ let selected = self.results_state.selected();
return c.to_digit(10).map_or(InputAction::Continue, |c| {
- InputAction::Accept(self.results_state.selected() + c as usize)
+ let new_index = selected + c as usize;
+ if !results.is_empty() && new_index < results.len() {
+ InputAction::Accept(results[new_index].clone())
+ } else {
+ InputAction::ReturnQuery
+ }
});
}
KeyCode::Left if ctrl => self
@@ -1141,8 +1173,11 @@ pub async fn history(
event_ready = event_ready => {
if event_ready?? {
loop {
- match app.handle_input(settings, &event::read()?, &mut std::io::stdout())? {
- InputAction::Continue => {},
+ match app.handle_input(settings, &event::read()?, &mut std::io::stdout(), &results)? {
+ InputAction::Continue => {
+ // Redraw the UI to keep it in sync with the selection state
+ terminal.draw(|f| app.draw(f, &results, stats.clone(), settings, theme))?;
+ },
InputAction::Delete(index) => {
if results.is_empty() {
break;
@@ -1194,8 +1229,12 @@ pub async fn history(
stats = if app.tab_index == 0 {
None
} else if !results.is_empty() {
- let selected = results[app.results_state.selected()].clone();
- Some(db.stats(&selected).await?)
+ let selected = app.results_state.selected();
+ if selected < results.len() {
+ Some(db.stats(&results[selected]).await?)
+ } else {
+ None
+ }
} else {
None
};
@@ -1208,29 +1247,26 @@ pub async fn history(
}
match result {
- InputAction::Accept(index) if index < results.len() => {
- let mut command = results.swap_remove(index).command;
+ InputAction::Accept(history) => {
+ let mut command = history.command;
if accept
&& (utils::is_zsh() || utils::is_fish() || utils::is_bash() || utils::is_xonsh())
{
command = String::from("__atuin_accept__:") + &command;
}
- // index is in bounds so we return that entry
+ // We have the actual history entry, so use it directly
Ok(command)
}
InputAction::ReturnOriginal => Ok(String::new()),
InputAction::Copy(index) => {
- let cmd = results.swap_remove(index).command;
- set_clipboard(cmd);
+ if !results.is_empty() && index < results.len() {
+ let cmd = results[index].command.clone();
+ set_clipboard(cmd);
+ }
Ok(String::new())
}
- InputAction::ReturnQuery | InputAction::Accept(_) => {
- // Either:
- // * index == RETURN_QUERY, in which case we should return the input
- // * out of bounds -> usually implies no selected entry so we return the input
- Ok(app.search.input.into_inner())
- }
+ InputAction::ReturnQuery => Ok(app.search.input.into_inner()),
InputAction::Continue | InputAction::Redraw | InputAction::Delete(_) => {
unreachable!("should have been handled!")
}