From ad15becd7a8e6851f4c8011e0586a4fc9fe8638c Mon Sep 17 00:00:00 2001 From: Priec Date: Mon, 8 Sep 2025 12:56:03 +0200 Subject: [PATCH] doing key sequencing via space --- client/src/config/binds/key_sequences.rs | 4 +- client/src/modes/handlers/event.rs | 265 ++++++++++++++++------- 2 files changed, 189 insertions(+), 80 deletions(-) diff --git a/client/src/config/binds/key_sequences.rs b/client/src/config/binds/key_sequences.rs index 4c4377a..c4b7ec4 100644 --- a/client/src/config/binds/key_sequences.rs +++ b/client/src/config/binds/key_sequences.rs @@ -67,6 +67,7 @@ impl KeySequenceTracker { // Helper function to convert any KeyCode to a string representation pub fn key_to_string(key: &KeyCode) -> String { match key { + KeyCode::Char(' ') => "space".to_string(), KeyCode::Char(c) => c.to_string(), KeyCode::Left => "left".to_string(), KeyCode::Right => "right".to_string(), @@ -90,6 +91,7 @@ pub fn key_to_string(key: &KeyCode) -> String { // Helper function to convert a string to a KeyCode pub fn string_to_keycode(s: &str) -> Option { match s.to_lowercase().as_str() { + "space" => Some(KeyCode::Char(' ')), "left" => Some(KeyCode::Left), "right" => Some(KeyCode::Right), "up" => Some(KeyCode::Up), @@ -140,7 +142,7 @@ fn is_compound_key(part: &str) -> bool { matches!(part.to_lowercase().as_str(), "esc" | "up" | "down" | "left" | "right" | "enter" | "backspace" | "delete" | "tab" | "backtab" | "home" | - "end" | "pageup" | "pagedown" | "insert" + "end" | "pageup" | "pagedown" | "insert" | "space" ) } diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index dd5a617..cd440f1 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -106,7 +106,7 @@ impl EventHandler { command_message: String::new(), edit_mode_cooldown: false, ideal_cursor_column: 0, - key_sequence_tracker: KeySequenceTracker::new(400), + key_sequence_tracker: KeySequenceTracker::new(1200), auth_client: AuthClient::new().await?, grpc_client, login_result_sender, @@ -297,12 +297,71 @@ impl EventHandler { let key_code = key_event.code; let modifiers = key_event.modifiers; - // LOGIN: canvas <-> buttons focus handoff - // Do not let Login canvas receive keys when overlays/palettes are active let overlay_active = self.command_mode || app_state.ui.show_search_palette || self.navigation_state.active; + // --- SPACE-LED SEQUENCES INTERCEPTION (all modes except Form Edit) --- + if !overlay_active { + // Detect if we are in a Form and the canvas is in Edit mode (then we don't intercept) + let in_form_edit_mode = matches!( + &router.current, + Page::Form(path) if { + if let Some(editor) = app_state.editor_for_path_ref(path) { + editor.mode() == CanvasMode::Edit + } else { false } + } + ); + + if !in_form_edit_mode { + let space = KeyCode::Char(' '); + let seq_active = !self.key_sequence_tracker.current_sequence.is_empty() + && self.key_sequence_tracker.current_sequence[0] == space; + + if seq_active { + self.key_sequence_tracker.add_key(key_code); + let sequence = self.key_sequence_tracker.get_sequence(); + + if let Some(action) = config.matches_key_sequence_generalized(&sequence) { + self.key_sequence_tracker.reset(); + if let Some(outcome) = self + .dispatch_space_sequence_action( + action, + config, + terminal, + command_handler, + auth_state, + buffer_state, + app_state, + router, + ) + .await? + { + return Ok(outcome); + } else { + return Ok(EventOutcome::Ok(String::new())); + } + } + + if config.is_key_sequence_prefix(&sequence) { + return Ok(EventOutcome::Ok(String::new())); + } + + // Not matched and not prefix → reset and fall through + self.key_sequence_tracker.reset(); + } else if key_code == space && config.is_key_sequence_prefix(&[space]) { + // Start new space-led sequence and do not let the page consume the space + self.key_sequence_tracker.reset(); + self.key_sequence_tracker.add_key(space); + return Ok(EventOutcome::Ok(String::new())); + } + } + } + // --- END SPACE-LED SEQUENCES --- + + // LOGIN: canvas <-> buttons focus handoff + // Do not let Login canvas receive keys when overlays/palettes are active + if !overlay_active { if let Page::Login(login_page) = &mut router.current { let outcome = @@ -322,15 +381,20 @@ impl EventHandler { return Ok(outcome); } } else if let Page::Form(path) = &router.current { - let outcome = forms::event::handle_form_event( - event, - app_state, - path, - &mut self.ideal_cursor_column, - )?; - // Only return if the form page actually consumed the key - if !outcome.get_message_if_ok().is_empty() { - return Ok(outcome); + // If a space-led sequence is in progress or has just begun, do not forward to editor + if self.key_sequence_tracker.current_sequence.is_empty() { + let outcome = forms::event::handle_form_event( + event, + app_state, + path, + &mut self.ideal_cursor_column, + )?; + if !outcome.get_message_if_ok().is_empty() { + return Ok(outcome); + } + } else { + // Sequence is active; we already handled or are waiting for more keys + return Ok(EventOutcome::Ok(String::new())); } } else if let Page::AddLogic(add_logic_page) = &mut router.current { // Allow ":" (enter_command_mode) even when inside AddLogic canvas @@ -742,74 +806,9 @@ impl EventHandler { return Ok(EventOutcome::Ok(String::new())); } + // Default command-mode handling: we do not treat Space as a leader here. + // Space-led sequences are handled only in General mode above. if let KeyCode::Char(c) = key_code { - if c == 'f' { - self.key_sequence_tracker.add_key(key_code); - let sequence = - self.key_sequence_tracker.get_sequence(); - - if config.matches_key_sequence_generalized( - &sequence, - ) == Some("find_file_palette_toggle") - { - if matches!(&router.current, Page::Form(_) | Page::Intro(_)) { - let mut all_table_paths: Vec = - app_state - .profile_tree - .profiles - .iter() - .flat_map(|profile| { - profile.tables.iter().map( - move |table| { - format!( - "{}/{}", - profile.name, - table.name - ) - }, - ) - }) - .collect(); - all_table_paths.sort(); - - self.navigation_state - .activate_find_file(all_table_paths); - - self.command_mode = false; - self.command_input.clear(); - self.command_message.clear(); - self.key_sequence_tracker.reset(); - return Ok(EventOutcome::Ok( - "Table selection palette activated" - .to_string(), - )); - } else { - self.key_sequence_tracker.reset(); - self.command_input.push('f'); - if sequence.len() > 1 - && sequence[0] == KeyCode::Char('f') - { - self.command_input.push('f'); - } - self.command_message = "Find File not available in this view." - .to_string(); - return Ok(EventOutcome::Ok( - self.command_message.clone(), - )); - } - } - - if config.is_key_sequence_prefix(&sequence) { - return Ok(EventOutcome::Ok(String::new())); - } - } - - if c != 'f' - && !self.key_sequence_tracker.current_sequence.is_empty() - { - self.key_sequence_tracker.reset(); - } - self.command_input.push(c); return Ok(EventOutcome::Ok(String::new())); } @@ -1004,3 +1003,111 @@ impl EventHandler { } } } + +impl EventHandler { + #[allow(clippy::too_many_arguments)] + async fn dispatch_space_sequence_action( + &mut self, + action: &str, + config: &Config, + terminal: &mut TerminalCore, + command_handler: &mut CommandHandler, + auth_state: &mut AuthState, + buffer_state: &mut BufferState, + app_state: &mut AppState, + router: &mut Router, + ) -> Result> { + match action { + // Reuse existing behavior + "next_buffer" => { + if switch_buffer(buffer_state, true) { + return Ok(Some(EventOutcome::Ok( + "Switched to next buffer".to_string(), + ))); + } + Ok(Some(EventOutcome::Ok(String::new()))) + } + "previous_buffer" => { + if switch_buffer(buffer_state, false) { + return Ok(Some(EventOutcome::Ok( + "Switched to previous buffer".to_string(), + ))); + } + Ok(Some(EventOutcome::Ok(String::new()))) + } + "close_buffer" => { + let current_table_name = app_state.current_view_table_name.as_deref(); + let message = + buffer_state.close_buffer_with_intro_fallback(current_table_name); + Ok(Some(EventOutcome::Ok(message))) + } + "open_search" => { + if let Page::Form(_) = &router.current { + if let Some(table_name) = app_state.current_view_table_name.clone() { + app_state.ui.show_search_palette = true; + app_state.search_state = Some(SearchState::new(table_name)); + self.set_focus_outside(router, true); + return Ok(Some(EventOutcome::Ok( + "Search palette opened".to_string(), + ))); + } + } + Ok(Some(EventOutcome::Ok(String::new()))) + } + "enter_command_mode" => { + if self.is_focus_outside(router) + && !self.command_mode + && !app_state.ui.show_search_palette + && !self.navigation_state.active + { + self.command_mode = true; + self.command_input.clear(); + self.command_message.clear(); + self.key_sequence_tracker.reset(); + self.set_focus_outside(router, true); + return Ok(Some(EventOutcome::Ok(String::new()))); + } + Ok(Some(EventOutcome::Ok(String::new()))) + } + "find_file_palette_toggle" => { + if matches!(&router.current, Page::Form(_) | Page::Intro(_)) { + let mut all_table_paths: Vec = app_state + .profile_tree + .profiles + .iter() + .flat_map(|profile| { + profile.tables.iter().map(move |table| { + format!("{}/{}", profile.name, table.name) + }) + }) + .collect(); + all_table_paths.sort(); + + self.navigation_state.activate_find_file(all_table_paths); + self.command_mode = false; + self.command_input.clear(); + self.command_message.clear(); + self.key_sequence_tracker.reset(); + return Ok(Some(EventOutcome::Ok( + "Table selection palette activated".to_string(), + ))); + } + Ok(Some(EventOutcome::Ok(String::new()))) + } + // Core actions that already have a handler + "save" | "force_quit" | "save_and_quit" | "revert" => { + let outcome = self + .handle_core_action( + action, + auth_state, + terminal, + app_state, + router, + ) + .await?; + Ok(Some(outcome)) + } + _ => Ok(None), + } + } +}