// examples/textarea_vim.rs //! Demonstrates automatic cursor management with the textarea widget //! //! This example REQUIRES the `cursor-style` and `textarea` features to compile. //! //! Run with: //! cargo run --example canvas_textarea_cursor_auto --features "gui,cursor-style,textarea" // REQUIRE cursor-style and textarea features #[cfg(not(feature = "cursor-style"))] compile_error!( "This example requires the 'cursor-style' feature. \ Run with: cargo run --example canvas_textarea_cursor_auto --features \"gui,cursor-style,textarea\"" ); #[cfg(not(feature = "textarea"))] compile_error!( "This example requires the 'textarea' feature. \ Run with: cargo run --example canvas_textarea_cursor_auto --features \"gui,cursor-style,textarea\"" ); use std::io; use crossterm::{ event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, 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::{ modes::AppMode, CursorManager, // This import only exists when cursor-style feature is enabled }, textarea::{TextArea, TextAreaState}, }; /// Enhanced TextArea that demonstrates automatic cursor management /// Now uses direct FormEditor method calls via Deref! struct AutoCursorTextArea { textarea: TextAreaState, has_unsaved_changes: bool, debug_message: String, command_buffer: String, } impl AutoCursorTextArea { fn new() -> Self { let initial_text = "🎯 Automatic Cursor Management Demo\n\ Welcome to the textarea cursor demo!\n\ \n\ Try different modes:\n\ • Normal mode: Block cursor █\n\ • Insert mode: Bar cursor |\n\ \n\ Navigation commands:\n\ • hjkl or arrow keys: move cursor\n\ • i/a/A/o/O: enter insert mode\n\ • w/b/e/W/B/E: word movements\n\ • Esc: return to normal mode\n\ \n\ Watch how the terminal cursor changes automatically!\n\ This text can be edited when in insert mode.\n\ \n\ Press ? for help, F1/F2 for manual cursor control demo."; let mut textarea = TextAreaState::from_text(initial_text); textarea.set_placeholder("Start typing..."); textarea.use_wrap(); Self { textarea, has_unsaved_changes: false, debug_message: "🎯 Automatic Cursor Demo - cursor-style feature enabled!".to_string(), command_buffer: String::new(), } } // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === fn enter_insert_mode(&mut self) -> std::io::Result<()> { self.textarea.enter_edit_mode(); // 🎯 Direct FormEditor method call via Deref! CursorManager::update_for_mode(AppMode::Edit)?; // 🎯 Automatic: cursor becomes bar | self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar |".to_string(); Ok(()) } fn enter_append_mode(&mut self) -> std::io::Result<()> { self.textarea.enter_append_mode(); // 🎯 Direct FormEditor method call! CursorManager::update_for_mode(AppMode::Edit)?; self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar |".to_string(); Ok(()) } fn exit_to_normal_mode(&mut self) -> std::io::Result<()> { self.textarea.exit_edit_mode(); // 🎯 Direct FormEditor method call! CursorManager::update_for_mode(AppMode::ReadOnly)?; // 🎯 Automatic: cursor becomes steady block self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string(); Ok(()) } // === MANUAL CURSOR OVERRIDE DEMONSTRATION === fn demo_manual_cursor_control(&mut self) -> std::io::Result<()> { // Users can still manually control cursor if needed CursorManager::update_for_mode(AppMode::Command)?; self.debug_message = "🔧 Manual override: Command cursor _".to_string(); Ok(()) } fn restore_automatic_cursor(&mut self) -> std::io::Result<()> { // Restore automatic cursor based on current mode CursorManager::update_for_mode(self.textarea.mode())?; // 🎯 Direct method call! self.debug_message = "🎯 Restored automatic cursor management".to_string(); Ok(()) } // === TEXTAREA OPERATIONS === fn handle_textarea_input(&mut self, key: KeyEvent) { self.textarea.input(key); self.has_unsaved_changes = true; } // === MOVEMENT OPERATIONS (using direct FormEditor methods!) === fn move_left(&mut self) { self.textarea.move_left(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("← left"); } fn move_right(&mut self) { self.textarea.move_right(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("→ right"); } fn move_up(&mut self) { self.textarea.move_up(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("↑ up"); } fn move_down(&mut self) { self.textarea.move_down(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("↓ down"); } fn move_word_next(&mut self) { self.textarea.move_word_next(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("w: next word"); } fn move_word_prev(&mut self) { self.textarea.move_word_prev(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("b: previous word"); } fn move_word_end(&mut self) { self.textarea.move_word_end(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("e: word end"); } fn move_word_end_prev(&mut self) { self.textarea.move_word_end_prev(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("ge: previous word end"); } fn move_line_start(&mut self) { self.textarea.move_line_start(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("0: line start"); } fn move_line_end(&mut self) { self.textarea.move_line_end(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("$: line end"); } fn move_first_line(&mut self) { self.textarea.move_first_line(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("gg: first line"); } fn move_last_line(&mut self) { self.textarea.move_last_line(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("G: last line"); } // === BIG WORD MOVEMENTS === fn move_big_word_next(&mut self) { self.textarea.move_big_word_next(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("W: next WORD"); } fn move_big_word_prev(&mut self) { self.textarea.move_big_word_prev(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("B: previous WORD"); } fn move_big_word_end(&mut self) { self.textarea.move_big_word_end(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("E: WORD end"); } fn move_big_word_end_prev(&mut self) { self.textarea.move_big_word_end_prev(); // 🎯 Direct FormEditor method call! self.update_debug_for_movement("gE: previous WORD end"); } fn update_debug_for_movement(&mut self, action: &str) { self.debug_message = action.to_string(); } // === DELETE OPERATIONS === fn delete_char_forward(&mut self) { if let Ok(_) = self.textarea.delete_forward() { // 🎯 Direct FormEditor method call! self.has_unsaved_changes = true; self.debug_message = "x: deleted character".to_string(); } } fn delete_char_backward(&mut self) { if let Ok(_) = self.textarea.delete_backward() { // 🎯 Direct FormEditor method call! self.has_unsaved_changes = true; self.debug_message = "X: deleted character backward".to_string(); } } // === VIM-STYLE EDITING === fn open_line_below(&mut self) -> anyhow::Result<()> { let result = self.textarea.open_line_below(); // 🎯 Textarea-specific override! if result.is_ok() { CursorManager::update_for_mode(AppMode::Edit)?; self.debug_message = "✏️ INSERT (open line below) - Cursor: Steady Bar |".to_string(); self.has_unsaved_changes = true; } result } fn open_line_above(&mut self) -> anyhow::Result<()> { let result = self.textarea.open_line_above(); // 🎯 Textarea-specific override! if result.is_ok() { CursorManager::update_for_mode(AppMode::Edit)?; self.debug_message = "✏️ INSERT (open line above) - Cursor: Steady Bar |".to_string(); self.has_unsaved_changes = true; } result } // === 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() } // === GETTERS === fn mode(&self) -> AppMode { self.textarea.mode() // 🎯 Direct FormEditor method call! } fn debug_message(&self) -> &str { &self.debug_message } fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } fn set_debug_message(&mut self, msg: String) { self.debug_message = msg; } fn get_cursor_info(&self) -> String { format!( "Line {}, Col {}", self.textarea.current_field() + 1, // 🎯 Direct FormEditor method call! self.textarea.cursor_position() + 1 // 🎯 Direct FormEditor method call! ) } } /// Handle key press with automatic cursor management fn handle_key_press( key_event: KeyEvent, editor: &mut AutoCursorTextArea, ) -> anyhow::Result { let KeyEvent { code: key, modifiers, .. } = key_event; 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 WITH AUTOMATIC CURSOR MANAGEMENT === (AppMode::ReadOnly, KeyCode::Char('i'), _) => { editor.enter_insert_mode()?; editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('a'), _) => { editor.enter_append_mode()?; editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('A'), _) => { editor.move_line_end(); editor.enter_insert_mode()?; editor.clear_command_buffer(); } // Vim o/O commands (AppMode::ReadOnly, KeyCode::Char('o'), _) => { if let Err(e) = editor.open_line_below() { editor.set_debug_message(format!("Error opening line below: {e}")); } editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('O'), _) => { if let Err(e) = editor.open_line_above() { editor.set_debug_message(format!("Error opening line above: {e}")); } editor.clear_command_buffer(); } // Escape: Exit any mode back to normal (AppMode::Edit, KeyCode::Esc, _) => { editor.exit_to_normal_mode()?; } // === INSERT MODE: Pass to textarea === (AppMode::Edit, _, _) => { editor.handle_textarea_input(key_event); } // === CURSOR MANAGEMENT DEMONSTRATION === (AppMode::ReadOnly, KeyCode::F(1), _) => { editor.demo_manual_cursor_control()?; } (AppMode::ReadOnly, KeyCode::F(2), _) => { editor.restore_automatic_cursor()?; } // === MOVEMENT: VIM-STYLE NAVIGATION (Normal mode) === (AppMode::ReadOnly, KeyCode::Char('h'), _) | (AppMode::ReadOnly, KeyCode::Left, _) => { editor.move_left(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('l'), _) | (AppMode::ReadOnly, KeyCode::Right, _) => { editor.move_right(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('j'), _) | (AppMode::ReadOnly, KeyCode::Down, _) => { editor.move_down(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('k'), _) | (AppMode::ReadOnly, KeyCode::Up, _) => { editor.move_up(); editor.clear_command_buffer(); } // Word movement (AppMode::ReadOnly, KeyCode::Char('w'), _) => { editor.move_word_next(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('b'), _) => { editor.move_word_prev(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('e'), _) => { if editor.get_command_buffer() == "g" { editor.move_word_end_prev(); editor.clear_command_buffer(); } else { editor.move_word_end(); editor.clear_command_buffer(); } } // Big word movement (vim W/B/E commands) (AppMode::ReadOnly, KeyCode::Char('W'), _) => { editor.move_big_word_next(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('B'), _) => { editor.move_big_word_prev(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('E'), _) => { if editor.get_command_buffer() == "g" { editor.move_big_word_end_prev(); editor.clear_command_buffer(); } else { editor.move_big_word_end(); editor.clear_command_buffer(); } } // Line movement (AppMode::ReadOnly, KeyCode::Char('0'), _) | (AppMode::ReadOnly, KeyCode::Home, _) => { editor.move_line_start(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('$'), _) | (AppMode::ReadOnly, KeyCode::End, _) => { editor.move_line_end(); editor.clear_command_buffer(); } // Document movement with command buffer (AppMode::ReadOnly, KeyCode::Char('g'), _) => { if editor.get_command_buffer() == "g" { editor.move_first_line(); editor.clear_command_buffer(); } else { editor.clear_command_buffer(); editor.add_to_command_buffer('g'); editor.set_debug_message("g".to_string()); } } (AppMode::ReadOnly, KeyCode::Char('G'), _) => { editor.move_last_line(); editor.clear_command_buffer(); } // === DELETE OPERATIONS (Normal mode) === (AppMode::ReadOnly, KeyCode::Char('x'), _) => { editor.delete_char_forward(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('X'), _) => { editor.delete_char_backward(); editor.clear_command_buffer(); } // === DEBUG/INFO COMMANDS === (AppMode::ReadOnly, KeyCode::Char('?'), _) => { editor.set_debug_message(format!( "{}, Mode: {:?} - Cursor managed automatically!", editor.get_cursor_info(), mode )); editor.clear_command_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: {key:?} + {modifiers:?} in {mode:?} mode" )); } } } Ok(true) } fn run_app( terminal: &mut Terminal, mut editor: AutoCursorTextArea, ) -> io::Result<()> { loop { terminal.draw(|f| ui(f, &mut editor))?; if let Event::Key(key) = event::read()? { match handle_key_press(key, &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: &mut AutoCursorTextArea) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(8), Constraint::Length(8)]) .split(f.area()); render_textarea(f, chunks[0], editor); render_status_and_help(f, chunks[1], editor); } fn render_textarea( f: &mut Frame, area: ratatui::layout::Rect, editor: &mut AutoCursorTextArea, ) { let block = Block::default() .borders(Borders::ALL) .title("🎯 Textarea with Automatic Cursor Management"); let textarea_widget = TextArea::default().block(block.clone()); f.render_stateful_widget(textarea_widget, area, &mut editor.textarea); // Set cursor position for terminal cursor // Always show cursor - CursorManager handles the style (block/bar/blinking) let (cx, cy) = editor.textarea.cursor(area, Some(&block)); f.set_cursor_position((cx, cy)); } fn render_status_and_help( f: &mut Frame, area: ratatui::layout::Rect, editor: &AutoCursorTextArea, ) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Length(5)]) .split(area); // Status bar with cursor information let mode_text = match editor.mode() { AppMode::Edit => "INSERT | (bar cursor)", AppMode::ReadOnly => "NORMAL █ (block cursor)", AppMode::Highlight => "VISUAL █ (blinking block)", _ => "NORMAL █ (block cursor)", }; 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(), editor.get_cursor_info()) } else { format!("-- {} -- {} | {}", mode_text, editor.debug_message(), editor.get_cursor_info()) }; let status = Paragraph::new(Line::from(Span::raw(status_text))) .block(Block::default().borders(Borders::ALL).title("🎯 Automatic Cursor Status")); f.render_widget(status, chunks[0]); // Help text let help_text = match editor.mode() { AppMode::ReadOnly => { if editor.has_pending_command() { match editor.get_command_buffer() { "g" => "Press 'g' again for first line, or any other key to cancel", _ => "Pending command... (Esc to cancel)" } } else { "🎯 CURSOR-STYLE DEMO: Normal █ | Insert | \n\ Normal: hjkl/arrows=move, w/b/e=words, W/B/E=WORDS, 0/$=line, g/G=first/last\n\ i/a/A/o/O=insert, x/X=delete, ?=info\n\ F1=demo manual cursor, F2=restore automatic, Ctrl+Q=quit" } } AppMode::Edit => { "🎯 INSERT MODE - Cursor: | (bar)\n\ Type to edit text, arrows=move, Enter=new line\n\ Esc=normal mode" } AppMode::Highlight => { "🎯 VISUAL MODE - Cursor: █ (blinking block)\n\ hjkl/arrows=extend selection\n\ Esc=normal mode" } _ => "🎯 Watch the cursor change automatically!" }; let help = Paragraph::new(help_text) .block(Block::default().borders(Borders::ALL).title("🚀 Automatic Cursor Management")) .style(Style::default().fg(Color::Gray)); f.render_widget(help, chunks[1]); } fn main() -> Result<(), Box> { // Print feature status println!("🎯 Canvas Textarea Cursor Auto Demo"); println!("✅ cursor-style feature: ENABLED"); println!("✅ textarea feature: ENABLED"); println!("🚀 Automatic cursor management: ACTIVE"); println!("📖 Watch your terminal cursor change based on mode!"); println!(); enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let mut editor = AutoCursorTextArea::new(); // Initialize with normal mode - library automatically sets block cursor editor.exit_to_normal_mode()?; let res = run_app(&mut terminal, editor); // Reset cursor on exit CursorManager::reset()?; disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{err:?}"); } println!("🎯 Cursor automatically reset to default!"); Ok(()) }