diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index b7e3c51..310a4b6 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -31,12 +31,9 @@ pub async fn execute_canvas_action_with_autocomplete { - println!("AUTO-T on Ins"); let current_field = state.current_field(); let current_input = state.get_current_input(); @@ -44,13 +41,11 @@ pub async fn execute_canvas_action_with_autocomplete= 1 { - println!("ACT AUTOC"); state.activate_autocomplete(); } } CanvasAction::NextField | CanvasAction::PrevField => { - println!("AUTO-T on nav"); let current_field = state.current_field(); if state.supports_autocomplete(current_field) && !state.is_autocomplete_active() { diff --git a/canvas/src/config/config.rs b/canvas/src/config/config.rs index b07e517..2ac911f 100644 --- a/canvas/src/config/config.rs +++ b/canvas/src/config/config.rs @@ -108,8 +108,6 @@ impl CanvasConfig { match Self::load_and_validate() { Ok(config) => config, Err(e) => { - eprintln!("⚠️ Failed to load canvas config: {}", e); - eprintln!(" Using default configuration"); Self::default() } } @@ -130,9 +128,7 @@ impl CanvasConfig { // Validate the handlers match their claimed capabilities if let Err(handler_errors) = registry.validate_against_implementation() { - eprintln!("⚠️ Handler validation failed:"); for error in handler_errors { - eprintln!(" - {}", error); } } @@ -141,15 +137,8 @@ impl CanvasConfig { let validation_result = validator.validate_keybindings(&config.keybindings); if !validation_result.is_valid { - eprintln!("❌ Canvas configuration validation failed:"); validator.print_validation_result(&validation_result); - eprintln!(); - eprintln!("🔧 To generate a working config template:"); - eprintln!(" CanvasConfig::generate_template()"); - eprintln!(); - eprintln!("📁 Expected config file location: canvas_config.toml"); } else if !validation_result.warnings.is_empty() { - eprintln!("⚠️ Canvas configuration has warnings:"); validator.print_validation_result(&validation_result); } @@ -162,9 +151,7 @@ impl CanvasConfig { // Validate handlers first if let Err(errors) = registry.validate_against_implementation() { - eprintln!("⚠️ Warning: Handler validation failed while generating template:"); for error in errors { - eprintln!(" - {}", error); } } @@ -177,7 +164,6 @@ impl CanvasConfig { // Validate handlers first if let Err(errors) = registry.validate_against_implementation() { - eprintln!("⚠️ Warning: Handler validation failed while generating template:"); for error in errors { eprintln!(" - {}", error); } @@ -263,26 +249,225 @@ impl CanvasConfig { None } - fn matches_keybinding(&self, _binding: &str, _key: KeyCode, _modifiers: KeyModifiers) -> bool { - // Keep your existing implementation - this is just a placeholder - true - } - - /// Debug method to print loaded keybindings with validation - pub fn debug_keybindings(&self) { - println!("📋 Canvas keybindings loaded:"); - println!(" Read-only: {} actions", self.keybindings.read_only.len()); - println!(" Edit: {} actions", self.keybindings.edit.len()); - - // NEW: Show validation status against actual implementation - let validation = self.validate(); - if validation.is_valid { - println!(" ✅ Configuration matches actual implementation"); - } else { - println!(" ❌ Configuration has {} errors vs implementation", validation.errors.len()); + fn matches_keybinding(&self, binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool { + // Special handling for shift+character combinations + if binding.to_lowercase().starts_with("shift+") { + let parts: Vec<&str> = binding.split('+').collect(); + if parts.len() == 2 && parts[1].len() == 1 { + let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap(); + let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap(); + if let KeyCode::Char(actual_char) = key { + if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) { + return true; + } + } + } } - if !validation.warnings.is_empty() { - println!(" ⚠️ Configuration has {} warnings", validation.warnings.len()); + + // Handle Shift+Tab -> BackTab + if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() { + return true; } + + // Handle multi-character bindings (all standard keys without modifiers) + if binding.len() > 1 && !binding.contains('+') { + return match binding.to_lowercase().as_str() { + // Navigation keys + "left" => key == KeyCode::Left, + "right" => key == KeyCode::Right, + "up" => key == KeyCode::Up, + "down" => key == KeyCode::Down, + "home" => key == KeyCode::Home, + "end" => key == KeyCode::End, + "pageup" | "pgup" => key == KeyCode::PageUp, + "pagedown" | "pgdn" => key == KeyCode::PageDown, + + // Editing keys + "insert" | "ins" => key == KeyCode::Insert, + "delete" | "del" => key == KeyCode::Delete, + "backspace" => key == KeyCode::Backspace, + + // Tab keys + "tab" => key == KeyCode::Tab, + "backtab" => key == KeyCode::BackTab, + + // Special keys + "enter" | "return" => key == KeyCode::Enter, + "escape" | "esc" => key == KeyCode::Esc, + "space" => key == KeyCode::Char(' '), + + // Function keys F1-F24 + "f1" => key == KeyCode::F(1), + "f2" => key == KeyCode::F(2), + "f3" => key == KeyCode::F(3), + "f4" => key == KeyCode::F(4), + "f5" => key == KeyCode::F(5), + "f6" => key == KeyCode::F(6), + "f7" => key == KeyCode::F(7), + "f8" => key == KeyCode::F(8), + "f9" => key == KeyCode::F(9), + "f10" => key == KeyCode::F(10), + "f11" => key == KeyCode::F(11), + "f12" => key == KeyCode::F(12), + "f13" => key == KeyCode::F(13), + "f14" => key == KeyCode::F(14), + "f15" => key == KeyCode::F(15), + "f16" => key == KeyCode::F(16), + "f17" => key == KeyCode::F(17), + "f18" => key == KeyCode::F(18), + "f19" => key == KeyCode::F(19), + "f20" => key == KeyCode::F(20), + "f21" => key == KeyCode::F(21), + "f22" => key == KeyCode::F(22), + "f23" => key == KeyCode::F(23), + "f24" => key == KeyCode::F(24), + + // Lock keys (may not work reliably in all terminals) + "capslock" => key == KeyCode::CapsLock, + "scrolllock" => key == KeyCode::ScrollLock, + "numlock" => key == KeyCode::NumLock, + + // System keys + "printscreen" => key == KeyCode::PrintScreen, + "pause" => key == KeyCode::Pause, + "menu" => key == KeyCode::Menu, + "keypadbegin" => key == KeyCode::KeypadBegin, + + // Media keys (rarely supported but included for completeness) + "mediaplay" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Play), + "mediapause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Pause), + "mediaplaypause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::PlayPause), + "mediareverse" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Reverse), + "mediastop" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Stop), + "mediafastforward" => key == KeyCode::Media(crossterm::event::MediaKeyCode::FastForward), + "mediarewind" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Rewind), + "mediatracknext" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackNext), + "mediatrackprevious" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackPrevious), + "mediarecord" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Record), + "medialowervolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::LowerVolume), + "mediaraisevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume), + "mediamutevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::MuteVolume), + + // Modifier keys (these work better as part of combinations) + "leftshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftShift), + "leftcontrol" | "leftctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftControl), + "leftalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftAlt), + "leftsuper" | "leftwindows" | "leftcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftSuper), + "lefthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftHyper), + "leftmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftMeta), + "rightshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightShift), + "rightcontrol" | "rightctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightControl), + "rightalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightAlt), + "rightsuper" | "rightwindows" | "rightcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightSuper), + "righthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightHyper), + "rightmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightMeta), + "isolevel3shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel3Shift), + "isolevel5shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel5Shift), + + // Multi-key sequences need special handling + "gg" => false, // This needs sequence handling + _ => { + // Handle single characters and punctuation + if binding.len() == 1 { + if let Some(c) = binding.chars().next() { + key == KeyCode::Char(c) + } else { + false + } + } else { + false + } + } + }; + } + + // Handle modifier combinations (like "Ctrl+F5", "Alt+Shift+A") + let parts: Vec<&str> = binding.split('+').collect(); + let mut expected_modifiers = KeyModifiers::empty(); + let mut expected_key = None; + + for part in parts { + match part.to_lowercase().as_str() { + // Modifiers + "ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL, + "shift" => expected_modifiers |= KeyModifiers::SHIFT, + "alt" => expected_modifiers |= KeyModifiers::ALT, + "super" | "windows" | "cmd" => expected_modifiers |= KeyModifiers::SUPER, + "hyper" => expected_modifiers |= KeyModifiers::HYPER, + "meta" => expected_modifiers |= KeyModifiers::META, + + // Navigation keys + "left" => expected_key = Some(KeyCode::Left), + "right" => expected_key = Some(KeyCode::Right), + "up" => expected_key = Some(KeyCode::Up), + "down" => expected_key = Some(KeyCode::Down), + "home" => expected_key = Some(KeyCode::Home), + "end" => expected_key = Some(KeyCode::End), + "pageup" | "pgup" => expected_key = Some(KeyCode::PageUp), + "pagedown" | "pgdn" => expected_key = Some(KeyCode::PageDown), + + // Editing keys + "insert" | "ins" => expected_key = Some(KeyCode::Insert), + "delete" | "del" => expected_key = Some(KeyCode::Delete), + "backspace" => expected_key = Some(KeyCode::Backspace), + + // Tab keys + "tab" => expected_key = Some(KeyCode::Tab), + "backtab" => expected_key = Some(KeyCode::BackTab), + + // Special keys + "enter" | "return" => expected_key = Some(KeyCode::Enter), + "escape" | "esc" => expected_key = Some(KeyCode::Esc), + "space" => expected_key = Some(KeyCode::Char(' ')), + + // Function keys + "f1" => expected_key = Some(KeyCode::F(1)), + "f2" => expected_key = Some(KeyCode::F(2)), + "f3" => expected_key = Some(KeyCode::F(3)), + "f4" => expected_key = Some(KeyCode::F(4)), + "f5" => expected_key = Some(KeyCode::F(5)), + "f6" => expected_key = Some(KeyCode::F(6)), + "f7" => expected_key = Some(KeyCode::F(7)), + "f8" => expected_key = Some(KeyCode::F(8)), + "f9" => expected_key = Some(KeyCode::F(9)), + "f10" => expected_key = Some(KeyCode::F(10)), + "f11" => expected_key = Some(KeyCode::F(11)), + "f12" => expected_key = Some(KeyCode::F(12)), + "f13" => expected_key = Some(KeyCode::F(13)), + "f14" => expected_key = Some(KeyCode::F(14)), + "f15" => expected_key = Some(KeyCode::F(15)), + "f16" => expected_key = Some(KeyCode::F(16)), + "f17" => expected_key = Some(KeyCode::F(17)), + "f18" => expected_key = Some(KeyCode::F(18)), + "f19" => expected_key = Some(KeyCode::F(19)), + "f20" => expected_key = Some(KeyCode::F(20)), + "f21" => expected_key = Some(KeyCode::F(21)), + "f22" => expected_key = Some(KeyCode::F(22)), + "f23" => expected_key = Some(KeyCode::F(23)), + "f24" => expected_key = Some(KeyCode::F(24)), + + // Lock keys + "capslock" => expected_key = Some(KeyCode::CapsLock), + "scrolllock" => expected_key = Some(KeyCode::ScrollLock), + "numlock" => expected_key = Some(KeyCode::NumLock), + + // System keys + "printscreen" => expected_key = Some(KeyCode::PrintScreen), + "pause" => expected_key = Some(KeyCode::Pause), + "menu" => expected_key = Some(KeyCode::Menu), + "keypadbegin" => expected_key = Some(KeyCode::KeypadBegin), + + // Single character (letters, numbers, punctuation) + part => { + if part.len() == 1 { + if let Some(c) = part.chars().next() { + expected_key = Some(KeyCode::Char(c)); + } + } + } + } + } + + modifiers == expected_modifiers && Some(key) == expected_key } } diff --git a/client/config.toml b/client/config.toml index e1479c3..d370570 100644 --- a/client/config.toml +++ b/client/config.toml @@ -54,10 +54,10 @@ move_word_next = ["w"] next_field = ["Tab"] move_word_prev = ["b"] move_word_end = ["e"] -move_last_line = ["G"] +move_last_line = ["shift+g"] move_word_end_prev = ["ge"] move_line_start = ["0"] -move_first_line = ["gg"] +move_first_line = ["g+g"] prev_field = ["Shift+Tab"] [keybindings.highlight] diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 9cff458..dd814d3 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -12,7 +12,7 @@ use canvas::canvas::CanvasState; use canvas::{canvas::CanvasAction, dispatcher::ActionDispatcher, canvas::ActionResult}; use anyhow::Result; use common::proto::komp_ac::search::search_response::Hit; -use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use tokio::sync::mpsc; use tracing::info; @@ -143,23 +143,46 @@ async fn execute_canvas_action( } } -/// NEW: Unified canvas action handler for any CanvasState (LoginState, RegisterState, etc.) -/// This replaces the old auth_e::execute_edit_action calls with the new canvas library -/// NEW: Unified canvas action handler for any CanvasState with character fallback -/// Complete canvas action handler with fallbacks for common keys -/// Debug version to see what's happening +/// FIXED: Unified canvas action handler with proper priority order for edit mode async fn handle_canvas_state_edit( key: KeyEvent, config: &Config, state: &mut S, ideal_cursor_column: &mut usize, ) -> Result { - println!("DEBUG: Key pressed: {:?}", key); // DEBUG - - // Try direct key mapping first (same pattern as FormState) + // println!("DEBUG: Key pressed: {:?}", key); // DEBUG + + // PRIORITY 1: Character insertion in edit mode comes FIRST + if let KeyCode::Char(c) = key.code { + // Only insert if no modifiers or just shift (for uppercase) + if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT { + // println!("DEBUG: Using character insertion priority for: {}", c); // DEBUG + let canvas_action = CanvasAction::InsertChar(c); + match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(e) => { + // println!("DEBUG: Character insertion failed: {:?}, trying config", e); + // Fall through to try config mappings + } + } + } + } + + // PRIORITY 2: Check canvas config for special keys/combinations let canvas_config = canvas::config::CanvasConfig::load(); if let Some(action_name) = canvas_config.get_edit_action(key.code, key.modifiers) { - println!("DEBUG: Canvas config mapped to: {}", action_name); // DEBUG + // println!("DEBUG: Canvas config mapped to: {}", action_name); // DEBUG let canvas_action = CanvasAction::from_string(action_name); match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { @@ -176,62 +199,43 @@ async fn handle_canvas_state_edit( return Ok(format!("Context needed: {}", msg)); } Err(_) => { - println!("DEBUG: Canvas action failed, trying client config"); // DEBUG + // println!("DEBUG: Canvas action failed, trying client config"); // DEBUG } } } else { - println!("DEBUG: No canvas config mapping found"); // DEBUG + // println!("DEBUG: No canvas config mapping found"); // DEBUG } - // Try config-mapped action (same pattern as FormState) - if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) { - println!("DEBUG: Client config mapped to: {}", action_str); // DEBUG - let canvas_action = CanvasAction::from_string(&action_str); - match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { - Ok(ActionResult::Success(msg)) => { - return Ok(msg.unwrap_or_default()); - } - Ok(ActionResult::HandledByFeature(msg)) => { - return Ok(msg); - } - Ok(ActionResult::Error(msg)) => { - return Ok(format!("Error: {}", msg)); - } - Ok(ActionResult::RequiresContext(msg)) => { - return Ok(format!("Context needed: {}", msg)); - } - Err(e) => { - return Ok(format!("Action failed: {}", e)); + // PRIORITY 3: Check client config ONLY for non-character keys or modified keys + if !matches!(key.code, KeyCode::Char(_)) || !key.modifiers.is_empty() { + if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) { + // println!("DEBUG: Client config mapped to: {} (for non-char key)", action_str); // DEBUG + let canvas_action = CanvasAction::from_string(&action_str); + match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(e) => { + return Ok(format!("Action failed: {}", e)); + } } + } else { + // println!("DEBUG: No client config mapping found for non-char key"); // DEBUG } } else { - println!("DEBUG: No client config mapping found"); // DEBUG + // println!("DEBUG: Skipping client config for character key in edit mode"); // DEBUG } - // Character insertion fallback - if let KeyCode::Char(c) = key.code { - println!("DEBUG: Using character fallback for: {}", c); // DEBUG - let canvas_action = CanvasAction::InsertChar(c); - match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { - Ok(ActionResult::Success(msg)) => { - return Ok(msg.unwrap_or_default()); - } - Ok(ActionResult::HandledByFeature(msg)) => { - return Ok(msg); - } - Ok(ActionResult::Error(msg)) => { - return Ok(format!("Error: {}", msg)); - } - Ok(ActionResult::RequiresContext(msg)) => { - return Ok(format!("Context needed: {}", msg)); - } - Err(e) => { - return Ok(format!("Character insertion failed: {}", e)); - } - } - } - - println!("DEBUG: No action taken for key: {:?}", key); // DEBUG + // println!("DEBUG: No action taken for key: {:?}", key); // DEBUG Ok(String::new()) }