From 0baf152c3eb7f31566a9fa55da7ed487c639163e Mon Sep 17 00:00:00 2001 From: Priec Date: Sat, 2 Aug 2025 15:06:29 +0200 Subject: [PATCH] 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 // ===================================================================