// examples/canvas_keymap.rs //! Demonstrates the centralized keymap system for canvas interactions //! //! This example shows how to use the canvas-keymap feature to delegate //! all canvas key handling to the library, supporting complex sequences //! like "gg", "ge", etc. //! //! Run with: //! cargo run --example canvas_keymap --features "gui,keymap,cursor-style" #[cfg(not(feature = "keymap"))] compile_error!( "This example requires the 'keymap' feature. \ Run with: cargo run --example canvas_keymap --features \"gui,keymap,cursor-style\"" ); use std::collections::HashMap; use std::io; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEvent}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, Frame, Terminal, }; use canvas::{ canvas::{gui::render_canvas_default, modes::AppMode}, keymap::{CanvasKeyMap, KeyEventOutcome}, DataProvider, FormEditor, }; /// Demo application using centralized keymap system struct KeymapDemoApp { editor: FormEditor, message: String, quit: bool, } impl KeymapDemoApp { fn new() -> Self { let data = DemoData::new(); let mut editor = FormEditor::new(data); // Build and inject the keymap from our config let keymap = Self::build_demo_keymap(); editor.set_keymap(keymap); Self { editor, message: "đŸŽ¯ Keymap system loaded! Try: gg, ge, hjkl, w/b/e, v, i, etc.".to_string(), quit: false, } } /// Build a comprehensive keymap configuration fn build_demo_keymap() -> CanvasKeyMap { let mut read_only = HashMap::new(); let mut edit = HashMap::new(); let mut highlight = HashMap::new(); // === READ-ONLY MODE KEYBINDINGS === // Basic movement read_only.insert("move_left".to_string(), vec!["h".to_string(), "Left".to_string()]); read_only.insert("move_right".to_string(), vec!["l".to_string(), "Right".to_string()]); read_only.insert("move_up".to_string(), vec!["k".to_string(), "Up".to_string()]); read_only.insert("move_down".to_string(), vec!["j".to_string(), "Down".to_string()]); // Word movement read_only.insert("move_word_next".to_string(), vec!["w".to_string()]); read_only.insert("move_word_prev".to_string(), vec!["b".to_string()]); read_only.insert("move_word_end".to_string(), vec!["e".to_string()]); read_only.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]); // Multi-key! // Big word movement read_only.insert("move_big_word_next".to_string(), vec!["W".to_string()]); read_only.insert("move_big_word_prev".to_string(), vec!["B".to_string()]); read_only.insert("move_big_word_end".to_string(), vec!["E".to_string()]); read_only.insert("move_big_word_end_prev".to_string(), vec!["gE".to_string()]); // Multi-key! // Line movement read_only.insert("move_line_start".to_string(), vec!["0".to_string(), "Home".to_string()]); read_only.insert("move_line_end".to_string(), vec!["$".to_string(), "End".to_string()]); // Field movement read_only.insert("move_first_line".to_string(), vec!["gg".to_string()]); // Multi-key! read_only.insert("move_last_line".to_string(), vec!["G".to_string()]); read_only.insert("next_field".to_string(), vec!["Tab".to_string()]); read_only.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]); // Mode transitions read_only.insert("enter_edit_mode_before".to_string(), vec!["i".to_string()]); read_only.insert("enter_edit_mode_after".to_string(), vec!["a".to_string()]); read_only.insert("enter_highlight_mode".to_string(), vec!["v".to_string()]); read_only.insert("enter_highlight_mode_linewise".to_string(), vec!["V".to_string()]); // Editing actions in normal mode read_only.insert("delete_char_forward".to_string(), vec!["x".to_string()]); read_only.insert("delete_char_backward".to_string(), vec!["X".to_string()]); read_only.insert("open_line_below".to_string(), vec!["o".to_string()]); read_only.insert("open_line_above".to_string(), vec!["O".to_string()]); // === EDIT MODE KEYBINDINGS === edit.insert("exit_edit_mode".to_string(), vec!["esc".to_string()]); edit.insert("move_left".to_string(), vec!["Left".to_string()]); edit.insert("move_right".to_string(), vec!["Right".to_string()]); edit.insert("move_up".to_string(), vec!["Up".to_string()]); edit.insert("move_down".to_string(), vec!["Down".to_string()]); edit.insert("move_line_start".to_string(), vec!["Home".to_string()]); edit.insert("move_line_end".to_string(), vec!["End".to_string()]); edit.insert("move_word_next".to_string(), vec!["Ctrl+Right".to_string()]); edit.insert("move_word_prev".to_string(), vec!["Ctrl+Left".to_string()]); edit.insert("next_field".to_string(), vec!["Tab".to_string()]); edit.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]); edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]); edit.insert("delete_char_forward".to_string(), vec!["Delete".to_string()]); // === HIGHLIGHT MODE KEYBINDINGS === highlight.insert("exit_highlight_mode".to_string(), vec!["esc".to_string()]); highlight.insert("enter_highlight_mode_linewise".to_string(), vec!["V".to_string()]); // Movement (extends selection) highlight.insert("move_left".to_string(), vec!["h".to_string(), "Left".to_string()]); highlight.insert("move_right".to_string(), vec!["l".to_string(), "Right".to_string()]); highlight.insert("move_up".to_string(), vec!["k".to_string(), "Up".to_string()]); highlight.insert("move_down".to_string(), vec!["j".to_string(), "Down".to_string()]); highlight.insert("move_word_next".to_string(), vec!["w".to_string()]); highlight.insert("move_word_prev".to_string(), vec!["b".to_string()]); highlight.insert("move_word_end".to_string(), vec!["e".to_string()]); highlight.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]); highlight.insert("move_line_start".to_string(), vec!["0".to_string()]); highlight.insert("move_line_end".to_string(), vec!["$".to_string()]); highlight.insert("move_first_line".to_string(), vec!["gg".to_string()]); highlight.insert("move_last_line".to_string(), vec!["G".to_string()]); CanvasKeyMap::from_mode_maps(&read_only, &edit, &highlight) } fn handle_key_event(&mut self, key_event: KeyEvent) -> io::Result<()> { // First, try canvas keymap match self.editor.handle_key_event(key_event) { KeyEventOutcome::Consumed(Some(msg)) => { self.message = format!("đŸŽ¯ Canvas: {}", msg); return Ok(()); } KeyEventOutcome::Consumed(None) => { self.message = "đŸŽ¯ Canvas action executed".to_string(); return Ok(()); } KeyEventOutcome::Pending => { self.message = "âŗ Waiting for next key in sequence...".to_string(); return Ok(()); } KeyEventOutcome::NotMatched => { // Fall through to client actions } } // Handle client-specific actions (non-canvas) use crossterm::event::{KeyCode, KeyModifiers}; match (key_event.code, key_event.modifiers) { (KeyCode::Char('q'), KeyModifiers::CONTROL) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => { self.quit = true; self.message = "👋 Goodbye!".to_string(); } (KeyCode::F(1), _) => { self.message = "â„šī¸ F1: This is a client action (not handled by canvas keymap)".to_string(); } (KeyCode::F(2), _) => { // Demonstrate saving self.message = "💾 F2: Save action (client-side)".to_string(); } (KeyCode::Char('?'), _) if self.editor.mode() == AppMode::ReadOnly => { self.show_help(); } _ => { // Unknown key self.message = format!( "❓ Unhandled key: {:?} (mode: {:?})", key_event.code, self.editor.mode() ); } } Ok(()) } fn show_help(&mut self) { self.message = "📖 Help: Multi-key sequences work! Try gg, ge, gE. Also: hjkl, w/b/e, v/V, i/a/o".to_string(); } fn should_quit(&self) -> bool { self.quit } fn editor(&self) -> &FormEditor { &self.editor } fn message(&self) -> &str { &self.message } } /// Demo form data with interesting examples for keymap testing struct DemoData { fields: Vec<(String, String)>, } impl DemoData { fn new() -> Self { Self { fields: vec![ ("đŸŽ¯ Name".to_string(), "John-Paul McDonald-Smith".to_string()), ("📧 Email".to_string(), "user@long-domain-name.example.com".to_string()), ("📱 Phone".to_string(), "+1 (555) 123-4567 ext. 890".to_string()), ("🏠 Address".to_string(), "123 Main Street, Apartment 4B, Suite 100".to_string()), ("đŸˇī¸ Tags".to_string(), "urgent,important,follow-up,high-priority".to_string()), ("📝 Notes".to_string(), "Test word movements: w=next-word, b=prev-word, e=word-end, ge=prev-word-end".to_string()), ("đŸ”Ĩ Multi-key".to_string(), "Try multi-key sequences: gg=first-field, ge=prev-word-end, gE=prev-WORD-end".to_string()), ("⚡ Vim Actions".to_string(), "Normal mode: x=delete-char, o=open-line-below, v=visual, i=insert".to_string()), ], } } } impl DataProvider for DemoData { 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 run_app(terminal: &mut Terminal, mut app: KeymapDemoApp) -> io::Result<()> { loop { terminal.draw(|f| ui(f, &app))?; if let Event::Key(key) = event::read()? { app.handle_key_event(key)?; if app.should_quit() { break; } } } Ok(()) } fn ui(f: &mut Frame, app: &KeymapDemoApp) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(8), Constraint::Length(12)]) .split(f.area()); // Render the canvas render_canvas_default(f, chunks[0], app.editor()); // Render status and help render_status_and_help(f, chunks[1], app); } fn render_status_and_help(f: &mut Frame, area: ratatui::layout::Rect, app: &KeymapDemoApp) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(9)]) .split(area); // Status message let status_text = format!( "Mode: {:?} | Field: {}/{} | Pos: {} | {}", app.editor().mode(), app.editor().current_field() + 1, app.editor().data_provider().field_count(), app.editor().cursor_position(), app.message() ); let status = Paragraph::new(Line::from(Span::raw(status_text))) .block(Block::default().borders(Borders::ALL).title("đŸŽ¯ Keymap Demo Status")); f.render_widget(status, chunks[0]); // Help text based on current mode let help_text = match app.editor().mode() { AppMode::ReadOnly => { "đŸŽ¯ KEYMAP DEMO - All keys handled by centralized keymap system!\n\ \n\ 📍 MOVEMENT: hjkl(basic) | w/b/e(words) | W/B/E(WORDS) | 0/$(line) | gg/G(fields)\n\ đŸ”Ĩ MULTI-KEY: gg=first-field, ge=prev-word-end, gE=prev-WORD-end\n\ âœī¸ MODES: i/a(insert) | v/V(visual) | o/O(open-line)\n\ đŸ—‘ī¸ DELETE: x/X(delete-char)\n\ 📂 FIELDS: Tab/Shift+Tab\n\ \n\ 💡 Try multi-key sequences like 'gg' or 'ge' - watch the status for 'Waiting...'\n\ đŸšĒ Ctrl+C=quit | ?=help | F1/F2=client actions (not canvas)" } AppMode::Edit => { "âœī¸ INSERT MODE - Keys handled by keymap system\n\ \n\ 🔄 NAVIGATION: arrows | Ctrl+arrows(words) | Home/End(line) | Tab/Shift+Tab(fields)\n\ đŸ—‘ī¸ DELETE: Backspace/Delete\n\ đŸšĒ EXIT: Esc=normal\n\ \n\ 💡 Type text normally - the keymap handles navigation!" } AppMode::Highlight => { "đŸŽ¯ VISUAL MODE - Selection extended by keymap movements\n\ \n\ 📍 EXTEND: hjkl(basic) | w/b/e(words) | 0/$(line) | gg/G(fields)\n\ 🔄 SWITCH: V=toggle-line-mode\n\ đŸšĒ EXIT: Esc=normal\n\ \n\ 💡 All movements extend the selection automatically!" } _ => "đŸŽ¯ Keymap system active!" }; let help = Paragraph::new(help_text) .block(Block::default().borders(Borders::ALL).title("🚀 Centralized Keymap System")) .style(Style::default().fg(Color::Gray)); f.render_widget(help, chunks[1]); } fn main() -> Result<(), Box> { println!("đŸŽ¯ Canvas Keymap Demo"); println!("✅ canvas-keymap feature: ENABLED"); println!("🚀 Centralized key handling: ACTIVE"); println!("📖 Multi-key sequences: SUPPORTED (gg, ge, gE, etc.)"); 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 app = KeymapDemoApp::new(); let res = run_app(&mut terminal, app); disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{err:?}"); } println!("đŸŽ¯ Keymap demo completed!"); Ok(()) }