diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 9533bf3..5c723a7 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -318,7 +318,7 @@ impl EventHandler { return Ok(EventOutcome::Ok(message)); } - if !matches!(current_mode, AppMode::Edit | AppMode::Command) { + if current_mode == AppMode::General { if let Some(action) = config.get_action_for_key_in_mode( &config.keybindings.global, key_code, @@ -352,9 +352,7 @@ impl EventHandler { } } - if let Some(action) = - config.get_general_action(key_code, modifiers) - { + if let Some(action) = config.get_general_action(key_code, modifiers) { if action == "open_search" { if let Page::Form(_) = &router.current { if let Some(table_name) = @@ -520,143 +518,35 @@ impl EventHandler { } } - AppMode::ReadOnly => { - // First let the canvas editor try to handle the key - if let Page::Form(_) = &router.current { - if let Some(editor) = &mut app_state.form_editor { - let outcome = editor.handle_key_event(key_event); - let new_mode = AppMode::from(editor.mode()); - match outcome { - KeyEventOutcome::Consumed(Some(msg)) => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(msg)); - } - KeyEventOutcome::Consumed(None) => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(String::new())); - } - KeyEventOutcome::Pending => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(String::new())); - } - KeyEventOutcome::NotMatched => { - app_state.update_mode(new_mode); - // Fall through + AppMode::General => { + match &router.current { + Page::Form(_) + | Page::Login(_) + | Page::Register(_) + | Page::AddTable(_) + | Page::AddLogic(_) => { + if !app_state.ui.focus_outside_canvas { + if let Some(editor) = &mut app_state.form_editor { + editor.set_keymap(config.build_canvas_keymap()); + match editor.handle_key_event(key_event) { + KeyEventOutcome::Consumed(Some(msg)) => { + return Ok(EventOutcome::Ok(msg)); + } + KeyEventOutcome::Consumed(None) => { + return Ok(EventOutcome::Ok(String::new())); + } + KeyEventOutcome::Pending => { + return Ok(EventOutcome::Ok(String::new())); + } + KeyEventOutcome::NotMatched => { + // fall through to client actions + } + } + } } } - } + _ => {} } - - // Entering command mode is still a client-level action - if config.get_app_action(key_code, modifiers) == Some("enter_command_mode") - && ModeManager::can_enter_command_mode(current_mode) - { - if let Some(editor) = &mut app_state.form_editor { - editor.set_mode(CanvasMode::Command); - } - self.command_mode = true; - self.command_input.clear(); - self.command_message.clear(); - return Ok(EventOutcome::Ok(String::new())); - } - - // Handle common actions (save, quit, etc.) - if let Some(action) = config.get_app_action(key_code, modifiers) { - match action { - "save" | "force_quit" | "save_and_quit" | "revert" => { - return self - .handle_core_action( - action, - auth_state, - terminal, - app_state, - router, - ) - .await; - } - _ => {} - } - } - - return Ok(EventOutcome::Ok(self.command_message.clone())); - } - - AppMode::Highlight => { - if let Page::Form(_) = &router.current { - if let Some(editor) = &mut app_state.form_editor { - let outcome = editor.handle_key_event(key_event); - let new_mode = AppMode::from(editor.mode()); - match outcome { - KeyEventOutcome::Consumed(Some(msg)) => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(msg)); - } - KeyEventOutcome::Consumed(None) => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(String::new())); - } - KeyEventOutcome::Pending => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(String::new())); - } - KeyEventOutcome::NotMatched => { - app_state.update_mode(new_mode); - // Fall through - } - } - } - } - - return Ok(EventOutcome::Ok(self.command_message.clone())); - } - - AppMode::Edit => { - // Handle common actions (save, quit, etc.) - if let Some(action) = config.get_app_action(key_code, modifiers) { - match action { - "save" | "force_quit" | "save_and_quit" | "revert" => { - return self - .handle_core_action( - action, - auth_state, - terminal, - app_state, - router, - ) - .await; - } - _ => {} - } - } - - // Let the canvas editor handle edit-mode keys - if let Page::Form(_) = &router.current { - if let Some(editor) = &mut app_state.form_editor { - let outcome = editor.handle_key_event(key_event); - let new_mode = AppMode::from(editor.mode()); - match outcome { - KeyEventOutcome::Consumed(Some(msg)) => { - self.command_message = msg.clone(); - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(msg)); - } - KeyEventOutcome::Consumed(None) => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(String::new())); - } - KeyEventOutcome::Pending => { - app_state.update_mode(new_mode); - return Ok(EventOutcome::Ok(String::new())); - } - KeyEventOutcome::NotMatched => { - app_state.update_mode(new_mode); - // Fall through - } - } - } - } - - return Ok(EventOutcome::Ok(self.command_message.clone())); } AppMode::Command => { diff --git a/client/src/modes/handlers/mode_manager.rs b/client/src/modes/handlers/mode_manager.rs index fb9bf62..8fc0716 100644 --- a/client/src/modes/handlers/mode_manager.rs +++ b/client/src/modes/handlers/mode_manager.rs @@ -1,34 +1,27 @@ // src/modes/handlers/mode_manager.rs + use crate::state::app::state::AppState; use crate::modes::handlers::event::EventHandler; -use crate::state::pages::add_logic::AddLogicFocus; use crate::pages::routing::{Router, Page}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppMode { - General, // For intro and admin screens - ReadOnly, // Canvas read-only mode - Edit, // Canvas edit mode - Highlight, // Canvas highlight/visual mode - Command, // Command mode overlay -} + /// General mode = when focus is outside any canvas + /// (Intro, Admin, Login/Register buttons, AddTable/AddLogic menus, dialogs, etc.) + General, -impl From for AppMode { - fn from(mode: canvas::AppMode) -> Self { - match mode { - canvas::AppMode::General => AppMode::General, - canvas::AppMode::ReadOnly => AppMode::ReadOnly, - canvas::AppMode::Edit => AppMode::Edit, - canvas::AppMode::Highlight => AppMode::Highlight, - canvas::AppMode::Command => AppMode::Command, - } - } + /// Command overlay (":" or "ctrl+;"), available globally + Command, } pub struct ModeManager; impl ModeManager { - /// Determine current mode based on app state + router + /// Determine current mode: + /// - If navigation palette is active → General + /// - If command overlay is active → Command + /// - If focus is inside a canvas (Form, Login, Register, AddTable, AddLogic) → let canvas handle its own mode + /// - Otherwise → General pub fn derive_mode( app_state: &AppState, event_handler: &EventHandler, @@ -39,76 +32,28 @@ impl ModeManager { return AppMode::General; } - // Explicit command mode flag + // Explicit command overlay flag if event_handler.command_mode { return AppMode::Command; } + // If focus is inside a canvas, we don't duplicate canvas modes here. + // Canvas crate owns ReadOnly/Edit/Highlight internally. match &router.current { - // --- Form view --- - Page::Form(_) if !app_state.ui.focus_outside_canvas => { - if let Some(editor) = &app_state.form_editor { - return AppMode::from(editor.mode()); - } + Page::Form(_) + | Page::Login(_) + | Page::Register(_) + | Page::AddTable(_) + | Page::AddLogic(_) if !app_state.ui.focus_outside_canvas => { + // Canvas active → let canvas handle its own AppMode AppMode::General } - - // --- AddLogic view --- - Page::AddLogic(state) => match state.current_focus { - AddLogicFocus::InputLogicName - | AddLogicFocus::InputTargetColumn - | AddLogicFocus::InputDescription => { - if event_handler.is_edit_mode { - AppMode::Edit - } else { - AppMode::ReadOnly - } - } - _ => AppMode::General, - }, - - // --- AddTable view --- - Page::AddTable(_) => { - if app_state.ui.focus_outside_canvas { - AppMode::General - } else if event_handler.is_edit_mode { - AppMode::Edit - } else { - AppMode::ReadOnly - } - } - - // --- Login/Register views --- - Page::Login(_) | Page::Register(_) => { - if event_handler.is_edit_mode { - AppMode::Edit - } else { - AppMode::ReadOnly - } - } - - // --- Everything else (Intro, Admin, etc.) --- _ => AppMode::General, } } - // Mode transition rules - pub fn can_enter_command_mode(current_mode: AppMode) -> bool { - !matches!(current_mode, AppMode::Edit) - } - - pub fn can_enter_edit_mode(current_mode: AppMode) -> bool { - matches!(current_mode, AppMode::ReadOnly) - } - - pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool { - matches!( - current_mode, - AppMode::Edit | AppMode::Command | AppMode::Highlight - ) - } - - pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool { - matches!(current_mode, AppMode::ReadOnly) + /// Command overlay can be entered from anywhere (General or Canvas). + pub fn can_enter_command_mode(_current_mode: AppMode) -> bool { + true } } diff --git a/client/src/pages/login/ui.rs b/client/src/pages/login/ui.rs index 2eab56c..1e40955 100644 --- a/client/src/pages/login/ui.rs +++ b/client/src/pages/login/ui.rs @@ -135,7 +135,7 @@ pub fn render_login( ); // --- SUGGESTIONS DROPDOWN (if active) --- - if app_state.current_mode == crate::modes::handlers::mode_manager::AppMode::Edit { + if editor.mode() == canvas::AppMode::Edit { if let Some(input_rect) = input_rect { render_suggestions_dropdown( f, diff --git a/client/src/pages/register/ui.rs b/client/src/pages/register/ui.rs index ee25cb7..173a89f 100644 --- a/client/src/pages/register/ui.rs +++ b/client/src/pages/register/ui.rs @@ -134,7 +134,7 @@ pub fn render_register( ); // --- AUTOCOMPLETE DROPDOWN (Using new canvas suggestions) --- - if app_state.current_mode == AppMode::Edit { + if editor.mode() == canvas::AppMode::Edit { if let Some(input_rect) = input_rect { render_suggestions_dropdown( f, diff --git a/client/src/tui/terminal/core.rs b/client/src/tui/terminal/core.rs index 599a2f2..8ff76e2 100644 --- a/client/src/tui/terminal/core.rs +++ b/client/src/tui/terminal/core.rs @@ -5,6 +5,7 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo}, }; +use crossterm::ExecutableCommand; use ratatui::{backend::CrosstermBackend, Terminal}; use std::io::{self, stdout, Write}; use anyhow::Result; @@ -81,6 +82,12 @@ impl TerminalCore { )?; Ok(()) } + + /// Move the cursor to a specific (x, y) position on screen. + pub fn set_cursor_position(&mut self, x: u16, y: u16) -> io::Result<()> { + self.terminal.backend_mut().execute(MoveTo(x, y))?; + Ok(()) + } } impl Drop for TerminalCore { diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 7af117d..a9bbd55 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -29,8 +29,9 @@ use crate::ui::handlers::context::DialogPurpose; use crate::utils::columns::filter_user_columns; use canvas::keymap::KeyEventOutcome; use anyhow::{Context, Result}; -use crossterm::cursor::SetCursorStyle; +use crossterm::cursor::{SetCursorStyle, MoveTo}; use crossterm::event as crossterm_event; +use crossterm::ExecutableCommand; use tracing::{error, info, warn}; use tokio::sync::mpsc; use std::time::Instant; @@ -641,53 +642,63 @@ pub async fn run_ui() -> Result<()> { if event_processed || needs_redraw || position_changed { let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router); + match current_mode { - AppMode::Edit => { terminal.show_cursor()?; } - AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; } - AppMode::ReadOnly => { - if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; } - else { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; } - terminal.show_cursor().context("Failed to show cursor in ReadOnly mode")?; - } AppMode::General => { - if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; } - else { terminal.hide_cursor()?; } + if app_state.ui.focus_outside_canvas { + // General mode, focus outside canvas but canvas exists + if let Some(editor) = &app_state.form_editor { + // Get last known cursor position from canvas + let x = editor.cursor_position() as u16; + let y = editor.current_field() as u16; + + // Force underscore cursor at that position + terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; + terminal.show_cursor()?; + + // Move cursor to last known canvas position + terminal.set_cursor_position(x, y)?; + } else { + // No canvas at all → hide cursor + terminal.hide_cursor()?; + } + } else { + // General mode, focus inside canvas → let canvas handle cursor + // Do nothing here + } } - AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; } + AppMode::Command => { + // Command line overlay → always steady block + terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; + terminal + .show_cursor() + .context("Failed to show cursor in Command mode")?; + } } - // Temporarily work around borrow checker by extracting needed values + // Workaround for borrow checker let current_dir = app_state.current_dir.clone(); - - // Since we can't borrow app_state both mutably and immutably, - // we'll need to either: - // 1. Modify render_ui to take just app_state and access form_state internally, OR - // 2. Extract the specific fields render_ui needs from app_state - - // For now, using approach where we temporarily clone what we need let form_state_clone = app_state.form_state().unwrap().clone(); - - terminal.draw(|f| { - // Use a mutable clone for rendering - let mut temp_form_state = form_state_clone.clone(); - render_ui( - f, - &mut router, - &buffer_state, - &theme, - event_handler.is_edit_mode, - &event_handler.command_input, - event_handler.command_mode, - &event_handler.command_message, - &event_handler.navigation_state, - ¤t_dir, - current_fps, - &app_state, - ); - - // If render_ui modified the form_state, we'd need to sync it back - // But typically render functions don't modify state, just read it - }).context("Terminal draw call failed")?; + + terminal + .draw(|f| { + let mut temp_form_state = form_state_clone.clone(); + render_ui( + f, + &mut router, + &buffer_state, + &theme, + event_handler.is_edit_mode, + &event_handler.command_input, + event_handler.command_mode, + &event_handler.command_message, + &event_handler.navigation_state, + ¤t_dir, + current_fps, + &app_state, + ); + }) + .context("Terminal draw call failed")?; needs_redraw = false; }