From 574803988d45bbb670c85d7dfd23a1c947bbb135 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 12:31:21 +0200 Subject: [PATCH 01/18] introspection to generated config now works --- canvas/src/canvas/actions/handlers/edit.rs | 149 ++++++ .../src/canvas/actions/handlers/highlight.rs | 99 ++++ .../src/canvas/actions/handlers/readonly.rs | 129 +++++ canvas/src/config/config.rs | 293 ++++------- canvas/src/config/introspection.rs | 75 +++ canvas/src/config/mod.rs | 2 + canvas/src/config/registry.rs | 472 +++--------------- canvas/src/config/validation.rs | 77 ++- 8 files changed, 679 insertions(+), 617 deletions(-) create mode 100644 canvas/src/config/introspection.rs diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs index cd65e7d..a26fe6f 100644 --- a/canvas/src/canvas/actions/handlers/edit.rs +++ b/canvas/src/canvas/actions/handlers/edit.rs @@ -1,12 +1,15 @@ // src/canvas/actions/handlers/edit.rs use crate::canvas::actions::types::{CanvasAction, ActionResult}; +use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; use crate::config::CanvasConfig; use anyhow::Result; const FOR_EDIT_MODE: bool = true; // Edit mode flag + +pub struct EditHandler; /// Handle actions in edit mode with edit-specific cursor behavior pub async fn handle_edit_action( @@ -201,3 +204,149 @@ pub async fn handle_edit_action( } } } + +impl ActionHandlerIntrospection for EditHandler { + fn introspect() -> HandlerCapabilities { + let mut actions = Vec::new(); + + // REQUIRED ACTIONS - These must be configured for edit mode to work + actions.push(ActionSpec { + name: "move_left".to_string(), + description: "Move cursor one position to the left".to_string(), + examples: vec!["Left".to_string(), "h".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_right".to_string(), + description: "Move cursor one position to the right".to_string(), + examples: vec!["Right".to_string(), "l".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_up".to_string(), + description: "Move to previous field or line".to_string(), + examples: vec!["Up".to_string(), "k".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_down".to_string(), + description: "Move to next field or line".to_string(), + examples: vec!["Down".to_string(), "j".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "delete_char_backward".to_string(), + description: "Delete character before cursor".to_string(), + examples: vec!["Backspace".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "next_field".to_string(), + description: "Move to next input field".to_string(), + examples: vec!["Tab".to_string(), "Enter".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "prev_field".to_string(), + description: "Move to previous input field".to_string(), + examples: vec!["Shift+Tab".to_string()], + is_required: true, + }); + + // OPTIONAL ACTIONS - These enhance functionality but aren't required + actions.push(ActionSpec { + name: "move_word_next".to_string(), + description: "Move cursor to start of next word".to_string(), + examples: vec!["Ctrl+Right".to_string(), "w".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_prev".to_string(), + description: "Move cursor to start of previous word".to_string(), + examples: vec!["Ctrl+Left".to_string(), "b".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_end".to_string(), + description: "Move cursor to end of current/next word".to_string(), + examples: vec!["e".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_end_prev".to_string(), + description: "Move cursor to end of previous word".to_string(), + examples: vec!["ge".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_line_start".to_string(), + description: "Move cursor to beginning of line".to_string(), + examples: vec!["Home".to_string(), "0".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_line_end".to_string(), + description: "Move cursor to end of line".to_string(), + examples: vec!["End".to_string(), "$".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_first_line".to_string(), + description: "Move to first field".to_string(), + examples: vec!["Ctrl+Home".to_string(), "gg".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_last_line".to_string(), + description: "Move to last field".to_string(), + examples: vec!["Ctrl+End".to_string(), "G".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "delete_char_forward".to_string(), + description: "Delete character after cursor".to_string(), + examples: vec!["Delete".to_string()], + is_required: false, + }); + + HandlerCapabilities { + mode_name: "edit".to_string(), + actions, + auto_handled: vec![ + "insert_char".to_string(), // Any printable character + ], + } + } + + fn validate_capabilities() -> Result<(), String> { + // TODO: Could add runtime validation that the handler actually + // implements all the actions it claims to support + + // For now, just validate that we have the essential actions + let caps = Self::introspect(); + let required_count = caps.actions.iter().filter(|a| a.is_required).count(); + + if required_count < 7 { // We expect at least 7 required actions + return Err(format!( + "Edit handler claims only {} required actions, expected at least 7", + required_count + )); + } + + Ok(()) + } +} diff --git a/canvas/src/canvas/actions/handlers/highlight.rs b/canvas/src/canvas/actions/handlers/highlight.rs index 1c850e5..7227443 100644 --- a/canvas/src/canvas/actions/handlers/highlight.rs +++ b/canvas/src/canvas/actions/handlers/highlight.rs @@ -1,12 +1,16 @@ // src/canvas/actions/handlers/highlight.rs use crate::canvas::actions::types::{CanvasAction, ActionResult}; +use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; + use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; use crate::config::CanvasConfig; use anyhow::Result; const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior + +pub struct HighlightHandler; /// Handle actions in highlight/visual mode /// TODO: Implement selection logic and highlight-specific behaviors @@ -104,3 +108,98 @@ pub async fn handle_highlight_action( } } } + +impl ActionHandlerIntrospection for HighlightHandler { + fn introspect() -> HandlerCapabilities { + let mut actions = Vec::new(); + + // For now, highlight mode uses similar movement to readonly + // but this will be discovered from actual implementation + + // REQUIRED ACTIONS - Basic movement in highlight mode + actions.push(ActionSpec { + name: "move_left".to_string(), + description: "Move cursor left and extend selection".to_string(), + examples: vec!["h".to_string(), "Left".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_right".to_string(), + description: "Move cursor right and extend selection".to_string(), + examples: vec!["l".to_string(), "Right".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_up".to_string(), + description: "Move up and extend selection".to_string(), + examples: vec!["k".to_string(), "Up".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_down".to_string(), + description: "Move down and extend selection".to_string(), + examples: vec!["j".to_string(), "Down".to_string()], + is_required: true, + }); + + // OPTIONAL ACTIONS - Advanced highlight movement + actions.push(ActionSpec { + name: "move_word_next".to_string(), + description: "Move to next word and extend selection".to_string(), + examples: vec!["w".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_end".to_string(), + description: "Move to word end and extend selection".to_string(), + examples: vec!["e".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_prev".to_string(), + description: "Move to previous word and extend selection".to_string(), + examples: vec!["b".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_line_start".to_string(), + description: "Move to line start and extend selection".to_string(), + examples: vec!["0".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_line_end".to_string(), + description: "Move to line end and extend selection".to_string(), + examples: vec!["$".to_string()], + is_required: false, + }); + + HandlerCapabilities { + mode_name: "highlight".to_string(), + actions, + auto_handled: vec![], // Highlight mode has no auto-handled actions + } + } + + fn validate_capabilities() -> Result<(), String> { + let caps = Self::introspect(); + let required_count = caps.actions.iter().filter(|a| a.is_required).count(); + + if required_count < 4 { // We expect at least 4 required actions (basic movement) + return Err(format!( + "Highlight handler claims only {} required actions, expected at least 4", + required_count + )); + } + + Ok(()) + } +} + diff --git a/canvas/src/canvas/actions/handlers/readonly.rs b/canvas/src/canvas/actions/handlers/readonly.rs index d7d6c4d..2c3817a 100644 --- a/canvas/src/canvas/actions/handlers/readonly.rs +++ b/canvas/src/canvas/actions/handlers/readonly.rs @@ -1,6 +1,7 @@ // src/canvas/actions/handlers/readonly.rs use crate::canvas::actions::types::{CanvasAction, ActionResult}; +use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; use crate::config::CanvasConfig; @@ -191,3 +192,131 @@ pub async fn handle_readonly_action( } } } + +pub struct ReadOnlyHandler; + +impl ActionHandlerIntrospection for ReadOnlyHandler { + fn introspect() -> HandlerCapabilities { + let mut actions = Vec::new(); + + // REQUIRED ACTIONS - Navigation is essential in read-only mode + actions.push(ActionSpec { + name: "move_left".to_string(), + description: "Move cursor one position to the left".to_string(), + examples: vec!["h".to_string(), "Left".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_right".to_string(), + description: "Move cursor one position to the right".to_string(), + examples: vec!["l".to_string(), "Right".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_up".to_string(), + description: "Move to previous field".to_string(), + examples: vec!["k".to_string(), "Up".to_string()], + is_required: true, + }); + + actions.push(ActionSpec { + name: "move_down".to_string(), + description: "Move to next field".to_string(), + examples: vec!["j".to_string(), "Down".to_string()], + is_required: true, + }); + + // OPTIONAL ACTIONS - Advanced navigation features + actions.push(ActionSpec { + name: "move_word_next".to_string(), + description: "Move cursor to start of next word".to_string(), + examples: vec!["w".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_prev".to_string(), + description: "Move cursor to start of previous word".to_string(), + examples: vec!["b".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_end".to_string(), + description: "Move cursor to end of current/next word".to_string(), + examples: vec!["e".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_word_end_prev".to_string(), + description: "Move cursor to end of previous word".to_string(), + examples: vec!["ge".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_line_start".to_string(), + description: "Move cursor to beginning of line".to_string(), + examples: vec!["0".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_line_end".to_string(), + description: "Move cursor to end of line".to_string(), + examples: vec!["$".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_first_line".to_string(), + description: "Move to first field".to_string(), + examples: vec!["gg".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "move_last_line".to_string(), + description: "Move to last field".to_string(), + examples: vec!["G".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "next_field".to_string(), + description: "Move to next input field".to_string(), + examples: vec!["Tab".to_string()], + is_required: false, + }); + + actions.push(ActionSpec { + name: "prev_field".to_string(), + description: "Move to previous input field".to_string(), + examples: vec!["Shift+Tab".to_string()], + is_required: false, + }); + + HandlerCapabilities { + mode_name: "read_only".to_string(), + actions, + auto_handled: vec![], // Read-only mode has no auto-handled actions + } + } + + fn validate_capabilities() -> Result<(), String> { + let caps = Self::introspect(); + let required_count = caps.actions.iter().filter(|a| a.is_required).count(); + + if required_count < 4 { // We expect at least 4 required actions (basic movement) + return Err(format!( + "ReadOnly handler claims only {} required actions, expected at least 4", + required_count + )); + } + + Ok(()) + } +} diff --git a/canvas/src/config/config.rs b/canvas/src/config/config.rs index f020b6d..b07e517 100644 --- a/canvas/src/config/config.rs +++ b/canvas/src/config/config.rs @@ -1,86 +1,73 @@ -// canvas/src/config.rs +// src/config/config.rs use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crossterm::event::{KeyCode, KeyModifiers}; use anyhow::{Context, Result}; -use super::registry::{ActionRegistry, ActionSpec, ModeRegistry}; +// Import from sibling modules +use super::registry::ActionRegistry; use super::validation::{ConfigValidator, ValidationError, ValidationResult, ValidationWarning}; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasConfig { - #[serde(default)] - pub keybindings: CanvasKeybindings, - #[serde(default)] - pub behavior: CanvasBehavior, - #[serde(default)] - pub appearance: CanvasAppearance, +pub struct CanvasKeybindings { + pub edit: HashMap>, + pub read_only: HashMap>, + pub global: HashMap>, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct CanvasKeybindings { - #[serde(default)] - pub read_only: HashMap>, - #[serde(default)] - pub edit: HashMap>, - #[serde(default)] - pub suggestions: HashMap>, - #[serde(default)] - pub global: HashMap>, +impl Default for CanvasKeybindings { + fn default() -> Self { + Self { + edit: HashMap::new(), + read_only: HashMap::new(), + global: HashMap::new(), + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CanvasBehavior { - #[serde(default = "default_wrap_around")] + pub confirm_on_save: bool, + pub auto_indent: bool, + pub wrap_search: bool, pub wrap_around_fields: bool, - #[serde(default = "default_auto_save")] - pub auto_save_on_field_change: bool, - #[serde(default = "default_word_chars")] - pub word_chars: String, - #[serde(default = "default_suggestion_limit")] - pub max_suggestions: usize, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasAppearance { - #[serde(default = "default_cursor_style")] - pub cursor_style: String, // "block", "bar", "underline" - #[serde(default = "default_show_field_numbers")] - pub show_field_numbers: bool, - #[serde(default = "default_highlight_current_field")] - pub highlight_current_field: bool, -} - -// Default values -fn default_wrap_around() -> bool { true } -fn default_auto_save() -> bool { false } -fn default_word_chars() -> String { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".to_string() } -fn default_suggestion_limit() -> usize { 10 } -fn default_cursor_style() -> String { "block".to_string() } -fn default_show_field_numbers() -> bool { false } -fn default_highlight_current_field() -> bool { true } - impl Default for CanvasBehavior { fn default() -> Self { Self { - wrap_around_fields: default_wrap_around(), - auto_save_on_field_change: default_auto_save(), - word_chars: default_word_chars(), - max_suggestions: default_suggestion_limit(), + confirm_on_save: true, + auto_indent: true, + wrap_search: true, + wrap_around_fields: true, } } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CanvasAppearance { + pub line_numbers: bool, + pub syntax_highlighting: bool, + pub current_line_highlight: bool, +} + impl Default for CanvasAppearance { fn default() -> Self { Self { - cursor_style: default_cursor_style(), - show_field_numbers: default_show_field_numbers(), - highlight_current_field: default_highlight_current_field(), + line_numbers: true, + syntax_highlighting: true, + current_line_highlight: true, } } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CanvasConfig { + pub keybindings: CanvasKeybindings, + pub behavior: CanvasBehavior, + pub appearance: CanvasAppearance, +} + impl Default for CanvasConfig { fn default() -> Self { Self { @@ -93,6 +80,7 @@ impl Default for CanvasConfig { impl CanvasKeybindings { pub fn with_vim_defaults() -> Self { + // TODO: Could be generated from introspection too let mut keybindings = Self::default(); // Read-only mode (vim-style navigation) @@ -100,171 +88,148 @@ impl CanvasKeybindings { 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()]); - keybindings.read_only.insert("move_word_next".to_string(), vec!["w".to_string()]); - keybindings.read_only.insert("move_word_end".to_string(), vec!["e".to_string()]); - keybindings.read_only.insert("move_word_prev".to_string(), vec!["b".to_string()]); - keybindings.read_only.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]); - keybindings.read_only.insert("move_line_start".to_string(), vec!["0".to_string()]); - keybindings.read_only.insert("move_line_end".to_string(), vec!["$".to_string()]); - keybindings.read_only.insert("move_first_line".to_string(), vec!["gg".to_string()]); - keybindings.read_only.insert("move_last_line".to_string(), vec!["G".to_string()]); - keybindings.read_only.insert("next_field".to_string(), vec!["Tab".to_string()]); - keybindings.read_only.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]); // Edit mode keybindings.edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]); - keybindings.edit.insert("delete_char_forward".to_string(), vec!["Delete".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("move_line_start".to_string(), vec!["Home".to_string()]); - keybindings.edit.insert("move_line_end".to_string(), vec!["End".to_string()]); - keybindings.edit.insert("move_word_next".to_string(), vec!["Ctrl+Right".to_string()]); - keybindings.edit.insert("move_word_prev".to_string(), vec!["Ctrl+Left".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()]); - // Suggestions - keybindings.suggestions.insert("suggestion_up".to_string(), vec!["Up".to_string(), "Ctrl+p".to_string()]); - keybindings.suggestions.insert("suggestion_down".to_string(), vec!["Down".to_string(), "Ctrl+n".to_string()]); - keybindings.suggestions.insert("select_suggestion".to_string(), vec!["Enter".to_string(), "Tab".to_string()]); - keybindings.suggestions.insert("exit_suggestions".to_string(), vec!["Esc".to_string()]); - - // Global (works in both modes) - keybindings.global.insert("move_up".to_string(), vec!["Up".to_string()]); - keybindings.global.insert("move_down".to_string(), vec!["Down".to_string()]); - - keybindings - } - - pub fn with_emacs_defaults() -> Self { - let mut keybindings = Self::default(); - - // Emacs-style bindings - keybindings.read_only.insert("move_left".to_string(), vec!["Ctrl+b".to_string()]); - keybindings.read_only.insert("move_right".to_string(), vec!["Ctrl+f".to_string()]); - keybindings.read_only.insert("move_up".to_string(), vec!["Ctrl+p".to_string()]); - keybindings.read_only.insert("move_down".to_string(), vec!["Ctrl+n".to_string()]); - keybindings.read_only.insert("move_word_next".to_string(), vec!["Alt+f".to_string()]); - keybindings.read_only.insert("move_word_prev".to_string(), vec!["Alt+b".to_string()]); - keybindings.read_only.insert("move_line_start".to_string(), vec!["Ctrl+a".to_string()]); - keybindings.read_only.insert("move_line_end".to_string(), vec!["Ctrl+e".to_string()]); - - keybindings.edit.insert("delete_char_backward".to_string(), vec!["Ctrl+h".to_string(), "Backspace".to_string()]); - keybindings.edit.insert("delete_char_forward".to_string(), vec!["Ctrl+d".to_string(), "Delete".to_string()]); - keybindings } } impl CanvasConfig { - /// NEW: Load and validate configuration + /// NEW: Load and validate configuration using dynamic registry pub fn load() -> Self { match Self::load_and_validate() { Ok(config) => config, Err(e) => { - eprintln!("⚠️ Canvas config validation failed: {}", e); - eprintln!(" Using vim defaults. Run CanvasConfig::generate_template() for help."); + eprintln!("⚠️ Failed to load canvas config: {}", e); + eprintln!(" Using default configuration"); Self::default() } } } - /// NEW: Load configuration with validation + /// NEW: Load configuration with validation using dynamic registry 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 { - // Fallback to vim defaults + // Use default if file doesn't exist Self::default() }; - // Validate the configuration - let validator = ConfigValidator::new(); + // NEW: Use dynamic registry from actual handlers + let registry = ActionRegistry::from_handlers(); + + // 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); + } + } + + // Validate the configuration against the dynamic registry + let validator = ConfigValidator::new(registry); let validation_result = validator.validate_keybindings(&config.keybindings); if !validation_result.is_valid { - // Print validation errors + eprintln!("❌ Canvas configuration validation failed:"); validator.print_validation_result(&validation_result); - - // Create error with suggestions - let error_msg = format!( - "Configuration validation failed with {} errors", - validation_result.errors.len() - ); - return Err(anyhow::anyhow!(error_msg)); - } - - // Print warnings if any - if !validation_result.warnings.is_empty() { + 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); } Ok(config) } - /// NEW: Generate a complete configuration template + /// NEW: Generate template from actual handler capabilities pub fn generate_template() -> String { - let registry = ActionRegistry::new(); + let registry = ActionRegistry::from_handlers(); + + // 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); + } + } + registry.generate_config_template() } - /// NEW: Generate a clean, minimal configuration template + /// NEW: Generate clean template from actual handler capabilities pub fn generate_clean_template() -> String { - let registry = ActionRegistry::new(); + let registry = ActionRegistry::from_handlers(); + + // 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); + } + } + registry.generate_clean_template() } - /// NEW: Validate current configuration + /// NEW: Validate current configuration against actual implementation pub fn validate(&self) -> ValidationResult { - let validator = ConfigValidator::new(); + let registry = ActionRegistry::from_handlers(); + let validator = ConfigValidator::new(registry); validator.validate_keybindings(&self.keybindings) } /// NEW: Print validation results for current config pub fn print_validation(&self) { - let validator = ConfigValidator::new(); + let registry = ActionRegistry::from_handlers(); + let validator = ConfigValidator::new(registry); let result = validator.validate_keybindings(&self.keybindings); validator.print_validation_result(&result); } - /// NEW: Generate config for missing required actions - pub fn generate_missing_config(&self) -> String { - let validator = ConfigValidator::new(); - validator.generate_missing_config(&self.keybindings) - } - /// Load from TOML string pub fn from_toml(toml_str: &str) -> Result { toml::from_str(toml_str) - .with_context(|| "Failed to parse canvas config TOML") + .context("Failed to parse TOML configuration") } /// Load from file pub fn from_file(path: &std::path::Path) -> Result { let contents = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read config file: {:?}", path))?; + .context("Failed to read config file")?; Self::from_toml(&contents) } - /// NEW: Check if autocomplete should auto-trigger (simple logic) + /// RESTORED: 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() } - /// NEW: Check if user has configured manual trigger keybinding + /// RESTORED: 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") } - // ... rest of your existing methods stay the same ... - + // ... 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) @@ -277,21 +242,9 @@ impl CanvasConfig { .or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers)) } - /// Get action for key in suggestions mode - pub fn get_suggestion_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { - self.get_action_in_mode(&self.keybindings.suggestions, key, modifiers) - } - /// Get action for key (mode-aware) - pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers, is_edit_mode: bool, has_suggestions: bool) -> Option<&str> { - // Suggestions take priority when active - if has_suggestions { - if let Some(action) = self.get_suggestion_action(key, modifiers) { - return Some(action); - } - } - - // Then check mode-specific + pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers, is_edit_mode: bool, _has_suggestions: bool) -> Option<&str> { + // Check mode-specific if is_edit_mode { self.get_edit_action(key, modifiers) } else { @@ -299,7 +252,6 @@ impl CanvasConfig { } } - // ... keep all your existing private methods ... fn get_action_in_mode<'a>(&self, mode_bindings: &'a HashMap>, key: KeyCode, modifiers: KeyModifiers) -> Option<&'a str> { for (action, bindings) in mode_bindings { for binding in bindings { @@ -311,53 +263,26 @@ impl CanvasConfig { None } - fn matches_keybinding(&self, binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool { - // ... keep all your existing key matching logic ... - // (This is a very long method, so I'm just indicating to keep it as-is) - - // Your existing implementation here... - true // placeholder - use your actual implementation + fn matches_keybinding(&self, _binding: &str, _key: KeyCode, _modifiers: KeyModifiers) -> bool { + // Keep your existing implementation - this is just a placeholder + true } - /// Convenience method to create vim preset - pub fn vim_preset() -> Self { - Self { - keybindings: CanvasKeybindings::with_vim_defaults(), - behavior: CanvasBehavior::default(), - appearance: CanvasAppearance::default(), - } - } - - /// Convenience method to create emacs preset - pub fn emacs_preset() -> Self { - Self { - keybindings: CanvasKeybindings::with_emacs_defaults(), - behavior: CanvasBehavior::default(), - appearance: CanvasAppearance::default(), - } - } - - /// Debug method to print loaded keybindings + /// 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()); - println!(" Suggestions: {} actions", self.keybindings.suggestions.len()); - println!(" Global: {} actions", self.keybindings.global.len()); - - // NEW: Show validation status + + // NEW: Show validation status against actual implementation let validation = self.validate(); if validation.is_valid { - println!(" ✅ Configuration is valid"); + println!(" ✅ Configuration matches actual implementation"); } else { - println!(" ❌ Configuration has {} errors", validation.errors.len()); + println!(" ❌ Configuration has {} errors vs implementation", validation.errors.len()); } if !validation.warnings.is_empty() { println!(" ⚠️ Configuration has {} warnings", validation.warnings.len()); } } } - -// Re-export for convenience -pub use crate::canvas::actions::CanvasAction; -pub use crate::dispatcher::ActionDispatcher; diff --git a/canvas/src/config/introspection.rs b/canvas/src/config/introspection.rs new file mode 100644 index 0000000..052a2e6 --- /dev/null +++ b/canvas/src/config/introspection.rs @@ -0,0 +1,75 @@ +// src/config/introspection.rs + +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct ActionSpec { + pub name: String, + pub description: String, + pub examples: Vec, + pub is_required: bool, +} + +#[derive(Debug, Clone)] +pub struct HandlerCapabilities { + pub mode_name: String, + pub actions: Vec, + pub auto_handled: Vec, // Actions handled automatically (like insert_char) +} + +/// Trait that each handler implements to report its capabilities +pub trait ActionHandlerIntrospection { + /// Return the capabilities of this handler + fn introspect() -> HandlerCapabilities; + + /// Validate that this handler actually supports the claimed actions + fn validate_capabilities() -> Result<(), String> { + // Default implementation - handlers can override for custom validation + Ok(()) + } +} + +/// System that discovers all handler capabilities +pub struct HandlerDiscovery; + +impl HandlerDiscovery { + /// Discover all handler capabilities by calling their introspect methods + pub fn discover_all() -> HashMap { + let mut capabilities = HashMap::new(); + + // Import and introspect each handler + let edit_caps = crate::canvas::actions::handlers::edit::EditHandler::introspect(); + capabilities.insert("edit".to_string(), edit_caps); + + let readonly_caps = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::introspect(); + capabilities.insert("read_only".to_string(), readonly_caps); + + let highlight_caps = crate::canvas::actions::handlers::highlight::HighlightHandler::introspect(); + capabilities.insert("highlight".to_string(), highlight_caps); + + capabilities + } + + /// Validate that all handlers actually support their claimed actions + pub fn validate_all_handlers() -> Result<(), Vec> { + let mut errors = Vec::new(); + + if let Err(e) = crate::canvas::actions::handlers::edit::EditHandler::validate_capabilities() { + errors.push(format!("Edit handler: {}", e)); + } + + if let Err(e) = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::validate_capabilities() { + errors.push(format!("ReadOnly handler: {}", e)); + } + + if let Err(e) = crate::canvas::actions::handlers::highlight::HighlightHandler::validate_capabilities() { + errors.push(format!("Highlight handler: {}", e)); + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} diff --git a/canvas/src/config/mod.rs b/canvas/src/config/mod.rs index 961bac2..d9afd7a 100644 --- a/canvas/src/config/mod.rs +++ b/canvas/src/config/mod.rs @@ -3,8 +3,10 @@ mod registry; mod config; mod validation; +pub mod introspection; // Re-export everything from the main config module pub use registry::*; pub use validation::*; pub use config::*; +pub use introspection::*; diff --git a/canvas/src/config/registry.rs b/canvas/src/config/registry.rs index dcf442d..7070149 100644 --- a/canvas/src/config/registry.rs +++ b/canvas/src/config/registry.rs @@ -1,357 +1,67 @@ // src/config/registry.rs use std::collections::HashMap; -use crate::canvas::modes::AppMode; - -#[derive(Debug, Clone)] -pub struct ActionSpec { - pub name: String, - pub description: String, - pub examples: Vec, - pub mode_specific: bool, // true if different behavior per mode -} +use crate::config::introspection::{HandlerDiscovery, ActionSpec, HandlerCapabilities}; #[derive(Debug, Clone)] pub struct ModeRegistry { pub required: HashMap, pub optional: HashMap, - pub auto_handled: Vec, // Never appear in config + pub auto_handled: Vec, } #[derive(Debug, Clone)] pub struct ActionRegistry { - pub edit_mode: ModeRegistry, - pub readonly_mode: ModeRegistry, - pub suggestions: ModeRegistry, - pub global: ModeRegistry, + pub modes: HashMap, } impl ActionRegistry { - pub fn new() -> Self { - Self { - edit_mode: Self::edit_mode_registry(), - readonly_mode: Self::readonly_mode_registry(), - suggestions: Self::suggestions_registry(), - global: Self::global_registry(), - } - } - - fn edit_mode_registry() -> ModeRegistry { - let mut required = HashMap::new(); - let mut optional = HashMap::new(); - - // REQUIRED - These MUST be configured - required.insert("move_left".to_string(), ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["Left".to_string(), "h".to_string()], - mode_specific: false, - }); + /// NEW: Create registry by discovering actual handler capabilities + pub fn from_handlers() -> Self { + let handler_capabilities = HandlerDiscovery::discover_all(); + let mut modes = HashMap::new(); - required.insert("move_right".to_string(), ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["Right".to_string(), "l".to_string()], - mode_specific: false, - }); - - required.insert("move_up".to_string(), ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field or line".to_string(), - examples: vec!["Up".to_string(), "k".to_string()], - mode_specific: false, - }); - - required.insert("move_down".to_string(), ActionSpec { - name: "move_down".to_string(), - description: "Move to next field or line".to_string(), - examples: vec!["Down".to_string(), "j".to_string()], - mode_specific: false, - }); - - required.insert("delete_char_backward".to_string(), ActionSpec { - name: "delete_char_backward".to_string(), - description: "Delete character before cursor".to_string(), - examples: vec!["Backspace".to_string()], - mode_specific: false, - }); - - required.insert("next_field".to_string(), ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string(), "Enter".to_string()], - mode_specific: false, - }); - - required.insert("prev_field".to_string(), ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - mode_specific: false, - }); - - // OPTIONAL - These can be configured or omitted - optional.insert("move_word_next".to_string(), ActionSpec { - name: "move_word_next".to_string(), - description: "Move cursor to start of next word".to_string(), - examples: vec!["Ctrl+Right".to_string(), "w".to_string()], - mode_specific: false, - }); - - optional.insert("move_word_prev".to_string(), ActionSpec { - name: "move_word_prev".to_string(), - description: "Move cursor to start of previous word".to_string(), - examples: vec!["Ctrl+Left".to_string(), "b".to_string()], - mode_specific: false, - }); - - optional.insert("move_word_end".to_string(), ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - mode_specific: false, - }); - - optional.insert("move_word_end_prev".to_string(), ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - mode_specific: false, - }); - - optional.insert("move_line_start".to_string(), ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["Home".to_string(), "0".to_string()], - mode_specific: false, - }); - - optional.insert("move_line_end".to_string(), ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["End".to_string(), "$".to_string()], - mode_specific: false, - }); - - optional.insert("move_first_line".to_string(), ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["Ctrl+Home".to_string(), "gg".to_string()], - mode_specific: false, - }); - - optional.insert("move_last_line".to_string(), ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["Ctrl+End".to_string(), "G".to_string()], - mode_specific: false, - }); - - optional.insert("delete_char_forward".to_string(), ActionSpec { - name: "delete_char_forward".to_string(), - description: "Delete character after cursor".to_string(), - examples: vec!["Delete".to_string()], - mode_specific: false, - }); - - ModeRegistry { - required, - optional, - auto_handled: vec![ - "insert_char".to_string(), // Any printable character - ], + for (mode_name, capabilities) in handler_capabilities { + let mode_registry = Self::build_mode_registry(capabilities); + modes.insert(mode_name, mode_registry); } - } - - fn readonly_mode_registry() -> ModeRegistry { - let mut required = HashMap::new(); - let mut optional = HashMap::new(); - - // REQUIRED - Navigation is essential in read-only mode - required.insert("move_left".to_string(), ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["h".to_string(), "Left".to_string()], - mode_specific: true, - }); - required.insert("move_right".to_string(), ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["l".to_string(), "Right".to_string()], - mode_specific: true, - }); - - required.insert("move_up".to_string(), ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field".to_string(), - examples: vec!["k".to_string(), "Up".to_string()], - mode_specific: true, - }); - - required.insert("move_down".to_string(), ActionSpec { - name: "move_down".to_string(), - description: "Move to next field".to_string(), - examples: vec!["j".to_string(), "Down".to_string()], - mode_specific: true, - }); - - // OPTIONAL - Advanced navigation - optional.insert("move_word_next".to_string(), ActionSpec { - name: "move_word_next".to_string(), - description: "Move cursor to start of next word".to_string(), - examples: vec!["w".to_string()], - mode_specific: true, - }); - - optional.insert("move_word_prev".to_string(), ActionSpec { - name: "move_word_prev".to_string(), - description: "Move cursor to start of previous word".to_string(), - examples: vec!["b".to_string()], - mode_specific: true, - }); - - optional.insert("move_word_end".to_string(), ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - mode_specific: true, - }); - - optional.insert("move_word_end_prev".to_string(), ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - mode_specific: true, - }); - - optional.insert("move_line_start".to_string(), ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["0".to_string()], - mode_specific: true, - }); - - optional.insert("move_line_end".to_string(), ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["$".to_string()], - mode_specific: true, - }); - - optional.insert("move_first_line".to_string(), ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["gg".to_string()], - mode_specific: true, - }); - - optional.insert("move_last_line".to_string(), ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["G".to_string()], - mode_specific: true, - }); - - optional.insert("next_field".to_string(), ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string()], - mode_specific: true, - }); - - optional.insert("prev_field".to_string(), ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - mode_specific: true, - }); - - ModeRegistry { - required, - optional, - auto_handled: vec![], // Read-only mode has no auto-handled actions - } + Self { modes } } - - fn suggestions_registry() -> ModeRegistry { + + /// Build a mode registry from handler capabilities + fn build_mode_registry(capabilities: HandlerCapabilities) -> ModeRegistry { let mut required = HashMap::new(); - - // REQUIRED - Essential for suggestion navigation - required.insert("suggestion_up".to_string(), ActionSpec { - name: "suggestion_up".to_string(), - description: "Move selection to previous suggestion".to_string(), - examples: vec!["Up".to_string(), "Ctrl+p".to_string()], - mode_specific: false, - }); - - required.insert("suggestion_down".to_string(), ActionSpec { - name: "suggestion_down".to_string(), - description: "Move selection to next suggestion".to_string(), - examples: vec!["Down".to_string(), "Ctrl+n".to_string()], - mode_specific: false, - }); - - required.insert("select_suggestion".to_string(), ActionSpec { - name: "select_suggestion".to_string(), - description: "Select the currently highlighted suggestion".to_string(), - examples: vec!["Enter".to_string(), "Tab".to_string()], - mode_specific: false, - }); - - required.insert("exit_suggestions".to_string(), ActionSpec { - name: "exit_suggestions".to_string(), - description: "Close suggestions without selecting".to_string(), - examples: vec!["Esc".to_string()], - mode_specific: false, - }); - + let mut optional = HashMap::new(); + + for action_spec in capabilities.actions { + if action_spec.is_required { + required.insert(action_spec.name.clone(), action_spec); + } else { + optional.insert(action_spec.name.clone(), action_spec); + } + } + ModeRegistry { required, - optional: HashMap::new(), - auto_handled: vec![], - } - } - - fn global_registry() -> ModeRegistry { - let mut optional = HashMap::new(); - - // OPTIONAL - Global overrides - optional.insert("move_up".to_string(), ActionSpec { - name: "move_up".to_string(), - description: "Global override for up movement".to_string(), - examples: vec!["Up".to_string()], - mode_specific: false, - }); - - optional.insert("move_down".to_string(), ActionSpec { - name: "move_down".to_string(), - description: "Global override for down movement".to_string(), - examples: vec!["Down".to_string()], - mode_specific: false, - }); - - ModeRegistry { - required: HashMap::new(), optional, - auto_handled: vec![], + auto_handled: capabilities.auto_handled, } } - - pub fn get_mode_registry(&self, mode: &str) -> &ModeRegistry { - match mode { - "edit" => &self.edit_mode, - "read_only" => &self.readonly_mode, - "suggestions" => &self.suggestions, - "global" => &self.global, - _ => &self.global, // fallback - } + + /// Validate that the registry matches the actual implementation + pub fn validate_against_implementation(&self) -> Result<(), Vec> { + HandlerDiscovery::validate_all_handlers() + } + + pub fn get_mode_registry(&self, mode: &str) -> Option<&ModeRegistry> { + self.modes.get(mode) } pub fn all_known_actions(&self) -> Vec { let mut actions = Vec::new(); - for registry in [&self.edit_mode, &self.readonly_mode, &self.suggestions, &self.global] { + for registry in self.modes.values() { actions.extend(registry.required.keys().cloned()); actions.extend(registry.optional.keys().cloned()); } @@ -364,39 +74,34 @@ impl ActionRegistry { pub fn generate_config_template(&self) -> String { let mut template = String::new(); template.push_str("# Canvas Library Configuration Template\n"); - template.push_str("# Generated automatically - customize as needed\n\n"); + template.push_str("# Generated automatically from actual handler capabilities\n\n"); - template.push_str("[keybindings.edit]\n"); - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in &self.edit_mode.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n"); - for (name, spec) in &self.edit_mode.optional { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("[keybindings.read_only]\n"); - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in &self.readonly_mode.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n"); - for (name, spec) in &self.readonly_mode.optional { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples)); - } - - template.push_str("[keybindings.suggestions]\n"); - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in &self.suggestions.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); + for (mode_name, registry) in &self.modes { + template.push_str(&format!("[keybindings.{}]\n", mode_name)); + + if !registry.required.is_empty() { + template.push_str("# REQUIRED ACTIONS - These must be configured\n"); + for (name, spec) in ®istry.required { + template.push_str(&format!("# {}\n", spec.description)); + template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); + } + } + + if !registry.optional.is_empty() { + template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n"); + for (name, spec) in ®istry.optional { + template.push_str(&format!("# {}\n", spec.description)); + template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples)); + } + } + + if !registry.auto_handled.is_empty() { + template.push_str("# AUTO-HANDLED - These are handled automatically, don't configure:\n"); + for auto_action in ®istry.auto_handled { + template.push_str(&format!("# {} (automatic)\n", auto_action)); + } + template.push('\n'); + } } template @@ -405,45 +110,24 @@ impl ActionRegistry { pub fn generate_clean_template(&self) -> String { let mut template = String::new(); - // Edit Mode - template.push_str("[keybindings.edit]\n"); - template.push_str("# Required\n"); - for (name, spec) in &self.edit_mode.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push_str("# Optional\n"); - for (name, spec) in &self.edit_mode.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push('\n'); - - // Read-Only Mode - template.push_str("[keybindings.read_only]\n"); - template.push_str("# Required\n"); - for (name, spec) in &self.readonly_mode.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push_str("# Optional\n"); - for (name, spec) in &self.readonly_mode.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push('\n'); - - // Suggestions Mode - template.push_str("[keybindings.suggestions]\n"); - template.push_str("# Required\n"); - for (name, spec) in &self.suggestions.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - template.push('\n'); - - // Global (all optional) - if !self.global.optional.is_empty() { - template.push_str("[keybindings.global]\n"); - template.push_str("# Optional\n"); - for (name, spec) in &self.global.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); + for (mode_name, registry) in &self.modes { + template.push_str(&format!("[keybindings.{}]\n", mode_name)); + + if !registry.required.is_empty() { + template.push_str("# Required\n"); + for (name, spec) in ®istry.required { + template.push_str(&format!("{} = {:?}\n", name, spec.examples)); + } } + + if !registry.optional.is_empty() { + template.push_str("# Optional\n"); + for (name, spec) in ®istry.optional { + template.push_str(&format!("{} = {:?}\n", name, spec.examples)); + } + } + + template.push('\n'); } template diff --git a/canvas/src/config/validation.rs b/canvas/src/config/validation.rs index e4be3c0..4673d19 100644 --- a/canvas/src/config/validation.rs +++ b/canvas/src/config/validation.rs @@ -8,19 +8,19 @@ use crate::config::CanvasKeybindings; #[derive(Error, Debug)] pub enum ValidationError { #[error("Missing required action '{action}' in {mode} mode")] - MissingRequired { - action: String, + MissingRequired { + action: String, mode: String, suggestion: String, }, - + #[error("Unknown action '{action}' in {mode} mode")] - UnknownAction { - action: String, + UnknownAction { + action: String, mode: String, similar: Vec, }, - + #[error("Multiple validation errors")] Multiple(Vec), } @@ -70,47 +70,46 @@ pub struct ConfigValidator { } impl ConfigValidator { - pub fn new() -> Self { + // FIXED: Accept registry parameter to match config.rs calls + pub fn new(registry: ActionRegistry) -> Self { Self { - registry: ActionRegistry::new(), + registry, } } pub fn validate_keybindings(&self, keybindings: &CanvasKeybindings) -> ValidationResult { let mut result = ValidationResult::new(); - // Validate each mode - result.merge(self.validate_mode_bindings( - "edit", - &keybindings.edit, - self.registry.get_mode_registry("edit") - )); + // Validate each mode that exists in the registry + if let Some(edit_registry) = self.registry.get_mode_registry("edit") { + result.merge(self.validate_mode_bindings( + "edit", + &keybindings.edit, + edit_registry + )); + } - result.merge(self.validate_mode_bindings( - "read_only", - &keybindings.read_only, - self.registry.get_mode_registry("read_only") - )); + if let Some(readonly_registry) = self.registry.get_mode_registry("read_only") { + result.merge(self.validate_mode_bindings( + "read_only", + &keybindings.read_only, + readonly_registry + )); + } - result.merge(self.validate_mode_bindings( - "suggestions", - &keybindings.suggestions, - self.registry.get_mode_registry("suggestions") - )); + // Skip suggestions mode if not discovered by introspection + // (autocomplete is separate concern as requested) - result.merge(self.validate_mode_bindings( - "global", - &keybindings.global, - self.registry.get_mode_registry("global") - )); + // Skip global mode if not discovered by introspection + // (can be added later if needed) result } fn validate_mode_bindings( - &self, - mode_name: &str, - bindings: &HashMap>, + &self, + mode_name: &str, + bindings: &HashMap>, registry: &ModeRegistry ) -> ValidationResult { let mut result = ValidationResult::new(); @@ -122,8 +121,8 @@ impl ConfigValidator { action: action_name.clone(), mode: mode_name.to_string(), suggestion: format!( - "Add to config: {} = {:?}", - action_name, + "Add to config: {} = {:?}", + action_name, spec.examples ), }); @@ -151,7 +150,7 @@ impl ConfigValidator { if key_list.is_empty() { result.add_warning(ValidationWarning { message: format!( - "Action '{}' in {} mode has empty keybinding list", + "Action '{}' in {} mode has empty keybinding list", action_name, mode_name ), suggestion: Some(format!( @@ -166,11 +165,11 @@ impl ConfigValidator { if bindings.contains_key(auto_action) { result.add_warning(ValidationWarning { message: format!( - "Action '{}' in {} mode is auto-handled and shouldn't be in config", + "Action '{}' in {} mode is auto-handled and shouldn't be in config", auto_action, mode_name ), suggestion: Some(format!( - "Remove '{}' from config - it's handled automatically", + "Remove '{}' from config - it's handled automatically", auto_action )), }); @@ -182,7 +181,7 @@ impl ConfigValidator { fn find_similar_actions(&self, action: &str, known_actions: &std::collections::HashSet<&String>) -> Vec { let mut similar = Vec::new(); - + for known in known_actions { if self.is_similar(action, known) { similar.push(known.to_string()); @@ -198,7 +197,7 @@ impl ConfigValidator { // Simple similarity check - could be improved with proper edit distance let a_lower = a.to_lowercase(); let b_lower = b.to_lowercase(); - + // Check if one contains the other if a_lower.contains(&b_lower) || b_lower.contains(&a_lower) { return true; From c3441647e06b47e119998a316a1f9c65374f0e41 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 13:18:27 +0200 Subject: [PATCH 02/18] docs and config adjustement --- canvas/docs/new_function_to_config.txt | 77 +++++++++++++++++++++ client/canvas_config.toml | 94 ++++++++++++-------------- client/config.toml | 61 ++++++++++++++++- 3 files changed, 179 insertions(+), 53 deletions(-) create mode 100644 canvas/docs/new_function_to_config.txt diff --git a/canvas/docs/new_function_to_config.txt b/canvas/docs/new_function_to_config.txt new file mode 100644 index 0000000..92aa4ad --- /dev/null +++ b/canvas/docs/new_function_to_config.txt @@ -0,0 +1,77 @@ +❯ git status +On branch main +Your branch is ahead of 'origin/main' by 1 commit. + (use "git push" to publish your local commits) + +Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git restore ..." to discard changes in working directory) + modified: src/canvas/actions/handlers/edit.rs + modified: src/canvas/actions/types.rs + +no changes added to commit (use "git add" and/or "git commit -a") +❯ git --no-pager diff +diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs +index a26fe6f..fa1becb 100644 +--- a/canvas/src/canvas/actions/handlers/edit.rs ++++ b/canvas/src/canvas/actions/handlers/edit.rs +@@ -29,6 +29,21 @@ pub async fn handle_edit_action( + Ok(ActionResult::success()) + } + ++ CanvasAction::SelectAll => { ++ // Select all text in current field ++ let current_input = state.get_current_input(); ++ let text_length = current_input.len(); ++ ++ // Set cursor to start and select all ++ state.set_current_cursor_pos(0); ++ // TODO: You'd need to add selection state to CanvasState trait ++ // For now, just move cursor to end to "select" all ++ state.set_current_cursor_pos(text_length); ++ *ideal_cursor_column = text_length; ++ ++ Ok(ActionResult::success_with_message(&format!("Selected all {} characters", text_length))) ++ } ++ + CanvasAction::DeleteBackward => { + let cursor_pos = state.current_cursor_pos(); + if cursor_pos > 0 { +@@ -323,6 +338,13 @@ impl ActionHandlerIntrospection for EditHandler { + is_required: false, + }); + ++ actions.push(ActionSpec { ++ name: "select_all".to_string(), ++ description: "Select all text in current field".to_string(), ++ examples: vec!["Ctrl+a".to_string()], ++ is_required: false, // Optional action ++ }); ++ + HandlerCapabilities { + mode_name: "edit".to_string(), + actions, +diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs +index 433a4d5..3794596 100644 +--- a/canvas/src/canvas/actions/types.rs ++++ b/canvas/src/canvas/actions/types.rs +@@ -31,6 +31,8 @@ pub enum CanvasAction { + NextField, + PrevField, + ++ SelectAll, ++ + // Autocomplete actions + TriggerAutocomplete, + SuggestionUp, +@@ -62,6 +64,7 @@ impl CanvasAction { + "move_word_end_prev" => Self::MoveWordEndPrev, + "next_field" => Self::NextField, + "prev_field" => Self::PrevField, ++ "select_all" => Self::SelectAll, + "trigger_autocomplete" => Self::TriggerAutocomplete, + "suggestion_up" => Self::SuggestionUp, + "suggestion_down" => Self::SuggestionDown, +╭─    ~/Doc/p/komp_ac/canvas  on   main ⇡1 !2  +╰─ + diff --git a/client/canvas_config.toml b/client/canvas_config.toml index 147ad93..09a4966 100644 --- a/client/canvas_config.toml +++ b/client/canvas_config.toml @@ -1,58 +1,52 @@ -# canvas_config.toml - Complete Canvas Configuration - -[behavior] -wrap_around_fields = true -auto_save_on_field_change = false -word_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" -max_suggestions = 6 - -[appearance] -cursor_style = "block" # "block", "bar", "underline" -show_field_numbers = false -highlight_current_field = true - -# Read-only mode keybindings (vim-style) -[keybindings.read_only] -move_left = ["h"] -move_right = ["l"] -move_up = ["k"] -move_down = ["j"] -move_word_next = ["w"] +[keybindings.edit] +# Required +move_right = ["Right", "l"] +delete_char_backward = ["Backspace"] +next_field = ["Tab", "Enter"] +move_up = ["Up", "k"] +move_down = ["Down", "j"] +prev_field = ["Shift+Tab"] +move_left = ["Left", "h"] +# Optional +move_last_line = ["Ctrl+End", "G"] +delete_char_forward = ["Delete"] +move_word_prev = ["Ctrl+Left", "b"] move_word_end = ["e"] -move_word_prev = ["b"] move_word_end_prev = ["ge"] +move_first_line = ["Ctrl+Home", "gg"] +move_word_next = ["Ctrl+Right", "w"] +move_line_start = ["Home", "0"] +move_line_end = ["End", "$"] + +[keybindings.highlight] +# Required +move_left = ["h", "Left"] +move_right = ["l", "Right"] +move_up = ["k", "Up"] +move_down = ["j", "Down"] +# Optional +move_word_next = ["w"] move_line_start = ["0"] move_line_end = ["$"] +move_word_prev = ["b"] +move_word_end = ["e"] + +[keybindings.read_only] +# Required +move_up = ["k", "Up"] +move_left = ["h", "Left"] +move_right = ["l", "Right"] +move_down = ["j", "Down"] +# Optional +move_line_end = ["$"] +move_word_next = ["w"] +next_field = ["Tab"] +move_word_prev = ["b"] +move_word_end = ["e"] +move_last_line = ["G"] +move_word_end_prev = ["ge"] +move_line_start = ["0"] move_first_line = ["gg"] -move_last_line = ["shift+g"] -next_field = ["Tab"] prev_field = ["Shift+Tab"] -# Edit mode keybindings -[keybindings.edit] -delete_char_backward = ["Backspace"] -delete_char_forward = ["Delete"] -move_left = ["Left"] -move_right = ["Right"] -move_up = ["Up"] -move_down = ["Down"] -move_line_start = ["Home"] -move_line_end = ["End"] -move_word_next = ["Ctrl+Right"] -move_word_prev = ["Ctrl+Left"] -next_field = ["Tab"] -prev_field = ["Shift+Tab"] -trigger_autocomplete = ["Ctrl+p"] -# Suggestion/autocomplete keybindings -[keybindings.suggestions] -suggestion_up = ["Up", "Ctrl+p"] -suggestion_down = ["Down", "Ctrl+n"] -select_suggestion = ["Enter", "Tab"] -exit_suggestions = ["Esc"] -trigger_autocomplete = ["Tab"] - -# Global keybindings (work in both modes) -[keybindings.global] -move_up = ["Up"] -move_down = ["Down"] diff --git a/client/config.toml b/client/config.toml index 1a43411..e1479c3 100644 --- a/client/config.toml +++ b/client/config.toml @@ -42,10 +42,42 @@ next_entry = ["right","1"] enter_highlight_mode = ["v"] enter_highlight_mode_linewise = ["ctrl+v"] +### AUTOGENERATED CANVAS CONFIG +# Required +move_up = ["k", "Up"] +move_left = ["h", "Left"] +move_right = ["l", "Right"] +move_down = ["j", "Down"] +# Optional +move_line_end = ["$"] +move_word_next = ["w"] +next_field = ["Tab"] +move_word_prev = ["b"] +move_word_end = ["e"] +move_last_line = ["G"] +move_word_end_prev = ["ge"] +move_line_start = ["0"] +move_first_line = ["gg"] +prev_field = ["Shift+Tab"] + [keybindings.highlight] exit_highlight_mode = ["esc"] enter_highlight_mode_linewise = ["ctrl+v"] +### AUTOGENERATED CANVAS CONFIG +# Required +move_left = ["h", "Left"] +move_right = ["l", "Right"] +move_up = ["k", "Up"] +move_down = ["j", "Down"] +# Optional +move_word_next = ["w"] +move_line_start = ["0"] +move_line_end = ["$"] +move_word_prev = ["b"] +move_word_end = ["e"] + + [keybindings.edit] # BIG CHANGES NOW EXIT HANDLES EITHER IF THOSE # exit_edit_mode = ["esc","ctrl+e"] @@ -53,13 +85,30 @@ enter_highlight_mode_linewise = ["ctrl+v"] # select_suggestion = ["enter"] # next_field = ["enter"] enter_decider = ["enter"] -prev_field = ["shift+enter"] exit = ["esc", "ctrl+e"] -delete_char_forward = ["delete"] -delete_char_backward = ["backspace"] suggestion_down = ["ctrl+n", "tab"] suggestion_up = ["ctrl+p", "shift+tab"] +### AUTOGENERATED CANVAS CONFIG +# Required +move_right = ["Right", "l"] +delete_char_backward = ["Backspace"] +next_field = ["Tab", "Enter"] +move_up = ["Up", "k"] +move_down = ["Down", "j"] +prev_field = ["Shift+Tab"] +move_left = ["Left", "h"] +# Optional +move_last_line = ["Ctrl+End", "G"] +delete_char_forward = ["Delete"] +move_word_prev = ["Ctrl+Left", "b"] +move_word_end = ["e"] +move_word_end_prev = ["ge"] +move_first_line = ["Ctrl+Home", "gg"] +move_word_next = ["Ctrl+Right", "w"] +move_line_start = ["Home", "0"] +move_line_end = ["End", "$"] + [keybindings.command] exit_command_mode = ["ctrl+g", "esc"] command_execute = ["enter"] @@ -77,3 +126,9 @@ keybinding_mode = "vim" # Options: "default", "vim", "emacs" [colors] theme = "dark" # Options: "light", "dark", "high_contrast" + + + + + + From ebe4adaa5d6dec58ab01a1c214cbda3637cd30f3 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 13:39:38 +0200 Subject: [PATCH 03/18] bug is present, i cant type or move in canvas from client --- client/.gitignore | 1 + client/canvas_config.toml | 52 ------------------------------ client/src/modes/handlers/event.rs | 49 ++++++++++++++-------------- 3 files changed, 26 insertions(+), 76 deletions(-) create mode 100644 client/.gitignore delete mode 100644 client/canvas_config.toml diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..24e2a7a --- /dev/null +++ b/client/.gitignore @@ -0,0 +1 @@ +canvas_config.toml.txt diff --git a/client/canvas_config.toml b/client/canvas_config.toml deleted file mode 100644 index 09a4966..0000000 --- a/client/canvas_config.toml +++ /dev/null @@ -1,52 +0,0 @@ -[keybindings.edit] -# Required -move_right = ["Right", "l"] -delete_char_backward = ["Backspace"] -next_field = ["Tab", "Enter"] -move_up = ["Up", "k"] -move_down = ["Down", "j"] -prev_field = ["Shift+Tab"] -move_left = ["Left", "h"] -# Optional -move_last_line = ["Ctrl+End", "G"] -delete_char_forward = ["Delete"] -move_word_prev = ["Ctrl+Left", "b"] -move_word_end = ["e"] -move_word_end_prev = ["ge"] -move_first_line = ["Ctrl+Home", "gg"] -move_word_next = ["Ctrl+Right", "w"] -move_line_start = ["Home", "0"] -move_line_end = ["End", "$"] - -[keybindings.highlight] -# Required -move_left = ["h", "Left"] -move_right = ["l", "Right"] -move_up = ["k", "Up"] -move_down = ["j", "Down"] -# Optional -move_word_next = ["w"] -move_line_start = ["0"] -move_line_end = ["$"] -move_word_prev = ["b"] -move_word_end = ["e"] - -[keybindings.read_only] -# Required -move_up = ["k", "Up"] -move_left = ["h", "Left"] -move_right = ["l", "Right"] -move_down = ["j", "Down"] -# Optional -move_line_end = ["$"] -move_word_next = ["w"] -next_field = ["Tab"] -move_word_prev = ["b"] -move_word_end = ["e"] -move_last_line = ["G"] -move_word_end_prev = ["ge"] -move_line_start = ["0"] -move_first_line = ["gg"] -prev_field = ["Shift+Tab"] - - diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 7e560dc..fadcc6e 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -45,6 +45,7 @@ use crate::ui::handlers::rat_state::UiStateHandler; use anyhow::Result; use common::proto::komp_ac::search::search_response::Hit; use crossterm::cursor::SetCursorStyle; +use crossterm::event::KeyModifiers; use crossterm::event::{Event, KeyCode, KeyEvent}; use tokio::sync::mpsc; use tokio::sync::mpsc::unbounded_channel; @@ -776,7 +777,6 @@ impl EventHandler { if app_state.ui.show_form { if let Ok(Some(canvas_message)) = self.handle_form_canvas_action( key_event, - config, form_state, false, ).await { @@ -866,7 +866,6 @@ impl EventHandler { if app_state.ui.show_form { if let Ok(Some(canvas_message)) = self.handle_form_canvas_action( key_event, - config, form_state, true, ).await { @@ -1102,18 +1101,39 @@ impl EventHandler { async fn handle_form_canvas_action( &mut self, key_event: KeyEvent, - _config: &Config, form_state: &mut FormState, is_edit_mode: bool, ) -> Result> { let canvas_config = canvas::config::CanvasConfig::load(); - // Get action from config - handles all modes (edit/read-only/suggestions) + // PRIORITY 1: Handle character insertion in edit mode FIRST + if is_edit_mode { + if let KeyCode::Char(c) = key_event.code { + // Only insert if it's not a special modifier combination + if key_event.modifiers.is_empty() || key_event.modifiers == KeyModifiers::SHIFT { + let canvas_action = CanvasAction::InsertChar(c); + 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("Character insertion failed".to_string())); + } + } + } + } + } + + // PRIORITY 2: Handle config-mapped actions for non-character keys let action_str = canvas_config.get_action_for_key( key_event.code, key_event.modifiers, is_edit_mode, - form_state.autocomplete_active + form_state.autocomplete_active, ); if let Some(action_str) = action_str { @@ -1138,25 +1158,6 @@ impl EventHandler { } } - // Handle character insertion for edit mode (not in config) - if is_edit_mode { - if let KeyCode::Char(c) = key_event.code { - let canvas_action = CanvasAction::InsertChar(c); - 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("Character insertion failed".to_string())); - } - } - } - } - // No action found Ok(None) } From 5d084bf8223cfcc9089d9817c8395b880c2dcae2 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 14:44:47 +0200 Subject: [PATCH 04/18] fixed working canvas in client, need more fixes now --- canvas/src/autocomplete/actions.rs | 5 - canvas/src/config/config.rs | 251 +++++++++++++++++++++++++---- client/config.toml | 4 +- client/src/modes/canvas/edit.rs | 116 ++++++------- 4 files changed, 280 insertions(+), 96 deletions(-) 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()) } From 59a29aa54b475ad76490cf547dcc54836a8a984c Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 15:07:28 +0200 Subject: [PATCH 05/18] not working example to canvas crate, improving and fixing now --- canvas/Cargo.toml | 4 + canvas/examples/canvas_gui_demo.rs | 316 +++++++++++++++++++++++++++++ canvas/view_docs.sh | 55 +++++ 3 files changed, 375 insertions(+) create mode 100644 canvas/examples/canvas_gui_demo.rs create mode 100755 canvas/view_docs.sh diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 1d70881..9a16fe2 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -29,3 +29,7 @@ tokio-test = "0.4.4" [features] default = [] gui = ["ratatui"] + +[[example]] +name = "ratatui_demo" +path = "examples/ratatui_demo.rs" diff --git a/canvas/examples/canvas_gui_demo.rs b/canvas/examples/canvas_gui_demo.rs new file mode 100644 index 0000000..0f4f492 --- /dev/null +++ b/canvas/examples/canvas_gui_demo.rs @@ -0,0 +1,316 @@ +// examples/canvas_gui_demo.rs + +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Style}, + widgets::{Block, Borders, Clear}, + Frame, Terminal, +}; +use std::{error::Error, io}; + +// Import canvas library components +use canvas::{ + canvas::{ + state::{CanvasState, ActionContext}, + gui::render_canvas, + theme::Theme, + }, + autocomplete::{ + state::AutocompleteCanvasState, + gui::render_autocomplete, + types::{AutocompleteState, SuggestionItem}, + }, + config::config::CanvasConfig, + dispatcher::ActionDispatcher, +}; + +// Example form data structure +#[derive(Debug)] +struct LoginForm { + fields: Vec, + field_labels: Vec, + current_field: usize, + cursor_position: usize, + autocomplete_state: AutocompleteState, +} + +impl LoginForm { + fn new() -> Self { + Self { + fields: vec![ + String::new(), // username + String::new(), // password + String::new(), // email + ], + field_labels: vec![ + "Username".to_string(), + "Password".to_string(), + "Email".to_string(), + ], + current_field: 0, + cursor_position: 0, + autocomplete_state: AutocompleteState::default(), + } + } +} + +// Implement CanvasState trait for your form +impl CanvasState for LoginForm { + fn field_count(&self) -> usize { + self.fields.len() + } + + fn current_field(&self) -> usize { + self.current_field + } + + fn set_current_field(&mut self, field_index: usize) { + if field_index < self.fields.len() { + self.current_field = field_index; + } + } + + fn cursor_position(&self) -> usize { + self.cursor_position + } + + fn set_cursor_position(&mut self, position: usize) { + if let Some(field) = self.fields.get(self.current_field) { + self.cursor_position = position.min(field.len()); + } + } + + fn field_value(&self, field_index: usize) -> Option<&str> { + self.fields.get(field_index).map(|s| s.as_str()) + } + + fn set_field_value(&mut self, field_index: usize, value: String) { + if let Some(field) = self.fields.get_mut(field_index) { + *field = value; + } + } + + fn field_label(&self, field_index: usize) -> Option<&str> { + self.field_labels.get(field_index).map(|s| s.as_str()) + } + + fn handle_action(&mut self, _action: &str, _context: ActionContext) -> Result<(), Box> { + // Custom action handling can go here + Ok(()) + } +} + +// Implement autocomplete support +impl AutocompleteCanvasState for LoginForm { + type SuggestionData = String; + + fn supports_autocomplete(&self, field_index: usize) -> bool { + // Only username and email fields support autocomplete + field_index == 0 || field_index == 2 + } + + fn autocomplete_state(&self) -> &AutocompleteState { + &self.autocomplete_state + } + + fn autocomplete_state_mut(&mut self) -> &mut AutocompleteState { + &mut self.autocomplete_state + } + + fn activate_autocomplete(&mut self) { + if self.supports_autocomplete(self.current_field) { + self.autocomplete_state.activate(self.current_field); + + // Simulate loading suggestions + let suggestions = match self.current_field { + 0 => vec![ // Username suggestions + SuggestionItem::simple("admin"), + SuggestionItem::simple("user"), + SuggestionItem::simple("guest"), + ], + 2 => vec![ // Email suggestions + SuggestionItem::simple("user@example.com"), + SuggestionItem::simple("admin@domain.com"), + SuggestionItem::simple("test@test.org"), + ], + _ => vec![], + }; + + self.autocomplete_state.set_suggestions(suggestions); + } + } + + fn apply_autocomplete_selection(&mut self) { + if let Some(suggestion) = self.autocomplete_state.selected_suggestion() { + self.set_field_value(self.current_field, suggestion.insert_value.clone()); + self.cursor_position = suggestion.insert_value.len(); + self.autocomplete_state.deactivate(); + } + } +} + +// Simple theme implementation +struct SimpleTheme; + +impl Theme for SimpleTheme { + fn field_style(&self, is_current: bool, _is_highlighted: bool) -> Style { + if is_current { + Style::default().bg(Color::DarkGray).fg(Color::White) + } else { + Style::default().fg(Color::Gray) + } + } + + fn label_style(&self, is_current: bool) -> Style { + if is_current { + Style::default().fg(Color::Cyan) + } else { + Style::default().fg(Color::Blue) + } + } + + fn cursor_style(&self) -> Style { + Style::default().bg(Color::White).fg(Color::Black) + } +} + +struct App { + form: LoginForm, + config: CanvasConfig, + dispatcher: ActionDispatcher, + theme: SimpleTheme, + should_quit: bool, +} + +impl App { + fn new() -> Result> { + Ok(App { + form: LoginForm::new(), + config: CanvasConfig::default(), + dispatcher: ActionDispatcher::new(), + theme: SimpleTheme, + should_quit: false, + }) + } + + fn handle_key(&mut self, key: KeyCode) -> Result<(), Box> { + match key { + KeyCode::Char('q') | KeyCode::Esc => { + self.should_quit = true; + } + KeyCode::Tab => { + // Activate autocomplete on tab + self.form.activate_autocomplete(); + } + KeyCode::Enter => { + // Apply autocomplete selection or just move to next field + if self.form.autocomplete_state().is_ready() { + self.form.apply_autocomplete_selection(); + } else { + let next_field = (self.form.current_field() + 1) % self.form.field_count(); + self.form.set_current_field(next_field); + self.form.set_cursor_position(0); + } + } + _ => { + // Use canvas dispatcher for all other keys + self.dispatcher.dispatch_key(key, &mut self.form, &self.config)?; + } + } + Ok(()) + } +} + +fn main() -> Result<(), Box> { + // 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 app and run + let mut app = App::new()?; + let res = run_app(&mut terminal, &mut app); + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{err:?}"); + } + + Ok(()) +} + +fn run_app(terminal: &mut Terminal, app: &mut App) -> Result<(), Box> { + loop { + terminal.draw(|f| ui(f, app))?; + + if let Event::Key(key) = event::read()? { + app.handle_key(key.code)?; + } + + if app.should_quit { + return Ok(()); + } + } +} + +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(10), + Constraint::Length(3), + ]) + .split(f.size()); + + // Header + let header_block = Block::default() + .borders(Borders::ALL) + .title("Canvas Library - Login Form Demo"); + f.render_widget(header_block, chunks[0]); + + // Main form area - use canvas GUI rendering + let form_area = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Min(0)]) + .split(chunks[1])[0]; + + // Use canvas library's GUI rendering + render_canvas(f, form_area, &app.form, &app.theme); + + // Render autocomplete overlay if active + if app.form.autocomplete_state().is_active() { + render_autocomplete(f, form_area, &app.form, &app.theme); + } + + // Footer with help + let footer_block = Block::default() + .borders(Borders::ALL) + .title("Controls"); + + let help_text = ratatui::widgets::Paragraph::new( + "↑↓ - Navigate fields | ←→ - Move cursor | Tab - Autocomplete | Enter - Select/Next | Esc/q - Quit" + ) + .block(footer_block) + .style(Style::default().fg(Color::Gray)); + + f.render_widget(help_text, chunks[2]); +} diff --git a/canvas/view_docs.sh b/canvas/view_docs.sh new file mode 100755 index 0000000..eece01d --- /dev/null +++ b/canvas/view_docs.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Enhanced documentation viewer for your canvas library +echo "==========================================" +echo "CANVAS LIBRARY DOCUMENTATION" +echo "==========================================" + +# Function to display module docs with colors +show_module() { + local module=$1 + local title=$2 + + echo -e "\n\033[1;34m=== $title ===\033[0m" + echo -e "\033[33mFiles in $module:\033[0m" + find src/$module -name "*.rs" 2>/dev/null | sort + echo + + # Show doc comments for this module + find src/$module -name "*.rs" 2>/dev/null | while read file; do + if grep -q "///" "$file"; then + echo -e "\033[32m--- $file ---\033[0m" + grep -n "^\s*///" "$file" | sed 's/^\([0-9]*:\)\s*\/\/\/ /\1 /' | head -10 + echo + fi + done +} + +# Main modules +show_module "canvas" "CANVAS SYSTEM" +show_module "autocomplete" "AUTOCOMPLETE SYSTEM" +show_module "config" "CONFIGURATION SYSTEM" + +# Show lib.rs and other root files +echo -e "\n\033[1;34m=== ROOT DOCUMENTATION ===\033[0m" +if [ -f "src/lib.rs" ]; then + echo -e "\033[32m--- src/lib.rs ---\033[0m" + grep -n "^\s*///" src/lib.rs | sed 's/^\([0-9]*:\)\s*\/\/\/ /\1 /' 2>/dev/null +fi + +if [ -f "src/dispatcher.rs" ]; then + echo -e "\033[32m--- src/dispatcher.rs ---\033[0m" + grep -n "^\s*///" src/dispatcher.rs | sed 's/^\([0-9]*:\)\s*\/\/\/ /\1 /' 2>/dev/null +fi + +echo -e "\n\033[1;36m==========================================" +echo "To view specific module documentation:" +echo " ./view_canvas_docs.sh canvas" +echo " ./view_canvas_docs.sh autocomplete" +echo " ./view_canvas_docs.sh config" +echo "==========================================\033[0m" + +# If specific module requested +if [ $# -eq 1 ]; then + show_module "$1" "$(echo $1 | tr '[:lower:]' '[:upper:]') MODULE DETAILS" +fi From 3f4380ff4812be95e2c0f51a4d7885ddc1ad19f0 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 17:29:03 +0200 Subject: [PATCH 06/18] documented code now --- canvas/src/canvas/actions/handlers/edit.rs | 48 +++++++++---- canvas/src/canvas/state.rs | 79 +++++++++++++++++++--- canvas/src/config/introspection.rs | 38 ++++++++--- 3 files changed, 134 insertions(+), 31 deletions(-) diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs index a26fe6f..70f2cb8 100644 --- a/canvas/src/canvas/actions/handlers/edit.rs +++ b/canvas/src/canvas/actions/handlers/edit.rs @@ -1,4 +1,8 @@ // src/canvas/actions/handlers/edit.rs +//! Edit mode action handler +//! +//! Handles user input when in edit mode, supporting text entry, deletion, +//! and cursor movement with edit-specific behavior (cursor can go past end of text). use crate::canvas::actions::types::{CanvasAction, ActionResult}; use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; @@ -7,11 +11,22 @@ use crate::canvas::state::CanvasState; use crate::config::CanvasConfig; use anyhow::Result; -const FOR_EDIT_MODE: bool = true; // Edit mode flag - +/// Edit mode uses cursor-past-end behavior for text insertion +const FOR_EDIT_MODE: bool = true; + +/// Empty struct that implements edit mode capabilities pub struct EditHandler; /// Handle actions in edit mode with edit-specific cursor behavior +/// +/// Edit mode allows text modification and uses cursor positioning that can +/// go past the end of existing text to facilitate insertion. +/// +/// # Arguments +/// * `action` - The action to perform +/// * `state` - Mutable canvas state +/// * `ideal_cursor_column` - Desired column for vertical movement (maintained across line changes) +/// * `config` - Optional configuration for behavior customization pub async fn handle_edit_action( action: CanvasAction, state: &mut S, @@ -20,6 +35,7 @@ pub async fn handle_edit_action( ) -> Result { match action { CanvasAction::InsertChar(c) => { + // Insert character at cursor position and advance cursor let cursor_pos = state.current_cursor_pos(); let input = state.get_current_input_mut(); input.insert(cursor_pos, c); @@ -30,6 +46,7 @@ pub async fn handle_edit_action( } CanvasAction::DeleteBackward => { + // Delete character before cursor (Backspace behavior) let cursor_pos = state.current_cursor_pos(); if cursor_pos > 0 { let input = state.get_current_input_mut(); @@ -42,6 +59,7 @@ pub async fn handle_edit_action( } CanvasAction::DeleteForward => { + // Delete character at cursor position (Delete key behavior) let cursor_pos = state.current_cursor_pos(); let input = state.get_current_input_mut(); if cursor_pos < input.len() { @@ -51,6 +69,7 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } + // Cursor movement actions CanvasAction::MoveLeft => { let new_pos = move_left(state.current_cursor_pos()); state.set_current_cursor_pos(new_pos); @@ -67,8 +86,8 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } + // Field navigation (treating single-line fields as "lines") CanvasAction::MoveUp => { - // For single-line fields, move to previous field let current_field = state.current_field(); if current_field > 0 { state.set_current_field(current_field - 1); @@ -80,7 +99,6 @@ pub async fn handle_edit_action( } CanvasAction::MoveDown => { - // For single-line fields, move to next field let current_field = state.current_field(); let total_fields = state.fields().len(); if current_field < total_fields - 1 { @@ -92,6 +110,7 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } + // Line-based movement CanvasAction::MoveLineStart => { let new_pos = line_start_position(); state.set_current_cursor_pos(new_pos); @@ -107,6 +126,7 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } + // Document-level movement (first/last field) CanvasAction::MoveFirstLine => { state.set_current_field(0); let current_input = state.get_current_input(); @@ -126,6 +146,7 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } + // Word-based movement CanvasAction::MoveWordNext => { let current_input = state.get_current_input(); if !current_input.is_empty() { @@ -166,6 +187,7 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } + // Field navigation with wrapping behavior CanvasAction::NextField | CanvasAction::PrevField => { let current_field = state.current_field(); let total_fields = state.fields().len(); @@ -173,16 +195,16 @@ pub async fn handle_edit_action( let new_field = match action { CanvasAction::NextField => { if config.map_or(true, |c| c.behavior.wrap_around_fields) { - (current_field + 1) % total_fields + (current_field + 1) % total_fields // Wrap to first field } else { - (current_field + 1).min(total_fields - 1) + (current_field + 1).min(total_fields - 1) // Stop at last field } } CanvasAction::PrevField => { if config.map_or(true, |c| c.behavior.wrap_around_fields) { - if current_field == 0 { total_fields - 1 } else { current_field - 1 } + if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Wrap to last field } else { - current_field.saturating_sub(1) + current_field.saturating_sub(1) // Stop at first field } } _ => unreachable!(), @@ -206,10 +228,12 @@ pub async fn handle_edit_action( } impl ActionHandlerIntrospection for EditHandler { + /// Report all actions this handler supports with examples and requirements + /// Used for automatic config generation and validation fn introspect() -> HandlerCapabilities { let mut actions = Vec::new(); - // REQUIRED ACTIONS - These must be configured for edit mode to work + // REQUIRED ACTIONS - These must be configured for edit mode to work properly actions.push(ActionSpec { name: "move_left".to_string(), description: "Move cursor one position to the left".to_string(), @@ -240,7 +264,7 @@ impl ActionHandlerIntrospection for EditHandler { actions.push(ActionSpec { name: "delete_char_backward".to_string(), - description: "Delete character before cursor".to_string(), + description: "Delete character before cursor (Backspace)".to_string(), examples: vec!["Backspace".to_string()], is_required: true, }); @@ -318,7 +342,7 @@ impl ActionHandlerIntrospection for EditHandler { actions.push(ActionSpec { name: "delete_char_forward".to_string(), - description: "Delete character after cursor".to_string(), + description: "Delete character after cursor (Delete key)".to_string(), examples: vec!["Delete".to_string()], is_required: false, }); @@ -327,7 +351,7 @@ impl ActionHandlerIntrospection for EditHandler { mode_name: "edit".to_string(), actions, auto_handled: vec![ - "insert_char".to_string(), // Any printable character + "insert_char".to_string(), // Any printable character is inserted automatically ], } } diff --git a/canvas/src/canvas/state.rs b/canvas/src/canvas/state.rs index 29915ba..f8a3e9b 100644 --- a/canvas/src/canvas/state.rs +++ b/canvas/src/canvas/state.rs @@ -1,48 +1,107 @@ // src/canvas/state.rs +//! Canvas state trait and related types +//! +//! This module defines the core trait that any form or input system must implement +//! to work with the canvas library. use crate::canvas::actions::CanvasAction; use crate::canvas::modes::AppMode; -/// Context passed to feature-specific action handlers +/// Context information passed to feature-specific action handlers #[derive(Debug)] pub struct ActionContext { - pub key_code: Option, // Kept for backwards compatibility + /// Original key code that triggered this action (for backwards compatibility) + pub key_code: Option, + /// Current ideal cursor column for vertical movement pub ideal_cursor_column: usize, + /// Current input text pub current_input: String, + /// Current field index pub current_field: usize, } -/// Core trait that any form-like state must implement to work with the canvas system. -/// This enables the same mode behaviors (edit, read-only, highlight) to work across -/// any implementation - login forms, data entry forms, configuration screens, etc. +/// Core trait that any form-like state must implement to work with canvas +/// +/// This trait enables the same mode behaviors (edit, read-only, highlight) to work +/// across any implementation - login forms, data entry forms, configuration screens, etc. +/// +/// # Required Implementation +/// +/// Your struct needs to track: +/// - Current field index and cursor position +/// - All input field values +/// - Current interaction mode +/// - Whether there are unsaved changes +/// +/// # Example Implementation +/// +/// ```rust +/// struct MyForm { +/// fields: Vec, +/// current_field: usize, +/// cursor_pos: usize, +/// mode: AppMode, +/// dirty: bool, +/// } +/// +/// impl CanvasState for MyForm { +/// fn current_field(&self) -> usize { self.current_field } +/// fn current_cursor_pos(&self) -> usize { self.cursor_pos } +/// // ... implement other required methods +/// } +/// ``` pub trait CanvasState { // --- Core Navigation --- + + /// Get current field index (0-based) fn current_field(&self) -> usize; + + /// Get current cursor position within the current field fn current_cursor_pos(&self) -> usize; + + /// Set current field index (should clamp to valid range) fn set_current_field(&mut self, index: usize); + + /// Set cursor position within current field (should clamp to valid range) fn set_current_cursor_pos(&mut self, pos: usize); // --- Mode Information --- + + /// Get current interaction mode (edit, read-only, highlight, etc.) fn current_mode(&self) -> AppMode; // --- Data Access --- + + /// Get immutable reference to current field's text fn get_current_input(&self) -> &str; + + /// Get mutable reference to current field's text fn get_current_input_mut(&mut self) -> &mut String; + + /// Get all input values as immutable references fn inputs(&self) -> Vec<&String>; + + /// Get all field names/labels fn fields(&self) -> Vec<&str>; // --- State Management --- + + /// Check if there are unsaved changes fn has_unsaved_changes(&self) -> bool; + + /// Mark whether there are unsaved changes fn set_has_unsaved_changes(&mut self, changed: bool); - // --- Feature-specific action handling --- + // --- Optional Overrides --- - /// Feature-specific action handling (Type-safe) + /// Handle application-specific actions not covered by standard handlers + /// Return Some(message) if the action was handled, None to use standard handling fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option { - None // Default: no feature-specific handling + None // Default: no custom handling } - // --- Display Overrides (for links, computed values, etc.) --- + /// Get display value for a field (may differ from actual value) + /// Used for things like password masking or computed display values fn get_display_value_for_field(&self, index: usize) -> &str { self.inputs() .get(index) @@ -50,6 +109,8 @@ pub trait CanvasState { .unwrap_or("") } + /// Check if a field has a custom display value + /// Return true if get_display_value_for_field returns something different than the actual value fn has_display_override(&self, _index: usize) -> bool { false } diff --git a/canvas/src/config/introspection.rs b/canvas/src/config/introspection.rs index 052a2e6..3986b4e 100644 --- a/canvas/src/config/introspection.rs +++ b/canvas/src/config/introspection.rs @@ -1,43 +1,60 @@ // src/config/introspection.rs +//! Handler capability introspection system +//! +//! This module provides traits and utilities for handlers to report their capabilities, +//! enabling automatic configuration generation and validation. use std::collections::HashMap; +/// Specification for a single action that a handler can perform #[derive(Debug, Clone)] pub struct ActionSpec { + /// Action name (e.g., "move_left", "delete_char_backward") pub name: String, + /// Human-readable description of what this action does pub description: String, + /// Example keybindings for this action (e.g., ["Left", "h"]) pub examples: Vec, + /// Whether this action is required for the handler to function properly pub is_required: bool, } +/// Complete capability description for a single handler #[derive(Debug, Clone)] pub struct HandlerCapabilities { + /// Mode name this handler operates in (e.g., "edit", "read_only") pub mode_name: String, + /// All actions this handler can perform pub actions: Vec, - pub auto_handled: Vec, // Actions handled automatically (like insert_char) + /// Actions handled automatically without configuration (e.g., "insert_char") + pub auto_handled: Vec, } -/// Trait that each handler implements to report its capabilities +/// Trait that handlers implement to report their capabilities +/// +/// This enables the configuration system to automatically discover what actions +/// are available and validate user configurations against actual implementations. pub trait ActionHandlerIntrospection { - /// Return the capabilities of this handler + /// Return complete capability information for this handler fn introspect() -> HandlerCapabilities; - /// Validate that this handler actually supports the claimed actions + /// Validate that this handler actually supports its claimed actions + /// Override this to add custom validation logic fn validate_capabilities() -> Result<(), String> { - // Default implementation - handlers can override for custom validation - Ok(()) + Ok(()) // Default: assume handler is valid } } -/// System that discovers all handler capabilities +/// Discovers capabilities from all registered handlers pub struct HandlerDiscovery; impl HandlerDiscovery { - /// Discover all handler capabilities by calling their introspect methods + /// Discover capabilities from all known handlers + /// Add new handlers to this function as they are created pub fn discover_all() -> HashMap { let mut capabilities = HashMap::new(); - // Import and introspect each handler + // Register all known handlers here let edit_caps = crate::canvas::actions::handlers::edit::EditHandler::introspect(); capabilities.insert("edit".to_string(), edit_caps); @@ -50,10 +67,11 @@ impl HandlerDiscovery { capabilities } - /// Validate that all handlers actually support their claimed actions + /// Validate all handlers support their claimed capabilities pub fn validate_all_handlers() -> Result<(), Vec> { let mut errors = Vec::new(); + // Validate each handler if let Err(e) = crate::canvas::actions::handlers::edit::EditHandler::validate_capabilities() { errors.push(format!("Edit handler: {}", e)); } From 5b649964623b4a2a30f7fd5f51020e920f07a074 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 19:05:57 +0200 Subject: [PATCH 07/18] example with debug stuff --- canvas/examples/canvas_gui_demo.rs | 636 ++++++++++++++++++----------- 1 file changed, 404 insertions(+), 232 deletions(-) diff --git a/canvas/examples/canvas_gui_demo.rs b/canvas/examples/canvas_gui_demo.rs index 0f4f492..b14d2be 100644 --- a/canvas/examples/canvas_gui_demo.rs +++ b/canvas/examples/canvas_gui_demo.rs @@ -1,7 +1,6 @@ -// examples/canvas_gui_demo.rs - +use std::io; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -9,225 +8,456 @@ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Style}, - widgets::{Block, Borders, Clear}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, Frame, Terminal, }; -use std::{error::Error, io}; -// Import canvas library components use canvas::{ canvas::{ - state::{CanvasState, ActionContext}, gui::render_canvas, - theme::Theme, + modes::{AppMode, HighlightState, ModeManager}, + state::{ActionContext, CanvasState}, + theme::CanvasTheme, }, - autocomplete::{ - state::AutocompleteCanvasState, - gui::render_autocomplete, - types::{AutocompleteState, SuggestionItem}, - }, - config::config::CanvasConfig, + config::CanvasConfig, dispatcher::ActionDispatcher, + CanvasAction, }; -// Example form data structure -#[derive(Debug)] -struct LoginForm { - fields: Vec, - field_labels: Vec, - current_field: usize, - cursor_position: usize, - autocomplete_state: AutocompleteState, +// Simple theme implementation +#[derive(Clone)] +struct DemoTheme; + +impl CanvasTheme for DemoTheme { + fn bg(&self) -> Color { Color::Reset } + fn fg(&self) -> Color { Color::White } + fn accent(&self) -> Color { Color::Cyan } + fn secondary(&self) -> Color { Color::Gray } + fn highlight(&self) -> Color { Color::Yellow } + fn highlight_bg(&self) -> Color { Color::DarkGray } + fn warning(&self) -> Color { Color::Red } + fn border(&self) -> Color { Color::Gray } } -impl LoginForm { +// Demo form state +struct DemoFormState { + fields: Vec, + field_names: Vec, + current_field: usize, + cursor_pos: usize, + mode: AppMode, + highlight_state: HighlightState, + has_changes: bool, + ideal_cursor_column: usize, + last_action: Option, + debug_message: String, +} + +impl DemoFormState { fn new() -> Self { Self { fields: vec![ - String::new(), // username - String::new(), // password - String::new(), // email + "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 ], - field_labels: vec![ - "Username".to_string(), - "Password".to_string(), + field_names: vec![ + "Name".to_string(), "Email".to_string(), + "Phone".to_string(), + "Address".to_string(), + "City".to_string(), + "Comments".to_string(), ], current_field: 0, - cursor_position: 0, - autocomplete_state: AutocompleteState::default(), + cursor_pos: 0, + mode: AppMode::ReadOnly, + highlight_state: HighlightState::Off, + has_changes: false, + ideal_cursor_column: 0, + last_action: None, + debug_message: "Ready - Form loaded with sample data".to_string(), } } + + fn enter_edit_mode(&mut self) { + if ModeManager::can_enter_edit_mode(self.mode) { + self.mode = AppMode::Edit; + self.debug_message = "Entered EDIT mode".to_string(); + } + } + + fn enter_readonly_mode(&mut self) { + if ModeManager::can_enter_read_only_mode(self.mode) { + self.mode = AppMode::ReadOnly; + self.highlight_state = HighlightState::Off; + self.debug_message = "Entered READ-ONLY mode".to_string(); + } + } + + fn enter_highlight_mode(&mut self) { + if ModeManager::can_enter_highlight_mode(self.mode) { + self.mode = AppMode::Highlight; + self.highlight_state = HighlightState::Characterwise { + anchor: (self.current_field, self.cursor_pos), + }; + 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); + } } -// Implement CanvasState trait for your form -impl CanvasState for LoginForm { - fn field_count(&self) -> usize { - self.fields.len() - } - +impl CanvasState for DemoFormState { fn current_field(&self) -> usize { self.current_field } - fn set_current_field(&mut self, field_index: usize) { - if field_index < self.fields.len() { - self.current_field = field_index; - } + fn current_cursor_pos(&self) -> usize { + self.cursor_pos } - fn cursor_position(&self) -> usize { - self.cursor_position + 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(); } - fn set_cursor_position(&mut self, position: usize) { - if let Some(field) = self.fields.get(self.current_field) { - self.cursor_position = position.min(field.len()); - } + fn set_current_cursor_pos(&mut self, pos: usize) { + let max_pos = self.fields[self.current_field].len(); + self.cursor_pos = pos.min(max_pos); } - fn field_value(&self, field_index: usize) -> Option<&str> { - self.fields.get(field_index).map(|s| s.as_str()) + fn current_mode(&self) -> AppMode { + self.mode } - fn set_field_value(&mut self, field_index: usize, value: String) { - if let Some(field) = self.fields.get_mut(field_index) { - *field = value; - } + fn get_current_input(&self) -> &str { + &self.fields[self.current_field] } - fn field_label(&self, field_index: usize) -> Option<&str> { - self.field_labels.get(field_index).map(|s| s.as_str()) + fn get_current_input_mut(&mut self) -> &mut String { + &mut self.fields[self.current_field] } - fn handle_action(&mut self, _action: &str, _context: ActionContext) -> Result<(), Box> { - // Custom action handling can go here - Ok(()) - } -} - -// Implement autocomplete support -impl AutocompleteCanvasState for LoginForm { - type SuggestionData = String; - - fn supports_autocomplete(&self, field_index: usize) -> bool { - // Only username and email fields support autocomplete - field_index == 0 || field_index == 2 + fn inputs(&self) -> Vec<&String> { + self.fields.iter().collect() } - fn autocomplete_state(&self) -> &AutocompleteState { - &self.autocomplete_state + fn fields(&self) -> Vec<&str> { + self.field_names.iter().map(|s| s.as_str()).collect() } - fn autocomplete_state_mut(&mut self) -> &mut AutocompleteState { - &mut self.autocomplete_state + fn has_unsaved_changes(&self) -> bool { + self.has_changes } - fn activate_autocomplete(&mut self) { - if self.supports_autocomplete(self.current_field) { - self.autocomplete_state.activate(self.current_field); + fn set_has_unsaved_changes(&mut self, changed: bool) { + self.has_changes = changed; + } + + 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()); - // Simulate loading suggestions - let suggestions = match self.current_field { - 0 => vec![ // Username suggestions - SuggestionItem::simple("admin"), - SuggestionItem::simple("user"), - SuggestionItem::simple("guest"), - ], - 2 => vec![ // Email suggestions - SuggestionItem::simple("user@example.com"), - SuggestionItem::simple("admin@domain.com"), - SuggestionItem::simple("test@test.org"), - ], - _ => vec![], - }; - - self.autocomplete_state.set_suggestions(suggestions); + // Return None to let the handler process it, but we'll see this debug message + return None; } - } - fn apply_autocomplete_selection(&mut self) { - if let Some(suggestion) = self.autocomplete_state.selected_suggestion() { - self.set_field_value(self.current_field, suggestion.insert_value.clone()); - self.cursor_position = suggestion.insert_value.len(); - self.autocomplete_state.deactivate(); - } - } -} - -// Simple theme implementation -struct SimpleTheme; - -impl Theme for SimpleTheme { - fn field_style(&self, is_current: bool, _is_highlighted: bool) -> Style { - if is_current { - Style::default().bg(Color::DarkGray).fg(Color::White) - } else { - Style::default().fg(Color::Gray) - } - } - - fn label_style(&self, is_current: bool) -> Style { - if is_current { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::Blue) - } - } - - fn cursor_style(&self) -> Style { - Style::default().bg(Color::White).fg(Color::Black) - } -} - -struct App { - form: LoginForm, - config: CanvasConfig, - dispatcher: ActionDispatcher, - theme: SimpleTheme, - should_quit: bool, -} - -impl App { - fn new() -> Result> { - Ok(App { - form: LoginForm::new(), - config: CanvasConfig::default(), - dispatcher: ActionDispatcher::new(), - theme: SimpleTheme, - should_quit: false, - }) - } - - fn handle_key(&mut self, key: KeyCode) -> Result<(), Box> { - match key { - KeyCode::Char('q') | KeyCode::Esc => { - self.should_quit = true; - } - KeyCode::Tab => { - // Activate autocomplete on tab - self.form.activate_autocomplete(); - } - KeyCode::Enter => { - // Apply autocomplete selection or just move to next field - if self.form.autocomplete_state().is_ready() { - self.form.apply_autocomplete_selection(); - } else { - let next_field = (self.form.current_field() + 1) % self.form.field_count(); - self.form.set_current_field(next_field); - self.form.set_cursor_position(0); + match action { + CanvasAction::Custom(cmd) => { + match cmd.as_str() { + "enter_edit_mode" => { + self.enter_edit_mode(); + Some("Entered edit mode".to_string()) + } + "enter_readonly_mode" => { + self.enter_readonly_mode(); + Some("Entered read-only mode".to_string()) + } + "enter_highlight_mode" => { + self.enter_highlight_mode(); + Some("Entered highlight mode".to_string()) + } + _ => None, } } - _ => { - // Use canvas dispatcher for all other keys - self.dispatcher.dispatch_key(key, &mut self.form, &self.config)?; - } + _ => None, } - Ok(()) } } -fn main() -> Result<(), Box> { +async fn run_app(terminal: &mut Terminal, mut state: DemoFormState, config: CanvasConfig) -> io::Result<()> { + let theme = DemoTheme; + + loop { + 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 + 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) { + break; + } + + 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 + 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 + ).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")); + } + + // Mark as changed for text modification keys in edit mode + if is_edit_mode { + match key.code { + KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => { + state.set_has_unsaved_changes(true); + } + _ => {} + } + } + 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) + if !handled && is_edit_mode { + if let KeyCode::Char(c) = key.code { + if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) { + let action = CanvasAction::InsertChar(c); + let mut ideal_cursor = state.ideal_cursor_column; + if let Ok(_) = ActionDispatcher::dispatch_with_config( + action, + &mut state, + &mut ideal_cursor, + Some(&config), + ).await { + state.ideal_cursor_column = ideal_cursor; + state.set_has_unsaved_changes(true); + state.debug_message = format!("Inserted char: '{}'", c); + handled = true; + } + } + } + } + + // Third priority: Fallback mode transitions (if not handled by config) + 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); + 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(); + handled = true; + } + // Any mode -> ReadOnly fallback + (_, KeyCode::Esc) => { + state.enter_readonly_mode(); + state.debug_message = "Fallback: entered read-only mode via Esc".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(", ")); + } + } + } + + Ok(()) +} + +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) + ]) + .split(f.area()); + + // Render the canvas form + render_canvas( + f, + chunks[0], + state, + theme, + state.mode == AppMode::Edit, + &state.highlight_state, + ); + + // Render status bar with debug info + let mode_text = match state.mode { + AppMode::Edit => "EDIT", + AppMode::ReadOnly => "NORMAL", + AppMode::Highlight => "VISUAL", + AppMode::General => "GENERAL", + AppMode::Command => "COMMAND", + }; + + let status_text = if state.has_changes { + format!("-- {} -- [Modified]", mode_text) + } else { + format!("-- {} --", mode_text) + }; + + 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::Highlight => "hjkl/arrows: Select | w/b/e: Words | 0/$: Line | Esc: Normal | F10: Quit", + _ => "Esc: Normal | F10: Quit", + }; + + let status = Paragraph::new(vec![ + Line::from(Span::styled(status_text, Style::default().fg(theme.accent()))), + Line::from(Span::styled(position_text, Style::default().fg(theme.fg()))), + 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")); + + 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(); @@ -235,9 +465,11 @@ fn main() -> Result<(), Box> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Create app and run - let mut app = App::new()?; - let res = run_app(&mut terminal, &mut app); + // Create demo state + let state = DemoFormState::new(); + + // Run app + let res = run_app(&mut terminal, state, config).await; // Restore terminal disable_raw_mode()?; @@ -249,68 +481,8 @@ fn main() -> Result<(), Box> { terminal.show_cursor()?; if let Err(err) = res { - println!("{err:?}"); + println!("{:?}", err); } Ok(()) } - -fn run_app(terminal: &mut Terminal, app: &mut App) -> Result<(), Box> { - loop { - terminal.draw(|f| ui(f, app))?; - - if let Event::Key(key) = event::read()? { - app.handle_key(key.code)?; - } - - if app.should_quit { - return Ok(()); - } - } -} - -fn ui(f: &mut Frame, app: &App) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([ - Constraint::Length(3), - Constraint::Min(10), - Constraint::Length(3), - ]) - .split(f.size()); - - // Header - let header_block = Block::default() - .borders(Borders::ALL) - .title("Canvas Library - Login Form Demo"); - f.render_widget(header_block, chunks[0]); - - // Main form area - use canvas GUI rendering - let form_area = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([Constraint::Min(0)]) - .split(chunks[1])[0]; - - // Use canvas library's GUI rendering - render_canvas(f, form_area, &app.form, &app.theme); - - // Render autocomplete overlay if active - if app.form.autocomplete_state().is_active() { - render_autocomplete(f, form_area, &app.form, &app.theme); - } - - // Footer with help - let footer_block = Block::default() - .borders(Borders::ALL) - .title("Controls"); - - let help_text = ratatui::widgets::Paragraph::new( - "↑↓ - Navigate fields | ←→ - Move cursor | Tab - Autocomplete | Enter - Select/Next | Esc/q - Quit" - ) - .block(footer_block) - .style(Style::default().fg(Color::Gray)); - - f.render_widget(help_text, chunks[2]); -} From 8788323c62172498d5d802d165706fd4b0d6c2e6 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 20:44:23 +0200 Subject: [PATCH 08/18] fixed canvas library --- canvas/examples/canvas_gui_demo.rs | 177 +++---------------- canvas/src/config/config.rs | 274 ++++++++++++++++++++++++----- 2 files changed, 261 insertions(+), 190 deletions(-) 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 { From 36690e674a139bb95fb92f98e2ea527e3426e4a0 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 21:41:54 +0200 Subject: [PATCH 09/18] canvas library config removed compeltely --- canvas/examples/canvas_gui_demo.rs | 219 +++--- canvas/examples/generate_template.rs | 21 - canvas/src/autocomplete/actions.rs | 146 ++-- .../src/canvas/actions/handlers/dispatcher.rs | 43 ++ canvas/src/canvas/actions/handlers/edit.rs | 175 +---- .../src/canvas/actions/handlers/highlight.rs | 105 +-- canvas/src/canvas/actions/handlers/mod.rs | 3 +- .../src/canvas/actions/handlers/readonly.rs | 157 +---- canvas/src/canvas/actions/mod.rs | 6 +- canvas/src/canvas/actions/types.rs | 120 ++-- canvas/src/canvas/mod.rs | 8 +- canvas/src/config/config.rs | 665 ------------------ canvas/src/config/introspection.rs | 93 --- canvas/src/config/mod.rs | 12 - canvas/src/config/registry.rs | 135 ---- canvas/src/config/validation.rs | 278 -------- canvas/src/dispatcher.rs | 110 --- canvas/src/lib.rs | 15 +- client/config.toml | 2 +- 19 files changed, 313 insertions(+), 2000 deletions(-) delete mode 100644 canvas/examples/generate_template.rs create mode 100644 canvas/src/canvas/actions/handlers/dispatcher.rs delete mode 100644 canvas/src/config/config.rs delete mode 100644 canvas/src/config/introspection.rs delete mode 100644 canvas/src/config/mod.rs delete mode 100644 canvas/src/config/registry.rs delete mode 100644 canvas/src/config/validation.rs delete mode 100644 canvas/src/dispatcher.rs diff --git a/canvas/examples/canvas_gui_demo.rs b/canvas/examples/canvas_gui_demo.rs index 534718f..8010043 100644 --- a/canvas/examples/canvas_gui_demo.rs +++ b/canvas/examples/canvas_gui_demo.rs @@ -1,3 +1,5 @@ +// examples/canvas_gui_demo.rs + use std::io; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, @@ -20,9 +22,7 @@ use canvas::{ state::{ActionContext, CanvasState}, theme::CanvasTheme, }, - config::CanvasConfig, - dispatcher::ActionDispatcher, - CanvasAction, + CanvasAction, execute, }; // Simple theme implementation @@ -49,8 +49,6 @@ struct DemoFormState { mode: AppMode, highlight_state: HighlightState, has_changes: bool, - ideal_cursor_column: usize, - last_action: Option, debug_message: String, } @@ -78,9 +76,7 @@ impl DemoFormState { mode: AppMode::ReadOnly, highlight_state: HighlightState::Off, has_changes: false, - ideal_cursor_column: 0, - last_action: None, - debug_message: "Ready".to_string(), + debug_message: "Ready - Use hjkl to move, w for next word, i to edit".to_string(), } } @@ -181,98 +177,125 @@ impl CanvasState for DemoFormState { } } -async fn run_app(terminal: &mut Terminal, mut state: DemoFormState, config: CanvasConfig) -> io::Result<()> { +/// Simple key mapping - users have full control! +async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut DemoFormState) -> bool { + let is_edit_mode = state.mode == AppMode::Edit; + + // Handle quit first + if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) || + (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) || + key == KeyCode::F(10) { + return false; // Signal to quit + } + + // Users directly map keys to actions - no configuration needed! + let action = match (state.mode, key, modifiers) { + // === READ-ONLY MODE KEYS === + (AppMode::ReadOnly, KeyCode::Char('h'), _) => Some(CanvasAction::MoveLeft), + (AppMode::ReadOnly, KeyCode::Char('j'), _) => Some(CanvasAction::MoveDown), + (AppMode::ReadOnly, KeyCode::Char('k'), _) => Some(CanvasAction::MoveUp), + (AppMode::ReadOnly, KeyCode::Char('l'), _) => Some(CanvasAction::MoveRight), + (AppMode::ReadOnly, KeyCode::Char('w'), _) => Some(CanvasAction::MoveWordNext), + (AppMode::ReadOnly, KeyCode::Char('b'), _) => Some(CanvasAction::MoveWordPrev), + (AppMode::ReadOnly, KeyCode::Char('e'), _) => Some(CanvasAction::MoveWordEnd), + (AppMode::ReadOnly, KeyCode::Char('0'), _) => Some(CanvasAction::MoveLineStart), + (AppMode::ReadOnly, KeyCode::Char('$'), _) => Some(CanvasAction::MoveLineEnd), + (AppMode::ReadOnly, KeyCode::Tab, _) => Some(CanvasAction::NextField), + (AppMode::ReadOnly, KeyCode::BackTab, _) => Some(CanvasAction::PrevField), + + // === EDIT MODE KEYS === + (AppMode::Edit, KeyCode::Left, _) => Some(CanvasAction::MoveLeft), + (AppMode::Edit, KeyCode::Right, _) => Some(CanvasAction::MoveRight), + (AppMode::Edit, KeyCode::Up, _) => Some(CanvasAction::MoveUp), + (AppMode::Edit, KeyCode::Down, _) => Some(CanvasAction::MoveDown), + (AppMode::Edit, KeyCode::Home, _) => Some(CanvasAction::MoveLineStart), + (AppMode::Edit, KeyCode::End, _) => Some(CanvasAction::MoveLineEnd), + (AppMode::Edit, KeyCode::Backspace, _) => Some(CanvasAction::DeleteBackward), + (AppMode::Edit, KeyCode::Delete, _) => Some(CanvasAction::DeleteForward), + (AppMode::Edit, KeyCode::Tab, _) => Some(CanvasAction::NextField), + (AppMode::Edit, KeyCode::BackTab, _) => Some(CanvasAction::PrevField), + + // Vim-style movement in edit mode (optional) + (AppMode::Edit, KeyCode::Char('h'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveLeft), + (AppMode::Edit, KeyCode::Char('l'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveRight), + + // Word movement with Ctrl in edit mode + (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordPrev), + (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordNext), + + // === MODE TRANSITIONS === + (AppMode::ReadOnly, KeyCode::Char('i'), _) => Some(CanvasAction::Custom("enter_edit_mode".to_string())), + (AppMode::ReadOnly, KeyCode::Char('a'), _) => { + // 'a' moves to end of line then enters edit mode + if let Ok(_) = execute(CanvasAction::MoveLineEnd, state).await { + Some(CanvasAction::Custom("enter_edit_mode".to_string())) + } else { + None + } + }, + (AppMode::ReadOnly, KeyCode::Char('v'), _) => Some(CanvasAction::Custom("enter_highlight_mode".to_string())), + (_, KeyCode::Esc, _) => Some(CanvasAction::Custom("enter_readonly_mode".to_string())), + + // === CHARACTER INPUT IN EDIT MODE === + (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) && !m.contains(KeyModifiers::ALT) => { + Some(CanvasAction::InsertChar(c)) + }, + + // === ARROW KEYS IN READ-ONLY MODE === + (AppMode::ReadOnly, KeyCode::Left, _) => Some(CanvasAction::MoveLeft), + (AppMode::ReadOnly, KeyCode::Right, _) => Some(CanvasAction::MoveRight), + (AppMode::ReadOnly, KeyCode::Up, _) => Some(CanvasAction::MoveUp), + (AppMode::ReadOnly, KeyCode::Down, _) => Some(CanvasAction::MoveDown), + + _ => None, + }; + + // Execute the action if we found one + if let Some(action) = action { + match execute(action.clone(), state).await { + Ok(result) => { + if result.is_success() { + // Mark as changed for editing actions + if is_edit_mode { + match action { + CanvasAction::InsertChar(_) | CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { + state.set_has_unsaved_changes(true); + } + _ => {} + } + } + + if let Some(msg) = result.message() { + state.debug_message = msg.to_string(); + } else { + state.debug_message = format!("Executed: {}", action.description()); + } + } else if let Some(msg) = result.message() { + state.debug_message = format!("Error: {}", msg); + } + } + Err(e) => { + state.debug_message = format!("Error executing action: {}", e); + } + } + } else { + state.debug_message = format!("Unhandled key: {:?} (mode: {:?})", key, state.mode); + } + + true // Continue running +} + +async fn run_app(terminal: &mut Terminal, mut state: DemoFormState) -> io::Result<()> { let theme = DemoTheme; loop { terminal.draw(|f| ui(f, &state, &theme))?; if let Event::Key(key) = event::read()? { - // 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) { + let should_continue = handle_key_press(key.code, key.modifiers, &mut state).await; + if !should_continue { break; } - - let is_edit_mode = state.mode == AppMode::Edit; - let mut handled = false; - - // First priority: Try to dispatch through config system - let mut ideal_cursor = state.ideal_cursor_column; - - if let Ok(Some(result)) = ActionDispatcher::dispatch_key( - key.code, - key.modifiers, - &mut state, - &mut ideal_cursor, - is_edit_mode, - false, - ).await { - state.ideal_cursor_column = ideal_cursor; - 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 { - KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => { - state.set_has_unsaved_changes(true); - } - _ => {} - } - } - handled = true; - } - - // 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) { - let action = CanvasAction::InsertChar(c); - let mut ideal_cursor = state.ideal_cursor_column; - if let Ok(_) = ActionDispatcher::dispatch_with_config( - action, - &mut state, - &mut ideal_cursor, - Some(&config), - ).await { - state.ideal_cursor_column = ideal_cursor; - state.set_has_unsaved_changes(true); - state.debug_message = format!("Inserted char: '{}'", c); - handled = true; - } - } - } - } - - // Third priority: Fallback mode transitions - if !handled { - match (state.mode, key.code) { - (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!("Entered edit mode via {:?}", key.code); - handled = true; - } - (AppMode::ReadOnly, KeyCode::Char('v')) => { - state.enter_highlight_mode(); - state.debug_message = "Entered visual mode".to_string(); - handled = true; - } - (_, KeyCode::Esc) => { - state.enter_readonly_mode(); - state.debug_message = "Entered read-only mode".to_string(); - handled = true; - } - _ => {} - } - } - - if !handled { - state.debug_message = format!("Unhandled key: {:?}", key.code); - } } } @@ -313,15 +336,15 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { format!("-- {} --", mode_text) }; - let position_text = format!("Field: {}/{} | Cursor: {} | Column: {}", + let position_text = format!("Field: {}/{} | Cursor: {} | Actions: {}", state.current_field + 1, state.fields.len(), state.cursor_pos, - state.ideal_cursor_column); + CanvasAction::movement_actions().len() + CanvasAction::editing_actions().len()); 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::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | i/a: Edit | v: Visual | F10: Quit", + AppMode::Edit => "Type to edit | Arrows/Ctrl+arrows: Move | Tab: 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", }; @@ -339,8 +362,6 @@ fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { #[tokio::main] async fn main() -> Result<(), Box> { - let config = CanvasConfig::load(); - enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; @@ -349,7 +370,7 @@ async fn main() -> Result<(), Box> { let state = DemoFormState::new(); - let res = run_app(&mut terminal, state, config).await; + let res = run_app(&mut terminal, state).await; disable_raw_mode()?; execute!( diff --git a/canvas/examples/generate_template.rs b/canvas/examples/generate_template.rs deleted file mode 100644 index 5b585c1..0000000 --- a/canvas/examples/generate_template.rs +++ /dev/null @@ -1,21 +0,0 @@ -// examples/generate_template.rs -use canvas::config::CanvasConfig; -use std::env; - -fn main() { - let args: Vec = env::args().collect(); - - if args.len() > 1 && args[1] == "clean" { - // Generate clean template with 80% active code - let template = CanvasConfig::generate_clean_template(); - println!("{}", template); - } else { - // Generate verbose template with descriptions (default) - let template = CanvasConfig::generate_template(); - println!("{}", template); - } -} - -// Usage: -// cargo run --example generate_template > canvas_config.toml -// cargo run --example generate_template clean > canvas_config_clean.toml diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index 310a4b6..2467268 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -3,126 +3,102 @@ use crate::canvas::state::{CanvasState, ActionContext}; use crate::autocomplete::state::AutocompleteCanvasState; use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::dispatcher::ActionDispatcher; // NEW: Use dispatcher directly -use crate::config::CanvasConfig; +use crate::canvas::actions::execute; use anyhow::Result; /// Version for states that implement rich autocomplete pub async fn execute_canvas_action_with_autocomplete( action: CanvasAction, state: &mut S, - ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, + _ideal_cursor_column: &mut usize, // Keep for compatibility + _config: Option<&()>, // Remove CanvasConfig, keep for compatibility ) -> Result { - // 1. Try feature-specific handler first - let context = ActionContext { - key_code: None, - ideal_cursor_column: *ideal_cursor_column, - current_input: state.get_current_input().to_string(), - current_field: state.current_field(), - }; - - if let Some(result) = handle_rich_autocomplete_action(action.clone(), state, &context) { - return Ok(result); - } - - // 2. Handle generic actions using the new dispatcher directly - let result = ActionDispatcher::dispatch_with_config(action.clone(), state, ideal_cursor_column, config).await?; - - // 3. AUTO-TRIGGER LOGIC: Check if we should activate/deactivate autocomplete - if let Some(cfg) = config { - if cfg.should_auto_trigger_autocomplete() { - match action { - CanvasAction::InsertChar(_) => { - let current_field = state.current_field(); - let current_input = state.get_current_input(); - - if state.supports_autocomplete(current_field) - && !state.is_autocomplete_active() - && current_input.len() >= 1 - { - state.activate_autocomplete(); - } - } - - CanvasAction::NextField | CanvasAction::PrevField => { - let current_field = state.current_field(); - - if state.supports_autocomplete(current_field) && !state.is_autocomplete_active() { - state.activate_autocomplete(); - } else if !state.supports_autocomplete(current_field) && state.is_autocomplete_active() { - state.deactivate_autocomplete(); - } - } - - _ => {} // No auto-trigger for other actions + // Check for autocomplete-specific actions first + match &action { + CanvasAction::InsertChar(_) => { + // Character insertion - execute then potentially trigger autocomplete + let result = execute(action, state).await?; + + // Check if we should trigger autocomplete after character insertion + if state.should_trigger_autocomplete() { + state.trigger_autocomplete_suggestions().await; } + + Ok(result) + } + + _ => { + // For other actions, clear suggestions and execute + let result = execute(action, state).await?; + + // Clear autocomplete on navigation/other actions + match action { + CanvasAction::MoveLeft | CanvasAction::MoveRight | + CanvasAction::MoveUp | CanvasAction::MoveDown | + CanvasAction::NextField | CanvasAction::PrevField => { + state.clear_autocomplete_suggestions(); + } + _ => {} + } + + Ok(result) } } - - Ok(result) } -/// Handle rich autocomplete actions for AutocompleteCanvasState -fn handle_rich_autocomplete_action( +/// Handle autocomplete-specific actions (called from handle_feature_action) +pub async fn handle_autocomplete_action( action: CanvasAction, state: &mut S, _context: &ActionContext, -) -> Option { - match action { +) -> Result { + match action { CanvasAction::TriggerAutocomplete => { - let current_field = state.current_field(); - if state.supports_autocomplete(current_field) { - state.activate_autocomplete(); - Some(ActionResult::success_with_message("Autocomplete activated")) - } else { - Some(ActionResult::success_with_message("Autocomplete not supported for this field")) - } + // Manual trigger of autocomplete + state.trigger_autocomplete_suggestions().await; + Ok(ActionResult::success_with_message("Triggered autocomplete")) } CanvasAction::SuggestionUp => { - if state.is_autocomplete_ready() { - if let Some(autocomplete_state) = state.autocomplete_state_mut() { - autocomplete_state.select_previous(); - } - Some(ActionResult::success()) + // Navigate up in suggestions + if state.has_autocomplete_suggestions() { + state.move_suggestion_selection(-1); + Ok(ActionResult::success()) } else { - Some(ActionResult::success_with_message("No suggestions available")) + Ok(ActionResult::success_with_message("No suggestions available")) } } CanvasAction::SuggestionDown => { - if state.is_autocomplete_ready() { - if let Some(autocomplete_state) = state.autocomplete_state_mut() { - autocomplete_state.select_next(); - } - Some(ActionResult::success()) + // Navigate down in suggestions + if state.has_autocomplete_suggestions() { + state.move_suggestion_selection(1); + Ok(ActionResult::success()) } else { - Some(ActionResult::success_with_message("No suggestions available")) + Ok(ActionResult::success_with_message("No suggestions available")) } } CanvasAction::SelectSuggestion => { - if state.is_autocomplete_ready() { - if let Some(msg) = state.apply_autocomplete_selection() { - Some(ActionResult::success_with_message(&msg)) - } else { - Some(ActionResult::success_with_message("No suggestion selected")) - } + // Accept the selected suggestion + if let Some(suggestion) = state.get_selected_suggestion() { + state.apply_suggestion(&suggestion); + state.clear_autocomplete_suggestions(); + Ok(ActionResult::success_with_message("Applied suggestion")) } else { - Some(ActionResult::success_with_message("No suggestions available")) + Ok(ActionResult::success_with_message("No suggestion selected")) } } CanvasAction::ExitSuggestions => { - if state.is_autocomplete_active() { - state.deactivate_autocomplete(); - Some(ActionResult::success_with_message("Exited autocomplete")) - } else { - Some(ActionResult::success()) - } + // Cancel autocomplete + state.clear_autocomplete_suggestions(); + Ok(ActionResult::success_with_message("Cleared suggestions")) } - _ => None, // Not a rich autocomplete action + _ => { + // Not an autocomplete action + Ok(ActionResult::success_with_message("Not an autocomplete action")) + } } } diff --git a/canvas/src/canvas/actions/handlers/dispatcher.rs b/canvas/src/canvas/actions/handlers/dispatcher.rs new file mode 100644 index 0000000..31dc0d3 --- /dev/null +++ b/canvas/src/canvas/actions/handlers/dispatcher.rs @@ -0,0 +1,43 @@ +// src/canvas/actions/handlers/dispatcher.rs + +use crate::canvas::state::{CanvasState, ActionContext}; +use crate::canvas::actions::{CanvasAction, ActionResult}; +use crate::canvas::modes::AppMode; +use anyhow::Result; + +use super::{handle_edit_action, handle_readonly_action, handle_highlight_action}; + +/// Main action dispatcher - routes actions to mode-specific handlers +pub async fn dispatch_action( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, +) -> Result { + // Check if the application wants to handle this action first + let context = ActionContext { + key_code: None, + ideal_cursor_column: *ideal_cursor_column, + current_input: state.get_current_input().to_string(), + current_field: state.current_field(), + }; + + if let Some(result) = state.handle_feature_action(&action, &context) { + return Ok(ActionResult::HandledByFeature(result)); + } + + // Route to mode-specific handler + match state.current_mode() { + AppMode::Edit => { + handle_edit_action(action, state, ideal_cursor_column).await + } + AppMode::ReadOnly => { + handle_readonly_action(action, state, ideal_cursor_column).await + } + AppMode::Highlight => { + handle_highlight_action(action, state, ideal_cursor_column).await + } + AppMode::General | AppMode::Command => { + Ok(ActionResult::success_with_message("Mode does not handle canvas actions directly")) + } + } +} diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs index 70f2cb8..b86a0ba 100644 --- a/canvas/src/canvas/actions/handlers/edit.rs +++ b/canvas/src/canvas/actions/handlers/edit.rs @@ -1,37 +1,30 @@ // src/canvas/actions/handlers/edit.rs //! Edit mode action handler -//! +//! //! Handles user input when in edit mode, supporting text entry, deletion, //! and cursor movement with edit-specific behavior (cursor can go past end of text). use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; -use crate::config::CanvasConfig; use anyhow::Result; /// Edit mode uses cursor-past-end behavior for text insertion const FOR_EDIT_MODE: bool = true; -/// Empty struct that implements edit mode capabilities -pub struct EditHandler; - /// Handle actions in edit mode with edit-specific cursor behavior -/// +/// /// Edit mode allows text modification and uses cursor positioning that can /// go past the end of existing text to facilitate insertion. -/// +/// /// # Arguments /// * `action` - The action to perform /// * `state` - Mutable canvas state /// * `ideal_cursor_column` - Desired column for vertical movement (maintained across line changes) -/// * `config` - Optional configuration for behavior customization pub async fn handle_edit_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, ) -> Result { match action { CanvasAction::InsertChar(c) => { @@ -187,25 +180,17 @@ pub async fn handle_edit_action( Ok(ActionResult::success()) } - // Field navigation with wrapping behavior + // Field navigation with simple wrapping behavior CanvasAction::NextField | CanvasAction::PrevField => { let current_field = state.current_field(); let total_fields = state.fields().len(); let new_field = match action { CanvasAction::NextField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - (current_field + 1) % total_fields // Wrap to first field - } else { - (current_field + 1).min(total_fields - 1) // Stop at last field - } + (current_field + 1) % total_fields // Simple wrap } CanvasAction::PrevField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Wrap to last field - } else { - current_field.saturating_sub(1) // Stop at first field - } + if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Simple wrap } _ => unreachable!(), }; @@ -226,151 +211,3 @@ pub async fn handle_edit_action( } } } - -impl ActionHandlerIntrospection for EditHandler { - /// Report all actions this handler supports with examples and requirements - /// Used for automatic config generation and validation - fn introspect() -> HandlerCapabilities { - let mut actions = Vec::new(); - - // REQUIRED ACTIONS - These must be configured for edit mode to work properly - actions.push(ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["Left".to_string(), "h".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["Right".to_string(), "l".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field or line".to_string(), - examples: vec!["Up".to_string(), "k".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_down".to_string(), - description: "Move to next field or line".to_string(), - examples: vec!["Down".to_string(), "j".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "delete_char_backward".to_string(), - description: "Delete character before cursor (Backspace)".to_string(), - examples: vec!["Backspace".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string(), "Enter".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - is_required: true, - }); - - // OPTIONAL ACTIONS - These enhance functionality but aren't required - actions.push(ActionSpec { - name: "move_word_next".to_string(), - description: "Move cursor to start of next word".to_string(), - examples: vec!["Ctrl+Right".to_string(), "w".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_prev".to_string(), - description: "Move cursor to start of previous word".to_string(), - examples: vec!["Ctrl+Left".to_string(), "b".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["Home".to_string(), "0".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["End".to_string(), "$".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["Ctrl+Home".to_string(), "gg".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["Ctrl+End".to_string(), "G".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "delete_char_forward".to_string(), - description: "Delete character after cursor (Delete key)".to_string(), - examples: vec!["Delete".to_string()], - is_required: false, - }); - - HandlerCapabilities { - mode_name: "edit".to_string(), - actions, - auto_handled: vec![ - "insert_char".to_string(), // Any printable character is inserted automatically - ], - } - } - - fn validate_capabilities() -> Result<(), String> { - // TODO: Could add runtime validation that the handler actually - // implements all the actions it claims to support - - // For now, just validate that we have the essential actions - let caps = Self::introspect(); - let required_count = caps.actions.iter().filter(|a| a.is_required).count(); - - if required_count < 7 { // We expect at least 7 required actions - return Err(format!( - "Edit handler claims only {} required actions, expected at least 7", - required_count - )); - } - - Ok(()) - } -} diff --git a/canvas/src/canvas/actions/handlers/highlight.rs b/canvas/src/canvas/actions/handlers/highlight.rs index 7227443..66b8604 100644 --- a/canvas/src/canvas/actions/handlers/highlight.rs +++ b/canvas/src/canvas/actions/handlers/highlight.rs @@ -1,16 +1,11 @@ // src/canvas/actions/handlers/highlight.rs use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; - use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; -use crate::config::CanvasConfig; use anyhow::Result; const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior - -pub struct HighlightHandler; /// Handle actions in highlight/visual mode /// TODO: Implement selection logic and highlight-specific behaviors @@ -18,7 +13,6 @@ pub async fn handle_highlight_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, ) -> Result { match action { // Movement actions work similar to read-only mode but with selection @@ -93,8 +87,8 @@ pub async fn handle_highlight_action( } // Highlight mode doesn't handle editing actions - CanvasAction::InsertChar(_) | - CanvasAction::DeleteBackward | + CanvasAction::InsertChar(_) | + CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { Ok(ActionResult::success_with_message("Action not available in highlight mode")) } @@ -108,98 +102,3 @@ pub async fn handle_highlight_action( } } } - -impl ActionHandlerIntrospection for HighlightHandler { - fn introspect() -> HandlerCapabilities { - let mut actions = Vec::new(); - - // For now, highlight mode uses similar movement to readonly - // but this will be discovered from actual implementation - - // REQUIRED ACTIONS - Basic movement in highlight mode - actions.push(ActionSpec { - name: "move_left".to_string(), - description: "Move cursor left and extend selection".to_string(), - examples: vec!["h".to_string(), "Left".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_right".to_string(), - description: "Move cursor right and extend selection".to_string(), - examples: vec!["l".to_string(), "Right".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_up".to_string(), - description: "Move up and extend selection".to_string(), - examples: vec!["k".to_string(), "Up".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_down".to_string(), - description: "Move down and extend selection".to_string(), - examples: vec!["j".to_string(), "Down".to_string()], - is_required: true, - }); - - // OPTIONAL ACTIONS - Advanced highlight movement - actions.push(ActionSpec { - name: "move_word_next".to_string(), - description: "Move to next word and extend selection".to_string(), - examples: vec!["w".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end".to_string(), - description: "Move to word end and extend selection".to_string(), - examples: vec!["e".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_prev".to_string(), - description: "Move to previous word and extend selection".to_string(), - examples: vec!["b".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_start".to_string(), - description: "Move to line start and extend selection".to_string(), - examples: vec!["0".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_end".to_string(), - description: "Move to line end and extend selection".to_string(), - examples: vec!["$".to_string()], - is_required: false, - }); - - HandlerCapabilities { - mode_name: "highlight".to_string(), - actions, - auto_handled: vec![], // Highlight mode has no auto-handled actions - } - } - - fn validate_capabilities() -> Result<(), String> { - let caps = Self::introspect(); - let required_count = caps.actions.iter().filter(|a| a.is_required).count(); - - if required_count < 4 { // We expect at least 4 required actions (basic movement) - return Err(format!( - "Highlight handler claims only {} required actions, expected at least 4", - required_count - )); - } - - Ok(()) - } -} - diff --git a/canvas/src/canvas/actions/handlers/mod.rs b/canvas/src/canvas/actions/handlers/mod.rs index 91c0b75..3bbbe8f 100644 --- a/canvas/src/canvas/actions/handlers/mod.rs +++ b/canvas/src/canvas/actions/handlers/mod.rs @@ -3,8 +3,9 @@ pub mod edit; pub mod readonly; pub mod highlight; +pub mod dispatcher; -// Re-export handler functions pub use edit::handle_edit_action; pub use readonly::handle_readonly_action; pub use highlight::handle_highlight_action; +pub use dispatcher::dispatch_action; diff --git a/canvas/src/canvas/actions/handlers/readonly.rs b/canvas/src/canvas/actions/handlers/readonly.rs index 2c3817a..19201bd 100644 --- a/canvas/src/canvas/actions/handlers/readonly.rs +++ b/canvas/src/canvas/actions/handlers/readonly.rs @@ -1,10 +1,8 @@ // src/canvas/actions/handlers/readonly.rs use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::config::introspection::{ActionHandlerIntrospection, HandlerCapabilities, ActionSpec}; use crate::canvas::actions::movement::*; use crate::canvas::state::CanvasState; -use crate::config::CanvasConfig; use anyhow::Result; const FOR_EDIT_MODE: bool = false; // Read-only mode flag @@ -14,7 +12,6 @@ pub async fn handle_readonly_action( action: CanvasAction, state: &mut S, ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, ) -> Result { match action { CanvasAction::MoveLeft => { @@ -37,7 +34,7 @@ pub async fn handle_readonly_action( let current_field = state.current_field(); let new_field = current_field.saturating_sub(1); state.set_current_field(new_field); - + // Apply ideal cursor column with read-only bounds let current_input = state.get_current_input(); let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); @@ -51,10 +48,10 @@ pub async fn handle_readonly_action( if total_fields == 0 { return Ok(ActionResult::success_with_message("No fields to navigate")); } - + let new_field = (current_field + 1).min(total_fields - 1); state.set_current_field(new_field); - + // Apply ideal cursor column with read-only bounds let current_input = state.get_current_input(); let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); @@ -67,7 +64,7 @@ pub async fn handle_readonly_action( if total_fields == 0 { return Ok(ActionResult::success_with_message("No fields to navigate")); } - + state.set_current_field(0); let current_input = state.get_current_input(); let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); @@ -81,7 +78,7 @@ pub async fn handle_readonly_action( if total_fields == 0 { return Ok(ActionResult::success_with_message("No fields to navigate")); } - + let last_field = total_fields - 1; state.set_current_field(last_field); let current_input = state.get_current_input(); @@ -155,18 +152,10 @@ pub async fn handle_readonly_action( let new_field = match action { CanvasAction::NextField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - (current_field + 1) % total_fields - } else { - (current_field + 1).min(total_fields - 1) - } + (current_field + 1) % total_fields // Simple wrap } CanvasAction::PrevField => { - if config.map_or(true, |c| c.behavior.wrap_around_fields) { - if current_field == 0 { total_fields - 1 } else { current_field - 1 } - } else { - current_field.saturating_sub(1) - } + if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Simple wrap } _ => unreachable!(), }; @@ -177,8 +166,8 @@ pub async fn handle_readonly_action( } // Read-only mode doesn't handle editing actions - CanvasAction::InsertChar(_) | - CanvasAction::DeleteBackward | + CanvasAction::InsertChar(_) | + CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { Ok(ActionResult::success_with_message("Action not available in read-only mode")) } @@ -192,131 +181,3 @@ pub async fn handle_readonly_action( } } } - -pub struct ReadOnlyHandler; - -impl ActionHandlerIntrospection for ReadOnlyHandler { - fn introspect() -> HandlerCapabilities { - let mut actions = Vec::new(); - - // REQUIRED ACTIONS - Navigation is essential in read-only mode - actions.push(ActionSpec { - name: "move_left".to_string(), - description: "Move cursor one position to the left".to_string(), - examples: vec!["h".to_string(), "Left".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_right".to_string(), - description: "Move cursor one position to the right".to_string(), - examples: vec!["l".to_string(), "Right".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_up".to_string(), - description: "Move to previous field".to_string(), - examples: vec!["k".to_string(), "Up".to_string()], - is_required: true, - }); - - actions.push(ActionSpec { - name: "move_down".to_string(), - description: "Move to next field".to_string(), - examples: vec!["j".to_string(), "Down".to_string()], - is_required: true, - }); - - // OPTIONAL ACTIONS - Advanced navigation features - actions.push(ActionSpec { - name: "move_word_next".to_string(), - description: "Move cursor to start of next word".to_string(), - examples: vec!["w".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_prev".to_string(), - description: "Move cursor to start of previous word".to_string(), - examples: vec!["b".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end".to_string(), - description: "Move cursor to end of current/next word".to_string(), - examples: vec!["e".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_word_end_prev".to_string(), - description: "Move cursor to end of previous word".to_string(), - examples: vec!["ge".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_start".to_string(), - description: "Move cursor to beginning of line".to_string(), - examples: vec!["0".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_line_end".to_string(), - description: "Move cursor to end of line".to_string(), - examples: vec!["$".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_first_line".to_string(), - description: "Move to first field".to_string(), - examples: vec!["gg".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "move_last_line".to_string(), - description: "Move to last field".to_string(), - examples: vec!["G".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "next_field".to_string(), - description: "Move to next input field".to_string(), - examples: vec!["Tab".to_string()], - is_required: false, - }); - - actions.push(ActionSpec { - name: "prev_field".to_string(), - description: "Move to previous input field".to_string(), - examples: vec!["Shift+Tab".to_string()], - is_required: false, - }); - - HandlerCapabilities { - mode_name: "read_only".to_string(), - actions, - auto_handled: vec![], // Read-only mode has no auto-handled actions - } - } - - fn validate_capabilities() -> Result<(), String> { - let caps = Self::introspect(); - let required_count = caps.actions.iter().filter(|a| a.is_required).count(); - - if required_count < 4 { // We expect at least 4 required actions (basic movement) - return Err(format!( - "ReadOnly handler claims only {} required actions, expected at least 4", - required_count - )); - } - - Ok(()) - } -} diff --git a/canvas/src/canvas/actions/mod.rs b/canvas/src/canvas/actions/mod.rs index 6beba1b..803d179 100644 --- a/canvas/src/canvas/actions/mod.rs +++ b/canvas/src/canvas/actions/mod.rs @@ -1,8 +1,8 @@ // src/canvas/actions/mod.rs pub mod types; -pub mod movement; pub mod handlers; +pub mod movement; -// Re-export the main types -pub use types::{CanvasAction, ActionResult}; +// Re-export the main API +pub use types::{CanvasAction, ActionResult, execute}; diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index 433a4d5..1a606cb 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -1,108 +1,94 @@ // src/canvas/actions/types.rs +use crate::canvas::state::CanvasState; +use anyhow::Result; + +/// All available canvas actions #[derive(Debug, Clone, PartialEq)] pub enum CanvasAction { - // Character input - InsertChar(char), - - // Deletion - DeleteBackward, - DeleteForward, - - // Basic cursor movement + // Movement actions MoveLeft, MoveRight, MoveUp, MoveDown, - + + // Word movement + MoveWordNext, + MoveWordPrev, + MoveWordEnd, + MoveWordEndPrev, + // Line movement MoveLineStart, MoveLineEnd, - MoveFirstLine, - MoveLastLine, - - // Word movement - MoveWordNext, - MoveWordEnd, - MoveWordPrev, - MoveWordEndPrev, - - // Field navigation + + // Field movement NextField, PrevField, - + MoveFirstLine, + MoveLastLine, + + // Editing actions + InsertChar(char), + DeleteBackward, + DeleteForward, + // Autocomplete actions TriggerAutocomplete, SuggestionUp, SuggestionDown, SelectSuggestion, ExitSuggestions, - + // Custom actions Custom(String), } -impl CanvasAction { - /// Convert string action name to CanvasAction enum (config-driven) - pub fn from_string(action: &str) -> Self { - match action { - "delete_char_backward" => Self::DeleteBackward, - "delete_char_forward" => Self::DeleteForward, - "move_left" => Self::MoveLeft, - "move_right" => Self::MoveRight, - "move_up" => Self::MoveUp, - "move_down" => Self::MoveDown, - "move_line_start" => Self::MoveLineStart, - "move_line_end" => Self::MoveLineEnd, - "move_first_line" => Self::MoveFirstLine, - "move_last_line" => Self::MoveLastLine, - "move_word_next" => Self::MoveWordNext, - "move_word_end" => Self::MoveWordEnd, - "move_word_prev" => Self::MoveWordPrev, - "move_word_end_prev" => Self::MoveWordEndPrev, - "next_field" => Self::NextField, - "prev_field" => Self::PrevField, - "trigger_autocomplete" => Self::TriggerAutocomplete, - "suggestion_up" => Self::SuggestionUp, - "suggestion_down" => Self::SuggestionDown, - "select_suggestion" => Self::SelectSuggestion, - "exit_suggestions" => Self::ExitSuggestions, - _ => Self::Custom(action.to_string()), - } - } -} - -#[derive(Debug, Clone, PartialEq)] +/// Result type for canvas actions +#[derive(Debug, Clone)] pub enum ActionResult { - Success(Option), - HandledByFeature(String), - RequiresContext(String), + Success, + Message(String), + HandledByApp(String), + HandledByFeature(String), // Keep for compatibility Error(String), } impl ActionResult { pub fn success() -> Self { - Self::Success(None) + Self::Success } - + pub fn success_with_message(msg: &str) -> Self { - Self::Success(Some(msg.to_string())) + Self::Message(msg.to_string()) } - + + pub fn handled_by_app(msg: &str) -> Self { + Self::HandledByApp(msg.to_string()) + } + pub fn error(msg: &str) -> Self { - Self::Error(msg.into()) + Self::Error(msg.to_string()) } - + pub fn is_success(&self) -> bool { - matches!(self, Self::Success(_) | Self::HandledByFeature(_)) + matches!(self, Self::Success | Self::Message(_) | Self::HandledByApp(_) | Self::HandledByFeature(_)) } - + pub fn message(&self) -> Option<&str> { match self { - Self::Success(msg) => msg.as_deref(), - Self::HandledByFeature(msg) => Some(msg), - Self::RequiresContext(msg) => Some(msg), - Self::Error(msg) => Some(msg), + Self::Message(msg) | Self::HandledByApp(msg) | Self::HandledByFeature(msg) | Self::Error(msg) => Some(msg), + Self::Success => None, } } } + +/// Execute a canvas action on the given state +pub async fn execute( + action: CanvasAction, + state: &mut S, +) -> Result { + let mut ideal_cursor_column = 0; + + super::handlers::dispatch_action(action, state, &mut ideal_cursor_column).await +} diff --git a/canvas/src/canvas/mod.rs b/canvas/src/canvas/mod.rs index e68fac2..aa72003 100644 --- a/canvas/src/canvas/mod.rs +++ b/canvas/src/canvas/mod.rs @@ -1,18 +1,16 @@ // src/canvas/mod.rs + pub mod actions; pub mod gui; -pub mod modes; +pub mod modes; pub mod state; pub mod theme; -// Re-export commonly used canvas types +// Re-export main types for convenience pub use actions::{CanvasAction, ActionResult}; pub use modes::{AppMode, ModeManager, HighlightState}; pub use state::{CanvasState, ActionContext}; -// Re-export the main entry point -pub use crate::dispatcher::execute_canvas_action; - #[cfg(feature = "gui")] pub use theme::CanvasTheme; diff --git a/canvas/src/config/config.rs b/canvas/src/config/config.rs deleted file mode 100644 index 9db4bc9..0000000 --- a/canvas/src/config/config.rs +++ /dev/null @@ -1,665 +0,0 @@ -// src/config/config.rs -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use crossterm::event::{KeyCode, KeyModifiers}; -use anyhow::{Context, Result}; - -// Import from sibling modules -use super::registry::ActionRegistry; -use super::validation::{ConfigValidator, ValidationError, ValidationResult, ValidationWarning}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasKeybindings { - pub edit: HashMap>, - pub read_only: HashMap>, - pub global: HashMap>, -} - -impl Default for CanvasKeybindings { - fn default() -> Self { - Self { - edit: HashMap::new(), - read_only: HashMap::new(), - global: HashMap::new(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasBehavior { - pub confirm_on_save: bool, - pub auto_indent: bool, - pub wrap_search: bool, - pub wrap_around_fields: bool, -} - -impl Default for CanvasBehavior { - fn default() -> Self { - Self { - confirm_on_save: true, - auto_indent: true, - wrap_search: true, - wrap_around_fields: true, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasAppearance { - pub line_numbers: bool, - pub syntax_highlighting: bool, - pub current_line_highlight: bool, -} - -impl Default for CanvasAppearance { - fn default() -> Self { - Self { - line_numbers: true, - syntax_highlighting: true, - current_line_highlight: true, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CanvasConfig { - pub keybindings: CanvasKeybindings, - pub behavior: CanvasBehavior, - pub appearance: CanvasAppearance, -} - -impl Default for CanvasConfig { - fn default() -> Self { - Self { - keybindings: CanvasKeybindings::with_vim_defaults(), - behavior: CanvasBehavior::default(), - appearance: CanvasAppearance::default(), - } - } -} - -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 { - 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(); - - // 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()]); - - 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 - } - - /// 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 { - /// 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() - } - } - } - - /// 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 auto-generated defaults if file doesn't exist - eprintln!("Config file not found, using auto-generated defaults"); - Self::default() - }; - - // Validate the configuration against current introspection state - let registry = ActionRegistry::from_handlers(); - - // 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); - } - } - - // Validate the configuration against the dynamic registry - let validator = ConfigValidator::new(registry); - 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) - } - - /// 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); - - 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'); - } - - template - } - - /// 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 { - eprintln!(" - {}", error); - } - } - - registry.generate_clean_template() - } - - /// 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) - } - - /// Print validation results for current config - pub fn print_validation(&self) { - let registry = ActionRegistry::from_handlers(); - let validator = ConfigValidator::new(registry); - let result = validator.validate_keybindings(&self.keybindings); - validator.print_validation_result(&result); - } - - /// Load from TOML string - pub fn from_toml(toml_str: &str) -> Result { - toml::from_str(toml_str) - .context("Failed to parse TOML configuration") - } - - /// Load from file - pub fn from_file(path: &std::path::Path) -> Result { - let contents = std::fs::read_to_string(path) - .context("Failed to read config file")?; - Self::from_toml(&contents) - } - - /// 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() - } - - /// 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") - } - - /// 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) - .or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers)) - } - - /// Get action for key in edit mode - pub fn get_edit_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { - self.get_action_in_mode(&self.keybindings.edit, key, modifiers) - .or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers)) - } - - /// Get action for key (mode-aware) - pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers, is_edit_mode: bool, _has_suggestions: bool) -> Option<&str> { - // Check mode-specific - if is_edit_mode { - self.get_edit_action(key, modifiers) - } else { - self.get_read_only_action(key, modifiers) - } - } - - fn get_action_in_mode<'a>(&self, mode_bindings: &'a HashMap>, key: KeyCode, modifiers: KeyModifiers) -> Option<&'a str> { - for (action, bindings) in mode_bindings { - for binding in bindings { - if self.matches_keybinding(binding, key, modifiers) { - return Some(action); - } - } - } - None - } - - 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; - } - } - } - } - - // 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/canvas/src/config/introspection.rs b/canvas/src/config/introspection.rs deleted file mode 100644 index 3986b4e..0000000 --- a/canvas/src/config/introspection.rs +++ /dev/null @@ -1,93 +0,0 @@ -// src/config/introspection.rs -//! Handler capability introspection system -//! -//! This module provides traits and utilities for handlers to report their capabilities, -//! enabling automatic configuration generation and validation. - -use std::collections::HashMap; - -/// Specification for a single action that a handler can perform -#[derive(Debug, Clone)] -pub struct ActionSpec { - /// Action name (e.g., "move_left", "delete_char_backward") - pub name: String, - /// Human-readable description of what this action does - pub description: String, - /// Example keybindings for this action (e.g., ["Left", "h"]) - pub examples: Vec, - /// Whether this action is required for the handler to function properly - pub is_required: bool, -} - -/// Complete capability description for a single handler -#[derive(Debug, Clone)] -pub struct HandlerCapabilities { - /// Mode name this handler operates in (e.g., "edit", "read_only") - pub mode_name: String, - /// All actions this handler can perform - pub actions: Vec, - /// Actions handled automatically without configuration (e.g., "insert_char") - pub auto_handled: Vec, -} - -/// Trait that handlers implement to report their capabilities -/// -/// This enables the configuration system to automatically discover what actions -/// are available and validate user configurations against actual implementations. -pub trait ActionHandlerIntrospection { - /// Return complete capability information for this handler - fn introspect() -> HandlerCapabilities; - - /// Validate that this handler actually supports its claimed actions - /// Override this to add custom validation logic - fn validate_capabilities() -> Result<(), String> { - Ok(()) // Default: assume handler is valid - } -} - -/// Discovers capabilities from all registered handlers -pub struct HandlerDiscovery; - -impl HandlerDiscovery { - /// Discover capabilities from all known handlers - /// Add new handlers to this function as they are created - pub fn discover_all() -> HashMap { - let mut capabilities = HashMap::new(); - - // Register all known handlers here - let edit_caps = crate::canvas::actions::handlers::edit::EditHandler::introspect(); - capabilities.insert("edit".to_string(), edit_caps); - - let readonly_caps = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::introspect(); - capabilities.insert("read_only".to_string(), readonly_caps); - - let highlight_caps = crate::canvas::actions::handlers::highlight::HighlightHandler::introspect(); - capabilities.insert("highlight".to_string(), highlight_caps); - - capabilities - } - - /// Validate all handlers support their claimed capabilities - pub fn validate_all_handlers() -> Result<(), Vec> { - let mut errors = Vec::new(); - - // Validate each handler - if let Err(e) = crate::canvas::actions::handlers::edit::EditHandler::validate_capabilities() { - errors.push(format!("Edit handler: {}", e)); - } - - if let Err(e) = crate::canvas::actions::handlers::readonly::ReadOnlyHandler::validate_capabilities() { - errors.push(format!("ReadOnly handler: {}", e)); - } - - if let Err(e) = crate::canvas::actions::handlers::highlight::HighlightHandler::validate_capabilities() { - errors.push(format!("Highlight handler: {}", e)); - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } - } -} diff --git a/canvas/src/config/mod.rs b/canvas/src/config/mod.rs deleted file mode 100644 index d9afd7a..0000000 --- a/canvas/src/config/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// src/config/mod.rs - -mod registry; -mod config; -mod validation; -pub mod introspection; - -// Re-export everything from the main config module -pub use registry::*; -pub use validation::*; -pub use config::*; -pub use introspection::*; diff --git a/canvas/src/config/registry.rs b/canvas/src/config/registry.rs deleted file mode 100644 index 7070149..0000000 --- a/canvas/src/config/registry.rs +++ /dev/null @@ -1,135 +0,0 @@ -// src/config/registry.rs - -use std::collections::HashMap; -use crate::config::introspection::{HandlerDiscovery, ActionSpec, HandlerCapabilities}; - -#[derive(Debug, Clone)] -pub struct ModeRegistry { - pub required: HashMap, - pub optional: HashMap, - pub auto_handled: Vec, -} - -#[derive(Debug, Clone)] -pub struct ActionRegistry { - pub modes: HashMap, -} - -impl ActionRegistry { - /// NEW: Create registry by discovering actual handler capabilities - pub fn from_handlers() -> Self { - let handler_capabilities = HandlerDiscovery::discover_all(); - let mut modes = HashMap::new(); - - for (mode_name, capabilities) in handler_capabilities { - let mode_registry = Self::build_mode_registry(capabilities); - modes.insert(mode_name, mode_registry); - } - - Self { modes } - } - - /// Build a mode registry from handler capabilities - fn build_mode_registry(capabilities: HandlerCapabilities) -> ModeRegistry { - let mut required = HashMap::new(); - let mut optional = HashMap::new(); - - for action_spec in capabilities.actions { - if action_spec.is_required { - required.insert(action_spec.name.clone(), action_spec); - } else { - optional.insert(action_spec.name.clone(), action_spec); - } - } - - ModeRegistry { - required, - optional, - auto_handled: capabilities.auto_handled, - } - } - - /// Validate that the registry matches the actual implementation - pub fn validate_against_implementation(&self) -> Result<(), Vec> { - HandlerDiscovery::validate_all_handlers() - } - - pub fn get_mode_registry(&self, mode: &str) -> Option<&ModeRegistry> { - self.modes.get(mode) - } - - pub fn all_known_actions(&self) -> Vec { - let mut actions = Vec::new(); - - for registry in self.modes.values() { - actions.extend(registry.required.keys().cloned()); - actions.extend(registry.optional.keys().cloned()); - } - - actions.sort(); - actions.dedup(); - actions - } - - pub fn generate_config_template(&self) -> String { - let mut template = String::new(); - template.push_str("# Canvas Library Configuration Template\n"); - template.push_str("# Generated automatically from actual handler capabilities\n\n"); - - for (mode_name, registry) in &self.modes { - template.push_str(&format!("[keybindings.{}]\n", mode_name)); - - if !registry.required.is_empty() { - template.push_str("# REQUIRED ACTIONS - These must be configured\n"); - for (name, spec) in ®istry.required { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("{} = {:?}\n\n", name, spec.examples)); - } - } - - if !registry.optional.is_empty() { - template.push_str("# OPTIONAL ACTIONS - Configure these if you want them enabled\n"); - for (name, spec) in ®istry.optional { - template.push_str(&format!("# {}\n", spec.description)); - template.push_str(&format!("# {} = {:?}\n\n", name, spec.examples)); - } - } - - if !registry.auto_handled.is_empty() { - template.push_str("# AUTO-HANDLED - These are handled automatically, don't configure:\n"); - for auto_action in ®istry.auto_handled { - template.push_str(&format!("# {} (automatic)\n", auto_action)); - } - template.push('\n'); - } - } - - template - } - - pub fn generate_clean_template(&self) -> String { - let mut template = String::new(); - - for (mode_name, registry) in &self.modes { - template.push_str(&format!("[keybindings.{}]\n", mode_name)); - - if !registry.required.is_empty() { - template.push_str("# Required\n"); - for (name, spec) in ®istry.required { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - } - - if !registry.optional.is_empty() { - template.push_str("# Optional\n"); - for (name, spec) in ®istry.optional { - template.push_str(&format!("{} = {:?}\n", name, spec.examples)); - } - } - - template.push('\n'); - } - - template - } -} diff --git a/canvas/src/config/validation.rs b/canvas/src/config/validation.rs deleted file mode 100644 index 4673d19..0000000 --- a/canvas/src/config/validation.rs +++ /dev/null @@ -1,278 +0,0 @@ -// src/config/validation.rs - -use std::collections::HashMap; -use thiserror::Error; -use crate::config::registry::{ActionRegistry, ModeRegistry}; -use crate::config::CanvasKeybindings; - -#[derive(Error, Debug)] -pub enum ValidationError { - #[error("Missing required action '{action}' in {mode} mode")] - MissingRequired { - action: String, - mode: String, - suggestion: String, - }, - - #[error("Unknown action '{action}' in {mode} mode")] - UnknownAction { - action: String, - mode: String, - similar: Vec, - }, - - #[error("Multiple validation errors")] - Multiple(Vec), -} - -#[derive(Debug)] -pub struct ValidationWarning { - pub message: String, - pub suggestion: Option, -} - -#[derive(Debug)] -pub struct ValidationResult { - pub errors: Vec, - pub warnings: Vec, - pub is_valid: bool, -} - -impl ValidationResult { - pub fn new() -> Self { - Self { - errors: Vec::new(), - warnings: Vec::new(), - is_valid: true, - } - } - - pub fn add_error(&mut self, error: ValidationError) { - self.errors.push(error); - self.is_valid = false; - } - - pub fn add_warning(&mut self, warning: ValidationWarning) { - self.warnings.push(warning); - } - - pub fn merge(&mut self, other: ValidationResult) { - self.errors.extend(other.errors); - self.warnings.extend(other.warnings); - if !other.is_valid { - self.is_valid = false; - } - } -} - -pub struct ConfigValidator { - registry: ActionRegistry, -} - -impl ConfigValidator { - // FIXED: Accept registry parameter to match config.rs calls - pub fn new(registry: ActionRegistry) -> Self { - Self { - registry, - } - } - - pub fn validate_keybindings(&self, keybindings: &CanvasKeybindings) -> ValidationResult { - let mut result = ValidationResult::new(); - - // Validate each mode that exists in the registry - if let Some(edit_registry) = self.registry.get_mode_registry("edit") { - result.merge(self.validate_mode_bindings( - "edit", - &keybindings.edit, - edit_registry - )); - } - - if let Some(readonly_registry) = self.registry.get_mode_registry("read_only") { - result.merge(self.validate_mode_bindings( - "read_only", - &keybindings.read_only, - readonly_registry - )); - } - - // Skip suggestions mode if not discovered by introspection - // (autocomplete is separate concern as requested) - - // Skip global mode if not discovered by introspection - // (can be added later if needed) - - result - } - - fn validate_mode_bindings( - &self, - mode_name: &str, - bindings: &HashMap>, - registry: &ModeRegistry - ) -> ValidationResult { - let mut result = ValidationResult::new(); - - // Check for missing required actions - for (action_name, spec) in ®istry.required { - if !bindings.contains_key(action_name) { - result.add_error(ValidationError::MissingRequired { - action: action_name.clone(), - mode: mode_name.to_string(), - suggestion: format!( - "Add to config: {} = {:?}", - action_name, - spec.examples - ), - }); - } - } - - // Check for unknown actions - let all_known: std::collections::HashSet<_> = registry.required.keys() - .chain(registry.optional.keys()) - .collect(); - - for action_name in bindings.keys() { - if !all_known.contains(action_name) { - let similar = self.find_similar_actions(action_name, &all_known); - result.add_error(ValidationError::UnknownAction { - action: action_name.clone(), - mode: mode_name.to_string(), - similar, - }); - } - } - - // Check for empty keybinding arrays - for (action_name, key_list) in bindings { - if key_list.is_empty() { - result.add_warning(ValidationWarning { - message: format!( - "Action '{}' in {} mode has empty keybinding list", - action_name, mode_name - ), - suggestion: Some(format!( - "Either add keybindings or remove the action from config" - )), - }); - } - } - - // Warn about auto-handled actions that shouldn't be in config - for auto_action in ®istry.auto_handled { - if bindings.contains_key(auto_action) { - result.add_warning(ValidationWarning { - message: format!( - "Action '{}' in {} mode is auto-handled and shouldn't be in config", - auto_action, mode_name - ), - suggestion: Some(format!( - "Remove '{}' from config - it's handled automatically", - auto_action - )), - }); - } - } - - result - } - - fn find_similar_actions(&self, action: &str, known_actions: &std::collections::HashSet<&String>) -> Vec { - let mut similar = Vec::new(); - - for known in known_actions { - if self.is_similar(action, known) { - similar.push(known.to_string()); - } - } - - similar.sort(); - similar.truncate(3); // Limit to 3 suggestions - similar - } - - fn is_similar(&self, a: &str, b: &str) -> bool { - // Simple similarity check - could be improved with proper edit distance - let a_lower = a.to_lowercase(); - let b_lower = b.to_lowercase(); - - // Check if one contains the other - if a_lower.contains(&b_lower) || b_lower.contains(&a_lower) { - return true; - } - - // Check for common prefixes - let common_prefixes = ["move_", "delete_", "suggestion_"]; - for prefix in &common_prefixes { - if a_lower.starts_with(prefix) && b_lower.starts_with(prefix) { - return true; - } - } - - false - } - - pub fn print_validation_result(&self, result: &ValidationResult) { - if result.is_valid && result.warnings.is_empty() { - println!("✅ Canvas configuration is valid!"); - return; - } - - if !result.errors.is_empty() { - println!("❌ Canvas configuration has errors:"); - for error in &result.errors { - match error { - ValidationError::MissingRequired { action, mode, suggestion } => { - println!(" • Missing required action '{}' in {} mode", action, mode); - println!(" 💡 {}", suggestion); - } - ValidationError::UnknownAction { action, mode, similar } => { - println!(" • Unknown action '{}' in {} mode", action, mode); - if !similar.is_empty() { - println!(" 💡 Did you mean: {}", similar.join(", ")); - } - } - ValidationError::Multiple(_) => { - println!(" • Multiple errors occurred"); - } - } - println!(); - } - } - - if !result.warnings.is_empty() { - println!("⚠️ Canvas configuration has warnings:"); - for warning in &result.warnings { - println!(" • {}", warning.message); - if let Some(suggestion) = &warning.suggestion { - println!(" 💡 {}", suggestion); - } - println!(); - } - } - - if !result.is_valid { - println!("🔧 To generate a config template, use:"); - println!(" CanvasConfig::generate_template()"); - } - } - - pub fn generate_missing_config(&self, keybindings: &CanvasKeybindings) -> String { - let mut config = String::new(); - let validation = self.validate_keybindings(keybindings); - - for error in &validation.errors { - if let ValidationError::MissingRequired { action, mode, suggestion } = error { - if config.is_empty() { - config.push_str(&format!("# Missing required actions for canvas\n\n")); - config.push_str(&format!("[keybindings.{}]\n", mode)); - } - config.push_str(&format!("{}\n", suggestion)); - } - } - - config - } -} diff --git a/canvas/src/dispatcher.rs b/canvas/src/dispatcher.rs deleted file mode 100644 index 09a284d..0000000 --- a/canvas/src/dispatcher.rs +++ /dev/null @@ -1,110 +0,0 @@ -// src/dispatcher.rs - -use crate::canvas::state::{CanvasState, ActionContext}; -use crate::canvas::actions::{CanvasAction, ActionResult}; -use crate::canvas::actions::handlers::{handle_edit_action, handle_readonly_action, handle_highlight_action}; -use crate::canvas::modes::AppMode; -use crate::config::CanvasConfig; -use crossterm::event::{KeyCode, KeyModifiers}; - -/// Main entry point for executing canvas actions -pub async fn execute_canvas_action( - action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, -) -> anyhow::Result { - ActionDispatcher::dispatch_with_config(action, state, ideal_cursor_column, config).await -} - -/// High-level action dispatcher that routes actions to mode-specific handlers -pub struct ActionDispatcher; - -impl ActionDispatcher { - /// Dispatch any action to the appropriate mode handler - pub async fn dispatch( - action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, - ) -> anyhow::Result { - let config = CanvasConfig::load(); - Self::dispatch_with_config(action, state, ideal_cursor_column, Some(&config)).await - } - - /// Dispatch action with provided config - pub async fn dispatch_with_config( - action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, - config: Option<&CanvasConfig>, - ) -> anyhow::Result { - // Check for feature-specific handling first - let context = ActionContext { - key_code: None, - ideal_cursor_column: *ideal_cursor_column, - current_input: state.get_current_input().to_string(), - current_field: state.current_field(), - }; - - if let Some(result) = state.handle_feature_action(&action, &context) { - return Ok(ActionResult::HandledByFeature(result)); - } - - // Route to mode-specific handler - match state.current_mode() { - AppMode::Edit => { - handle_edit_action(action, state, ideal_cursor_column, config).await - } - AppMode::ReadOnly => { - handle_readonly_action(action, state, ideal_cursor_column, config).await - } - AppMode::Highlight => { - handle_highlight_action(action, state, ideal_cursor_column, config).await - } - AppMode::General | AppMode::Command => { - // These modes might not handle canvas actions directly - Ok(ActionResult::success_with_message("Mode does not handle canvas actions")) - } - } - } - - /// Quick action dispatch from KeyCode using config - pub async fn dispatch_key( - key: KeyCode, - modifiers: KeyModifiers, - state: &mut S, - ideal_cursor_column: &mut usize, - is_edit_mode: bool, - has_suggestions: bool, - ) -> anyhow::Result> { - 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_with_config(action, state, ideal_cursor_column, Some(&config)).await?; - Ok(Some(result)) - } else { - Ok(None) - } - } - - /// Batch dispatch multiple actions - pub async fn dispatch_batch( - actions: Vec, - state: &mut S, - ideal_cursor_column: &mut usize, - ) -> anyhow::Result> { - let mut results = Vec::new(); - for action in actions { - let result = Self::dispatch(action, state, ideal_cursor_column).await?; - let is_success = result.is_success(); - results.push(result); - - // Stop on first error - if !is_success { - break; - } - } - Ok(results) - } -} diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 26c5a40..5cd0143 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,11 +1,16 @@ // src/lib.rs + pub mod canvas; -pub mod autocomplete; -pub mod config; -pub mod dispatcher; +// pub mod autocomplete; +pub mod dispatcher; // Keep for compatibility // Re-export the main API for easy access -pub use dispatcher::{execute_canvas_action, ActionDispatcher}; -pub use canvas::actions::{CanvasAction, ActionResult}; +pub use canvas::actions::{CanvasAction, ActionResult, execute}; pub use canvas::state::{CanvasState, ActionContext}; pub use canvas::modes::{AppMode, HighlightState, ModeManager}; + +// Keep legacy exports for compatibility +pub use dispatcher::{execute_canvas_action, ActionDispatcher}; + +// Re-export result type for convenience +pub type Result = anyhow::Result; diff --git a/client/config.toml b/client/config.toml index d370570..ab3ae25 100644 --- a/client/config.toml +++ b/client/config.toml @@ -50,7 +50,7 @@ move_right = ["l", "Right"] move_down = ["j", "Down"] # Optional move_line_end = ["$"] -move_word_next = ["w"] +# move_word_next = ["w"] next_field = ["Tab"] move_word_prev = ["b"] move_word_end = ["e"] From 828a63c30c14050e958d9fa59a85e46f03b705df Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 22:04:15 +0200 Subject: [PATCH 10/18] canvas is fixed, lets fix autocomplete also --- canvas/src/canvas/actions/types.rs | 90 ++++++++++++++++++++++++++++++ canvas/src/lib.rs | 30 +++++++--- 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index 1a606cb..4f4d163 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -92,3 +92,93 @@ pub async fn execute( super::handlers::dispatch_action(action, state, &mut ideal_cursor_column).await } + +impl CanvasAction { + /// Get a human-readable description of this action + pub fn description(&self) -> &'static str { + match self { + Self::MoveLeft => "move left", + Self::MoveRight => "move right", + Self::MoveUp => "move up", + Self::MoveDown => "move down", + Self::MoveWordNext => "next word", + Self::MoveWordPrev => "previous word", + Self::MoveWordEnd => "word end", + Self::MoveWordEndPrev => "previous word end", + Self::MoveLineStart => "line start", + Self::MoveLineEnd => "line end", + Self::NextField => "next field", + Self::PrevField => "previous field", + Self::MoveFirstLine => "first field", + Self::MoveLastLine => "last field", + Self::InsertChar(c) => "insert character", + Self::DeleteBackward => "delete backward", + Self::DeleteForward => "delete forward", + Self::TriggerAutocomplete => "trigger autocomplete", + Self::SuggestionUp => "suggestion up", + Self::SuggestionDown => "suggestion down", + Self::SelectSuggestion => "select suggestion", + Self::ExitSuggestions => "exit suggestions", + Self::Custom(name) => "custom action", + } + } + + /// Get all movement-related actions + pub fn movement_actions() -> Vec { + vec![ + Self::MoveLeft, + Self::MoveRight, + Self::MoveUp, + Self::MoveDown, + Self::MoveWordNext, + Self::MoveWordPrev, + Self::MoveWordEnd, + Self::MoveWordEndPrev, + Self::MoveLineStart, + Self::MoveLineEnd, + Self::NextField, + Self::PrevField, + Self::MoveFirstLine, + Self::MoveLastLine, + ] + } + + /// Get all editing-related actions + pub fn editing_actions() -> Vec { + vec![ + Self::InsertChar(' '), // Example char + Self::DeleteBackward, + Self::DeleteForward, + ] + } + + /// Get all autocomplete-related actions + pub fn autocomplete_actions() -> Vec { + vec![ + Self::TriggerAutocomplete, + Self::SuggestionUp, + Self::SuggestionDown, + Self::SelectSuggestion, + Self::ExitSuggestions, + ] + } + + /// Check if this action modifies text content + pub fn is_editing_action(&self) -> bool { + matches!(self, + Self::InsertChar(_) | + Self::DeleteBackward | + Self::DeleteForward + ) + } + + /// Check if this action moves the cursor + pub fn is_movement_action(&self) -> bool { + matches!(self, + Self::MoveLeft | Self::MoveRight | Self::MoveUp | Self::MoveDown | + Self::MoveWordNext | Self::MoveWordPrev | Self::MoveWordEnd | Self::MoveWordEndPrev | + Self::MoveLineStart | Self::MoveLineEnd | Self::NextField | Self::PrevField | + Self::MoveFirstLine | Self::MoveLastLine + ) + } +} diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 5cd0143..6b2fd59 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,16 +1,30 @@ -// src/lib.rs +// src/lib.rs - Updated to conditionally include autocomplete pub mod canvas; -// pub mod autocomplete; -pub mod dispatcher; // Keep for compatibility + +// Only include autocomplete module if feature is enabled +#[cfg(feature = "autocomplete")] +pub mod autocomplete; // Re-export the main API for easy access pub use canvas::actions::{CanvasAction, ActionResult, execute}; pub use canvas::state::{CanvasState, ActionContext}; -pub use canvas::modes::{AppMode, HighlightState, ModeManager}; +pub use canvas::modes::{AppMode, ModeManager, HighlightState}; -// Keep legacy exports for compatibility -pub use dispatcher::{execute_canvas_action, ActionDispatcher}; +#[cfg(feature = "gui")] +pub use canvas::theme::CanvasTheme; -// Re-export result type for convenience -pub type Result = anyhow::Result; +#[cfg(feature = "gui")] +pub use canvas::gui::render_canvas; + +// Re-export autocomplete API if feature is enabled +#[cfg(feature = "autocomplete")] +pub use autocomplete::{ + AutocompleteCanvasState, + AutocompleteState, + SuggestionItem, + actions::execute_with_autocomplete, +}; + +#[cfg(all(feature = "gui", feature = "autocomplete"))] +pub use autocomplete::gui::render_autocomplete_dropdown; From c594c35b37746dc1c67370613ce628eaa13c97c7 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 22:25:43 +0200 Subject: [PATCH 11/18] autocomplete now working --- canvas/Cargo.toml | 13 +- canvas/examples/autocomplete.rs | 412 +++++++++++++++++++++++++++++ canvas/src/autocomplete/actions.rs | 182 +++++++++---- canvas/src/autocomplete/gui.rs | 11 +- canvas/src/autocomplete/mod.rs | 18 +- canvas/src/autocomplete/state.rs | 144 ++++++++-- canvas/src/lib.rs | 7 +- 7 files changed, 686 insertions(+), 101 deletions(-) create mode 100644 canvas/examples/autocomplete.rs diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 9a16fe2..4a5bc51 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -14,7 +14,7 @@ common = { path = "../common" } ratatui = { workspace = true, optional = true } crossterm = { workspace = true } anyhow = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, optional = true } toml = { workspace = true } serde = { workspace = true } unicode-width.workspace = true @@ -29,7 +29,14 @@ tokio-test = "0.4.4" [features] default = [] gui = ["ratatui"] +autocomplete = ["tokio"] [[example]] -name = "ratatui_demo" -path = "examples/ratatui_demo.rs" +name = "autocomplete" +required-features = ["autocomplete", "gui"] +path = "examples/autocomplete.rs" + +[[example]] +name = "canvas_gui_demo" +required-features = ["gui"] +path = "examples/canvas_gui_demo.rs" diff --git a/canvas/examples/autocomplete.rs b/canvas/examples/autocomplete.rs new file mode 100644 index 0000000..7cc927d --- /dev/null +++ b/canvas/examples/autocomplete.rs @@ -0,0 +1,412 @@ +// examples/autocomplete.rs +// Run with: cargo run --example autocomplete --features "autocomplete,gui" + +use std::io; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::Color, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, Terminal, +}; + +use canvas::{ + canvas::{ + gui::render_canvas, + modes::AppMode, + state::{ActionContext, CanvasState}, + theme::CanvasTheme, + }, + autocomplete::{ + AutocompleteCanvasState, + AutocompleteState, + SuggestionItem, + execute_with_autocomplete, + handle_autocomplete_feature_action, + }, + CanvasAction, +}; + +// Simple theme implementation +#[derive(Clone)] +struct DemoTheme; + +impl CanvasTheme for DemoTheme { + fn bg(&self) -> Color { Color::Reset } + fn fg(&self) -> Color { Color::White } + fn accent(&self) -> Color { Color::Cyan } + fn secondary(&self) -> Color { Color::Gray } + fn highlight(&self) -> Color { Color::Yellow } + fn highlight_bg(&self) -> Color { Color::DarkGray } + fn warning(&self) -> Color { Color::Red } + fn border(&self) -> Color { Color::Gray } +} + +// Custom suggestion data type +#[derive(Clone, Debug)] +struct EmailSuggestion { + email: String, + provider: String, +} + +// Demo form state with autocomplete +struct AutocompleteFormState { + fields: Vec, + field_names: Vec, + current_field: usize, + cursor_pos: usize, + mode: AppMode, + has_changes: bool, + debug_message: String, + + // Autocomplete state + autocomplete: AutocompleteState, +} + +impl AutocompleteFormState { + fn new() -> Self { + Self { + fields: vec![ + "John Doe".to_string(), + "john@".to_string(), // Partial email to demonstrate autocomplete + "+1 234 567 8900".to_string(), + "San Francisco".to_string(), + ], + field_names: vec![ + "Name".to_string(), + "Email".to_string(), + "Phone".to_string(), + "City".to_string(), + ], + current_field: 1, // Start on email field + cursor_pos: 5, // Position after "john@" + mode: AppMode::Edit, + has_changes: false, + debug_message: "Type in email field, Tab to trigger autocomplete, Enter to select, Esc to cancel".to_string(), + autocomplete: AutocompleteState::new(), + } + } +} + +impl CanvasState for AutocompleteFormState { + 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.min(self.fields.len().saturating_sub(1)); + // Clear autocomplete when changing fields + if self.is_autocomplete_active() { + self.clear_autocomplete_suggestions(); + } + } + fn set_current_cursor_pos(&mut self, pos: usize) { + let max_pos = if self.mode == AppMode::Edit { + self.fields[self.current_field].len() + } else { + self.fields[self.current_field].len().saturating_sub(1) + }; + self.cursor_pos = pos.min(max_pos); + } + fn current_mode(&self) -> AppMode { self.mode } + fn get_current_input(&self) -> &str { &self.fields[self.current_field] } + fn get_current_input_mut(&mut self) -> &mut String { &mut self.fields[self.current_field] } + fn inputs(&self) -> Vec<&String> { self.fields.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; } + + fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { + // Handle autocomplete actions first + if let Some(result) = handle_autocomplete_feature_action(action, self) { + return Some(result); + } + + // Handle other custom actions + match action { + CanvasAction::Custom(cmd) => { + match cmd.as_str() { + "toggle_mode" => { + self.mode = match self.mode { + AppMode::Edit => AppMode::ReadOnly, + AppMode::ReadOnly => AppMode::Edit, + _ => AppMode::Edit, + }; + Some(format!("Switched to {:?} mode", self.mode)) + } + _ => None, + } + } + _ => None, + } + } +} + +impl AutocompleteCanvasState for AutocompleteFormState { + type SuggestionData = EmailSuggestion; + + fn supports_autocomplete(&self, field_index: usize) -> bool { + // Only enable autocomplete for email field (index 1) + field_index == 1 + } + + fn autocomplete_state(&self) -> Option<&AutocompleteState> { + Some(&self.autocomplete) + } + + fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState> { + Some(&mut self.autocomplete) + } + + fn should_trigger_autocomplete(&self) -> bool { + let current_input = self.get_current_input(); + let current_field = self.current_field(); + + // Trigger for email field when we have "@" and at least 1 more character + self.supports_autocomplete(current_field) && + current_input.contains('@') && + current_input.len() > current_input.find('@').unwrap_or(0) + 1 && + !self.is_autocomplete_active() + } + + /// This is where the magic happens - user implements their own async fetching + async fn trigger_autocomplete_suggestions(&mut self) { + // 1. Activate UI (shows loading spinner) + self.activate_autocomplete(); + self.set_autocomplete_loading(true); + + // 2. Get current input for querying + let query = self.get_current_input().to_string(); + + // 3. Extract domain part from email + let domain_part = if let Some(at_pos) = query.find('@') { + query[at_pos + 1..].to_string() + } else { + self.set_autocomplete_loading(false); + return; // No @ symbol, can't suggest + }; + + // 4. SIMULATE ASYNC API CALL (in real code, this would be HTTP request) + let email_prefix = query[..query.find('@').unwrap()].to_string(); + let suggestions = tokio::task::spawn_blocking(move || { + // Simulate network delay + std::thread::sleep(std::time::Duration::from_millis(200)); + + // Create mock suggestions based on domain input + let popular_domains = vec![ + ("gmail.com", "Gmail"), + ("yahoo.com", "Yahoo Mail"), + ("outlook.com", "Outlook"), + ("hotmail.com", "Hotmail"), + ("company.com", "Company Email"), + ("university.edu", "University"), + ]; + + let mut results = Vec::new(); + + for (domain, provider) in popular_domains { + if domain.starts_with(&domain_part) || domain_part.is_empty() { + let full_email = format!("{}@{}", email_prefix, domain); + results.push(SuggestionItem::new( + EmailSuggestion { + email: full_email.clone(), + provider: provider.to_string(), + }, + format!("{} ({})", full_email, provider), // display text + full_email, // value to store + )); + } + } + + results + }).await.unwrap_or_default(); + + // 5. Provide suggestions back to library + self.set_autocomplete_suggestions(suggestions); + } +} + +async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut AutocompleteFormState) -> bool { + if key == KeyCode::F(10) || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) { + return false; // Quit + } + + let action = match key { + // === AUTOCOMPLETE KEYS === + KeyCode::Tab => { + if state.is_autocomplete_active() { + Some(CanvasAction::SuggestionDown) // Navigate suggestions + } else if state.supports_autocomplete(state.current_field()) { + Some(CanvasAction::TriggerAutocomplete) // Manual trigger + } else { + Some(CanvasAction::NextField) // Normal tab + } + } + + KeyCode::BackTab => { + if state.is_autocomplete_active() { + Some(CanvasAction::SuggestionUp) + } else { + Some(CanvasAction::PrevField) + } + } + + KeyCode::Enter => { + if state.is_autocomplete_active() { + Some(CanvasAction::SelectSuggestion) // Apply suggestion + } else { + Some(CanvasAction::NextField) + } + } + + KeyCode::Esc => { + if state.is_autocomplete_active() { + Some(CanvasAction::ExitSuggestions) // Close autocomplete + } else { + Some(CanvasAction::Custom("toggle_mode".to_string())) + } + } + + // === STANDARD CANVAS KEYS === + 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::Backspace => Some(CanvasAction::DeleteBackward), + KeyCode::Delete => Some(CanvasAction::DeleteForward), + + // Character input + KeyCode::Char(c) if !modifiers.contains(KeyModifiers::CONTROL) => { + Some(CanvasAction::InsertChar(c)) + } + + _ => None, + }; + + if let Some(action) = action { + match execute_with_autocomplete(action.clone(), state).await { + Ok(result) => { + if let Some(msg) = result.message() { + state.debug_message = msg.to_string(); + } else { + state.debug_message = format!("Executed: {:?}", action); + } + true + } + Err(e) => { + state.debug_message = format!("Error: {}", e); + true + } + } + } else { + state.debug_message = format!("Unhandled key: {:?}", key); + true + } +} + +async fn run_app(terminal: &mut Terminal, mut state: AutocompleteFormState) -> io::Result<()> { + let theme = DemoTheme; + + loop { + terminal.draw(|f| ui(f, &state, &theme))?; + + if let Event::Key(key) = event::read()? { + let should_continue = handle_key_press(key.code, key.modifiers, &mut state).await; + if !should_continue { + break; + } + } + } + + Ok(()) +} + +fn ui(f: &mut Frame, state: &AutocompleteFormState, theme: &DemoTheme) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(8), + Constraint::Length(5), + ]) + .split(f.area()); + + // Render the canvas form + let active_field_rect = render_canvas( + f, + chunks[0], + state, + theme, + state.mode == AppMode::Edit, + &canvas::HighlightState::Off, + ); + + // Render autocomplete dropdown on top if active + if let Some(input_rect) = active_field_rect { + canvas::render_autocomplete_dropdown( + f, + chunks[0], + input_rect, + theme, + &state.autocomplete, + ); + } + + // Status info + let autocomplete_status = if state.is_autocomplete_active() { + if state.autocomplete.is_loading { + "Loading suggestions..." + } else if state.has_autocomplete_suggestions() { + "Use Tab/Shift+Tab to navigate, Enter to select, Esc to cancel" + } else { + "No suggestions found" + } + } else { + "Tab to trigger autocomplete" + }; + + let status_lines = vec![ + Line::from(Span::raw(format!("Mode: {:?} | Field: {}/{} | Cursor: {}", + state.mode, state.current_field + 1, state.fields.len(), state.cursor_pos))), + Line::from(Span::raw(format!("Autocomplete: {}", autocomplete_status))), + Line::from(Span::raw(state.debug_message.clone())), + Line::from(Span::raw("F10: Quit | Tab: Trigger/Navigate autocomplete | Enter: Select | Esc: Cancel/Toggle mode")), + ]; + + let status = Paragraph::new(status_lines) + .block(Block::default().borders(Borders::ALL).title("Status & Help")); + + f.render_widget(status, chunks[1]); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let state = AutocompleteFormState::new(); + + let res = run_app(&mut terminal, state).await; + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + Ok(()) +} diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index 2467268..223535b 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -1,66 +1,35 @@ // src/autocomplete/actions.rs -use crate::canvas::state::{CanvasState, ActionContext}; +use crate::canvas::state::CanvasState; use crate::autocomplete::state::AutocompleteCanvasState; use crate::canvas::actions::types::{CanvasAction, ActionResult}; use crate::canvas::actions::execute; use anyhow::Result; -/// Version for states that implement rich autocomplete -pub async fn execute_canvas_action_with_autocomplete( +/// Enhanced execute function for states that support autocomplete +/// This is the main entry point for autocomplete-aware canvas execution +/// +/// Use this instead of canvas::execute() if you want autocomplete behavior: +/// ```rust +/// execute_with_autocomplete(action, &mut state).await?; +/// ``` +pub async fn execute_with_autocomplete( action: CanvasAction, state: &mut S, - _ideal_cursor_column: &mut usize, // Keep for compatibility - _config: Option<&()>, // Remove CanvasConfig, keep for compatibility ) -> Result { - // Check for autocomplete-specific actions first match &action { - CanvasAction::InsertChar(_) => { - // Character insertion - execute then potentially trigger autocomplete - let result = execute(action, state).await?; - - // Check if we should trigger autocomplete after character insertion - if state.should_trigger_autocomplete() { - state.trigger_autocomplete_suggestions().await; - } - - Ok(result) - } + // === AUTOCOMPLETE-SPECIFIC ACTIONS === - _ => { - // For other actions, clear suggestions and execute - let result = execute(action, state).await?; - - // Clear autocomplete on navigation/other actions - match action { - CanvasAction::MoveLeft | CanvasAction::MoveRight | - CanvasAction::MoveUp | CanvasAction::MoveDown | - CanvasAction::NextField | CanvasAction::PrevField => { - state.clear_autocomplete_suggestions(); - } - _ => {} - } - - Ok(result) - } - } -} - -/// Handle autocomplete-specific actions (called from handle_feature_action) -pub async fn handle_autocomplete_action( - action: CanvasAction, - state: &mut S, - _context: &ActionContext, -) -> Result { - match action { CanvasAction::TriggerAutocomplete => { - // Manual trigger of autocomplete - state.trigger_autocomplete_suggestions().await; - Ok(ActionResult::success_with_message("Triggered autocomplete")) + if state.supports_autocomplete(state.current_field()) { + state.trigger_autocomplete_suggestions().await; + Ok(ActionResult::success_with_message("Triggered autocomplete")) + } else { + Ok(ActionResult::success_with_message("Autocomplete not supported for this field")) + } } CanvasAction::SuggestionUp => { - // Navigate up in suggestions if state.has_autocomplete_suggestions() { state.move_suggestion_selection(-1); Ok(ActionResult::success()) @@ -70,7 +39,6 @@ pub async fn handle_autocomplete_action { - // Navigate down in suggestions if state.has_autocomplete_suggestions() { state.move_suggestion_selection(1); Ok(ActionResult::success()) @@ -80,25 +48,123 @@ pub async fn handle_autocomplete_action { - // Accept the selected suggestion - if let Some(suggestion) = state.get_selected_suggestion() { - state.apply_suggestion(&suggestion); - state.clear_autocomplete_suggestions(); - Ok(ActionResult::success_with_message("Applied suggestion")) + if let Some(message) = state.apply_selected_suggestion() { + Ok(ActionResult::success_with_message(&message)) } else { - Ok(ActionResult::success_with_message("No suggestion selected")) + Ok(ActionResult::success_with_message("No suggestion to select")) } } CanvasAction::ExitSuggestions => { - // Cancel autocomplete state.clear_autocomplete_suggestions(); - Ok(ActionResult::success_with_message("Cleared suggestions")) + Ok(ActionResult::success_with_message("Closed autocomplete")) } + // === TEXT INSERTION WITH AUTO-TRIGGER === + + CanvasAction::InsertChar(_) => { + // First, execute the character insertion normally + let result = execute(action, state).await?; + + // After successful insertion, check if we should auto-trigger autocomplete + if result.is_success() && state.should_trigger_autocomplete() { + state.trigger_autocomplete_suggestions().await; + } + + Ok(result) + } + + // === NAVIGATION/EDITING ACTIONS (clear autocomplete first) === + + CanvasAction::MoveLeft | CanvasAction::MoveRight | + CanvasAction::MoveUp | CanvasAction::MoveDown | + CanvasAction::NextField | CanvasAction::PrevField | + CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { + // Clear autocomplete when navigating/editing + if state.is_autocomplete_active() { + state.clear_autocomplete_suggestions(); + } + + // Execute the action normally + execute(action, state).await + } + + // === ALL OTHER ACTIONS (normal execution) === + _ => { - // Not an autocomplete action - Ok(ActionResult::success_with_message("Not an autocomplete action")) + // For all other actions, just execute normally + execute(action, state).await } } } + +/// Helper function to integrate autocomplete actions with CanvasState.handle_feature_action() +/// +/// Use this in your CanvasState implementation like this: +/// ```rust +/// fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option { +/// // Try autocomplete first +/// if let Some(result) = handle_autocomplete_feature_action(action, self) { +/// return Some(result); +/// } +/// +/// // Handle your other custom actions... +/// None +/// } +/// ``` +pub fn handle_autocomplete_feature_action( + action: &CanvasAction, + state: &S, +) -> Option { + match action { + CanvasAction::TriggerAutocomplete => { + if state.supports_autocomplete(state.current_field()) { + if state.is_autocomplete_active() { + Some("Autocomplete already active".to_string()) + } else { + None // Let execute_with_autocomplete handle it + } + } else { + Some("Autocomplete not available for this field".to_string()) + } + } + + CanvasAction::SuggestionUp | CanvasAction::SuggestionDown => { + if state.is_autocomplete_active() { + None // Let execute_with_autocomplete handle navigation + } else { + Some("No autocomplete suggestions to navigate".to_string()) + } + } + + CanvasAction::SelectSuggestion => { + if state.has_autocomplete_suggestions() { + None // Let execute_with_autocomplete handle selection + } else { + Some("No suggestion to select".to_string()) + } + } + + CanvasAction::ExitSuggestions => { + if state.is_autocomplete_active() { + None // Let execute_with_autocomplete handle exit + } else { + Some("No autocomplete to close".to_string()) + } + } + + _ => None // Not an autocomplete action + } +} + +/// Legacy compatibility function - kept for backward compatibility +/// This is the old function signature, now it just wraps the new system +#[deprecated(note = "Use execute_with_autocomplete instead")] +pub async fn execute_canvas_action_with_autocomplete( + action: CanvasAction, + state: &mut S, + _ideal_cursor_column: &mut usize, // Ignored - new system manages this internally + _config: Option<&()>, // Ignored - no more config system +) -> Result { + execute_with_autocomplete(action, state).await +} diff --git a/canvas/src/autocomplete/gui.rs b/canvas/src/autocomplete/gui.rs index dab3dbc..52ffbbd 100644 --- a/canvas/src/autocomplete/gui.rs +++ b/canvas/src/autocomplete/gui.rs @@ -1,4 +1,4 @@ -// canvas/src/autocomplete/gui.rs +// src/autocomplete/gui.rs #[cfg(feature = "gui")] use ratatui::{ @@ -8,6 +8,7 @@ use ratatui::{ Frame, }; +// Use the correct import from our types module use crate::autocomplete::types::AutocompleteState; #[cfg(feature = "gui")] @@ -18,12 +19,12 @@ use unicode_width::UnicodeWidthStr; /// Render autocomplete dropdown - call this AFTER rendering canvas #[cfg(feature = "gui")] -pub fn render_autocomplete_dropdown( +pub fn render_autocomplete_dropdown( f: &mut Frame, frame_area: Rect, input_rect: Rect, theme: &T, - autocomplete_state: &AutocompleteState, + autocomplete_state: &AutocompleteState, ) { if !autocomplete_state.is_active { return; @@ -68,12 +69,12 @@ fn render_loading_indicator( /// Show actual suggestions list #[cfg(feature = "gui")] -fn render_suggestions_dropdown( +fn render_suggestions_dropdown( f: &mut Frame, frame_area: Rect, input_rect: Rect, theme: &T, - autocomplete_state: &AutocompleteState, + autocomplete_state: &AutocompleteState, ) { let display_texts: Vec<&str> = autocomplete_state.suggestions .iter() diff --git a/canvas/src/autocomplete/mod.rs b/canvas/src/autocomplete/mod.rs index 26583b9..31bcb4a 100644 --- a/canvas/src/autocomplete/mod.rs +++ b/canvas/src/autocomplete/mod.rs @@ -1,10 +1,22 @@ // src/autocomplete/mod.rs + pub mod types; -pub mod gui; pub mod state; pub mod actions; -// Re-export autocomplete types +#[cfg(feature = "gui")] +pub mod gui; + +// Re-export the main autocomplete API pub use types::{SuggestionItem, AutocompleteState}; pub use state::AutocompleteCanvasState; -pub use actions::execute_canvas_action_with_autocomplete; + +// Re-export the new action functions +pub use actions::{ + execute_with_autocomplete, + handle_autocomplete_feature_action, +}; + +// Re-export GUI functions if available +#[cfg(feature = "gui")] +pub use gui::render_autocomplete_dropdown; diff --git a/canvas/src/autocomplete/state.rs b/canvas/src/autocomplete/state.rs index 90f31e3..2cb6b73 100644 --- a/canvas/src/autocomplete/state.rs +++ b/canvas/src/autocomplete/state.rs @@ -1,14 +1,22 @@ -// canvas/src/state.rs +// src/autocomplete/state.rs use crate::canvas::state::CanvasState; /// OPTIONAL extension trait for states that want rich autocomplete functionality. /// Only implement this if you need the new autocomplete features. +/// +/// # User Workflow: +/// 1. User presses trigger key (Tab, Ctrl+K, etc.) +/// 2. User's key mapping calls CanvasAction::TriggerAutocomplete +/// 3. Library calls your trigger_autocomplete_suggestions() method +/// 4. You implement async fetching logic in that method +/// 5. You call set_autocomplete_suggestions() with results +/// 6. Library manages UI state and navigation pub trait AutocompleteCanvasState: CanvasState { /// Associated type for suggestion data (e.g., Hit, String, CustomType) type SuggestionData: Clone + Send + 'static; - /// Check if a field supports autocomplete + /// Check if a field supports autocomplete (user decides which fields) fn supports_autocomplete(&self, _field_index: usize) -> bool { false // Default: no autocomplete support } @@ -23,74 +31,152 @@ pub trait AutocompleteCanvasState: CanvasState { None // Default: no autocomplete state } - /// CLIENT API: Activate autocomplete for current field + // === PUBLIC API METHODS (called by library) === + + /// Activate autocomplete for current field (shows loading spinner) fn activate_autocomplete(&mut self) { - let current_field = self.current_field(); // Get field first + let current_field = self.current_field(); if let Some(state) = self.autocomplete_state_mut() { - state.activate(current_field); // Then use it + state.activate(current_field); } } - /// CLIENT API: Deactivate autocomplete + /// Deactivate autocomplete (hides dropdown) fn deactivate_autocomplete(&mut self) { if let Some(state) = self.autocomplete_state_mut() { state.deactivate(); } } - /// CLIENT API: Set suggestions (called after async fetch completes) + /// Set suggestions (called after your async fetch completes) fn set_autocomplete_suggestions(&mut self, suggestions: Vec>) { if let Some(state) = self.autocomplete_state_mut() { state.set_suggestions(suggestions); } } - /// CLIENT API: Set loading state + /// Set loading state (show/hide spinner) fn set_autocomplete_loading(&mut self, loading: bool) { if let Some(state) = self.autocomplete_state_mut() { state.is_loading = loading; } } - /// Check if autocomplete is currently active + // === QUERY METHODS === + + /// Check if autocomplete is currently active/visible fn is_autocomplete_active(&self) -> bool { self.autocomplete_state() .map(|state| state.is_active) .unwrap_or(false) } - /// Check if autocomplete is ready for interaction + /// Check if autocomplete has suggestions ready for navigation fn is_autocomplete_ready(&self) -> bool { self.autocomplete_state() .map(|state| state.is_ready()) .unwrap_or(false) } - /// INTERNAL: Apply selected autocomplete value to current field - fn apply_autocomplete_selection(&mut self) -> Option { - // First, get the selected value and display text (if any) - let selection_info = if let Some(state) = self.autocomplete_state() { - state.get_selected().map(|selected| { - (selected.value_to_store.clone(), selected.display_text.clone()) - }) - } else { - None - }; + /// Check if there are available suggestions + fn has_autocomplete_suggestions(&self) -> bool { + self.autocomplete_state() + .map(|state| !state.suggestions.is_empty()) + .unwrap_or(false) + } - // Apply the selection if we have one - if let Some((value, display)) = selection_info { - // Apply the value to current field - *self.get_current_input_mut() = value; - self.set_has_unsaved_changes(true); + // === USER-IMPLEMENTABLE METHODS === - // Deactivate autocomplete - if let Some(state_mut) = self.autocomplete_state_mut() { - state_mut.deactivate(); + /// Check if autocomplete should be triggered automatically (e.g., after typing 2+ chars) + /// Override this to implement your own trigger logic + fn should_trigger_autocomplete(&self) -> bool { + let current_input = self.get_current_input(); + let current_field = self.current_field(); + + self.supports_autocomplete(current_field) && + current_input.len() >= 2 && // Default: trigger after 2 chars + !self.is_autocomplete_active() + } + + /// **USER MUST IMPLEMENT**: Trigger autocomplete suggestions (async) + /// This is where you implement your API calls, caching, etc. + /// + /// # Example Implementation: + /// ```rust + /// async fn trigger_autocomplete_suggestions(&mut self) { + /// self.activate_autocomplete(); // Show loading state + /// + /// let query = self.get_current_input().to_string(); + /// let suggestions = my_api.search(&query).await.unwrap_or_default(); + /// + /// self.set_autocomplete_suggestions(suggestions); + /// } + /// ``` + async fn trigger_autocomplete_suggestions(&mut self) { + // Activate autocomplete UI + self.activate_autocomplete(); + + // Default: just show loading state + // User should override this to do actual async fetching + self.set_autocomplete_loading(true); + + // In a real implementation, you'd: + // 1. Get current input: let query = self.get_current_input(); + // 2. Make API call: let results = api.search(query).await; + // 3. Convert to suggestions: let suggestions = results.into_suggestions(); + // 4. Set suggestions: self.set_autocomplete_suggestions(suggestions); + } + + // === INTERNAL NAVIGATION METHODS (called by library actions) === + + /// Clear autocomplete suggestions and hide dropdown + fn clear_autocomplete_suggestions(&mut self) { + self.deactivate_autocomplete(); + } + + /// Move selection up/down in suggestions list + fn move_suggestion_selection(&mut self, direction: i32) { + if let Some(state) = self.autocomplete_state_mut() { + if direction > 0 { + state.select_next(); + } else { + state.select_previous(); } + } + } - Some(format!("Selected: {}", display)) + /// Get currently selected suggestion for display/application + fn get_selected_suggestion(&self) -> Option> { + self.autocomplete_state()? + .get_selected() + .cloned() + } + + /// Apply the selected suggestion to the current field + fn apply_suggestion(&mut self, suggestion: &crate::autocomplete::SuggestionItem) { + // Apply the value to current field + *self.get_current_input_mut() = suggestion.value_to_store.clone(); + self.set_has_unsaved_changes(true); + + // Clear autocomplete + self.clear_autocomplete_suggestions(); + } + + /// Apply the currently selected suggestion (convenience method) + fn apply_selected_suggestion(&mut self) -> Option { + if let Some(suggestion) = self.get_selected_suggestion() { + let display_text = suggestion.display_text.clone(); + self.apply_suggestion(&suggestion); + Some(format!("Applied: {}", display_text)) } else { None } } + + // === LEGACY COMPATIBILITY === + + /// INTERNAL: Apply selected autocomplete value to current field (legacy method) + fn apply_autocomplete_selection(&mut self) -> Option { + self.apply_selected_suggestion() + } } diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 6b2fd59..b0630ec 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,4 +1,4 @@ -// src/lib.rs - Updated to conditionally include autocomplete +// src/lib.rs pub mod canvas; @@ -23,8 +23,9 @@ pub use autocomplete::{ AutocompleteCanvasState, AutocompleteState, SuggestionItem, - actions::execute_with_autocomplete, + execute_with_autocomplete, + handle_autocomplete_feature_action, }; #[cfg(all(feature = "gui", feature = "autocomplete"))] -pub use autocomplete::gui::render_autocomplete_dropdown; +pub use autocomplete::render_autocomplete_dropdown; From 8f99aa79ec7f4c164374902d2745031cf5a83c58 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 31 Jul 2025 22:44:21 +0200 Subject: [PATCH 12/18] working autocomplete now, with backwards deprecation --- Cargo.lock | 1 + canvas/Cargo.toml | 3 ++- canvas/examples/autocomplete.rs | 43 +++++++++++++++++------------- canvas/src/autocomplete/actions.rs | 22 +++++++-------- canvas/src/autocomplete/state.rs | 35 ++++++++++++++---------- 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 577a663..2f3692c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,6 +475,7 @@ name = "canvas" version = "0.4.2" dependencies = [ "anyhow", + "async-trait", "common", "crossterm", "ratatui", diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 4a5bc51..3f1549d 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -22,6 +22,7 @@ thiserror = { workspace = true } tracing = "0.1.41" tracing-subscriber = "0.3.19" +async-trait = { workspace = true, optional = true } [dev-dependencies] tokio-test = "0.4.4" @@ -29,7 +30,7 @@ tokio-test = "0.4.4" [features] default = [] gui = ["ratatui"] -autocomplete = ["tokio"] +autocomplete = ["tokio", "async-trait"] [[example]] name = "autocomplete" diff --git a/canvas/examples/autocomplete.rs b/canvas/examples/autocomplete.rs index 7cc927d..955638f 100644 --- a/canvas/examples/autocomplete.rs +++ b/canvas/examples/autocomplete.rs @@ -24,8 +24,8 @@ use canvas::{ theme::CanvasTheme, }, autocomplete::{ - AutocompleteCanvasState, - AutocompleteState, + AutocompleteCanvasState, + AutocompleteState, SuggestionItem, execute_with_autocomplete, handle_autocomplete_feature_action, @@ -33,6 +33,9 @@ use canvas::{ CanvasAction, }; +// Add the async_trait import +use async_trait::async_trait; + // Simple theme implementation #[derive(Clone)] struct DemoTheme; @@ -64,7 +67,7 @@ struct AutocompleteFormState { mode: AppMode, has_changes: bool, debug_message: String, - + // Autocomplete state autocomplete: AutocompleteState, } @@ -97,14 +100,14 @@ impl AutocompleteFormState { impl CanvasState for AutocompleteFormState { 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) { + fn set_current_field(&mut self, index: usize) { self.current_field = index.min(self.fields.len().saturating_sub(1)); // Clear autocomplete when changing fields if self.is_autocomplete_active() { self.clear_autocomplete_suggestions(); } } - fn set_current_cursor_pos(&mut self, pos: usize) { + fn set_current_cursor_pos(&mut self, pos: usize) { let max_pos = if self.mode == AppMode::Edit { self.fields[self.current_field].len() } else { @@ -146,6 +149,8 @@ impl CanvasState for AutocompleteFormState { } } +// Add the #[async_trait] attribute to the implementation +#[async_trait] impl AutocompleteCanvasState for AutocompleteFormState { type SuggestionData = EmailSuggestion; @@ -165,9 +170,9 @@ impl AutocompleteCanvasState for AutocompleteFormState { fn should_trigger_autocomplete(&self) -> bool { let current_input = self.get_current_input(); let current_field = self.current_field(); - + // Trigger for email field when we have "@" and at least 1 more character - self.supports_autocomplete(current_field) && + self.supports_autocomplete(current_field) && current_input.contains('@') && current_input.len() > current_input.find('@').unwrap_or(0) + 1 && !self.is_autocomplete_active() @@ -181,7 +186,7 @@ impl AutocompleteCanvasState for AutocompleteFormState { // 2. Get current input for querying let query = self.get_current_input().to_string(); - + // 3. Extract domain part from email let domain_part = if let Some(at_pos) = query.find('@') { query[at_pos + 1..].to_string() @@ -195,19 +200,19 @@ impl AutocompleteCanvasState for AutocompleteFormState { let suggestions = tokio::task::spawn_blocking(move || { // Simulate network delay std::thread::sleep(std::time::Duration::from_millis(200)); - + // Create mock suggestions based on domain input let popular_domains = vec![ ("gmail.com", "Gmail"), - ("yahoo.com", "Yahoo Mail"), + ("yahoo.com", "Yahoo Mail"), ("outlook.com", "Outlook"), ("hotmail.com", "Hotmail"), ("company.com", "Company Email"), ("university.edu", "University"), ]; - + let mut results = Vec::new(); - + for (domain, provider) in popular_domains { if domain.starts_with(&domain_part) || domain_part.is_empty() { let full_email = format!("{}@{}", email_prefix, domain); @@ -221,7 +226,7 @@ impl AutocompleteCanvasState for AutocompleteFormState { )); } } - + results }).await.unwrap_or_default(); @@ -246,7 +251,7 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut Some(CanvasAction::NextField) // Normal tab } } - + KeyCode::BackTab => { if state.is_autocomplete_active() { Some(CanvasAction::SuggestionUp) @@ -254,7 +259,7 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut Some(CanvasAction::PrevField) } } - + KeyCode::Enter => { if state.is_autocomplete_active() { Some(CanvasAction::SelectSuggestion) // Apply suggestion @@ -262,7 +267,7 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut Some(CanvasAction::NextField) } } - + KeyCode::Esc => { if state.is_autocomplete_active() { Some(CanvasAction::ExitSuggestions) // Close autocomplete @@ -280,12 +285,12 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut Aut KeyCode::End => Some(CanvasAction::MoveLineEnd), KeyCode::Backspace => Some(CanvasAction::DeleteBackward), KeyCode::Delete => Some(CanvasAction::DeleteForward), - + // Character input KeyCode::Char(c) if !modifiers.contains(KeyModifiers::CONTROL) => { Some(CanvasAction::InsertChar(c)) } - + _ => None, }; @@ -371,7 +376,7 @@ fn ui(f: &mut Frame, state: &AutocompleteFormState, theme: &DemoTheme) { }; let status_lines = vec![ - Line::from(Span::raw(format!("Mode: {:?} | Field: {}/{} | Cursor: {}", + Line::from(Span::raw(format!("Mode: {:?} | Field: {}/{} | Cursor: {}", state.mode, state.current_field + 1, state.fields.len(), state.cursor_pos))), Line::from(Span::raw(format!("Autocomplete: {}", autocomplete_status))), Line::from(Span::raw(state.debug_message.clone())), diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index 223535b..4c86e6c 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -8,18 +8,18 @@ use anyhow::Result; /// Enhanced execute function for states that support autocomplete /// This is the main entry point for autocomplete-aware canvas execution -/// +/// /// Use this instead of canvas::execute() if you want autocomplete behavior: /// ```rust /// execute_with_autocomplete(action, &mut state).await?; /// ``` -pub async fn execute_with_autocomplete( +pub async fn execute_with_autocomplete( action: CanvasAction, state: &mut S, ) -> Result { match &action { // === AUTOCOMPLETE-SPECIFIC ACTIONS === - + CanvasAction::TriggerAutocomplete => { if state.supports_autocomplete(state.current_field()) { state.trigger_autocomplete_suggestions().await; @@ -61,7 +61,7 @@ pub async fn execute_with_autocomplete } // === TEXT INSERTION WITH AUTO-TRIGGER === - + CanvasAction::InsertChar(_) => { // First, execute the character insertion normally let result = execute(action, state).await?; @@ -75,7 +75,7 @@ pub async fn execute_with_autocomplete } // === NAVIGATION/EDITING ACTIONS (clear autocomplete first) === - + CanvasAction::MoveLeft | CanvasAction::MoveRight | CanvasAction::MoveUp | CanvasAction::MoveDown | CanvasAction::NextField | CanvasAction::PrevField | @@ -84,13 +84,13 @@ pub async fn execute_with_autocomplete if state.is_autocomplete_active() { state.clear_autocomplete_suggestions(); } - + // Execute the action normally execute(action, state).await } // === ALL OTHER ACTIONS (normal execution) === - + _ => { // For all other actions, just execute normally execute(action, state).await @@ -99,7 +99,7 @@ pub async fn execute_with_autocomplete } /// Helper function to integrate autocomplete actions with CanvasState.handle_feature_action() -/// +/// /// Use this in your CanvasState implementation like this: /// ```rust /// fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option { @@ -107,12 +107,12 @@ pub async fn execute_with_autocomplete /// if let Some(result) = handle_autocomplete_feature_action(action, self) { /// return Some(result); /// } -/// +/// /// // Handle your other custom actions... /// None /// } /// ``` -pub fn handle_autocomplete_feature_action( +pub fn handle_autocomplete_feature_action( action: &CanvasAction, state: &S, ) -> Option { @@ -160,7 +160,7 @@ pub fn handle_autocomplete_feature_action( +pub async fn execute_canvas_action_with_autocomplete( action: CanvasAction, state: &mut S, _ideal_cursor_column: &mut usize, // Ignored - new system manages this internally diff --git a/canvas/src/autocomplete/state.rs b/canvas/src/autocomplete/state.rs index 2cb6b73..46b0905 100644 --- a/canvas/src/autocomplete/state.rs +++ b/canvas/src/autocomplete/state.rs @@ -1,17 +1,19 @@ // src/autocomplete/state.rs use crate::canvas::state::CanvasState; +use async_trait::async_trait; /// OPTIONAL extension trait for states that want rich autocomplete functionality. /// Only implement this if you need the new autocomplete features. -/// +/// /// # User Workflow: /// 1. User presses trigger key (Tab, Ctrl+K, etc.) -/// 2. User's key mapping calls CanvasAction::TriggerAutocomplete +/// 2. User's key mapping calls CanvasAction::TriggerAutocomplete /// 3. Library calls your trigger_autocomplete_suggestions() method /// 4. You implement async fetching logic in that method /// 5. You call set_autocomplete_suggestions() with results /// 6. Library manages UI state and navigation +#[async_trait] pub trait AutocompleteCanvasState: CanvasState { /// Associated type for suggestion data (e.g., Hit, String, CustomType) type SuggestionData: Clone + Send + 'static; @@ -92,34 +94,39 @@ pub trait AutocompleteCanvasState: CanvasState { fn should_trigger_autocomplete(&self) -> bool { let current_input = self.get_current_input(); let current_field = self.current_field(); - - self.supports_autocomplete(current_field) && + + self.supports_autocomplete(current_field) && current_input.len() >= 2 && // Default: trigger after 2 chars !self.is_autocomplete_active() } /// **USER MUST IMPLEMENT**: Trigger autocomplete suggestions (async) /// This is where you implement your API calls, caching, etc. - /// + /// /// # Example Implementation: /// ```rust - /// async fn trigger_autocomplete_suggestions(&mut self) { - /// self.activate_autocomplete(); // Show loading state + /// #[async_trait] + /// impl AutocompleteCanvasState for MyState { + /// type SuggestionData = MyData; /// - /// let query = self.get_current_input().to_string(); - /// let suggestions = my_api.search(&query).await.unwrap_or_default(); - /// - /// self.set_autocomplete_suggestions(suggestions); + /// async fn trigger_autocomplete_suggestions(&mut self) { + /// self.activate_autocomplete(); // Show loading state + /// + /// let query = self.get_current_input().to_string(); + /// let suggestions = my_api.search(&query).await.unwrap_or_default(); + /// + /// self.set_autocomplete_suggestions(suggestions); + /// } /// } /// ``` async fn trigger_autocomplete_suggestions(&mut self) { // Activate autocomplete UI self.activate_autocomplete(); - + // Default: just show loading state // User should override this to do actual async fetching self.set_autocomplete_loading(true); - + // In a real implementation, you'd: // 1. Get current input: let query = self.get_current_input(); // 2. Make API call: let results = api.search(query).await; @@ -157,7 +164,7 @@ pub trait AutocompleteCanvasState: CanvasState { // Apply the value to current field *self.get_current_input_mut() = suggestion.value_to_store.clone(); self.set_has_unsaved_changes(true); - + // Clear autocomplete self.clear_autocomplete_suggestions(); } From 5c39386a3a2e88b71100209699c320af071facc6 Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 1 Aug 2025 22:54:05 +0200 Subject: [PATCH 13/18] completely redesign philosofy of this library --- canvas/Cargo.toml | 6 +- canvas/examples/autocomplete.rs | 389 ++++++++---------- canvas/src/autocomplete/actions.rs | 193 ++------- canvas/src/autocomplete/gui.rs | 46 ++- canvas/src/autocomplete/mod.rs | 1 - canvas/src/autocomplete/state.rs | 192 +-------- canvas/src/autocomplete/types.rs | 129 +----- .../src/canvas/actions/handlers/dispatcher.rs | 37 +- canvas/src/canvas/actions/handlers/edit.rs | 218 ++++------ .../src/canvas/actions/handlers/highlight.rs | 89 ++-- canvas/src/canvas/actions/handlers/mod.rs | 8 +- .../src/canvas/actions/handlers/readonly.rs | 185 ++++----- canvas/src/canvas/actions/mod.rs | 2 +- canvas/src/canvas/actions/types.rs | 45 +- canvas/src/canvas/gui.rs | 61 ++- canvas/src/canvas/mod.rs | 18 +- canvas/src/canvas/state.rs | 225 +++++----- canvas/src/data_provider.rs | 47 +++ canvas/src/editor.rs | 234 +++++++++++ canvas/src/lib.rs | 46 ++- 20 files changed, 961 insertions(+), 1210 deletions(-) create mode 100644 canvas/src/data_provider.rs create mode 100644 canvas/src/editor.rs diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 3f1549d..df1d04f 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -13,7 +13,7 @@ categories.workspace = true common = { path = "../common" } ratatui = { workspace = true, optional = true } crossterm = { workspace = true } -anyhow = { workspace = true } +anyhow.workspace = true tokio = { workspace = true, optional = true } toml = { workspace = true } serde = { workspace = true } @@ -22,7 +22,7 @@ thiserror = { workspace = true } tracing = "0.1.41" tracing-subscriber = "0.3.19" -async-trait = { workspace = true, optional = true } +async-trait.workspace = true [dev-dependencies] tokio-test = "0.4.4" @@ -30,7 +30,7 @@ tokio-test = "0.4.4" [features] default = [] gui = ["ratatui"] -autocomplete = ["tokio", "async-trait"] +autocomplete = ["tokio"] [[example]] name = "autocomplete" diff --git a/canvas/examples/autocomplete.rs b/canvas/examples/autocomplete.rs index 955638f..0237524 100644 --- a/canvas/examples/autocomplete.rs +++ b/canvas/examples/autocomplete.rs @@ -20,21 +20,14 @@ use canvas::{ canvas::{ gui::render_canvas, modes::AppMode, - state::{ActionContext, CanvasState}, theme::CanvasTheme, }, - autocomplete::{ - AutocompleteCanvasState, - AutocompleteState, - SuggestionItem, - execute_with_autocomplete, - handle_autocomplete_feature_action, - }, - CanvasAction, + autocomplete::gui::render_autocomplete_dropdown, + FormEditor, DataProvider, AutocompleteProvider, SuggestionItem, }; -// Add the async_trait import use async_trait::async_trait; +use anyhow::Result; // Simple theme implementation #[derive(Clone)] @@ -58,150 +51,94 @@ struct EmailSuggestion { provider: String, } -// Demo form state with autocomplete -struct AutocompleteFormState { - fields: Vec, - field_names: Vec, - current_field: usize, - cursor_pos: usize, - mode: AppMode, - has_changes: bool, - debug_message: String, +// =================================================================== +// SIMPLE DATA PROVIDER - Only business data, no UI concerns! +// =================================================================== - // Autocomplete state - autocomplete: AutocompleteState, +struct ContactForm { + // Only business data - no UI state! + name: String, + email: String, + phone: String, + city: String, } -impl AutocompleteFormState { +impl ContactForm { fn new() -> Self { Self { - fields: vec![ - "John Doe".to_string(), - "john@".to_string(), // Partial email to demonstrate autocomplete - "+1 234 567 8900".to_string(), - "San Francisco".to_string(), - ], - field_names: vec![ - "Name".to_string(), - "Email".to_string(), - "Phone".to_string(), - "City".to_string(), - ], - current_field: 1, // Start on email field - cursor_pos: 5, // Position after "john@" - mode: AppMode::Edit, - has_changes: false, - debug_message: "Type in email field, Tab to trigger autocomplete, Enter to select, Esc to cancel".to_string(), - autocomplete: AutocompleteState::new(), + name: "John Doe".to_string(), + email: "john@".to_string(), // Partial email for demo + phone: "+1 234 567 8900".to_string(), + city: "San Francisco".to_string(), } } } -impl CanvasState for AutocompleteFormState { - 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.min(self.fields.len().saturating_sub(1)); - // Clear autocomplete when changing fields - if self.is_autocomplete_active() { - self.clear_autocomplete_suggestions(); +// Simple trait implementation - only 4 methods! +impl DataProvider for ContactForm { + fn field_count(&self) -> usize { 4 } + + fn field_name(&self, index: usize) -> &str { + match index { + 0 => "Name", + 1 => "Email", + 2 => "Phone", + 3 => "City", + _ => "", } } - fn set_current_cursor_pos(&mut self, pos: usize) { - let max_pos = if self.mode == AppMode::Edit { - self.fields[self.current_field].len() - } else { - self.fields[self.current_field].len().saturating_sub(1) - }; - self.cursor_pos = pos.min(max_pos); - } - fn current_mode(&self) -> AppMode { self.mode } - fn get_current_input(&self) -> &str { &self.fields[self.current_field] } - fn get_current_input_mut(&mut self) -> &mut String { &mut self.fields[self.current_field] } - fn inputs(&self) -> Vec<&String> { self.fields.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; } - - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - // Handle autocomplete actions first - if let Some(result) = handle_autocomplete_feature_action(action, self) { - return Some(result); - } - - // Handle other custom actions - match action { - CanvasAction::Custom(cmd) => { - match cmd.as_str() { - "toggle_mode" => { - self.mode = match self.mode { - AppMode::Edit => AppMode::ReadOnly, - AppMode::ReadOnly => AppMode::Edit, - _ => AppMode::Edit, - }; - Some(format!("Switched to {:?} mode", self.mode)) - } - _ => None, - } - } - _ => None, + + fn field_value(&self, index: usize) -> &str { + match index { + 0 => &self.name, + 1 => &self.email, + 2 => &self.phone, + 3 => &self.city, + _ => "", } } -} - -// Add the #[async_trait] attribute to the implementation -#[async_trait] -impl AutocompleteCanvasState for AutocompleteFormState { - type SuggestionData = EmailSuggestion; - + + fn set_field_value(&mut self, index: usize, value: String) { + match index { + 0 => self.name = value, + 1 => self.email = value, + 2 => self.phone = value, + 3 => self.city = value, + _ => {} + } + } + fn supports_autocomplete(&self, field_index: usize) -> bool { - // Only enable autocomplete for email field (index 1) - field_index == 1 + field_index == 1 // Only email field } +} - fn autocomplete_state(&self) -> Option<&AutocompleteState> { - Some(&self.autocomplete) - } +// =================================================================== +// SIMPLE AUTOCOMPLETE PROVIDER - Only data fetching! +// =================================================================== - fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState> { - Some(&mut self.autocomplete) - } +struct EmailAutocomplete; - fn should_trigger_autocomplete(&self) -> bool { - let current_input = self.get_current_input(); - let current_field = self.current_field(); - - // Trigger for email field when we have "@" and at least 1 more character - self.supports_autocomplete(current_field) && - current_input.contains('@') && - current_input.len() > current_input.find('@').unwrap_or(0) + 1 && - !self.is_autocomplete_active() - } - - /// This is where the magic happens - user implements their own async fetching - async fn trigger_autocomplete_suggestions(&mut self) { - // 1. Activate UI (shows loading spinner) - self.activate_autocomplete(); - self.set_autocomplete_loading(true); - - // 2. Get current input for querying - let query = self.get_current_input().to_string(); - - // 3. Extract domain part from email - let domain_part = if let Some(at_pos) = query.find('@') { - query[at_pos + 1..].to_string() +#[async_trait] +impl AutocompleteProvider for EmailAutocomplete { + type SuggestionData = EmailSuggestion; + + async fn fetch_suggestions(&mut self, _field_index: usize, query: &str) + -> Result>> + { + // Extract domain part from email + let (email_prefix, domain_part) = if let Some(at_pos) = query.find('@') { + (query[..at_pos].to_string(), query[at_pos + 1..].to_string()) } else { - self.set_autocomplete_loading(false); - return; // No @ symbol, can't suggest + return Ok(Vec::new()); // No @ symbol }; - // 4. SIMULATE ASYNC API CALL (in real code, this would be HTTP request) - let email_prefix = query[..query.find('@').unwrap()].to_string(); + // Simulate async API call let suggestions = tokio::task::spawn_blocking(move || { // Simulate network delay std::thread::sleep(std::time::Duration::from_millis(200)); - // Create mock suggestions based on domain input + // Mock email suggestions let popular_domains = vec![ ("gmail.com", "Gmail"), ("yahoo.com", "Yahoo Mail"), @@ -212,110 +149,148 @@ impl AutocompleteCanvasState for AutocompleteFormState { ]; let mut results = Vec::new(); - for (domain, provider) in popular_domains { if domain.starts_with(&domain_part) || domain_part.is_empty() { let full_email = format!("{}@{}", email_prefix, domain); - results.push(SuggestionItem::new( - EmailSuggestion { + results.push(SuggestionItem { + data: EmailSuggestion { email: full_email.clone(), provider: provider.to_string(), }, - format!("{} ({})", full_email, provider), // display text - full_email, // value to store - )); + display_text: format!("{} ({})", full_email, provider), + value_to_store: full_email, + }); } } - results }).await.unwrap_or_default(); - // 5. Provide suggestions back to library - self.set_autocomplete_suggestions(suggestions); + Ok(suggestions) } } -async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut AutocompleteFormState) -> bool { +// =================================================================== +// APPLICATION STATE - Much simpler! +// =================================================================== + +struct AppState { + editor: FormEditor, + autocomplete: EmailAutocomplete, + debug_message: String, +} + +impl AppState { + fn new() -> Self { + let contact_form = ContactForm::new(); + let mut editor = FormEditor::new(contact_form); + + // Start on email field (index 1) at end of existing text + editor.set_mode(AppMode::Edit); + // TODO: Add method to set initial field/cursor position + + Self { + editor, + autocomplete: EmailAutocomplete, + debug_message: "Type in email field, Tab to trigger autocomplete, Enter to select, Esc to cancel".to_string(), + } + } +} + +// =================================================================== +// INPUT HANDLING - Much cleaner! +// =================================================================== + +async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut AppState) -> bool { if key == KeyCode::F(10) || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) { return false; // Quit } - let action = match key { + // Handle input based on key + let result = match key { // === AUTOCOMPLETE KEYS === KeyCode::Tab => { - if state.is_autocomplete_active() { - Some(CanvasAction::SuggestionDown) // Navigate suggestions - } else if state.supports_autocomplete(state.current_field()) { - Some(CanvasAction::TriggerAutocomplete) // Manual trigger + if state.editor.is_autocomplete_active() { + state.editor.autocomplete_next(); + Ok("Navigated to next suggestion".to_string()) + } else if state.editor.data_provider().supports_autocomplete(state.editor.current_field()) { + state.editor.trigger_autocomplete(&mut state.autocomplete).await + .map(|_| "Triggered autocomplete".to_string()) } else { - Some(CanvasAction::NextField) // Normal tab - } - } - - KeyCode::BackTab => { - if state.is_autocomplete_active() { - Some(CanvasAction::SuggestionUp) - } else { - Some(CanvasAction::PrevField) + state.editor.move_to_next_field(); + Ok("Moved to next field".to_string()) } } KeyCode::Enter => { - if state.is_autocomplete_active() { - Some(CanvasAction::SelectSuggestion) // Apply suggestion + if state.editor.is_autocomplete_active() { + if let Some(applied) = state.editor.apply_autocomplete() { + Ok(format!("Applied: {}", applied)) + } else { + Ok("No suggestion to apply".to_string()) + } } else { - Some(CanvasAction::NextField) + state.editor.move_to_next_field(); + Ok("Moved to next field".to_string()) } } KeyCode::Esc => { - if state.is_autocomplete_active() { - Some(CanvasAction::ExitSuggestions) // Close autocomplete + if state.editor.is_autocomplete_active() { + // Autocomplete will be cleared automatically by mode change + Ok("Cancelled autocomplete".to_string()) } else { - Some(CanvasAction::Custom("toggle_mode".to_string())) + // Toggle between edit and readonly mode + let new_mode = match state.editor.mode() { + AppMode::Edit => AppMode::ReadOnly, + _ => AppMode::Edit, + }; + state.editor.set_mode(new_mode); + Ok(format!("Switched to {:?} mode", new_mode)) } } - // === STANDARD CANVAS KEYS === - 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::Backspace => Some(CanvasAction::DeleteBackward), - KeyCode::Delete => Some(CanvasAction::DeleteForward), - - // Character input - KeyCode::Char(c) if !modifiers.contains(KeyModifiers::CONTROL) => { - Some(CanvasAction::InsertChar(c)) + // === MOVEMENT KEYS === + KeyCode::Left => { + state.editor.move_left(); + Ok("Moved left".to_string()) + } + KeyCode::Right => { + state.editor.move_right(); + Ok("Moved right".to_string()) + } + KeyCode::Up => { + state.editor.move_to_next_field(); // TODO: Add move_up method + Ok("Moved up".to_string()) + } + KeyCode::Down => { + state.editor.move_to_next_field(); // TODO: Add move_down method + Ok("Moved down".to_string()) } - _ => None, + // === TEXT INPUT === + KeyCode::Char(c) if !modifiers.contains(KeyModifiers::CONTROL) => { + state.editor.insert_char(c) + .map(|_| format!("Inserted '{}'", c)) + } + + KeyCode::Backspace => { + // TODO: Add delete_backward method to FormEditor + Ok("Backspace (not implemented yet)".to_string()) + } + + _ => Ok(format!("Unhandled key: {:?}", key)), }; - if let Some(action) = action { - match execute_with_autocomplete(action.clone(), state).await { - Ok(result) => { - if let Some(msg) = result.message() { - state.debug_message = msg.to_string(); - } else { - state.debug_message = format!("Executed: {:?}", action); - } - true - } - Err(e) => { - state.debug_message = format!("Error: {}", e); - true - } - } - } else { - state.debug_message = format!("Unhandled key: {:?}", key); - true + // Update debug message + match result { + Ok(msg) => state.debug_message = msg, + Err(e) => state.debug_message = format!("Error: {}", e), } + + true } -async fn run_app(terminal: &mut Terminal, mut state: AutocompleteFormState) -> io::Result<()> { +async fn run_app(terminal: &mut Terminal, mut state: AppState) -> io::Result<()> { let theme = DemoTheme; loop { @@ -332,7 +307,7 @@ async fn run_app(terminal: &mut Terminal, mut state: Autocomplete Ok(()) } -fn ui(f: &mut Frame, state: &AutocompleteFormState, theme: &DemoTheme) { +fn ui(f: &mut Frame, state: &AppState, theme: &DemoTheme) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -341,33 +316,31 @@ fn ui(f: &mut Frame, state: &AutocompleteFormState, theme: &DemoTheme) { ]) .split(f.area()); - // Render the canvas form + // Render the canvas form - much simpler! let active_field_rect = render_canvas( f, chunks[0], - state, + &state.editor, theme, - state.mode == AppMode::Edit, - &canvas::HighlightState::Off, ); - // Render autocomplete dropdown on top if active + // Render autocomplete dropdown if active if let Some(input_rect) = active_field_rect { - canvas::render_autocomplete_dropdown( + render_autocomplete_dropdown( f, chunks[0], input_rect, theme, - &state.autocomplete, + &state.editor, ); } // Status info - let autocomplete_status = if state.is_autocomplete_active() { - if state.autocomplete.is_loading { + let autocomplete_status = if state.editor.is_autocomplete_active() { + if state.editor.ui_state().is_autocomplete_loading() { "Loading suggestions..." - } else if state.has_autocomplete_suggestions() { - "Use Tab/Shift+Tab to navigate, Enter to select, Esc to cancel" + } else if !state.editor.suggestions().is_empty() { + "Use Tab to navigate, Enter to select, Esc to cancel" } else { "No suggestions found" } @@ -377,7 +350,10 @@ fn ui(f: &mut Frame, state: &AutocompleteFormState, theme: &DemoTheme) { let status_lines = vec![ Line::from(Span::raw(format!("Mode: {:?} | Field: {}/{} | Cursor: {}", - state.mode, state.current_field + 1, state.fields.len(), state.cursor_pos))), + state.editor.mode(), + state.editor.current_field() + 1, + state.editor.data_provider().field_count(), + state.editor.cursor_position()))), Line::from(Span::raw(format!("Autocomplete: {}", autocomplete_status))), Line::from(Span::raw(state.debug_message.clone())), Line::from(Span::raw("F10: Quit | Tab: Trigger/Navigate autocomplete | Enter: Select | Esc: Cancel/Toggle mode")), @@ -397,8 +373,7 @@ async fn main() -> Result<(), Box> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let state = AutocompleteFormState::new(); - + let state = AppState::new(); let res = run_app(&mut terminal, state).await; disable_raw_mode()?; diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs index 4c86e6c..1fa130d 100644 --- a/canvas/src/autocomplete/actions.rs +++ b/canvas/src/autocomplete/actions.rs @@ -1,170 +1,47 @@ // src/autocomplete/actions.rs +//! Legacy autocomplete actions - deprecated in favor of FormEditor -use crate::canvas::state::CanvasState; -use crate::autocomplete::state::AutocompleteCanvasState; use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::canvas::actions::execute; use anyhow::Result; -/// Enhanced execute function for states that support autocomplete -/// This is the main entry point for autocomplete-aware canvas execution -/// -/// Use this instead of canvas::execute() if you want autocomplete behavior: -/// ```rust +/// Legacy function - use FormEditor.trigger_autocomplete() instead +/// +/// # Migration Guide +/// +/// **Old way:** +/// ```rust,ignore /// execute_with_autocomplete(action, &mut state).await?; /// ``` -pub async fn execute_with_autocomplete( - action: CanvasAction, - state: &mut S, -) -> Result { - match &action { - // === AUTOCOMPLETE-SPECIFIC ACTIONS === - - CanvasAction::TriggerAutocomplete => { - if state.supports_autocomplete(state.current_field()) { - state.trigger_autocomplete_suggestions().await; - Ok(ActionResult::success_with_message("Triggered autocomplete")) - } else { - Ok(ActionResult::success_with_message("Autocomplete not supported for this field")) - } - } - - CanvasAction::SuggestionUp => { - if state.has_autocomplete_suggestions() { - state.move_suggestion_selection(-1); - Ok(ActionResult::success()) - } else { - Ok(ActionResult::success_with_message("No suggestions available")) - } - } - - CanvasAction::SuggestionDown => { - if state.has_autocomplete_suggestions() { - state.move_suggestion_selection(1); - Ok(ActionResult::success()) - } else { - Ok(ActionResult::success_with_message("No suggestions available")) - } - } - - CanvasAction::SelectSuggestion => { - if let Some(message) = state.apply_selected_suggestion() { - Ok(ActionResult::success_with_message(&message)) - } else { - Ok(ActionResult::success_with_message("No suggestion to select")) - } - } - - CanvasAction::ExitSuggestions => { - state.clear_autocomplete_suggestions(); - Ok(ActionResult::success_with_message("Closed autocomplete")) - } - - // === TEXT INSERTION WITH AUTO-TRIGGER === - - CanvasAction::InsertChar(_) => { - // First, execute the character insertion normally - let result = execute(action, state).await?; - - // After successful insertion, check if we should auto-trigger autocomplete - if result.is_success() && state.should_trigger_autocomplete() { - state.trigger_autocomplete_suggestions().await; - } - - Ok(result) - } - - // === NAVIGATION/EDITING ACTIONS (clear autocomplete first) === - - CanvasAction::MoveLeft | CanvasAction::MoveRight | - CanvasAction::MoveUp | CanvasAction::MoveDown | - CanvasAction::NextField | CanvasAction::PrevField | - CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { - // Clear autocomplete when navigating/editing - if state.is_autocomplete_active() { - state.clear_autocomplete_suggestions(); - } - - // Execute the action normally - execute(action, state).await - } - - // === ALL OTHER ACTIONS (normal execution) === - - _ => { - // For all other actions, just execute normally - execute(action, state).await - } - } -} - -/// Helper function to integrate autocomplete actions with CanvasState.handle_feature_action() -/// -/// Use this in your CanvasState implementation like this: -/// ```rust -/// fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option { -/// // Try autocomplete first -/// if let Some(result) = handle_autocomplete_feature_action(action, self) { -/// return Some(result); +/// +/// **New way:** +/// ```rust,ignore +/// let mut editor = FormEditor::new(your_data_provider); +/// match action { +/// CanvasAction::TriggerAutocomplete => { +/// editor.trigger_autocomplete(&mut autocomplete_provider).await?; /// } -/// -/// // Handle your other custom actions... -/// None +/// CanvasAction::InsertChar(c) => { +/// editor.insert_char(c)?; +/// } +/// // ... etc /// } /// ``` -pub fn handle_autocomplete_feature_action( - action: &CanvasAction, - state: &S, -) -> Option { - match action { - CanvasAction::TriggerAutocomplete => { - if state.supports_autocomplete(state.current_field()) { - if state.is_autocomplete_active() { - Some("Autocomplete already active".to_string()) - } else { - None // Let execute_with_autocomplete handle it - } - } else { - Some("Autocomplete not available for this field".to_string()) - } - } - - CanvasAction::SuggestionUp | CanvasAction::SuggestionDown => { - if state.is_autocomplete_active() { - None // Let execute_with_autocomplete handle navigation - } else { - Some("No autocomplete suggestions to navigate".to_string()) - } - } - - CanvasAction::SelectSuggestion => { - if state.has_autocomplete_suggestions() { - None // Let execute_with_autocomplete handle selection - } else { - Some("No suggestion to select".to_string()) - } - } - - CanvasAction::ExitSuggestions => { - if state.is_autocomplete_active() { - None // Let execute_with_autocomplete handle exit - } else { - Some("No autocomplete to close".to_string()) - } - } - - _ => None // Not an autocomplete action - } -} - -/// Legacy compatibility function - kept for backward compatibility -/// This is the old function signature, now it just wraps the new system -#[deprecated(note = "Use execute_with_autocomplete instead")] -pub async fn execute_canvas_action_with_autocomplete( - action: CanvasAction, - state: &mut S, - _ideal_cursor_column: &mut usize, // Ignored - new system manages this internally - _config: Option<&()>, // Ignored - no more config system +#[deprecated(note = "Use FormEditor.trigger_autocomplete() and related methods instead")] +pub async fn execute_with_autocomplete( + _action: CanvasAction, + _state: &mut T, ) -> Result { - execute_with_autocomplete(action, state).await + Err(anyhow::anyhow!( + "execute_with_autocomplete is deprecated. Use FormEditor API instead.\n\ + Migration: Replace CanvasState trait with DataProvider trait and use FormEditor." + )) +} + +/// Legacy function - use FormEditor methods instead +#[deprecated(note = "Use FormEditor methods instead")] +pub fn handle_autocomplete_feature_action( + _action: &CanvasAction, + _state: &T, +) -> Option { + Some("handle_autocomplete_feature_action is deprecated. Use FormEditor API instead.".to_string()) } diff --git a/canvas/src/autocomplete/gui.rs b/canvas/src/autocomplete/gui.rs index 52ffbbd..0f74cb0 100644 --- a/canvas/src/autocomplete/gui.rs +++ b/canvas/src/autocomplete/gui.rs @@ -1,4 +1,5 @@ // src/autocomplete/gui.rs +//! Autocomplete GUI updated to work with FormEditor #[cfg(feature = "gui")] use ratatui::{ @@ -8,32 +9,33 @@ use ratatui::{ Frame, }; -// Use the correct import from our types module -use crate::autocomplete::types::AutocompleteState; - #[cfg(feature = "gui")] use crate::canvas::theme::CanvasTheme; +use crate::data_provider::{DataProvider, SuggestionItem}; +use crate::editor::FormEditor; #[cfg(feature = "gui")] use unicode_width::UnicodeWidthStr; -/// Render autocomplete dropdown - call this AFTER rendering canvas +/// Render autocomplete dropdown for FormEditor - call this AFTER rendering canvas #[cfg(feature = "gui")] -pub fn render_autocomplete_dropdown( +pub fn render_autocomplete_dropdown( f: &mut Frame, frame_area: Rect, input_rect: Rect, theme: &T, - autocomplete_state: &AutocompleteState, + editor: &FormEditor, ) { - if !autocomplete_state.is_active { + let ui_state = editor.ui_state(); + + if !ui_state.is_autocomplete_active() { return; } - if autocomplete_state.is_loading { + if ui_state.autocomplete.is_loading { render_loading_indicator(f, frame_area, input_rect, theme); - } else if !autocomplete_state.suggestions.is_empty() { - render_suggestions_dropdown(f, frame_area, input_rect, theme, autocomplete_state); + } else if !editor.suggestions().is_empty() { + render_suggestions_dropdown(f, frame_area, input_rect, theme, editor.suggestions(), ui_state.autocomplete.selected_index); } } @@ -69,14 +71,15 @@ fn render_loading_indicator( /// Show actual suggestions list #[cfg(feature = "gui")] -fn render_suggestions_dropdown( +fn render_suggestions_dropdown( f: &mut Frame, frame_area: Rect, input_rect: Rect, theme: &T, - autocomplete_state: &AutocompleteState, + suggestions: &[SuggestionItem], + selected_index: Option, ) { - let display_texts: Vec<&str> = autocomplete_state.suggestions + let display_texts: Vec<&str> = suggestions .iter() .map(|item| item.display_text.as_str()) .collect(); @@ -96,19 +99,19 @@ fn render_suggestions_dropdown( // List items let items = create_suggestion_list_items( &display_texts, - autocomplete_state.selected_index, + selected_index, dropdown_dimensions.width, theme, ); let list = List::new(items).block(dropdown_block); let mut list_state = ListState::default(); - list_state.select(autocomplete_state.selected_index); + list_state.select(selected_index); f.render_stateful_widget(list, dropdown_area, &mut list_state); } -/// Calculate dropdown size based on suggestions - updated to match client dimensions +/// Calculate dropdown size based on suggestions #[cfg(feature = "gui")] fn calculate_dropdown_dimensions(display_texts: &[&str]) -> DropdownDimensions { let max_width = display_texts @@ -117,9 +120,9 @@ fn calculate_dropdown_dimensions(display_texts: &[&str]) -> DropdownDimensions { .max() .unwrap_or(0) as u16; - let horizontal_padding = 2; // Changed from 4 to 2 to match client - let width = (max_width + horizontal_padding).max(10); // Changed from 12 to 10 to match client - let height = (display_texts.len() as u16).min(5); // Removed +2 since no borders + let horizontal_padding = 2; + let width = (max_width + horizontal_padding).max(10); + let height = (display_texts.len() as u16).min(5); DropdownDimensions { width, height } } @@ -152,7 +155,7 @@ fn calculate_dropdown_position( dropdown_area } -/// Create styled list items - updated to match client spacing +/// Create styled list items #[cfg(feature = "gui")] fn create_suggestion_list_items<'a, T: CanvasTheme>( display_texts: &'a [&'a str], @@ -160,8 +163,7 @@ fn create_suggestion_list_items<'a, T: CanvasTheme>( dropdown_width: u16, theme: &T, ) -> Vec> { - let horizontal_padding = 2; // Changed from 4 to 2 to match client - let available_width = dropdown_width; // No border padding needed + let available_width = dropdown_width; display_texts .iter() diff --git a/canvas/src/autocomplete/mod.rs b/canvas/src/autocomplete/mod.rs index 31bcb4a..0ef3b62 100644 --- a/canvas/src/autocomplete/mod.rs +++ b/canvas/src/autocomplete/mod.rs @@ -9,7 +9,6 @@ pub mod gui; // Re-export the main autocomplete API pub use types::{SuggestionItem, AutocompleteState}; -pub use state::AutocompleteCanvasState; // Re-export the new action functions pub use actions::{ diff --git a/canvas/src/autocomplete/state.rs b/canvas/src/autocomplete/state.rs index 46b0905..6b4fe11 100644 --- a/canvas/src/autocomplete/state.rs +++ b/canvas/src/autocomplete/state.rs @@ -1,189 +1,9 @@ // src/autocomplete/state.rs +//! Simple autocomplete provider pattern - replaces complex trait -use crate::canvas::state::CanvasState; -use async_trait::async_trait; +// Re-export the main types from data_provider for backward compatibility +pub use crate::data_provider::{AutocompleteProvider, SuggestionItem}; -/// OPTIONAL extension trait for states that want rich autocomplete functionality. -/// Only implement this if you need the new autocomplete features. -/// -/// # User Workflow: -/// 1. User presses trigger key (Tab, Ctrl+K, etc.) -/// 2. User's key mapping calls CanvasAction::TriggerAutocomplete -/// 3. Library calls your trigger_autocomplete_suggestions() method -/// 4. You implement async fetching logic in that method -/// 5. You call set_autocomplete_suggestions() with results -/// 6. Library manages UI state and navigation -#[async_trait] -pub trait AutocompleteCanvasState: CanvasState { - /// Associated type for suggestion data (e.g., Hit, String, CustomType) - type SuggestionData: Clone + Send + 'static; - - /// Check if a field supports autocomplete (user decides which fields) - fn supports_autocomplete(&self, _field_index: usize) -> bool { - false // Default: no autocomplete support - } - - /// Get autocomplete state (read-only) - fn autocomplete_state(&self) -> Option<&crate::autocomplete::AutocompleteState> { - None // Default: no autocomplete state - } - - /// Get autocomplete state (mutable) - fn autocomplete_state_mut(&mut self) -> Option<&mut crate::autocomplete::AutocompleteState> { - None // Default: no autocomplete state - } - - // === PUBLIC API METHODS (called by library) === - - /// Activate autocomplete for current field (shows loading spinner) - fn activate_autocomplete(&mut self) { - let current_field = self.current_field(); - if let Some(state) = self.autocomplete_state_mut() { - state.activate(current_field); - } - } - - /// Deactivate autocomplete (hides dropdown) - fn deactivate_autocomplete(&mut self) { - if let Some(state) = self.autocomplete_state_mut() { - state.deactivate(); - } - } - - /// Set suggestions (called after your async fetch completes) - fn set_autocomplete_suggestions(&mut self, suggestions: Vec>) { - if let Some(state) = self.autocomplete_state_mut() { - state.set_suggestions(suggestions); - } - } - - /// Set loading state (show/hide spinner) - fn set_autocomplete_loading(&mut self, loading: bool) { - if let Some(state) = self.autocomplete_state_mut() { - state.is_loading = loading; - } - } - - // === QUERY METHODS === - - /// Check if autocomplete is currently active/visible - fn is_autocomplete_active(&self) -> bool { - self.autocomplete_state() - .map(|state| state.is_active) - .unwrap_or(false) - } - - /// Check if autocomplete has suggestions ready for navigation - fn is_autocomplete_ready(&self) -> bool { - self.autocomplete_state() - .map(|state| state.is_ready()) - .unwrap_or(false) - } - - /// Check if there are available suggestions - fn has_autocomplete_suggestions(&self) -> bool { - self.autocomplete_state() - .map(|state| !state.suggestions.is_empty()) - .unwrap_or(false) - } - - // === USER-IMPLEMENTABLE METHODS === - - /// Check if autocomplete should be triggered automatically (e.g., after typing 2+ chars) - /// Override this to implement your own trigger logic - fn should_trigger_autocomplete(&self) -> bool { - let current_input = self.get_current_input(); - let current_field = self.current_field(); - - self.supports_autocomplete(current_field) && - current_input.len() >= 2 && // Default: trigger after 2 chars - !self.is_autocomplete_active() - } - - /// **USER MUST IMPLEMENT**: Trigger autocomplete suggestions (async) - /// This is where you implement your API calls, caching, etc. - /// - /// # Example Implementation: - /// ```rust - /// #[async_trait] - /// impl AutocompleteCanvasState for MyState { - /// type SuggestionData = MyData; - /// - /// async fn trigger_autocomplete_suggestions(&mut self) { - /// self.activate_autocomplete(); // Show loading state - /// - /// let query = self.get_current_input().to_string(); - /// let suggestions = my_api.search(&query).await.unwrap_or_default(); - /// - /// self.set_autocomplete_suggestions(suggestions); - /// } - /// } - /// ``` - async fn trigger_autocomplete_suggestions(&mut self) { - // Activate autocomplete UI - self.activate_autocomplete(); - - // Default: just show loading state - // User should override this to do actual async fetching - self.set_autocomplete_loading(true); - - // In a real implementation, you'd: - // 1. Get current input: let query = self.get_current_input(); - // 2. Make API call: let results = api.search(query).await; - // 3. Convert to suggestions: let suggestions = results.into_suggestions(); - // 4. Set suggestions: self.set_autocomplete_suggestions(suggestions); - } - - // === INTERNAL NAVIGATION METHODS (called by library actions) === - - /// Clear autocomplete suggestions and hide dropdown - fn clear_autocomplete_suggestions(&mut self) { - self.deactivate_autocomplete(); - } - - /// Move selection up/down in suggestions list - fn move_suggestion_selection(&mut self, direction: i32) { - if let Some(state) = self.autocomplete_state_mut() { - if direction > 0 { - state.select_next(); - } else { - state.select_previous(); - } - } - } - - /// Get currently selected suggestion for display/application - fn get_selected_suggestion(&self) -> Option> { - self.autocomplete_state()? - .get_selected() - .cloned() - } - - /// Apply the selected suggestion to the current field - fn apply_suggestion(&mut self, suggestion: &crate::autocomplete::SuggestionItem) { - // Apply the value to current field - *self.get_current_input_mut() = suggestion.value_to_store.clone(); - self.set_has_unsaved_changes(true); - - // Clear autocomplete - self.clear_autocomplete_suggestions(); - } - - /// Apply the currently selected suggestion (convenience method) - fn apply_selected_suggestion(&mut self) -> Option { - if let Some(suggestion) = self.get_selected_suggestion() { - let display_text = suggestion.display_text.clone(); - self.apply_suggestion(&suggestion); - Some(format!("Applied: {}", display_text)) - } else { - None - } - } - - // === LEGACY COMPATIBILITY === - - /// INTERNAL: Apply selected autocomplete value to current field (legacy method) - fn apply_autocomplete_selection(&mut self) -> Option { - self.apply_selected_suggestion() - } -} +// Legacy compatibility - empty trait for migration +#[deprecated(note = "Use AutocompleteProvider instead")] +pub trait AutocompleteCanvasState {} diff --git a/canvas/src/autocomplete/types.rs b/canvas/src/autocomplete/types.rs index e9f96b2..dfaea81 100644 --- a/canvas/src/autocomplete/types.rs +++ b/canvas/src/autocomplete/types.rs @@ -1,126 +1,21 @@ -// canvas/src/autocomplete.rs +// src/autocomplete/types.rs +//! Legacy autocomplete types - deprecated -/// Generic suggestion item that clients push to canvas -#[derive(Debug, Clone)] -pub struct SuggestionItem { - /// The underlying data (client-specific, e.g., Hit, String, etc.) - pub data: T, - /// Text to display in the dropdown - pub display_text: String, - /// Value to store in the form field when selected - pub value_to_store: String, -} +// Re-export the new simplified types +pub use crate::data_provider::SuggestionItem; -impl SuggestionItem { - pub fn new(data: T, display_text: String, value_to_store: String) -> Self { - Self { - data, - display_text, - value_to_store, - } - } - - /// Convenience constructor for simple string suggestions - pub fn simple(data: T, text: String) -> Self { - Self { - data, - display_text: text.clone(), - value_to_store: text, - } - } -} - -/// Autocomplete state managed by canvas +/// Legacy type - use FormEditor instead +#[deprecated(note = "Use FormEditor instead")] #[derive(Debug, Clone)] pub struct AutocompleteState { - /// Whether autocomplete is currently active/visible - pub is_active: bool, - /// Whether suggestions are being loaded (for spinner/loading indicator) - pub is_loading: bool, - /// Current suggestions to display - pub suggestions: Vec>, - /// Currently selected suggestion index - pub selected_index: Option, - /// Field index that triggered autocomplete (for context) - pub active_field: Option, -} - -impl Default for AutocompleteState { - fn default() -> Self { - Self { - is_active: false, - is_loading: false, - suggestions: Vec::new(), - selected_index: None, - active_field: None, - } - } + _phantom: std::marker::PhantomData, } +#[allow(dead_code)] impl AutocompleteState { - pub fn new() -> Self { - Self::default() - } - - /// Activate autocomplete for a specific field - pub fn activate(&mut self, field_index: usize) { - self.is_active = true; - self.active_field = Some(field_index); - self.selected_index = None; - self.suggestions.clear(); - self.is_loading = true; - } - - /// Deactivate autocomplete and clear state - pub fn deactivate(&mut self) { - self.is_active = false; - self.is_loading = false; - self.suggestions.clear(); - self.selected_index = None; - self.active_field = None; - } - - /// Set suggestions and stop loading - pub fn set_suggestions(&mut self, suggestions: Vec>) { - self.suggestions = suggestions; - self.is_loading = false; - self.selected_index = if self.suggestions.is_empty() { - None - } else { - Some(0) - }; - } - - /// Move selection down - pub fn select_next(&mut self) { - if !self.suggestions.is_empty() { - let current = self.selected_index.unwrap_or(0); - self.selected_index = Some((current + 1) % self.suggestions.len()); - } - } - - /// Move selection up - pub fn select_previous(&mut self) { - if !self.suggestions.is_empty() { - let current = self.selected_index.unwrap_or(0); - self.selected_index = Some( - if current == 0 { - self.suggestions.len() - 1 - } else { - current - 1 - } - ); - } - } - - /// Get currently selected suggestion - pub fn get_selected(&self) -> Option<&SuggestionItem> { - self.selected_index - .and_then(|idx| self.suggestions.get(idx)) - } - - /// Check if autocomplete is ready for interaction (active and has suggestions) - pub fn is_ready(&self) -> bool { - self.is_active && !self.suggestions.is_empty() && !self.is_loading + /// Legacy method - use FormEditor.is_autocomplete_active() instead + #[deprecated(note = "Use FormEditor.is_autocomplete_active() instead")] + pub fn is_active(&self) -> bool { + false } } diff --git a/canvas/src/canvas/actions/handlers/dispatcher.rs b/canvas/src/canvas/actions/handlers/dispatcher.rs index 31dc0d3..6636574 100644 --- a/canvas/src/canvas/actions/handlers/dispatcher.rs +++ b/canvas/src/canvas/actions/handlers/dispatcher.rs @@ -1,43 +1,30 @@ // src/canvas/actions/handlers/dispatcher.rs -use crate::canvas::state::{CanvasState, ActionContext}; +use crate::canvas::state::EditorState; use crate::canvas::actions::{CanvasAction, ActionResult}; use crate::canvas::modes::AppMode; -use anyhow::Result; use super::{handle_edit_action, handle_readonly_action, handle_highlight_action}; -/// Main action dispatcher - routes actions to mode-specific handlers -pub async fn dispatch_action( +/// Internal action dispatcher - routes actions to mode-specific handlers +pub(crate) fn dispatch_action_internal( action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, -) -> Result { - // Check if the application wants to handle this action first - let context = ActionContext { - key_code: None, - ideal_cursor_column: *ideal_cursor_column, - current_input: state.get_current_input().to_string(), - current_field: state.current_field(), - }; - - if let Some(result) = state.handle_feature_action(&action, &context) { - return Ok(ActionResult::HandledByFeature(result)); - } - - // Route to mode-specific handler - match state.current_mode() { + editor_state: &mut EditorState, + current_text: &str, +) -> ActionResult { + // Route to mode-specific handler based on current mode + match editor_state.current_mode { AppMode::Edit => { - handle_edit_action(action, state, ideal_cursor_column).await + handle_edit_action(action, editor_state, current_text) } AppMode::ReadOnly => { - handle_readonly_action(action, state, ideal_cursor_column).await + handle_readonly_action(action, editor_state, current_text) } AppMode::Highlight => { - handle_highlight_action(action, state, ideal_cursor_column).await + handle_highlight_action(action, editor_state, current_text) } AppMode::General | AppMode::Command => { - Ok(ActionResult::success_with_message("Mode does not handle canvas actions directly")) + ActionResult::success_with_message("Mode does not handle canvas actions directly") } } } diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs index b86a0ba..c2706da 100644 --- a/canvas/src/canvas/actions/handlers/edit.rs +++ b/canvas/src/canvas/actions/handlers/edit.rs @@ -1,213 +1,143 @@ // src/canvas/actions/handlers/edit.rs -//! Edit mode action handler -//! -//! Handles user input when in edit mode, supporting text entry, deletion, -//! and cursor movement with edit-specific behavior (cursor can go past end of text). +//! Edit mode action handler with EditorState use crate::canvas::actions::types::{CanvasAction, ActionResult}; use crate::canvas::actions::movement::*; -use crate::canvas::state::CanvasState; -use anyhow::Result; +use crate::canvas::state::EditorState; /// Edit mode uses cursor-past-end behavior for text insertion const FOR_EDIT_MODE: bool = true; /// Handle actions in edit mode with edit-specific cursor behavior -/// -/// Edit mode allows text modification and uses cursor positioning that can -/// go past the end of existing text to facilitate insertion. -/// -/// # Arguments -/// * `action` - The action to perform -/// * `state` - Mutable canvas state -/// * `ideal_cursor_column` - Desired column for vertical movement (maintained across line changes) -pub async fn handle_edit_action( +pub(crate) fn handle_edit_action( action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, -) -> Result { + editor_state: &mut EditorState, + current_text: &str, +) -> ActionResult { match action { - CanvasAction::InsertChar(c) => { - // Insert character at cursor position and advance cursor - let cursor_pos = state.current_cursor_pos(); - let input = state.get_current_input_mut(); - input.insert(cursor_pos, c); - state.set_current_cursor_pos(cursor_pos + 1); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = cursor_pos + 1; - Ok(ActionResult::success()) - } - - CanvasAction::DeleteBackward => { - // Delete character before cursor (Backspace behavior) - let cursor_pos = state.current_cursor_pos(); - if cursor_pos > 0 { - let input = state.get_current_input_mut(); - input.remove(cursor_pos - 1); - state.set_current_cursor_pos(cursor_pos - 1); - state.set_has_unsaved_changes(true); - *ideal_cursor_column = cursor_pos - 1; - } - Ok(ActionResult::success()) - } - - CanvasAction::DeleteForward => { - // Delete character at cursor position (Delete key behavior) - let cursor_pos = state.current_cursor_pos(); - let input = state.get_current_input_mut(); - if cursor_pos < input.len() { - input.remove(cursor_pos); - state.set_has_unsaved_changes(true); - } - Ok(ActionResult::success()) - } - + // Note: Text insertion is handled at the FormEditor level + // These handlers only deal with cursor movement and navigation + // Cursor movement actions CanvasAction::MoveLeft => { - let new_pos = move_left(state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + let new_pos = move_left(editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveRight => { - let current_input = state.get_current_input(); - let current_pos = state.current_cursor_pos(); - let new_pos = move_right(current_pos, current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + let new_pos = move_right(editor_state.cursor_pos, current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } // Field navigation (treating single-line fields as "lines") CanvasAction::MoveUp => { - let current_field = state.current_field(); - if current_field > 0 { - state.set_current_field(current_field - 1); - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); + if editor_state.current_field > 0 { + editor_state.current_field -= 1; + let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveDown => { - let current_field = state.current_field(); - let total_fields = state.fields().len(); - if current_field < total_fields - 1 { - state.set_current_field(current_field + 1); - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - } - Ok(ActionResult::success()) + // Note: field count validation happens at FormEditor level + editor_state.current_field += 1; + let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + ActionResult::success() } // Line-based movement CanvasAction::MoveLineStart => { let new_pos = line_start_position(); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveLineEnd => { - let current_input = state.get_current_input(); - let new_pos = line_end_position(current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + let new_pos = line_end_position(current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } // Document-level movement (first/last field) CanvasAction::MoveFirstLine => { - state.set_current_field(0); - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, 0, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + editor_state.current_field = 0; + let new_pos = safe_cursor_position(current_text, 0, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveLastLine => { - let last_field = state.fields().len() - 1; - state.set_current_field(last_field); - let current_input = state.get_current_input(); - let new_pos = line_end_position(current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + // Note: field count validation happens at FormEditor level + let new_pos = line_end_position(current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } // Word-based movement CanvasAction::MoveWordNext => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + if !current_text.is_empty() { + let new_pos = find_next_word_start(current_text, editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordEnd => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_word_end(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + if !current_text.is_empty() { + let new_pos = find_word_end(current_text, editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordPrev => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + if !current_text.is_empty() { + let new_pos = find_prev_word_start(current_text, editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordEndPrev => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_end(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + if !current_text.is_empty() { + let new_pos = find_prev_word_end(current_text, editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; } - Ok(ActionResult::success()) + ActionResult::success() } - // Field navigation with simple wrapping behavior + // Field navigation - handled at FormEditor level for bounds checking CanvasAction::NextField | CanvasAction::PrevField => { - let current_field = state.current_field(); - let total_fields = state.fields().len(); + ActionResult::success_with_message("Field navigation handled by FormEditor") + } - let new_field = match action { - CanvasAction::NextField => { - (current_field + 1) % total_fields // Simple wrap - } - CanvasAction::PrevField => { - if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Simple wrap - } - _ => unreachable!(), - }; - - state.set_current_field(new_field); - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - Ok(ActionResult::success()) + // Text editing actions - handled at FormEditor level + CanvasAction::InsertChar(_) | + CanvasAction::DeleteBackward | + CanvasAction::DeleteForward => { + ActionResult::success_with_message("Text editing handled by FormEditor") } CanvasAction::Custom(action_str) => { - Ok(ActionResult::success_with_message(&format!("Custom edit action: {}", action_str))) + ActionResult::success_with_message(&format!("Custom edit action: {}", action_str)) } _ => { - Ok(ActionResult::success_with_message("Action not implemented for edit mode")) + ActionResult::success_with_message("Action not implemented for edit mode") } } } diff --git a/canvas/src/canvas/actions/handlers/highlight.rs b/canvas/src/canvas/actions/handlers/highlight.rs index 66b8604..77314f5 100644 --- a/canvas/src/canvas/actions/handlers/highlight.rs +++ b/canvas/src/canvas/actions/handlers/highlight.rs @@ -1,104 +1,97 @@ // src/canvas/actions/handlers/highlight.rs +//! Highlight mode action handler with EditorState use crate::canvas::actions::types::{CanvasAction, ActionResult}; use crate::canvas::actions::movement::*; -use crate::canvas::state::CanvasState; -use anyhow::Result; +use crate::canvas::state::EditorState; const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior /// Handle actions in highlight/visual mode -/// TODO: Implement selection logic and highlight-specific behaviors -pub async fn handle_highlight_action( +pub(crate) fn handle_highlight_action( action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, -) -> Result { + editor_state: &mut EditorState, + current_text: &str, +) -> ActionResult { match action { // Movement actions work similar to read-only mode but with selection CanvasAction::MoveLeft => { - let new_pos = move_left(state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + let new_pos = move_left(editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; // TODO: Update selection range - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveRight => { - let current_input = state.get_current_input(); - let current_pos = state.current_cursor_pos(); - let new_pos = move_right(current_pos, current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + let new_pos = move_right(editor_state.cursor_pos, current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; // TODO: Update selection range - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordNext => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); - let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(final_pos); - *ideal_cursor_column = final_pos; + if !current_text.is_empty() { + let new_pos = find_next_word_start(current_text, editor_state.cursor_pos); + let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = final_pos; + editor_state.ideal_cursor_column = final_pos; // TODO: Update selection range } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordEnd => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_word_end(current_input, state.current_cursor_pos()); - let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(final_pos); - *ideal_cursor_column = final_pos; + if !current_text.is_empty() { + let new_pos = find_word_end(current_text, editor_state.cursor_pos); + let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = final_pos; + editor_state.ideal_cursor_column = final_pos; // TODO: Update selection range } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordPrev => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + if !current_text.is_empty() { + let new_pos = find_prev_word_start(current_text, editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; // TODO: Update selection range } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveLineStart => { let new_pos = line_start_position(); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; // TODO: Update selection range - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveLineEnd => { - let current_input = state.get_current_input(); - let new_pos = line_end_position(current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + let new_pos = line_end_position(current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; // TODO: Update selection range - Ok(ActionResult::success()) + ActionResult::success() } // Highlight mode doesn't handle editing actions CanvasAction::InsertChar(_) | CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { - Ok(ActionResult::success_with_message("Action not available in highlight mode")) + ActionResult::success_with_message("Action not available in highlight mode") } CanvasAction::Custom(action_str) => { - Ok(ActionResult::success_with_message(&format!("Custom highlight action: {}", action_str))) + ActionResult::success_with_message(&format!("Custom highlight action: {}", action_str)) } _ => { - Ok(ActionResult::success_with_message("Action not implemented for highlight mode")) + ActionResult::success_with_message("Action not implemented for highlight mode") } } } diff --git a/canvas/src/canvas/actions/handlers/mod.rs b/canvas/src/canvas/actions/handlers/mod.rs index 3bbbe8f..1810a00 100644 --- a/canvas/src/canvas/actions/handlers/mod.rs +++ b/canvas/src/canvas/actions/handlers/mod.rs @@ -5,7 +5,7 @@ pub mod readonly; pub mod highlight; pub mod dispatcher; -pub use edit::handle_edit_action; -pub use readonly::handle_readonly_action; -pub use highlight::handle_highlight_action; -pub use dispatcher::dispatch_action; +pub use edit::*; +pub use readonly::*; +pub use highlight::*; +pub use dispatcher::*; diff --git a/canvas/src/canvas/actions/handlers/readonly.rs b/canvas/src/canvas/actions/handlers/readonly.rs index 19201bd..551457d 100644 --- a/canvas/src/canvas/actions/handlers/readonly.rs +++ b/canvas/src/canvas/actions/handlers/readonly.rs @@ -1,183 +1,136 @@ // src/canvas/actions/handlers/readonly.rs +//! ReadOnly mode action handler with EditorState use crate::canvas::actions::types::{CanvasAction, ActionResult}; use crate::canvas::actions::movement::*; -use crate::canvas::state::CanvasState; -use anyhow::Result; +use crate::canvas::state::EditorState; const FOR_EDIT_MODE: bool = false; // Read-only mode flag /// Handle actions in read-only mode with read-only specific cursor behavior -pub async fn handle_readonly_action( +pub(crate) fn handle_readonly_action( action: CanvasAction, - state: &mut S, - ideal_cursor_column: &mut usize, -) -> Result { + editor_state: &mut EditorState, + current_text: &str, +) -> ActionResult { match action { CanvasAction::MoveLeft => { - let new_pos = move_left(state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + let new_pos = move_left(editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveRight => { - let current_input = state.get_current_input(); - let current_pos = state.current_cursor_pos(); - let new_pos = move_right(current_pos, current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + let new_pos = move_right(editor_state.cursor_pos, current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveUp => { - let current_field = state.current_field(); - let new_field = current_field.saturating_sub(1); - state.set_current_field(new_field); - - // Apply ideal cursor column with read-only bounds - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - Ok(ActionResult::success()) + if editor_state.current_field > 0 { + editor_state.current_field -= 1; + let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + } + ActionResult::success() } CanvasAction::MoveDown => { - let current_field = state.current_field(); - let total_fields = state.fields().len(); - if total_fields == 0 { - return Ok(ActionResult::success_with_message("No fields to navigate")); - } - - let new_field = (current_field + 1).min(total_fields - 1); - state.set_current_field(new_field); - - // Apply ideal cursor column with read-only bounds - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - Ok(ActionResult::success()) + // Note: bounds checking happens at FormEditor level + editor_state.current_field += 1; + let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + ActionResult::success() } CanvasAction::MoveFirstLine => { - let total_fields = state.fields().len(); - if total_fields == 0 { - return Ok(ActionResult::success_with_message("No fields to navigate")); - } - - state.set_current_field(0); - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + editor_state.current_field = 0; + let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveLastLine => { - let total_fields = state.fields().len(); - if total_fields == 0 { - return Ok(ActionResult::success_with_message("No fields to navigate")); - } - - let last_field = total_fields - 1; - state.set_current_field(last_field); - let current_input = state.get_current_input(); - let new_pos = safe_cursor_position(current_input, *ideal_cursor_column, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + // Note: field count validation happens at FormEditor level + let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveLineStart => { let new_pos = line_start_position(); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveLineEnd => { - let current_input = state.get_current_input(); - let new_pos = line_end_position(current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; - Ok(ActionResult::success()) + let new_pos = line_end_position(current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; + ActionResult::success() } CanvasAction::MoveWordNext => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_next_word_start(current_input, state.current_cursor_pos()); - let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(final_pos); - *ideal_cursor_column = final_pos; + if !current_text.is_empty() { + let new_pos = find_next_word_start(current_text, editor_state.cursor_pos); + let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = final_pos; + editor_state.ideal_cursor_column = final_pos; } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordEnd => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let current_pos = state.current_cursor_pos(); - let new_pos = find_word_end(current_input, current_pos); - let final_pos = clamp_cursor_position(new_pos, current_input, FOR_EDIT_MODE); - state.set_current_cursor_pos(final_pos); - *ideal_cursor_column = final_pos; + if !current_text.is_empty() { + let new_pos = find_word_end(current_text, editor_state.cursor_pos); + let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); + editor_state.cursor_pos = final_pos; + editor_state.ideal_cursor_column = final_pos; } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordPrev => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_start(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + if !current_text.is_empty() { + let new_pos = find_prev_word_start(current_text, editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; } - Ok(ActionResult::success()) + ActionResult::success() } CanvasAction::MoveWordEndPrev => { - let current_input = state.get_current_input(); - if !current_input.is_empty() { - let new_pos = find_prev_word_end(current_input, state.current_cursor_pos()); - state.set_current_cursor_pos(new_pos); - *ideal_cursor_column = new_pos; + if !current_text.is_empty() { + let new_pos = find_prev_word_end(current_text, editor_state.cursor_pos); + editor_state.cursor_pos = new_pos; + editor_state.ideal_cursor_column = new_pos; } - Ok(ActionResult::success()) + ActionResult::success() } + // Field navigation - handled at FormEditor level CanvasAction::NextField | CanvasAction::PrevField => { - let current_field = state.current_field(); - let total_fields = state.fields().len(); - - let new_field = match action { - CanvasAction::NextField => { - (current_field + 1) % total_fields // Simple wrap - } - CanvasAction::PrevField => { - if current_field == 0 { total_fields - 1 } else { current_field - 1 } // Simple wrap - } - _ => unreachable!(), - }; - - state.set_current_field(new_field); - *ideal_cursor_column = state.current_cursor_pos(); - Ok(ActionResult::success()) + ActionResult::success_with_message("Field navigation handled by FormEditor") } // Read-only mode doesn't handle editing actions CanvasAction::InsertChar(_) | CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { - Ok(ActionResult::success_with_message("Action not available in read-only mode")) + ActionResult::success_with_message("Action not available in read-only mode") } CanvasAction::Custom(action_str) => { - Ok(ActionResult::success_with_message(&format!("Custom readonly action: {}", action_str))) + ActionResult::success_with_message(&format!("Custom readonly action: {}", action_str)) } _ => { - Ok(ActionResult::success_with_message("Action not implemented for read-only mode")) + ActionResult::success_with_message("Action not implemented for read-only mode") } } } diff --git a/canvas/src/canvas/actions/mod.rs b/canvas/src/canvas/actions/mod.rs index 803d179..21a80f8 100644 --- a/canvas/src/canvas/actions/mod.rs +++ b/canvas/src/canvas/actions/mod.rs @@ -5,4 +5,4 @@ pub mod handlers; pub mod movement; // Re-export the main API -pub use types::{CanvasAction, ActionResult, execute}; +pub use types::{CanvasAction, ActionResult}; diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index 4f4d163..d5a2de4 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -1,7 +1,6 @@ // src/canvas/actions/types.rs -use crate::canvas::state::CanvasState; -use anyhow::Result; +use crate::canvas::state::EditorState; /// All available canvas actions #[derive(Debug, Clone, PartialEq)] @@ -11,35 +10,35 @@ pub enum CanvasAction { MoveRight, MoveUp, MoveDown, - + // Word movement MoveWordNext, MoveWordPrev, MoveWordEnd, MoveWordEndPrev, - + // Line movement MoveLineStart, MoveLineEnd, - + // Field movement NextField, PrevField, MoveFirstLine, MoveLastLine, - + // Editing actions InsertChar(char), DeleteBackward, DeleteForward, - + // Autocomplete actions TriggerAutocomplete, SuggestionUp, SuggestionDown, SelectSuggestion, ExitSuggestions, - + // Custom actions Custom(String), } @@ -58,23 +57,23 @@ impl ActionResult { pub fn success() -> Self { Self::Success } - + pub fn success_with_message(msg: &str) -> Self { Self::Message(msg.to_string()) } - + pub fn handled_by_app(msg: &str) -> Self { Self::HandledByApp(msg.to_string()) } - + pub fn error(msg: &str) -> Self { Self::Error(msg.to_string()) } - + pub fn is_success(&self) -> bool { matches!(self, Self::Success | Self::Message(_) | Self::HandledByApp(_) | Self::HandledByFeature(_)) } - + pub fn message(&self) -> Option<&str> { match self { Self::Message(msg) | Self::HandledByApp(msg) | Self::HandledByFeature(msg) | Self::Error(msg) => Some(msg), @@ -83,17 +82,13 @@ impl ActionResult { } } -/// Execute a canvas action on the given state -pub async fn execute( - action: CanvasAction, - state: &mut S, -) -> Result { - let mut ideal_cursor_column = 0; - - super::handlers::dispatch_action(action, state, &mut ideal_cursor_column).await -} - impl CanvasAction { + /// Internal method used by FormEditor + pub(crate) fn apply_to_editor_state(self, editor_state: &mut EditorState, current_text: &str) -> ActionResult { + // Internal method used by FormEditor + crate::canvas::actions::handlers::dispatch_action_internal(self, editor_state, current_text) + } + /// Get a human-readable description of this action pub fn description(&self) -> &'static str { match self { @@ -111,7 +106,7 @@ impl CanvasAction { Self::PrevField => "previous field", Self::MoveFirstLine => "first field", Self::MoveLastLine => "last field", - Self::InsertChar(c) => "insert character", + Self::InsertChar(_c) => "insert character", Self::DeleteBackward => "delete backward", Self::DeleteForward => "delete forward", Self::TriggerAutocomplete => "trigger autocomplete", @@ -119,7 +114,7 @@ impl CanvasAction { Self::SuggestionDown => "suggestion down", Self::SelectSuggestion => "select suggestion", Self::ExitSuggestions => "exit suggestions", - Self::Custom(name) => "custom action", + Self::Custom(_name) => "custom action", } } diff --git a/canvas/src/canvas/gui.rs b/canvas/src/canvas/gui.rs index 4e06985..af7131a 100644 --- a/canvas/src/canvas/gui.rs +++ b/canvas/src/canvas/gui.rs @@ -1,4 +1,5 @@ -// canvas/src/canvas/gui.rs +// src/canvas/gui.rs +//! Canvas GUI updated to work with FormEditor #[cfg(feature = "gui")] use ratatui::{ @@ -9,29 +10,43 @@ use ratatui::{ Frame, }; -use crate::canvas::state::CanvasState; -use crate::canvas::modes::HighlightState; - #[cfg(feature = "gui")] use crate::canvas::theme::CanvasTheme; +use crate::canvas::modes::HighlightState; +use crate::data_provider::DataProvider; +use crate::editor::FormEditor; #[cfg(feature = "gui")] use std::cmp::{max, min}; /// Render ONLY the canvas form fields - no autocomplete +/// Updated to work with FormEditor instead of CanvasState trait #[cfg(feature = "gui")] -pub fn render_canvas( +pub fn render_canvas( f: &mut Frame, area: Rect, - form_state: &impl CanvasState, + editor: &FormEditor, theme: &T, - is_edit_mode: bool, - highlight_state: &HighlightState, ) -> Option { - let fields: Vec<&str> = form_state.fields(); - let current_field_idx = form_state.current_field(); - let inputs: Vec<&String> = form_state.inputs(); - + let ui_state = editor.ui_state(); + let data_provider = editor.data_provider(); + + // Build field information + let field_count = data_provider.field_count(); + let mut fields: Vec<&str> = Vec::with_capacity(field_count); + let mut inputs: Vec = Vec::with_capacity(field_count); + + for i in 0..field_count { + fields.push(data_provider.field_name(i)); + inputs.push(data_provider.field_value(i).to_string()); + } + + let current_field_idx = ui_state.current_field(); + let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit); + + // For now, create a default highlight state (TODO: get from editor state) + let highlight_state = HighlightState::Off; + render_canvas_fields( f, area, @@ -40,11 +55,13 @@ pub fn render_canvas( &inputs, theme, is_edit_mode, - highlight_state, - form_state.current_cursor_pos(), - form_state.has_unsaved_changes(), - |i| form_state.get_display_value_for_field(i).to_string(), - |i| form_state.has_display_override(i), + &highlight_state, + ui_state.cursor_position(), + false, // TODO: track unsaved changes in editor + |i| { + data_provider.display_value(i).unwrap_or(data_provider.field_value(i)).to_string() + }, + |i| data_provider.display_value(i).is_some(), ) } @@ -55,7 +72,7 @@ fn render_canvas_fields( area: Rect, fields: &[&str], current_field_idx: &usize, - inputs: &[&String], + inputs: &[String], theme: &T, is_edit_mode: bool, highlight_state: &HighlightState, @@ -112,7 +129,7 @@ where // Render field values and return active field rect render_field_values( f, - input_rows.to_vec(), // Fix: Convert Rc<[Rect]> to Vec + input_rows.to_vec(), inputs, current_field_idx, theme, @@ -154,7 +171,7 @@ fn render_field_labels( fn render_field_values( f: &mut Frame, input_rows: Vec, - inputs: &[&String], + inputs: &[String], current_field_idx: &usize, theme: &T, highlight_state: &HighlightState, @@ -171,7 +188,7 @@ where for (i, _input) in inputs.iter().enumerate() { let is_active = i == *current_field_idx; let text = get_display_value(i); - + // Apply highlighting let line = apply_highlighting( &text, @@ -301,7 +318,7 @@ fn apply_linewise_highlighting<'a, T: CanvasTheme>( ) -> Line<'a> { let start_field = min(*anchor_line, *current_field_idx); let end_field = max(*anchor_line, *current_field_idx); - + let highlight_style = Style::default() .fg(theme.highlight()) .bg(theme.highlight_bg()) diff --git a/canvas/src/canvas/mod.rs b/canvas/src/canvas/mod.rs index aa72003..85a3e51 100644 --- a/canvas/src/canvas/mod.rs +++ b/canvas/src/canvas/mod.rs @@ -1,18 +1,14 @@ // src/canvas/mod.rs pub mod actions; -pub mod gui; -pub mod modes; pub mod state; +pub mod modes; + +#[cfg(feature = "gui")] +pub mod gui; + +#[cfg(feature = "gui")] pub mod theme; -// Re-export main types for convenience -pub use actions::{CanvasAction, ActionResult}; +// Keep these exports for current functionality pub use modes::{AppMode, ModeManager, HighlightState}; -pub use state::{CanvasState, ActionContext}; - -#[cfg(feature = "gui")] -pub use theme::CanvasTheme; - -#[cfg(feature = "gui")] -pub use gui::render_canvas; diff --git a/canvas/src/canvas/state.rs b/canvas/src/canvas/state.rs index f8a3e9b..fe59ac3 100644 --- a/canvas/src/canvas/state.rs +++ b/canvas/src/canvas/state.rs @@ -1,117 +1,132 @@ // src/canvas/state.rs -//! Canvas state trait and related types -//! -//! This module defines the core trait that any form or input system must implement -//! to work with the canvas library. +//! Library-owned UI state - user never directly modifies this -use crate::canvas::actions::CanvasAction; use crate::canvas::modes::AppMode; -/// Context information passed to feature-specific action handlers -#[derive(Debug)] -pub struct ActionContext { - /// Original key code that triggered this action (for backwards compatibility) - pub key_code: Option, - /// Current ideal cursor column for vertical movement - pub ideal_cursor_column: usize, - /// Current input text - pub current_input: String, - /// Current field index - pub current_field: usize, +/// Library-owned UI state - user never directly modifies this +#[derive(Debug, Clone)] +pub struct EditorState { + // Navigation state + pub(crate) current_field: usize, + pub(crate) cursor_pos: usize, + pub(crate) ideal_cursor_column: usize, + + // Mode state + pub(crate) current_mode: AppMode, + + // Autocomplete state + pub(crate) autocomplete: AutocompleteUIState, + + // Selection state (for vim visual mode) + pub(crate) selection: SelectionState, } -/// Core trait that any form-like state must implement to work with canvas -/// -/// This trait enables the same mode behaviors (edit, read-only, highlight) to work -/// across any implementation - login forms, data entry forms, configuration screens, etc. -/// -/// # Required Implementation -/// -/// Your struct needs to track: -/// - Current field index and cursor position -/// - All input field values -/// - Current interaction mode -/// - Whether there are unsaved changes -/// -/// # Example Implementation -/// -/// ```rust -/// struct MyForm { -/// fields: Vec, -/// current_field: usize, -/// cursor_pos: usize, -/// mode: AppMode, -/// dirty: bool, -/// } -/// -/// impl CanvasState for MyForm { -/// fn current_field(&self) -> usize { self.current_field } -/// fn current_cursor_pos(&self) -> usize { self.cursor_pos } -/// // ... implement other required methods -/// } -/// ``` -pub trait CanvasState { - // --- Core Navigation --- - - /// Get current field index (0-based) - fn current_field(&self) -> usize; - - /// Get current cursor position within the current field - fn current_cursor_pos(&self) -> usize; - - /// Set current field index (should clamp to valid range) - fn set_current_field(&mut self, index: usize); - - /// Set cursor position within current field (should clamp to valid range) - fn set_current_cursor_pos(&mut self, pos: usize); +#[derive(Debug, Clone)] +pub struct AutocompleteUIState { + pub(crate) is_active: bool, + pub(crate) is_loading: bool, + pub(crate) selected_index: Option, + pub(crate) active_field: Option, +} - // --- Mode Information --- - - /// Get current interaction mode (edit, read-only, highlight, etc.) - fn current_mode(&self) -> AppMode; +#[derive(Debug, Clone)] +pub enum SelectionState { + None, + Characterwise { anchor: (usize, usize) }, + Linewise { anchor_field: usize }, +} - // --- Data Access --- - - /// Get immutable reference to current field's text - fn get_current_input(&self) -> &str; - - /// Get mutable reference to current field's text - fn get_current_input_mut(&mut self) -> &mut String; - - /// Get all input values as immutable references - fn inputs(&self) -> Vec<&String>; - - /// Get all field names/labels - fn fields(&self) -> Vec<&str>; - - // --- State Management --- - - /// Check if there are unsaved changes - fn has_unsaved_changes(&self) -> bool; - - /// Mark whether there are unsaved changes - fn set_has_unsaved_changes(&mut self, changed: bool); - - // --- Optional Overrides --- - - /// Handle application-specific actions not covered by standard handlers - /// Return Some(message) if the action was handled, None to use standard handling - fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option { - None // Default: no custom handling +impl EditorState { + pub fn new() -> Self { + Self { + current_field: 0, + cursor_pos: 0, + ideal_cursor_column: 0, + current_mode: AppMode::Edit, + autocomplete: AutocompleteUIState { + is_active: false, + is_loading: false, + selected_index: None, + active_field: None, + }, + selection: SelectionState::None, + } } - - /// Get display value for a field (may differ from actual value) - /// Used for things like password masking or computed display values - fn get_display_value_for_field(&self, index: usize) -> &str { - self.inputs() - .get(index) - .map(|s| s.as_str()) - .unwrap_or("") + + // =================================================================== + // READ-ONLY ACCESS: User can fetch UI state for compatibility + // =================================================================== + + /// Get current field index (for user's business logic) + pub fn current_field(&self) -> usize { + self.current_field } - - /// Check if a field has a custom display value - /// Return true if get_display_value_for_field returns something different than the actual value - fn has_display_override(&self, _index: usize) -> bool { - false + + /// Get current cursor position (for user's business logic) + pub fn cursor_position(&self) -> usize { + self.cursor_pos + } + + /// Get current mode (for user's business logic) + pub fn mode(&self) -> AppMode { + self.current_mode + } + + /// Check if autocomplete is active (for user's business logic) + pub fn is_autocomplete_active(&self) -> bool { + self.autocomplete.is_active + } + + /// Check if autocomplete is loading (for user's business logic) + pub fn is_autocomplete_loading(&self) -> bool { + self.autocomplete.is_loading + } + + /// Get selection state (for user's business logic) + pub fn selection_state(&self) -> &SelectionState { + &self.selection + } + + // =================================================================== + // INTERNAL MUTATIONS: Only library modifies these + // =================================================================== + + pub(crate) fn move_to_field(&mut self, field_index: usize, field_count: usize) { + if field_index < field_count { + self.current_field = field_index; + // Reset cursor to safe position - will be clamped by movement logic + self.cursor_pos = 0; + } + } + + pub(crate) fn set_cursor(&mut self, position: usize, max_position: usize, for_edit_mode: bool) { + if for_edit_mode { + // Edit mode: can go past end for insertion + self.cursor_pos = position.min(max_position); + } else { + // ReadOnly/Highlight: stay within text bounds + self.cursor_pos = position.min(max_position.saturating_sub(1)); + } + self.ideal_cursor_column = self.cursor_pos; + } + + pub(crate) fn activate_autocomplete(&mut self, field_index: usize) { + self.autocomplete.is_active = true; + self.autocomplete.is_loading = true; + self.autocomplete.active_field = Some(field_index); + self.autocomplete.selected_index = None; + } + + pub(crate) fn deactivate_autocomplete(&mut self) { + self.autocomplete.is_active = false; + self.autocomplete.is_loading = false; + self.autocomplete.active_field = None; + self.autocomplete.selected_index = None; + } +} + +impl Default for EditorState { + fn default() -> Self { + Self::new() } } diff --git a/canvas/src/data_provider.rs b/canvas/src/data_provider.rs new file mode 100644 index 0000000..3b737f1 --- /dev/null +++ b/canvas/src/data_provider.rs @@ -0,0 +1,47 @@ +// src/data_provider.rs +//! Simplified user interface - only business data, no UI state + +use anyhow::Result; +use async_trait::async_trait; + +/// User implements this - only business data, no UI state +pub trait DataProvider { + /// How many fields in the form + fn field_count(&self) -> usize; + + /// Get field label/name + fn field_name(&self, index: usize) -> &str; + + /// Get field value + fn field_value(&self, index: usize) -> &str; + + /// Set field value (library calls this when text changes) + fn set_field_value(&mut self, index: usize, value: String); + + /// Check if field supports autocomplete (optional) + fn supports_autocomplete(&self, _field_index: usize) -> bool { + false + } + + /// Get display value (for password masking, etc.) - optional + fn display_value(&self, index: usize) -> Option<&str> { + None // Default: use actual value + } +} + +/// Optional: User implements this for autocomplete data +#[async_trait] +pub trait AutocompleteProvider { + type SuggestionData: Clone + Send + 'static; + + /// Fetch autocomplete suggestions (user's business logic) + async fn fetch_suggestions(&mut self, field_index: usize, query: &str) + -> Result>>; +} + +#[derive(Debug, Clone)] +pub struct SuggestionItem { + pub data: T, + pub display_text: String, + pub value_to_store: String, +} diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs new file mode 100644 index 0000000..81dc16a --- /dev/null +++ b/canvas/src/editor.rs @@ -0,0 +1,234 @@ +// src/editor.rs +//! Main API for the canvas library - FormEditor with library-owned state + +use anyhow::Result; +use async_trait::async_trait; +use crate::canvas::state::EditorState; +use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; +use crate::canvas::modes::AppMode; + +/// Main editor that manages UI state internally and delegates data to user +pub struct FormEditor { + // Library owns all UI state + ui_state: EditorState, + + // User owns business data + data_provider: D, + + // Autocomplete suggestions (library manages UI, user provides data) + pub(crate) suggestions: Vec>, +} + +impl FormEditor { + pub fn new(data_provider: D) -> Self { + Self { + ui_state: EditorState::new(), + data_provider, + suggestions: Vec::new(), + } + } + + // =================================================================== + // READ-ONLY ACCESS: User can fetch UI state + // =================================================================== + + /// Get current field index (for user's compatibility) + pub fn current_field(&self) -> usize { + self.ui_state.current_field() + } + + /// Get current cursor position (for user's compatibility) + pub fn cursor_position(&self) -> usize { + self.ui_state.cursor_position() + } + + /// Get current mode (for user's mode-dependent logic) + pub fn mode(&self) -> AppMode { + self.ui_state.mode() + } + + /// Check if autocomplete is active (for user's logic) + pub fn is_autocomplete_active(&self) -> bool { + self.ui_state.is_autocomplete_active() + } + + /// Get current field text (convenience method) + pub fn current_text(&self) -> &str { + let field_index = self.ui_state.current_field; + if field_index < self.data_provider.field_count() { + self.data_provider.field_value(field_index) + } else { + "" + } + } + + /// Get reference to UI state for rendering + pub fn ui_state(&self) -> &EditorState { + &self.ui_state + } + + /// Get reference to data provider for rendering + pub fn data_provider(&self) -> &D { + &self.data_provider + } + + /// Get autocomplete suggestions for rendering (read-only) + pub fn suggestions(&self) -> &[SuggestionItem] { + &self.suggestions + } + + // =================================================================== + // SYNC OPERATIONS: No async needed for basic editing + // =================================================================== + + /// Handle character insertion + pub fn insert_char(&mut self, ch: char) -> Result<()> { + if self.ui_state.current_mode != AppMode::Edit { + return Ok(()); // Ignore in non-edit modes + } + + let field_index = self.ui_state.current_field; + let cursor_pos = self.ui_state.cursor_pos; + + // Get current text from user + let mut current_text = self.data_provider.field_value(field_index).to_string(); + + // Insert character + current_text.insert(cursor_pos, ch); + + // Update user's data + self.data_provider.set_field_value(field_index, current_text); + + // Update library's UI state + self.ui_state.cursor_pos += 1; + self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; + + Ok(()) + } + + /// Handle cursor movement + pub fn move_left(&mut self) { + if self.ui_state.cursor_pos > 0 { + self.ui_state.cursor_pos -= 1; + self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; + } + } + + pub fn move_right(&mut self) { + let current_text = self.current_text(); + let max_pos = if self.ui_state.current_mode == AppMode::Edit { + current_text.len() // Edit mode: can go past end + } else { + current_text.len().saturating_sub(1) // ReadOnly: stay in bounds + }; + + if self.ui_state.cursor_pos < max_pos { + self.ui_state.cursor_pos += 1; + self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; + } + } + + /// Handle field navigation + pub fn move_to_next_field(&mut self) { + let field_count = self.data_provider.field_count(); + let next_field = (self.ui_state.current_field + 1) % field_count; + self.ui_state.move_to_field(next_field, field_count); + + // Clamp cursor to new field + let current_text = self.current_text(); + let max_pos = current_text.len(); + self.ui_state.set_cursor( + self.ui_state.ideal_cursor_column, + max_pos, + self.ui_state.current_mode == AppMode::Edit + ); + } + + /// Change mode (for vim compatibility) + pub fn set_mode(&mut self, mode: AppMode) { + self.ui_state.current_mode = mode; + + // Clear autocomplete when changing modes + if mode != AppMode::Edit { + self.ui_state.deactivate_autocomplete(); + } + } + + // =================================================================== + // ASYNC OPERATIONS: Only autocomplete needs async + // =================================================================== + + /// Trigger autocomplete (async because it fetches data) + pub async fn trigger_autocomplete(&mut self, provider: &mut A) -> Result<()> + where + A: AutocompleteProvider, + A::SuggestionData: std::fmt::Debug, // Change from Display to Debug + { + let field_index = self.ui_state.current_field; + + if !self.data_provider.supports_autocomplete(field_index) { + return Ok(()); + } + + // Activate autocomplete UI + self.ui_state.activate_autocomplete(field_index); + + // Fetch suggestions from user + let query = self.current_text(); + let suggestions = provider.fetch_suggestions(field_index, query).await?; + + // Convert to library's format (could be avoided with better generics) + self.suggestions = suggestions.into_iter() + .map(|item| SuggestionItem { + data: format!("{:?}", item.data), // Use Debug formatting instead + display_text: item.display_text, + value_to_store: item.value_to_store, + }) + .collect(); + + // Update UI state + self.ui_state.autocomplete.is_loading = false; + if !self.suggestions.is_empty() { + self.ui_state.autocomplete.selected_index = Some(0); + } + + Ok(()) + } + + /// Navigate autocomplete suggestions + pub fn autocomplete_next(&mut self) { + if !self.ui_state.autocomplete.is_active || self.suggestions.is_empty() { + return; + } + + let current = self.ui_state.autocomplete.selected_index.unwrap_or(0); + let next = (current + 1) % self.suggestions.len(); + self.ui_state.autocomplete.selected_index = Some(next); + } + + /// Apply selected autocomplete suggestion + pub fn apply_autocomplete(&mut self) -> Option { + if let Some(selected_index) = self.ui_state.autocomplete.selected_index { + if let Some(suggestion) = self.suggestions.get(selected_index).cloned() { + let field_index = self.ui_state.current_field; + + // Apply to user's data + self.data_provider.set_field_value( + field_index, + suggestion.value_to_store.clone() + ); + + // Update cursor position + self.ui_state.cursor_pos = suggestion.value_to_store.len(); + self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; + + // Close autocomplete + self.ui_state.deactivate_autocomplete(); + self.suggestions.clear(); + + return Some(suggestion.display_text); + } + } + None + } +} diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index b0630ec..31084f6 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,31 +1,47 @@ // src/lib.rs pub mod canvas; +pub mod editor; +pub mod data_provider; // Only include autocomplete module if feature is enabled #[cfg(feature = "autocomplete")] pub mod autocomplete; -// Re-export the main API for easy access -pub use canvas::actions::{CanvasAction, ActionResult, execute}; -pub use canvas::state::{CanvasState, ActionContext}; -pub use canvas::modes::{AppMode, ModeManager, HighlightState}; +// =================================================================== +// NEW API: Library-owned state pattern +// =================================================================== +// Main API exports +pub use editor::FormEditor; +pub use data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; + +// UI state (read-only access for users) +pub use canvas::state::EditorState; +pub use canvas::modes::AppMode; + +// Actions and results (for users who want to handle actions manually) +pub use canvas::actions::{CanvasAction, ActionResult}; + +// Theming and GUI #[cfg(feature = "gui")] pub use canvas::theme::CanvasTheme; #[cfg(feature = "gui")] pub use canvas::gui::render_canvas; -// Re-export autocomplete API if feature is enabled -#[cfg(feature = "autocomplete")] -pub use autocomplete::{ - AutocompleteCanvasState, - AutocompleteState, - SuggestionItem, - execute_with_autocomplete, - handle_autocomplete_feature_action, -}; - #[cfg(all(feature = "gui", feature = "autocomplete"))] -pub use autocomplete::render_autocomplete_dropdown; +pub use autocomplete::gui::render_autocomplete_dropdown; + +// =================================================================== +// LEGACY COMPATIBILITY: Old trait-based API (deprecated) +// =================================================================== + +// Legacy exports for backward compatibility - mark as deprecated + +#[deprecated(note = "Use FormEditor and AutocompleteProvider instead")] +#[cfg(feature = "autocomplete")] +pub use crate::autocomplete::state::AutocompleteCanvasState; + +// Mode management (still used) +pub use canvas::modes::{ModeManager, HighlightState}; From 643db8e58667adbe3b24d8a8143800bd2308a68d Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 1 Aug 2025 23:38:24 +0200 Subject: [PATCH 14/18] removed deprecantions --- canvas/src/autocomplete/actions.rs | 47 ------ canvas/src/autocomplete/mod.rs | 13 +- canvas/src/autocomplete/state.rs | 8 +- canvas/src/autocomplete/types.rs | 21 --- .../src/canvas/actions/handlers/dispatcher.rs | 30 ---- canvas/src/canvas/actions/handlers/edit.rs | 143 ------------------ .../src/canvas/actions/handlers/highlight.rs | 97 ------------ canvas/src/canvas/actions/handlers/mod.rs | 11 -- .../src/canvas/actions/handlers/readonly.rs | 136 ----------------- canvas/src/canvas/actions/mod.rs | 1 - canvas/src/canvas/actions/types.rs | 8 - canvas/src/editor.rs | 1 - canvas/src/lib.rs | 13 -- 13 files changed, 4 insertions(+), 525 deletions(-) delete mode 100644 canvas/src/autocomplete/actions.rs delete mode 100644 canvas/src/autocomplete/types.rs delete mode 100644 canvas/src/canvas/actions/handlers/dispatcher.rs delete mode 100644 canvas/src/canvas/actions/handlers/edit.rs delete mode 100644 canvas/src/canvas/actions/handlers/highlight.rs delete mode 100644 canvas/src/canvas/actions/handlers/mod.rs delete mode 100644 canvas/src/canvas/actions/handlers/readonly.rs diff --git a/canvas/src/autocomplete/actions.rs b/canvas/src/autocomplete/actions.rs deleted file mode 100644 index 1fa130d..0000000 --- a/canvas/src/autocomplete/actions.rs +++ /dev/null @@ -1,47 +0,0 @@ -// src/autocomplete/actions.rs -//! Legacy autocomplete actions - deprecated in favor of FormEditor - -use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use anyhow::Result; - -/// Legacy function - use FormEditor.trigger_autocomplete() instead -/// -/// # Migration Guide -/// -/// **Old way:** -/// ```rust,ignore -/// execute_with_autocomplete(action, &mut state).await?; -/// ``` -/// -/// **New way:** -/// ```rust,ignore -/// let mut editor = FormEditor::new(your_data_provider); -/// match action { -/// CanvasAction::TriggerAutocomplete => { -/// editor.trigger_autocomplete(&mut autocomplete_provider).await?; -/// } -/// CanvasAction::InsertChar(c) => { -/// editor.insert_char(c)?; -/// } -/// // ... etc -/// } -/// ``` -#[deprecated(note = "Use FormEditor.trigger_autocomplete() and related methods instead")] -pub async fn execute_with_autocomplete( - _action: CanvasAction, - _state: &mut T, -) -> Result { - Err(anyhow::anyhow!( - "execute_with_autocomplete is deprecated. Use FormEditor API instead.\n\ - Migration: Replace CanvasState trait with DataProvider trait and use FormEditor." - )) -} - -/// Legacy function - use FormEditor methods instead -#[deprecated(note = "Use FormEditor methods instead")] -pub fn handle_autocomplete_feature_action( - _action: &CanvasAction, - _state: &T, -) -> Option { - Some("handle_autocomplete_feature_action is deprecated. Use FormEditor API instead.".to_string()) -} diff --git a/canvas/src/autocomplete/mod.rs b/canvas/src/autocomplete/mod.rs index 0ef3b62..2027914 100644 --- a/canvas/src/autocomplete/mod.rs +++ b/canvas/src/autocomplete/mod.rs @@ -1,20 +1,11 @@ // src/autocomplete/mod.rs -pub mod types; pub mod state; -pub mod actions; - #[cfg(feature = "gui")] pub mod gui; -// Re-export the main autocomplete API -pub use types::{SuggestionItem, AutocompleteState}; - -// Re-export the new action functions -pub use actions::{ - execute_with_autocomplete, - handle_autocomplete_feature_action, -}; +// Re-export the main autocomplete types +pub use state::{AutocompleteProvider, SuggestionItem}; // Re-export GUI functions if available #[cfg(feature = "gui")] diff --git a/canvas/src/autocomplete/state.rs b/canvas/src/autocomplete/state.rs index 6b4fe11..11c03fa 100644 --- a/canvas/src/autocomplete/state.rs +++ b/canvas/src/autocomplete/state.rs @@ -1,9 +1,5 @@ // src/autocomplete/state.rs -//! Simple autocomplete provider pattern - replaces complex trait +//! Autocomplete provider types -// Re-export the main types from data_provider for backward compatibility +// Re-export the main types from data_provider pub use crate::data_provider::{AutocompleteProvider, SuggestionItem}; - -// Legacy compatibility - empty trait for migration -#[deprecated(note = "Use AutocompleteProvider instead")] -pub trait AutocompleteCanvasState {} diff --git a/canvas/src/autocomplete/types.rs b/canvas/src/autocomplete/types.rs deleted file mode 100644 index dfaea81..0000000 --- a/canvas/src/autocomplete/types.rs +++ /dev/null @@ -1,21 +0,0 @@ -// src/autocomplete/types.rs -//! Legacy autocomplete types - deprecated - -// Re-export the new simplified types -pub use crate::data_provider::SuggestionItem; - -/// Legacy type - use FormEditor instead -#[deprecated(note = "Use FormEditor instead")] -#[derive(Debug, Clone)] -pub struct AutocompleteState { - _phantom: std::marker::PhantomData, -} - -#[allow(dead_code)] -impl AutocompleteState { - /// Legacy method - use FormEditor.is_autocomplete_active() instead - #[deprecated(note = "Use FormEditor.is_autocomplete_active() instead")] - pub fn is_active(&self) -> bool { - false - } -} diff --git a/canvas/src/canvas/actions/handlers/dispatcher.rs b/canvas/src/canvas/actions/handlers/dispatcher.rs deleted file mode 100644 index 6636574..0000000 --- a/canvas/src/canvas/actions/handlers/dispatcher.rs +++ /dev/null @@ -1,30 +0,0 @@ -// src/canvas/actions/handlers/dispatcher.rs - -use crate::canvas::state::EditorState; -use crate::canvas::actions::{CanvasAction, ActionResult}; -use crate::canvas::modes::AppMode; - -use super::{handle_edit_action, handle_readonly_action, handle_highlight_action}; - -/// Internal action dispatcher - routes actions to mode-specific handlers -pub(crate) fn dispatch_action_internal( - action: CanvasAction, - editor_state: &mut EditorState, - current_text: &str, -) -> ActionResult { - // Route to mode-specific handler based on current mode - match editor_state.current_mode { - AppMode::Edit => { - handle_edit_action(action, editor_state, current_text) - } - AppMode::ReadOnly => { - handle_readonly_action(action, editor_state, current_text) - } - AppMode::Highlight => { - handle_highlight_action(action, editor_state, current_text) - } - AppMode::General | AppMode::Command => { - ActionResult::success_with_message("Mode does not handle canvas actions directly") - } - } -} diff --git a/canvas/src/canvas/actions/handlers/edit.rs b/canvas/src/canvas/actions/handlers/edit.rs deleted file mode 100644 index c2706da..0000000 --- a/canvas/src/canvas/actions/handlers/edit.rs +++ /dev/null @@ -1,143 +0,0 @@ -// src/canvas/actions/handlers/edit.rs -//! Edit mode action handler with EditorState - -use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::canvas::actions::movement::*; -use crate::canvas::state::EditorState; - -/// Edit mode uses cursor-past-end behavior for text insertion -const FOR_EDIT_MODE: bool = true; - -/// Handle actions in edit mode with edit-specific cursor behavior -pub(crate) fn handle_edit_action( - action: CanvasAction, - editor_state: &mut EditorState, - current_text: &str, -) -> ActionResult { - match action { - // Note: Text insertion is handled at the FormEditor level - // These handlers only deal with cursor movement and navigation - - // Cursor movement actions - CanvasAction::MoveLeft => { - let new_pos = move_left(editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveRight => { - let new_pos = move_right(editor_state.cursor_pos, current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - // Field navigation (treating single-line fields as "lines") - CanvasAction::MoveUp => { - if editor_state.current_field > 0 { - editor_state.current_field -= 1; - let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - } - ActionResult::success() - } - - CanvasAction::MoveDown => { - // Note: field count validation happens at FormEditor level - editor_state.current_field += 1; - let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - ActionResult::success() - } - - // Line-based movement - CanvasAction::MoveLineStart => { - let new_pos = line_start_position(); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveLineEnd => { - let new_pos = line_end_position(current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - // Document-level movement (first/last field) - CanvasAction::MoveFirstLine => { - editor_state.current_field = 0; - let new_pos = safe_cursor_position(current_text, 0, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveLastLine => { - // Note: field count validation happens at FormEditor level - let new_pos = line_end_position(current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - // Word-based movement - CanvasAction::MoveWordNext => { - if !current_text.is_empty() { - let new_pos = find_next_word_start(current_text, editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - } - ActionResult::success() - } - - CanvasAction::MoveWordEnd => { - if !current_text.is_empty() { - let new_pos = find_word_end(current_text, editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - } - ActionResult::success() - } - - CanvasAction::MoveWordPrev => { - if !current_text.is_empty() { - let new_pos = find_prev_word_start(current_text, editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - } - ActionResult::success() - } - - CanvasAction::MoveWordEndPrev => { - if !current_text.is_empty() { - let new_pos = find_prev_word_end(current_text, editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - } - ActionResult::success() - } - - // Field navigation - handled at FormEditor level for bounds checking - CanvasAction::NextField | CanvasAction::PrevField => { - ActionResult::success_with_message("Field navigation handled by FormEditor") - } - - // Text editing actions - handled at FormEditor level - CanvasAction::InsertChar(_) | - CanvasAction::DeleteBackward | - CanvasAction::DeleteForward => { - ActionResult::success_with_message("Text editing handled by FormEditor") - } - - CanvasAction::Custom(action_str) => { - ActionResult::success_with_message(&format!("Custom edit action: {}", action_str)) - } - - _ => { - ActionResult::success_with_message("Action not implemented for edit mode") - } - } -} diff --git a/canvas/src/canvas/actions/handlers/highlight.rs b/canvas/src/canvas/actions/handlers/highlight.rs deleted file mode 100644 index 77314f5..0000000 --- a/canvas/src/canvas/actions/handlers/highlight.rs +++ /dev/null @@ -1,97 +0,0 @@ -// src/canvas/actions/handlers/highlight.rs -//! Highlight mode action handler with EditorState - -use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::canvas::actions::movement::*; -use crate::canvas::state::EditorState; - -const FOR_EDIT_MODE: bool = false; // Highlight mode uses read-only cursor behavior - -/// Handle actions in highlight/visual mode -pub(crate) fn handle_highlight_action( - action: CanvasAction, - editor_state: &mut EditorState, - current_text: &str, -) -> ActionResult { - match action { - // Movement actions work similar to read-only mode but with selection - CanvasAction::MoveLeft => { - let new_pos = move_left(editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - // TODO: Update selection range - ActionResult::success() - } - - CanvasAction::MoveRight => { - let new_pos = move_right(editor_state.cursor_pos, current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - // TODO: Update selection range - ActionResult::success() - } - - CanvasAction::MoveWordNext => { - if !current_text.is_empty() { - let new_pos = find_next_word_start(current_text, editor_state.cursor_pos); - let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = final_pos; - editor_state.ideal_cursor_column = final_pos; - // TODO: Update selection range - } - ActionResult::success() - } - - CanvasAction::MoveWordEnd => { - if !current_text.is_empty() { - let new_pos = find_word_end(current_text, editor_state.cursor_pos); - let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = final_pos; - editor_state.ideal_cursor_column = final_pos; - // TODO: Update selection range - } - ActionResult::success() - } - - CanvasAction::MoveWordPrev => { - if !current_text.is_empty() { - let new_pos = find_prev_word_start(current_text, editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - // TODO: Update selection range - } - ActionResult::success() - } - - CanvasAction::MoveLineStart => { - let new_pos = line_start_position(); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - // TODO: Update selection range - ActionResult::success() - } - - CanvasAction::MoveLineEnd => { - let new_pos = line_end_position(current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - // TODO: Update selection range - ActionResult::success() - } - - // Highlight mode doesn't handle editing actions - CanvasAction::InsertChar(_) | - CanvasAction::DeleteBackward | - CanvasAction::DeleteForward => { - ActionResult::success_with_message("Action not available in highlight mode") - } - - CanvasAction::Custom(action_str) => { - ActionResult::success_with_message(&format!("Custom highlight action: {}", action_str)) - } - - _ => { - ActionResult::success_with_message("Action not implemented for highlight mode") - } - } -} diff --git a/canvas/src/canvas/actions/handlers/mod.rs b/canvas/src/canvas/actions/handlers/mod.rs deleted file mode 100644 index 1810a00..0000000 --- a/canvas/src/canvas/actions/handlers/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -// src/canvas/actions/handlers/mod.rs - -pub mod edit; -pub mod readonly; -pub mod highlight; -pub mod dispatcher; - -pub use edit::*; -pub use readonly::*; -pub use highlight::*; -pub use dispatcher::*; diff --git a/canvas/src/canvas/actions/handlers/readonly.rs b/canvas/src/canvas/actions/handlers/readonly.rs deleted file mode 100644 index 551457d..0000000 --- a/canvas/src/canvas/actions/handlers/readonly.rs +++ /dev/null @@ -1,136 +0,0 @@ -// src/canvas/actions/handlers/readonly.rs -//! ReadOnly mode action handler with EditorState - -use crate::canvas::actions::types::{CanvasAction, ActionResult}; -use crate::canvas::actions::movement::*; -use crate::canvas::state::EditorState; - -const FOR_EDIT_MODE: bool = false; // Read-only mode flag - -/// Handle actions in read-only mode with read-only specific cursor behavior -pub(crate) fn handle_readonly_action( - action: CanvasAction, - editor_state: &mut EditorState, - current_text: &str, -) -> ActionResult { - match action { - CanvasAction::MoveLeft => { - let new_pos = move_left(editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveRight => { - let new_pos = move_right(editor_state.cursor_pos, current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveUp => { - if editor_state.current_field > 0 { - editor_state.current_field -= 1; - let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - } - ActionResult::success() - } - - CanvasAction::MoveDown => { - // Note: bounds checking happens at FormEditor level - editor_state.current_field += 1; - let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - ActionResult::success() - } - - CanvasAction::MoveFirstLine => { - editor_state.current_field = 0; - let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveLastLine => { - // Note: field count validation happens at FormEditor level - let new_pos = safe_cursor_position(current_text, editor_state.ideal_cursor_column, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveLineStart => { - let new_pos = line_start_position(); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveLineEnd => { - let new_pos = line_end_position(current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - ActionResult::success() - } - - CanvasAction::MoveWordNext => { - if !current_text.is_empty() { - let new_pos = find_next_word_start(current_text, editor_state.cursor_pos); - let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = final_pos; - editor_state.ideal_cursor_column = final_pos; - } - ActionResult::success() - } - - CanvasAction::MoveWordEnd => { - if !current_text.is_empty() { - let new_pos = find_word_end(current_text, editor_state.cursor_pos); - let final_pos = clamp_cursor_position(new_pos, current_text, FOR_EDIT_MODE); - editor_state.cursor_pos = final_pos; - editor_state.ideal_cursor_column = final_pos; - } - ActionResult::success() - } - - CanvasAction::MoveWordPrev => { - if !current_text.is_empty() { - let new_pos = find_prev_word_start(current_text, editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - } - ActionResult::success() - } - - CanvasAction::MoveWordEndPrev => { - if !current_text.is_empty() { - let new_pos = find_prev_word_end(current_text, editor_state.cursor_pos); - editor_state.cursor_pos = new_pos; - editor_state.ideal_cursor_column = new_pos; - } - ActionResult::success() - } - - // Field navigation - handled at FormEditor level - CanvasAction::NextField | CanvasAction::PrevField => { - ActionResult::success_with_message("Field navigation handled by FormEditor") - } - - // Read-only mode doesn't handle editing actions - CanvasAction::InsertChar(_) | - CanvasAction::DeleteBackward | - CanvasAction::DeleteForward => { - ActionResult::success_with_message("Action not available in read-only mode") - } - - CanvasAction::Custom(action_str) => { - ActionResult::success_with_message(&format!("Custom readonly action: {}", action_str)) - } - - _ => { - ActionResult::success_with_message("Action not implemented for read-only mode") - } - } -} diff --git a/canvas/src/canvas/actions/mod.rs b/canvas/src/canvas/actions/mod.rs index 21a80f8..412d758 100644 --- a/canvas/src/canvas/actions/mod.rs +++ b/canvas/src/canvas/actions/mod.rs @@ -1,7 +1,6 @@ // src/canvas/actions/mod.rs pub mod types; -pub mod handlers; pub mod movement; // Re-export the main API diff --git a/canvas/src/canvas/actions/types.rs b/canvas/src/canvas/actions/types.rs index d5a2de4..fe9ae9c 100644 --- a/canvas/src/canvas/actions/types.rs +++ b/canvas/src/canvas/actions/types.rs @@ -1,7 +1,5 @@ // src/canvas/actions/types.rs -use crate::canvas::state::EditorState; - /// All available canvas actions #[derive(Debug, Clone, PartialEq)] pub enum CanvasAction { @@ -83,12 +81,6 @@ impl ActionResult { } impl CanvasAction { - /// Internal method used by FormEditor - pub(crate) fn apply_to_editor_state(self, editor_state: &mut EditorState, current_text: &str) -> ActionResult { - // Internal method used by FormEditor - crate::canvas::actions::handlers::dispatch_action_internal(self, editor_state, current_text) - } - /// Get a human-readable description of this action pub fn description(&self) -> &'static str { match self { diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 81dc16a..275463c 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -2,7 +2,6 @@ //! Main API for the canvas library - FormEditor with library-owned state use anyhow::Result; -use async_trait::async_trait; use crate::canvas::state::EditorState; use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; use crate::canvas::modes::AppMode; diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 31084f6..d145f29 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -32,16 +32,3 @@ pub use canvas::gui::render_canvas; #[cfg(all(feature = "gui", feature = "autocomplete"))] pub use autocomplete::gui::render_autocomplete_dropdown; - -// =================================================================== -// LEGACY COMPATIBILITY: Old trait-based API (deprecated) -// =================================================================== - -// Legacy exports for backward compatibility - mark as deprecated - -#[deprecated(note = "Use FormEditor and AutocompleteProvider instead")] -#[cfg(feature = "autocomplete")] -pub use crate::autocomplete::state::AutocompleteCanvasState; - -// Mode management (still used) -pub use canvas::modes::{ModeManager, HighlightState}; From 2b08e64db83ca6a63abab9a55d9034d559b76dcf Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 2 Aug 2025 00:19:45 +0200 Subject: [PATCH 15/18] fixed generics --- canvas/src/data_provider.rs | 23 ++++---- canvas/src/editor.rs | 108 ++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 72 deletions(-) diff --git a/canvas/src/data_provider.rs b/canvas/src/data_provider.rs index 3b737f1..aabce75 100644 --- a/canvas/src/data_provider.rs +++ b/canvas/src/data_provider.rs @@ -8,23 +8,23 @@ use async_trait::async_trait; pub trait DataProvider { /// How many fields in the form fn field_count(&self) -> usize; - + /// Get field label/name fn field_name(&self, index: usize) -> &str; - - /// Get field value + + /// Get field value fn field_value(&self, index: usize) -> &str; - + /// Set field value (library calls this when text changes) fn set_field_value(&mut self, index: usize, value: String); - + /// Check if field supports autocomplete (optional) fn supports_autocomplete(&self, _field_index: usize) -> bool { false } - + /// Get display value (for password masking, etc.) - optional - fn display_value(&self, index: usize) -> Option<&str> { + fn display_value(&self, _index: usize) -> Option<&str> { None // Default: use actual value } } @@ -32,16 +32,13 @@ pub trait DataProvider { /// Optional: User implements this for autocomplete data #[async_trait] pub trait AutocompleteProvider { - type SuggestionData: Clone + Send + 'static; - /// Fetch autocomplete suggestions (user's business logic) - async fn fetch_suggestions(&mut self, field_index: usize, query: &str) - -> Result>>; + async fn fetch_suggestions(&mut self, field_index: usize, query: &str) + -> Result>; } #[derive(Debug, Clone)] -pub struct SuggestionItem { - pub data: T, +pub struct SuggestionItem { pub display_text: String, pub value_to_store: String, } diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 275463c..4e5b941 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -10,12 +10,12 @@ use crate::canvas::modes::AppMode; pub struct FormEditor { // Library owns all UI state ui_state: EditorState, - + // User owns business data data_provider: D, - + // Autocomplete suggestions (library manages UI, user provides data) - pub(crate) suggestions: Vec>, + pub(crate) suggestions: Vec, } impl FormEditor { @@ -26,31 +26,31 @@ impl FormEditor { suggestions: Vec::new(), } } - + // =================================================================== // READ-ONLY ACCESS: User can fetch UI state // =================================================================== - + /// Get current field index (for user's compatibility) pub fn current_field(&self) -> usize { self.ui_state.current_field() } - + /// Get current cursor position (for user's compatibility) pub fn cursor_position(&self) -> usize { self.ui_state.cursor_position() } - + /// Get current mode (for user's mode-dependent logic) pub fn mode(&self) -> AppMode { self.ui_state.mode() } - + /// Check if autocomplete is active (for user's logic) pub fn is_autocomplete_active(&self) -> bool { self.ui_state.is_autocomplete_active() } - + /// Get current field text (convenience method) pub fn current_text(&self) -> &str { let field_index = self.ui_state.current_field; @@ -60,51 +60,51 @@ impl FormEditor { "" } } - + /// Get reference to UI state for rendering pub fn ui_state(&self) -> &EditorState { &self.ui_state } - + /// Get reference to data provider for rendering pub fn data_provider(&self) -> &D { &self.data_provider } - + /// Get autocomplete suggestions for rendering (read-only) - pub fn suggestions(&self) -> &[SuggestionItem] { + pub fn suggestions(&self) -> &[SuggestionItem] { &self.suggestions } - + // =================================================================== // SYNC OPERATIONS: No async needed for basic editing // =================================================================== - + /// Handle character insertion pub fn insert_char(&mut self, ch: char) -> Result<()> { if self.ui_state.current_mode != AppMode::Edit { return Ok(()); // Ignore in non-edit modes } - + let field_index = self.ui_state.current_field; let cursor_pos = self.ui_state.cursor_pos; - + // Get current text from user let mut current_text = self.data_provider.field_value(field_index).to_string(); - + // Insert character current_text.insert(cursor_pos, ch); - + // Update user's data self.data_provider.set_field_value(field_index, current_text); - + // Update library's UI state self.ui_state.cursor_pos += 1; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; - + Ok(()) } - + /// Handle cursor movement pub fn move_left(&mut self) { if self.ui_state.cursor_pos > 0 { @@ -112,7 +112,7 @@ impl FormEditor { self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; } } - + pub fn move_right(&mut self) { let current_text = self.current_text(); let max_pos = if self.ui_state.current_mode == AppMode::Edit { @@ -120,111 +120,101 @@ impl FormEditor { } else { current_text.len().saturating_sub(1) // ReadOnly: stay in bounds }; - + if self.ui_state.cursor_pos < max_pos { self.ui_state.cursor_pos += 1; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; } } - + /// Handle field navigation pub fn move_to_next_field(&mut self) { let field_count = self.data_provider.field_count(); let next_field = (self.ui_state.current_field + 1) % field_count; self.ui_state.move_to_field(next_field, field_count); - + // Clamp cursor to new field let current_text = self.current_text(); let max_pos = current_text.len(); self.ui_state.set_cursor( - self.ui_state.ideal_cursor_column, - max_pos, + self.ui_state.ideal_cursor_column, + max_pos, self.ui_state.current_mode == AppMode::Edit ); } - + /// Change mode (for vim compatibility) pub fn set_mode(&mut self, mode: AppMode) { self.ui_state.current_mode = mode; - + // Clear autocomplete when changing modes if mode != AppMode::Edit { self.ui_state.deactivate_autocomplete(); } } - + // =================================================================== // ASYNC OPERATIONS: Only autocomplete needs async // =================================================================== - + /// Trigger autocomplete (async because it fetches data) - pub async fn trigger_autocomplete(&mut self, provider: &mut A) -> Result<()> - where + pub async fn trigger_autocomplete(&mut self, provider: &mut A) -> Result<()> + where A: AutocompleteProvider, - A::SuggestionData: std::fmt::Debug, // Change from Display to Debug { let field_index = self.ui_state.current_field; - + if !self.data_provider.supports_autocomplete(field_index) { return Ok(()); } - + // Activate autocomplete UI self.ui_state.activate_autocomplete(field_index); - - // Fetch suggestions from user + + // Fetch suggestions from user (no conversion needed!) let query = self.current_text(); - let suggestions = provider.fetch_suggestions(field_index, query).await?; - - // Convert to library's format (could be avoided with better generics) - self.suggestions = suggestions.into_iter() - .map(|item| SuggestionItem { - data: format!("{:?}", item.data), // Use Debug formatting instead - display_text: item.display_text, - value_to_store: item.value_to_store, - }) - .collect(); - + self.suggestions = provider.fetch_suggestions(field_index, query).await?; + // Update UI state self.ui_state.autocomplete.is_loading = false; if !self.suggestions.is_empty() { self.ui_state.autocomplete.selected_index = Some(0); } - + Ok(()) } - + /// Navigate autocomplete suggestions pub fn autocomplete_next(&mut self) { if !self.ui_state.autocomplete.is_active || self.suggestions.is_empty() { return; } - + let current = self.ui_state.autocomplete.selected_index.unwrap_or(0); let next = (current + 1) % self.suggestions.len(); self.ui_state.autocomplete.selected_index = Some(next); } - + /// Apply selected autocomplete suggestion pub fn apply_autocomplete(&mut self) -> Option { if let Some(selected_index) = self.ui_state.autocomplete.selected_index { if let Some(suggestion) = self.suggestions.get(selected_index).cloned() { let field_index = self.ui_state.current_field; - + // Apply to user's data self.data_provider.set_field_value( - field_index, + field_index, suggestion.value_to_store.clone() ); - + // Update cursor position self.ui_state.cursor_pos = suggestion.value_to_store.len(); self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; - + // Close autocomplete self.ui_state.deactivate_autocomplete(); self.suggestions.clear(); - + return Some(suggestion.display_text); } } From 8c8ba536685fbf3132d77275654237cb8a5dfa7d Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 2 Aug 2025 10:45:21 +0200 Subject: [PATCH 16/18] better example --- canvas/examples/full_canvas_demo.rs | 657 ++++++++++++++++++++++++++++ canvas/src/autocomplete/gui.rs | 6 +- canvas/src/canvas/gui.rs | 13 +- canvas/src/canvas/theme.rs | 33 ++ canvas/src/lib.rs | 5 +- 5 files changed, 709 insertions(+), 5 deletions(-) create mode 100644 canvas/examples/full_canvas_demo.rs diff --git a/canvas/examples/full_canvas_demo.rs b/canvas/examples/full_canvas_demo.rs new file mode 100644 index 0000000..343c44f --- /dev/null +++ b/canvas/examples/full_canvas_demo.rs @@ -0,0 +1,657 @@ +// examples/full_canvas_demo.rs +//! Demonstrates the FULL potential of the canvas library (excluding autocomplete) + +use std::io; +use crossterm::{ + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, + }, + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + }, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, Terminal, +}; + +use canvas::{ + canvas::{ + gui::render_canvas_default, + modes::{AppMode, ModeManager, HighlightState}, + actions::movement::{ + find_next_word_start, find_word_end, find_prev_word_start, find_prev_word_end, + line_start_position, line_end_position, safe_cursor_position, + clamp_cursor_position, + }, + }, + DataProvider, FormEditor, +}; + +// Enhanced FormEditor that exposes the full action system +struct EnhancedFormEditor { + editor: FormEditor, + highlight_state: HighlightState, + has_unsaved_changes: bool, + debug_message: String, +} + +impl EnhancedFormEditor { + fn new(data_provider: D) -> Self { + Self { + editor: FormEditor::new(data_provider), + highlight_state: HighlightState::Off, + has_unsaved_changes: false, + debug_message: "Full Canvas Demo - All features enabled".to_string(), + } + } + + // === EXPOSE ALL THE MISSING METHODS === + + /// Word movement using library's sophisticated logic + fn move_word_next(&mut self) { + let current_text = self.editor.current_text().to_string(); + let current_pos = self.editor.cursor_position(); + let new_pos = find_next_word_start(¤t_text, current_pos); + let is_edit = self.editor.mode() == AppMode::Edit; + + self.set_cursor_clamped(new_pos, ¤t_text, is_edit); + self.update_visual_selection(); + } + + fn move_word_prev(&mut self) { + let current_text = self.editor.current_text().to_string(); + let current_pos = self.editor.cursor_position(); + let new_pos = find_prev_word_start(¤t_text, current_pos); + let is_edit = self.editor.mode() == AppMode::Edit; + + self.set_cursor_clamped(new_pos, ¤t_text, is_edit); + self.update_visual_selection(); + } + + fn move_word_end(&mut self) { + let current_text = self.editor.current_text().to_string(); + let current_pos = self.editor.cursor_position(); + let new_pos = find_word_end(¤t_text, current_pos); + let is_edit = self.editor.mode() == AppMode::Edit; + + self.set_cursor_clamped(new_pos, ¤t_text, is_edit); + self.update_visual_selection(); + } + + fn move_word_end_prev(&mut self) { + let current_text = self.editor.current_text().to_string(); + let current_pos = self.editor.cursor_position(); + let new_pos = find_prev_word_end(¤t_text, current_pos); + let is_edit = self.editor.mode() == AppMode::Edit; + + self.set_cursor_clamped(new_pos, ¤t_text, is_edit); + self.update_visual_selection(); + } + + /// Line movement using library's functions + fn move_line_start(&mut self) { + let pos = line_start_position(); + let current_text = self.editor.current_text().to_string(); + let is_edit = self.editor.mode() == AppMode::Edit; + + self.set_cursor_clamped(pos, ¤t_text, is_edit); + self.update_visual_selection(); + } + + fn move_line_end(&mut self) { + let current_text = self.editor.current_text().to_string(); + let is_edit = self.editor.mode() == AppMode::Edit; + let pos = line_end_position(¤t_text, is_edit); + + self.set_cursor_clamped(pos, ¤t_text, is_edit); + self.update_visual_selection(); + } + + /// Field movement - proper implementations + fn move_to_prev_field(&mut self) { + let current = self.editor.current_field(); + let total = self.editor.data_provider().field_count(); + let _prev = if current == 0 { total - 1 } else { current - 1 }; + + // Move to previous field and position cursor properly + for _ in 0..(total - 1) { + self.editor.move_to_next_field(); + } + + // Position cursor using safe positioning + let current_text = self.editor.current_text().to_string(); + let ideal_column = 0; // Start of field when switching + let is_edit = self.editor.mode() == AppMode::Edit; + let safe_pos = safe_cursor_position(¤t_text, ideal_column, is_edit); + self.set_cursor_clamped(safe_pos, ¤t_text, is_edit); + self.update_visual_selection(); + } + + fn move_to_first_field(&mut self) { + let current = self.editor.current_field(); + let total = self.editor.data_provider().field_count(); + + // Move to first field (index 0) + for _ in 0..(total - current) { + self.editor.move_to_next_field(); + } + + let current_text = self.editor.current_text().to_string(); + let is_edit = self.editor.mode() == AppMode::Edit; + self.set_cursor_clamped(0, ¤t_text, is_edit); + self.update_visual_selection(); + } + + fn move_to_last_field(&mut self) { + let current = self.editor.current_field(); + let total = self.editor.data_provider().field_count(); + let moves_needed = (total - 1 - current) % total; + + // Move to last field + for _ in 0..moves_needed { + self.editor.move_to_next_field(); + } + + let current_text = self.editor.current_text().to_string(); + let is_edit = self.editor.mode() == AppMode::Edit; + self.set_cursor_clamped(0, ¤t_text, is_edit); + self.update_visual_selection(); + } + + /// Delete operations - proper implementations + fn delete_backward(&mut self) -> anyhow::Result<()> { + if self.editor.mode() != AppMode::Edit || self.editor.cursor_position() == 0 { + return Ok(()); + } + + let field_idx = self.editor.current_field(); + let cursor_pos = self.editor.cursor_position(); + let mut text = self.editor.data_provider().field_value(field_idx).to_string(); + + if cursor_pos > 0 && cursor_pos <= text.len() { + text.remove(cursor_pos - 1); + + // This is a limitation - we need mutable access to update the field + // For now, we'll show a message that this would work with a proper API + self.debug_message = + "Delete backward: API limitation - would remove character".to_string(); + self.has_unsaved_changes = true; + } + + Ok(()) + } + + fn delete_forward(&mut self) -> anyhow::Result<()> { + if self.editor.mode() != AppMode::Edit { + return Ok(()); + } + + let field_idx = self.editor.current_field(); + let cursor_pos = self.editor.cursor_position(); + let text = self.editor.data_provider().field_value(field_idx); + + if cursor_pos < text.len() { + // Same limitation as above + self.debug_message = + "Delete forward: API limitation - would remove character".to_string(); + self.has_unsaved_changes = true; + } + + Ok(()) + } + + /// Visual/Highlight mode support + fn enter_visual_mode(&mut self) { + if ModeManager::can_enter_highlight_mode(self.editor.mode()) { + self.editor.set_mode(AppMode::Highlight); + self.highlight_state = HighlightState::Characterwise { + anchor: ( + self.editor.current_field(), + self.editor.cursor_position(), + ), + }; + self.debug_message = "-- VISUAL --".to_string(); + } + } + + fn enter_visual_line_mode(&mut self) { + if ModeManager::can_enter_highlight_mode(self.editor.mode()) { + self.editor.set_mode(AppMode::Highlight); + self.highlight_state = + HighlightState::Linewise { anchor_line: self.editor.current_field() }; + self.debug_message = "-- VISUAL LINE --".to_string(); + } + } + + fn exit_visual_mode(&mut self) { + self.highlight_state = HighlightState::Off; + if self.editor.mode() == AppMode::Highlight { + self.editor.set_mode(AppMode::ReadOnly); + self.debug_message = "Visual mode exited".to_string(); + } + } + + /// Enhanced movement with visual selection updates + fn move_left(&mut self) { + self.editor.move_left(); + self.update_visual_selection(); + } + + fn move_right(&mut self) { + self.editor.move_right(); + self.update_visual_selection(); + } + + fn move_up(&mut self) { + self.move_to_prev_field(); + } + + fn move_down(&mut self) { + self.editor.move_to_next_field(); + self.update_visual_selection(); + } + + // === UTILITY METHODS === + + fn set_cursor_clamped(&mut self, pos: usize, text: &str, is_edit: bool) { + let clamped_pos = clamp_cursor_position(pos, text, is_edit); + // Since we can't directly set cursor, we need to move to it + while self.editor.cursor_position() < clamped_pos { + self.editor.move_right(); + } + while self.editor.cursor_position() > clamped_pos { + self.editor.move_left(); + } + } + + fn update_visual_selection(&mut self) { + if self.editor.mode() == AppMode::Highlight { + match &self.highlight_state { + HighlightState::Characterwise { anchor: _ } => { + let _current_pos = + (self.editor.current_field(), self.editor.cursor_position()); + self.debug_message = format!( + "Visual selection: char {} to {}", + self.editor.cursor_position(), + self.editor.cursor_position() + ); + } + HighlightState::Linewise { anchor_line: _ } => { + self.debug_message = format!( + "Visual line selection: field {}", + self.editor.current_field() + ); + } + _ => {} + } + } + } + + // === DELEGATE TO ORIGINAL EDITOR === + + fn current_field(&self) -> usize { + self.editor.current_field() + } + fn cursor_position(&self) -> usize { + self.editor.cursor_position() + } + fn mode(&self) -> AppMode { + self.editor.mode() + } + fn current_text(&self) -> &str { + self.editor.current_text() + } + fn data_provider(&self) -> &D { + self.editor.data_provider() + } + fn ui_state(&self) -> &canvas::EditorState { + self.editor.ui_state() + } + + fn set_mode(&mut self, mode: AppMode) { + self.editor.set_mode(mode); + if mode != AppMode::Highlight { + self.exit_visual_mode(); + } + } + + fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { + let result = self.editor.insert_char(ch); + if result.is_ok() { + self.has_unsaved_changes = true; + } + result + } + + fn set_debug_message(&mut self, msg: String) { + self.debug_message = msg; + } + + fn debug_message(&self) -> &str { + &self.debug_message + } + + fn highlight_state(&self) -> &HighlightState { + &self.highlight_state + } + + fn has_unsaved_changes(&self) -> bool { + self.has_unsaved_changes + } +} + +// Demo form data with interesting text for word movement +struct FullDemoData { + fields: Vec<(String, String)>, +} + +impl FullDemoData { + fn new() -> Self { + Self { + fields: vec![ + ("Name".to_string(), "John-Paul McDonald".to_string()), + ( + "Email".to_string(), + "user@example-domain.com".to_string(), + ), + ("Phone".to_string(), "+1 (555) 123-4567".to_string()), + ("Address".to_string(), "123 Main St, Apt 4B".to_string()), + ( + "Tags".to_string(), + "urgent,important,follow-up".to_string(), + ), + ( + "Notes".to_string(), + "This is a sample note with multiple words, punctuation! And symbols @#$" + .to_string(), + ), + ], + } + } +} + +impl DataProvider for FullDemoData { + fn field_count(&self) -> usize { + self.fields.len() + } + fn field_name(&self, index: usize) -> &str { + &self.fields[index].0 + } + fn field_value(&self, index: usize) -> &str { + &self.fields[index].1 + } + fn set_field_value(&mut self, index: usize, value: String) { + self.fields[index].1 = value; + } + fn supports_autocomplete(&self, _field_index: usize) -> bool { + false + } + fn display_value(&self, _index: usize) -> Option<&str> { + None + } +} + +/// Full vim-like key handling using ALL library features +fn handle_key_press( + key: KeyCode, + modifiers: KeyModifiers, + editor: &mut EnhancedFormEditor, +) -> anyhow::Result { + let mode = editor.mode(); + + // Quit handling + if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) + || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) + || key == KeyCode::F(10) + { + return Ok(false); + } + + match (mode, key, modifiers) { + // === MODE TRANSITIONS === + (AppMode::ReadOnly, KeyCode::Char('i'), _) => { + if ModeManager::can_enter_edit_mode(mode) { + editor.set_mode(AppMode::Edit); + editor.set_debug_message("-- INSERT --".to_string()); + } + } + (AppMode::ReadOnly, KeyCode::Char('a'), _) => { + editor.move_line_end(); + if ModeManager::can_enter_edit_mode(mode) { + editor.set_mode(AppMode::Edit); + editor.set_debug_message("-- INSERT -- (append)".to_string()); + } + } + (AppMode::ReadOnly, KeyCode::Char('A'), _) => { + editor.move_line_end(); + if ModeManager::can_enter_edit_mode(mode) { + editor.set_mode(AppMode::Edit); + editor.set_debug_message("-- INSERT -- (end of line)".to_string()); + } + } + (AppMode::ReadOnly, KeyCode::Char('v'), _) => { + editor.enter_visual_mode(); + } + (AppMode::ReadOnly, KeyCode::Char('V'), _) => { + editor.enter_visual_line_mode(); + } + (_, KeyCode::Esc, _) => { + if ModeManager::can_enter_read_only_mode(mode) { + editor.set_mode(AppMode::ReadOnly); + editor.exit_visual_mode(); + editor.set_debug_message("".to_string()); + } + } + + // === MOVEMENT: All the vim goodness === + + // Basic movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('h'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => { + editor.move_left(); + editor.set_debug_message("move left".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => { + editor.move_right(); + editor.set_debug_message("move right".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => { + editor.move_down(); + editor.set_debug_message("move down".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => { + editor.move_up(); + editor.set_debug_message("move up".to_string()); + } + + // Word movement - THE FULL VIM EXPERIENCE + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => { + editor.move_word_next(); + editor.set_debug_message("next word start".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => { + editor.move_word_prev(); + editor.set_debug_message("previous word start".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => { + editor.move_word_end(); + editor.set_debug_message("word end".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('B'), _) => { + editor.move_word_end_prev(); + editor.set_debug_message("previous word end".to_string()); + } + + // Line movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('0'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Home, _) => { + editor.move_line_start(); + editor.set_debug_message("line start".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('$'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::End, _) => { + editor.move_line_end(); + editor.set_debug_message("line end".to_string()); + } + + // Field movement - advanced navigation + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => { + editor.move_to_first_field(); + editor.set_debug_message("first field".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => { + editor.move_to_last_field(); + editor.set_debug_message("last field".to_string()); + } + + // === EDIT MODE === + (AppMode::Edit, KeyCode::Left, _) => editor.move_left(), + (AppMode::Edit, KeyCode::Right, _) => editor.move_right(), + (AppMode::Edit, KeyCode::Up, _) => editor.move_up(), + (AppMode::Edit, KeyCode::Down, _) => editor.move_down(), + (AppMode::Edit, KeyCode::Home, _) => editor.move_line_start(), + (AppMode::Edit, KeyCode::End, _) => editor.move_line_end(), + + // Word movement in edit mode with Ctrl + (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_prev(); + } + (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_next(); + } + + // DELETE OPERATIONS + (AppMode::Edit, KeyCode::Backspace, _) => { + editor.delete_backward()?; + } + (AppMode::Edit, KeyCode::Delete, _) => { + editor.delete_forward()?; + } + + // Tab navigation + (_, KeyCode::Tab, _) => { + editor.editor.move_to_next_field(); + editor.set_debug_message("next field".to_string()); + } + (_, KeyCode::BackTab, _) => { + editor.move_to_prev_field(); + editor.set_debug_message("previous field".to_string()); + } + + // Character input + (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { + editor.insert_char(c)?; + } + + _ => { + editor.set_debug_message(format!("Unhandled: {:?} in {:?} mode", key, mode)); + } + } + + Ok(true) +} + +fn run_app( + terminal: &mut Terminal, + mut editor: EnhancedFormEditor, +) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &editor))?; + + if let Event::Key(key) = event::read()? { + match handle_key_press(key.code, key.modifiers, &mut editor) { + Ok(should_continue) => { + if !should_continue { + break; + } + } + Err(e) => { + editor.set_debug_message(format!("Error: {}", e)); + } + } + } + } + + Ok(()) +} + +fn ui(f: &mut Frame, editor: &EnhancedFormEditor) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(8), Constraint::Length(6)]) + .split(f.area()); + + render_enhanced_canvas(f, chunks[0], editor); + render_status_bar(f, chunks[1], editor); +} + +fn render_enhanced_canvas( + f: &mut Frame, + area: ratatui::layout::Rect, + editor: &EnhancedFormEditor, +) { + // Uses the library default theme; no theme needed. + render_canvas_default(f, area, &editor.editor); +} + +fn render_status_bar( + f: &mut Frame, + area: ratatui::layout::Rect, + editor: &EnhancedFormEditor, +) { + let mode_text = match editor.mode() { + AppMode::Edit => "INSERT", + AppMode::ReadOnly => "NORMAL", + AppMode::Highlight => match editor.highlight_state() { + HighlightState::Characterwise { .. } => "VISUAL", + HighlightState::Linewise { .. } => "VISUAL", + _ => "VISUAL", + }, + _ => "NORMAL", + }; + + let status = Paragraph::new(Line::from(Span::raw(format!( + "-- {} --", + mode_text + )))) + .block(Block::default().borders(Borders::ALL).title("Mode")); + + f.render_widget(status, area); +} + +fn main() -> Result<(), Box> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let data = FullDemoData::new(); + let mut editor = EnhancedFormEditor::new(data); + editor.set_mode(AppMode::ReadOnly); // Start in normal mode + + let res = run_app(&mut terminal, editor); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + Ok(()) +} diff --git a/canvas/src/autocomplete/gui.rs b/canvas/src/autocomplete/gui.rs index 0f74cb0..70fb894 100644 --- a/canvas/src/autocomplete/gui.rs +++ b/canvas/src/autocomplete/gui.rs @@ -5,7 +5,7 @@ use ratatui::{ layout::{Alignment, Rect}, style::{Modifier, Style}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + widgets::{Block, List, ListItem, ListState, Paragraph}, // Removed Borders Frame, }; @@ -27,7 +27,7 @@ pub fn render_autocomplete_dropdown( editor: &FormEditor, ) { let ui_state = editor.ui_state(); - + if !ui_state.is_autocomplete_active() { return; } @@ -76,7 +76,7 @@ fn render_suggestions_dropdown( frame_area: Rect, input_rect: Rect, theme: &T, - suggestions: &[SuggestionItem], + suggestions: &[SuggestionItem], // Fixed: Removed generic parameter selected_index: Option, ) { let display_texts: Vec<&str> = suggestions diff --git a/canvas/src/canvas/gui.rs b/canvas/src/canvas/gui.rs index af7131a..dcd106e 100644 --- a/canvas/src/canvas/gui.rs +++ b/canvas/src/canvas/gui.rs @@ -11,7 +11,7 @@ use ratatui::{ }; #[cfg(feature = "gui")] -use crate::canvas::theme::CanvasTheme; +use crate::canvas::theme::{CanvasTheme, DefaultCanvasTheme}; use crate::canvas::modes::HighlightState; use crate::data_provider::DataProvider; use crate::editor::FormEditor; @@ -353,3 +353,14 @@ fn set_cursor_position( let cursor_y = field_rect.y; f.set_cursor_position((cursor_x, cursor_y)); } + +/// Set default theme if custom not specified +#[cfg(feature = "gui")] +pub fn render_canvas_default( + f: &mut Frame, + area: Rect, + editor: &FormEditor, +) -> Option { + let theme = DefaultCanvasTheme::default(); + render_canvas(f, area, editor, &theme) +} diff --git a/canvas/src/canvas/theme.rs b/canvas/src/canvas/theme.rs index 6ea3932..d2f02d2 100644 --- a/canvas/src/canvas/theme.rs +++ b/canvas/src/canvas/theme.rs @@ -15,3 +15,36 @@ pub trait CanvasTheme { fn highlight_bg(&self) -> Color; fn warning(&self) -> Color; } + + +#[cfg(feature = "gui")] +#[derive(Debug, Clone, Default)] +pub struct DefaultCanvasTheme; + +#[cfg(feature = "gui")] +impl CanvasTheme for DefaultCanvasTheme { + fn bg(&self) -> Color { + Color::Black + } + fn fg(&self) -> Color { + Color::White + } + fn border(&self) -> Color { + Color::DarkGray + } + fn accent(&self) -> Color { + Color::Cyan + } + fn secondary(&self) -> Color { + Color::Gray + } + fn highlight(&self) -> Color { + Color::Yellow + } + fn highlight_bg(&self) -> Color { + Color::Blue + } + fn warning(&self) -> Color { + Color::Red + } +} diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index d145f29..a5c2696 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -25,10 +25,13 @@ pub use canvas::actions::{CanvasAction, ActionResult}; // Theming and GUI #[cfg(feature = "gui")] -pub use canvas::theme::CanvasTheme; +pub use canvas::theme::{CanvasTheme, DefaultCanvasTheme}; #[cfg(feature = "gui")] pub use canvas::gui::render_canvas; +#[cfg(feature = "gui")] +pub use canvas::gui::render_canvas_default; + #[cfg(all(feature = "gui", feature = "autocomplete"))] pub use autocomplete::gui::render_autocomplete_dropdown; From c92c6173148c7810d5adc7501b3ca9717449126e Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 2 Aug 2025 13:41:21 +0200 Subject: [PATCH 17/18] exposed api to full vim mode --- canvas/examples/full_canvas_demo.rs | 531 ++++++++++++++-------------- canvas/src/canvas/state.rs | 5 + canvas/src/editor.rs | 237 +++++++++++++ 3 files changed, 499 insertions(+), 274 deletions(-) diff --git a/canvas/examples/full_canvas_demo.rs b/canvas/examples/full_canvas_demo.rs index 343c44f..b65c0b4 100644 --- a/canvas/examples/full_canvas_demo.rs +++ b/canvas/examples/full_canvas_demo.rs @@ -1,5 +1,5 @@ // examples/full_canvas_demo.rs -//! Demonstrates the FULL potential of the canvas library (excluding autocomplete) +//! Demonstrates the FULL potential of the canvas library using the native API use std::io; use crossterm::{ @@ -24,16 +24,11 @@ use canvas::{ canvas::{ gui::render_canvas_default, modes::{AppMode, ModeManager, HighlightState}, - actions::movement::{ - find_next_word_start, find_word_end, find_prev_word_start, find_prev_word_end, - line_start_position, line_end_position, safe_cursor_position, - clamp_cursor_position, - }, }, DataProvider, FormEditor, }; -// Enhanced FormEditor that exposes the full action system +// Enhanced FormEditor that adds visual mode and status tracking struct EnhancedFormEditor { editor: FormEditor, highlight_state: HighlightState, @@ -51,162 +46,8 @@ impl EnhancedFormEditor { } } - // === EXPOSE ALL THE MISSING METHODS === - - /// Word movement using library's sophisticated logic - fn move_word_next(&mut self) { - let current_text = self.editor.current_text().to_string(); - let current_pos = self.editor.cursor_position(); - let new_pos = find_next_word_start(¤t_text, current_pos); - let is_edit = self.editor.mode() == AppMode::Edit; - - self.set_cursor_clamped(new_pos, ¤t_text, is_edit); - self.update_visual_selection(); - } - - fn move_word_prev(&mut self) { - let current_text = self.editor.current_text().to_string(); - let current_pos = self.editor.cursor_position(); - let new_pos = find_prev_word_start(¤t_text, current_pos); - let is_edit = self.editor.mode() == AppMode::Edit; - - self.set_cursor_clamped(new_pos, ¤t_text, is_edit); - self.update_visual_selection(); - } - - fn move_word_end(&mut self) { - let current_text = self.editor.current_text().to_string(); - let current_pos = self.editor.cursor_position(); - let new_pos = find_word_end(¤t_text, current_pos); - let is_edit = self.editor.mode() == AppMode::Edit; - - self.set_cursor_clamped(new_pos, ¤t_text, is_edit); - self.update_visual_selection(); - } - - fn move_word_end_prev(&mut self) { - let current_text = self.editor.current_text().to_string(); - let current_pos = self.editor.cursor_position(); - let new_pos = find_prev_word_end(¤t_text, current_pos); - let is_edit = self.editor.mode() == AppMode::Edit; - - self.set_cursor_clamped(new_pos, ¤t_text, is_edit); - self.update_visual_selection(); - } - - /// Line movement using library's functions - fn move_line_start(&mut self) { - let pos = line_start_position(); - let current_text = self.editor.current_text().to_string(); - let is_edit = self.editor.mode() == AppMode::Edit; - - self.set_cursor_clamped(pos, ¤t_text, is_edit); - self.update_visual_selection(); - } - - fn move_line_end(&mut self) { - let current_text = self.editor.current_text().to_string(); - let is_edit = self.editor.mode() == AppMode::Edit; - let pos = line_end_position(¤t_text, is_edit); - - self.set_cursor_clamped(pos, ¤t_text, is_edit); - self.update_visual_selection(); - } - - /// Field movement - proper implementations - fn move_to_prev_field(&mut self) { - let current = self.editor.current_field(); - let total = self.editor.data_provider().field_count(); - let _prev = if current == 0 { total - 1 } else { current - 1 }; - - // Move to previous field and position cursor properly - for _ in 0..(total - 1) { - self.editor.move_to_next_field(); - } - - // Position cursor using safe positioning - let current_text = self.editor.current_text().to_string(); - let ideal_column = 0; // Start of field when switching - let is_edit = self.editor.mode() == AppMode::Edit; - let safe_pos = safe_cursor_position(¤t_text, ideal_column, is_edit); - self.set_cursor_clamped(safe_pos, ¤t_text, is_edit); - self.update_visual_selection(); - } - - fn move_to_first_field(&mut self) { - let current = self.editor.current_field(); - let total = self.editor.data_provider().field_count(); - - // Move to first field (index 0) - for _ in 0..(total - current) { - self.editor.move_to_next_field(); - } - - let current_text = self.editor.current_text().to_string(); - let is_edit = self.editor.mode() == AppMode::Edit; - self.set_cursor_clamped(0, ¤t_text, is_edit); - self.update_visual_selection(); - } - - fn move_to_last_field(&mut self) { - let current = self.editor.current_field(); - let total = self.editor.data_provider().field_count(); - let moves_needed = (total - 1 - current) % total; - - // Move to last field - for _ in 0..moves_needed { - self.editor.move_to_next_field(); - } - - let current_text = self.editor.current_text().to_string(); - let is_edit = self.editor.mode() == AppMode::Edit; - self.set_cursor_clamped(0, ¤t_text, is_edit); - self.update_visual_selection(); - } - - /// Delete operations - proper implementations - fn delete_backward(&mut self) -> anyhow::Result<()> { - if self.editor.mode() != AppMode::Edit || self.editor.cursor_position() == 0 { - return Ok(()); - } - - let field_idx = self.editor.current_field(); - let cursor_pos = self.editor.cursor_position(); - let mut text = self.editor.data_provider().field_value(field_idx).to_string(); - - if cursor_pos > 0 && cursor_pos <= text.len() { - text.remove(cursor_pos - 1); - - // This is a limitation - we need mutable access to update the field - // For now, we'll show a message that this would work with a proper API - self.debug_message = - "Delete backward: API limitation - would remove character".to_string(); - self.has_unsaved_changes = true; - } - - Ok(()) - } - - fn delete_forward(&mut self) -> anyhow::Result<()> { - if self.editor.mode() != AppMode::Edit { - return Ok(()); - } - - let field_idx = self.editor.current_field(); - let cursor_pos = self.editor.cursor_position(); - let text = self.editor.data_provider().field_value(field_idx); - - if cursor_pos < text.len() { - // Same limitation as above - self.debug_message = - "Delete forward: API limitation - would remove character".to_string(); - self.has_unsaved_changes = true; - } - - Ok(()) - } - - /// Visual/Highlight mode support + // === VISUAL/HIGHLIGHT MODE SUPPORT === + fn enter_visual_mode(&mut self) { if ModeManager::can_enter_highlight_mode(self.editor.mode()) { self.editor.set_mode(AppMode::Highlight); @@ -237,49 +78,14 @@ impl EnhancedFormEditor { } } - /// Enhanced movement with visual selection updates - fn move_left(&mut self) { - self.editor.move_left(); - self.update_visual_selection(); - } - - fn move_right(&mut self) { - self.editor.move_right(); - self.update_visual_selection(); - } - - fn move_up(&mut self) { - self.move_to_prev_field(); - } - - fn move_down(&mut self) { - self.editor.move_to_next_field(); - self.update_visual_selection(); - } - - // === UTILITY METHODS === - - fn set_cursor_clamped(&mut self, pos: usize, text: &str, is_edit: bool) { - let clamped_pos = clamp_cursor_position(pos, text, is_edit); - // Since we can't directly set cursor, we need to move to it - while self.editor.cursor_position() < clamped_pos { - self.editor.move_right(); - } - while self.editor.cursor_position() > clamped_pos { - self.editor.move_left(); - } - } - fn update_visual_selection(&mut self) { if self.editor.mode() == AppMode::Highlight { match &self.highlight_state { HighlightState::Characterwise { anchor: _ } => { - let _current_pos = - (self.editor.current_field(), self.editor.cursor_position()); self.debug_message = format!( - "Visual selection: char {} to {}", + "Visual selection: char {} in field {}", self.editor.cursor_position(), - self.editor.cursor_position() + self.editor.current_field() ); } HighlightState::Linewise { anchor_line: _ } => { @@ -293,23 +99,141 @@ impl EnhancedFormEditor { } } + // === ENHANCED MOVEMENT WITH VISUAL UPDATES === + + fn move_left(&mut self) { + self.editor.move_left(); + self.update_visual_selection(); + } + + fn move_right(&mut self) { + self.editor.move_right(); + self.update_visual_selection(); + } + + fn move_up(&mut self) { + self.editor.move_up(); + self.update_visual_selection(); + } + + fn move_down(&mut self) { + self.editor.move_down(); + self.update_visual_selection(); + } + + fn move_word_next(&mut self) { + self.editor.move_word_next(); + self.update_visual_selection(); + } + + fn move_word_prev(&mut self) { + self.editor.move_word_prev(); + self.update_visual_selection(); + } + + fn move_word_end(&mut self) { + self.editor.move_word_end(); + self.update_visual_selection(); + } + + fn move_word_end_prev(&mut self) { + self.editor.move_word_end_prev(); + self.update_visual_selection(); + } + + fn move_line_start(&mut self) { + self.editor.move_line_start(); + self.update_visual_selection(); + } + + fn move_line_end(&mut self) { + self.editor.move_line_end(); + self.update_visual_selection(); + } + + fn move_first_line(&mut self) { + self.editor.move_first_line(); + self.update_visual_selection(); + } + + fn move_last_line(&mut self) { + self.editor.move_last_line(); + self.update_visual_selection(); + } + + fn prev_field(&mut self) { + self.editor.prev_field(); + self.update_visual_selection(); + } + + fn next_field(&mut self) { + self.editor.next_field(); + self.update_visual_selection(); + } + + // === DELETE OPERATIONS === + + fn delete_backward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_backward(); + if result.is_ok() { + self.has_unsaved_changes = true; + self.debug_message = "Deleted character backward".to_string(); + } + Ok(result?) + } + + fn delete_forward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_forward(); + if result.is_ok() { + self.has_unsaved_changes = true; + self.debug_message = "Deleted character forward".to_string(); + } + Ok(result?) + } + + // === MODE TRANSITIONS === + + fn enter_edit_mode(&mut self) { + self.editor.enter_edit_mode(); + self.debug_message = "-- INSERT --".to_string(); + } + + fn exit_edit_mode(&mut self) { + self.editor.exit_edit_mode(); + self.exit_visual_mode(); + self.debug_message = "".to_string(); + } + + fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { + let result = self.editor.insert_char(ch); + if result.is_ok() { + self.has_unsaved_changes = true; + } + Ok(result?) + } + // === DELEGATE TO ORIGINAL EDITOR === fn current_field(&self) -> usize { self.editor.current_field() } + fn cursor_position(&self) -> usize { self.editor.cursor_position() } + fn mode(&self) -> AppMode { self.editor.mode() } + fn current_text(&self) -> &str { self.editor.current_text() } + fn data_provider(&self) -> &D { self.editor.data_provider() } + fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } @@ -321,14 +245,8 @@ impl EnhancedFormEditor { } } - fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { - let result = self.editor.insert_char(ch); - if result.is_ok() { - self.has_unsaved_changes = true; - } - result - } - + // === STATUS AND DEBUG === + fn set_debug_message(&mut self, msg: String) { self.debug_message = msg; } @@ -380,24 +298,29 @@ impl DataProvider for FullDemoData { fn field_count(&self) -> usize { self.fields.len() } + fn field_name(&self, index: usize) -> &str { &self.fields[index].0 } + fn field_value(&self, index: usize) -> &str { &self.fields[index].1 } + fn set_field_value(&mut self, index: usize, value: String) { self.fields[index].1 = value; } + fn supports_autocomplete(&self, _field_index: usize) -> bool { false } + fn display_value(&self, _index: usize) -> Option<&str> { None } } -/// Full vim-like key handling using ALL library features +/// Full vim-like key handling using the native FormEditor API fn handle_key_press( key: KeyCode, modifiers: KeyModifiers, @@ -416,24 +339,22 @@ fn handle_key_press( match (mode, key, modifiers) { // === MODE TRANSITIONS === (AppMode::ReadOnly, KeyCode::Char('i'), _) => { - if ModeManager::can_enter_edit_mode(mode) { - editor.set_mode(AppMode::Edit); - editor.set_debug_message("-- INSERT --".to_string()); - } + editor.enter_edit_mode(); } (AppMode::ReadOnly, KeyCode::Char('a'), _) => { - editor.move_line_end(); - if ModeManager::can_enter_edit_mode(mode) { - editor.set_mode(AppMode::Edit); - editor.set_debug_message("-- INSERT -- (append)".to_string()); - } + editor.move_right(); // Move after current character + editor.enter_edit_mode(); + editor.set_debug_message("-- INSERT -- (append)".to_string()); } (AppMode::ReadOnly, KeyCode::Char('A'), _) => { editor.move_line_end(); - if ModeManager::can_enter_edit_mode(mode) { - editor.set_mode(AppMode::Edit); - editor.set_debug_message("-- INSERT -- (end of line)".to_string()); - } + editor.enter_edit_mode(); + editor.set_debug_message("-- INSERT -- (end of line)".to_string()); + } + (AppMode::ReadOnly, KeyCode::Char('o'), _) => { + editor.move_line_end(); + editor.enter_edit_mode(); + editor.set_debug_message("-- INSERT -- (open line)".to_string()); } (AppMode::ReadOnly, KeyCode::Char('v'), _) => { editor.enter_visual_mode(); @@ -442,94 +363,104 @@ fn handle_key_press( editor.enter_visual_line_mode(); } (_, KeyCode::Esc, _) => { - if ModeManager::can_enter_read_only_mode(mode) { - editor.set_mode(AppMode::ReadOnly); - editor.exit_visual_mode(); - editor.set_debug_message("".to_string()); - } + editor.exit_edit_mode(); } - // === MOVEMENT: All the vim goodness === + // === MOVEMENT: VIM-STYLE NAVIGATION === - // Basic movement + // Basic movement (hjkl and arrows) (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('h'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => { editor.move_left(); - editor.set_debug_message("move left".to_string()); + editor.set_debug_message("← left".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => { editor.move_right(); - editor.set_debug_message("move right".to_string()); + editor.set_debug_message("→ right".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => { editor.move_down(); - editor.set_debug_message("move down".to_string()); + editor.set_debug_message("↓ next field".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => { editor.move_up(); - editor.set_debug_message("move up".to_string()); + editor.set_debug_message("↑ previous field".to_string()); } - // Word movement - THE FULL VIM EXPERIENCE + // Word movement - Full vim word navigation (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => { editor.move_word_next(); - editor.set_debug_message("next word start".to_string()); + editor.set_debug_message("w: next word start".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => { editor.move_word_prev(); - editor.set_debug_message("previous word start".to_string()); + editor.set_debug_message("b: previous word start".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => { editor.move_word_end(); - editor.set_debug_message("word end".to_string()); + editor.set_debug_message("e: word end".to_string()); } - (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('B'), _) => { + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('W'), _) => { editor.move_word_end_prev(); - editor.set_debug_message("previous word end".to_string()); + editor.set_debug_message("W: previous word end".to_string()); } // Line movement (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('0'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Home, _) => { editor.move_line_start(); - editor.set_debug_message("line start".to_string()); + editor.set_debug_message("0: line start".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('$'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::End, _) => { editor.move_line_end(); - editor.set_debug_message("line end".to_string()); + editor.set_debug_message("$: line end".to_string()); } - // Field movement - advanced navigation + // Field/document movement (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => { - editor.move_to_first_field(); - editor.set_debug_message("first field".to_string()); + editor.move_first_line(); + editor.set_debug_message("gg: first field".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => { - editor.move_to_last_field(); - editor.set_debug_message("last field".to_string()); + editor.move_last_line(); + editor.set_debug_message("G: last field".to_string()); } - // === EDIT MODE === - (AppMode::Edit, KeyCode::Left, _) => editor.move_left(), - (AppMode::Edit, KeyCode::Right, _) => editor.move_right(), - (AppMode::Edit, KeyCode::Up, _) => editor.move_up(), - (AppMode::Edit, KeyCode::Down, _) => editor.move_down(), - (AppMode::Edit, KeyCode::Home, _) => editor.move_line_start(), - (AppMode::Edit, KeyCode::End, _) => editor.move_line_end(), + // === EDIT MODE MOVEMENT === + (AppMode::Edit, KeyCode::Left, _) => { + editor.move_left(); + } + (AppMode::Edit, KeyCode::Right, _) => { + editor.move_right(); + } + (AppMode::Edit, KeyCode::Up, _) => { + editor.move_up(); + } + (AppMode::Edit, KeyCode::Down, _) => { + editor.move_down(); + } + (AppMode::Edit, KeyCode::Home, _) => { + editor.move_line_start(); + } + (AppMode::Edit, KeyCode::End, _) => { + editor.move_line_end(); + } // Word movement in edit mode with Ctrl (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => { editor.move_word_prev(); + editor.set_debug_message("Ctrl+← word back".to_string()); } (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => { editor.move_word_next(); + editor.set_debug_message("Ctrl+→ word forward".to_string()); } - // DELETE OPERATIONS + // === DELETE OPERATIONS === (AppMode::Edit, KeyCode::Backspace, _) => { editor.delete_backward()?; } @@ -537,23 +468,47 @@ fn handle_key_press( editor.delete_forward()?; } - // Tab navigation - (_, KeyCode::Tab, _) => { - editor.editor.move_to_next_field(); - editor.set_debug_message("next field".to_string()); + // Delete operations in normal mode (vim x) + (AppMode::ReadOnly, KeyCode::Char('x'), _) => { + editor.delete_forward()?; + editor.set_debug_message("x: deleted character".to_string()); } - (_, KeyCode::BackTab, _) => { - editor.move_to_prev_field(); - editor.set_debug_message("previous field".to_string()); + (AppMode::ReadOnly, KeyCode::Char('X'), _) => { + editor.delete_backward()?; + editor.set_debug_message("X: deleted character backward".to_string()); } - // Character input + // === TAB NAVIGATION === + (_, KeyCode::Tab, _) => { + editor.next_field(); + editor.set_debug_message("Tab: next field".to_string()); + } + (_, KeyCode::BackTab, _) => { + editor.prev_field(); + editor.set_debug_message("Shift+Tab: previous field".to_string()); + } + + // === CHARACTER INPUT === (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { editor.insert_char(c)?; } + // === DEBUG/INFO COMMANDS === + (AppMode::ReadOnly, KeyCode::Char('?'), _) => { + editor.set_debug_message(format!( + "Field {}/{}, Pos {}, Mode: {:?}", + editor.current_field() + 1, + editor.data_provider().field_count(), + editor.cursor_position(), + editor.mode() + )); + } + _ => { - editor.set_debug_message(format!("Unhandled: {:?} in {:?} mode", key, mode)); + editor.set_debug_message(format!( + "Unhandled: {:?} + {:?} in {:?} mode", + key, modifiers, mode + )); } } @@ -587,11 +542,11 @@ fn run_app( fn ui(f: &mut Frame, editor: &EnhancedFormEditor) { let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(8), Constraint::Length(6)]) + .constraints([Constraint::Min(8), Constraint::Length(8)]) .split(f.area()); render_enhanced_canvas(f, chunks[0], editor); - render_status_bar(f, chunks[1], editor); + render_status_and_help(f, chunks[1], editor); } fn render_enhanced_canvas( @@ -599,33 +554,61 @@ fn render_enhanced_canvas( area: ratatui::layout::Rect, editor: &EnhancedFormEditor, ) { - // Uses the library default theme; no theme needed. render_canvas_default(f, area, &editor.editor); } -fn render_status_bar( +fn render_status_and_help( f: &mut Frame, area: ratatui::layout::Rect, editor: &EnhancedFormEditor, ) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Length(5)]) + .split(area); + + // Status bar let mode_text = match editor.mode() { AppMode::Edit => "INSERT", AppMode::ReadOnly => "NORMAL", AppMode::Highlight => match editor.highlight_state() { HighlightState::Characterwise { .. } => "VISUAL", - HighlightState::Linewise { .. } => "VISUAL", + HighlightState::Linewise { .. } => "VISUAL LINE", _ => "VISUAL", }, _ => "NORMAL", }; - let status = Paragraph::new(Line::from(Span::raw(format!( - "-- {} --", - mode_text - )))) - .block(Block::default().borders(Borders::ALL).title("Mode")); + let status_text = if editor.has_unsaved_changes() { + format!("-- {} -- [Modified] {}", mode_text, editor.debug_message()) + } else { + format!("-- {} -- {}", mode_text, editor.debug_message()) + }; - f.render_widget(status, area); + let status = Paragraph::new(Line::from(Span::raw(status_text))) + .block(Block::default().borders(Borders::ALL).title("Status")); + + f.render_widget(status, chunks[0]); + + // Help text + let help_text = match editor.mode() { + AppMode::ReadOnly => { + "Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last, i/a/A=insert, v/V=visual, x/X=delete, ?=info" + } + AppMode::Edit => { + "Insert: arrows=move, Ctrl+arrows=words, Backspace/Del=delete, Esc=normal, Tab/Shift+Tab=fields" + } + AppMode::Highlight => { + "Visual: hjkl/arrows=extend selection, w/b/e=word selection, Esc=normal" + } + _ => "Press ? for help" + }; + + let help = Paragraph::new(Line::from(Span::raw(help_text))) + .block(Block::default().borders(Borders::ALL).title("Commands")) + .style(Style::default().fg(Color::Gray)); + + f.render_widget(help, chunks[1]); } fn main() -> Result<(), Box> { diff --git a/canvas/src/canvas/state.rs b/canvas/src/canvas/state.rs index fe59ac3..54fcad8 100644 --- a/canvas/src/canvas/state.rs +++ b/canvas/src/canvas/state.rs @@ -66,6 +66,11 @@ impl EditorState { pub fn cursor_position(&self) -> usize { self.cursor_pos } + + /// Get ideal cursor column (for vim-like behavior) + pub fn ideal_cursor_column(&self) -> usize { // ADD THIS + self.ideal_cursor_column + } /// Get current mode (for user's business logic) pub fn mode(&self) -> AppMode { diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 4e5b941..c84c2e3 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -220,4 +220,241 @@ impl FormEditor { } None } + + // =================================================================== + // ADD THESE MISSING MOVEMENT METHODS + // =================================================================== + + /// Move to previous field (vim k / up arrow) + pub fn move_up(&mut self) { + let field_count = self.data_provider.field_count(); + if field_count == 0 { + return; + } + + let current_field = self.ui_state.current_field; + let new_field = current_field.saturating_sub(1); + + self.ui_state.move_to_field(new_field, field_count); + self.clamp_cursor_to_current_field(); + } + + /// Move to next field (vim j / down arrow) + pub fn move_down(&mut self) { + let field_count = self.data_provider.field_count(); + if field_count == 0 { + return; + } + + let current_field = self.ui_state.current_field; + let new_field = (current_field + 1).min(field_count - 1); + + self.ui_state.move_to_field(new_field, field_count); + self.clamp_cursor_to_current_field(); + } + + /// Move to first field (vim gg) + pub fn move_first_line(&mut self) { + let field_count = self.data_provider.field_count(); + if field_count == 0 { + return; + } + + self.ui_state.move_to_field(0, field_count); + self.clamp_cursor_to_current_field(); + } + + /// Move to last field (vim G) + pub fn move_last_line(&mut self) { + let field_count = self.data_provider.field_count(); + if field_count == 0 { + return; + } + + let last_field = field_count - 1; + self.ui_state.move_to_field(last_field, field_count); + self.clamp_cursor_to_current_field(); + } + + /// Move to previous field (alternative to move_up) + pub fn prev_field(&mut self) { + self.move_up(); + } + + /// Move to next field (alternative to move_down) + pub fn next_field(&mut self) { + self.move_down(); + } + + /// Move to start of current field (vim 0) + pub fn move_line_start(&mut self) { + use crate::canvas::actions::movement::line::line_start_position; + let new_pos = line_start_position(); + self.ui_state.cursor_pos = new_pos; + self.ui_state.ideal_cursor_column = new_pos; + } + + /// Move to end of current field (vim $) + pub fn move_line_end(&mut self) { + use crate::canvas::actions::movement::line::line_end_position; + let current_text = self.current_text(); + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + + let new_pos = line_end_position(current_text, is_edit_mode); + self.ui_state.cursor_pos = new_pos; + self.ui_state.ideal_cursor_column = new_pos; + } + + /// Move to start of next word (vim w) + pub fn move_word_next(&mut self) { + use crate::canvas::actions::movement::word::find_next_word_start; + let current_text = self.current_text(); + + if current_text.is_empty() { + return; + } + + let new_pos = find_next_word_start(current_text, self.ui_state.cursor_pos); + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + + // Clamp to valid bounds for current mode + let final_pos = if is_edit_mode { + new_pos.min(current_text.len()) + } else { + new_pos.min(current_text.len().saturating_sub(1)) + }; + + self.ui_state.cursor_pos = final_pos; + self.ui_state.ideal_cursor_column = final_pos; + } + + /// Move to start of previous word (vim b) + pub fn move_word_prev(&mut self) { + use crate::canvas::actions::movement::word::find_prev_word_start; + let current_text = self.current_text(); + + if current_text.is_empty() { + return; + } + + let new_pos = find_prev_word_start(current_text, self.ui_state.cursor_pos); + self.ui_state.cursor_pos = new_pos; + self.ui_state.ideal_cursor_column = new_pos; + } + + /// Move to end of current/next word (vim e) + pub fn move_word_end(&mut self) { + use crate::canvas::actions::movement::word::find_word_end; + let current_text = self.current_text(); + + if current_text.is_empty() { + return; + } + + let current_pos = self.ui_state.cursor_pos; + let new_pos = find_word_end(current_text, current_pos); + + // If we didn't move, try next word + let final_pos = if new_pos == current_pos && current_pos + 1 < current_text.len() { + find_word_end(current_text, current_pos + 1) + } else { + new_pos + }; + + // Clamp for read-only mode + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + let clamped_pos = if is_edit_mode { + final_pos.min(current_text.len()) + } else { + final_pos.min(current_text.len().saturating_sub(1)) + }; + + self.ui_state.cursor_pos = clamped_pos; + self.ui_state.ideal_cursor_column = clamped_pos; + } + + /// Move to end of previous word (vim ge) + pub fn move_word_end_prev(&mut self) { + use crate::canvas::actions::movement::word::find_prev_word_end; + let current_text = self.current_text(); + + if current_text.is_empty() { + return; + } + + let new_pos = find_prev_word_end(current_text, self.ui_state.cursor_pos); + self.ui_state.cursor_pos = new_pos; + self.ui_state.ideal_cursor_column = new_pos; + } + + /// Delete character before cursor (vim x in insert mode / backspace) + pub fn delete_backward(&mut self) -> Result<()> { + if self.ui_state.current_mode != AppMode::Edit { + return Ok(()); // Silently ignore in non-edit modes + } + + if self.ui_state.cursor_pos == 0 { + return Ok(()); // Nothing to delete + } + + let field_index = self.ui_state.current_field; + let mut current_text = self.data_provider.field_value(field_index).to_string(); + + if self.ui_state.cursor_pos <= current_text.len() { + current_text.remove(self.ui_state.cursor_pos - 1); + self.data_provider.set_field_value(field_index, current_text); + self.ui_state.cursor_pos -= 1; + self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; + } + + Ok(()) + } + + /// Delete character under cursor (vim x / delete key) + pub fn delete_forward(&mut self) -> Result<()> { + if self.ui_state.current_mode != AppMode::Edit { + return Ok(()); // Silently ignore in non-edit modes + } + + let field_index = self.ui_state.current_field; + let mut current_text = self.data_provider.field_value(field_index).to_string(); + + if self.ui_state.cursor_pos < current_text.len() { + current_text.remove(self.ui_state.cursor_pos); + self.data_provider.set_field_value(field_index, current_text); + } + + Ok(()) + } + + /// Exit edit mode to read-only mode (vim Escape) + pub fn exit_edit_mode(&mut self) { + self.set_mode(AppMode::ReadOnly); + // Deactivate autocomplete when exiting edit mode + self.ui_state.deactivate_autocomplete(); + } + + /// Enter edit mode from read-only mode (vim i/a/o) + pub fn enter_edit_mode(&mut self) { + self.set_mode(AppMode::Edit); + } + + // =================================================================== + // HELPER METHODS + // =================================================================== + + /// Clamp cursor position to valid bounds for current field and mode + fn clamp_cursor_to_current_field(&mut self) { + let current_text = self.current_text(); + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + + use crate::canvas::actions::movement::line::safe_cursor_position; + let safe_pos = safe_cursor_position( + current_text, + self.ui_state.ideal_cursor_column, + is_edit_mode + ); + + self.ui_state.cursor_pos = safe_pos; + } } From 0baf152c3eb7f31566a9fa55da7ed487c639163e Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 2 Aug 2025 15:06:29 +0200 Subject: [PATCH 18/18] automatic cursor style handled by the library --- canvas/Cargo.toml | 3 +- canvas/examples/canvas_gui_demo.rs | 388 ---------------------------- canvas/examples/full_canvas_demo.rs | 156 ++++++++--- canvas/src/canvas/cursor.rs | 45 ++++ canvas/src/canvas/mod.rs | 7 +- canvas/src/canvas/modes/manager.rs | 37 +++ canvas/src/editor.rs | 83 ++++++ canvas/src/lib.rs | 3 + 8 files changed, 296 insertions(+), 426 deletions(-) delete mode 100644 canvas/examples/canvas_gui_demo.rs create mode 100644 canvas/src/canvas/cursor.rs diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index df1d04f..c046767 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -12,7 +12,7 @@ categories.workspace = true [dependencies] common = { path = "../common" } ratatui = { workspace = true, optional = true } -crossterm = { workspace = true } +crossterm = { workspace = true, optional = true } anyhow.workspace = true tokio = { workspace = true, optional = true } toml = { workspace = true } @@ -31,6 +31,7 @@ tokio-test = "0.4.4" default = [] gui = ["ratatui"] autocomplete = ["tokio"] +cursor-style = ["crossterm"] [[example]] name = "autocomplete" diff --git a/canvas/examples/canvas_gui_demo.rs b/canvas/examples/canvas_gui_demo.rs deleted file mode 100644 index 8010043..0000000 --- a/canvas/examples/canvas_gui_demo.rs +++ /dev/null @@ -1,388 +0,0 @@ -// examples/canvas_gui_demo.rs - -use std::io; -use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout}, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, - Frame, Terminal, -}; - -use canvas::{ - canvas::{ - gui::render_canvas, - modes::{AppMode, HighlightState, ModeManager}, - state::{ActionContext, CanvasState}, - theme::CanvasTheme, - }, - CanvasAction, execute, -}; - -// Simple theme implementation -#[derive(Clone)] -struct DemoTheme; - -impl CanvasTheme for DemoTheme { - fn bg(&self) -> Color { Color::Reset } - fn fg(&self) -> Color { Color::White } - fn accent(&self) -> Color { Color::Cyan } - fn secondary(&self) -> Color { Color::Gray } - fn highlight(&self) -> Color { Color::Yellow } - fn highlight_bg(&self) -> Color { Color::DarkGray } - fn warning(&self) -> Color { Color::Red } - fn border(&self) -> Color { Color::Gray } -} - -// Demo form state -struct DemoFormState { - fields: Vec, - field_names: Vec, - current_field: usize, - cursor_pos: usize, - mode: AppMode, - highlight_state: HighlightState, - has_changes: bool, - debug_message: String, -} - -impl DemoFormState { - fn new() -> Self { - Self { - fields: vec![ - "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(), - "Email".to_string(), - "Phone".to_string(), - "Address".to_string(), - "City".to_string(), - "Comments".to_string(), - ], - current_field: 0, - cursor_pos: 0, - mode: AppMode::ReadOnly, - highlight_state: HighlightState::Off, - has_changes: false, - debug_message: "Ready - Use hjkl to move, w for next word, i to edit".to_string(), - } - } - - fn enter_edit_mode(&mut self) { - if ModeManager::can_enter_edit_mode(self.mode) { - self.mode = AppMode::Edit; - self.debug_message = "Entered EDIT mode".to_string(); - } - } - - fn enter_readonly_mode(&mut self) { - if ModeManager::can_enter_read_only_mode(self.mode) { - self.mode = AppMode::ReadOnly; - self.highlight_state = HighlightState::Off; - self.debug_message = "Entered READ-ONLY mode".to_string(); - } - } - - fn enter_highlight_mode(&mut self) { - if ModeManager::can_enter_highlight_mode(self.mode) { - self.mode = AppMode::Highlight; - self.highlight_state = HighlightState::Characterwise { - anchor: (self.current_field, self.cursor_pos), - }; - self.debug_message = "Entered VISUAL mode".to_string(); - } - } -} - -impl CanvasState for DemoFormState { - 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.min(self.fields.len().saturating_sub(1)); - self.cursor_pos = self.fields[self.current_field].len(); - } - - fn set_current_cursor_pos(&mut self, pos: usize) { - let max_pos = self.fields[self.current_field].len(); - self.cursor_pos = pos.min(max_pos); - } - - fn current_mode(&self) -> AppMode { - self.mode - } - - fn get_current_input(&self) -> &str { - &self.fields[self.current_field] - } - - fn get_current_input_mut(&mut self) -> &mut String { - &mut self.fields[self.current_field] - } - - fn inputs(&self) -> Vec<&String> { - self.fields.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; - } - - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - match action { - CanvasAction::Custom(cmd) => { - match cmd.as_str() { - "enter_edit_mode" => { - self.enter_edit_mode(); - Some("Entered edit mode".to_string()) - } - "enter_readonly_mode" => { - self.enter_readonly_mode(); - Some("Entered read-only mode".to_string()) - } - "enter_highlight_mode" => { - self.enter_highlight_mode(); - Some("Entered highlight mode".to_string()) - } - _ => None, - } - } - _ => None, - } - } -} - -/// Simple key mapping - users have full control! -async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut DemoFormState) -> bool { - let is_edit_mode = state.mode == AppMode::Edit; - - // Handle quit first - if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) || - (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) || - key == KeyCode::F(10) { - return false; // Signal to quit - } - - // Users directly map keys to actions - no configuration needed! - let action = match (state.mode, key, modifiers) { - // === READ-ONLY MODE KEYS === - (AppMode::ReadOnly, KeyCode::Char('h'), _) => Some(CanvasAction::MoveLeft), - (AppMode::ReadOnly, KeyCode::Char('j'), _) => Some(CanvasAction::MoveDown), - (AppMode::ReadOnly, KeyCode::Char('k'), _) => Some(CanvasAction::MoveUp), - (AppMode::ReadOnly, KeyCode::Char('l'), _) => Some(CanvasAction::MoveRight), - (AppMode::ReadOnly, KeyCode::Char('w'), _) => Some(CanvasAction::MoveWordNext), - (AppMode::ReadOnly, KeyCode::Char('b'), _) => Some(CanvasAction::MoveWordPrev), - (AppMode::ReadOnly, KeyCode::Char('e'), _) => Some(CanvasAction::MoveWordEnd), - (AppMode::ReadOnly, KeyCode::Char('0'), _) => Some(CanvasAction::MoveLineStart), - (AppMode::ReadOnly, KeyCode::Char('$'), _) => Some(CanvasAction::MoveLineEnd), - (AppMode::ReadOnly, KeyCode::Tab, _) => Some(CanvasAction::NextField), - (AppMode::ReadOnly, KeyCode::BackTab, _) => Some(CanvasAction::PrevField), - - // === EDIT MODE KEYS === - (AppMode::Edit, KeyCode::Left, _) => Some(CanvasAction::MoveLeft), - (AppMode::Edit, KeyCode::Right, _) => Some(CanvasAction::MoveRight), - (AppMode::Edit, KeyCode::Up, _) => Some(CanvasAction::MoveUp), - (AppMode::Edit, KeyCode::Down, _) => Some(CanvasAction::MoveDown), - (AppMode::Edit, KeyCode::Home, _) => Some(CanvasAction::MoveLineStart), - (AppMode::Edit, KeyCode::End, _) => Some(CanvasAction::MoveLineEnd), - (AppMode::Edit, KeyCode::Backspace, _) => Some(CanvasAction::DeleteBackward), - (AppMode::Edit, KeyCode::Delete, _) => Some(CanvasAction::DeleteForward), - (AppMode::Edit, KeyCode::Tab, _) => Some(CanvasAction::NextField), - (AppMode::Edit, KeyCode::BackTab, _) => Some(CanvasAction::PrevField), - - // Vim-style movement in edit mode (optional) - (AppMode::Edit, KeyCode::Char('h'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveLeft), - (AppMode::Edit, KeyCode::Char('l'), m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveRight), - - // Word movement with Ctrl in edit mode - (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordPrev), - (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => Some(CanvasAction::MoveWordNext), - - // === MODE TRANSITIONS === - (AppMode::ReadOnly, KeyCode::Char('i'), _) => Some(CanvasAction::Custom("enter_edit_mode".to_string())), - (AppMode::ReadOnly, KeyCode::Char('a'), _) => { - // 'a' moves to end of line then enters edit mode - if let Ok(_) = execute(CanvasAction::MoveLineEnd, state).await { - Some(CanvasAction::Custom("enter_edit_mode".to_string())) - } else { - None - } - }, - (AppMode::ReadOnly, KeyCode::Char('v'), _) => Some(CanvasAction::Custom("enter_highlight_mode".to_string())), - (_, KeyCode::Esc, _) => Some(CanvasAction::Custom("enter_readonly_mode".to_string())), - - // === CHARACTER INPUT IN EDIT MODE === - (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) && !m.contains(KeyModifiers::ALT) => { - Some(CanvasAction::InsertChar(c)) - }, - - // === ARROW KEYS IN READ-ONLY MODE === - (AppMode::ReadOnly, KeyCode::Left, _) => Some(CanvasAction::MoveLeft), - (AppMode::ReadOnly, KeyCode::Right, _) => Some(CanvasAction::MoveRight), - (AppMode::ReadOnly, KeyCode::Up, _) => Some(CanvasAction::MoveUp), - (AppMode::ReadOnly, KeyCode::Down, _) => Some(CanvasAction::MoveDown), - - _ => None, - }; - - // Execute the action if we found one - if let Some(action) = action { - match execute(action.clone(), state).await { - Ok(result) => { - if result.is_success() { - // Mark as changed for editing actions - if is_edit_mode { - match action { - CanvasAction::InsertChar(_) | CanvasAction::DeleteBackward | CanvasAction::DeleteForward => { - state.set_has_unsaved_changes(true); - } - _ => {} - } - } - - if let Some(msg) = result.message() { - state.debug_message = msg.to_string(); - } else { - state.debug_message = format!("Executed: {}", action.description()); - } - } else if let Some(msg) = result.message() { - state.debug_message = format!("Error: {}", msg); - } - } - Err(e) => { - state.debug_message = format!("Error executing action: {}", e); - } - } - } else { - state.debug_message = format!("Unhandled key: {:?} (mode: {:?})", key, state.mode); - } - - true // Continue running -} - -async fn run_app(terminal: &mut Terminal, mut state: DemoFormState) -> io::Result<()> { - let theme = DemoTheme; - - loop { - terminal.draw(|f| ui(f, &state, &theme))?; - - if let Event::Key(key) = event::read()? { - let should_continue = handle_key_press(key.code, key.modifiers, &mut state).await; - if !should_continue { - break; - } - } - } - - Ok(()) -} - -fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Min(8), - Constraint::Length(4), - ]) - .split(f.area()); - - // Render the canvas form - render_canvas( - f, - chunks[0], - state, - theme, - state.mode == AppMode::Edit, - &state.highlight_state, - ); - - // Render status bar - let mode_text = match state.mode { - AppMode::Edit => "EDIT", - AppMode::ReadOnly => "NORMAL", - AppMode::Highlight => "VISUAL", - AppMode::General => "GENERAL", - AppMode::Command => "COMMAND", - }; - - let status_text = if state.has_changes { - format!("-- {} -- [Modified]", mode_text) - } else { - format!("-- {} --", mode_text) - }; - - let position_text = format!("Field: {}/{} | Cursor: {} | Actions: {}", - state.current_field + 1, - state.fields.len(), - state.cursor_pos, - CanvasAction::movement_actions().len() + CanvasAction::editing_actions().len()); - - let help_text = match state.mode { - AppMode::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | i/a: Edit | v: Visual | F10: Quit", - AppMode::Edit => "Type to edit | Arrows/Ctrl+arrows: Move | Tab: 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", - }; - - let status = Paragraph::new(vec![ - Line::from(Span::styled(status_text, Style::default().fg(theme.accent()))), - Line::from(Span::styled(position_text, Style::default().fg(theme.fg()))), - 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")); - - f.render_widget(status, chunks[1]); -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let state = DemoFormState::new(); - - let res = run_app(&mut terminal, state).await; - - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{:?}", err); - } - - Ok(()) -} diff --git a/canvas/examples/full_canvas_demo.rs b/canvas/examples/full_canvas_demo.rs index b65c0b4..5487e55 100644 --- a/canvas/examples/full_canvas_demo.rs +++ b/canvas/examples/full_canvas_demo.rs @@ -3,6 +3,7 @@ use std::io; use crossterm::{ + cursor::SetCursorStyle, event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, }, @@ -28,12 +29,26 @@ use canvas::{ DataProvider, FormEditor, }; +/// Update cursor style based on current AppMode +fn update_cursor_for_mode(mode: AppMode) -> io::Result<()> { + let style = match mode { + AppMode::Edit => SetCursorStyle::SteadyBar, // Thin line for insert mode + AppMode::ReadOnly => SetCursorStyle::SteadyBlock, // Block for normal mode + AppMode::Highlight => SetCursorStyle::BlinkingBlock, // Blinking block for visual mode + AppMode::General => SetCursorStyle::SteadyBlock, // Block for general mode + AppMode::Command => SetCursorStyle::SteadyUnderScore, // Underscore for command mode + }; + + execute!(io::stdout(), style) +} + // Enhanced FormEditor that adds visual mode and status tracking struct EnhancedFormEditor { editor: FormEditor, highlight_state: HighlightState, has_unsaved_changes: bool, debug_message: String, + command_buffer: String, // For multi-key vim commands like "gg" } impl EnhancedFormEditor { @@ -43,11 +58,30 @@ impl EnhancedFormEditor { highlight_state: HighlightState::Off, has_unsaved_changes: false, debug_message: "Full Canvas Demo - All features enabled".to_string(), + command_buffer: String::new(), } } + // === COMMAND BUFFER HANDLING === + + fn clear_command_buffer(&mut self) { + self.command_buffer.clear(); + } + + fn add_to_command_buffer(&mut self, ch: char) { + self.command_buffer.push(ch); + } + + fn get_command_buffer(&self) -> &str { + &self.command_buffer + } + + fn has_pending_command(&self) -> bool { + !self.command_buffer.is_empty() + } + // === VISUAL/HIGHLIGHT MODE SUPPORT === - + fn enter_visual_mode(&mut self) { if ModeManager::can_enter_highlight_mode(self.editor.mode()) { self.editor.set_mode(AppMode::Highlight); @@ -100,7 +134,7 @@ impl EnhancedFormEditor { } // === ENHANCED MOVEMENT WITH VISUAL UPDATES === - + fn move_left(&mut self) { self.editor.move_left(); self.update_visual_selection(); @@ -172,7 +206,7 @@ impl EnhancedFormEditor { } // === DELETE OPERATIONS === - + fn delete_backward(&mut self) -> anyhow::Result<()> { let result = self.editor.delete_backward(); if result.is_ok() { @@ -192,7 +226,7 @@ impl EnhancedFormEditor { } // === MODE TRANSITIONS === - + fn enter_edit_mode(&mut self) { self.editor.enter_edit_mode(); self.debug_message = "-- INSERT --".to_string(); @@ -217,23 +251,23 @@ impl EnhancedFormEditor { fn current_field(&self) -> usize { self.editor.current_field() } - + fn cursor_position(&self) -> usize { self.editor.cursor_position() } - + fn mode(&self) -> AppMode { self.editor.mode() } - + fn current_text(&self) -> &str { self.editor.current_text() } - + fn data_provider(&self) -> &D { self.editor.data_provider() } - + fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } @@ -246,7 +280,7 @@ impl EnhancedFormEditor { } // === STATUS AND DEBUG === - + fn set_debug_message(&mut self, msg: String) { self.debug_message = msg; } @@ -298,23 +332,23 @@ impl DataProvider for FullDemoData { fn field_count(&self) -> usize { self.fields.len() } - + fn field_name(&self, index: usize) -> &str { &self.fields[index].0 } - + fn field_value(&self, index: usize) -> &str { &self.fields[index].1 } - + fn set_field_value(&mut self, index: usize, value: String) { self.fields[index].1 = value; } - + fn supports_autocomplete(&self, _field_index: usize) -> bool { false } - + fn display_value(&self, _index: usize) -> Option<&str> { None } @@ -326,7 +360,7 @@ fn handle_key_press( modifiers: KeyModifiers, editor: &mut EnhancedFormEditor, ) -> anyhow::Result { - let mode = editor.mode(); + let old_mode = editor.mode(); // Store mode before processing // Quit handling if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) @@ -336,34 +370,41 @@ fn handle_key_press( return Ok(false); } - match (mode, key, modifiers) { + match (old_mode, key, modifiers) { // === MODE TRANSITIONS === (AppMode::ReadOnly, KeyCode::Char('i'), _) => { editor.enter_edit_mode(); + editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('a'), _) => { editor.move_right(); // Move after current character editor.enter_edit_mode(); editor.set_debug_message("-- INSERT -- (append)".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('A'), _) => { editor.move_line_end(); editor.enter_edit_mode(); editor.set_debug_message("-- INSERT -- (end of line)".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('o'), _) => { editor.move_line_end(); editor.enter_edit_mode(); editor.set_debug_message("-- INSERT -- (open line)".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('v'), _) => { editor.enter_visual_mode(); + editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('V'), _) => { editor.enter_visual_line_mode(); + editor.clear_command_buffer(); } (_, KeyCode::Esc, _) => { editor.exit_edit_mode(); + editor.clear_command_buffer(); } // === MOVEMENT: VIM-STYLE NAVIGATION === @@ -373,39 +414,47 @@ fn handle_key_press( | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => { editor.move_left(); editor.set_debug_message("← left".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => { editor.move_right(); editor.set_debug_message("→ right".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => { editor.move_down(); editor.set_debug_message("↓ next field".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => { editor.move_up(); editor.set_debug_message("↑ previous field".to_string()); + editor.clear_command_buffer(); } // Word movement - Full vim word navigation (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => { editor.move_word_next(); editor.set_debug_message("w: next word start".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => { editor.move_word_prev(); editor.set_debug_message("b: previous word start".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => { editor.move_word_end(); editor.set_debug_message("e: word end".to_string()); + editor.clear_command_buffer(); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('W'), _) => { editor.move_word_end_prev(); editor.set_debug_message("W: previous word end".to_string()); + editor.clear_command_buffer(); } // Line movement @@ -422,15 +471,33 @@ fn handle_key_press( // Field/document movement (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => { - editor.move_first_line(); - editor.set_debug_message("gg: first field".to_string()); + if editor.get_command_buffer() == "g" { + // Second 'g' - execute "gg" command + editor.move_first_line(); + editor.set_debug_message("gg: first field".to_string()); + editor.clear_command_buffer(); + } else { + // First 'g' - start command buffer + editor.clear_command_buffer(); + editor.add_to_command_buffer('g'); + editor.set_debug_message("g".to_string()); + } } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => { editor.move_last_line(); editor.set_debug_message("G: last field".to_string()); + editor.clear_command_buffer(); } // === EDIT MODE MOVEMENT === + (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_prev(); + editor.set_debug_message("Ctrl+← word back".to_string()); + } + (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_next(); + editor.set_debug_message("Ctrl+→ word forward".to_string()); + } (AppMode::Edit, KeyCode::Left, _) => { editor.move_left(); } @@ -450,16 +517,6 @@ fn handle_key_press( editor.move_line_end(); } - // Word movement in edit mode with Ctrl - (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => { - editor.move_word_prev(); - editor.set_debug_message("Ctrl+← word back".to_string()); - } - (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => { - editor.move_word_next(); - editor.set_debug_message("Ctrl+→ word forward".to_string()); - } - // === DELETE OPERATIONS === (AppMode::Edit, KeyCode::Backspace, _) => { editor.delete_backward()?; @@ -496,7 +553,7 @@ fn handle_key_press( // === DEBUG/INFO COMMANDS === (AppMode::ReadOnly, KeyCode::Char('?'), _) => { editor.set_debug_message(format!( - "Field {}/{}, Pos {}, Mode: {:?}", + "Field {}/{}, Pos {}, Mode: {:?}", editor.current_field() + 1, editor.data_provider().field_count(), editor.cursor_position(), @@ -505,13 +562,25 @@ fn handle_key_press( } _ => { - editor.set_debug_message(format!( - "Unhandled: {:?} + {:?} in {:?} mode", - key, modifiers, mode - )); + // If we have a pending command and this key doesn't complete it, clear the buffer + if editor.has_pending_command() { + editor.clear_command_buffer(); + editor.set_debug_message("Invalid command sequence".to_string()); + } else { + editor.set_debug_message(format!( + "Unhandled: {:?} + {:?} in {:?} mode", + key, modifiers, old_mode + )); + } } } + // Update cursor if mode changed + let new_mode = editor.mode(); + if old_mode != new_mode { + update_cursor_for_mode(new_mode)?; + } + Ok(true) } @@ -579,7 +648,9 @@ fn render_status_and_help( _ => "NORMAL", }; - let status_text = if editor.has_unsaved_changes() { + let status_text = if editor.has_pending_command() { + format!("-- {} -- {} [{}]", mode_text, editor.debug_message(), editor.get_command_buffer()) + } else if editor.has_unsaved_changes() { format!("-- {} -- [Modified] {}", mode_text, editor.debug_message()) } else { format!("-- {} -- {}", mode_text, editor.debug_message()) @@ -593,7 +664,14 @@ fn render_status_and_help( // Help text let help_text = match editor.mode() { AppMode::ReadOnly => { - "Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last, i/a/A=insert, v/V=visual, x/X=delete, ?=info" + if editor.has_pending_command() { + match editor.get_command_buffer() { + "g" => "Press 'g' again for first field, or any other key to cancel", + _ => "Pending command... (Esc to cancel)" + } + } else { + "Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last, i/a/A=insert, v/V=visual, x/X=delete, ?=info" + } } AppMode::Edit => { "Insert: arrows=move, Ctrl+arrows=words, Backspace/Del=delete, Esc=normal, Tab/Shift+Tab=fields" @@ -621,9 +699,15 @@ fn main() -> Result<(), Box> { let data = FullDemoData::new(); let mut editor = EnhancedFormEditor::new(data); editor.set_mode(AppMode::ReadOnly); // Start in normal mode + + // Set initial cursor style + update_cursor_for_mode(editor.mode())?; let res = run_app(&mut terminal, editor); + // Reset cursor style on exit + execute!(io::stdout(), SetCursorStyle::DefaultUserShape)?; + disable_raw_mode()?; execute!( terminal.backend_mut(), diff --git a/canvas/src/canvas/cursor.rs b/canvas/src/canvas/cursor.rs new file mode 100644 index 0000000..720ca6a --- /dev/null +++ b/canvas/src/canvas/cursor.rs @@ -0,0 +1,45 @@ +// src/canvas/cursor.rs +//! Cursor style management for different canvas modes + +#[cfg(feature = "cursor-style")] +use crossterm::{cursor::SetCursorStyle, execute}; +#[cfg(feature = "cursor-style")] +use std::io; + +use crate::canvas::modes::AppMode; + +/// Manages cursor styles based on canvas modes +pub struct CursorManager; + +impl CursorManager { + /// Update cursor style based on current mode + #[cfg(feature = "cursor-style")] + pub fn update_for_mode(mode: AppMode) -> io::Result<()> { + let style = match mode { + AppMode::Edit => SetCursorStyle::SteadyBar, // Thin line for insert + AppMode::ReadOnly => SetCursorStyle::SteadyBlock, // Block for normal + AppMode::Highlight => SetCursorStyle::BlinkingBlock, // Blinking for visual + AppMode::General => SetCursorStyle::SteadyBlock, // Block for general + AppMode::Command => SetCursorStyle::SteadyUnderScore, // Underscore for command + }; + + execute!(io::stdout(), style) + } + + /// No-op when cursor-style feature is disabled + #[cfg(not(feature = "cursor-style"))] + pub fn update_for_mode(_mode: AppMode) -> io::Result<()> { + Ok(()) + } + + /// Reset cursor to default on cleanup + #[cfg(feature = "cursor-style")] + pub fn reset() -> io::Result<()> { + execute!(io::stdout(), SetCursorStyle::DefaultUserShape) + } + + #[cfg(not(feature = "cursor-style"))] + pub fn reset() -> io::Result<()> { + Ok(()) + } +} diff --git a/canvas/src/canvas/mod.rs b/canvas/src/canvas/mod.rs index 85a3e51..7485443 100644 --- a/canvas/src/canvas/mod.rs +++ b/canvas/src/canvas/mod.rs @@ -6,9 +6,14 @@ pub mod modes; #[cfg(feature = "gui")] pub mod gui; - #[cfg(feature = "gui")] pub mod theme; +#[cfg(feature = "cursor-style")] +pub mod cursor; + // Keep these exports for current functionality pub use modes::{AppMode, ModeManager, HighlightState}; + +#[cfg(feature = "cursor-style")] +pub use cursor::CursorManager; diff --git a/canvas/src/canvas/modes/manager.rs b/canvas/src/canvas/modes/manager.rs index dcdf7b9..4b0556b 100644 --- a/canvas/src/canvas/modes/manager.rs +++ b/canvas/src/canvas/modes/manager.rs @@ -1,6 +1,8 @@ // src/modes/handlers/mode_manager.rs // canvas/src/modes/manager.rs +#[cfg(feature = "cursor-style")] +use crate::canvas::CursorManager; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppMode { @@ -30,4 +32,39 @@ impl ModeManager { pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool { matches!(current_mode, AppMode::ReadOnly) } + + + /// Transition to new mode with automatic cursor update (when cursor-style feature enabled) + pub fn transition_to_mode(current_mode: AppMode, new_mode: AppMode) -> std::io::Result { + if current_mode != new_mode { + #[cfg(feature = "cursor-style")] + { + let _ = CursorManager::update_for_mode(new_mode); + } + } + Ok(new_mode) + } + + /// Enter highlight mode with cursor styling + pub fn enter_highlight_mode_with_cursor(current_mode: AppMode) -> std::io::Result { + if Self::can_enter_highlight_mode(current_mode) { + #[cfg(feature = "cursor-style")] + { + let _ = CursorManager::update_for_mode(AppMode::Highlight); + } + Ok(true) + } else { + Ok(false) + } + } + + /// Exit highlight mode with cursor styling + pub fn exit_highlight_mode_with_cursor() -> std::io::Result { + let new_mode = AppMode::ReadOnly; + #[cfg(feature = "cursor-style")] + { + let _ = CursorManager::update_for_mode(new_mode); + } + Ok(new_mode) + } } diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index c84c2e3..47165a1 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -1,6 +1,9 @@ // src/editor.rs //! Main API for the canvas library - FormEditor with library-owned state +#[cfg(feature = "cursor-style")] +use crate::canvas::CursorManager; + use anyhow::Result; use crate::canvas::state::EditorState; use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; @@ -145,12 +148,21 @@ impl FormEditor { /// Change mode (for vim compatibility) pub fn set_mode(&mut self, mode: AppMode) { + #[cfg(feature = "cursor-style")] + let old_mode = self.ui_state.current_mode; + self.ui_state.current_mode = mode; // Clear autocomplete when changing modes if mode != AppMode::Edit { self.ui_state.deactivate_autocomplete(); } + + // Update cursor style if mode changed and cursor-style feature is enabled + #[cfg(feature = "cursor-style")] + if old_mode != mode { + let _ = crate::canvas::CursorManager::update_for_mode(mode); + } } // =================================================================== @@ -457,4 +469,75 @@ impl FormEditor { self.ui_state.cursor_pos = safe_pos; } + + + /// Set the value of the current field + pub fn set_current_field_value(&mut self, value: String) { + let field_index = self.ui_state.current_field; + self.data_provider.set_field_value(field_index, value); + // Reset cursor to start of field + self.ui_state.cursor_pos = 0; + self.ui_state.ideal_cursor_column = 0; + } + + /// Set the value of a specific field by index + pub fn set_field_value(&mut self, field_index: usize, value: String) { + if field_index < self.data_provider.field_count() { + self.data_provider.set_field_value(field_index, value); + // If we're modifying the current field, reset cursor + if field_index == self.ui_state.current_field { + self.ui_state.cursor_pos = 0; + self.ui_state.ideal_cursor_column = 0; + } + } + } + + /// Clear the current field (set to empty string) + pub fn clear_current_field(&mut self) { + self.set_current_field_value(String::new()); + } + + /// Get mutable access to data provider (for advanced operations) + pub fn data_provider_mut(&mut self) -> &mut D { + &mut self.data_provider + } + + /// Set cursor to exact position (for vim-style movements like f, F, t, T) + pub fn set_cursor_position(&mut self, position: usize) { + let current_text = self.current_text(); + let is_edit_mode = self.ui_state.current_mode == AppMode::Edit; + + // Clamp to valid bounds for current mode + let max_pos = if is_edit_mode { + current_text.len() // Edit mode: can go past end + } else { + current_text.len().saturating_sub(1).max(0) // Read-only: stay within text + }; + + let clamped_pos = position.min(max_pos); + + // Update cursor position directly + self.ui_state.cursor_pos = clamped_pos; + self.ui_state.ideal_cursor_column = clamped_pos; + } + + /// Cleanup cursor style (call this when shutting down) + pub fn cleanup_cursor(&self) -> std::io::Result<()> { + #[cfg(feature = "cursor-style")] + { + crate::canvas::CursorManager::reset() + } + #[cfg(not(feature = "cursor-style"))] + { + Ok(()) + } + } +} + +// Add Drop implementation for automatic cleanup +impl Drop for FormEditor { + fn drop(&mut self) { + // Reset cursor to default when FormEditor is dropped + let _ = self.cleanup_cursor(); + } } diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index a5c2696..badc944 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -8,6 +8,9 @@ pub mod data_provider; #[cfg(feature = "autocomplete")] pub mod autocomplete; +#[cfg(feature = "cursor-style")] +pub use canvas::CursorManager; + // =================================================================== // NEW API: Library-owned state pattern // ===================================================================