From d584a25fdb23cc03624aef5f410bc6126079301e Mon Sep 17 00:00:00 2001 From: Priec Date: Wed, 30 Jul 2025 21:16:16 +0200 Subject: [PATCH] removed hardcoded values from the canvas library --- canvas/src/canvas/actions/types.rs | 19 +--- canvas/src/dispatcher.rs | 142 ++------------------------- client/config.toml | 14 --- client/src/modes/canvas/edit.rs | 9 +- client/src/modes/canvas/read_only.rs | 4 +- client/src/modes/handlers/event.rs | 95 ++---------------- 6 files changed, 30 insertions(+), 253 deletions(-) diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index 45188bf..17878af 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -42,24 +42,7 @@ pub enum CanvasAction { } impl CanvasAction { - pub fn from_key(key: crossterm::event::KeyCode) -> Option { - match key { - crossterm::event::KeyCode::Char(c) => Some(Self::InsertChar(c)), - crossterm::event::KeyCode::Backspace => Some(Self::DeleteBackward), - crossterm::event::KeyCode::Delete => Some(Self::DeleteForward), - crossterm::event::KeyCode::Left => Some(Self::MoveLeft), - crossterm::event::KeyCode::Right => Some(Self::MoveRight), - crossterm::event::KeyCode::Up => Some(Self::MoveUp), - crossterm::event::KeyCode::Down => Some(Self::MoveDown), - crossterm::event::KeyCode::Home => Some(Self::MoveLineStart), - crossterm::event::KeyCode::End => Some(Self::MoveLineEnd), - crossterm::event::KeyCode::Tab => Some(Self::NextField), - crossterm::event::KeyCode::BackTab => Some(Self::PrevField), - _ => None, - } - } - - // Backward compatibility method + /// Convert string action name to CanvasAction enum (config-driven) pub fn from_string(action: &str) -> Self { match action { "delete_char_backward" => Self::DeleteBackward, diff --git a/canvas/src/dispatcher.rs b/canvas/src/dispatcher.rs index 78332c0..fab0622 100644 --- a/canvas/src/dispatcher.rs +++ b/canvas/src/dispatcher.rs @@ -3,6 +3,7 @@ use crate::canvas::state::CanvasState; use crate::canvas::actions::{CanvasAction, ActionResult, execute_canvas_action}; use crate::config::CanvasConfig; +use crossterm::event::{KeyCode, KeyModifiers}; /// High-level action dispatcher that coordinates between different action types pub struct ActionDispatcher; @@ -14,18 +15,23 @@ impl ActionDispatcher { state: &mut S, ideal_cursor_column: &mut usize, ) -> anyhow::Result { - // Load config once here instead of threading it everywhere execute_canvas_action(action, state, ideal_cursor_column, Some(&CanvasConfig::load())).await } - /// Quick action dispatch from KeyCode + /// Quick action dispatch from KeyCode using config pub async fn dispatch_key( - key: crossterm::event::KeyCode, + key: KeyCode, + modifiers: KeyModifiers, state: &mut S, ideal_cursor_column: &mut usize, + is_edit_mode: bool, + has_suggestions: bool, ) -> anyhow::Result> { - if let Some(action) = CanvasAction::from_key(key) { + let config = CanvasConfig::load(); + + if let Some(action_name) = config.get_action_for_key(key, modifiers, is_edit_mode, has_suggestions) { + let action = CanvasAction::from_string(action_name); let result = Self::dispatch(action, state, ideal_cursor_column).await?; Ok(Some(result)) } else { @@ -53,131 +59,3 @@ impl ActionDispatcher { Ok(results) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::actions::CanvasAction; - - // Simple test implementation - struct TestFormState { - current_field: usize, - cursor_pos: usize, - inputs: Vec, - field_names: Vec, - has_changes: bool, - } - - impl TestFormState { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - inputs: vec!["".to_string(), "".to_string()], - field_names: vec!["username".to_string(), "password".to_string()], - has_changes: false, - } - } - } - - impl CanvasState for TestFormState { - fn current_field(&self) -> usize { self.current_field } - fn current_cursor_pos(&self) -> usize { self.cursor_pos } - fn set_current_field(&mut self, index: usize) { self.current_field = index; } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { &self.inputs[self.current_field] } - fn get_current_input_mut(&mut self) -> &mut String { &mut self.inputs[self.current_field] } - fn inputs(&self) -> Vec<&String> { self.inputs.iter().collect() } - fn fields(&self) -> Vec<&str> { self.field_names.iter().map(|s| s.as_str()).collect() } - - fn has_unsaved_changes(&self) -> bool { self.has_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; } - - // Custom action handling for testing - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &crate::state::ActionContext) -> Option { - match action { - CanvasAction::Custom(s) if s == "test_custom" => { - Some("Custom action handled".to_string()) - } - _ => None, - } - } - } - - #[tokio::test] - async fn test_typed_action_dispatch() { - let mut state = TestFormState::new(); - let mut ideal_cursor = 0; - - // Test character insertion - let result = ActionDispatcher::dispatch( - CanvasAction::InsertChar('a'), - &mut state, - &mut ideal_cursor, - ).await.unwrap(); - - assert!(result.is_success()); - assert_eq!(state.get_current_input(), "a"); - assert_eq!(state.cursor_pos, 1); - assert!(state.has_changes); - } - - #[tokio::test] - async fn test_key_dispatch() { - let mut state = TestFormState::new(); - let mut ideal_cursor = 0; - - let result = ActionDispatcher::dispatch_key( - crossterm::event::KeyCode::Char('b'), - &mut state, - &mut ideal_cursor, - ).await.unwrap(); - - assert!(result.is_some()); - assert!(result.unwrap().is_success()); - assert_eq!(state.get_current_input(), "b"); - } - - #[tokio::test] - async fn test_custom_action() { - let mut state = TestFormState::new(); - let mut ideal_cursor = 0; - - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("test_custom".to_string()), - &mut state, - &mut ideal_cursor, - ).await.unwrap(); - - match result { - ActionResult::HandledByFeature(msg) => { - assert_eq!(msg, "Custom action handled"); - } - _ => panic!("Expected HandledByFeature result"), - } - } - - #[tokio::test] - async fn test_batch_dispatch() { - let mut state = TestFormState::new(); - let mut ideal_cursor = 0; - - let actions = vec![ - CanvasAction::InsertChar('h'), - CanvasAction::InsertChar('i'), - CanvasAction::MoveLeft, - CanvasAction::InsertChar('e'), - ]; - - let results = ActionDispatcher::dispatch_batch( - actions, - &mut state, - &mut ideal_cursor, - ).await.unwrap(); - - assert_eq!(results.len(), 4); - assert!(results.iter().all(|r| r.is_success())); - assert_eq!(state.get_current_input(), "hei"); - } -} diff --git a/client/config.toml b/client/config.toml index d889e2b..1a43411 100644 --- a/client/config.toml +++ b/client/config.toml @@ -39,18 +39,6 @@ enter_edit_mode_after = ["a"] previous_entry = ["left","q"] next_entry = ["right","1"] -move_left = ["h"] -move_right = ["l"] -move_up = ["k"] -move_down = ["j"] -move_word_next = ["w"] -move_word_end = ["e"] -move_word_prev = ["b"] -move_word_end_prev = ["ge"] -move_line_start = ["0"] -move_line_end = ["$"] -move_first_line = ["gg"] -move_last_line = ["x"] enter_highlight_mode = ["v"] enter_highlight_mode_linewise = ["ctrl+v"] @@ -69,8 +57,6 @@ prev_field = ["shift+enter"] exit = ["esc", "ctrl+e"] delete_char_forward = ["delete"] delete_char_backward = ["backspace"] -move_left = ["left"] -move_right = ["right"] suggestion_down = ["ctrl+n", "tab"] suggestion_up = ["ctrl+p", "shift+tab"] diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 9db2f2a..778bc7f 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -79,7 +79,9 @@ pub async fn handle_form_edit_with_canvas( ideal_cursor_column: &mut usize, ) -> Result { // Try canvas action from key first - if let Some(canvas_action) = CanvasAction::from_key(key_event.code) { + let canvas_config = canvas::config::CanvasConfig::load(); + if let Some(action_name) = canvas_config.get_edit_action(key_event.code, key_event.modifiers) { + let canvas_action = CanvasAction::from_string(action_name); match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await { Ok(ActionResult::Success(msg)) => { return Ok(msg.unwrap_or_default()); @@ -150,7 +152,10 @@ async fn handle_canvas_state_edit( ideal_cursor_column: &mut usize, ) -> Result { // Try direct key mapping first (same pattern as FormState) - if let Some(canvas_action) = CanvasAction::from_key(key.code) { + let canvas_config = canvas::config::CanvasConfig::load(); + if let Some(action_name) = canvas_config.get_edit_action(key.code, key.modifiers) { + let canvas_action = CanvasAction::from_string(action_name); + match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { Ok(ActionResult::Success(msg)) => { return Ok(msg.unwrap_or_default()); diff --git a/client/src/modes/canvas/read_only.rs b/client/src/modes/canvas/read_only.rs index e456c0f..93f5a6b 100644 --- a/client/src/modes/canvas/read_only.rs +++ b/client/src/modes/canvas/read_only.rs @@ -91,7 +91,9 @@ pub async fn handle_form_readonly_with_canvas( ideal_cursor_column: &mut usize, ) -> Result { // Try canvas action from key first - if let Some(canvas_action) = CanvasAction::from_key(key_event.code) { + let canvas_config = canvas::config::CanvasConfig::load(); + if let Some(action_name) = canvas_config.get_read_only_action(key_event.code, key_event.modifiers) { + let canvas_action = CanvasAction::from_string(action_name); match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await { Ok(ActionResult::Success(msg)) => { return Ok(msg.unwrap_or_default()); diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index ea131c2..7e560dc 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -1108,58 +1108,7 @@ impl EventHandler { ) -> Result> { let canvas_config = canvas::config::CanvasConfig::load(); - // Handle suggestion actions first if suggestions are active - if form_state.autocomplete_active { - if let Some(action_str) = canvas_config.get_suggestion_action(key_event.code, key_event.modifiers) { - let canvas_action = CanvasAction::from_string(&action_str); - match ActionDispatcher::dispatch(canvas_action, form_state, &mut self.ideal_cursor_column).await { - Ok(result) => return Ok(Some(result.message().unwrap_or("").to_string())), - Err(_) => return Ok(Some("Suggestion action failed".to_string())), - } - } - - // Fallback hardcoded suggestion handling - match key_event.code { - KeyCode::Up => { - if let Ok(result) = ActionDispatcher::dispatch( - CanvasAction::SuggestionUp, - form_state, - &mut self.ideal_cursor_column, - ).await { - return Ok(Some(result.message().unwrap_or("").to_string())); - } - } - KeyCode::Down => { - if let Ok(result) = ActionDispatcher::dispatch( - CanvasAction::SuggestionDown, - form_state, - &mut self.ideal_cursor_column, - ).await { - return Ok(Some(result.message().unwrap_or("").to_string())); - } - } - KeyCode::Enter => { - if let Ok(result) = ActionDispatcher::dispatch( - CanvasAction::SelectSuggestion, - form_state, - &mut self.ideal_cursor_column, - ).await { - return Ok(Some(result.message().unwrap_or("").to_string())); - } - } - KeyCode::Esc => { - if let Ok(result) = ActionDispatcher::dispatch( - CanvasAction::ExitSuggestions, - form_state, - &mut self.ideal_cursor_column, - ).await { - return Ok(Some(result.message().unwrap_or("").to_string())); - } - } - _ => {} - } - } - + // Get action from config - handles all modes (edit/read-only/suggestions) let action_str = canvas_config.get_action_for_key( key_event.code, key_event.modifiers, @@ -1168,11 +1117,13 @@ impl EventHandler { ); if let Some(action_str) = action_str { + // Skip mode transition actions - let the main event handler deal with them if Self::is_mode_transition_action(action_str) { return Ok(None); } - let canvas_action = CanvasAction::from_string(&action_str); + // Execute the config-mapped action + let canvas_action = CanvasAction::from_string(action_str); match ActionDispatcher::dispatch( canvas_action, form_state, @@ -1187,9 +1138,10 @@ impl EventHandler { } } - // Fallback to automatic key handling for edit mode + // Handle character insertion for edit mode (not in config) if is_edit_mode { - if let Some(canvas_action) = CanvasAction::from_key(key_event.code) { + if let KeyCode::Char(c) = key_event.code { + let canvas_action = CanvasAction::InsertChar(c); match ActionDispatcher::dispatch( canvas_action, form_state, @@ -1199,42 +1151,13 @@ impl EventHandler { return Ok(Some(result.message().unwrap_or("").to_string())); } Err(_) => { - return Ok(Some("Auto action failed".to_string())); - } - } - } - } else { - // In read-only mode, only handle non-character keys - let canvas_action = match key_event.code { - KeyCode::Left => Some(CanvasAction::MoveLeft), - KeyCode::Right => Some(CanvasAction::MoveRight), - KeyCode::Up => Some(CanvasAction::MoveUp), - KeyCode::Down => Some(CanvasAction::MoveDown), - KeyCode::Home => Some(CanvasAction::MoveLineStart), - KeyCode::End => Some(CanvasAction::MoveLineEnd), - KeyCode::Tab => Some(CanvasAction::NextField), - KeyCode::BackTab => Some(CanvasAction::PrevField), - KeyCode::Delete => Some(CanvasAction::DeleteForward), - KeyCode::Backspace => Some(CanvasAction::DeleteBackward), - _ => None, - }; - - if let Some(canvas_action) = canvas_action { - match ActionDispatcher::dispatch( - canvas_action, - form_state, - &mut self.ideal_cursor_column, - ).await { - Ok(result) => { - return Ok(Some(result.message().unwrap_or("").to_string())); - } - Err(_) => { - return Ok(Some("Action failed".to_string())); + return Ok(Some("Character insertion failed".to_string())); } } } } + // No action found Ok(None) }