1087 lines
44 KiB
Rust
1087 lines
44 KiB
Rust
// src/modes/handlers/event.rs
|
|
use crate::config::binds::config::Config;
|
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
|
use crate::buffer::{AppView, BufferState, switch_buffer, functions, toggle_buffer_list};
|
|
use crate::sidebar::toggle_sidebar;
|
|
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::{
|
|
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 canvas::KeyEventOutcome;
|
|
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 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,
|
|
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,
|
|
app_state: &mut AppState,
|
|
) -> Result<EventOutcome> {
|
|
let mut should_close = false;
|
|
let mut outcome_message = String::new();
|
|
let mut trigger_search = false;
|
|
|
|
// Step 1: Handle search_state logic in a short scope
|
|
let (maybe_data, maybe_id) = {
|
|
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();
|
|
(None, None)
|
|
}
|
|
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)
|
|
{
|
|
(Some(data), Some(selected_hit.id))
|
|
} else {
|
|
(None, None)
|
|
}
|
|
} else {
|
|
(None, None)
|
|
}
|
|
}
|
|
KeyCode::Up => {
|
|
search_state.previous_result();
|
|
(None, None)
|
|
}
|
|
KeyCode::Down => {
|
|
search_state.next_result();
|
|
(None, None)
|
|
}
|
|
KeyCode::Char(c) => {
|
|
search_state.input.insert(search_state.cursor_position, c);
|
|
search_state.cursor_position += 1;
|
|
trigger_search = true;
|
|
(None, None)
|
|
}
|
|
KeyCode::Backspace => {
|
|
if search_state.cursor_position > 0 {
|
|
search_state.cursor_position -= 1;
|
|
search_state.input.remove(search_state.cursor_position);
|
|
trigger_search = true;
|
|
}
|
|
(None, None)
|
|
}
|
|
KeyCode::Left => {
|
|
search_state.cursor_position =
|
|
search_state.cursor_position.saturating_sub(1);
|
|
(None, None)
|
|
}
|
|
KeyCode::Right => {
|
|
if search_state.cursor_position < search_state.input.len() {
|
|
search_state.cursor_position += 1;
|
|
}
|
|
(None, None)
|
|
}
|
|
_ => (None, None),
|
|
}
|
|
} else {
|
|
(None, None)
|
|
}
|
|
};
|
|
|
|
// Step 2: Now safe to borrow form_state
|
|
if let (Some(data), Some(id)) = (maybe_data, maybe_id) {
|
|
if let Some(fs) = app_state.form_state_mut() {
|
|
let detached_pos = fs.total_count + 2;
|
|
fs.update_from_response(&data, detached_pos);
|
|
}
|
|
should_close = true;
|
|
outcome_message = format!("Loaded record ID {}", id);
|
|
}
|
|
|
|
// Step 3: Trigger async search if needed
|
|
if trigger_search {
|
|
if let Some(search_state) = app_state.search_state.as_mut() {
|
|
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,
|
|
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, 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 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 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 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));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
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,
|
|
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 => {
|
|
// First let the canvas editor try to handle the key
|
|
if app_state.ui.show_form {
|
|
if let Some(editor) = &mut app_state.form_editor {
|
|
let outcome = editor.handle_key_event(key_event);
|
|
let new_mode = AppMode::from(editor.mode());
|
|
match outcome {
|
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(msg));
|
|
}
|
|
KeyEventOutcome::Consumed(None) => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(String::new()));
|
|
}
|
|
KeyEventOutcome::Pending => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(String::new()));
|
|
}
|
|
KeyEventOutcome::NotMatched => {
|
|
app_state.update_mode(new_mode);
|
|
// Fall through
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Entering command mode is still a client-level action
|
|
if config.get_app_action(key_code, modifiers) == Some("enter_command_mode")
|
|
&& ModeManager::can_enter_command_mode(current_mode)
|
|
{
|
|
if let Some(editor) = &mut app_state.form_editor {
|
|
editor.set_mode(CanvasMode::Command);
|
|
}
|
|
self.command_mode = true;
|
|
self.command_input.clear();
|
|
self.command_message.clear();
|
|
return Ok(EventOutcome::Ok(String::new()));
|
|
}
|
|
|
|
// Handle common actions (save, quit, etc.)
|
|
if let Some(action) = config.get_app_action(key_code, modifiers) {
|
|
match action {
|
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
|
return self
|
|
.handle_core_action(
|
|
action,
|
|
auth_state,
|
|
login_state,
|
|
register_state,
|
|
terminal,
|
|
app_state,
|
|
)
|
|
.await;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
}
|
|
|
|
AppMode::Highlight => {
|
|
if app_state.ui.show_form {
|
|
if let Some(editor) = &mut app_state.form_editor {
|
|
let outcome = editor.handle_key_event(key_event);
|
|
let new_mode = AppMode::from(editor.mode());
|
|
match outcome {
|
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(msg));
|
|
}
|
|
KeyEventOutcome::Consumed(None) => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(String::new()));
|
|
}
|
|
KeyEventOutcome::Pending => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(String::new()));
|
|
}
|
|
KeyEventOutcome::NotMatched => {
|
|
app_state.update_mode(new_mode);
|
|
// Fall through
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
}
|
|
|
|
AppMode::Edit => {
|
|
// Handle common actions (save, quit, etc.)
|
|
if let Some(action) = config.get_app_action(key_code, modifiers) {
|
|
match action {
|
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
|
return self
|
|
.handle_core_action(
|
|
action,
|
|
auth_state,
|
|
login_state,
|
|
register_state,
|
|
terminal,
|
|
app_state,
|
|
)
|
|
.await;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Let the canvas editor handle edit-mode keys
|
|
if app_state.ui.show_form {
|
|
if let Some(editor) = &mut app_state.form_editor {
|
|
let outcome = editor.handle_key_event(key_event);
|
|
let new_mode = AppMode::from(editor.mode());
|
|
match outcome {
|
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
self.command_message = msg.clone();
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(msg));
|
|
}
|
|
KeyEventOutcome::Consumed(None) => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(String::new()));
|
|
}
|
|
KeyEventOutcome::Pending => {
|
|
app_state.update_mode(new_mode);
|
|
return Ok(EventOutcome::Ok(String::new()));
|
|
}
|
|
KeyEventOutcome::NotMatched => {
|
|
app_state.update_mode(new_mode);
|
|
// Fall through
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
}
|
|
|
|
AppMode::Command => {
|
|
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, total_count) = if let Some(fs) = app_state.form_state() {
|
|
(fs.current_position, fs.total_count)
|
|
} else {
|
|
(1, 0)
|
|
};
|
|
|
|
let outcome = command_mode::handle_command_event(
|
|
key_event,
|
|
config,
|
|
app_state,
|
|
login_state,
|
|
register_state,
|
|
&mut self.command_input,
|
|
&mut self.command_message,
|
|
&mut self.grpc_client,
|
|
command_handler,
|
|
terminal,
|
|
&mut current_position,
|
|
total_count,
|
|
).await?;
|
|
if let Some(fs) = app_state.form_state_mut() {
|
|
fs.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_core_action(
|
|
&mut self,
|
|
action: &str,
|
|
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 = if let Some(fs) = app_state.form_state_mut() {
|
|
crate::tui::functions::common::form::save(
|
|
app_state,
|
|
&mut self.grpc_client,
|
|
)
|
|
.await?
|
|
} else {
|
|
SaveOutcome::NoChange
|
|
};
|
|
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,
|
|
&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 {
|
|
if let Some(fs) = app_state.form_state_mut() {
|
|
crate::tui::functions::common::form::revert(
|
|
app_state,
|
|
&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"
|
|
)
|
|
}
|
|
}
|