diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2026-04-04 04:00:37 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-04 04:00:37 +0100 |
| commit | f68c3de3b7bd15a5f16ca09352277d297574787e (patch) | |
| tree | 32cf481b6af1b1ff035a7b7fde89f93c2a8bab9d | |
| parent | feat: option to disable mouse support (#3372) (diff) | |
| download | atuin-f68c3de3b7bd15a5f16ca09352277d297574787e.zip | |
feat: add support for deleting all matching commands via keybindings (#3375)
| -rw-r--r-- | crates/atuin-client/src/history/store.rs | 14 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/search.rs | 10 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/search/interactive.rs | 37 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/search/keybindings/actions.rs | 3 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/search/keybindings/defaults.rs | 1 | ||||
| -rw-r--r-- | docs/docs/configuration/advanced-key-binding.md | 1 | ||||
| -rw-r--r-- | docs/docs/configuration/key-binding.md | 2 |
7 files changed, 55 insertions, 13 deletions
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<Item = History>, + ) -> Result<Vec<RecordId>> { + 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"), diff --git a/docs/docs/configuration/advanced-key-binding.md b/docs/docs/configuration/advanced-key-binding.md index 7f73b5d6..21cf35c6 100644 --- a/docs/docs/configuration/advanced-key-binding.md +++ b/docs/docs/configuration/advanced-key-binding.md @@ -201,6 +201,7 @@ Note: `select-next` and `select-previous` respect the `invert` setting. When `in | `return-query` | Close the TUI and return the current search query | | `copy` | Copy the selected entry to the clipboard | | `delete` | Delete the selected entry from history | +| `delete-all` | Delete **all** history entries matching the selected command text | | `exit` | Exit the TUI (behavior depends on the `exit_mode` setting) | | `redraw` | Redraw the screen | | `cycle-filter-mode` | Cycle through filter modes (global, host, session, directory) | diff --git a/docs/docs/configuration/key-binding.md b/docs/docs/configuration/key-binding.md index 942a2400..47f24cd4 100644 --- a/docs/docs/configuration/key-binding.md +++ b/docs/docs/configuration/key-binding.md @@ -227,6 +227,8 @@ $env.config = ( | page up | Scroll search results one page up | | ↓ (with no entry selected) | Return original or return query depending on [settings](config.md#exit_mode) | | ↓ | Select the next item on the list | +| ctrl + a, d | Delete the selected history entry | +| ctrl + a, D | Delete **all** history entries matching the selected command | | ctrl + a, c | Switch to the context of the currently selected command / return to default | |
