diff options
Diffstat (limited to 'crates')
7 files changed, 132 insertions, 10 deletions
diff --git a/crates/atuin-client/src/database.rs b/crates/atuin-client/src/database.rs index 28d6c0f0..7aa095f7 100644 --- a/crates/atuin-client/src/database.rs +++ b/crates/atuin-client/src/database.rs @@ -32,6 +32,7 @@ use super::{ settings::{FilterMode, SearchMode, Settings}, }; +#[derive(Clone)] pub struct Context { pub session: String, pub cwd: String, @@ -72,6 +73,18 @@ pub async fn current_context() -> eyre::Result<Context> { }) } +impl Context { + pub fn from_history(entry: &History) -> Self { + Context { + session: entry.session.to_string(), + cwd: entry.cwd.to_string(), + hostname: entry.hostname.to_string(), + host_id: String::new(), + git_root: utils::in_git_repo(entry.cwd.as_str()), + } + } +} + fn get_session_start_time(session_id: &str) -> Option<i64> { if let Ok(uuid) = Uuid::parse_str(session_id) && let Some(timestamp) = uuid.get_timestamp() diff --git a/crates/atuin/src/command/client/search/engines.rs b/crates/atuin/src/command/client/search/engines.rs index 95d6658b..5c53817e 100644 --- a/crates/atuin/src/command/client/search/engines.rs +++ b/crates/atuin/src/command/client/search/engines.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use atuin_client::{ database::{Context, Database}, - history::History, + history::{History, HistoryId}, settings::{FilterMode, SearchMode, Settings}, }; use eyre::Result; @@ -22,6 +22,7 @@ pub struct SearchState { pub input: Cursor, pub filter_mode: FilterMode, pub context: Context, + pub custom_context: Option<HistoryId>, } impl SearchState { @@ -44,6 +45,7 @@ impl SearchState { fn filter_mode_available(&self, mode: FilterMode, settings: &Settings) -> bool { match mode { + FilterMode::Global | FilterMode::SessionPreload => self.custom_context.is_none(), FilterMode::Workspace => settings.workspaces && self.context.git_root.is_some(), _ => true, } diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs index b5186706..c6a6064a 100644 --- a/crates/atuin/src/command/client/search/interactive.rs +++ b/crates/atuin/src/command/client/search/interactive.rs @@ -16,10 +16,11 @@ use super::{ history_list::{HistoryList, ListState}, }; use atuin_client::{ - database::{Database, current_context}, + database::{Context, Database, current_context}, history::{History, HistoryId, HistoryStats, store::HistoryStore}, settings::{ - CursorStyle, ExitMode, KeymapMode, PreviewStrategy, SearchMode, Settings, UiColumn, + CursorStyle, ExitMode, FilterMode, KeymapMode, PreviewStrategy, SearchMode, Settings, + UiColumn, }, }; @@ -59,6 +60,7 @@ pub enum InputAction { ReturnQuery, Continue, Redraw, + SwitchContext(Option<usize>), } #[derive(Clone)] @@ -304,6 +306,7 @@ impl State { selected_index: self.results_state.selected(), results_len: self.results_len, original_input_empty: self.original_input_empty, + has_context: self.search.custom_context.is_some(), }; // Convert KeyEvent to SingleKey @@ -657,6 +660,10 @@ impl State { self.engine = engines::engine(self.search_mode); InputAction::Continue } + Action::SwitchContext => { + InputAction::SwitchContext(Some(self.results_state.selected())) + } + Action::ClearContext => InputAction::SwitchContext(None), Action::ToggleTab => { self.tab_index = (self.tab_index + 1) % TAB_TITLES.len(); InputAction::Continue @@ -916,6 +923,11 @@ impl State { Compactness::Ultracompact => { if self.switched_search_mode { format!("S{}>", self.search_mode.as_str().chars().next().unwrap()) + } else if self.search.custom_context.is_some() { + format!( + "C{}>", + self.search.filter_mode.as_str().chars().next().unwrap() + ) } else { format!( "{}> ", @@ -1166,6 +1178,8 @@ impl State { fn build_input(&self, style: StyleState, prefix_width: u16) -> Paragraph<'_> { let (pref, mode) = if self.switched_search_mode { (" SRCH:", self.search_mode.as_str()) + } else if self.search.custom_context.is_some() { + (" CTX:", self.search.filter_mode.as_str()) } else { ("", self.search.filter_mode.as_str()) }; @@ -1372,7 +1386,7 @@ pub async fn history( let update_needed = tokio::spawn(async move { settings2.needs_update().await }).fuse(); tokio::pin!(update_needed); - let context = current_context().await?; + let initial_context = current_context().await?; let history_count = db.history_count(false).await?; let search_mode = if settings.shell_up_key_binding { @@ -1382,6 +1396,10 @@ pub async fn history( } else { settings.search_mode }; + let default_filter_mode = settings + .filter_mode_shell_up_key_binding + .filter(|_| settings.shell_up_key_binding) + .unwrap_or_else(|| settings.default_filter_mode(initial_context.git_root.is_some())); let mut app = State { history_count, results_state: ListState::default(), @@ -1397,11 +1415,9 @@ pub async fn history( keymaps: KeymapSet::from_settings(settings), search: SearchState { input, - filter_mode: settings - .filter_mode_shell_up_key_binding - .filter(|_| settings.shell_up_key_binding) - .unwrap_or_else(|| settings.default_filter_mode(context.git_root.is_some())), - context, + filter_mode: default_filter_mode, + context: initial_context.clone(), + custom_context: None, }, engine: engines::engine(search_mode), results_len: 0, @@ -1448,6 +1464,7 @@ pub async fn history( let initial_input = app.search.input.as_str().to_owned(); let initial_filter_mode = app.search.filter_mode; let initial_search_mode = app.search_mode; + let initial_custom_context = app.search.custom_context.clone(); let event_ready = tokio::task::spawn_blocking(|| event::poll(Duration::from_millis(250))); @@ -1479,6 +1496,19 @@ pub async fn history( 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()); + app.search.context = Context::from_history(entry); + app.search.filter_mode = FilterMode::Session; + app.search.input = Cursor::from(String::new()); + app.results_state = ListState::default(); + } else { + app.search.custom_context = None; + app.search.context = initial_context.clone(); + app.search.filter_mode = default_filter_mode; + } + }, InputAction::Redraw => { terminal.clear()?; terminal.draw(|f| app.draw(f, &results, stats.clone(), inspecting.as_ref(), settings, theme))?; @@ -1504,10 +1534,23 @@ pub async fn history( if initial_input != app.search.input.as_str() || initial_filter_mode != app.search.filter_mode || initial_search_mode != app.search_mode + || initial_custom_context != app.search.custom_context { results = app.query_results(&mut db, settings.smart_sort).await?; } + // In custom context mode, when no filter is applied, highlight the entry which was used + // to enter the context when changing modes. This helps to find your way around. + if app.search.custom_context.is_some() + && app.search.input.as_str().is_empty() + && (initial_custom_context != app.search.custom_context + || initial_filter_mode != app.search.filter_mode) + && let Some(history_id) = app.search.custom_context.clone() + && let Some(pos) = results.iter().position(|entry| entry.id == history_id) + { + app.results_state.select(pos); + } + let inspecting_id = app.inspecting_state.clone().current; // If inspecting ID is not the current inspecting History, update it. match inspecting_id { @@ -1600,7 +1643,10 @@ pub async fn history( // * out of bounds -> usually implies no selected entry so we return the input Ok(app.search.input.into_inner()) } - InputAction::Continue | InputAction::Redraw | InputAction::Delete(_) => { + InputAction::Continue + | InputAction::Redraw + | InputAction::Delete(_) + | InputAction::SwitchContext(_) => { unreachable!("should have been handled!") } } @@ -1827,6 +1873,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -1881,6 +1928,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -1999,6 +2047,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -2057,6 +2106,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -2111,6 +2161,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -2161,6 +2212,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -2220,6 +2272,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -2280,6 +2333,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), @@ -2490,6 +2544,26 @@ mod tests { } #[test] + fn execute_switch_context() { + use crate::command::client::search::keybindings::Action; + + let mut state = make_executor_state(100, 7); + let settings = Settings::utc(); + let result = state.execute_action(&Action::SwitchContext, &settings); + assert!(matches!(result, super::InputAction::SwitchContext(Some(7)))); + } + + #[test] + fn execute_clear_context() { + use crate::command::client::search::keybindings::Action; + + let mut state = make_executor_state(100, 7); + let settings = Settings::utc(); + let result = state.execute_action(&Action::ClearContext, &settings); + assert!(matches!(result, super::InputAction::SwitchContext(None))); + } + + #[test] fn execute_noop() { use crate::command::client::search::keybindings::Action; @@ -2638,6 +2712,7 @@ mod tests { host_id: String::new(), git_root: None, }, + custom_context: None, }, engine: engines::engine(SearchMode::Fuzzy), now: Box::new(OffsetDateTime::now_utc), diff --git a/crates/atuin/src/command/client/search/keybindings/actions.rs b/crates/atuin/src/command/client/search/keybindings/actions.rs index fae811d6..66e2709e 100644 --- a/crates/atuin/src/command/client/search/keybindings/actions.rs +++ b/crates/atuin/src/command/client/search/keybindings/actions.rs @@ -52,6 +52,8 @@ pub enum Action { Redraw, CycleFilterMode, CycleSearchMode, + SwitchContext, + ClearContext, ToggleTab, // Mode changes @@ -129,6 +131,8 @@ impl Action { "redraw" => Ok(Action::Redraw), "cycle-filter-mode" => Ok(Action::CycleFilterMode), "cycle-search-mode" => Ok(Action::CycleSearchMode), + "switch-context" => Ok(Action::SwitchContext), + "clear-context" => Ok(Action::ClearContext), "toggle-tab" => Ok(Action::ToggleTab), "vim-enter-normal" => Ok(Action::VimEnterNormal), @@ -193,6 +197,8 @@ impl Action { Action::Redraw => "redraw".to_string(), Action::CycleFilterMode => "cycle-filter-mode".to_string(), Action::CycleSearchMode => "cycle-search-mode".to_string(), + Action::SwitchContext => "switch-context".to_string(), + Action::ClearContext => "clear-context".to_string(), Action::ToggleTab => "toggle-tab".to_string(), Action::VimEnterNormal => "vim-enter-normal".to_string(), diff --git a/crates/atuin/src/command/client/search/keybindings/conditions.rs b/crates/atuin/src/command/client/search/keybindings/conditions.rs index bc485713..d460d7d4 100644 --- a/crates/atuin/src/command/client/search/keybindings/conditions.rs +++ b/crates/atuin/src/command/client/search/keybindings/conditions.rs @@ -13,6 +13,7 @@ pub enum ConditionAtom { ListAtStart, NoResults, HasResults, + HasContext, } /// Boolean expression tree over condition atoms. @@ -49,6 +50,8 @@ pub struct EvalContext { pub results_len: usize, /// Whether the original input (query passed to the TUI) was empty. pub original_input_empty: bool, + /// Whether we use a search context of a command from the history. + pub has_context: bool, } // --------------------------------------------------------------------------- @@ -69,6 +72,7 @@ impl ConditionAtom { ConditionAtom::ListAtStart => ctx.results_len == 0 || ctx.selected_index == 0, ConditionAtom::NoResults => ctx.results_len == 0, ConditionAtom::HasResults => ctx.results_len > 0, + ConditionAtom::HasContext => ctx.has_context, } } @@ -83,6 +87,7 @@ impl ConditionAtom { "list-at-start" => Ok(ConditionAtom::ListAtStart), "no-results" => Ok(ConditionAtom::NoResults), "has-results" => Ok(ConditionAtom::HasResults), + "has-context" => Ok(ConditionAtom::HasContext), _ => Err(format!("unknown condition: {s}")), } } @@ -98,6 +103,7 @@ impl ConditionAtom { ConditionAtom::ListAtStart => "list-at-start", ConditionAtom::NoResults => "no-results", ConditionAtom::HasResults => "has-results", + ConditionAtom::HasContext => "has-context", } } } @@ -394,6 +400,7 @@ mod tests { selected_index: selected, results_len: len, original_input_empty, + has_context: false, } } @@ -457,6 +464,14 @@ mod tests { } #[test] + fn atom_has_context() { + let mut context = ctx(0, 0, 0, 0, 0); + assert!(!ConditionAtom::HasContext.evaluate(&context)); + context.has_context = true; + assert!(ConditionAtom::HasContext.evaluate(&context)); + } + + #[test] fn atom_parse_round_trip() { let conditions = [ "cursor-at-start", diff --git a/crates/atuin/src/command/client/search/keybindings/defaults.rs b/crates/atuin/src/command/client/search/keybindings/defaults.rs index 64dca691..f19bf377 100644 --- a/crates/atuin/src/command/client/search/keybindings/defaults.rs +++ b/crates/atuin/src/command/client/search/keybindings/defaults.rs @@ -405,6 +405,13 @@ pub fn default_prefix_keymap() -> Keymap { km.bind(key("d"), Action::Delete); km.bind(key("a"), Action::CursorStart); + km.bind_conditional( + key("c"), + vec![ + KeyRule::when(ConditionAtom::HasContext, Action::ClearContext), + KeyRule::always(Action::SwitchContext), + ], + ); km } @@ -530,6 +537,7 @@ mod tests { selected_index: selected, results_len: len, original_input_empty: false, + has_context: false, } } @@ -1250,6 +1258,7 @@ mod tests { selected_index: 0, results_len: 10, original_input_empty: true, + has_context: false, }; assert_eq!( set.emacs.resolve(&key("esc"), &ctx_original_empty), @@ -1265,6 +1274,7 @@ mod tests { selected_index: 0, results_len: 10, original_input_empty: false, + has_context: false, }; assert_eq!( set.emacs.resolve(&key("esc"), &ctx_original_not_empty), diff --git a/crates/atuin/src/command/client/search/keybindings/keymap.rs b/crates/atuin/src/command/client/search/keybindings/keymap.rs index bbf034b2..8c7fcfa8 100644 --- a/crates/atuin/src/command/client/search/keybindings/keymap.rs +++ b/crates/atuin/src/command/client/search/keybindings/keymap.rs @@ -127,6 +127,7 @@ mod tests { selected_index: selected, results_len: len, original_input_empty: false, + has_context: false, } } |
