From db938a2c8da2f609a60e92ddacb5afa29d5ddd86 Mon Sep 17 00:00:00 2001 From: Priec Date: Tue, 29 Jul 2025 15:20:00 +0200 Subject: [PATCH] canvas library usage instead of internal canvas on the form, others are still using canvas from state. Needed debugging because its not working yet --- client/Cargo.toml | 1 + client/src/components/form/form.rs | 4 +- client/src/components/handlers/canvas.rs | 93 +++++- client/src/functions/modes/edit/form_e.rs | 2 +- .../src/functions/modes/read_only/form_ro.rs | 2 +- client/src/modes/canvas/edit.rs | 60 +++- client/src/modes/canvas/read_only.rs | 101 +++++- client/src/modes/handlers.rs | 3 +- client/src/modes/handlers/event.rs | 316 +++++++++++++++--- client/src/modes/handlers/event_helper.rs | 105 ++++++ client/src/state/pages/form.rs | 95 +++++- client/src/tui/functions/form.rs | 2 +- 12 files changed, 677 insertions(+), 107 deletions(-) create mode 100644 client/src/modes/handlers/event_helper.rs diff --git a/client/Cargo.toml b/client/Cargo.toml index a39f1ec..2668585 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true anyhow = { workspace = true } async-trait = "0.1.88" common = { path = "../common" } +canvas = { path = "../canvas" } ratatui = { workspace = true } crossterm = { workspace = true } diff --git a/client/src/components/form/form.rs b/client/src/components/form/form.rs index 181837f..33e40f0 100644 --- a/client/src/components/form/form.rs +++ b/client/src/components/form/form.rs @@ -3,7 +3,7 @@ use crate::components::common::autocomplete; use crate::components::handlers::canvas::render_canvas; use crate::config::colors::themes::Theme; use crate::state::app::highlight::HighlightState; -use crate::state::pages::canvas_state::CanvasState; +use canvas::CanvasState; use crate::state::pages::form::FormState; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, @@ -64,7 +64,7 @@ pub fn render_form( f.render_widget(count_para, main_layout[0]); // Get the active field's rect from render_canvas - let active_field_rect = render_canvas( + let active_field_rect = crate::components::handlers::canvas::render_canvas_library( f, main_layout[1], form_state, diff --git a/client/src/components/handlers/canvas.rs b/client/src/components/handlers/canvas.rs index a218c79..8d31c63 100644 --- a/client/src/components/handlers/canvas.rs +++ b/client/src/components/handlers/canvas.rs @@ -9,13 +9,15 @@ use ratatui::{ }; use crate::config::colors::themes::Theme; use crate::state::app::highlight::HighlightState; -use crate::state::pages::canvas_state::CanvasState; +use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState; +use canvas::CanvasState as LibraryCanvasState; use std::cmp::{max, min}; +/// Render canvas for legacy CanvasState (AddTableState, LoginState, RegisterState, AddLogicState) pub fn render_canvas( f: &mut Frame, area: Rect, - form_state: &impl CanvasState, + form_state: &impl LegacyCanvasState, fields: &[&str], current_field_idx: &usize, inputs: &[&String], @@ -23,12 +25,75 @@ pub fn render_canvas( is_edit_mode: bool, highlight_state: &HighlightState, ) -> Option { + render_canvas_impl( + f, + area, + fields, + current_field_idx, + inputs, + theme, + is_edit_mode, + highlight_state, + form_state.current_cursor_pos(), + form_state.has_unsaved_changes(), + |i| form_state.get_display_value_for_field(i).to_string(), + |i| form_state.has_display_override(i), + ) +} + +/// Render canvas for library CanvasState (FormState) +pub fn render_canvas_library( + f: &mut Frame, + area: Rect, + form_state: &impl LibraryCanvasState, + fields: &[&str], + current_field_idx: &usize, + inputs: &[&String], + theme: &Theme, + is_edit_mode: bool, + highlight_state: &HighlightState, +) -> Option { + render_canvas_impl( + f, + area, + fields, + current_field_idx, + inputs, + theme, + is_edit_mode, + highlight_state, + form_state.current_cursor_pos(), + form_state.has_unsaved_changes(), + |i| form_state.get_display_value_for_field(i).to_string(), + |i| form_state.has_display_override(i), + ) +} + +/// Internal implementation shared by both render functions +fn render_canvas_impl( + f: &mut Frame, + area: Rect, + fields: &[&str], + current_field_idx: &usize, + inputs: &[&String], + theme: &Theme, + is_edit_mode: bool, + highlight_state: &HighlightState, + current_cursor_pos: usize, + has_unsaved_changes: bool, + get_display_value: F1, + has_display_override: F2, +) -> Option +where + F1: Fn(usize) -> String, + F2: Fn(usize) -> bool, +{ let columns = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) .split(area); - let border_style = if form_state.has_unsaved_changes() { + let border_style = if has_unsaved_changes { Style::default().fg(theme.warning) } else if is_edit_mode { Style::default().fg(theme.accent) @@ -75,17 +140,16 @@ pub fn render_canvas( for (i, _input) in inputs.iter().enumerate() { let is_active = i == *current_field_idx; - let current_cursor_pos = form_state.current_cursor_pos(); - // Use the trait method to get display value - let text = form_state.get_display_value_for_field(i); + // Use the provided closure to get display value + let text = get_display_value(i); let text_len = text.chars().count(); let line: Line; match highlight_state { HighlightState::Off => { line = Line::from(Span::styled( - text, + &text, if is_active { Style::default().fg(theme.highlight) } else { @@ -141,11 +205,11 @@ pub fn render_canvas( Span::styled(after, normal_style_in_highlight), ]); } else { - line = Line::from(Span::styled(text, highlight_style)); + line = Line::from(Span::styled(&text, highlight_style)); } } else { line = Line::from(Span::styled( - text, + &text, if is_active { normal_style_in_highlight } else { normal_style_outside } )); } @@ -158,10 +222,10 @@ pub fn render_canvas( let normal_style_outside = Style::default().fg(theme.fg); if i >= start_field && i <= end_field { - line = Line::from(Span::styled(text, highlight_style)); + line = Line::from(Span::styled(&text, highlight_style)); } else { line = Line::from(Span::styled( - text, + &text, if is_active { normal_style_in_highlight } else { normal_style_outside } )); } @@ -174,14 +238,13 @@ pub fn render_canvas( if is_active { active_field_input_rect = Some(input_rows[i]); - // --- CORRECTED CURSOR POSITIONING LOGIC --- - // Use the new generic trait method to check for an override. - let cursor_x = if form_state.has_display_override(i) { + // Use the provided closure to check for display override + let cursor_x = if has_display_override(i) { // If an override exists, place the cursor at the end. input_rows[i].x + text.chars().count() as u16 } else { // Otherwise, use the real cursor position. - input_rows[i].x + form_state.current_cursor_pos() as u16 + input_rows[i].x + current_cursor_pos as u16 }; let cursor_y = input_rows[i].y; f.set_cursor_position((cursor_x, cursor_y)); diff --git a/client/src/functions/modes/edit/form_e.rs b/client/src/functions/modes/edit/form_e.rs index a9cf483..5916be1 100644 --- a/client/src/functions/modes/edit/form_e.rs +++ b/client/src/functions/modes/edit/form_e.rs @@ -1,13 +1,13 @@ // src/functions/modes/edit/form_e.rs use crate::services::grpc_client::GrpcClient; -use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::form::FormState; use crate::state::app::state::AppState; use crate::tui::functions::common::form::{revert, save}; use crate::tui::functions::common::form::SaveOutcome; use crate::modes::handlers::event::EventOutcome; use crossterm::event::{KeyCode, KeyEvent}; +use canvas::CanvasState; use std::any::Any; use anyhow::Result; diff --git a/client/src/functions/modes/read_only/form_ro.rs b/client/src/functions/modes/read_only/form_ro.rs index da3ecda..d13abd1 100644 --- a/client/src/functions/modes/read_only/form_ro.rs +++ b/client/src/functions/modes/read_only/form_ro.rs @@ -1,7 +1,7 @@ // src/functions/modes/read_only/form_ro.rs use crate::config::binds::key_sequences::KeySequenceTracker; -use crate::state::pages::canvas_state::CanvasState; +use canvas::CanvasState; use anyhow::Result; #[derive(PartialEq)] diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index fefe7f9..42c20a6 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -9,9 +9,10 @@ use crate::state::app::state::AppState; use crate::state::pages::admin::AdminState; use crate::state::pages::{ auth::{LoginState, RegisterState}, - canvas_state::CanvasState, form::FormState, }; +use canvas::CanvasState; +use canvas::{CanvasAction, ActionDispatcher, ActionResult}; use anyhow::Result; use common::proto::komp_ac::search::search_response::Hit; use crossterm::event::{KeyCode, KeyEvent}; @@ -74,6 +75,57 @@ async fn trigger_form_autocomplete_search( } } +pub async fn handle_form_edit_with_canvas( + key_event: KeyEvent, + config: &Config, + form_state: &mut FormState, + ideal_cursor_column: &mut usize, +) -> Result { + // Try canvas action from key first + if let Some(canvas_action) = CanvasAction::from_key(key_event.code) { + match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(_) => { + // Fall through to try config mapping + } + } + } + + // Try config-mapped action + if let Some(action_str) = config.get_edit_action_for_key(key_event.code, key_event.modifiers) { + let canvas_action = CanvasAction::from_string(&action_str); + match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(e) => { + return Ok(format!("Action failed: {}", e)); + } + } + } + + Ok(String::new()) +} #[allow(clippy::too_many_arguments)] pub async fn handle_edit_event( @@ -118,7 +170,7 @@ pub async fn handle_edit_event( return Ok(EditEventOutcome::Message(String::new())); } "exit" => { - form_state.deactivate_autocomplete(); + form_state.deactivate_suggestions(); return Ok(EditEventOutcome::Message( "Autocomplete cancelled".to_string(), )); @@ -150,14 +202,14 @@ pub async fn handle_edit_event( ); // 4. Finalize state - form_state.deactivate_autocomplete(); + form_state.deactivate_suggestions(); form_state.set_has_unsaved_changes(true); return Ok(EditEventOutcome::Message( "Selection made".to_string(), )); } } - form_state.deactivate_autocomplete(); + form_state.deactivate_suggestions(); // Fall through to default 'enter' behavior } _ => {} // Let other keys fall through to the live search logic diff --git a/client/src/modes/canvas/read_only.rs b/client/src/modes/canvas/read_only.rs index c8705de..4a421e3 100644 --- a/client/src/modes/canvas/read_only.rs +++ b/client/src/modes/canvas/read_only.rs @@ -10,9 +10,62 @@ use crate::state::pages::add_logic::AddLogicState; use crate::state::pages::add_table::AddTableState; use crate::state::app::state::AppState; use crate::functions::modes::read_only::{add_logic_ro, auth_ro, form_ro, add_table_ro}; +use canvas::{CanvasAction, ActionDispatcher, ActionResult}; use crossterm::event::KeyEvent; use anyhow::Result; +pub async fn handle_form_readonly_with_canvas( + key_event: KeyEvent, + config: &Config, + form_state: &mut FormState, + ideal_cursor_column: &mut usize, +) -> Result { + // Try canvas action from key first + if let Some(canvas_action) = CanvasAction::from_key(key_event.code) { + match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(_) => { + // Fall through to try config mapping + } + } + } + + // Try config-mapped action + if let Some(action_str) = config.get_read_only_action_for_key(key_event.code, key_event.modifiers) { + let canvas_action = CanvasAction::from_string(&action_str); + match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(e) => { + return Ok(format!("Action failed: {}", e)); + } + } + } + + Ok(String::new()) +} + pub async fn handle_read_only_event( app_state: &mut AppState, key: KeyEvent, @@ -36,18 +89,46 @@ pub async fn handle_read_only_event( if config.is_enter_edit_mode_after(key.code, key.modifiers) { // Determine target state to adjust cursor - let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state } - else if app_state.ui.show_add_logic { add_logic_state } - else if app_state.ui.show_register { register_state } - else if app_state.ui.show_add_table { add_table_state } - else { form_state }; - let current_input = target_state.get_current_input(); - let current_pos = target_state.current_cursor_pos(); - if !current_input.is_empty() && current_pos < current_input.len() { - target_state.set_current_cursor_pos(current_pos + 1); - *ideal_cursor_column = target_state.current_cursor_pos(); + if app_state.ui.show_login { + let current_input = login_state.get_current_input(); + let current_pos = login_state.current_cursor_pos(); + if !current_input.is_empty() && current_pos < current_input.len() { + login_state.set_current_cursor_pos(current_pos + 1); + *ideal_cursor_column = login_state.current_cursor_pos(); + } + } else if app_state.ui.show_add_logic { + let current_input = add_logic_state.get_current_input(); + let current_pos = add_logic_state.current_cursor_pos(); + if !current_input.is_empty() && current_pos < current_input.len() { + add_logic_state.set_current_cursor_pos(current_pos + 1); + *ideal_cursor_column = add_logic_state.current_cursor_pos(); + } + } else if app_state.ui.show_register { + let current_input = register_state.get_current_input(); + let current_pos = register_state.current_cursor_pos(); + if !current_input.is_empty() && current_pos < current_input.len() { + register_state.set_current_cursor_pos(current_pos + 1); + *ideal_cursor_column = register_state.current_cursor_pos(); + } + } else if app_state.ui.show_add_table { + let current_input = add_table_state.get_current_input(); + let current_pos = add_table_state.current_cursor_pos(); + if !current_input.is_empty() && current_pos < current_input.len() { + add_table_state.set_current_cursor_pos(current_pos + 1); + *ideal_cursor_column = add_table_state.current_cursor_pos(); + } + } else { + // Handle FormState (uses library CanvasState) + use canvas::CanvasState as LibraryCanvasState; // Import at the top of the function + let current_input = form_state.get_current_input(); + let current_pos = form_state.current_cursor_pos(); + if !current_input.is_empty() && current_pos < current_input.len() { + form_state.set_current_cursor_pos(current_pos + 1); + *ideal_cursor_column = form_state.current_cursor_pos(); + } } + *edit_mode_cooldown = true; *command_message = "Entering Edit mode (after cursor)".to_string(); return Ok((false, command_message.clone())); diff --git a/client/src/modes/handlers.rs b/client/src/modes/handlers.rs index 8210779..f78843c 100644 --- a/client/src/modes/handlers.rs +++ b/client/src/modes/handlers.rs @@ -1,3 +1,4 @@ -// src/client/modes/handlers.rs +// src/modes/handlers.rs pub mod event; +pub mod event_helper; pub mod mode_manager; diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index b2a130d..f3078b0 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -15,8 +15,12 @@ use crate::modes::{ general::{dialog, navigation}, handlers::mode_manager::{AppMode, ModeManager}, }; +use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState; use crate::services::auth::AuthClient; use crate::services::grpc_client::GrpcClient; +use canvas::{CanvasAction, ActionDispatcher, ActionResult}; +use canvas::CanvasState as LibraryCanvasState; +use super::event_helper::*; use crate::state::{ app::{ buffer::{AppView, BufferState}, @@ -573,55 +577,106 @@ impl EventHandler { } AppMode::ReadOnly => { - 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 }; + // 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 = 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 = 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() }; + } + 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 = get_current_field_for_state( + app_state, + login_state, + register_state, + form_state + ); + let current_cursor_pos = 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())); - } 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) { + } + + // 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) + { 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())); - } 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() }; + } + 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 = get_current_input_for_state( + app_state, + login_state, + register_state, + form_state + ); + let current_cursor_pos = 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() { - 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(); - } + let new_cursor_pos = current_cursor_pos + 1; + set_current_cursor_pos_for_state( + app_state, + login_state, + register_state, + form_state, + new_cursor_pos + ); + self.ideal_cursor_column = get_current_cursor_pos_for_state( + app_state, + login_state, + register_state, + form_state + ); } + 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())); - } else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode") && ModeManager::can_enter_command_mode(current_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())); } - if let Some(action) = - config.get_common_action(key_code, modifiers) - { + // 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" => { + "save" | "force_quit" | "save_and_quit" | "revert" => { return common_mode::handle_core_action( action, form_state, @@ -639,23 +694,36 @@ impl EventHandler { } } - let (_should_exit, message) = - read_only::handle_read_only_event( - app_state, + // Try canvas action for form first (NEW: Canvas library integration) + if app_state.ui.show_form { + if let Ok(Some(canvas_message)) = self.handle_form_canvas_action( key_event, config, form_state, - login_state, - register_state, - &mut admin_state.add_table_state, - &mut admin_state.add_logic_state, - &mut self.key_sequence_tracker, - &mut self.grpc_client, // <-- FIX 1 - &mut self.command_message, - &mut self.edit_mode_cooldown, - &mut self.ideal_cursor_column, - ) - .await?; + false, // not edit mode + ).await { + return Ok(EventOutcome::Ok(canvas_message)); + } + } + + // Fallback to legacy read-only event handling + let (_should_exit, message) = read_only::handle_read_only_event( + app_state, + key_event, + config, + form_state, + login_state, + register_state, + &mut admin_state.add_table_state, + &mut admin_state.add_logic_state, + &mut self.key_sequence_tracker, + &mut self.grpc_client, + &mut self.command_message, + &mut self.edit_mode_cooldown, + &mut self.ideal_cursor_column, + ) + .await?; + return Ok(EventOutcome::Ok(message)); } @@ -695,12 +763,10 @@ impl EventHandler { } AppMode::Edit => { - if let Some(action) = - config.get_common_action(key_code, modifiers) - { + // 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" => { + "save" | "force_quit" | "save_and_quit" | "revert" => { return common_mode::handle_core_action( action, form_state, @@ -718,9 +784,25 @@ impl EventHandler { } } + // Try canvas action for form first (NEW: Canvas library integration) + if app_state.ui.show_form { + if let Ok(Some(canvas_message)) = self.handle_form_canvas_action( + key_event, + config, + form_state, + true, // edit mode + ).await { + if !canvas_message.is_empty() { + self.command_message = canvas_message.clone(); + } + return Ok(EventOutcome::Ok(canvas_message)); + } + } + + // Handle legacy edit events let mut current_position = form_state.current_position; let total_count = form_state.total_count; - // --- MODIFIED: Pass `self` instead of `grpc_client` --- + let edit_result = edit::handle_edit_event( key_event, config, @@ -739,30 +821,62 @@ impl EventHandler { Ok(edit::EditEventOutcome::ExitEditMode) => { 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() }; + + // Check for unsaved changes across all states + let has_changes = get_has_unsaved_changes_for_state( + app_state, + login_state, + register_state, + form_state + ); + + // Set appropriate message based on 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)?; - 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() }; + + // Get current input and cursor position + let current_input = get_current_input_for_state( + app_state, + login_state, + register_state, + form_state + ); + let current_cursor_pos = get_current_cursor_pos_for_state( + app_state, + login_state, + register_state, + form_state + ); + + // Adjust cursor if it's beyond the input length 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); + set_current_cursor_pos_for_state( + app_state, + login_state, + register_state, + form_state, + new_pos + ); self.ideal_cursor_column = new_pos; } - return Ok(EventOutcome::Ok( - self.command_message.clone(), - )); + + return Ok(EventOutcome::Ok(self.command_message.clone())); } + Ok(edit::EditEventOutcome::Message(msg)) => { if !msg.is_empty() { self.command_message = msg; } self.key_sequence_tracker.reset(); - return Ok(EventOutcome::Ok( - self.command_message.clone(), - )); + return Ok(EventOutcome::Ok(self.command_message.clone())); } + Err(e) => { return Err(e.into()); } @@ -906,4 +1020,96 @@ impl EventHandler { 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, + config: &Config, + form_state: &mut FormState, + is_edit_mode: bool, + ) -> Result> { + // Handle suggestion actions first if suggestions are active + if form_state.autocomplete_active { + match key_event.code { + KeyCode::Up => { + if let Ok(result) = ActionDispatcher::dispatch( + CanvasAction::SuggestionUp, + form_state, + &mut self.ideal_cursor_column, + ).await { + return Ok(result.message().map(|s| s.to_string())); + } + } + KeyCode::Down => { + if let Ok(result) = ActionDispatcher::dispatch( + CanvasAction::SuggestionDown, + form_state, + &mut self.ideal_cursor_column, + ).await { + return Ok(result.message().map(|s| s.to_string())); + } + } + KeyCode::Enter => { + if let Ok(result) = ActionDispatcher::dispatch( + CanvasAction::SelectSuggestion, + form_state, + &mut self.ideal_cursor_column, + ).await { + return Ok(result.message().map(|s| s.to_string())); + } + } + KeyCode::Esc => { + if let Ok(result) = ActionDispatcher::dispatch( + CanvasAction::ExitSuggestions, + form_state, + &mut self.ideal_cursor_column, + ).await { + return Ok(result.message().map(|s| s.to_string())); + } + } + _ => {} + } + } + + // Try to create canvas action from key + if let Some(canvas_action) = CanvasAction::from_key(key_event.code) { + match ActionDispatcher::dispatch( + canvas_action, + form_state, + &mut self.ideal_cursor_column, + ).await { + Ok(result) => { + return Ok(result.message().map(|s| s.to_string())); + } + Err(_) => { + // Fall through to try config mapping + } + } + } + + // Try mapped actions from config + let action_str = if is_edit_mode { + config.get_edit_action_for_key(key_event.code, key_event.modifiers) + } else { + config.get_read_only_action_for_key(key_event.code, key_event.modifiers) + }; + + if let Some(action_str) = action_str { + let canvas_action = CanvasAction::from_string(&action_str); + match ActionDispatcher::dispatch( + canvas_action, + form_state, + &mut self.ideal_cursor_column, + ).await { + Ok(result) => { + return Ok(result.message().map(|s| s.to_string())); + } + Err(_) => { + // Ignore error, let existing code handle it + } + } + } + + Ok(None) + } } diff --git a/client/src/modes/handlers/event_helper.rs b/client/src/modes/handlers/event_helper.rs new file mode 100644 index 0000000..e45e8b9 --- /dev/null +++ b/client/src/modes/handlers/event_helper.rs @@ -0,0 +1,105 @@ + +// src/modes/handlers/event_helper.rs +//! Helper functions to handle the differences between legacy and library CanvasState traits + +use crate::state::app::state::AppState; +use crate::state::pages::{ + form::FormState, + auth::{LoginState, RegisterState}, +}; +use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState; +use canvas::CanvasState as LibraryCanvasState; + +/// Get the current field index from the appropriate state based on which UI is active +pub 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() // Uses LegacyCanvasState + } else if app_state.ui.show_register { + register_state.current_field() // Uses LegacyCanvasState + } else { + form_state.current_field() // Uses LibraryCanvasState + } +} + +/// Get the current cursor position from the appropriate state based on which UI is active +pub 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() // Uses LegacyCanvasState + } else if app_state.ui.show_register { + register_state.current_cursor_pos() // Uses LegacyCanvasState + } else { + form_state.current_cursor_pos() // Uses LibraryCanvasState + } +} + +/// Check if the appropriate state has unsaved changes based on which UI is active +pub 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() // Uses LegacyCanvasState + } else if app_state.ui.show_register { + register_state.has_unsaved_changes() // Uses LegacyCanvasState + } else { + form_state.has_unsaved_changes() // Uses LibraryCanvasState + } +} + +/// Get the current input from the appropriate state based on which UI is active +pub 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() // Uses LegacyCanvasState + } else if app_state.ui.show_register { + register_state.get_current_input() // Uses LegacyCanvasState + } else { + form_state.get_current_input() // Uses LibraryCanvasState + } +} + +/// Set the cursor position for the appropriate state based on which UI is active +pub 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); // Uses LegacyCanvasState + } else if app_state.ui.show_register { + register_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState + } else { + form_state.set_current_cursor_pos(pos); // Uses LibraryCanvasState + } +} + +/// Get cursor position for mixed login/register vs form logic +pub 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() // Uses LegacyCanvasState + } else { + form_state.current_cursor_pos() // Uses LibraryCanvasState + } +} diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index bcb0c5b..05d3f50 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -2,7 +2,7 @@ use crate::config::colors::themes::Theme; use crate::state::app::highlight::HighlightState; -use crate::state::pages::canvas_state::CanvasState; +use canvas::{CanvasState, CanvasAction, ActionContext}; // CHANGED: Use canvas crate use common::proto::komp_ac::search::search_response::Hit; use ratatui::layout::Rect; use ratatui::Frame; @@ -146,7 +146,7 @@ impl FormState { } else { self.current_position = 1; } - self.deactivate_autocomplete(); + self.deactivate_suggestions(); // CHANGED: Use canvas trait method self.link_display_map.clear(); } @@ -205,14 +205,25 @@ impl FormState { self.has_unsaved_changes = false; self.current_field = 0; self.current_cursor_pos = 0; - self.deactivate_autocomplete(); + self.deactivate_suggestions(); // CHANGED: Use canvas trait method self.link_display_map.clear(); } - pub fn deactivate_autocomplete(&mut self) { - self.autocomplete_active = false; - self.autocomplete_suggestions.clear(); - self.selected_suggestion_index = None; + // REMOVED: deactivate_autocomplete() - now using trait method + + // NEW: Keep the rich suggestions methods for compatibility + pub fn get_rich_suggestions(&self) -> Option<&[Hit]> { + if self.autocomplete_active { + Some(&self.autocomplete_suggestions) + } else { + None + } + } + + pub fn activate_rich_suggestions(&mut self, suggestions: Vec) { + self.autocomplete_suggestions = suggestions; + self.autocomplete_active = !self.autocomplete_suggestions.is_empty(); + self.selected_suggestion_index = if self.autocomplete_active { Some(0) } else { None }; self.autocomplete_loading = false; } } @@ -221,49 +232,54 @@ impl CanvasState for FormState { fn current_field(&self) -> usize { self.current_field } + fn current_cursor_pos(&self) -> usize { self.current_cursor_pos } + fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } + fn inputs(&self) -> Vec<&String> { self.values.iter().collect() } + fn get_current_input(&self) -> &str { FormState::get_current_input(self) } + fn get_current_input_mut(&mut self) -> &mut String { FormState::get_current_input_mut(self) } + fn fields(&self) -> Vec<&str> { self.fields .iter() .map(|f| f.display_name.as_str()) .collect() } + fn set_current_field(&mut self, index: usize) { if index < self.fields.len() { self.current_field = index; } - self.deactivate_autocomplete(); + self.deactivate_suggestions(); // CHANGED: Use canvas trait method } + fn set_current_cursor_pos(&mut self, pos: usize) { self.current_cursor_pos = pos; } + fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_unsaved_changes = changed; } + + // --- CANVAS CRATE SUGGESTIONS SUPPORT --- fn get_suggestions(&self) -> Option<&[String]> { - None - } - fn get_rich_suggestions(&self) -> Option<&[Hit]> { - if self.autocomplete_active { - Some(&self.autocomplete_suggestions) - } else { - None - } + None // We use rich suggestions instead } + fn get_selected_suggestion_index(&self) -> Option { if self.autocomplete_active { self.selected_suggestion_index @@ -272,6 +288,52 @@ impl CanvasState for FormState { } } + fn set_selected_suggestion_index(&mut self, index: Option) { + if self.autocomplete_active { + self.selected_suggestion_index = index; + } + } + + fn activate_suggestions(&mut self, suggestions: Vec) { + // For compatibility - convert to rich format if needed + self.autocomplete_active = true; + self.selected_suggestion_index = if suggestions.is_empty() { None } else { Some(0) }; + } + + fn deactivate_suggestions(&mut self) { + self.autocomplete_active = false; + self.autocomplete_suggestions.clear(); + self.selected_suggestion_index = None; + self.autocomplete_loading = false; + } + + // --- FEATURE-SPECIFIC ACTION HANDLING --- + fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { + match action { + CanvasAction::SelectSuggestion => { + if let Some(selected_idx) = self.selected_suggestion_index { + if let Some(hit) = self.autocomplete_suggestions.get(selected_idx).cloned() { // ADD .cloned() + // Extract the value from the selected suggestion + if let Ok(content_map) = serde_json::from_str::>(&hit.content_json) { + let current_field_def = &self.fields[self.current_field]; + if let Some(value) = content_map.get(¤t_field_def.data_key) { + let new_value = json_value_to_string(value); + let display_name = self.get_display_name_for_hit(&hit); // Calculate first + *self.get_current_input_mut() = new_value.clone(); + self.set_current_cursor_pos(new_value.len()); + self.set_has_unsaved_changes(true); + self.deactivate_suggestions(); + return Some(format!("Selected: {}", display_name)); // Use calculated value + } + } + } + } + None + } + _ => None, // Let canvas handle other actions + } + } + fn get_display_value_for_field(&self, index: usize) -> &str { if let Some(display_text) = self.link_display_map.get(&index) { return display_text.as_str(); @@ -282,7 +344,6 @@ impl CanvasState for FormState { .unwrap_or("") } - // --- IMPLEMENT THE NEW TRAIT METHOD --- fn has_display_override(&self, index: usize) -> bool { self.link_display_map.contains_key(&index) } diff --git a/client/src/tui/functions/form.rs b/client/src/tui/functions/form.rs index f081d44..d0676b8 100644 --- a/client/src/tui/functions/form.rs +++ b/client/src/tui/functions/form.rs @@ -1,7 +1,7 @@ // src/tui/functions/form.rs -use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::form::FormState; use crate::services::grpc_client::GrpcClient; +use canvas::CanvasState; use anyhow::{anyhow, Result}; pub async fn handle_action(