working general mode only with canvas, removing highlight, readonly or edit

This commit is contained in:
filipriec
2025-08-23 23:34:14 +02:00
parent 88a4b2d69c
commit 06cc1663b3
6 changed files with 112 additions and 259 deletions

View File

@@ -318,7 +318,7 @@ impl EventHandler {
return Ok(EventOutcome::Ok(message)); 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( if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.global, &config.keybindings.global,
key_code, key_code,
@@ -352,9 +352,7 @@ impl EventHandler {
} }
} }
if let Some(action) = if let Some(action) = config.get_general_action(key_code, modifiers) {
config.get_general_action(key_code, modifiers)
{
if action == "open_search" { if action == "open_search" {
if let Page::Form(_) = &router.current { if let Page::Form(_) = &router.current {
if let Some(table_name) = if let Some(table_name) =
@@ -520,143 +518,35 @@ impl EventHandler {
} }
} }
AppMode::ReadOnly => { AppMode::General => {
// First let the canvas editor try to handle the key match &router.current {
if let Page::Form(_) = &router.current { Page::Form(_)
if let Some(editor) = &mut app_state.form_editor { | Page::Login(_)
let outcome = editor.handle_key_event(key_event); | Page::Register(_)
let new_mode = AppMode::from(editor.mode()); | Page::AddTable(_)
match outcome { | Page::AddLogic(_) => {
KeyEventOutcome::Consumed(Some(msg)) => { if !app_state.ui.focus_outside_canvas {
app_state.update_mode(new_mode); if let Some(editor) = &mut app_state.form_editor {
return Ok(EventOutcome::Ok(msg)); editor.set_keymap(config.build_canvas_keymap());
} match editor.handle_key_event(key_event) {
KeyEventOutcome::Consumed(None) => { KeyEventOutcome::Consumed(Some(msg)) => {
app_state.update_mode(new_mode); return Ok(EventOutcome::Ok(msg));
return Ok(EventOutcome::Ok(String::new())); }
} KeyEventOutcome::Consumed(None) => {
KeyEventOutcome::Pending => { return Ok(EventOutcome::Ok(String::new()));
app_state.update_mode(new_mode); }
return Ok(EventOutcome::Ok(String::new())); KeyEventOutcome::Pending => {
} return Ok(EventOutcome::Ok(String::new()));
KeyEventOutcome::NotMatched => { }
app_state.update_mode(new_mode); KeyEventOutcome::NotMatched => {
// Fall through // 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 => { AppMode::Command => {

View File

@@ -1,34 +1,27 @@
// src/modes/handlers/mode_manager.rs // src/modes/handlers/mode_manager.rs
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::modes::handlers::event::EventHandler; use crate::modes::handlers::event::EventHandler;
use crate::state::pages::add_logic::AddLogicFocus;
use crate::pages::routing::{Router, Page}; use crate::pages::routing::{Router, Page};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode { pub enum AppMode {
General, // For intro and admin screens /// General mode = when focus is outside any canvas
ReadOnly, // Canvas read-only mode /// (Intro, Admin, Login/Register buttons, AddTable/AddLogic menus, dialogs, etc.)
Edit, // Canvas edit mode General,
Highlight, // Canvas highlight/visual mode
Command, // Command mode overlay
}
impl From<canvas::AppMode> for AppMode { /// Command overlay (":" or "ctrl+;"), available globally
fn from(mode: canvas::AppMode) -> Self { Command,
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,
}
}
} }
pub struct ModeManager; pub struct ModeManager;
impl 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( pub fn derive_mode(
app_state: &AppState, app_state: &AppState,
event_handler: &EventHandler, event_handler: &EventHandler,
@@ -39,76 +32,28 @@ impl ModeManager {
return AppMode::General; return AppMode::General;
} }
// Explicit command mode flag // Explicit command overlay flag
if event_handler.command_mode { if event_handler.command_mode {
return AppMode::Command; 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 { match &router.current {
// --- Form view --- Page::Form(_)
Page::Form(_) if !app_state.ui.focus_outside_canvas => { | Page::Login(_)
if let Some(editor) = &app_state.form_editor { | Page::Register(_)
return AppMode::from(editor.mode()); | Page::AddTable(_)
} | Page::AddLogic(_) if !app_state.ui.focus_outside_canvas => {
// Canvas active → let canvas handle its own AppMode
AppMode::General 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, _ => AppMode::General,
} }
} }
// Mode transition rules /// Command overlay can be entered from anywhere (General or Canvas).
pub fn can_enter_command_mode(current_mode: AppMode) -> bool { pub fn can_enter_command_mode(_current_mode: AppMode) -> bool {
!matches!(current_mode, AppMode::Edit) true
}
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)
} }
} }

View File

@@ -135,7 +135,7 @@ pub fn render_login(
); );
// --- SUGGESTIONS DROPDOWN (if active) --- // --- 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 { if let Some(input_rect) = input_rect {
render_suggestions_dropdown( render_suggestions_dropdown(
f, f,

View File

@@ -134,7 +134,7 @@ pub fn render_register(
); );
// --- AUTOCOMPLETE DROPDOWN (Using new canvas suggestions) --- // --- 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 { if let Some(input_rect) = input_rect {
render_suggestions_dropdown( render_suggestions_dropdown(
f, f,

View File

@@ -5,6 +5,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo}, cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo},
}; };
use crossterm::ExecutableCommand;
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::{self, stdout, Write}; use std::io::{self, stdout, Write};
use anyhow::Result; use anyhow::Result;
@@ -81,6 +82,12 @@ impl TerminalCore {
)?; )?;
Ok(()) 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 { impl Drop for TerminalCore {

View File

@@ -29,8 +29,9 @@ use crate::ui::handlers::context::DialogPurpose;
use crate::utils::columns::filter_user_columns; use crate::utils::columns::filter_user_columns;
use canvas::keymap::KeyEventOutcome; use canvas::keymap::KeyEventOutcome;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::{SetCursorStyle, MoveTo};
use crossterm::event as crossterm_event; use crossterm::event as crossterm_event;
use crossterm::ExecutableCommand;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use std::time::Instant; use std::time::Instant;
@@ -641,53 +642,63 @@ pub async fn run_ui() -> Result<()> {
if event_processed || needs_redraw || position_changed { if event_processed || needs_redraw || position_changed {
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router); let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
match current_mode { 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 => { AppMode::General => {
if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; } if app_state.ui.focus_outside_canvas {
else { terminal.hide_cursor()?; } // 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(); 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(); let form_state_clone = app_state.form_state().unwrap().clone();
terminal.draw(|f| { terminal
// Use a mutable clone for rendering .draw(|f| {
let mut temp_form_state = form_state_clone.clone(); let mut temp_form_state = form_state_clone.clone();
render_ui( render_ui(
f, f,
&mut router, &mut router,
&buffer_state, &buffer_state,
&theme, &theme,
event_handler.is_edit_mode, event_handler.is_edit_mode,
&event_handler.command_input, &event_handler.command_input,
event_handler.command_mode, event_handler.command_mode,
&event_handler.command_message, &event_handler.command_message,
&event_handler.navigation_state, &event_handler.navigation_state,
&current_dir, &current_dir,
current_fps, current_fps,
&app_state, &app_state,
); );
})
// If render_ui modified the form_state, we'd need to sync it back .context("Terminal draw call failed")?;
// But typically render functions don't modify state, just read it
}).context("Terminal draw call failed")?;
needs_redraw = false; needs_redraw = false;
} }