inputs from keyboard are now decoupled

This commit is contained in:
Priec
2025-09-10 22:12:22 +02:00
parent 2cbbfd21aa
commit a604d62d44
6 changed files with 460 additions and 298 deletions

View File

@@ -2,10 +2,14 @@
[keybindings] [keybindings]
enter_command_mode = [":", "ctrl+;"] enter_command_mode = [":", "ctrl+;"]
next_buffer = ["space+b+n"] next_buffer = ["ctrl+b+n"]
previous_buffer = ["space+b+p"] previous_buffer = ["ctrl+b+p"]
close_buffer = ["space+b+d"] close_buffer = ["ctrl+b+d"]
revert = ["space+b+r"] # SPACE NOT WORKING, NEEDS REDESIGN
# next_buffer = ["space+b+n"]
# previous_buffer = ["space+b+p"]
# close_buffer = ["space+b+d"]
# revert = ["space+b+r"]
[keybindings.general] [keybindings.general]
up = ["k", "Up"] up = ["k", "Up"]

View File

@@ -0,0 +1,41 @@
// src/input/action.rs
use crate::movement::MovementAction;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BufferAction {
Next,
Previous,
Close,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CoreAction {
Save,
ForceQuit,
SaveAndQuit,
Revert,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppAction {
// Global/UI
ToggleSidebar,
ToggleBufferList,
OpenSearch,
FindFilePaletteToggle,
// Buffers
Buffer(BufferAction),
// Command mode
EnterCommandMode,
ExitCommandMode,
CommandExecute,
CommandBackspace,
// Navigation across UI
Navigate(MovementAction),
// Core actions
Core(CoreAction),
}

176
client/src/input/engine.rs Normal file
View File

@@ -0,0 +1,176 @@
// src/input/engine.rs
use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::input::action::{AppAction, BufferAction, CoreAction};
use crate::movement::MovementAction;
use crate::modes::handlers::mode_manager::AppMode;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone, Copy)]
pub struct InputContext {
pub app_mode: AppMode,
pub overlay_active: bool,
pub allow_navigation_capture: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputOutcome {
Action(AppAction),
Pending, // sequence in progress
PassThrough, // let page/canvas handle it
}
pub struct InputEngine {
seq: KeySequenceTracker,
}
impl InputEngine {
pub fn new(timeout_ms: u64) -> Self {
Self {
seq: KeySequenceTracker::new(timeout_ms),
}
}
pub fn reset_sequence(&mut self) {
self.seq.reset();
}
pub fn process_key(
&mut self,
key_event: KeyEvent,
ctx: &InputContext,
config: &Config,
) -> InputOutcome {
// Command mode keys are special (exit/execute/backspace) and typed chars
if ctx.app_mode == AppMode::Command {
if config.is_exit_command_mode(key_event.code, key_event.modifiers) {
self.seq.reset();
return InputOutcome::Action(AppAction::ExitCommandMode);
}
if config.is_command_execute(key_event.code, key_event.modifiers) {
self.seq.reset();
return InputOutcome::Action(AppAction::CommandExecute);
}
if config.is_command_backspace(key_event.code, key_event.modifiers) {
self.seq.reset();
return InputOutcome::Action(AppAction::CommandBackspace);
}
// Let command-line collect characters and other keys pass through
self.seq.reset();
return InputOutcome::PassThrough;
}
// If overlays are active, do not intercept (palette, navigation, etc.)
if ctx.overlay_active {
self.seq.reset();
return InputOutcome::PassThrough;
}
// Space-led multi-key sequences (leader = space)
if ctx.allow_navigation_capture {
let space = KeyCode::Char(' ');
let seq_active = !self.seq.current_sequence.is_empty()
&& self.seq.current_sequence[0] == space;
if seq_active {
self.seq.add_key(key_event.code);
let sequence = self.seq.get_sequence();
if let Some(action_str) = config.matches_key_sequence_generalized(&sequence) {
if let Some(app_action) = map_action_string(action_str, ctx) {
self.seq.reset();
return InputOutcome::Action(app_action);
}
// A non-app action sequence (canvas stuff) → pass-through
self.seq.reset();
return InputOutcome::PassThrough;
}
if config.is_key_sequence_prefix(&sequence) {
return InputOutcome::Pending;
}
// Not matched and not a prefix → reset and continue to single key
self.seq.reset();
} else if key_event.code == space && config.is_key_sequence_prefix(&[space]) {
self.seq.reset();
self.seq.add_key(space);
return InputOutcome::Pending;
}
}
// Single-key mapping: try general binds first (arrows, open_search, enter_command_mode)
if let Some(action_str) =
config.get_general_action(key_event.code, key_event.modifiers)
{
if let Some(app_action) = map_action_string(action_str, ctx) {
return InputOutcome::Action(app_action);
}
// Unknown to app layer (likely canvas movement etc.) → pass
return InputOutcome::PassThrough;
}
// Then app-level common/read-only/edit/highlight for UI toggles or core actions
if let Some(action_str) = config.get_app_action(key_event.code, key_event.modifiers) {
if let Some(app_action) = map_action_string(action_str, ctx) {
return InputOutcome::Action(app_action);
}
}
InputOutcome::PassThrough
}
/// Check if a key sequence is currently active
pub fn has_active_sequence(&self) -> bool {
!self.seq.current_sequence.is_empty()
}
}
fn str_to_movement(s: &str) -> Option<MovementAction> {
match s {
"up" => Some(MovementAction::Up),
"down" => Some(MovementAction::Down),
"left" => Some(MovementAction::Left),
"right" => Some(MovementAction::Right),
"next" => Some(MovementAction::Next),
"previous" => Some(MovementAction::Previous),
"select" => Some(MovementAction::Select),
"esc" => Some(MovementAction::Esc),
_ => None,
}
}
fn map_action_string(action: &str, ctx: &InputContext) -> Option<AppAction> {
match action {
// Global/UI
"toggle_sidebar" => Some(AppAction::ToggleSidebar),
"toggle_buffer_list" => Some(AppAction::ToggleBufferList),
"open_search" => Some(AppAction::OpenSearch),
"find_file_palette_toggle" => Some(AppAction::FindFilePaletteToggle),
// Buffers
"next_buffer" => Some(AppAction::Buffer(BufferAction::Next)),
"previous_buffer" => Some(AppAction::Buffer(BufferAction::Previous)),
"close_buffer" => Some(AppAction::Buffer(BufferAction::Close)),
// Command mode
"enter_command_mode" => Some(AppAction::EnterCommandMode),
"exit_command_mode" => Some(AppAction::ExitCommandMode),
"command_execute" => Some(AppAction::CommandExecute),
"command_backspace" => Some(AppAction::CommandBackspace),
// Navigation across UI (only if allowed)
s if str_to_movement(s).is_some() && ctx.allow_navigation_capture => {
Some(AppAction::Navigate(str_to_movement(s).unwrap()))
}
// Core actions
"save" => Some(AppAction::Core(CoreAction::Save)),
"force_quit" => Some(AppAction::Core(CoreAction::ForceQuit)),
"save_and_quit" => Some(AppAction::Core(CoreAction::SaveAndQuit)),
"revert" => Some(AppAction::Core(CoreAction::Revert)),
// Unknown to app layer: ignore (canvas-specific actions, etc.)
_ => None,
}
}

3
client/src/input/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
// src/input/mod.rs
pub mod action;
pub mod engine;

View File

@@ -14,6 +14,7 @@ pub mod search;
pub mod bottom_panel; pub mod bottom_panel;
pub mod pages; pub mod pages;
pub mod movement; pub mod movement;
pub mod input;
pub use ui::run_ui; pub use ui::run_ui;

View File

@@ -1,6 +1,7 @@
// src/modes/handlers/event.rs // src/modes/handlers/event.rs
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::KeySequenceTracker; use crate::input::engine::{InputContext, InputEngine, InputOutcome};
use crate::input::action::{AppAction, BufferAction, CoreAction};
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list}; use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
use crate::sidebar::toggle_sidebar; use crate::sidebar::toggle_sidebar;
use crate::search::event::handle_search_palette_event; use crate::search::event::handle_search_palette_event;
@@ -76,7 +77,7 @@ pub struct EventHandler {
pub command_message: String, pub command_message: String,
pub edit_mode_cooldown: bool, pub edit_mode_cooldown: bool,
pub ideal_cursor_column: usize, pub ideal_cursor_column: usize,
pub key_sequence_tracker: KeySequenceTracker, pub input_engine: InputEngine,
pub auth_client: AuthClient, pub auth_client: AuthClient,
pub grpc_client: GrpcClient, pub grpc_client: GrpcClient,
pub login_result_sender: mpsc::Sender<LoginResult>, pub login_result_sender: mpsc::Sender<LoginResult>,
@@ -106,7 +107,7 @@ impl EventHandler {
command_message: String::new(), command_message: String::new(),
edit_mode_cooldown: false, edit_mode_cooldown: false,
ideal_cursor_column: 0, ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(1200), input_engine: InputEngine::new(1200),
auth_client: AuthClient::new().await?, auth_client: AuthClient::new().await?,
grpc_client, grpc_client,
login_result_sender, login_result_sender,
@@ -301,9 +302,7 @@ impl EventHandler {
|| app_state.ui.show_search_palette || app_state.ui.show_search_palette
|| self.navigation_state.active; || self.navigation_state.active;
// --- SPACE-LED SEQUENCES INTERCEPTION (all modes except Form Edit) --- // Determine if canvas is in edit mode (we avoid capturing navigation then)
if !overlay_active {
// Detect if we are in a Form and the canvas is in Edit mode (then we don't intercept)
let in_form_edit_mode = matches!( let in_form_edit_mode = matches!(
&router.current, &router.current,
Page::Form(path) if { Page::Form(path) if {
@@ -313,20 +312,18 @@ impl EventHandler {
} }
); );
if !in_form_edit_mode { // Centralized key -> action resolution
let space = KeyCode::Char(' '); let input_ctx = InputContext {
let seq_active = !self.key_sequence_tracker.current_sequence.is_empty() app_mode: current_mode,
&& self.key_sequence_tracker.current_sequence[0] == space; overlay_active,
allow_navigation_capture: !in_form_edit_mode && !overlay_active,
if seq_active { };
self.key_sequence_tracker.add_key(key_code); match self.input_engine.process_key(key_event, &input_ctx, config) {
let sequence = self.key_sequence_tracker.get_sequence(); InputOutcome::Action(action) => {
if let Some(action) = config.matches_key_sequence_generalized(&sequence) {
self.key_sequence_tracker.reset();
if let Some(outcome) = self if let Some(outcome) = self
.dispatch_space_sequence_action( .handle_app_action(
action, action,
key_event, // pass original key
config, config,
terminal, terminal,
command_handler, command_handler,
@@ -338,26 +335,17 @@ impl EventHandler {
.await? .await?
{ {
return Ok(outcome); return Ok(outcome);
} else { }
// No early return on None (e.g., Navigate) — fall through
}
InputOutcome::Pending => {
// waiting for more keys in a sequence
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
} InputOutcome::PassThrough => {
// fall through to page/canvas handlers
if config.is_key_sequence_prefix(&sequence) {
return Ok(EventOutcome::Ok(String::new()));
}
// Not matched and not prefix → reset and fall through
self.key_sequence_tracker.reset();
} else if key_code == space && config.is_key_sequence_prefix(&[space]) {
// Start new space-led sequence and do not let the page consume the space
self.key_sequence_tracker.reset();
self.key_sequence_tracker.add_key(space);
return Ok(EventOutcome::Ok(String::new()));
} }
} }
}
// --- END SPACE-LED SEQUENCES ---
// LOGIN: canvas <-> buttons focus handoff // LOGIN: canvas <-> buttons focus handoff
// Do not let Login canvas receive keys when overlays/palettes are active // Do not let Login canvas receive keys when overlays/palettes are active
@@ -397,7 +385,7 @@ impl EventHandler {
} }
} else if let Page::Form(path) = &router.current { } else if let Page::Form(path) = &router.current {
// If a space-led sequence is in progress or has just begun, do not forward to editor // If a space-led sequence is in progress or has just begun, do not forward to editor
if self.key_sequence_tracker.current_sequence.is_empty() { if !self.input_engine.has_active_sequence() {
let outcome = forms::event::handle_form_event( let outcome = forms::event::handle_form_event(
event, event,
app_state, app_state,
@@ -424,7 +412,7 @@ impl EventHandler {
self.command_mode = true; self.command_mode = true;
self.command_input.clear(); self.command_input.clear();
self.command_message.clear(); self.command_message.clear();
self.key_sequence_tracker.reset(); self.input_engine.reset_sequence();
self.set_focus_outside(router, true); self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
@@ -470,7 +458,7 @@ impl EventHandler {
self.command_mode = true; self.command_mode = true;
self.command_input.clear(); self.command_input.clear();
self.command_message.clear(); self.command_message.clear();
self.key_sequence_tracker.reset(); self.input_engine.reset_sequence();
self.set_focus_outside(router, true); self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
@@ -526,116 +514,11 @@ impl EventHandler {
} }
} }
} }
if toggle_sidebar(
&mut app_state.ui, // Sidebar/buffer toggles now handled via AppAction in the engine
config,
key_code,
modifiers,
) {
let message = format!(
"Sidebar {}",
if app_state.ui.show_sidebar {
"shown"
} else {
"hidden"
}
);
return Ok(EventOutcome::Ok(message));
}
if toggle_buffer_list(
&mut app_state.ui,
config,
key_code,
modifiers,
) {
let message = format!(
"Buffer {}",
if app_state.ui.show_buffer_list {
"shown"
} else {
"hidden"
}
);
return Ok(EventOutcome::Ok(message));
}
if current_mode == AppMode::General { if current_mode == AppMode::General {
if let Some(action) = config.get_action_for_key_in_mode( // General mode specific key mapping now handled via AppAction
&config.keybindings.global,
key_code,
modifiers,
) {
match action {
"next_buffer" => {
if switch_buffer(buffer_state, true) {
return Ok(EventOutcome::Ok(
"Switched to next buffer".to_string(),
));
}
}
"previous_buffer" => {
if switch_buffer(buffer_state, false) {
return Ok(EventOutcome::Ok(
"Switched to previous buffer".to_string(),
));
}
}
"close_buffer" => {
let current_table_name =
app_state.current_view_table_name.as_deref();
let message = buffer_state
.close_buffer_with_intro_fallback(
current_table_name,
);
return Ok(EventOutcome::Ok(message));
}
"revert" | "save" | "force_quit" | "save_and_quit" => {
let outcome = self.handle_core_action(
action,
auth_state,
terminal,
app_state,
router,
).await?;
return Ok(outcome);
}
_ => {}
}
}
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) =
app_state.current_view_table_name.clone()
{
app_state.ui.show_search_palette = true;
app_state.search_state =
Some(SearchState::new(table_name));
self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(
"Search palette opened".to_string(),
));
}
}
}
// Allow ":" / ctrl+; to enter command mode only when outside canvas.
if action == "enter_command_mode" {
if self.is_focus_outside(router)
&& !self.command_mode
&& !app_state.ui.show_search_palette
&& !self.navigation_state.active
{
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
self.key_sequence_tracker.reset();
// Keep focus outside so canvas won't receive keys
self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new()));
}
}
}
} }
match current_mode { match current_mode {
@@ -767,81 +650,20 @@ impl EventHandler {
} }
AppMode::Command => { AppMode::Command => {
if config.is_exit_command_mode(key_code, modifiers) { // Command-mode keys already handled by the engine.
self.command_input.clear(); // Collect characters not handled (typed command input).
self.command_message.clear(); match key_code {
self.command_mode = false; KeyCode::Char(c) => {
self.key_sequence_tracker.reset();
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) {
editor.set_mode(CanvasMode::ReadOnly);
}
}
return Ok(EventOutcome::Ok(
"Exited command mode".to_string(),
));
}
if config.is_command_execute(key_code, modifiers) {
let (mut current_position, total_count) =
if let Page::Form(path) = &router.current {
if let Some(fs) =
app_state.form_state_for_path_ref(path)
{
(fs.current_position, fs.total_count)
} else {
(1, 0)
}
} else {
(1, 0)
};
let outcome = command_mode::handle_command_event(
key_event,
config,
app_state,
router,
&mut self.command_input,
&mut self.command_message,
&mut self.grpc_client,
command_handler,
terminal,
&mut current_position,
total_count,
).await?;
if let Page::Form(path) = &router.current {
if let Some(fs) = app_state.form_state_for_path(path) {
fs.current_position = current_position;
}
}
self.command_mode = false;
self.key_sequence_tracker.reset();
let new_mode = ModeManager::derive_mode(
app_state,
self,
router,
);
app_state.update_mode(new_mode);
return Ok(outcome);
}
if key_code == KeyCode::Backspace {
self.command_input.pop();
self.key_sequence_tracker.reset();
return Ok(EventOutcome::Ok(String::new()));
}
// Default command-mode handling: we do not treat Space as a leader here.
// Space-led sequences are handled only in General mode above.
if let KeyCode::Char(c) = key_code {
self.command_input.push(c); self.command_input.push(c);
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
_ => {
self.key_sequence_tracker.reset(); self.input_engine.reset_sequence();
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
} }
}
}
} else if let Event::Resize(_, _) = event { } else if let Event::Resize(_, _) = event {
return Ok(EventOutcome::Ok("Resized".to_string())); return Ok(EventOutcome::Ok("Resized".to_string()));
} }
@@ -1027,13 +849,59 @@ impl EventHandler {
_ => 0, _ => 0,
} }
} }
async fn execute_command(
&mut self,
key_event: crossterm::event::KeyEvent,
config: &Config,
terminal: &mut TerminalCore,
command_handler: &mut CommandHandler,
app_state: &mut AppState,
router: &mut Router,
) -> Result<EventOutcome> {
let (mut current_position, total_count) = if let Page::Form(path) = &router.current {
if let Some(fs) = app_state.form_state_for_path_ref(path) {
(fs.current_position, fs.total_count)
} else {
(1, 0)
}
} else {
(1, 0)
};
let outcome = command_mode::handle_command_event(
key_event,
config,
app_state,
router,
&mut self.command_input,
&mut self.command_message,
&mut self.grpc_client,
command_handler,
terminal,
&mut current_position,
total_count,
)
.await?;
if let Page::Form(path) = &router.current {
if let Some(fs) = app_state.form_state_for_path(path) {
fs.current_position = current_position;
}
}
self.command_mode = false;
self.input_engine.reset_sequence();
let new_mode = ModeManager::derive_mode(app_state, self, router);
app_state.update_mode(new_mode);
Ok(outcome)
} }
impl EventHandler {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn dispatch_space_sequence_action( async fn handle_app_action(
&mut self, &mut self,
action: &str, action: AppAction,
key_event: crossterm::event::KeyEvent,
config: &Config, config: &Config,
terminal: &mut TerminalCore, terminal: &mut TerminalCore,
command_handler: &mut CommandHandler, command_handler: &mut CommandHandler,
@@ -1043,8 +911,31 @@ impl EventHandler {
router: &mut Router, router: &mut Router,
) -> Result<Option<EventOutcome>> { ) -> Result<Option<EventOutcome>> {
match action { match action {
// Reuse existing behavior AppAction::ToggleSidebar => {
"next_buffer" => { app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
let message = format!(
"Sidebar {}",
if app_state.ui.show_sidebar {
"shown"
} else {
"hidden"
}
);
Ok(Some(EventOutcome::Ok(message)))
}
AppAction::ToggleBufferList => {
app_state.ui.show_buffer_list = !app_state.ui.show_buffer_list;
let message = format!(
"Buffer {}",
if app_state.ui.show_buffer_list {
"shown"
} else {
"hidden"
}
);
Ok(Some(EventOutcome::Ok(message)))
}
AppAction::Buffer(BufferAction::Next) => {
if switch_buffer(buffer_state, true) { if switch_buffer(buffer_state, true) {
return Ok(Some(EventOutcome::Ok( return Ok(Some(EventOutcome::Ok(
"Switched to next buffer".to_string(), "Switched to next buffer".to_string(),
@@ -1052,7 +943,7 @@ impl EventHandler {
} }
Ok(Some(EventOutcome::Ok(String::new()))) Ok(Some(EventOutcome::Ok(String::new())))
} }
"previous_buffer" => { AppAction::Buffer(BufferAction::Previous) => {
if switch_buffer(buffer_state, false) { if switch_buffer(buffer_state, false) {
return Ok(Some(EventOutcome::Ok( return Ok(Some(EventOutcome::Ok(
"Switched to previous buffer".to_string(), "Switched to previous buffer".to_string(),
@@ -1060,13 +951,13 @@ impl EventHandler {
} }
Ok(Some(EventOutcome::Ok(String::new()))) Ok(Some(EventOutcome::Ok(String::new())))
} }
"close_buffer" => { AppAction::Buffer(BufferAction::Close) => {
let current_table_name = app_state.current_view_table_name.as_deref(); let current_table_name = app_state.current_view_table_name.as_deref();
let message = let message = buffer_state
buffer_state.close_buffer_with_intro_fallback(current_table_name); .close_buffer_with_intro_fallback(current_table_name);
Ok(Some(EventOutcome::Ok(message))) Ok(Some(EventOutcome::Ok(message)))
} }
"open_search" => { AppAction::OpenSearch => {
if let Page::Form(_) = &router.current { if let Page::Form(_) = &router.current {
if let Some(table_name) = app_state.current_view_table_name.clone() { if let Some(table_name) = app_state.current_view_table_name.clone() {
app_state.ui.show_search_palette = true; app_state.ui.show_search_palette = true;
@@ -1079,22 +970,7 @@ impl EventHandler {
} }
Ok(Some(EventOutcome::Ok(String::new()))) Ok(Some(EventOutcome::Ok(String::new())))
} }
"enter_command_mode" => { AppAction::FindFilePaletteToggle => {
if self.is_focus_outside(router)
&& !self.command_mode
&& !app_state.ui.show_search_palette
&& !self.navigation_state.active
{
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
self.key_sequence_tracker.reset();
self.set_focus_outside(router, true);
return Ok(Some(EventOutcome::Ok(String::new())));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
"find_file_palette_toggle" => {
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) { if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
let mut all_table_paths: Vec<String> = app_state let mut all_table_paths: Vec<String> = app_state
.profile_tree .profile_tree
@@ -1112,27 +988,88 @@ impl EventHandler {
self.command_mode = false; self.command_mode = false;
self.command_input.clear(); self.command_input.clear();
self.command_message.clear(); self.command_message.clear();
self.key_sequence_tracker.reset(); self.input_engine.reset_sequence();
return Ok(Some(EventOutcome::Ok( return Ok(Some(EventOutcome::Ok(
"Table selection palette activated".to_string(), "Table selection palette activated".to_string(),
))); )));
} }
Ok(Some(EventOutcome::Ok(String::new()))) Ok(Some(EventOutcome::Ok(String::new())))
} }
// Core actions that already have a handler AppAction::EnterCommandMode => {
"save" | "force_quit" | "save_and_quit" | "revert" => { if !self.is_in_form_edit_mode(router, app_state)
let outcome = self && !self.command_mode
.handle_core_action( && !app_state.ui.show_search_palette
action, && !self.navigation_state.active
auth_state, {
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
self.input_engine.reset_sequence();
// Keep focus outside so canvas wont consume keystrokes
self.set_focus_outside(router, true);
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::ExitCommandMode => {
self.command_input.clear();
self.command_message.clear();
self.command_mode = false;
self.input_engine.reset_sequence();
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) {
editor.set_mode(CanvasMode::ReadOnly);
}
}
Ok(Some(EventOutcome::Ok(
"Exited command mode".to_string(),
)))
}
AppAction::CommandExecute => {
// Execute using the actual configured key that triggered the action
let out = self
.execute_command(
key_event,
config,
terminal, terminal,
command_handler,
app_state, app_state,
router, router,
) )
.await?; .await?;
Ok(Some(outcome)) Ok(Some(out))
} }
_ => Ok(None), AppAction::CommandBackspace => {
self.command_input.pop();
self.input_engine.reset_sequence();
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::Core(core) => {
let s = match core {
CoreAction::Save => "save",
CoreAction::ForceQuit => "force_quit",
CoreAction::SaveAndQuit => "save_and_quit",
CoreAction::Revert => "revert",
};
let out = self
.handle_core_action(s, auth_state, terminal, app_state, router)
.await?;
Ok(Some(out))
}
AppAction::Navigate(_ma) => {
// Movement is still handled by page/nav code paths that
// follow after PassThrough. We return None here to keep flow.
Ok(None)
} }
} }
} }
fn is_in_form_edit_mode(&self, router: &Router, app_state: &AppState) -> bool {
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path_ref(path) {
return editor.mode() == CanvasMode::Edit;
}
}
false
}
}