From f68c3de3b7bd15a5f16ca09352277d297574787e Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Sat, 4 Apr 2026 04:00:37 +0100 Subject: feat: add support for deleting all matching commands via keybindings (#3375) --- crates/atuin-client/src/history/store.rs | 14 ++++++++ crates/atuin/src/command/client/search.rs | 10 ++---- .../atuin/src/command/client/search/interactive.rs | 37 ++++++++++++++++++---- .../command/client/search/keybindings/actions.rs | 3 ++ .../command/client/search/keybindings/defaults.rs | 1 + 5 files changed, 52 insertions(+), 13 deletions(-) (limited to 'crates') diff --git a/crates/atuin-client/src/history/store.rs b/crates/atuin-client/src/history/store.rs index d166564f..ce7b43a1 100644 --- a/crates/atuin-client/src/history/store.rs +++ b/crates/atuin-client/src/history/store.rs @@ -180,6 +180,20 @@ impl HistoryStore { self.push_record(record).await } + /// Delete a batch of history entries via the record store. + /// Returns the record IDs so the caller can run incremental_build when ready. + pub async fn delete_entries( + &self, + entries: impl IntoIterator, + ) -> Result> { + let mut record_ids = Vec::new(); + for entry in entries { + let (id, _) = self.delete(entry.id).await?; + record_ids.push(id); + } + Ok(record_ids) + } + pub async fn push(&self, history: History) -> Result<(RecordId, RecordIdx)> { // TODO(ellie): move the history store to its own file // it's tiny rn so fine as is diff --git a/crates/atuin/src/command/client/search.rs b/crates/atuin/src/command/client/search.rs index 7c72e13d..3d348473 100644 --- a/crates/atuin/src/command/client/search.rs +++ b/crates/atuin/src/command/client/search.rs @@ -265,15 +265,11 @@ impl Cmd { while !entries.is_empty() { for entry in &entries { eprintln!("deleting {}", entry.id); - - if settings.sync.records { - let (id, _) = history_store.delete(entry.id.clone()).await?; - history_store.incremental_build(&db, &[id]).await?; - } else { - db.delete(entry.clone()).await?; - } } + let ids = history_store.delete_entries(entries).await?; + history_store.incremental_build(&db, &ids).await?; + entries = run_non_interactive(settings, opt_filter.clone(), &query, &db).await?; } diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs index 8e5f8551..ee38ddaa 100644 --- a/crates/atuin/src/command/client/search/interactive.rs +++ b/crates/atuin/src/command/client/search/interactive.rs @@ -62,6 +62,7 @@ pub enum InputAction { AcceptInspecting, Copy(usize), Delete(usize), + DeleteAllMatching(usize), ReturnOriginal, ReturnQuery, Continue, @@ -643,6 +644,7 @@ impl State { } Action::Copy => InputAction::Copy(self.results_state.selected()), Action::Delete => InputAction::Delete(self.results_state.selected()), + Action::DeleteAll => InputAction::DeleteAllMatching(self.results_state.selected()), Action::ReturnOriginal => InputAction::ReturnOriginal, Action::ReturnQuery => InputAction::ReturnQuery, Action::Exit => Self::handle_key_exit(settings), @@ -1836,15 +1838,37 @@ pub async fn history( let entry = results.remove(index); - if settings.sync.records { - let (id, _) = history_store.delete(entry.id).await?; - history_store.incremental_build(&db, &[id]).await?; - } else { - db.delete(entry.clone()).await?; - } + let ids = history_store.delete_entries([entry]).await?; + history_store.incremental_build(&db, &ids).await?; app.tab_index = 0; }, + InputAction::DeleteAllMatching(index) => { + if results.is_empty() { + break; + } + + let command = results[index].command.clone(); + + // Remove matching entries from the visible results + results.retain(|e| e.command != command); + + // Query the DB for ALL entries with this command and delete them + let all_matching = db.query_history( + &format!( + "select * from history where command = '{}' and deleted_at is null", + command.replace('\'', "''") + ) + ).await?; + + let ids = history_store.delete_entries(all_matching).await?; + history_store.incremental_build(&db, &ids).await?; + + app.results_len = results.len(); + app.results_state = ListState::default(); + app.inspecting_state.reset(); + app.tab_index = 0; + }, InputAction::SwitchContext(index) => { if let Some(index) = index && let Some(entry) = results.get(index) { app.search.custom_context = Some(entry.id.clone()); @@ -2006,6 +2030,7 @@ pub async fn history( InputAction::Continue | InputAction::Redraw | InputAction::Delete(_) + | InputAction::DeleteAllMatching(_) | InputAction::SwitchContext(_) => { unreachable!("should have been handled!") } diff --git a/crates/atuin/src/command/client/search/keybindings/actions.rs b/crates/atuin/src/command/client/search/keybindings/actions.rs index 66e2709e..ff2ef7de 100644 --- a/crates/atuin/src/command/client/search/keybindings/actions.rs +++ b/crates/atuin/src/command/client/search/keybindings/actions.rs @@ -46,6 +46,7 @@ pub enum Action { // Commands — other Copy, Delete, + DeleteAll, ReturnOriginal, ReturnQuery, Exit, @@ -125,6 +126,7 @@ impl Action { "return-selection" => Ok(Action::ReturnSelection), "copy" => Ok(Action::Copy), "delete" => Ok(Action::Delete), + "delete-all" => Ok(Action::DeleteAll), "return-original" => Ok(Action::ReturnOriginal), "return-query" => Ok(Action::ReturnQuery), "exit" => Ok(Action::Exit), @@ -191,6 +193,7 @@ impl Action { Action::ReturnSelectionNth(n) => format!("return-selection-{n}"), Action::Copy => "copy".to_string(), Action::Delete => "delete".to_string(), + Action::DeleteAll => "delete-all".to_string(), Action::ReturnOriginal => "return-original".to_string(), Action::ReturnQuery => "return-query".to_string(), Action::Exit => "exit".to_string(), diff --git a/crates/atuin/src/command/client/search/keybindings/defaults.rs b/crates/atuin/src/command/client/search/keybindings/defaults.rs index f19bf377..e9b3972c 100644 --- a/crates/atuin/src/command/client/search/keybindings/defaults.rs +++ b/crates/atuin/src/command/client/search/keybindings/defaults.rs @@ -404,6 +404,7 @@ pub fn default_prefix_keymap() -> Keymap { let mut km = Keymap::new(); km.bind(key("d"), Action::Delete); + km.bind(key("D"), Action::DeleteAll); km.bind(key("a"), Action::CursorStart); km.bind_conditional( key("c"), -- cgit v1.3.1