aboutsummaryrefslogtreecommitdiff
path: root/src/app/input/modal.rs
diff options
context:
space:
mode:
authorcorrius <alvaro.correa@taxdown.es>2026-07-24 21:52:17 +0200
committerGitHub <noreply@github.com>2026-07-24 22:52:17 +0300
commit38d2b0780c50e4773d6948660873386261cde71d (patch)
treeb899dd7ce1dfd1a70650be0209a48132c1539a25 /src/app/input/modal.rs
parente9b22084e03880f43d1f1daaf5d9f45c717b77c4 (diff)
feat: filter keybind help (#1832)
* feat: filter keybind help refs #1825 * fix: recognize shifted slash in keybind help --------- Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/app/input/modal.rs')
-rw-r--r--src/app/input/modal.rs204
1 files changed, 202 insertions, 2 deletions
diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs
index dc126b40..d8a14602 100644
--- a/src/app/input/modal.rs
+++ b/src/app/input/modal.rs
@@ -101,6 +101,8 @@ pub(super) fn open_global_menu(state: &mut AppState) {
pub(super) fn open_keybind_help(state: &mut AppState) {
state.keybind_help.scroll = 0;
+ state.keybind_help.query.clear();
+ state.keybind_help.search_focused = false;
state.mode = Mode::KeybindHelp;
}
@@ -284,7 +286,56 @@ pub(crate) fn insert_navigator_search_text(
state.select_first_navigator_match_from(terminal_runtimes);
}
-pub(crate) fn handle_keybind_help_key(state: &mut AppState, key: KeyEvent) {
+pub(crate) fn insert_keybind_help_query_text(state: &mut AppState, text: &str) {
+ if !state.keybind_help.search_focused {
+ return;
+ }
+ state
+ .keybind_help
+ .query
+ .extend(text.chars().filter(|ch| !ch.is_control()));
+ state.keybind_help.scroll = 0;
+}
+
+pub(super) fn keybind_help_back(state: &mut AppState) {
+ if state.keybind_help.search_focused {
+ state.keybind_help.query.clear();
+ state.keybind_help.search_focused = false;
+ state.keybind_help.scroll = 0;
+ } else {
+ leave_modal(state);
+ }
+}
+
+pub(crate) fn handle_keybind_help_key(state: &mut AppState, key: TerminalKey) {
+ if state.keybind_help.search_focused {
+ let text_char = keybind_help_text_char(key);
+ match key.code {
+ KeyCode::Up => state.scroll_keybind_help(-1),
+ KeyCode::Down => state.scroll_keybind_help(1),
+ KeyCode::PageUp => state.scroll_keybind_help(-8),
+ KeyCode::PageDown => state.scroll_keybind_help(8),
+ KeyCode::Home => state.keybind_help.scroll = 0,
+ KeyCode::End => state.keybind_help.scroll = state.keybind_help_max_scroll(),
+ KeyCode::Backspace => {
+ state.keybind_help.query.pop();
+ state.keybind_help.scroll = 0;
+ }
+ KeyCode::Char('u') if key.modifiers == KeyModifiers::CONTROL => {
+ state.keybind_help.query.clear();
+ state.keybind_help.scroll = 0;
+ }
+ KeyCode::Esc => keybind_help_back(state),
+ KeyCode::Enter => leave_modal(state),
+ _ => {
+ if let Some(character) = text_char {
+ insert_keybind_help_query_text(state, &character.to_string());
+ }
+ }
+ }
+ return;
+ }
+
match key.code {
KeyCode::Up | KeyCode::Char('k') => state.scroll_keybind_help(-1),
KeyCode::Down | KeyCode::Char('j') => state.scroll_keybind_help(1),
@@ -292,11 +343,30 @@ pub(crate) fn handle_keybind_help_key(state: &mut AppState, key: KeyEvent) {
KeyCode::PageDown => state.scroll_keybind_help(8),
KeyCode::Home => state.keybind_help.scroll = 0,
KeyCode::End => state.keybind_help.scroll = state.keybind_help_max_scroll(),
- KeyCode::Esc | KeyCode::Enter | KeyCode::Char('?') => leave_modal(state),
+ _ if keybind_help_text_char(key) == Some('/') => {
+ state.keybind_help.search_focused = true;
+ state.keybind_help.scroll = 0;
+ }
+ KeyCode::Esc => keybind_help_back(state),
+ KeyCode::Enter => leave_modal(state),
+ _ if keybind_help_text_char(key) == Some('?') => leave_modal(state),
_ => {}
}
}
+fn keybind_help_text_char(key: TerminalKey) -> Option<char> {
+ if !key.modifiers.difference(KeyModifiers::SHIFT).is_empty() {
+ return None;
+ }
+ if let Some(character) = key.shifted_codepoint.and_then(char::from_u32) {
+ return Some(character);
+ }
+ let KeyCode::Char(character) = key.code else {
+ return None;
+ };
+ Some(character)
+}
+
pub(super) fn open_rename_workspace(
state: &mut AppState,
terminal_runtimes: &crate::terminal::TerminalRuntimeRegistry,
@@ -1655,6 +1725,136 @@ mod tests {
}
#[test]
+ fn keybind_help_slash_focuses_filter_and_preserves_vim_scroll() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.keybind_help.query = "stale".into();
+ state.keybind_help.search_focused = true;
+ state.view.terminal_area = Rect::new(0, 0, 100, 30);
+
+ open_keybind_help(&mut state);
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('j'), KeyModifiers::empty()),
+ );
+ assert_eq!(state.keybind_help.scroll, 1);
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('k'), KeyModifiers::empty()),
+ );
+ assert_eq!(state.keybind_help.scroll, 0);
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('w'), KeyModifiers::empty()),
+ );
+ assert!(state.keybind_help.query.is_empty());
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('/'), KeyModifiers::empty()),
+ );
+ for character in "work".chars() {
+ state.keybind_help.scroll = 2;
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char(character), KeyModifiers::empty()),
+ );
+ }
+
+ assert!(state.keybind_help.search_focused);
+ assert_eq!(state.keybind_help.query, "work");
+ assert_eq!(state.keybind_help.scroll, 0);
+ }
+
+ #[test]
+ fn keybind_help_query_supports_backspace_clear_and_sanitized_paste() {
+ let mut state = state_with_workspaces(&["test"]);
+ open_keybind_help(&mut state);
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('/'), KeyModifiers::empty()),
+ );
+
+ insert_keybind_help_query_text(&mut state, "work\nspace");
+ assert_eq!(state.keybind_help.query, "workspace");
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Backspace, KeyModifiers::empty()),
+ );
+ assert_eq!(state.keybind_help.query, "workspac");
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
+ );
+ assert!(state.keybind_help.query.is_empty());
+ }
+
+ #[test]
+ fn keybind_help_escape_leaves_search_before_closing() {
+ let mut state = state_with_workspaces(&["test"]);
+ open_keybind_help(&mut state);
+ state.keybind_help.search_focused = true;
+ state.keybind_help.query = "work".into();
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Esc, KeyModifiers::empty()),
+ );
+ assert_eq!(state.mode, Mode::KeybindHelp);
+ assert!(!state.keybind_help.search_focused);
+ assert!(state.keybind_help.query.is_empty());
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Esc, KeyModifiers::empty()),
+ );
+ assert_eq!(state.mode, Mode::Terminal);
+ }
+
+ #[test]
+ fn enhanced_shifted_slash_focuses_keybind_help_filter() {
+ let mut state = state_with_workspaces(&["test"]);
+ open_keybind_help(&mut state);
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('7'), KeyModifiers::SHIFT)
+ .with_shifted_codepoint('/' as u32),
+ );
+
+ assert!(state.keybind_help.search_focused);
+ }
+
+ #[test]
+ fn enhanced_shifted_question_mark_closes_keybind_help_when_not_searching() {
+ let mut state = state_with_workspaces(&["test"]);
+ open_keybind_help(&mut state);
+
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('/'), KeyModifiers::SHIFT)
+ .with_shifted_codepoint('?' as u32),
+ );
+
+ assert_eq!(state.mode, Mode::Terminal);
+
+ open_keybind_help(&mut state);
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('/'), KeyModifiers::empty()),
+ );
+ handle_keybind_help_key(
+ &mut state,
+ TerminalKey::new(KeyCode::Char('/'), KeyModifiers::SHIFT)
+ .with_shifted_codepoint('?' as u32),
+ );
+
+ assert_eq!(state.keybind_help.query, "?");
+ }
+
+ #[test]
fn navigator_search_accepts_pasted_text_when_focused() {
let mut state = state_with_workspaces(&["alpha", "beta"]);
let terminal_runtimes = crate::terminal::TerminalRuntimeRegistry::new();