Files
komp_ac/client/src/modes/handlers/event.rs

543 lines
27 KiB
Rust

// src/modes/handlers/event.rs
use crossterm::event::Event;
use crossterm::cursor::SetCursorStyle;
use crate::services::grpc_client::GrpcClient;
use crate::services::auth::AuthClient;
use crate::config::binds::config::Config;
use crate::ui::handlers::rat_state::UiStateHandler;
use crate::ui::handlers::context::UiContext;
use crate::functions::common::buffer;
use anyhow::Result;
use crate::tui::{
terminal::core::TerminalCore,
functions::{
common::{form::SaveOutcome, login, register},
},
{intro, admin},
};
use crate::state::{
app::{
highlight::HighlightState,
state::AppState,
buffer::{AppView, BufferState},
},
pages::{
auth::{AuthState, LoginState, RegisterState},
admin::AdminState,
canvas_state::CanvasState,
form::FormState,
intro::IntroState,
},
};
use crate::modes::{
common::{command_mode, commands::CommandHandler},
handlers::mode_manager::{ModeManager, AppMode},
canvas::{edit, read_only, common_mode},
general::{navigation, dialog},
};
use crate::functions::modes::navigation::{admin_nav, add_table_nav};
use crate::config::binds::key_sequences::KeySequenceTracker;
use tokio::sync::mpsc;
use crate::tui::functions::common::login::LoginResult;
use crate::tui::functions::common::register::RegisterResult;
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventOutcome {
Ok(String),
Exit(String),
DataSaved(SaveOutcome, String),
ButtonSelected { context: UiContext, index: usize },
}
pub struct EventHandler {
pub command_mode: bool,
pub command_input: String,
pub command_message: String,
pub is_edit_mode: bool,
pub highlight_state: HighlightState,
pub edit_mode_cooldown: bool,
pub ideal_cursor_column: usize,
pub key_sequence_tracker: KeySequenceTracker,
pub auth_client: AuthClient,
pub login_result_sender: mpsc::Sender<LoginResult>,
pub register_result_sender: mpsc::Sender<RegisterResult>,
pub save_table_result_sender: SaveTableResultSender,
}
impl EventHandler {
pub async fn new(
login_result_sender: mpsc::Sender<LoginResult>,
register_result_sender: mpsc::Sender<RegisterResult>,
save_table_result_sender: SaveTableResultSender,
) -> Result<Self> {
Ok(EventHandler {
command_mode: false,
command_input: String::new(),
command_message: String::new(),
is_edit_mode: false,
highlight_state: HighlightState::Off,
edit_mode_cooldown: false,
ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(800),
auth_client: AuthClient::new().await?,
login_result_sender,
register_result_sender,
save_table_result_sender,
})
}
pub async fn handle_event(
&mut self,
event: Event,
config: &Config,
terminal: &mut TerminalCore,
grpc_client: &mut GrpcClient,
command_handler: &mut CommandHandler,
form_state: &mut FormState,
auth_state: &mut AuthState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
intro_state: &mut IntroState,
admin_state: &mut AdminState,
buffer_state: &mut BufferState,
app_state: &mut AppState,
total_count: u64,
current_position: &mut u64,
) -> Result<EventOutcome> {
let current_mode = ModeManager::derive_mode(app_state, self);
app_state.update_mode(current_mode);
let current_view = {
let ui = &app_state.ui;
if ui.show_intro { AppView::Intro }
else if ui.show_login { AppView::Login }
else if ui.show_register { AppView::Register }
else if ui.show_admin { AppView::Admin }
else if ui.show_add_table { AppView::AddTable }
else if ui.show_form {
let form_name = app_state.selected_profile.clone().unwrap_or_else(|| "Data Form".to_string());
AppView::Form(form_name)
}
else { AppView::Scratch }
};
buffer_state.update_history(current_view);
if app_state.ui.dialog.dialog_show {
if let Some(dialog_result) = dialog::handle_dialog_event(
&event,
config,
app_state,
login_state,
register_state,
buffer_state,
admin_state,
).await {
return dialog_result;
}
return Ok(EventOutcome::Ok(String::new()));
}
if let Event::Key(key) = event {
let key_code = key.code;
let modifiers = key.modifiers;
if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) {
let message = format!("Sidebar {}",
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
);
return Ok(EventOutcome::Ok(message));
}
if UiStateHandler::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 !matches!(current_mode, AppMode::Edit | AppMode::Command) {
if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.global, key_code, modifiers
) {
match action {
"next_buffer" => {
if buffer::switch_buffer(buffer_state, true) {
return Ok(EventOutcome::Ok("Switched to next buffer".to_string()));
}
}
"previous_buffer" => {
if buffer::switch_buffer(buffer_state, false) {
return Ok(EventOutcome::Ok("Switched to previous buffer".to_string()));
}
}
_ => {}
}
}
}
match current_mode {
AppMode::General => {
// Prioritize Admin Panel navigation if it's visible
if app_state.ui.show_admin
&& auth_state.role.as_deref() == Some("admin") {
if admin_nav::handle_admin_navigation(
key,
config,
app_state,
admin_state,
buffer_state,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
// --- Add Table Page Navigation ---
if app_state.ui.show_add_table {
let client_clone = grpc_client.clone();
let sender_clone = self.save_table_result_sender.clone();
if add_table_nav::handle_add_table_navigation(
key,
config,
app_state,
&mut admin_state.add_table_state,
client_clone,
sender_clone,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
let nav_outcome = navigation::handle_navigation_event(
key,
config,
form_state,
app_state,
login_state,
register_state,
intro_state,
admin_state,
&mut self.command_mode,
&mut self.command_input,
&mut self.command_message,
).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 app_state.ui.show_admin {
if !app_state.profile_tree.profiles.is_empty() {
admin_state.profile_list_state.select(Some(0));
}
}
format!("Intro Option {} selected", index)
}
UiContext::Login => {
let login_action_message = 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(),
};
login_action_message
}
UiContext::Register => {
let register_action_message = 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(),
};
register_action_message
}
UiContext::Admin => {
admin::handle_admin_selection(app_state, admin_state);
format!("Admin Option {} selected", index)
}
UiContext::Dialog => {
"Internal error: Unexpected dialog state".to_string()
}
}; // Semicolon added here
return Ok(EventOutcome::Ok(message));
}
other => return other,
}
},
AppMode::ReadOnly => {
// Check for Linewise highlight first
if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise")
&& ModeManager::can_enter_highlight_mode(current_mode)
{
let current_field_index = if app_state.ui.show_login { login_state.current_field() }
else if app_state.ui.show_register { register_state.current_field() }
else { form_state.current_field() };
self.highlight_state = HighlightState::Linewise { anchor_line: current_field_index };
self.command_message = "-- LINE HIGHLIGHT --".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// Check for Character-wise highlight
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode")
&& ModeManager::can_enter_highlight_mode(current_mode)
{
let current_field_index = if app_state.ui.show_login { login_state.current_field() }
else if app_state.ui.show_register { register_state.current_field() }
else { form_state.current_field() };
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() }
else if app_state.ui.show_register { register_state.current_cursor_pos() }
else { form_state.current_cursor_pos() };
let anchor = (current_field_index, current_cursor_pos);
self.highlight_state = HighlightState::Characterwise { anchor };
self.command_message = "-- HIGHLIGHT --".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// Check for entering edit mode (before cursor)
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before")
&& ModeManager::can_enter_edit_mode(current_mode) {
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
self.command_message = "Edit mode".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// Check for entering edit mode (after cursor)
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after")
&& ModeManager::can_enter_edit_mode(current_mode) {
let current_input = if app_state.ui.show_login || app_state.ui.show_register{
login_state.get_current_input()
} else {
form_state.get_current_input()
};
let current_cursor_pos = if app_state.ui.show_login || app_state.ui.show_register{
login_state.current_cursor_pos()
} else {
form_state.current_cursor_pos()
};
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
if app_state.ui.show_login || app_state.ui.show_register{
login_state.set_current_cursor_pos(current_cursor_pos + 1);
self.ideal_cursor_column = login_state.current_cursor_pos();
} else {
form_state.set_current_cursor_pos(current_cursor_pos + 1);
self.ideal_cursor_column = form_state.current_cursor_pos();
}
}
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
app_state.ui.focus_outside_canvas = false;
self.command_message = "Edit mode (after cursor)".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// Check for entering command mode
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode")
&& ModeManager::can_enter_command_mode(current_mode) {
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
return Ok(EventOutcome::Ok(String::new()));
}
// Check for common actions (save, quit, etc.) only if no mode change happened
if let Some(action) = config.get_common_action(key_code, modifiers) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return common_mode::handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
grpc_client,
&mut self.auth_client,
terminal,
app_state,
current_position,
total_count,
).await;
},
_ => {}
}
}
// If no mode change or specific common action handled, delegate to read_only handler
let (_should_exit, message) = read_only::handle_read_only_event(
app_state,
key,
config,
form_state,
login_state,
register_state,
&mut admin_state.add_table_state,
&mut self.key_sequence_tracker,
current_position,
total_count,
grpc_client,
&mut self.command_message,
&mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column,
).await?;
// Note: handle_read_only_event should ignore mode entry keys internally now
return Ok(EventOutcome::Ok(message));
}, // End AppMode::ReadOnly
AppMode::Highlight => {
// --- Handle Highlight Mode Specific Keys ---
// 1. Check for Exit first
if config.get_highlight_action_for_key(key_code, modifiers) == Some("exit_highlight_mode") {
self.highlight_state = HighlightState::Off;
self.command_message = "Exited highlight mode".to_string();
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
// 2. Check for Switch to Linewise
else if config.get_highlight_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") {
// Only switch if currently characterwise
if let HighlightState::Characterwise { anchor } = self.highlight_state {
self.highlight_state = HighlightState::Linewise { anchor_line: anchor.0 };
self.command_message = "-- LINE HIGHLIGHT --".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
return Ok(EventOutcome::Ok("".to_string()));
}
let (_should_exit, message) = read_only::handle_read_only_event(
app_state, key, config, form_state, login_state,
register_state, &mut admin_state.add_table_state, &mut self.key_sequence_tracker,
current_position, total_count, grpc_client,
&mut self.command_message, &mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column,
)
.await?;
return Ok(EventOutcome::Ok(message));
}
AppMode::Edit => {
// First, check for common actions (save, revert, etc.) that apply in Edit mode
// These might take precedence or have different behavior than the edit handler
if let Some(action) = config.get_common_action(key_code, modifiers) {
// Handle common actions like save, revert, force_quit, save_and_quit
// Ensure these actions return EventOutcome directly if they might exit the app
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
// This call likely returns EventOutcome, handle it directly
return common_mode::handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
grpc_client,
&mut self.auth_client,
terminal,
app_state,
current_position,
total_count,
).await;
},
// Handle other common actions if necessary
_ => {}
}
// If a common action was handled but didn't return/exit,
// we might want to stop further processing for this key event.
// Depending on the action, you might return Ok(EventOutcome::Ok(...)) here.
// For now, assume common actions either exit or don't prevent further processing.
}
// If no common action took precedence, delegate to the edit-specific handler
let edit_result = edit::handle_edit_event(
key,
config,
form_state,
login_state,
register_state,
&mut admin_state.add_table_state,
&mut self.ideal_cursor_column,
current_position,
total_count,
grpc_client,
app_state,
).await;
match edit_result {
Ok(edit::EditEventOutcome::ExitEditMode) => {
// The edit handler signaled to exit the mode
self.is_edit_mode = false;
self.edit_mode_cooldown = true;
let has_changes = if app_state.ui.show_login { login_state.has_unsaved_changes() }
else if app_state.ui.show_register { register_state.has_unsaved_changes() }
else { form_state.has_unsaved_changes() };
self.command_message = if has_changes {
"Exited edit mode (unsaved changes remain)".to_string()
} else {
"Read-only mode".to_string()
};
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
// Adjust cursor position if needed
let current_input = if app_state.ui.show_login { login_state.get_current_input() }
else if app_state.ui.show_register { register_state.get_current_input() }
else { form_state.get_current_input() };
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() }
else if app_state.ui.show_register { register_state.current_cursor_pos() }
else { form_state.current_cursor_pos() };
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
let new_pos = current_input.len() - 1;
let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state } else if app_state.ui.show_register { register_state } else { form_state };
target_state.set_current_cursor_pos(new_pos);
self.ideal_cursor_column = new_pos;
}
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
Ok(edit::EditEventOutcome::Message(msg)) => {
// Stay in edit mode, update message if not empty
if !msg.is_empty() {
self.command_message = msg;
}
self.key_sequence_tracker.reset(); // Reset sequence tracker on successful edit action
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
Err(e) => {
// Handle error from the edit handler
return Err(e.into());
}
}
}, // End AppMode::Edit
AppMode::Command => {
let outcome = command_mode::handle_command_event(
key,
config,
app_state,
login_state,
register_state,
form_state,
&mut self.command_input,
&mut self.command_message,
grpc_client,
command_handler,
terminal,
current_position,
total_count,
).await?;
if let EventOutcome::Ok(msg) = &outcome {
if msg == "Exited command mode" {
self.command_mode = false;
}
}
return Ok(outcome);
}
}
}
self.edit_mode_cooldown = false;
Ok(EventOutcome::Ok(self.command_message.clone()))
}
}