aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-02-11 12:33:41 -0800
committerGitHub <noreply@github.com>2026-02-11 12:33:41 -0800
commit9768d60399f344c7ceb0c2ecec6cfd2a0e191176 (patch)
treee8fd29586a6ba5e5bc8679871a87d49180a85cf0 /crates
parentfix(shell): fix ATUIN_SESSION errors in tmux popup (#3170) (diff)
downloadatuin-9768d60399f344c7ceb0c2ecec6cfd2a0e191176.zip
feat: Add original-input-empty keybind condition (#3171)
This PR adds a keybind condition called `original-input-empty` that is true when the TUI was invoked from an empty prompt line; users can use this to change behavior based on the original prompt's contents; for example: ```toml [keymap.emacs] "esc" = [ { when = "original-input-empty", action = "return-query" }, { action = "return-original" } ] ``` Thanks Hazilo in Discord for the suggestion.
Diffstat (limited to 'crates')
-rw-r--r--crates/atuin/src/command/client/search/interactive.rs68
-rw-r--r--crates/atuin/src/command/client/search/keybindings/conditions.rs35
-rw-r--r--crates/atuin/src/command/client/search/keybindings/defaults.rs55
-rw-r--r--crates/atuin/src/command/client/search/keybindings/keymap.rs1
4 files changed, 159 insertions, 0 deletions
diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs
index 7cddb163..b5186706 100644
--- a/crates/atuin/src/command/client/search/interactive.rs
+++ b/crates/atuin/src/command/client/search/interactive.rs
@@ -105,6 +105,7 @@ pub fn to_compactness(f: &Frame, settings: &Settings) -> Compactness {
}
#[allow(clippy::struct_field_names)]
+#[allow(clippy::struct_excessive_bools)]
pub struct State {
history_count: i64,
update_needed: Option<Version>,
@@ -118,6 +119,7 @@ pub struct State {
current_cursor: Option<CursorStyle>,
tab_index: usize,
pending_vim_key: Option<char>,
+ original_input_empty: bool,
pub inspecting_state: InspectingState,
@@ -301,6 +303,7 @@ impl State {
input_byte_len: self.search.input.as_str().len(),
selected_index: self.results_state.selected(),
results_len: self.results_len,
+ original_input_empty: self.original_input_empty,
};
// Convert KeyEvent to SingleKey
@@ -1416,6 +1419,7 @@ pub async fn history(
},
prefix: false,
pending_vim_key: None,
+ original_input_empty: original_query.is_empty(),
};
app.initialize_keymap_cursor(settings);
@@ -1806,6 +1810,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -1859,6 +1864,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -1976,6 +1982,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -2033,6 +2040,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -2086,6 +2094,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -2135,6 +2144,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -2193,6 +2203,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -2252,6 +2263,7 @@ mod tests {
current_cursor: None,
tab_index: 0,
pending_vim_key: None,
+ original_input_empty: false,
inspecting_state: InspectingState {
current: None,
next: None,
@@ -2582,4 +2594,60 @@ mod tests {
state.execute_action(&Action::ClearLine, &settings);
assert_eq!(state.search.input.as_str(), "");
}
+
+ #[test]
+ fn keymap_config_return_query() {
+ use atuin_client::settings::KeyBindingConfig;
+ use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
+ use std::collections::HashMap;
+
+ let mut settings = Settings::utc();
+ // Configure tab to return-query
+ settings.keymap.emacs = HashMap::from([(
+ "tab".to_string(),
+ KeyBindingConfig::Simple("return-query".to_string()),
+ )]);
+
+ let mut state = State {
+ history_count: 100,
+ update_needed: None,
+ results_state: ListState::default(),
+ switched_search_mode: false,
+ search_mode: SearchMode::Fuzzy,
+ results_len: 100,
+ accept: false,
+ keymap_mode: KeymapMode::Emacs,
+ prefix: false,
+ current_cursor: None,
+ tab_index: 0,
+ pending_vim_key: None,
+ original_input_empty: false,
+ inspecting_state: InspectingState {
+ current: None,
+ next: None,
+ previous: None,
+ },
+ keymaps: KeymapSet::from_settings(&settings),
+ search: SearchState {
+ input: "test query".to_string().into(),
+ filter_mode: FilterMode::Global,
+ context: Context {
+ session: String::new(),
+ cwd: String::new(),
+ hostname: String::new(),
+ host_id: String::new(),
+ git_root: None,
+ },
+ },
+ engine: engines::engine(SearchMode::Fuzzy),
+ now: Box::new(OffsetDateTime::now_utc),
+ };
+
+ let tab_event = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
+ let result = state.handle_key_input(&settings, &tab_event);
+ assert!(
+ matches!(result, super::InputAction::ReturnQuery),
+ "Tab configured as return-query should return InputAction::ReturnQuery"
+ );
+ }
}
diff --git a/crates/atuin/src/command/client/search/keybindings/conditions.rs b/crates/atuin/src/command/client/search/keybindings/conditions.rs
index ed414300..bc485713 100644
--- a/crates/atuin/src/command/client/search/keybindings/conditions.rs
+++ b/crates/atuin/src/command/client/search/keybindings/conditions.rs
@@ -8,6 +8,7 @@ pub enum ConditionAtom {
CursorAtStart,
CursorAtEnd,
InputEmpty,
+ OriginalInputEmpty,
ListAtEnd,
ListAtStart,
NoResults,
@@ -46,6 +47,8 @@ pub struct EvalContext {
pub selected_index: usize,
/// Total number of results.
pub results_len: usize,
+ /// Whether the original input (query passed to the TUI) was empty.
+ pub original_input_empty: bool,
}
// ---------------------------------------------------------------------------
@@ -59,6 +62,7 @@ impl ConditionAtom {
ConditionAtom::CursorAtStart => ctx.cursor_position == 0,
ConditionAtom::CursorAtEnd => ctx.cursor_position == ctx.input_width,
ConditionAtom::InputEmpty => ctx.input_byte_len == 0,
+ ConditionAtom::OriginalInputEmpty => ctx.original_input_empty,
ConditionAtom::ListAtEnd => {
ctx.results_len == 0 || ctx.selected_index >= ctx.results_len.saturating_sub(1)
}
@@ -74,6 +78,7 @@ impl ConditionAtom {
"cursor-at-start" => Ok(ConditionAtom::CursorAtStart),
"cursor-at-end" => Ok(ConditionAtom::CursorAtEnd),
"input-empty" => Ok(ConditionAtom::InputEmpty),
+ "original-input-empty" => Ok(ConditionAtom::OriginalInputEmpty),
"list-at-end" => Ok(ConditionAtom::ListAtEnd),
"list-at-start" => Ok(ConditionAtom::ListAtStart),
"no-results" => Ok(ConditionAtom::NoResults),
@@ -88,6 +93,7 @@ impl ConditionAtom {
ConditionAtom::CursorAtStart => "cursor-at-start",
ConditionAtom::CursorAtEnd => "cursor-at-end",
ConditionAtom::InputEmpty => "input-empty",
+ ConditionAtom::OriginalInputEmpty => "original-input-empty",
ConditionAtom::ListAtEnd => "list-at-end",
ConditionAtom::ListAtStart => "list-at-start",
ConditionAtom::NoResults => "no-results",
@@ -370,12 +376,24 @@ mod tests {
selected: usize,
len: usize,
) -> EvalContext {
+ ctx_with_original(cursor, width, byte_len, selected, len, false)
+ }
+
+ fn ctx_with_original(
+ cursor: usize,
+ width: usize,
+ byte_len: usize,
+ selected: usize,
+ len: usize,
+ original_input_empty: bool,
+ ) -> EvalContext {
EvalContext {
cursor_position: cursor,
input_width: width,
input_byte_len: byte_len,
selected_index: selected,
results_len: len,
+ original_input_empty,
}
}
@@ -401,6 +419,22 @@ mod tests {
}
#[test]
+ fn atom_original_input_empty() {
+ // original_input_empty = true
+ assert!(
+ ConditionAtom::OriginalInputEmpty.evaluate(&ctx_with_original(0, 0, 0, 0, 10, true))
+ );
+ // original_input_empty = false
+ assert!(
+ !ConditionAtom::OriginalInputEmpty.evaluate(&ctx_with_original(0, 0, 0, 0, 10, false))
+ );
+ // original_input_empty is independent of current input state
+ assert!(
+ ConditionAtom::OriginalInputEmpty.evaluate(&ctx_with_original(0, 5, 5, 0, 10, true))
+ );
+ }
+
+ #[test]
fn atom_list_at_end() {
assert!(ConditionAtom::ListAtEnd.evaluate(&ctx(0, 0, 0, 99, 100)));
assert!(!ConditionAtom::ListAtEnd.evaluate(&ctx(0, 0, 0, 50, 100)));
@@ -428,6 +462,7 @@ mod tests {
"cursor-at-start",
"cursor-at-end",
"input-empty",
+ "original-input-empty",
"list-at-end",
"list-at-start",
"no-results",
diff --git a/crates/atuin/src/command/client/search/keybindings/defaults.rs b/crates/atuin/src/command/client/search/keybindings/defaults.rs
index 121c59fe..64dca691 100644
--- a/crates/atuin/src/command/client/search/keybindings/defaults.rs
+++ b/crates/atuin/src/command/client/search/keybindings/defaults.rs
@@ -529,6 +529,7 @@ mod tests {
input_byte_len: width,
selected_index: selected,
results_len: len,
+ original_input_empty: false,
}
}
@@ -1217,4 +1218,58 @@ mod tests {
modified.prefix = "x".to_string();
assert!(modified.has_non_default_values());
}
+
+ #[test]
+ fn original_input_empty_condition_in_config() {
+ use atuin_client::settings::{KeyBindingConfig, KeyRuleConfig};
+ use std::collections::HashMap;
+
+ let mut settings = default_settings();
+ // Configure esc to: if original-input-empty -> return-query, else return-original
+ settings.keymap.emacs = HashMap::from([(
+ "esc".to_string(),
+ KeyBindingConfig::Rules(vec![
+ KeyRuleConfig {
+ when: Some("original-input-empty".to_string()),
+ action: "return-query".to_string(),
+ },
+ KeyRuleConfig {
+ when: None,
+ action: "return-original".to_string(),
+ },
+ ]),
+ )]);
+
+ let set = KeymapSet::from_settings(&settings);
+
+ // When original input was empty, should return-query
+ let ctx_original_empty = EvalContext {
+ cursor_position: 0,
+ input_width: 5,
+ input_byte_len: 5,
+ selected_index: 0,
+ results_len: 10,
+ original_input_empty: true,
+ };
+ assert_eq!(
+ set.emacs.resolve(&key("esc"), &ctx_original_empty),
+ Some(Action::ReturnQuery),
+ "esc with original_input_empty=true should return-query"
+ );
+
+ // When original input was not empty, should return-original
+ let ctx_original_not_empty = EvalContext {
+ cursor_position: 0,
+ input_width: 5,
+ input_byte_len: 5,
+ selected_index: 0,
+ results_len: 10,
+ original_input_empty: false,
+ };
+ assert_eq!(
+ set.emacs.resolve(&key("esc"), &ctx_original_not_empty),
+ Some(Action::ReturnOriginal),
+ "esc with original_input_empty=false should return-original"
+ );
+ }
}
diff --git a/crates/atuin/src/command/client/search/keybindings/keymap.rs b/crates/atuin/src/command/client/search/keybindings/keymap.rs
index 4d91e180..bbf034b2 100644
--- a/crates/atuin/src/command/client/search/keybindings/keymap.rs
+++ b/crates/atuin/src/command/client/search/keybindings/keymap.rs
@@ -126,6 +126,7 @@ mod tests {
input_byte_len: width,
selected_index: selected,
results_len: len,
+ original_input_empty: false,
}
}