diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2025-04-28 15:25:37 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-28 15:25:37 +0100 |
| commit | cd5d337b52ad16a834cf8909b48598366e9a6efa (patch) | |
| tree | b4c1fb66ac8106dde6a3bdb4532de05e602df368 | |
| parent | feat: sort `atuin store status` output (#2719) (diff) | |
| download | atuin-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.rs | 96 |
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!") } |
