Files
komp_ac/client/src/modes/handlers/event.rs
2025-09-11 22:36:40 +02:00

1090 lines
45 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/modes/handlers/event.rs
use crate::config::binds::config::Config;
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::sidebar::toggle_sidebar;
use crate::search::event::handle_search_palette_event;
use crate::pages::admin_panel::add_logic;
use crate::pages::admin_panel::add_table;
use crate::pages::register::suggestions::RoleSuggestionsProvider;
use crate::pages::admin::main::logic::handle_admin_navigation;
use crate::pages::admin::admin;
use crate::modes::general::command_navigation::{
handle_command_navigation_event, NavigationState,
};
use crate::modes::{
common::{command_mode, commands::CommandHandler},
general::navigation,
handlers::mode_manager::{AppMode, ModeManager},
};
use crate::services::auth::AuthClient;
use crate::services::grpc_client::GrpcClient;
use canvas::AppMode as CanvasMode;
use canvas::DataProvider;
use crate::state::app::state::AppState;
use crate::pages::admin::AdminState;
use crate::state::pages::auth::AuthState;
use crate::state::pages::auth::UserRole;
use crate::pages::login::LoginState;
use crate::pages::register::RegisterState;
use crate::pages::intro::IntroState;
use crate::pages::login;
use crate::pages::register;
use crate::pages::intro;
use crate::pages::login::logic::LoginResult;
use crate::pages::register::RegisterResult;
use crate::pages::routing::{Router, Page};
use crate::movement::MovementAction;
use crate::dialog;
use crate::pages::forms;
use crate::pages::forms::FormState;
use crate::pages::forms::logic::{save, revert, SaveOutcome};
use crate::search::state::SearchState;
use crate::tui::{
terminal::core::TerminalCore,
};
use crate::ui::handlers::context::UiContext;
use canvas::KeyEventOutcome;
use canvas::SuggestionsProvider;
use anyhow::Result;
use common::proto::komp_ac::search::search_response::Hit;
use crossterm::event::{Event, KeyCode};
use tokio::sync::mpsc;
use tokio::sync::mpsc::unbounded_channel;
use tracing::info;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventOutcome {
Ok(String),
Exit(String),
DataSaved(SaveOutcome, String),
ButtonSelected { context: UiContext, index: usize },
TableSelected { path: String },
}
impl EventOutcome {
pub fn get_message_if_ok(&self) -> String {
match self {
EventOutcome::Ok(msg) => msg.clone(),
_ => String::new(),
}
}
}
pub struct EventHandler {
pub command_mode: bool,
pub command_input: String,
pub command_message: String,
pub edit_mode_cooldown: bool,
pub ideal_cursor_column: usize,
pub input_engine: InputEngine,
pub auth_client: AuthClient,
pub grpc_client: GrpcClient,
pub login_result_sender: mpsc::Sender<LoginResult>,
pub register_result_sender: mpsc::Sender<RegisterResult>,
pub save_table_result_sender: add_table::nav::SaveTableResultSender,
pub save_logic_result_sender: add_logic::nav::SaveLogicResultSender,
pub navigation_state: NavigationState,
pub search_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
pub search_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
pub autocomplete_result_sender: mpsc::UnboundedSender<Vec<Hit>>,
pub autocomplete_result_receiver: mpsc::UnboundedReceiver<Vec<Hit>>,
}
impl EventHandler {
pub async fn new(
login_result_sender: mpsc::Sender<LoginResult>,
register_result_sender: mpsc::Sender<RegisterResult>,
save_table_result_sender: add_table::nav::SaveTableResultSender,
save_logic_result_sender: add_logic::nav::SaveLogicResultSender,
grpc_client: GrpcClient,
) -> Result<Self> {
let (search_tx, search_rx) = unbounded_channel();
let (autocomplete_tx, autocomplete_rx) = unbounded_channel();
Ok(EventHandler {
command_mode: false,
command_input: String::new(),
command_message: String::new(),
edit_mode_cooldown: false,
ideal_cursor_column: 0,
input_engine: InputEngine::new(400, 5000),
auth_client: AuthClient::new().await?,
grpc_client,
login_result_sender,
register_result_sender,
save_table_result_sender,
save_logic_result_sender,
navigation_state: NavigationState::new(),
search_result_sender: search_tx,
search_result_receiver: search_rx,
autocomplete_result_sender: autocomplete_tx,
autocomplete_result_receiver: autocomplete_rx,
})
}
pub fn is_navigation_active(&self) -> bool {
self.navigation_state.active
}
pub fn activate_find_file(&mut self, options: Vec<String>) {
self.navigation_state.activate_find_file(options);
}
// Helper functions - replace the removed event_helper functions
fn get_current_field_for_state(&self, router: &Router, app_state: &AppState) -> usize {
match &router.current {
Page::Login(state) => state.current_field(),
Page::Register(state) => state.current_field(),
Page::Form(path) => app_state
.editor_for_path_ref(path)
.map(|e| e.data_provider().current_field())
.unwrap_or(0),
_ => 0,
}
}
fn get_current_cursor_pos_for_state(&self, router: &Router, app_state: &AppState) -> usize {
match &router.current {
Page::Login(state) => state.current_cursor_pos(),
Page::Register(state) => state.current_cursor_pos(),
Page::Form(path) => app_state
.form_state_for_path_ref(path)
.map(|fs| fs.current_cursor_pos())
.unwrap_or(0),
_ => 0,
}
}
fn get_has_unsaved_changes_for_state(&self, router: &Router, app_state: &AppState) -> bool {
match &router.current {
Page::Login(state) => state.has_unsaved_changes(),
Page::Register(state) => state.has_unsaved_changes(),
Page::Form(path) => app_state
.form_state_for_path_ref(path)
.map(|fs| fs.has_unsaved_changes())
.unwrap_or(false),
_ => false,
}
}
fn get_current_input_for_state<'a>(
&'a self,
router: &'a Router,
app_state: &'a AppState,
) -> &'a str {
match &router.current {
Page::Login(state) => state.get_current_input(),
Page::Register(state) => state.get_current_input(),
Page::Form(path) => app_state
.form_state_for_path_ref(path)
.map(|fs| fs.get_current_input())
.unwrap_or(""),
_ => "",
}
}
fn set_current_cursor_pos_for_state(
&mut self,
router: &mut Router,
app_state: &mut AppState,
pos: usize,
) {
match &mut router.current {
Page::Login(state) => state.set_current_cursor_pos(pos),
Page::Register(state) => state.set_current_cursor_pos(pos),
Page::Form(path) => {
if let Some(fs) = app_state.form_state_for_path(path) {
fs.set_current_cursor_pos(pos);
}
}
_ => {},
}
}
fn get_cursor_pos_for_mixed_state(&self, router: &Router, app_state: &AppState) -> usize {
match &router.current {
Page::Login(state) => state.current_cursor_pos(),
Page::Register(state) => state.current_cursor_pos(),
Page::Form(path) => app_state
.form_state_for_path_ref(path)
.map(|fs| fs.current_cursor_pos())
.unwrap_or(0),
_ => 0,
}
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_event(
&mut self,
event: Event,
config: &Config,
terminal: &mut TerminalCore,
command_handler: &mut CommandHandler,
auth_state: &mut AuthState,
buffer_state: &mut BufferState,
app_state: &mut AppState,
router: &mut Router,
) -> Result<EventOutcome> {
if app_state.ui.show_search_palette {
if let Event::Key(key_event) = event {
info!(
"RAW KEY: code={:?} mods={:?} active_seq={} ",
key_event.code,
key_event.modifiers,
self.input_engine.has_active_sequence(),
);
if let Some(message) = handle_search_palette_event(
key_event,
app_state,
&mut self.grpc_client,
self.search_result_sender.clone(),
).await? {
return Ok(EventOutcome::Ok(message));
}
return Ok(EventOutcome::Ok(String::new()));
}
}
let mut current_mode =
ModeManager::derive_mode(app_state, self, router);
if current_mode == AppMode::General && self.navigation_state.active {
if let Event::Key(key_event) = event {
let outcome = handle_command_navigation_event(
&mut self.navigation_state,
key_event,
config,
)
.await?;
if !self.navigation_state.active {
self.command_message = outcome.get_message_if_ok();
current_mode =
ModeManager::derive_mode(app_state, self, router);
}
app_state.update_mode(current_mode);
return Ok(outcome);
}
app_state.update_mode(current_mode);
return Ok(EventOutcome::Ok(String::new()));
}
app_state.update_mode(current_mode);
let current_view = match &router.current {
Page::Intro(_) => AppView::Intro,
Page::Login(_) => AppView::Login,
Page::Register(_) => AppView::Register,
Page::Admin(_) => AppView::Admin,
Page::AddLogic(_) => AppView::AddLogic,
Page::AddTable(_) => AppView::AddTable,
Page::Form(path) => AppView::Form(path.clone()),
};
buffer_state.update_history(current_view);
if app_state.ui.dialog.dialog_show {
if let Event::Key(key_event) = event {
if let Some(dialog_result) = dialog::handle_dialog_event(
&Event::Key(key_event),
config,
app_state,
buffer_state,
router,
)
.await
{
return dialog_result;
}
} else if let Event::Resize(_, _) = event {
}
return Ok(EventOutcome::Ok(String::new()));
}
if let Event::Key(key_event) = event {
let key_code = key_event.code;
let modifiers = key_event.modifiers;
info!(
"RAW KEY: code={:?} mods={:?} pre_active_seq={}",
key_code, modifiers, self.input_engine.has_active_sequence()
);
let overlay_active = self.command_mode
|| app_state.ui.show_search_palette
|| self.navigation_state.active;
// Determine if canvas is in edit mode (we avoid capturing navigation then)
let in_form_edit_mode = matches!(
&router.current,
Page::Form(path) if {
if let Some(editor) = app_state.editor_for_path_ref(path) {
editor.mode() == CanvasMode::Edit
} else { false }
}
);
// Centralized key -> action resolution
let allow_nav = self.input_engine.has_active_sequence()
|| (!in_form_edit_mode && !overlay_active);
let input_ctx = InputContext {
app_mode: current_mode,
overlay_active,
allow_navigation_capture: allow_nav,
};
info!(
"InputContext: app_mode={:?}, overlay_active={}, in_form_edit_mode={}, allow_nav={}, has_active_seq={}",
current_mode, overlay_active, in_form_edit_mode, allow_nav, self.input_engine.has_active_sequence()
);
let outcome = self.input_engine.process_key(key_event, &input_ctx, config);
info!(
"ENGINE OUTCOME: {:?} post_active_seq={}",
outcome, self.input_engine.has_active_sequence()
);
match outcome {
InputOutcome::Action(action) => {
if let Some(outcome) = self
.handle_app_action(
action,
key_event, // pass original key
config,
terminal,
command_handler,
auth_state,
buffer_state,
app_state,
router,
)
.await?
{
return Ok(outcome);
}
// 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()));
}
InputOutcome::PassThrough => {
// fall through to page/canvas handlers
}
}
// LOGIN: canvas <-> buttons focus handoff
// Do not let Login canvas receive keys when overlays/palettes are active
if !overlay_active {
if let Page::Login(login_page) = &mut router.current {
let outcome =
login::event::handle_login_event(event.clone(), app_state, login_page)?;
// Only return if the login page actually consumed the key
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::Register(register_page) = &mut router.current {
let outcome = crate::pages::register::event::handle_register_event(
event,
app_state,
register_page,
)?;
// Only stop if page actually consumed the key; else fall through to global handling
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::Form(path_str) = &router.current {
let path = path_str.clone();
if let Event::Key(_key_event) = event {
// Do NOT call the input engine here again. The top-level
// process_key call above already ran for this key.
// If we are waiting for more leader keys, swallow the key.
if self.input_engine.has_active_sequence() {
return Ok(EventOutcome::Ok(String::new()));
}
// Otherwise, forward to the form editor/canvas.
let outcome = forms::event::handle_form_event(
event,
app_state,
&path,
&mut self.ideal_cursor_column,
)?;
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
}
} else if let Page::AddLogic(add_logic_page) = &mut router.current {
// Allow ":" (enter_command_mode) even when inside AddLogic canvas
if let Some(action) =
config.get_general_action(key_event.code, key_event.modifiers)
{
if action == "enter_command_mode"
&& !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.input_engine.reset_sequence();
self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new()));
}
}
let movement_action_early = if let Some(act) =
config.get_general_action(key_event.code, key_event.modifiers)
{
match act {
"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,
}
} else { None };
let outcome = add_logic::event::handle_add_logic_event(
key_event,
movement_action_early,
config,
app_state,
add_logic_page,
self.grpc_client.clone(),
self.save_logic_result_sender.clone(),
)?;
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::AddTable(add_table_page) = &mut router.current {
// Allow ":" (enter_command_mode) even when inside AddTable canvas
if let Some(action) =
config.get_general_action(key_event.code, key_event.modifiers)
{
if action == "enter_command_mode"
&& !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.input_engine.reset_sequence();
self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new()));
}
}
// Handle AddTable before global actions so canvas gets first shot at keys.
// Map keys to MovementAction (same as AddLogic early handler)
let movement_action_early = if let Some(act) =
config.get_general_action(key_event.code, key_event.modifiers)
{
match act {
"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,
}
} else {
None
};
let outcome = add_table::event::handle_add_table_event(
key_event,
movement_action_early,
config,
app_state,
add_table_page,
self.grpc_client.clone(),
self.save_table_result_sender.clone(),
)?;
// Only stop if the page consumed the key; else let global handling proceed.
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::Admin(admin_state) = &mut router.current {
if matches!(auth_state.role, Some(UserRole::Admin)) {
if let Event::Key(key_event) = event {
if admin::event::handle_admin_event(
key_event,
config,
app_state,
buffer_state,
router,
&mut self.command_message,
)? {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
}
}
}
// Sidebar/buffer toggles now handled via AppAction in the engine
if current_mode == AppMode::General {
// General mode specific key mapping now handled via AppAction
}
match current_mode {
AppMode::General => {
// Map keys to MovementAction
let movement_action = if let Some(act) =
config.get_general_action(key_event.code, key_event.modifiers)
{
use crate::movement::MovementAction;
match act {
"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,
}
} else {
None
};
// Let the current page handle decoupled movement first
if let Some(ma) = movement_action {
match &mut router.current {
Page::Intro(state) => {
if state.handle_movement(ma) {
return Ok(EventOutcome::Ok(String::new()));
}
}
_ => {}
}
}
// Generic navigation for the rest (Intro/Login/Register/Form)
let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) {
// Skip generic navigation for AddTable/AddLogic (they have their own handlers)
Ok(EventOutcome::Ok(String::new()))
} else {
navigation::handle_navigation_event(
key_event,
config,
app_state,
router,
&mut self.command_mode,
&mut self.command_input,
&mut self.command_message,
&mut self.navigation_state,
).await
};
match nav_outcome {
Ok(EventOutcome::ButtonSelected { context, index }) => {
let message = match context {
UiContext::Intro => {
intro::handle_intro_selection(
app_state,
buffer_state,
index,
);
if let Page::Admin(admin_state) = &mut router.current {
if !app_state.profile_tree.profiles.is_empty() {
admin_state.profile_list_state.select(Some(0));
}
}
format!("Intro Option {} selected", index)
}
UiContext::Login => {
if let Page::Login(login_state) = &mut router.current {
match index {
0 => login::initiate_login(
login_state,
app_state,
self.auth_client.clone(),
self.login_result_sender.clone(),
),
1 => login::back_to_main(
login_state,
app_state,
buffer_state,
)
.await,
_ => "Invalid Login Option".to_string(),
}
} else {
"Invalid state".to_string()
}
}
UiContext::Register => {
if let Page::Register(register_state) = &mut router.current {
match index {
0 => register::initiate_registration(
register_state,
app_state,
self.auth_client.clone(),
self.register_result_sender.clone(),
),
1 => register::back_to_login(
register_state,
app_state,
buffer_state,
)
.await,
_ => "Invalid Login Option".to_string(),
}
} else {
"Invalid state".to_string()
}
}
UiContext::Admin => {
if let Page::Admin(admin_state) = &router.current {
admin::tui::handle_admin_selection(
app_state,
admin_state,
);
}
format!("Admin Option {} selected", index)
}
UiContext::Dialog => {
"Internal error: Unexpected dialog state".to_string()
}
};
return Ok(EventOutcome::Ok(message));
}
other => return other,
}
}
AppMode::Command => {
// Command-mode keys already handled by the engine.
// Collect characters not handled (typed command input).
match key_code {
KeyCode::Char(c) => {
self.command_input.push(c);
return Ok(EventOutcome::Ok(String::new()));
}
_ => {
self.input_engine.reset_sequence();
return Ok(EventOutcome::Ok(String::new()));
}
}
}
}
} else if let Event::Resize(_, _) = event {
return Ok(EventOutcome::Ok("Resized".to_string()));
}
self.edit_mode_cooldown = false;
Ok(EventOutcome::Ok(self.command_message.clone()))
}
fn is_processed_command(&self, command: &str) -> bool {
matches!(command, "w" | "q" | "q!" | "wq" | "r")
}
async fn handle_core_action(
&mut self,
action: &str,
auth_state: &mut AuthState,
terminal: &mut TerminalCore,
app_state: &mut AppState,
router: &mut Router,
) -> Result<EventOutcome> {
match action {
"save" => {
if let Page::Login(login_state) = &mut router.current {
let message = login::logic::save(
auth_state,
login_state,
&mut self.auth_client,
app_state,
)
.await?;
Ok(EventOutcome::Ok(message))
} else {
if let Page::Form(path) = &router.current {
forms::event::save_form(app_state, path, &mut self.grpc_client).await
} else {
Ok(EventOutcome::Ok("Nothing to save".to_string()))
}
}
}
"force_quit" => {
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) {
editor.cleanup_cursor()?;
}
}
terminal.cleanup()?;
Ok(EventOutcome::Exit(
"Force exiting without saving.".to_string(),
))
}
"save_and_quit" => {
let message = if let Page::Login(login_state) = &mut router.current {
login::logic::save(
auth_state,
login_state,
&mut self.auth_client,
app_state,
)
.await?
} else if let Page::Form(path) = &router.current {
let save_result = forms::event::save_form(app_state, path, &mut self.grpc_client).await?;
match save_result {
EventOutcome::DataSaved(_, msg) => msg,
EventOutcome::Ok(msg) => msg,
_ => "Saved".to_string(),
}
} else {
"No changes to save.".to_string()
};
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) {
editor.cleanup_cursor()?;
}
}
terminal.cleanup()?;
Ok(EventOutcome::Exit(format!(
"{}. Exiting application.",
message
)))
}
"revert" => {
let message = if let Page::Login(login_state) = &mut router.current {
login::logic::revert(login_state, app_state).await
} else if let Page::Register(register_state) = &mut router.current {
register::revert(
register_state,
app_state,
)
.await
} else {
if let Page::Form(path) = &router.current {
return forms::event::revert_form(app_state, path, &mut self.grpc_client).await;
} else {
"Nothing to revert".to_string()
}
};
Ok(EventOutcome::Ok(message))
}
_ => Ok(EventOutcome::Ok(format!(
"Core action not handled: {}",
action
))),
}
}
fn is_mode_transition_action(action: &str) -> bool {
matches!(action,
"exit" |
"exit_edit_mode" |
"enter_edit_mode_before" |
"enter_edit_mode_after" |
"enter_command_mode" |
"exit_command_mode" |
"enter_highlight_mode" |
"enter_highlight_mode_linewise" |
"exit_highlight_mode" |
"save" |
"quit" |
"Force_quit" |
"save_and_quit" |
"revert" |
"enter_decider" |
"trigger_autocomplete" |
"suggestion_up" |
"suggestion_down" |
"previous_entry" |
"next_entry" |
"toggle_sidebar" |
"toggle_buffer_list" |
"next_buffer" |
"previous_buffer" |
"close_buffer" |
"open_search" |
"find_file_palette_toggle"
)
}
fn set_focus_outside(&mut self, router: &mut Router, outside: bool) {
match &mut router.current {
Page::Login(state) => state.focus_outside_canvas = outside,
Page::Register(state) => state.focus_outside_canvas = outside,
Page::Intro(state) => state.focus_outside_canvas = outside,
Page::Admin(state) => state.focus_outside_canvas = outside,
Page::AddLogic(state) => state.focus_outside_canvas = outside,
Page::AddTable(state) => state.focus_outside_canvas = outside,
_ => {}
}
}
fn set_focused_button(&mut self, router: &mut Router, index: usize) {
match &mut router.current {
Page::Login(state) => state.focused_button_index = index,
Page::Register(state) => state.focused_button_index = index,
Page::Intro(state) => state.focused_button_index = index,
Page::Admin(state) => state.focused_button_index = index,
Page::AddLogic(state) => state.focused_button_index = index,
Page::AddTable(state) => state.focused_button_index = index,
_ => {}
}
}
fn is_focus_outside(&self, router: &Router) -> bool {
match &router.current {
Page::Login(state) => state.focus_outside_canvas,
Page::Register(state) => state.focus_outside_canvas,
Page::Intro(state) => state.focus_outside_canvas,
Page::Admin(state) => state.focus_outside_canvas,
Page::AddLogic(state) => state.focus_outside_canvas,
Page::AddTable(state) => state.focus_outside_canvas,
_ => false,
}
}
fn focused_button(&self, router: &Router) -> usize {
match &router.current {
Page::Login(state) => state.focused_button_index,
Page::Register(state) => state.focused_button_index,
Page::Intro(state) => state.focused_button_index,
Page::Admin(state) => state.focused_button_index,
Page::AddLogic(state) => state.focused_button_index,
Page::AddTable(state) => state.focused_button_index,
_ => 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)
}
#[allow(clippy::too_many_arguments)]
async fn handle_app_action(
&mut self,
action: AppAction,
key_event: crossterm::event::KeyEvent,
config: &Config,
terminal: &mut TerminalCore,
command_handler: &mut CommandHandler,
auth_state: &mut AuthState,
buffer_state: &mut BufferState,
app_state: &mut AppState,
router: &mut Router,
) -> Result<Option<EventOutcome>> {
match action {
AppAction::ToggleSidebar => {
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) {
return Ok(Some(EventOutcome::Ok(
"Switched to next buffer".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::Buffer(BufferAction::Previous) => {
if switch_buffer(buffer_state, false) {
return Ok(Some(EventOutcome::Ok(
"Switched to previous buffer".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::Buffer(BufferAction::Close) => {
let current_table_name = app_state.current_view_table_name.as_deref();
let message = buffer_state
.close_buffer_with_intro_fallback(current_table_name);
Ok(Some(EventOutcome::Ok(message)))
}
AppAction::OpenSearch => {
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(Some(EventOutcome::Ok(
"Search palette opened".to_string(),
)));
}
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::FindFilePaletteToggle => {
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
let mut all_table_paths: Vec<String> = app_state
.profile_tree
.profiles
.iter()
.flat_map(|profile| {
profile.tables.iter().map(move |table| {
format!("{}/{}", profile.name, table.name)
})
})
.collect();
all_table_paths.sort();
self.navigation_state.activate_find_file(all_table_paths);
self.command_mode = false;
self.command_input.clear();
self.command_message.clear();
self.input_engine.reset_sequence();
return Ok(Some(EventOutcome::Ok(
"Table selection palette activated".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::EnterCommandMode => {
if !self.is_in_form_edit_mode(router, app_state)
&& !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.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,
command_handler,
app_state,
router,
)
.await?;
Ok(Some(out))
}
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
}
}