// examples/suggestions2.rs //! Production-ready Tab-triggered suggestions demonstration //! //! This example demonstrates: //! - Tab-triggered suggestions dropdown //! - Non-blocking architecture for real network/database calls //! - Multiple suggestion field types //! - Professional-grade user experience //! //! Run with: //! cargo run --example suggestions2 --features "gui,cursor-style,suggestions" // REQUIRE cursor-style feature - example won't compile without it #[cfg(not(feature = "cursor-style"))] compile_error!( "This example requires the 'cursor-style' feature. \ Run with: cargo run --example suggestions2 --features \"gui,cursor-style,suggestions\"" ); use std::io; use crossterm::{ event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, }, execute, terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }, }; use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, Frame, Terminal, }; use canvas::{ canvas::{ gui::render_canvas_default, modes::AppMode, CursorManager, // This import only exists when cursor-style feature is enabled }, suggestions::gui::render_suggestions_dropdown, DataProvider, FormEditor, SuggestionsProvider, SuggestionItem, }; use async_trait::async_trait; use anyhow::Result; // Enhanced FormEditor that demonstrates professional suggestions architecture struct AutoCursorFormEditor { editor: FormEditor, has_unsaved_changes: bool, debug_message: String, command_buffer: String, // For multi-key vim commands like "gg" } impl AutoCursorFormEditor { fn new(data_provider: D) -> Self { Self { editor: FormEditor::new(data_provider), has_unsaved_changes: false, debug_message: "πŸš€ Production-Ready Tab-Triggered Suggestions Demo - Copy this architecture for your app!".to_string(), command_buffer: String::new(), } } fn close_suggestions(&mut self) { self.editor.close_suggestions(); } // === 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) { self.editor.enter_highlight_mode(); self.debug_message = "πŸ”₯ VISUAL MODE - Cursor: Blinking Block β–ˆ".to_string(); } fn enter_visual_line_mode(&mut self) { self.editor.enter_highlight_line_mode(); self.debug_message = "πŸ”₯ VISUAL LINE MODE - Cursor: Blinking Block β–ˆ".to_string(); } fn exit_visual_mode(&mut self) { self.editor.exit_highlight_mode(); self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); } fn update_visual_selection(&mut self) { if self.editor.is_highlight_mode() { use canvas::canvas::state::SelectionState; match self.editor.selection_state() { SelectionState::Characterwise { anchor } => { self.debug_message = format!( "🎯 Visual selection: anchor=({},{}) current=({},{}) - Cursor: Blinking Block β–ˆ", anchor.0, anchor.1, self.editor.current_field(), self.editor.cursor_position() ); } SelectionState::Linewise { anchor_field } => { self.debug_message = format!( "🎯 Visual LINE selection: anchor={} current={} - Cursor: Blinking Block β–ˆ", anchor_field, self.editor.current_field() ); } _ => {} } } } // === ENHANCED MOVEMENT WITH VISUAL UPDATES === fn move_left(&mut self) { let _ = self.editor.move_left(); self.update_visual_selection(); } fn move_right(&mut self) { let _ = self.editor.move_right(); self.update_visual_selection(); } fn move_up(&mut self) { let _ = self.editor.move_up(); self.update_visual_selection(); } fn move_down(&mut self) { let _ = self.editor.move_down(); self.update_visual_selection(); } fn move_word_next(&mut self) { self.editor.move_word_next(); self.update_visual_selection(); } fn move_word_prev(&mut self) { self.editor.move_word_prev(); self.update_visual_selection(); } fn move_word_end(&mut self) { self.editor.move_word_end(); self.update_visual_selection(); } fn move_word_end_prev(&mut self) { self.editor.move_word_end_prev(); self.update_visual_selection(); } fn move_line_start(&mut self) { self.editor.move_line_start(); self.update_visual_selection(); } fn move_line_end(&mut self) { self.editor.move_line_end(); self.update_visual_selection(); } fn move_first_line(&mut self) { let _ = self.editor.move_first_line(); self.update_visual_selection(); } fn move_last_line(&mut self) { let _ = self.editor.move_last_line(); self.update_visual_selection(); } fn prev_field(&mut self) { let _ = self.editor.prev_field(); self.update_visual_selection(); } fn next_field(&mut self) { let _ = self.editor.next_field(); self.update_visual_selection(); } // === DELETE OPERATIONS === fn delete_backward(&mut self) -> anyhow::Result<()> { let result = self.editor.delete_backward(); if result.is_ok() { self.has_unsaved_changes = true; self.debug_message = "⌫ Deleted character backward".to_string(); } result } fn delete_forward(&mut self) -> anyhow::Result<()> { let result = self.editor.delete_forward(); if result.is_ok() { self.has_unsaved_changes = true; self.debug_message = "⌦ Deleted character forward".to_string(); } result } // === SUGGESTIONS CONTROL WRAPPERS === fn open_suggestions(&mut self, field_index: usize) { self.editor.open_suggestions(field_index); } // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === fn enter_edit_mode(&mut self) { self.editor.enter_edit_mode(); // 🎯 Library automatically sets cursor to bar | self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar | - Press Tab for suggestions".to_string(); } fn enter_append_mode(&mut self) { self.editor.enter_append_mode(); // 🎯 Library automatically positions cursor and sets mode self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar | - Press Tab for suggestions".to_string(); } fn exit_edit_mode(&mut self) { let _ = self.editor.exit_edit_mode(); // 🎯 Library automatically sets cursor to block β–ˆ self.exit_visual_mode(); self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); } fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { let result = self.editor.insert_char(ch); if result.is_ok() { self.has_unsaved_changes = true; } result } // === PRODUCTION-READY NON-BLOCKING SUGGESTIONS === /// Trigger suggestions with non-blocking approach (production pattern) /// /// This method demonstrates the proper way to integrate suggestions with /// real APIs, databases, or any async data source without blocking the UI. async fn trigger_suggestions_async( &mut self, provider: &mut ProductionSuggestionsProvider, field_index: usize, ) { // Step 1: Start loading immediately (UI updates instantly) if let Some(query) = self.editor.start_suggestions(field_index) { // Step 2: Fetch from your data source (API, database, etc.) match provider.fetch_suggestions(field_index, &query).await { Ok(results) => { // Step 3: Apply results with built-in stale protection let applied = self.editor.apply_suggestions_result(field_index, &query, results); if applied { self.editor.update_inline_completion(); if self.editor.suggestions().is_empty() { self.set_debug_message(format!("πŸ” No matches for '{query}'")); } else { self.set_debug_message(format!("✨ {} matches for '{}'", self.editor.suggestions().len(), query)); } } // If not applied, results were stale (user kept typing) } Err(e) => { self.set_debug_message(format!("❌ Suggestion error: {e}")); } } } } fn suggestions_next(&mut self) { self.editor.suggestions_next(); } fn apply_suggestion(&mut self) -> Option { self.editor.apply_suggestion() } fn is_suggestions_active(&self) -> bool { self.editor.is_suggestions_active() } fn suggestions(&self) -> &[SuggestionItem] { self.editor.suggestions() } pub fn update_inline_completion(&mut self) { self.editor.update_inline_completion(); } // === MANUAL CURSOR OVERRIDE DEMONSTRATION === fn demo_manual_cursor_control(&mut self) -> std::io::Result<()> { 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<()> { CursorManager::update_for_mode(self.editor.mode())?; self.debug_message = "🎯 Restored automatic cursor management".to_string(); Ok(()) } // === DELEGATE TO ORIGINAL EDITOR === fn current_field(&self) -> usize { self.editor.current_field() } fn cursor_position(&self) -> usize { self.editor.cursor_position() } fn mode(&self) -> AppMode { self.editor.mode() } fn current_text(&self) -> &str { let field_index = self.editor.current_field(); self.editor.data_provider().field_value(field_index) } fn data_provider(&self) -> &D { self.editor.data_provider() } fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } fn set_mode(&mut self, mode: AppMode) { self.editor.set_mode(mode); // 🎯 Library automatically updates cursor if mode != AppMode::Highlight { self.exit_visual_mode(); } } // === STATUS AND DEBUG === fn set_debug_message(&mut self, msg: String) { self.debug_message = msg; } fn debug_message(&self) -> &str { &self.debug_message } fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } } // =================================================================== // PRODUCTION DATA MODEL - Copy this pattern for your application // =================================================================== struct ApplicationData { fields: Vec<(String, String)>, } impl ApplicationData { fn new() -> Self { Self { fields: vec![ ("🍎 Favorite Fruit".to_string(), "".to_string()), ("πŸ’Ό Job Role".to_string(), "".to_string()), ("πŸ’» Programming Language".to_string(), "".to_string()), ("🌍 Country".to_string(), "".to_string()), ("🎨 Favorite Color".to_string(), "".to_string()), ], } } } impl DataProvider for ApplicationData { 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_suggestions(&self, field_index: usize) -> bool { // Configure which fields support suggestions field_index < 5 } fn display_value(&self, _index: usize) -> Option<&str> { None } } // =================================================================== // PRODUCTION SUGGESTIONS PROVIDER - Copy this pattern for your APIs // =================================================================== /// Production-ready suggestions provider /// /// Replace the data sources below with your actual: /// - REST API calls (reqwest, hyper) /// - Database queries (sqlx, diesel) /// - Search engines (elasticsearch, algolia) /// - Cache lookups (redis, memcached) /// - GraphQL queries /// - gRPC services /// /// The non-blocking architecture works with any async data source. struct ProductionSuggestionsProvider { // Add your API clients, database connections, cache clients here // Example: // api_client: reqwest::Client, // db_pool: sqlx::PgPool, // cache: redis::Client, } impl ProductionSuggestionsProvider { fn new() -> Self { Self { // Initialize your clients here // api_client: reqwest::Client::new(), // db_pool: create_db_pool().await, // cache: redis::Client::open("redis://localhost").unwrap(), } } /// Get fruit suggestions (replace with your API call) async fn get_fruit_suggestions(&self, query: &str) -> Result> { // Example: Replace with actual API call // let response = self.api_client // .get(&format!("https://api.example.com/fruits?q={}", query)) // .send() // .await?; // let fruits: Vec = response.json().await?; let fruits = vec![ ("Apple", "🍎 Crisp and sweet"), ("Banana", "🍌 Rich in potassium"), ("Cherry", "πŸ’ Small and tart"), ("Date", "πŸ“… Sweet and chewy"), ("Ananas", "🍎 Crisp and sweet"), ("Elderberry", "🫐 Dark purple berry"), ("Fig", "πŸ‡ Sweet Mediterranean fruit"), ("Grape", "πŸ‡ Perfect for wine"), ("Honeydew", "🍈 Sweet melon"), ("avocado", "🍎 Crisp and sweet"), ]; Ok(self.filter_suggestions(fruits, query)) } /// Get job suggestions (replace with your database query) async fn get_job_suggestions(&self, query: &str) -> Result> { // Example: Replace with actual database query // let jobs = sqlx::query_as!( // JobRow, // "SELECT title, description FROM jobs WHERE title ILIKE $1 LIMIT 10", // format!("%{}%", query) // ) // .fetch_all(&self.db_pool) // .await?; let jobs = vec![ ("Software Engineer", "πŸ‘¨β€πŸ’» Build applications"), ("Product Manager", "πŸ“‹ Manage product roadmap"), ("Data Scientist", "πŸ“Š Analyze data patterns"), ("UX Designer", "🎨 Design user experiences"), ("DevOps Engineer", "βš™οΈ Manage infrastructure"), ("Marketing Manager", "πŸ“’ Drive growth"), ("Sales Representative", "πŸ’° Generate revenue"), ("Accountant", "πŸ’Ό Manage finances"), ]; Ok(self.filter_suggestions(jobs, query)) } /// Get language suggestions (replace with your cache lookup) async fn get_language_suggestions(&self, query: &str) -> Result> { // Example: Replace with cache lookup + fallback to API // let cached = self.cache.get(&format!("langs:{}", query)).await?; // if let Some(cached_result) = cached { // return Ok(serde_json::from_str(&cached_result)?); // } let languages = vec![ ("Rust", "πŸ¦€ Systems programming"), ("Python", "🐍 Versatile and popular"), ("JavaScript", "⚑ Web development"), ("TypeScript", "πŸ”· Typed JavaScript"), ("Go", "πŸƒ Fast and simple"), ("Java", "β˜• Enterprise favorite"), ("C++", "⚑ High performance"), ("Swift", "🍎 iOS development"), ]; Ok(self.filter_suggestions(languages, query)) } /// Get country suggestions (replace with your geographic API) async fn get_country_suggestions(&self, query: &str) -> Result> { // Example: Replace with geographic API call // let response = self.api_client // .get(&format!("https://restcountries.com/v3.1/name/{}", query)) // .send() // .await?; // let countries: Vec = response.json().await?; let countries = vec![ ("United States", "πŸ‡ΊπŸ‡Έ North America"), ("Canada", "πŸ‡¨πŸ‡¦ Great neighbors"), ("United Kingdom", "πŸ‡¬πŸ‡§ Tea and crumpets"), ("Germany", "πŸ‡©πŸ‡ͺ Engineering excellence"), ("France", "πŸ‡«πŸ‡· Art and cuisine"), ("Japan", "πŸ‡―πŸ‡΅ Technology hub"), ("Australia", "πŸ‡¦πŸ‡Ί Down under"), ("Brazil", "πŸ‡§πŸ‡· Carnival country"), ]; Ok(self.filter_suggestions(countries, query)) } /// Get color suggestions (local data) async fn get_color_suggestions(&self, query: &str) -> Result> { let colors = vec![ ("Red", "πŸ”΄ Bold and energetic"), ("Blue", "πŸ”΅ Calm and trustworthy"), ("Green", "🟒 Natural and fresh"), ("Yellow", "🟑 Bright and cheerful"), ("Purple", "🟣 Royal and mysterious"), ("Orange", "🟠 Warm and vibrant"), ("Pink", "🩷 Soft and gentle"), ("Black", "⚫ Classic and elegant"), ]; Ok(self.filter_suggestions(colors, query)) } /// Generic filtering helper (reusable for any data source) fn filter_suggestions(&self, items: Vec<(&str, &str)>, query: &str) -> Vec { let query_lower = query.to_lowercase(); items.iter() .filter(|(item, _)| { query.is_empty() || item.to_lowercase().starts_with(&query_lower) }) .map(|(item, description)| SuggestionItem { display_text: format!("{item} - {description}"), value_to_store: item.to_string(), }) .collect() } } #[async_trait] impl SuggestionsProvider for ProductionSuggestionsProvider { /// Main suggestions entry point - route to appropriate data source async fn fetch_suggestions(&mut self, field_index: usize, query: &str) -> Result> { match field_index { 0 => self.get_fruit_suggestions(query).await, // API call 1 => self.get_job_suggestions(query).await, // Database query 2 => self.get_language_suggestions(query).await, // Cache + API 3 => self.get_country_suggestions(query).await, // Geographic API 4 => self.get_color_suggestions(query).await, // Local data _ => Ok(Vec::new()), } } } /// Production-ready key handling with Tab-triggered suggestions async fn handle_key_press( key: KeyCode, modifiers: KeyModifiers, editor: &mut AutoCursorFormEditor, suggestions_provider: &mut ProductionSuggestionsProvider, ) -> anyhow::Result { let mode = editor.mode(); // Quit handling if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) || key == KeyCode::F(10) { return Ok(false); } match (mode, key, modifiers) { // === TAB-TRIGGERED SUGGESTIONS HANDLING === (_, KeyCode::Tab, _) => { if editor.is_suggestions_active() { // Cycle through suggestions editor.suggestions_next(); editor.set_debug_message("πŸ“ Next suggestion".to_string()); } else if editor.data_provider().supports_suggestions(editor.current_field()) { // Trigger non-blocking suggestions let field_index = editor.current_field(); editor.trigger_suggestions_async(suggestions_provider, field_index).await; } else { editor.next_field(); editor.set_debug_message("Tab: next field".to_string()); } } // Enter: Apply suggestion or move to next field (_, KeyCode::Enter, _) => { if editor.is_suggestions_active() { if let Some(applied) = editor.apply_suggestion() { editor.set_debug_message(format!("βœ… Selected: {applied}")); } else { editor.set_debug_message("❌ No suggestion selected".to_string()); } } else { editor.next_field(); let field_names = ["Fruit", "Job", "Language", "Country", "Color"]; let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field"); editor.set_debug_message(format!("Enter: moved to {field_name} field")); } } // Escape: Close suggestions or exit mode (_, KeyCode::Esc, _) => { if editor.is_suggestions_active() { editor.close_suggestions(); editor.set_debug_message("❌ Suggestions closed".to_string()); } else { match mode { AppMode::Edit => { editor.exit_edit_mode(); } AppMode::Highlight => { editor.exit_visual_mode(); } _ => { editor.clear_command_buffer(); } } } } // === MODE TRANSITIONS (NO AUTO-SUGGESTIONS) === (AppMode::ReadOnly, KeyCode::Char('i'), _) => { editor.enter_edit_mode(); editor.clear_command_buffer(); // For auto-suggestions on insert: add `editor.auto_trigger_suggestions(suggestions_provider).await;` } (AppMode::ReadOnly, KeyCode::Char('a'), _) => { editor.enter_append_mode(); editor.set_debug_message("✏️ INSERT (append) - Cursor: Steady Bar | - Press Tab for suggestions".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) - Cursor: Steady Bar | - Press Tab for suggestions".to_string()); editor.clear_command_buffer(); } // From Normal Mode: Enter visual modes (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(); } // === 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 === // Basic movement (hjkl and arrows) (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('h'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => { editor.move_left(); editor.set_debug_message("← 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(); let field_names = ["Fruit", "Job", "Language", "Country", "Color"]; let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field"); editor.set_debug_message(format!("↓ moved to {field_name} field")); editor.clear_command_buffer(); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => { editor.move_up(); let field_names = ["Fruit", "Job", "Language", "Country", "Color"]; let field_name = field_names.get(editor.current_field()).unwrap_or(&"Field"); editor.set_debug_message(format!("↑ moved to {field_name} field")); editor.clear_command_buffer(); } // Word movement (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(); } // Line movement (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('0'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Home, _) => { editor.move_line_start(); editor.set_debug_message("0: line start".to_string()); } (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('$'), _) | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::End, _) => { editor.move_line_end(); editor.set_debug_message("$: line end".to_string()); } // Document movement (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => { if editor.get_command_buffer() == "g" { editor.move_first_line(); editor.set_debug_message("gg: first field (Fruit)".to_string()); editor.clear_command_buffer(); } else { 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 (Color)".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(); } (AppMode::Edit, KeyCode::Right, _) => { editor.move_right(); } (AppMode::Edit, KeyCode::Up, _) => { editor.move_up(); } (AppMode::Edit, KeyCode::Down, _) => { editor.move_down(); } (AppMode::Edit, KeyCode::Home, _) => { editor.move_line_start(); } (AppMode::Edit, KeyCode::End, _) => { editor.move_line_end(); } // === DELETE OPERATIONS (AUTO-FETCH WHEN SUGGESTIONS ACTIVE) === (AppMode::Edit, KeyCode::Backspace, _) => { editor.delete_backward()?; // Auto-fetch only if suggestions are already active (triggered by Tab) // For full auto-triggering: remove the `if` check below if editor.is_suggestions_active() { let field_index = editor.current_field(); editor.trigger_suggestions_async(suggestions_provider, field_index).await; } } (AppMode::Edit, KeyCode::Delete, _) => { editor.delete_forward()?; // Auto-fetch only if suggestions are already active (triggered by Tab) // For full auto-triggering: remove the `if` check below if editor.is_suggestions_active() { let field_index = editor.current_field(); editor.trigger_suggestions_async(suggestions_provider, field_index).await; } } // Delete operations in normal mode (vim x) (AppMode::ReadOnly, KeyCode::Char('x'), _) => { editor.delete_forward()?; editor.set_debug_message("x: deleted character".to_string()); } (AppMode::ReadOnly, KeyCode::Char('X'), _) => { editor.delete_backward()?; editor.set_debug_message("X: deleted character backward".to_string()); } // === CHARACTER INPUT (AUTO-FETCH WHEN SUGGESTIONS ACTIVE) === (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { editor.insert_char(c)?; // Auto-fetch only if suggestions are already active (triggered by Tab) // For full auto-triggering: remove the `if` check below if editor.is_suggestions_active() { let field_index = editor.current_field(); editor.trigger_suggestions_async(suggestions_provider, field_index).await; } } // === DEBUG/INFO COMMANDS === (AppMode::ReadOnly, KeyCode::Char('?'), _) => { let field_names = ["Fruit🍎", "JobπŸ’Ό", "LanguageπŸ’»", "Country🌍", "Color🎨"]; let current_field_name = field_names.get(editor.current_field()).unwrap_or(&"Unknown"); editor.set_debug_message(format!( "Field: {} ({}/{}), Pos: {}, Mode: {:?}", current_field_name, editor.current_field() + 1, editor.data_provider().field_count(), editor.cursor_position(), editor.mode() )); } _ => { if editor.has_pending_command() { editor.clear_command_buffer(); editor.set_debug_message("Invalid command sequence".to_string()); } else { let field_names = ["Fruit", "Job", "Language", "Country", "Color"]; let current_field = field_names.get(editor.current_field()).unwrap_or(&"Field"); editor.set_debug_message(format!( "{current_field} field - Try: i=insert, Tab=suggestions, j/k=move. Key: {key:?}" )); } } } Ok(true) } async fn run_app( terminal: &mut Terminal, mut editor: AutoCursorFormEditor, ) -> io::Result<()> { let mut suggestions_provider = ProductionSuggestionsProvider::new(); loop { terminal.draw(|f| ui(f, &editor))?; if let Event::Key(key) = event::read()? { match handle_key_press(key.code, key.modifiers, &mut editor, &mut suggestions_provider).await { Ok(should_continue) => { if !should_continue { break; } } Err(e) => { editor.set_debug_message(format!("Error: {e}")); } } } } Ok(()) } fn ui(f: &mut Frame, editor: &AutoCursorFormEditor) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(8), Constraint::Length(12)]) .split(f.area()); let active_field_rect = render_enhanced_canvas(f, chunks[0], editor); // Render suggestions dropdown if active if let Some(input_rect) = active_field_rect { render_suggestions_dropdown( f, chunks[0], input_rect, &canvas::canvas::theme::DefaultCanvasTheme, &editor.editor, ); } render_status_and_help(f, chunks[1], editor); } fn render_enhanced_canvas( f: &mut Frame, area: ratatui::layout::Rect, editor: &AutoCursorFormEditor, ) -> Option { render_canvas_default(f, area, &editor.editor) } fn render_status_and_help( f: &mut Frame, area: ratatui::layout::Rect, editor: &AutoCursorFormEditor, ) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Length(9)]) .split(area); // Status bar with current field and cursor information let field_names = ["Fruit🍎", "JobπŸ’Ό", "LanguageπŸ’»", "Country🌍", "Color🎨"]; let current_field_name = field_names.get(editor.current_field()).unwrap_or(&"Unknown"); 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 suggestions_info = if editor.is_suggestions_active() { if editor.editor.ui_state().is_suggestions_loading() { " | ⏳ Loading suggestions...".to_string() } else if !editor.suggestions().is_empty() { format!(" | ✨ {} suggestions", editor.suggestions().len()) } else { " | πŸ” No matches".to_string() } } else { "".to_string() }; let status_text = format!( "-- {} -- {} | Field: {}{}", mode_text, editor.debug_message(), current_field_name, suggestions_info ); let status = Paragraph::new(Line::from(Span::raw(status_text))) .block(Block::default().borders(Borders::ALL).title("πŸš€ Production-Ready Smart Suggestions (Tab to activate β†’ type to filter)")); f.render_widget(status, chunks[0]); // Production help text let help_text = match editor.mode() { AppMode::ReadOnly => { "πŸš€ PRODUCTION-READY SUGGESTIONS: Copy this architecture for your app!\n\ Movement: j/k or ↑↓=fields, h/l or ←→=chars, gg/G=first/last, w/b/e=words\n\ Actions: i/a/A=insert, v/V=visual, x/X=delete, ?=info, Enter=next field\n\ Integration: Replace data sources with your APIs, databases, caches\n\ Architecture: Non-blocking β€’ Instant UI β€’ Stale protection β€’ Professional UX\n\ πŸ”‘ Tab=activate suggestions β†’ type to filter β€’ Enter=select β€’ Ready for: REST, GraphQL, SQL, Redis" } AppMode::Edit => { "πŸš€ INSERT MODE - Press Tab to activate suggestions, then type to filter!\n\ Tab=activate suggestions β€’ Type/Backspace=filter while active β€’ Enter=select\n\ Perfect for: Autocomplete, search dropdowns, data entry assistance\n\ Navigation: arrows=move, Ctrl+arrows=words, Home/End=line edges\n\ Copy this pattern for production: API calls, database queries, cache lookups" } AppMode::Highlight => { "πŸš€ VISUAL MODE - Selection with suggestions support\n\ Selection: hjkl/arrows=extend, w/b/e=word selection, Esc=normal\n\ Professional editor experience with Tab-triggered autocomplete!" } _ => "πŸš€ Copy this suggestions architecture for your production app!" }; let help = Paragraph::new(help_text) .block(Block::default().borders(Borders::ALL).title("πŸ“‹ Production Integration Guide")) .style(Style::default().fg(Color::Gray)); f.render_widget(help, chunks[1]); } #[tokio::main] async fn main() -> Result<(), Box> { // Print production-ready information println!("πŸš€ Production-Ready Tab-Triggered Suggestions Demo"); println!("βœ… Press Tab to activate suggestions, then type to filter in real-time"); println!("βœ… Professional autocomplete architecture"); println!("βœ… Copy this pattern for your production application!"); println!(); println!("πŸ—οΈ Integration Ready For:"); println!(" πŸ“‘ REST APIs (reqwest, hyper)"); println!(" πŸ—„οΈ Databases (sqlx, diesel, mongodb)"); println!(" πŸ” Search Engines (elasticsearch, algolia, typesense)"); println!(" πŸ’Ύ Caches (redis, memcached)"); println!(" 🌐 GraphQL APIs"); println!(" πŸ”— gRPC Services"); println!(); println!("⚑ Key Features:"); println!(" β€’ Press Tab to activate suggestions dropdown"); println!(" β€’ Real-time filtering while suggestions are active"); println!(" β€’ Built-in stale result protection"); println!(" β€’ Tab cycles through suggestions"); println!(" β€’ Professional-grade user experience"); println!(" β€’ Easy to integrate with any async data source"); 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 data = ApplicationData::new(); let mut editor = AutoCursorFormEditor::new(data); // Initialize with normal mode - library automatically sets block cursor editor.set_mode(AppMode::ReadOnly); // Demonstrate that CursorManager is available and working CursorManager::update_for_mode(AppMode::ReadOnly)?; let res = run_app(&mut terminal, editor).await; // Library automatically resets cursor on FormEditor::drop() // But we can also manually reset if needed CursorManager::reset()?; disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{err:?}"); } println!("πŸš€ Ready to integrate this architecture into your production app!"); Ok(()) }