diff --git a/canvas/examples/canvas_gui_demo.rs b/canvas/examples/canvas_gui_demo.rs index b14d2be..534718f 100644 --- a/canvas/examples/canvas_gui_demo.rs +++ b/canvas/examples/canvas_gui_demo.rs @@ -58,12 +58,12 @@ impl DemoFormState { fn new() -> Self { Self { fields: vec![ - "John Doe".to_string(), // Name - has words to test with - "john.doe@example.com".to_string(), // Email - has punctuation - "+1 234 567 8900".to_string(), // Phone - has spaces and numbers - "123 Main Street Apt 4B".to_string(), // Address - multiple words - "San Francisco".to_string(), // City - two words - "This is a test comment with multiple words".to_string(), // Comments - lots of words + "John Doe".to_string(), + "john.doe@example.com".to_string(), + "+1 234 567 8900".to_string(), + "123 Main Street Apt 4B".to_string(), + "San Francisco".to_string(), + "This is a test comment with multiple words".to_string(), ], field_names: vec![ "Name".to_string(), @@ -80,7 +80,7 @@ impl DemoFormState { has_changes: false, ideal_cursor_column: 0, last_action: None, - debug_message: "Ready - Form loaded with sample data".to_string(), + debug_message: "Ready".to_string(), } } @@ -108,11 +108,6 @@ impl DemoFormState { self.debug_message = "Entered VISUAL mode".to_string(); } } - - fn log_action(&mut self, action: &str) { - self.last_action = Some(action.to_string()); - self.debug_message = format!("Action: {}", action); - } } impl CanvasState for DemoFormState { @@ -126,7 +121,6 @@ impl CanvasState for DemoFormState { fn set_current_field(&mut self, index: usize) { self.current_field = index.min(self.fields.len().saturating_sub(1)); - // Reset cursor to end of field when switching self.cursor_pos = self.fields[self.current_field].len(); } @@ -164,17 +158,6 @@ impl CanvasState for DemoFormState { } fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - // FOCUS: Debug specifically for 'w' key (move_word_next) - if let CanvasAction::MoveWordNext = action { - let current_input = self.get_current_input(); - let old_cursor = self.cursor_pos; - self.debug_message = format!("🔍 MoveWordNext: cursor {} -> text '{}' (len {})", - old_cursor, current_input, current_input.len()); - - // Return None to let the handler process it, but we'll see this debug message - return None; - } - match action { CanvasAction::Custom(cmd) => { match cmd.as_str() { @@ -205,15 +188,7 @@ async fn run_app(terminal: &mut Terminal, mut state: DemoFormStat terminal.draw(|f| ui(f, &state, &theme))?; if let Event::Key(key) = event::read()? { - // BASIC DEBUG: Show EVERY key press for j, k, w - match key.code { - KeyCode::Char('j') | KeyCode::Char('k') | KeyCode::Char('w') => { - println!("🔥 KEY PRESSED: {:?} with modifiers {:?}", key.code, key.modifiers); - } - _ => {} - } - - // Handle quit - multiple options + // Handle quit if (key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL)) || (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL)) || key.code == KeyCode::F(10) { @@ -223,74 +198,20 @@ async fn run_app(terminal: &mut Terminal, mut state: DemoFormStat let is_edit_mode = state.mode == AppMode::Edit; let mut handled = false; - // Debug: Show what key was pressed and check config lookup - let key_debug = format!("{:?}", key.code); - let config_action = if is_edit_mode { - config.get_edit_action(key.code, key.modifiers) - } else { - config.get_read_only_action(key.code, key.modifiers) - }; - - // FOCUS: Special debug for j, k, w keys - match key.code { - KeyCode::Char('j') => { - println!("🔥 J KEY: Config action: {:?}", config_action); - state.debug_message = format!("🔍 'j' KEY: Mode={} | Config action: {:?}", - if is_edit_mode { "EDIT" } else { "READ-ONLY" }, config_action); - } - KeyCode::Char('k') => { - println!("🔥 K KEY: Config action: {:?}", config_action); - state.debug_message = format!("🔍 'k' KEY: Mode={} | Config action: {:?}", - if is_edit_mode { "EDIT" } else { "READ-ONLY" }, config_action); - } - KeyCode::Char('w') => { - println!("🔥 W KEY: Config action: {:?}", config_action); - state.debug_message = format!("🔍 'w' KEY: Mode={} | Config action: {:?} | Current pos: {} | Text: '{}'", - if is_edit_mode { "EDIT" } else { "READ-ONLY" }, - config_action, - state.cursor_pos, - state.get_current_input()); - } - _ => { - state.debug_message = format!("Key: {} | Mods: {:?} | Mode: {} | Config found: {:?}", - key_debug, key.modifiers, - if is_edit_mode { "EDIT" } else { "READ-ONLY" }, - config_action); - } - } - - // First priority: Try to dispatch through your config system + // First priority: Try to dispatch through config system let mut ideal_cursor = state.ideal_cursor_column; - let old_cursor_pos = state.cursor_pos; // Track cursor before action - - // EXTRA DEBUG for w key - if key.code == KeyCode::Char('w') { - println!("🔥 W KEY: About to call ActionDispatcher::dispatch_key"); - println!("🔥 W KEY: cursor before = {}, text = '{}'", old_cursor_pos, state.get_current_input()); - } - + if let Ok(Some(result)) = ActionDispatcher::dispatch_key( key.code, key.modifiers, &mut state, &mut ideal_cursor, is_edit_mode, - false, // no autocomplete suggestions + false, ).await { state.ideal_cursor_column = ideal_cursor; - - let new_cursor_pos = state.cursor_pos; // Track cursor after action - - // FOCUS: Special debug for 'w' key - if key.code == KeyCode::Char('w') { - println!("SUCCESS W KEY PROCESSED: cursor {} -> {} | text: '{}'", old_cursor_pos, new_cursor_pos, state.get_current_input()); - state.debug_message = format!("SUCCESS 'w' PROCESSED: cursor {} -> {} | text: '{}'", - old_cursor_pos, new_cursor_pos, state.get_current_input()); - } else { - state.debug_message = format!("SUCCESS Config handled: {} -> {}", key_debug, - result.message().unwrap_or("success")); - } - + state.debug_message = format!("Config handled: {:?}", key.code); + // Mark as changed for text modification keys in edit mode if is_edit_mode { match key.code { @@ -301,34 +222,9 @@ async fn run_app(terminal: &mut Terminal, mut state: DemoFormStat } } handled = true; - } else { - // Debug dispatch failures - if key.code == KeyCode::Char('w') { - println!("FAILED W KEY: ActionDispatcher::dispatch_key returned None or Error"); - - // Try calling dispatch_with_config directly to see the error - let action = CanvasAction::MoveWordNext; - println!("FAILED W KEY: Trying direct dispatch of MoveWordNext action"); - - match ActionDispatcher::dispatch_with_config( - action, - &mut state, - &mut ideal_cursor, - Some(&config), - ).await { - Ok(result) => { - println!("FAILED W KEY: Direct dispatch SUCCESS: {:?}", result); - state.debug_message = "Direct dispatch worked!".to_string(); - } - Err(e) => { - println!("FAILED W KEY: Direct dispatch ERROR: {:?}", e); - state.debug_message = format!("Direct dispatch error: {:?}", e); - } - } - } } - // Second priority: Handle character input in edit mode (if not handled by config) + // Second priority: Handle character input in edit mode if !handled && is_edit_mode { if let KeyCode::Char(c) = key.code { if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) { @@ -349,45 +245,33 @@ async fn run_app(terminal: &mut Terminal, mut state: DemoFormStat } } - // Third priority: Fallback mode transitions (if not handled by config) + // Third priority: Fallback mode transitions if !handled { match (state.mode, key.code) { - // ReadOnly -> Edit mode fallbacks (AppMode::ReadOnly, KeyCode::Char('i') | KeyCode::Char('a') | KeyCode::Insert) => { state.enter_edit_mode(); if key.code == KeyCode::Char('a') { state.cursor_pos = state.fields[state.current_field].len(); } - state.debug_message = format!("Fallback: entered edit mode via {:?}", key.code); + state.debug_message = format!("Entered edit mode via {:?}", key.code); handled = true; } - // ReadOnly -> Visual mode fallback (AppMode::ReadOnly, KeyCode::Char('v')) => { state.enter_highlight_mode(); - state.debug_message = "Fallback: entered visual mode via 'v'".to_string(); + state.debug_message = "Entered visual mode".to_string(); handled = true; } - // Any mode -> ReadOnly fallback (_, KeyCode::Esc) => { state.enter_readonly_mode(); - state.debug_message = "Fallback: entered read-only mode via Esc".to_string(); + state.debug_message = "Entered read-only mode".to_string(); handled = true; } _ => {} } } - // If nothing handled the key, show more debug info if !handled { - let available_actions: Vec = if is_edit_mode { - config.keybindings.edit.keys().cloned().collect() - } else { - config.keybindings.read_only.keys().cloned().collect() - }; - - state.debug_message = format!("❌ Unhandled: {} | Available actions: {}", - key_debug, - available_actions.join(", ")); + state.debug_message = format!("Unhandled key: {:?}", key.code); } } } @@ -399,8 +283,8 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Min(8), // Main form area - Constraint::Length(4), // Status area (increased for debug info) + Constraint::Min(8), + Constraint::Length(4), ]) .split(f.area()); @@ -414,7 +298,7 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { &state.highlight_state, ); - // Render status bar with debug info + // Render status bar let mode_text = match state.mode { AppMode::Edit => "EDIT", AppMode::ReadOnly => "NORMAL", @@ -429,15 +313,15 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { format!("-- {} --", mode_text) }; - let position_text = format!("Field: {}/{} | Cursor: {} | Column: {}", - state.current_field + 1, - state.fields.len(), - state.cursor_pos, + let position_text = format!("Field: {}/{} | Cursor: {} | Column: {}", + state.current_field + 1, + state.fields.len(), + state.cursor_pos, state.ideal_cursor_column); let help_text = match state.mode { AppMode::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | gg/G: File | i/a: Edit | v: Visual | F10: Quit", - AppMode::Edit => "Type to edit | hjkl/arrows: Move | Tab/Enter: Next field | Backspace/Delete: Delete | Home/End: Line | Esc: Normal | F10: Quit", + AppMode::Edit => "Type to edit | hjkl/arrows: Move | Tab/Enter: Next field | Backspace/Delete: Delete | Home/End: Line | Esc: Normal | F10: Quit", AppMode::Highlight => "hjkl/arrows: Select | w/b/e: Words | 0/$: Line | Esc: Normal | F10: Quit", _ => "Esc: Normal | F10: Quit", }; @@ -448,30 +332,25 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { Line::from(Span::styled(state.debug_message.clone(), Style::default().fg(theme.warning()))), Line::from(Span::styled(help_text, Style::default().fg(theme.secondary()))), ]) - .block(Block::default().borders(Borders::ALL).title("Status & Debug")); + .block(Block::default().borders(Borders::ALL).title("Status")); f.render_widget(status, chunks[1]); } #[tokio::main] async fn main() -> Result<(), Box> { - // Load configuration let config = CanvasConfig::load(); - // Setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Create demo state let state = DemoFormState::new(); - // Run app let res = run_app(&mut terminal, state, config).await; - // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), diff --git a/canvas/src/config/config.rs b/canvas/src/config/config.rs index 2ac911f..9db4bc9 100644 --- a/canvas/src/config/config.rs +++ b/canvas/src/config/config.rs @@ -79,56 +79,156 @@ impl Default for CanvasConfig { } impl CanvasKeybindings { + /// Generate complete vim defaults from introspection system + /// This ensures defaults are always in sync with actual handler capabilities pub fn with_vim_defaults() -> Self { - // TODO: Could be generated from introspection too + let registry = ActionRegistry::from_handlers(); + Self::generate_from_registry(®istry) + } + + /// Generate keybindings from action registry (used by both defaults and config generation) + /// This is the single source of truth for what keybindings should exist + fn generate_from_registry(registry: &ActionRegistry) -> Self { let mut keybindings = Self::default(); - // Read-only mode (vim-style navigation) + // Generate keybindings for each mode discovered by introspection + for (mode_name, mode_registry) in ®istry.modes { + let mode_bindings = match mode_name.as_str() { + "edit" => &mut keybindings.edit, + "read_only" => &mut keybindings.read_only, + "highlight" => &mut keybindings.global, // Highlight actions go in global + _ => { + // Handle any future modes discovered by introspection + eprintln!("Warning: Unknown mode '{}' discovered by introspection", mode_name); + continue; + } + }; + + // Add ALL required actions + for (action_name, action_spec) in &mode_registry.required { + if !action_spec.examples.is_empty() { + mode_bindings.insert( + action_name.clone(), + action_spec.examples.clone() + ); + } + } + + // Add ALL optional actions + for (action_name, action_spec) in &mode_registry.optional { + if !action_spec.examples.is_empty() { + mode_bindings.insert( + action_name.clone(), + action_spec.examples.clone() + ); + } + } + } + + keybindings + } + + /// Generate a minimal fallback configuration if introspection fails + /// This should rarely be used, but provides safety net + fn minimal_fallback() -> Self { + let mut keybindings = Self::default(); + + // Absolute minimum required for basic functionality keybindings.read_only.insert("move_left".to_string(), vec!["h".to_string()]); keybindings.read_only.insert("move_right".to_string(), vec!["l".to_string()]); keybindings.read_only.insert("move_up".to_string(), vec!["k".to_string()]); keybindings.read_only.insert("move_down".to_string(), vec!["j".to_string()]); - // Edit mode keybindings.edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]); keybindings.edit.insert("move_left".to_string(), vec!["Left".to_string()]); keybindings.edit.insert("move_right".to_string(), vec!["Right".to_string()]); keybindings.edit.insert("move_up".to_string(), vec!["Up".to_string()]); keybindings.edit.insert("move_down".to_string(), vec!["Down".to_string()]); - keybindings.edit.insert("next_field".to_string(), vec!["Tab".to_string()]); - keybindings.edit.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]); keybindings } + + /// Validate that generated keybindings match the current introspection state + /// This helps catch when handlers change but defaults become stale + pub fn validate_against_introspection(&self) -> Result<(), Vec> { + let registry = ActionRegistry::from_handlers(); + let expected = Self::generate_from_registry(®istry); + let mut errors = Vec::new(); + + // Check each mode + for (mode_name, expected_bindings) in [ + ("edit", &expected.edit), + ("read_only", &expected.read_only), + ("global", &expected.global), + ] { + let actual_bindings = match mode_name { + "edit" => &self.edit, + "read_only" => &self.read_only, + "global" => &self.global, + _ => continue, + }; + + // Check for missing actions + for action_name in expected_bindings.keys() { + if !actual_bindings.contains_key(action_name) { + errors.push(format!( + "Missing action '{}' in {} mode (expected by introspection)", + action_name, mode_name + )); + } + } + + // Check for unexpected actions + for action_name in actual_bindings.keys() { + if !expected_bindings.contains_key(action_name) { + errors.push(format!( + "Unexpected action '{}' in {} mode (not found in introspection)", + action_name, mode_name + )); + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } } impl CanvasConfig { - /// NEW: Load and validate configuration using dynamic registry + /// Enhanced load method with introspection validation pub fn load() -> Self { match Self::load_and_validate() { Ok(config) => config, Err(e) => { + eprintln!("Failed to load config file: {}", e); + eprintln!("Using auto-generated defaults from introspection"); Self::default() } } } - /// NEW: Load configuration with validation using dynamic registry + /// Load and validate configuration with enhanced introspection checks pub fn load_and_validate() -> Result { // Try to load canvas_config.toml from current directory let config = if let Ok(config) = Self::from_file(std::path::Path::new("canvas_config.toml")) { config } else { - // Use default if file doesn't exist + // Use auto-generated defaults if file doesn't exist + eprintln!("Config file not found, using auto-generated defaults"); Self::default() }; - // NEW: Use dynamic registry from actual handlers + // Validate the configuration against current introspection state let registry = ActionRegistry::from_handlers(); - // Validate the handlers match their claimed capabilities + // Validate handlers are working correctly if let Err(handler_errors) = registry.validate_against_implementation() { + eprintln!("Handler validation warnings:"); for error in handler_errors { + eprintln!(" - {}", error); } } @@ -137,31 +237,125 @@ impl CanvasConfig { let validation_result = validator.validate_keybindings(&config.keybindings); if !validation_result.is_valid { + eprintln!("Configuration validation failed:"); validator.print_validation_result(&validation_result); } else if !validation_result.warnings.is_empty() { + eprintln!("Configuration validation warnings:"); validator.print_validation_result(&validation_result); } + // Optional: Validate that our defaults match introspection + if let Err(sync_errors) = config.keybindings.validate_against_introspection() { + eprintln!("Default keybindings out of sync with introspection:"); + for error in sync_errors { + eprintln!(" - {}", error); + } + } + Ok(config) } - /// NEW: Generate template from actual handler capabilities - pub fn generate_template() -> String { + /// Generate a complete configuration template that matches current defaults + /// This ensures the generated config file has the same content as defaults + pub fn generate_complete_template() -> String { let registry = ActionRegistry::from_handlers(); + let defaults = CanvasKeybindings::generate_from_registry(®istry); - // Validate handlers first - if let Err(errors) = registry.validate_against_implementation() { - for error in errors { + let mut template = String::new(); + template.push_str("# Canvas Library Configuration\n"); + template.push_str("# Auto-generated from handler introspection\n"); + template.push_str("# This config contains ALL available actions\n\n"); + + // Generate sections for each mode + for (mode_name, bindings) in [ + ("read_only", &defaults.read_only), + ("edit", &defaults.edit), + ("global", &defaults.global), + ] { + if bindings.is_empty() { + continue; } + + template.push_str(&format!("[keybindings.{}]\n", mode_name)); + + // Get mode registry for categorization + if let Some(mode_registry) = registry.get_mode_registry(mode_name) { + // Required actions first + let mut found_required = false; + for (action_name, keybindings) in bindings { + if mode_registry.required.contains_key(action_name) { + if !found_required { + template.push_str("# Required\n"); + found_required = true; + } + template.push_str(&format!("{} = {:?}\n", action_name, keybindings)); + } + } + + // Optional actions second + let mut found_optional = false; + for (action_name, keybindings) in bindings { + if mode_registry.optional.contains_key(action_name) { + if !found_optional { + template.push_str("# Optional\n"); + found_optional = true; + } + template.push_str(&format!("{} = {:?}\n", action_name, keybindings)); + } + } + } else { + // Fallback: just list all actions + for (action_name, keybindings) in bindings { + template.push_str(&format!("{} = {:?}\n", action_name, keybindings)); + } + } + + template.push('\n'); } - registry.generate_config_template() + template } - /// NEW: Generate clean template from actual handler capabilities + /// Generate config that only contains actions different from defaults + /// Useful for minimal user configs + pub fn generate_minimal_template() -> String { + let defaults = CanvasKeybindings::with_vim_defaults(); + + let mut template = String::new(); + template.push_str("# Minimal Canvas Configuration\n"); + template.push_str("# Only uncomment and modify the keybindings you want to change\n"); + template.push_str("# All other actions will use their default vim-style keybindings\n\n"); + + for (mode_name, bindings) in [ + ("read_only", &defaults.read_only), + ("edit", &defaults.edit), + ("global", &defaults.global), + ] { + if bindings.is_empty() { + continue; + } + + template.push_str(&format!("# [keybindings.{}]\n", mode_name)); + + for (action_name, keybindings) in bindings { + template.push_str(&format!("# {} = {:?}\n", action_name, keybindings)); + } + + template.push('\n'); + } + + template + } + + /// Generate template from actual handler capabilities (legacy method for compatibility) + pub fn generate_template() -> String { + Self::generate_complete_template() + } + + /// Generate clean template from actual handler capabilities (legacy method for compatibility) pub fn generate_clean_template() -> String { let registry = ActionRegistry::from_handlers(); - + // Validate handlers first if let Err(errors) = registry.validate_against_implementation() { for error in errors { @@ -172,14 +366,14 @@ impl CanvasConfig { registry.generate_clean_template() } - /// NEW: Validate current configuration against actual implementation + /// Validate current configuration against actual implementation pub fn validate(&self) -> ValidationResult { let registry = ActionRegistry::from_handlers(); let validator = ConfigValidator::new(registry); validator.validate_keybindings(&self.keybindings) } - /// NEW: Print validation results for current config + /// Print validation results for current config pub fn print_validation(&self) { let registry = ActionRegistry::from_handlers(); let validator = ConfigValidator::new(registry); @@ -200,22 +394,20 @@ impl CanvasConfig { Self::from_toml(&contents) } - /// RESTORED: Check if autocomplete should auto-trigger (simple logic) + /// Check if autocomplete should auto-trigger (simple logic) pub fn should_auto_trigger_autocomplete(&self) -> bool { // If trigger_autocomplete keybinding exists anywhere, use manual mode only // If no trigger_autocomplete keybinding, use auto-trigger mode !self.has_trigger_autocomplete_keybinding() } - /// RESTORED: Check if user has configured manual trigger keybinding + /// Check if user has configured manual trigger keybinding pub fn has_trigger_autocomplete_keybinding(&self) -> bool { self.keybindings.edit.contains_key("trigger_autocomplete") || self.keybindings.read_only.contains_key("trigger_autocomplete") || self.keybindings.global.contains_key("trigger_autocomplete") } - // ... keep all your existing key matching methods ... - /// Get action for key in read-only mode pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { self.get_action_in_mode(&self.keybindings.read_only, key, modifiers) @@ -281,21 +473,21 @@ impl CanvasConfig { "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), @@ -321,18 +513,18 @@ impl CanvasConfig { "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), @@ -347,7 +539,7 @@ impl CanvasConfig { "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), @@ -363,7 +555,7 @@ impl CanvasConfig { "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 _ => { @@ -395,7 +587,7 @@ impl CanvasConfig { "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), @@ -405,21 +597,21 @@ impl CanvasConfig { "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)), @@ -445,18 +637,18 @@ impl CanvasConfig { "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 {