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));
}
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 => {

View File

@@ -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<canvas::AppMode> 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
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,
&current_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,
&current_dir,
current_fps,
&app_state,
);
})
.context("Terminal draw call failed")?;
needs_redraw = false;
}