Files
komp_ac/client/src/modes/handlers/event.rs
2025-08-20 23:52:14 +02:00

1192 lines
49 KiB
Rust

// src/modes/handlers/event.rs
use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::functions::common::buffer;
use crate::functions::modes::navigation::add_logic_nav;
use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender;
use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender;
use crate::functions::modes::navigation::{add_table_nav, admin_nav};
use crate::modes::general::command_navigation::{
handle_command_navigation_event, NavigationState,
};
use crate::modes::{
common::{command_mode, commands::CommandHandler},
general::{dialog, navigation},
handlers::mode_manager::{AppMode, ModeManager},
};
use crate::services::auth::AuthClient;
use crate::services::grpc_client::GrpcClient;
use canvas::{FormEditor, AppMode as CanvasMode};
use crate::state::{
app::{
buffer::{AppView, BufferState},
highlight::HighlightState,
search::SearchState,
state::AppState,
},
pages::{
admin::AdminState,
auth::{AuthState, LoginState, RegisterState},
form::FormState,
intro::IntroState,
},
};
use crate::tui::functions::common::login::LoginResult;
use crate::tui::functions::common::register::RegisterResult;
use crate::tui::{
functions::common::{form::SaveOutcome, login, register},
terminal::core::TerminalCore,
{admin, intro},
};
use crate::ui::handlers::context::UiContext;
use crate::ui::handlers::rat_state::UiStateHandler;
use anyhow::Result;
use common::proto::komp_ac::search::search_response::Hit;
use crossterm::event::KeyModifiers;
use crossterm::event::{Event, KeyCode, KeyEvent};
use tokio::sync::mpsc;
use tokio::sync::mpsc::unbounded_channel;
use tracing::{error, 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 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 grpc_client: GrpcClient,
pub login_result_sender: mpsc::Sender<LoginResult>,
pub register_result_sender: mpsc::Sender<RegisterResult>,
pub save_table_result_sender: SaveTableResultSender,
pub save_logic_result_sender: 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: SaveTableResultSender,
save_logic_result_sender: 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(),
is_edit_mode: false,
highlight_state: HighlightState::Off,
edit_mode_cooldown: false,
ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(400),
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(
app_state: &AppState,
login_state: &LoginState,
register_state: &RegisterState,
form_state: &FormState,
) -> usize {
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()
}
}
fn get_current_cursor_pos_for_state(
app_state: &AppState,
login_state: &LoginState,
register_state: &RegisterState,
form_state: &FormState,
) -> usize {
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()
}
}
fn get_has_unsaved_changes_for_state(
app_state: &AppState,
login_state: &LoginState,
register_state: &RegisterState,
form_state: &FormState,
) -> bool {
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()
}
}
fn get_current_input_for_state<'a>(
app_state: &AppState,
login_state: &'a LoginState,
register_state: &'a RegisterState,
form_state: &'a FormState,
) -> &'a str {
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()
}
}
fn set_current_cursor_pos_for_state(
app_state: &AppState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
form_state: &mut FormState,
pos: usize,
) {
if app_state.ui.show_login {
login_state.set_current_cursor_pos(pos);
} else if app_state.ui.show_register {
register_state.set_current_cursor_pos(pos);
} else {
form_state.set_current_cursor_pos(pos);
}
}
fn get_cursor_pos_for_mixed_state(
app_state: &AppState,
login_state: &LoginState,
form_state: &FormState,
) -> usize {
if app_state.ui.show_login || app_state.ui.show_register {
login_state.current_cursor_pos()
} else {
form_state.current_cursor_pos()
}
}
// This function handles state changes.
async fn handle_search_palette_event(
&mut self,
key_event: KeyEvent,
form_state: &mut FormState,
app_state: &mut AppState,
) -> Result<EventOutcome> {
let mut should_close = false;
let mut outcome_message = String::new();
let mut trigger_search = false;
if let Some(search_state) = app_state.search_state.as_mut() {
match key_event.code {
KeyCode::Esc => {
should_close = true;
outcome_message = "Search cancelled".to_string();
}
KeyCode::Enter => {
if let Some(selected_hit) =
search_state.results.get(search_state.selected_index)
{
if let Ok(data) = serde_json::from_str::<
std::collections::HashMap<String, String>,
>(&selected_hit.content_json)
{
let detached_pos = form_state.total_count + 2;
form_state
.update_from_response(&data, detached_pos);
}
should_close = true;
outcome_message =
format!("Loaded record ID {}", selected_hit.id);
}
}
KeyCode::Up => search_state.previous_result(),
KeyCode::Down => search_state.next_result(),
KeyCode::Char(c) => {
search_state
.input
.insert(search_state.cursor_position, c);
search_state.cursor_position += 1;
trigger_search = true;
}
KeyCode::Backspace => {
if search_state.cursor_position > 0 {
search_state.cursor_position -= 1;
search_state.input.remove(search_state.cursor_position);
trigger_search = true;
}
}
KeyCode::Left => {
search_state.cursor_position =
search_state.cursor_position.saturating_sub(1);
}
KeyCode::Right => {
if search_state.cursor_position < search_state.input.len()
{
search_state.cursor_position += 1;
}
}
_ => {}
}
if trigger_search {
search_state.is_loading = true;
search_state.results.clear();
search_state.selected_index = 0;
let query = search_state.input.clone();
let table_name = search_state.table_name.clone();
let sender = self.search_result_sender.clone();
let mut grpc_client = self.grpc_client.clone();
info!(
"--- 1. Spawning search task for query: '{}' ---",
query
);
tokio::spawn(async move {
info!("--- 2. Background task started. ---");
match grpc_client.search_table(table_name, query).await {
Ok(response) => {
info!(
"--- 3a. gRPC call successful. Found {} hits. ---",
response.hits.len()
);
let _ = sender.send(response.hits);
}
Err(e) => {
error!("--- 3b. gRPC call failed: {:?} ---", e);
let _ = sender.send(vec![]);
}
}
});
}
}
if should_close {
app_state.search_state = None;
app_state.ui.show_search_palette = false;
app_state.ui.focus_outside_canvas = false;
}
Ok(EventOutcome::Ok(outcome_message))
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_event(
&mut self,
event: Event,
config: &Config,
terminal: &mut TerminalCore,
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,
) -> Result<EventOutcome> {
if app_state.ui.show_search_palette {
if let Event::Key(key_event) = event {
return self
.handle_search_palette_event(
key_event,
form_state,
app_state,
)
.await;
}
return Ok(EventOutcome::Ok(String::new()));
}
let mut current_mode =
ModeManager::derive_mode(app_state, self, admin_state);
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, admin_state);
}
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 = {
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_logic {
AppView::AddLogic
} else if ui.show_add_table {
AppView::AddTable
} else if ui.show_form {
AppView::Form
} else {
AppView::Scratch
}
};
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,
login_state,
register_state,
buffer_state,
admin_state,
)
.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;
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(),
));
}
}
"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));
}
_ => {}
}
}
if let Some(action) =
config.get_general_action(key_code, modifiers)
{
if action == "open_search" {
if app_state.ui.show_form {
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));
app_state.ui.focus_outside_canvas = true;
return Ok(EventOutcome::Ok(
"Search palette opened".to_string(),
));
}
}
}
}
}
match current_mode {
AppMode::General => {
if app_state.ui.show_admin
&& auth_state.role.as_deref() == Some("admin")
{
if admin_nav::handle_admin_navigation(
key_event,
config,
app_state,
admin_state,
buffer_state,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(
self.command_message.clone(),
));
}
}
if app_state.ui.show_add_logic {
let client_clone = self.grpc_client.clone();
let sender_clone = self.save_logic_result_sender.clone();
if add_logic_nav::handle_add_logic_navigation(
key_event,
config,
app_state,
&mut admin_state.add_logic_state,
&mut self.is_edit_mode,
buffer_state,
client_clone,
sender_clone,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(
self.command_message.clone(),
));
}
}
if app_state.ui.show_add_table {
let client_clone = self.grpc_client.clone();
let sender_clone = self.save_table_result_sender.clone();
if add_table_nav::handle_add_table_navigation(
key_event,
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_event,
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,
&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 app_state.ui.show_admin
&& !app_state
.profile_tree
.profiles
.is_empty()
{
admin_state
.profile_list_state
.select(Some(0));
}
format!("Intro Option {} selected", index)
}
UiContext::Login => 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(),
},
UiContext::Register => 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(),
},
UiContext::Admin => {
admin::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::ReadOnly => {
// Handle highlight mode transitions
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 = Self::get_current_field_for_state(
app_state,
login_state,
register_state,
form_state
);
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()));
}
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 = Self::get_current_field_for_state(
app_state,
login_state,
register_state,
form_state
);
let current_cursor_pos = Self::get_current_cursor_pos_for_state(
app_state,
login_state,
register_state,
form_state
);
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()));
}
// Handle edit mode transitions
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)
{
if let Some(editor) = &mut app_state.form_editor {
editor.enter_edit_mode();
}
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
self.command_message = "Edit mode".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
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 = Self::get_current_input_for_state(
app_state,
login_state,
register_state,
form_state
);
let current_cursor_pos = Self::get_cursor_pos_for_mixed_state(
app_state,
login_state,
form_state
);
// Move cursor forward if possible
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
let new_cursor_pos = current_cursor_pos + 1;
Self::set_current_cursor_pos_for_state(
app_state,
login_state,
register_state,
form_state,
new_cursor_pos
);
self.ideal_cursor_column = Self::get_current_cursor_pos_for_state(
app_state,
login_state,
register_state,
form_state
);
}
if let Some(editor) = &mut app_state.form_editor {
editor.enter_edit_mode();
}
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();
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
else if config.get_read_only_action_for_key(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_common_action(key_code, modifiers) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return self
.handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
terminal,
app_state,
)
.await;
}
_ => {}
}
}
// Try canvas action for form first
if app_state.ui.show_form {
if let Some(editor) = &mut app_state.form_editor {
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
key_event,
editor,
config,
false,
).await {
return Ok(EventOutcome::Ok(canvas_message));
}
}
}
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
AppMode::Highlight => {
if config.get_highlight_action_for_key(key_code, modifiers) == Some("exit_highlight_mode") {
if let Some(editor) = &mut app_state.form_editor {
editor.exit_highlight_mode();
}
self.highlight_state = HighlightState::Off;
self.command_message = "Exited highlight mode".to_string();
return Ok(EventOutcome::Ok(self.command_message.clone()));
} else if config.get_highlight_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") {
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()));
}
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
AppMode::Edit => {
// Handle common actions (save, quit, etc.)
if let Some(action) = config.get_common_action(key_code, modifiers) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return self
.handle_core_action(
action,
form_state,
auth_state,
login_state,
register_state,
terminal,
app_state,
)
.await;
}
_ => {}
}
}
// Try canvas action for form first
if app_state.ui.show_form {
if let Some(editor) = &mut app_state.form_editor {
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
key_event,
editor,
config,
true,
).await {
if !canvas_message.is_empty() {
self.command_message = canvas_message.clone();
}
return Ok(EventOutcome::Ok(canvas_message));
}
}
}
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
AppMode::Command => {
if config.is_exit_command_mode(key_code, modifiers) {
self.command_input.clear();
self.command_message.clear();
self.command_mode = false;
self.key_sequence_tracker.reset();
if let Some(editor) = &mut app_state.form_editor {
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 = form_state.current_position;
let total_count = form_state.total_count;
let outcome = command_mode::handle_command_event(
key_event,
config,
app_state,
login_state,
register_state,
form_state,
&mut self.command_input,
&mut self.command_message,
&mut self.grpc_client,
command_handler,
terminal,
&mut current_position,
total_count,
)
.await?;
form_state.current_position = current_position;
self.command_mode = false;
self.key_sequence_tracker.reset();
let new_mode = ModeManager::derive_mode(
app_state,
self,
admin_state,
);
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()));
}
if let KeyCode::Char(c) = key_code {
if c == 'f' {
self.key_sequence_tracker.add_key(key_code);
let sequence =
self.key_sequence_tracker.get_sequence();
if config.matches_key_sequence_generalized(
&sequence,
) == Some("find_file_palette_toggle")
{
if app_state.ui.show_form
|| app_state.ui.show_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.key_sequence_tracker.reset();
return Ok(EventOutcome::Ok(
"Table selection palette activated"
.to_string(),
));
} else {
self.key_sequence_tracker.reset();
self.command_input.push('f');
if sequence.len() > 1
&& sequence[0] == KeyCode::Char('f')
{
self.command_input.push('f');
}
self.command_message = "Find File not available in this view."
.to_string();
return Ok(EventOutcome::Ok(
self.command_message.clone(),
));
}
}
if config.is_key_sequence_prefix(&sequence) {
return Ok(EventOutcome::Ok(String::new()));
}
}
if c != 'f'
&& !self.key_sequence_tracker.current_sequence.is_empty()
{
self.key_sequence_tracker.reset();
}
self.command_input.push(c);
return Ok(EventOutcome::Ok(String::new()));
}
self.key_sequence_tracker.reset();
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_form_canvas_action(
&mut self,
key_event: KeyEvent,
editor: &mut FormEditor<FormState>,
config: &Config,
is_edit_mode: bool,
) -> Result<Option<String>> {
use crossterm::event::KeyCode;
if is_edit_mode {
if let KeyCode::Char(c) = key_event.code {
if key_event.modifiers.is_empty() || key_event.modifiers == KeyModifiers::SHIFT {
editor.insert_char(c)?;
return Ok(Some(format!("Inserted '{}'", c)));
}
}
}
// Use your config to resolve actions
if let Some(action) = config.get_edit_action_for_key(key_event.code, key_event.modifiers) {
match action {
"delete_char_backward" => { editor.delete_backward()?; return Ok(Some("Deleted backward".to_string())); }
"delete_char_forward" => { editor.delete_forward()?; return Ok(Some("Deleted forward".to_string())); }
"move_left" => { editor.move_left()?; return Ok(Some("Moved left".to_string())); }
"move_right" => { editor.move_right()?; return Ok(Some("Moved right".to_string())); }
"move_up" => { editor.move_up()?; return Ok(Some("Moved up".to_string())); }
"move_down" => { editor.move_down()?; return Ok(Some("Moved down".to_string())); }
"move_line_start" => { editor.move_line_start(); return Ok(Some("Line start".to_string())); }
"move_line_end" => { editor.move_line_end(); return Ok(Some("Line end".to_string())); }
"move_word_next" => { editor.move_word_next(); return Ok(Some("Next word".to_string())); }
"move_word_prev" => { editor.move_word_prev(); return Ok(Some("Prev word".to_string())); }
"move_word_end" => { editor.move_word_end(); return Ok(Some("Word end".to_string())); }
"move_word_end_prev" => { editor.move_word_end_prev(); return Ok(Some("Prev word end".to_string())); }
"next_field" => { editor.next_field()?; return Ok(Some("Next field".to_string())); }
"prev_field" => { editor.prev_field()?; return Ok(Some("Prev field".to_string())); }
"open_suggestions" => {
let field_index = editor.current_field();
editor.open_suggestions(field_index);
return Ok(Some("Opened suggestions".to_string()));
}
"apply_suggestion" | "enter_decider" => {
if let Some(s) = editor.apply_suggestion() {
return Ok(Some(format!("Applied suggestion: {}", s)));
} else {
return Ok(Some("No suggestion applied".to_string()));
}
}
"exit" | "exit_edit_mode" => {
editor.exit_edit_mode()?;
return Ok(Some("Exited edit mode".to_string()));
}
_ => {}
}
}
Ok(None)
}
async fn handle_core_action(
&mut self,
action: &str,
form_state: &mut FormState,
auth_state: &mut AuthState,
login_state: &mut LoginState,
register_state: &mut RegisterState,
terminal: &mut TerminalCore,
app_state: &mut AppState,
) -> Result<EventOutcome> {
match action {
"save" => {
if app_state.ui.show_login {
let message = crate::tui::functions::common::login::save(
auth_state,
login_state,
&mut self.auth_client,
app_state,
)
.await?;
Ok(EventOutcome::Ok(message))
} else {
let save_outcome = crate::tui::functions::common::form::save(
app_state,
form_state,
&mut self.grpc_client,
)
.await?;
let message = match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
};
Ok(EventOutcome::DataSaved(save_outcome, message))
}
}
"force_quit" => {
if let Some(editor) = &mut app_state.form_editor {
editor.cleanup_cursor()?;
}
terminal.cleanup()?;
Ok(EventOutcome::Exit(
"Force exiting without saving.".to_string(),
))
}
"save_and_quit" => {
let message = if app_state.ui.show_login {
crate::tui::functions::common::login::save(
auth_state,
login_state,
&mut self.auth_client,
app_state,
)
.await?
} else {
let save_outcome = crate::tui::functions::common::form::save(
app_state,
form_state,
&mut self.grpc_client,
)
.await?;
match save_outcome {
SaveOutcome::NoChange => "No changes to save.".to_string(),
SaveOutcome::UpdatedExisting => "Entry updated.".to_string(),
SaveOutcome::CreatedNew(_) => "New entry created.".to_string(),
}
};
if let Some(editor) = &mut app_state.form_editor {
editor.cleanup_cursor()?;
}
terminal.cleanup()?;
Ok(EventOutcome::Exit(format!(
"{}. Exiting application.",
message
)))
}
"revert" => {
let message = if app_state.ui.show_login {
crate::tui::functions::common::login::revert(login_state, app_state)
.await
} else if app_state.ui.show_register {
crate::tui::functions::common::register::revert(
register_state,
app_state,
)
.await
} else {
crate::tui::functions::common::form::revert(
form_state,
&mut self.grpc_client,
)
.await?
};
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"
)
}
}