diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 390b07c..71a219e 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -24,19 +24,3 @@ tokio-test = "0.4.4" [features] default = [] gui = ["ratatui"] - -[[example]] -name = "simple_login" -path = "examples/simple_login.rs" - -[[example]] -name = "config_screen" -path = "examples/config_screen.rs" - -[[example]] -name = "basic_usage" -path = "examples/basic_usage.rs" - -[[example]] -name = "integration_patterns" -path = "examples/integration_patterns.rs" diff --git a/canvas/README.md b/canvas/README.md index 0b532c9..23af107 100644 --- a/canvas/README.md +++ b/canvas/README.md @@ -100,35 +100,6 @@ async fn main() -> Result<(), Box> { } ``` -## 📚 Examples - -The `examples/` directory contains comprehensive examples showing different usage patterns: - -### Run the Examples - -```bash -# Basic login form with TUI -cargo run --example simple_login - -# Advanced configuration screen with suggestions and validation -cargo run --example config_screen - -# API usage patterns and quick start guide -cargo run --example basic_usage - -# Advanced integration patterns (state machines, events, validation) -cargo run --example integration_patterns -``` - -### Example Overview - -| Example | Description | Key Features | -|---------|-------------|--------------| -| `simple_login` | Interactive login form TUI | Basic form, custom actions, password masking | -| `config_screen` | Configuration editor | Auto-suggestions, field validation, complex UI | -| `basic_usage` | API demonstration | All core patterns, non-interactive | -| `integration_patterns` | Architecture patterns | State machines, events, validation pipelines | - ## 🎯 Type-Safe Actions The Canvas system uses strongly-typed actions instead of error-prone strings: diff --git a/canvas/examples/basic_usage.rs b/canvas/examples/basic_usage.rs deleted file mode 100644 index b2604f5..0000000 --- a/canvas/examples/basic_usage.rs +++ /dev/null @@ -1,378 +0,0 @@ -// examples/basic_usage.rs -//! Basic usage patterns and quick start guide -//! -//! This example demonstrates the core patterns for using the canvas crate: -//! 1. Implementing CanvasState -//! 2. Using the ActionDispatcher -//! 3. Handling different types of actions -//! 4. Working with suggestions -//! -//! Run with: cargo run --example basic_usage - -use canvas::prelude::*; - -#[tokio::main] -async fn main() { - println!("🎨 Canvas Crate - Basic Usage Patterns"); - println!("=====================================\n"); - - // Example 1: Minimal form implementation - example_1_minimal_form(); - - // Example 2: Form with suggestions - example_2_with_suggestions(); - - // Example 3: Custom actions - example_3_custom_actions().await; - - // Example 4: Batch operations - example_4_batch_operations().await; -} - -// Example 1: Minimal form - just the required methods -fn example_1_minimal_form() { - println!("📝 Example 1: Minimal Form Implementation"); - - #[derive(Debug)] - struct SimpleForm { - current_field: usize, - cursor_pos: usize, - name: String, - email: String, - has_changes: bool, - } - - impl SimpleForm { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - name: String::new(), - email: String::new(), - has_changes: false, - } - } - } - - impl CanvasState for SimpleForm { - 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(1); } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { - match self.current_field { - 0 => &self.name, - 1 => &self.email, - _ => "", - } - } - - fn get_current_input_mut(&mut self) -> &mut String { - match self.current_field { - 0 => &mut self.name, - 1 => &mut self.email, - _ => unreachable!(), - } - } - - fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.email] } - fn fields(&self) -> Vec<&str> { vec!["Name", "Email"] } - fn has_unsaved_changes(&self) -> bool { self.has_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; } - } - - let form = SimpleForm::new(); - println!(" Created form with {} fields", form.fields().len()); - println!(" Current field: {}", form.fields()[form.current_field()]); - println!(" ✅ Minimal implementation works!\n"); -} - -// Example 2: Form with suggestion support -fn example_2_with_suggestions() { - println!("💡 Example 2: Form with Suggestions"); - - #[derive(Debug)] - struct FormWithSuggestions { - current_field: usize, - cursor_pos: usize, - country: String, - has_changes: bool, - suggestions: SuggestionState, - } - - impl FormWithSuggestions { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - country: String::new(), - has_changes: false, - suggestions: SuggestionState::default(), - } - } - } - - impl CanvasState for FormWithSuggestions { - fn current_field(&self) -> usize { self.current_field } - fn current_cursor_pos(&self) -> usize { self.cursor_pos } - fn set_current_field(&mut self, index: usize) { self.current_field = index; } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { &self.country } - fn get_current_input_mut(&mut self) -> &mut String { &mut self.country } - fn inputs(&self) -> Vec<&String> { vec![&self.country] } - fn fields(&self) -> Vec<&str> { vec!["Country"] } - fn has_unsaved_changes(&self) -> bool { self.has_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; } - - // Suggestion support - fn get_suggestions(&self) -> Option<&[String]> { - if self.suggestions.is_active { - Some(&self.suggestions.suggestions) - } else { - None - } - } - - fn get_selected_suggestion_index(&self) -> Option { - self.suggestions.selected_index - } - - fn set_selected_suggestion_index(&mut self, index: Option) { - self.suggestions.selected_index = index; - } - - fn activate_suggestions(&mut self, suggestions: Vec) { - self.suggestions.activate_with_suggestions(suggestions); - } - - fn deactivate_suggestions(&mut self) { - self.suggestions.deactivate(); - } - - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - match action { - CanvasAction::SelectSuggestion => { - // Fix: Clone the suggestion first to avoid borrow checker issues - if let Some(suggestion) = self.suggestions.get_selected().cloned() { - self.country = suggestion.clone(); - self.cursor_pos = suggestion.len(); - self.deactivate_suggestions(); - self.has_changes = true; - return Some(format!("Selected: {}", suggestion)); - } - None - } - _ => None, - } - } - } - - let mut form = FormWithSuggestions::new(); - - // Simulate user typing and triggering suggestions - form.activate_suggestions(vec![ - "United States".to_string(), - "United Kingdom".to_string(), - "Ukraine".to_string(), - ]); - - println!(" Activated suggestions: {:?}", form.get_suggestions().unwrap()); - println!(" Current selection: {:?}", form.get_selected_suggestion_index()); - - // Navigate suggestions - form.set_selected_suggestion_index(Some(1)); - println!(" Navigated to: {}", form.suggestions.get_selected().unwrap()); - println!(" ✅ Suggestions work!\n"); -} - -// Example 3: Custom actions -async fn example_3_custom_actions() { - println!("⚡ Example 3: Custom Actions"); - - #[derive(Debug)] - struct FormWithCustomActions { - current_field: usize, - cursor_pos: usize, - text: String, - has_changes: bool, - } - - impl FormWithCustomActions { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - text: "hello world".to_string(), - has_changes: false, - } - } - } - - impl CanvasState for FormWithCustomActions { - fn current_field(&self) -> usize { self.current_field } - fn current_cursor_pos(&self) -> usize { self.cursor_pos } - fn set_current_field(&mut self, index: usize) { self.current_field = index; } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { &self.text } - fn get_current_input_mut(&mut self) -> &mut String { &mut self.text } - fn inputs(&self) -> Vec<&String> { vec![&self.text] } - fn fields(&self) -> Vec<&str> { vec!["Text"] } - 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() { - "uppercase" => { - self.text = self.text.to_uppercase(); - self.has_changes = true; - Some("Converted to uppercase".to_string()) - } - "reverse" => { - self.text = self.text.chars().rev().collect(); - self.has_changes = true; - Some("Reversed text".to_string()) - } - "word_count" => { - let count = self.text.split_whitespace().count(); - Some(format!("Word count: {}", count)) - } - _ => None, - }, - _ => None, - } - } - } - - let mut form = FormWithCustomActions::new(); - let mut ideal_cursor = 0; - - println!(" Initial text: '{}'", form.text); - - // Execute custom actions - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("uppercase".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - println!(" After uppercase: '{}' - {}", form.text, result.message().unwrap()); - - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("reverse".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - println!(" After reverse: '{}' - {}", form.text, result.message().unwrap()); - - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("word_count".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - println!(" {}", result.message().unwrap()); - println!(" ✅ Custom actions work!\n"); -} - -// Example 4: Batch operations -async fn example_4_batch_operations() { - println!("📦 Example 4: Batch Operations"); - - // Reuse the simple form from example 1 - #[derive(Debug)] - struct SimpleForm { - current_field: usize, - cursor_pos: usize, - name: String, - email: String, - has_changes: bool, - } - - impl SimpleForm { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - name: String::new(), - email: String::new(), - has_changes: false, - } - } - } - - impl CanvasState for SimpleForm { - 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(1); } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { - match self.current_field { - 0 => &self.name, - 1 => &self.email, - _ => "", - } - } - - fn get_current_input_mut(&mut self) -> &mut String { - match self.current_field { - 0 => &mut self.name, - 1 => &mut self.email, - _ => unreachable!(), - } - } - - fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.email] } - fn fields(&self) -> Vec<&str> { vec!["Name", "Email"] } - fn has_unsaved_changes(&self) -> bool { self.has_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; } - } - - let mut form = SimpleForm::new(); - let mut ideal_cursor = 0; - - // Execute a sequence of actions to type "John" in the name field - let actions = vec![ - CanvasAction::InsertChar('J'), - CanvasAction::InsertChar('o'), - CanvasAction::InsertChar('h'), - CanvasAction::InsertChar('n'), - CanvasAction::NextField, // Move to email field - CanvasAction::InsertChar('j'), - CanvasAction::InsertChar('o'), - CanvasAction::InsertChar('h'), - CanvasAction::InsertChar('n'), - CanvasAction::InsertChar('@'), - CanvasAction::InsertChar('e'), - CanvasAction::InsertChar('x'), - CanvasAction::InsertChar('a'), - CanvasAction::InsertChar('m'), - CanvasAction::InsertChar('p'), - CanvasAction::InsertChar('l'), - CanvasAction::InsertChar('e'), - CanvasAction::InsertChar('.'), - CanvasAction::InsertChar('c'), - CanvasAction::InsertChar('o'), - CanvasAction::InsertChar('m'), - ]; - - println!(" Executing {} actions in batch...", actions.len()); - - let results = ActionDispatcher::dispatch_batch( - actions, - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - println!(" Completed {} actions", results.len()); - println!(" Final state:"); - println!(" Name: '{}'", form.name); - println!(" Email: '{}'", form.email); - println!(" Current field: {}", form.fields()[form.current_field()]); - println!(" Has changes: {}", form.has_changes); -} diff --git a/canvas/examples/config_screen.rs b/canvas/examples/config_screen.rs deleted file mode 100644 index df808d2..0000000 --- a/canvas/examples/config_screen.rs +++ /dev/null @@ -1,590 +0,0 @@ -// examples/config_screen.rs -//! Advanced configuration screen with suggestions and validation -//! -//! This example demonstrates: -//! - Multiple field types -//! - Auto-suggestions -//! - Field validation -//! - Custom actions -//! -//! Run with: cargo run --example config_screen - -use canvas::prelude::*; -use crossterm::{ - event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, -}; -use std::io::{self, Write}; - -#[derive(Debug)] -struct ConfigForm { - current_field: usize, - cursor_pos: usize, - - // Configuration fields - server_host: String, - server_port: String, - database_url: String, - log_level: String, - max_connections: String, - - has_changes: bool, - suggestions: SuggestionState, -} - -impl ConfigForm { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - server_host: "localhost".to_string(), - server_port: "8080".to_string(), - database_url: String::new(), - log_level: "info".to_string(), - max_connections: "100".to_string(), - has_changes: false, - suggestions: SuggestionState::default(), - } - } - - fn field_names() -> Vec<&'static str> { - vec![ - "Server Host", - "Server Port", - "Database URL", - "Log Level", - "Max Connections" - ] - } - - fn get_field_value(&self, index: usize) -> &String { - match index { - 0 => &self.server_host, - 1 => &self.server_port, - 2 => &self.database_url, - 3 => &self.log_level, - 4 => &self.max_connections, - _ => panic!("Invalid field index: {}", index), - } - } - - fn get_field_value_mut(&mut self, index: usize) -> &mut String { - match index { - 0 => &mut self.server_host, - 1 => &mut self.server_port, - 2 => &mut self.database_url, - 3 => &mut self.log_level, - 4 => &mut self.max_connections, - _ => panic!("Invalid field index: {}", index), - } - } - - fn validate_field(&self, index: usize) -> Option { - let value = self.get_field_value(index); - match index { - 0 => { // Server Host - if value.trim().is_empty() { - Some("Server host cannot be empty".to_string()) - } else { - None - } - } - 1 => { // Server Port - if let Ok(port) = value.parse::() { - if port == 0 { - Some("Port must be greater than 0".to_string()) - } else { - None - } - } else { - Some("Port must be a valid number (1-65535)".to_string()) - } - } - 2 => { // Database URL - if !value.is_empty() && !value.starts_with("postgresql://") && !value.starts_with("mysql://") && !value.starts_with("sqlite://") { - Some("Database URL should start with postgresql://, mysql://, or sqlite://".to_string()) - } else { - None - } - } - 3 => { // Log Level - let valid_levels = ["trace", "debug", "info", "warn", "error"]; - if !valid_levels.contains(&value.to_lowercase().as_str()) { - Some("Log level must be one of: trace, debug, info, warn, error".to_string()) - } else { - None - } - } - 4 => { // Max Connections - if let Ok(connections) = value.parse::() { - if connections == 0 { - Some("Max connections must be greater than 0".to_string()) - } else if connections > 10000 { - Some("Max connections seems too high (>10000)".to_string()) - } else { - None - } - } else { - Some("Max connections must be a valid number".to_string()) - } - } - _ => None, - } - } - - fn get_suggestions_for_field(&self, index: usize, current_value: &str) -> Vec { - match index { - 0 => { // Server Host - vec![ - "localhost".to_string(), - "127.0.0.1".to_string(), - "0.0.0.0".to_string(), - format!("{}.local", current_value), - ] - } - 1 => { // Server Port - vec![ - "8080".to_string(), - "3000".to_string(), - "8000".to_string(), - "80".to_string(), - "443".to_string(), - ] - } - 2 => { // Database URL - if current_value.is_empty() { - vec![ - "postgresql://localhost:5432/mydb".to_string(), - "mysql://localhost:3306/mydb".to_string(), - "sqlite://./database.db".to_string(), - ] - } else { - vec![] - } - } - 3 => { // Log Level - vec![ - "trace".to_string(), - "debug".to_string(), - "info".to_string(), - "warn".to_string(), - "error".to_string(), - ] - .into_iter() - .filter(|level| level.starts_with(¤t_value.to_lowercase())) - .collect() - } - 4 => { // Max Connections - vec![ - "10".to_string(), - "50".to_string(), - "100".to_string(), - "200".to_string(), - "500".to_string(), - ] - } - _ => vec![], - } - } -} - -impl CanvasState for ConfigForm { - 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(4); // 5 fields total (0-4) - // Deactivate suggestions when changing fields - self.deactivate_suggestions(); - } - - fn set_current_cursor_pos(&mut self, pos: usize) { - self.cursor_pos = pos; - } - - fn get_current_input(&self) -> &str { - self.get_field_value(self.current_field) - } - - fn get_current_input_mut(&mut self) -> &mut String { - self.get_field_value_mut(self.current_field) - } - - fn inputs(&self) -> Vec<&String> { - vec![ - &self.server_host, - &self.server_port, - &self.database_url, - &self.log_level, - &self.max_connections, - ] - } - - fn fields(&self) -> Vec<&str> { - Self::field_names() - } - - fn has_unsaved_changes(&self) -> bool { - self.has_changes - } - - fn set_has_unsaved_changes(&mut self, changed: bool) { - self.has_changes = changed; - } - - // Suggestion support - fn get_suggestions(&self) -> Option<&[String]> { - if self.suggestions.is_active { - Some(&self.suggestions.suggestions) - } else { - None - } - } - - fn get_selected_suggestion_index(&self) -> Option { - self.suggestions.selected_index - } - - fn set_selected_suggestion_index(&mut self, index: Option) { - self.suggestions.selected_index = index; - } - - fn activate_suggestions(&mut self, suggestions: Vec) { - self.suggestions.activate_with_suggestions(suggestions); - } - - fn deactivate_suggestions(&mut self) { - self.suggestions.deactivate(); - } - - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - match action { - CanvasAction::SelectSuggestion => { - // Fix: Clone the suggestion first to avoid borrow checker issues - if let Some(suggestion) = self.suggestions.get_selected().cloned() { - *self.get_current_input_mut() = suggestion.clone(); - self.set_current_cursor_pos(suggestion.len()); - self.deactivate_suggestions(); - self.set_has_unsaved_changes(true); - return Some("Applied suggestion".to_string()); - } - None - } - - CanvasAction::Custom(cmd) => match cmd.as_str() { - "trigger_suggestions" => { - let current_value = self.get_current_input(); - let suggestions = self.get_suggestions_for_field(self.current_field, current_value); - if !suggestions.is_empty() { - self.activate_suggestions(suggestions); - Some("Showing suggestions".to_string()) - } else { - Some("No suggestions available".to_string()) - } - } - "validate_current" => { - if let Some(error) = self.validate_field(self.current_field) { - Some(format!("Validation Error: {}", error)) - } else { - Some("Field is valid".to_string()) - } - } - "validate_all" => { - let mut errors = Vec::new(); - for i in 0..5 { - if let Some(error) = self.validate_field(i) { - errors.push(format!("{}: {}", Self::field_names()[i], error)); - } - } - if errors.is_empty() { - Some("All fields are valid!".to_string()) - } else { - Some(format!("Errors found: {}", errors.join("; "))) - } - } - "save_config" => { - // Validate all fields first - for i in 0..5 { - if self.validate_field(i).is_some() { - return Some("Cannot save: Please fix validation errors first".to_string()); - } - } - self.set_has_unsaved_changes(false); - Some("Configuration saved successfully!".to_string()) - } - _ => None, - }, - - // Auto-trigger suggestions for certain fields - CanvasAction::InsertChar(_) => { - // After character insertion, check if we should show suggestions - match self.current_field { - 3 => { // Log level - always show suggestions for autocomplete - let current_value = self.get_current_input(); - let suggestions = self.get_suggestions_for_field(self.current_field, current_value); - if !suggestions.is_empty() { - self.activate_suggestions(suggestions); - } - } - _ => {} - } - None // Let the generic handler insert the character - } - - _ => None, - } - } -} - -fn draw_ui(form: &ConfigForm, message: &str) -> io::Result<()> { - print!("\x1B[2J\x1B[1;1H"); - - println!("╔════════════════════════════════════════════════════════════════╗"); - println!("║ CONFIGURATION EDITOR ║"); - println!("╠════════════════════════════════════════════════════════════════╣"); - - let field_names = ConfigForm::field_names(); - - for (i, field_name) in field_names.iter().enumerate() { - let is_current = i == form.current_field; - let indicator = if is_current { ">" } else { " " }; - let value = form.get_field_value(i); - let display_value = if value.is_empty() { - format!("", field_name.to_lowercase()) - } else { - value.clone() - }; - - // Truncate long values for display - let display_value = if display_value.len() > 35 { - format!("{}...", &display_value[..32]) - } else { - display_value - }; - - println!("║ {} {:15}: {:35} ║", indicator, field_name, display_value); - - // Show cursor for current field - if is_current { - let cursor_pos = form.cursor_pos.min(value.len()); - let cursor_line = format!("║ {}{}║", - " ".repeat(18 + cursor_pos), - "▊" - ); - println!("{:66}", cursor_line); - } - - // Show validation error if any - if let Some(error) = form.validate_field(i) { - let error_display = if error.len() > 58 { - format!("{}...", &error[..55]) - } else { - error - }; - println!("║ ⚠️ {:58} ║", error_display); - } else if is_current { - println!("║{:64}║", ""); - } - } - - println!("╠════════════════════════════════════════════════════════════════╣"); - - // Show suggestions if active - if let Some(suggestions) = form.get_suggestions() { - println!("║ SUGGESTIONS: ║"); - for (i, suggestion) in suggestions.iter().enumerate() { - let selected = form.get_selected_suggestion_index() == Some(i); - let marker = if selected { "→" } else { " " }; - let display_suggestion = if suggestion.len() > 55 { - format!("{}...", &suggestion[..52]) - } else { - suggestion.clone() - }; - println!("║ {} {:58} ║", marker, display_suggestion); - } - println!("╠════════════════════════════════════════════════════════════════╣"); - } - - println!("║ CONTROLS: ║"); - println!("║ Tab/↑↓ - Navigate fields ║"); - println!("║ Ctrl+Space - Show suggestions ║"); - println!("║ ↑↓ - Navigate suggestions (when shown) ║"); - println!("║ Enter - Select suggestion / Validate field ║"); - println!("║ Ctrl+S - Save configuration ║"); - println!("║ Ctrl+V - Validate all fields ║"); - println!("║ Ctrl+C - Exit ║"); - println!("╠════════════════════════════════════════════════════════════════╣"); - - // Status - let status = if !message.is_empty() { - message.to_string() - } else if form.has_changes { - "Configuration modified - press Ctrl+S to save".to_string() - } else { - "Ready".to_string() - }; - - let status_display = if status.len() > 58 { - format!("{}...", &status[..55]) - } else { - status - }; - - println!("║ Status: {:55} ║", status_display); - println!("╚════════════════════════════════════════════════════════════════╝"); - - io::stdout().flush()?; - Ok(()) -} - -#[tokio::main] -async fn main() -> io::Result<()> { - enable_raw_mode()?; - io::stdout().execute(EnterAlternateScreen)?; - - let mut form = ConfigForm::new(); - let mut ideal_cursor = 0; - let mut message = String::new(); - - draw_ui(&form, &message)?; - - loop { - if let Event::Key(key) = event::read()? { - if !message.is_empty() { - message.clear(); - } - - match key { - // Exit - KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => { - break; - } - - // Show suggestions - KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::CONTROL, .. } => { - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("trigger_suggestions".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - if let Some(msg) = result.message() { - message = msg.to_string(); - } - } - - // Validate current field or select suggestion - KeyEvent { code: KeyCode::Enter, .. } => { - if form.get_suggestions().is_some() { - // Select suggestion - let result = ActionDispatcher::dispatch( - CanvasAction::SelectSuggestion, - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - if let Some(msg) = result.message() { - message = msg.to_string(); - } - } else { - // Validate current field - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("validate_current".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - if let Some(msg) = result.message() { - message = msg.to_string(); - } - } - } - - // Save configuration - KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, .. } => { - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("save_config".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - if let Some(msg) = result.message() { - message = msg.to_string(); - } - } - - // Validate all fields - KeyEvent { code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, .. } => { - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("validate_all".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - if let Some(msg) = result.message() { - message = msg.to_string(); - } - } - - // Handle up/down for suggestions - KeyEvent { code: KeyCode::Up, .. } => { - let action = if form.get_suggestions().is_some() { - CanvasAction::SuggestionUp - } else { - CanvasAction::MoveUp - }; - - let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await; - } - - KeyEvent { code: KeyCode::Down, .. } => { - let action = if form.get_suggestions().is_some() { - CanvasAction::SuggestionDown - } else { - CanvasAction::MoveDown - }; - - let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await; - } - - // Handle escape to close suggestions - KeyEvent { code: KeyCode::Esc, .. } => { - if form.get_suggestions().is_some() { - let _ = ActionDispatcher::dispatch( - CanvasAction::ExitSuggestions, - &mut form, - &mut ideal_cursor, - ).await; - } - } - - // Regular key handling - _ => { - if let Some(action) = CanvasAction::from_key(key.code) { - let result = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await.unwrap(); - - if !result.is_success() { - if let Some(msg) = result.message() { - message = format!("Error: {}", msg); - } - } - } - } - } - - draw_ui(&form, &message)?; - } - } - - disable_raw_mode()?; - io::stdout().execute(LeaveAlternateScreen)?; - println!("Configuration editor closed!"); - - Ok(()) -} diff --git a/canvas/examples/integration_patterns.rs b/canvas/examples/integration_patterns.rs deleted file mode 100644 index 95da154..0000000 --- a/canvas/examples/integration_patterns.rs +++ /dev/null @@ -1,617 +0,0 @@ -// examples/integration_patterns.rs -//! Advanced integration patterns showing how Canvas works with: -//! - State management patterns -//! - Event-driven architectures -//! - Validation systems -//! - Custom rendering -//! -//! Run with: cargo run --example integration_patterns - -use canvas::prelude::*; -use std::collections::HashMap; - -#[tokio::main] -async fn main() { - println!("🔧 Canvas Integration Patterns"); - println!("==============================\n"); - - // Pattern 1: State machine integration - state_machine_example().await; - - // Pattern 2: Event-driven architecture - event_driven_example().await; - - // Pattern 3: Validation pipeline - validation_pipeline_example().await; - - // Pattern 4: Multi-form orchestration - multi_form_example().await; -} - -// Pattern 1: Canvas with state machine -async fn state_machine_example() { - println!("🔄 Pattern 1: State Machine Integration"); - - #[derive(Debug, Clone, PartialEq)] - enum FormState { - Initial, - Editing, - Validating, - Submitting, - Success, - Error(String), - } - - #[derive(Debug)] - struct StateMachineForm { - // Canvas state - current_field: usize, - cursor_pos: usize, - username: String, - password: String, - has_changes: bool, - - // State machine - state: FormState, - } - - impl StateMachineForm { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - username: String::new(), - password: String::new(), - has_changes: false, - state: FormState::Initial, - } - } - - fn transition_to(&mut self, new_state: FormState) -> String { - let old_state = self.state.clone(); - self.state = new_state; - format!("State transition: {:?} -> {:?}", old_state, self.state) - } - - fn can_submit(&self) -> bool { - matches!(self.state, FormState::Editing) && - !self.username.trim().is_empty() && - !self.password.trim().is_empty() - } - } - - impl CanvasState for StateMachineForm { - 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(1); } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { - match self.current_field { - 0 => &self.username, - 1 => &self.password, - _ => "", - } - } - - fn get_current_input_mut(&mut self) -> &mut String { - match self.current_field { - 0 => &mut self.username, - 1 => &mut self.password, - _ => unreachable!(), - } - } - - fn inputs(&self) -> Vec<&String> { vec![&self.username, &self.password] } - fn fields(&self) -> Vec<&str> { vec!["Username", "Password"] } - fn has_unsaved_changes(&self) -> bool { self.has_changes } - - fn set_has_unsaved_changes(&mut self, changed: bool) { - self.has_changes = changed; - // Transition to editing state when user starts typing - if changed && self.state == FormState::Initial { - self.state = FormState::Editing; - } - } - - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - match action { - CanvasAction::Custom(cmd) => match cmd.as_str() { - "submit" => { - if self.can_submit() { - let msg = self.transition_to(FormState::Submitting); - // Simulate submission - self.state = FormState::Success; - Some(format!("{} -> Form submitted successfully", msg)) - } else { - let msg = self.transition_to(FormState::Error("Invalid form data".to_string())); - Some(msg) - } - } - "reset" => { - self.username.clear(); - self.password.clear(); - self.has_changes = false; - Some(self.transition_to(FormState::Initial)) - } - _ => None, - }, - _ => None, - } - } - } - - let mut form = StateMachineForm::new(); - let mut ideal_cursor = 0; - - println!(" Initial state: {:?}", form.state); - - // Type some text to trigger state change - let result = ActionDispatcher::dispatch( - CanvasAction::InsertChar('u'), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - println!(" After typing: {:?}", form.state); - - // Try to submit (should fail) - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("submit".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - println!(" Submit result: {}", result.message().unwrap_or("")); - println!(" ✅ State machine integration works!\n"); -} - -// Pattern 2: Event-driven architecture -async fn event_driven_example() { - println!("📡 Pattern 2: Event-Driven Architecture"); - - #[derive(Debug, Clone)] - enum FormEvent { - FieldChanged { field: usize, old_value: String, new_value: String }, - ValidationTriggered { field: usize, is_valid: bool }, - ActionExecuted { action: String, success: bool }, - } - - #[derive(Debug)] - struct EventDrivenForm { - current_field: usize, - cursor_pos: usize, - email: String, - has_changes: bool, - events: Vec, - } - - impl EventDrivenForm { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - email: String::new(), - has_changes: false, - events: Vec::new(), - } - } - - fn emit_event(&mut self, event: FormEvent) { - println!(" 📡 Event: {:?}", event); - self.events.push(event); - } - - fn validate_email(&self) -> bool { - self.email.contains('@') && self.email.contains('.') - } - } - - impl CanvasState for EventDrivenForm { - fn current_field(&self) -> usize { self.current_field } - fn current_cursor_pos(&self) -> usize { self.cursor_pos } - fn set_current_field(&mut self, index: usize) { self.current_field = index; } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { &self.email } - fn get_current_input_mut(&mut self) -> &mut String { &mut self.email } - fn inputs(&self) -> Vec<&String> { vec![&self.email] } - fn fields(&self) -> Vec<&str> { vec!["Email"] } - fn has_unsaved_changes(&self) -> bool { self.has_changes } - - fn set_has_unsaved_changes(&mut self, changed: bool) { - if changed != self.has_changes { - let old_value = if self.has_changes { "modified" } else { "unmodified" }; - let new_value = if changed { "modified" } else { "unmodified" }; - - self.emit_event(FormEvent::FieldChanged { - field: self.current_field, - old_value: old_value.to_string(), - new_value: new_value.to_string(), - }); - } - self.has_changes = changed; - } - - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - match action { - CanvasAction::Custom(cmd) => match cmd.as_str() { - "validate" => { - let is_valid = self.validate_email(); - self.emit_event(FormEvent::ValidationTriggered { - field: self.current_field, - is_valid, - }); - - self.emit_event(FormEvent::ActionExecuted { - action: "validate".to_string(), - success: true, - }); - - if is_valid { - Some("Email is valid!".to_string()) - } else { - Some("Email is invalid".to_string()) - } - } - _ => None, - }, - _ => None, - } - } - } - - let mut form = EventDrivenForm::new(); - let mut ideal_cursor = 0; - - // Type an email address - let email = "user@example.com"; - for c in email.chars() { - ActionDispatcher::dispatch( - CanvasAction::InsertChar(c), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - } - - // Validate the email - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("validate".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - - println!(" Final email: {}", form.email); - println!(" Validation result: {}", result.message().unwrap_or("")); - println!(" Total events captured: {}", form.events.len()); - println!(" ✅ Event-driven architecture works!\n"); -} - -// Pattern 3: Validation pipeline -async fn validation_pipeline_example() { - println!("✅ Pattern 3: Validation Pipeline"); - - type ValidationRule = Box Result<(), String>>; - - #[derive(Debug)] - struct ValidatedForm { - current_field: usize, - cursor_pos: usize, - password: String, - has_changes: bool, - validators: HashMap>, - } - - impl ValidatedForm { - fn new() -> Self { - let mut validators: HashMap> = HashMap::new(); - - // Password validators - let mut password_validators: Vec = Vec::new(); - password_validators.push(Box::new(|value| { - if value.len() < 8 { - Err("Password must be at least 8 characters".to_string()) - } else { - Ok(()) - } - })); - password_validators.push(Box::new(|value| { - if !value.chars().any(|c| c.is_uppercase()) { - Err("Password must contain at least one uppercase letter".to_string()) - } else { - Ok(()) - } - })); - password_validators.push(Box::new(|value| { - if !value.chars().any(|c| c.is_numeric()) { - Err("Password must contain at least one number".to_string()) - } else { - Ok(()) - } - })); - - validators.insert(0, password_validators); - - Self { - current_field: 0, - cursor_pos: 0, - password: String::new(), - has_changes: false, - validators, - } - } - - fn validate_field(&self, field_index: usize) -> Vec { - let mut errors = Vec::new(); - - if let Some(validators) = self.validators.get(&field_index) { - let value = match field_index { - 0 => &self.password, - _ => return errors, - }; - - for validator in validators { - if let Err(error) = validator(value) { - errors.push(error); - } - } - } - - errors - } - } - - impl CanvasState for ValidatedForm { - fn current_field(&self) -> usize { self.current_field } - fn current_cursor_pos(&self) -> usize { self.cursor_pos } - fn set_current_field(&mut self, index: usize) { self.current_field = index; } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { &self.password } - fn get_current_input_mut(&mut self) -> &mut String { &mut self.password } - fn inputs(&self) -> Vec<&String> { vec![&self.password] } - fn fields(&self) -> Vec<&str> { vec!["Password"] } - 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() { - "validate" => { - let errors = self.validate_field(self.current_field); - if errors.is_empty() { - Some("Password meets all requirements!".to_string()) - } else { - Some(format!("Validation errors: {}", errors.join(", "))) - } - } - _ => None, - }, - _ => None, - } - } - } - - let mut form = ValidatedForm::new(); - let mut ideal_cursor = 0; - - // Test with weak password - let weak_password = "abc"; - for c in weak_password.chars() { - ActionDispatcher::dispatch( - CanvasAction::InsertChar(c), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - } - - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("validate".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - println!(" Weak password '{}': {}", form.password, result.message().unwrap_or("")); - - // Clear and test with strong password - form.password.clear(); - form.cursor_pos = 0; - - let strong_password = "StrongPass123"; - for c in strong_password.chars() { - ActionDispatcher::dispatch( - CanvasAction::InsertChar(c), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - } - - let result = ActionDispatcher::dispatch( - CanvasAction::Custom("validate".to_string()), - &mut form, - &mut ideal_cursor, - ).await.unwrap(); - println!(" Strong password '{}': {}", form.password, result.message().unwrap_or("")); - println!(" ✅ Validation pipeline works!\n"); -} - -// Pattern 4: Multi-form orchestration -async fn multi_form_example() { - println!("🎭 Pattern 4: Multi-Form Orchestration"); - - #[derive(Debug)] - struct PersonalInfoForm { - current_field: usize, - cursor_pos: usize, - name: String, - age: String, - has_changes: bool, - } - - #[derive(Debug)] - struct ContactInfoForm { - current_field: usize, - cursor_pos: usize, - email: String, - phone: String, - has_changes: bool, - } - - // Implement CanvasState for both forms - impl CanvasState for PersonalInfoForm { - 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(1); } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { - match self.current_field { - 0 => &self.name, - 1 => &self.age, - _ => "", - } - } - - fn get_current_input_mut(&mut self) -> &mut String { - match self.current_field { - 0 => &mut self.name, - 1 => &mut self.age, - _ => unreachable!(), - } - } - - fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.age] } - fn fields(&self) -> Vec<&str> { vec!["Name", "Age"] } - fn has_unsaved_changes(&self) -> bool { self.has_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; } - } - - impl CanvasState for ContactInfoForm { - 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(1); } - fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; } - - fn get_current_input(&self) -> &str { - match self.current_field { - 0 => &self.email, - 1 => &self.phone, - _ => "", - } - } - - fn get_current_input_mut(&mut self) -> &mut String { - match self.current_field { - 0 => &mut self.email, - 1 => &mut self.phone, - _ => unreachable!(), - } - } - - fn inputs(&self) -> Vec<&String> { vec![&self.email, &self.phone] } - fn fields(&self) -> Vec<&str> { vec!["Email", "Phone"] } - fn has_unsaved_changes(&self) -> bool { self.has_changes } - fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; } - } - - // Form orchestrator - #[derive(Debug)] - struct FormOrchestrator { - personal_form: PersonalInfoForm, - contact_form: ContactInfoForm, - current_form: usize, // 0 = personal, 1 = contact - } - - impl FormOrchestrator { - fn new() -> Self { - Self { - personal_form: PersonalInfoForm { - current_field: 0, - cursor_pos: 0, - name: String::new(), - age: String::new(), - has_changes: false, - }, - contact_form: ContactInfoForm { - current_field: 0, - cursor_pos: 0, - email: String::new(), - phone: String::new(), - has_changes: false, - }, - current_form: 0, - } - } - - async fn execute_action(&mut self, action: CanvasAction) -> ActionResult { - let mut ideal_cursor = 0; - - match self.current_form { - 0 => ActionDispatcher::dispatch(action, &mut self.personal_form, &mut ideal_cursor).await.unwrap(), - 1 => ActionDispatcher::dispatch(action, &mut self.contact_form, &mut ideal_cursor).await.unwrap(), - _ => ActionResult::error("Invalid form index"), - } - } - - fn switch_form(&mut self) -> String { - self.current_form = (self.current_form + 1) % 2; - match self.current_form { - 0 => "Switched to Personal Info form".to_string(), - 1 => "Switched to Contact Info form".to_string(), - _ => "Unknown form".to_string(), - } - } - - fn current_form_name(&self) -> &str { - match self.current_form { - 0 => "Personal Info", - 1 => "Contact Info", - _ => "Unknown", - } - } - } - - let mut orchestrator = FormOrchestrator::new(); - - println!(" Current form: {}", orchestrator.current_form_name()); - - // Fill personal info - let personal_data = vec![ - ('J', 'o', 'h', 'n'), - ]; - - for &c in &['J', 'o', 'h', 'n'] { - orchestrator.execute_action(CanvasAction::InsertChar(c)).await; - } - - orchestrator.execute_action(CanvasAction::NextField).await; - - for &c in &['2', '5'] { - orchestrator.execute_action(CanvasAction::InsertChar(c)).await; - } - - println!(" Personal form - Name: '{}', Age: '{}'", - orchestrator.personal_form.name, - orchestrator.personal_form.age); - - // Switch to contact form - let switch_msg = orchestrator.switch_form(); - println!(" {}", switch_msg); - - // Fill contact info - for &c in &['j', 'o', 'h', 'n', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'] { - orchestrator.execute_action(CanvasAction::InsertChar(c)).await; - } - - orchestrator.execute_action(CanvasAction::NextField).await; - - for &c in &['5', '5', '5', '-', '1', '2', '3', '4'] { - orchestrator.execute_action(CanvasAction::InsertChar(c)).await; - } - - println!(" Contact form - Email: '{}', Phone: '{}'", - orchestrator.contact_form.email, - orchestrator.contact_form.phone); - - println!(" ✅ Multi-form orchestration works!\n"); - - println!("🎉 All integration patterns completed!"); - println!("The Canvas crate seamlessly integrates with various architectural patterns!"); -} diff --git a/canvas/examples/simple_login.rs b/canvas/examples/simple_login.rs deleted file mode 100644 index caaadb8..0000000 --- a/canvas/examples/simple_login.rs +++ /dev/null @@ -1,354 +0,0 @@ -// examples/simple_login.rs -//! A simple login form demonstrating basic canvas usage -//! -//! Run with: cargo run --example simple_login - -use canvas::prelude::*; -use crossterm::{ - event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, -}; -use std::io::{self, Write}; - -#[derive(Debug)] -struct LoginForm { - current_field: usize, - cursor_pos: usize, - username: String, - password: String, - has_changes: bool, -} - -impl LoginForm { - fn new() -> Self { - Self { - current_field: 0, - cursor_pos: 0, - username: String::new(), - password: String::new(), - has_changes: false, - } - } - - fn reset(&mut self) { - self.username.clear(); - self.password.clear(); - self.current_field = 0; - self.cursor_pos = 0; - self.has_changes = false; - } - - fn is_valid(&self) -> bool { - !self.username.trim().is_empty() && !self.password.trim().is_empty() - } -} - -impl CanvasState for LoginForm { - 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(1); // Only 2 fields: username(0), password(1) - } - - fn set_current_cursor_pos(&mut self, pos: usize) { - self.cursor_pos = pos; - } - - fn get_current_input(&self) -> &str { - match self.current_field { - 0 => &self.username, - 1 => &self.password, - _ => "", - } - } - - fn get_current_input_mut(&mut self) -> &mut String { - match self.current_field { - 0 => &mut self.username, - 1 => &mut self.password, - _ => unreachable!(), - } - } - - fn inputs(&self) -> Vec<&String> { - vec![&self.username, &self.password] - } - - fn fields(&self) -> Vec<&str> { - vec!["Username", "Password"] - } - - fn has_unsaved_changes(&self) -> bool { - self.has_changes - } - - fn set_has_unsaved_changes(&mut self, changed: bool) { - self.has_changes = changed; - } - - // Custom action handling - fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { - match action { - CanvasAction::Custom(cmd) => match cmd.as_str() { - "submit" => { - if self.is_valid() { - Some(format!("Login successful! Welcome, {}", self.username)) - } else { - Some("Error: Username and password are required".to_string()) - } - } - "clear" => { - self.reset(); - Some("Form cleared".to_string()) - } - _ => None, - }, - _ => None, - } - } - - // Override display for password field - fn get_display_value_for_field(&self, index: usize) -> &str { - match index { - 0 => &self.username, // Username shows normally - 1 => &self.password, // We'll handle masking in the UI drawing - _ => "", - } - } - - fn has_display_override(&self, index: usize) -> bool { - index == 1 // Password field has display override - } -} - -fn draw_ui(form: &LoginForm, message: &str) -> io::Result<()> { - // Clear screen and move cursor to top-left - print!("\x1B[2J\x1B[1;1H"); - - println!("╔═══════════════════════════════════════╗"); - println!("║ LOGIN FORM ║"); - println!("╠═══════════════════════════════════════╣"); - - // Username field - let username_indicator = if form.current_field == 0 { "→" } else { " " }; - let username_display = if form.username.is_empty() { - "".to_string() - } else { - form.username.clone() - }; - println!("║ {} Username: {:22} ║", username_indicator, - if username_display.len() > 22 { - format!("{}...", &username_display[..19]) - } else { - format!("{:22}", username_display) - }); - - // Show cursor for username field - if form.current_field == 0 && !form.username.is_empty() { - let cursor_pos = form.cursor_pos.min(form.username.len()); - let spaces_before = 11 + cursor_pos; // "Username: " = 10 chars + 1 space - let cursor_line = format!("║ {}█{:width$}║", - " ".repeat(spaces_before), - "", - width = 25_usize.saturating_sub(spaces_before) - ); - println!("{}", cursor_line); - } else { - println!("║{:37}║", ""); - } - - // Password field - let password_indicator = if form.current_field == 1 { "→" } else { " " }; - let password_display = if form.password.is_empty() { - "".to_string() - } else { - "*".repeat(form.password.len()) - }; - println!("║ {} Password: {:22} ║", password_indicator, - if password_display.len() > 22 { - format!("{}...", &password_display[..19]) - } else { - format!("{:22}", password_display) - }); - - // Show cursor for password field - if form.current_field == 1 && !form.password.is_empty() { - let cursor_pos = form.cursor_pos.min(form.password.len()); - let spaces_before = 11 + cursor_pos; // "Password: " = 10 chars + 1 space - let cursor_line = format!("║ {}█{:width$}║", - " ".repeat(spaces_before), - "", - width = 25_usize.saturating_sub(spaces_before) - ); - println!("{}", cursor_line); - } else { - println!("║{:37}║", ""); - } - - println!("╠═══════════════════════════════════════╣"); - println!("║ CONTROLS: ║"); - println!("║ Tab/↑↓ - Navigate fields ║"); - println!("║ Enter - Submit form ║"); - println!("║ Ctrl+R - Clear form ║"); - println!("║ Ctrl+C - Exit ║"); - println!("╠═══════════════════════════════════════╣"); - - // Status message - let status = if !message.is_empty() { - message.to_string() - } else if form.has_changes { - "Form modified".to_string() - } else { - "Ready - enter your credentials".to_string() - }; - - let status_display = if status.len() > 33 { - format!("{}...", &status[..30]) - } else { - format!("{:33}", status) - }; - - println!("║ Status: {} ║", status_display); - println!("╚═══════════════════════════════════════╝"); - - // Show current state info - println!(); - println!("Current field: {} ({})", - form.current_field, - form.fields()[form.current_field]); - println!("Cursor position: {}", form.cursor_pos); - println!("Has changes: {}", form.has_changes); - - io::stdout().flush()?; - Ok(()) -} - -#[tokio::main] -async fn main() -> io::Result<()> { - println!("Starting Canvas Login Demo..."); - println!("Setting up terminal..."); - - // Setup terminal - enable_raw_mode()?; - io::stdout().execute(EnterAlternateScreen)?; - - let mut form = LoginForm::new(); - let mut ideal_cursor = 0; - let mut message = String::new(); - - // Initial draw - if let Err(e) = draw_ui(&form, &message) { - // Cleanup on error - let _ = disable_raw_mode(); - let _ = io::stdout().execute(LeaveAlternateScreen); - return Err(e); - } - - println!("Canvas Login Demo started. Use Ctrl+C to exit."); - - loop { - match event::read() { - Ok(Event::Key(key)) => { - // Clear message after key press - if !message.is_empty() { - message.clear(); - } - - match key { - // Exit - KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => { - break; - } - - // Clear form - KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::CONTROL, .. } => { - match ActionDispatcher::dispatch( - CanvasAction::Custom("clear".to_string()), - &mut form, - &mut ideal_cursor, - ).await { - Ok(result) => { - if let Some(msg) = result.message() { - message = msg.to_string(); - } - } - Err(e) => { - message = format!("Error: {}", e); - } - } - } - - // Submit form - KeyEvent { code: KeyCode::Enter, .. } => { - match ActionDispatcher::dispatch( - CanvasAction::Custom("submit".to_string()), - &mut form, - &mut ideal_cursor, - ).await { - Ok(result) => { - if let Some(msg) = result.message() { - message = msg.to_string(); - } - } - Err(e) => { - message = format!("Error: {}", e); - } - } - } - - // Regular key handling - let canvas handle it! - _ => { - if let Some(action) = CanvasAction::from_key(key.code) { - match ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await { - Ok(result) => { - if !result.is_success() { - if let Some(msg) = result.message() { - message = format!("Error: {}", msg); - } - } - } - Err(e) => { - message = format!("Error: {}", e); - } - } - } - } - } - - // Redraw UI - if let Err(e) = draw_ui(&form, &message) { - eprintln!("Error drawing UI: {}", e); - break; - } - } - Ok(_) => { - // Ignore other events (mouse, resize, etc.) - } - Err(e) => { - message = format!("Event error: {}", e); - if let Err(_) = draw_ui(&form, &message) { - break; - } - } - } - } - - // Cleanup - disable_raw_mode()?; - io::stdout().execute(LeaveAlternateScreen)?; - - println!("Thanks for using Canvas Login Demo!"); - println!("Final form state:"); - println!(" Username: '{}'", form.username); - println!(" Password: '{}'", "*".repeat(form.password.len())); - println!(" Valid: {}", form.is_valid()); - - Ok(()) -} diff --git a/client/canvas_config.toml b/client/canvas_config.toml index 930e1bc..3006cd5 100644 --- a/client/canvas_config.toml +++ b/client/canvas_config.toml @@ -24,7 +24,7 @@ move_word_end_prev = ["ge"] move_line_start = ["0"] move_line_end = ["$"] move_first_line = ["gg"] -move_last_line = ["CapsLock"] +move_last_line = ["shift+g"] next_field = ["Tab"] prev_field = ["Shift+Tab"] diff --git a/client/src/components/common/search_palette.rs b/client/src/components/common/search_palette.rs index f0810ff..feba96f 100644 --- a/client/src/components/common/search_palette.rs +++ b/client/src/components/common/search_palette.rs @@ -55,10 +55,10 @@ pub fn render_search_palette( .style(Style::default().fg(theme.fg)); f.render_widget(input_text, inner_chunks[0]); // Set cursor position - f.set_cursor( + f.set_cursor_position(( inner_chunks[0].x + state.cursor_position as u16 + 1, inner_chunks[0].y + 1, - ); + )); // --- Render Results List --- if state.is_loading { diff --git a/client/src/components/common/status_line.rs b/client/src/components/common/status_line.rs index cccd879..19dd385 100644 --- a/client/src/components/common/status_line.rs +++ b/client/src/components/common/status_line.rs @@ -5,9 +5,10 @@ use ratatui::{ layout::Rect, style::Style, text::{Line, Span, Text}, - widgets::{Paragraph, Wrap}, // Make sure Wrap is imported + widgets::Paragraph, Frame, }; +use ratatui::widgets::Wrap; use std::path::Path; use unicode_width::UnicodeWidthStr; diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 42c20a6..7dc40a9 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -17,7 +17,7 @@ use anyhow::Result; use common::proto::komp_ac::search::search_response::Hit; use crossterm::event::{KeyCode, KeyEvent}; use tokio::sync::mpsc; -use tracing::{debug, info}; +use tracing::info; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EditEventOutcome { diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 302644c..062fb94 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -18,7 +18,7 @@ use crate::modes::{ use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState; use crate::services::auth::AuthClient; use crate::services::grpc_client::GrpcClient; -use canvas::{CanvasAction, ActionDispatcher, ActionResult}; +use canvas::{CanvasAction, ActionDispatcher}; use canvas::CanvasState as LibraryCanvasState; use super::event_helper::*; use crate::state::{ diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index ed0019d..6f640a4 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -7,7 +7,6 @@ use crate::components::{ common::dialog::render_dialog, common::find_file_palette, common::search_palette::render_search_palette, - form::form::render_form, handlers::sidebar::{self, calculate_sidebar_layout}, intro::intro::render_intro, render_background, diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 30b1c60..c4f3dcd 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -27,12 +27,12 @@ use crate::ui::handlers::context::DialogPurpose; use crate::tui::functions::common::login; use crate::tui::functions::common::register; use crate::utils::columns::filter_user_columns; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use crossterm::cursor::SetCursorStyle; use crossterm::event as crossterm_event; use tracing::{error, info, warn}; use tokio::sync::mpsc; -use std::time::{Duration, Instant}; +use std::time::{Instant, Duration}; #[cfg(feature = "ui-debug")] use crate::state::app::state::DebugState; #[cfg(feature = "ui-debug")] diff --git a/client/tests/form/gui/form_tests.rs b/client/tests/form/gui/form_tests.rs index 00b9bd1..334ae07 100644 --- a/client/tests/form/gui/form_tests.rs +++ b/client/tests/form/gui/form_tests.rs @@ -2,6 +2,7 @@ use rstest::{fixture, rstest}; use std::collections::HashMap; use client::state::pages::form::{FormState, FieldDefinition}; +use canvas::state::CanvasState use client::state::pages::canvas_state::CanvasState; #[fixture]