diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 9654927..3027855 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -305,94 +305,33 @@ impl EventHandler { if !overlay_active { if let Page::Login(login_page) = &mut router.current { - use crossterm::event::{KeyCode, KeyModifiers}; - - // Inside canvas: at the last field, 'j' or Down moves focus to buttons - if !app_state.ui.focus_outside_canvas { - let last_idx = login_page - .editor - .data_provider() - .field_count() - .saturating_sub(1); - let at_last = login_page.editor.current_field() >= last_idx; - if at_last - && matches!( - (key_code, modifiers), - (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) - ) - { - app_state.ui.focus_outside_canvas = true; - app_state.focused_button_index = 0; // focus "Login" button - // Ensure canvas mode is ReadOnly when leaving - login_page.editor.set_mode(CanvasMode::ReadOnly); - return Ok(EventOutcome::Ok("Focus moved to buttons".to_string())); - } - } - - // Only forward to the canvas while focus is inside it - if !app_state.ui.focus_outside_canvas { - match login_page.handle_key_event(key_event) { - KeyEventOutcome::Consumed(Some(msg)) => { - self.command_message = msg; - return Ok(EventOutcome::Ok("Login input updated".to_string())); - } - KeyEventOutcome::Consumed(None) => { - return Ok(EventOutcome::Ok("Login input updated".to_string())); - } - KeyEventOutcome::Pending => { - return Ok(EventOutcome::Ok("Waiting for next key...".to_string())); - } - KeyEventOutcome::NotMatched => { - // fall through to other handlers (buttons, etc.) - } - } + let outcome = + login::event::handle_login_event(event, app_state, login_page)?; + // Only return if the login page actually consumed the key + if !outcome.get_message_if_ok().is_empty() { + return Ok(outcome); } } else if let Page::Register(register_page) = &mut router.current { - use crossterm::event::{KeyCode, KeyModifiers}; - - // Inside canvas: at the last field, 'j' or Down moves focus to buttons - if !app_state.ui.focus_outside_canvas { - let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1); - let at_last = register_page.editor.current_field() >= last_idx; - if at_last - && matches!( - (key_code, modifiers), - (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) - ) - { - app_state.ui.focus_outside_canvas = true; - app_state.focused_button_index = 0; // focus "Register" button - register_page.editor.set_mode(CanvasMode::ReadOnly); - return Ok(EventOutcome::Ok("Focus moved to buttons".to_string())); - } - } - - // Only forward to the canvas while focus is inside it - if !app_state.ui.focus_outside_canvas { - match register_page.handle_key_event(key_event) { - KeyEventOutcome::Consumed(Some(msg)) => { - self.command_message = msg; - return Ok(EventOutcome::Ok("Register input updated".to_string())); - } - KeyEventOutcome::Consumed(None) => { - return Ok(EventOutcome::Ok("Register input updated".to_string())); - } - KeyEventOutcome::Pending => { - return Ok(EventOutcome::Ok("Waiting for next key...".to_string())); - } - KeyEventOutcome::NotMatched => { - // fall through - } - } + let outcome = crate::pages::register::event::handle_register_event( + event, + app_state, + register_page, + )?; + // Only stop if page actually consumed the key; else fall through to global handling + if !outcome.get_message_if_ok().is_empty() { + return Ok(outcome); } } else if let Page::Form(path) = &router.current { - // NEW: Delegate Form event handling - return forms::event::handle_form_event( - event, - app_state, - path, - &mut self.ideal_cursor_column - ); + let outcome = forms::event::handle_form_event( + event, + app_state, + path, + &mut self.ideal_cursor_column, + )?; + // Only return if the form page actually consumed the key + if !outcome.get_message_if_ok().is_empty() { + return Ok(outcome); + } } } if toggle_sidebar( @@ -508,19 +447,6 @@ impl EventHandler { match &mut router.current { // LOGIN: From buttons (general) back into the canvas with 'k' (Up), // but ONLY from the left-most "Login" button. - Page::Login(page) => { - if app_state.ui.focus_outside_canvas { - if app_state.focused_button_index == 0 - && matches!(ma, crate::movement::MovementAction::Up) - { - app_state.ui.focus_outside_canvas = false; - // Enter canvas in ReadOnly mode (never jump straight to Edit) - page.editor.set_mode(CanvasMode::ReadOnly); - // Optional: keep current field (usually 0 initially) - return Ok(EventOutcome::Ok(String::new())); - } - } - } Page::AddTable(state) => { if state.handle_movement(ma) { // Keep UI focus consistent with inputs vs. outer elements diff --git a/client/src/pages/admin/admin/event.rs b/client/src/pages/admin/admin/event.rs new file mode 100644 index 0000000..d60593f --- /dev/null +++ b/client/src/pages/admin/admin/event.rs @@ -0,0 +1,60 @@ +// src/pages/admin/admin/event.rs +use anyhow::Result; +use crossterm::event::KeyEvent; + +use crate::buffer::state::BufferState; +use crate::config::binds::config::Config; +use crate::pages::admin::AdminState; +use crate::pages::admin::main::logic::handle_admin_navigation; +use crate::state::app::state::AppState; + +/// Handle all Admin page-specific key events (movement + actions). +/// Returns true if the key was handled (so the caller should stop propagation). +pub fn handle_admin_event( + key_event: KeyEvent, + config: &Config, + app_state: &mut AppState, + admin_state: &mut AdminState, + buffer_state: &mut BufferState, + command_message: &mut String, +) -> Result { + // 1) Map general action to MovementAction (same mapping used in event.rs) + let movement_action = if let Some(act) = + config.get_general_action(key_event.code, key_event.modifiers) + { + use crate::movement::MovementAction; + match act { + "up" => Some(MovementAction::Up), + "down" => Some(MovementAction::Down), + "left" => Some(MovementAction::Left), + "right" => Some(MovementAction::Right), + "next" => Some(MovementAction::Next), + "previous" => Some(MovementAction::Previous), + "select" => Some(MovementAction::Select), + "esc" => Some(MovementAction::Esc), + _ => None, + } + } else { + None + }; + + if let Some(ma) = movement_action { + if admin_state.handle_movement(app_state, ma) { + return Ok(true); + } + } + + // 2) Rich Admin navigation (buttons, selections, etc.) + if handle_admin_navigation( + key_event, + config, + app_state, + admin_state, + buffer_state, + command_message, + ) { + return Ok(true); + } + + Ok(false) +} diff --git a/client/src/pages/admin/admin/loader.rs b/client/src/pages/admin/admin/loader.rs new file mode 100644 index 0000000..a902833 --- /dev/null +++ b/client/src/pages/admin/admin/loader.rs @@ -0,0 +1,54 @@ +// src/pages/admin/admin/loader.rs +use anyhow::{Context, Result}; + +use crate::pages::admin::{AdminFocus, AdminState}; +use crate::services::grpc_client::GrpcClient; +use crate::state::app::state::AppState; + +/// Refresh admin data and ensure focus and selections are valid. +pub async fn refresh_admin_state( + grpc_client: &mut GrpcClient, + app_state: &mut AppState, + admin_state: &mut AdminState, +) -> Result<()> { + // Fetch latest profile tree + let refreshed_tree = grpc_client + .get_profile_tree() + .await + .context("Failed to refresh profile tree for Admin panel")?; + app_state.profile_tree = refreshed_tree; + + // Populate profile names for AdminState's list + let profile_names = app_state + .profile_tree + .profiles + .iter() + .map(|p| p.name.clone()) + .collect::>(); + admin_state.set_profiles(profile_names); + + // Ensure a sane focus + if admin_state.current_focus == AdminFocus::default() + || !matches!( + admin_state.current_focus, + AdminFocus::InsideProfilesList + | AdminFocus::Tables + | AdminFocus::InsideTablesList + | AdminFocus::Button1 + | AdminFocus::Button2 + | AdminFocus::Button3 + | AdminFocus::ProfilesPane + ) + { + admin_state.current_focus = AdminFocus::ProfilesPane; + } + + // Ensure a selection exists when profiles are present + if admin_state.profile_list_state.selected().is_none() + && !app_state.profile_tree.profiles.is_empty() + { + admin_state.profile_list_state.select(Some(0)); + } + + Ok(()) +} diff --git a/client/src/pages/admin/admin/mod.rs b/client/src/pages/admin/admin/mod.rs index 5622ae5..1a4d8c2 100644 --- a/client/src/pages/admin/admin/mod.rs +++ b/client/src/pages/admin/admin/mod.rs @@ -3,5 +3,7 @@ pub mod state; pub mod ui; pub mod tui; +pub mod event; +pub mod loader; pub use state::{AdminState, AdminFocus}; diff --git a/client/src/pages/login/event.rs b/client/src/pages/login/event.rs new file mode 100644 index 0000000..e96f86f --- /dev/null +++ b/client/src/pages/login/event.rs @@ -0,0 +1,73 @@ +// src/pages/login/event.rs +use anyhow::Result; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use canvas::{keymap::KeyEventOutcome, AppMode as CanvasMode}; +use crate::{ + state::app::state::AppState, + pages::login::LoginFormState, + modes::handlers::event::EventOutcome, +}; +use canvas::DataProvider; + +/// Handles all Login page-specific events +pub fn handle_login_event( + event: Event, + app_state: &mut AppState, + login_page: &mut LoginFormState, +) -> Result { + if let Event::Key(key_event) = event { + let key_code = key_event.code; + let modifiers = key_event.modifiers; + + // From buttons (outside) back into the canvas (ReadOnly) with Up/k from the left-most button + if login_page.focus_outside_canvas + && login_page.focused_button_index == 0 + && matches!(key_code, KeyCode::Up | KeyCode::Char('k')) + && modifiers.is_empty() + { + login_page.focus_outside_canvas = false; + app_state.ui.focus_outside_canvas = false; // 🔑 keep global in sync + login_page.editor.set_mode(CanvasMode::ReadOnly); + return Ok(EventOutcome::Ok(String::new())); + } + + // Focus handoff: inside canvas → buttons + if !login_page.focus_outside_canvas { + let last_idx = login_page.editor.data_provider().field_count().saturating_sub(1); + let at_last = login_page.editor.current_field() >= last_idx; + if at_last + && matches!( + (key_code, modifiers), + (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) + ) + { + login_page.focus_outside_canvas = true; + login_page.focused_button_index = 0; // focus "Login" button + app_state.ui.focus_outside_canvas = true; + app_state.focused_button_index = 0; + login_page.editor.set_mode(CanvasMode::ReadOnly); + return Ok(EventOutcome::Ok("Focus moved to buttons".into())); + } + } + + // Forward to canvas if focus is inside + if !login_page.focus_outside_canvas { + match login_page.handle_key_event(key_event) { + KeyEventOutcome::Consumed(Some(msg)) => { + return Ok(EventOutcome::Ok(msg)); + } + KeyEventOutcome::Consumed(None) => { + return Ok(EventOutcome::Ok("Login input updated".into())); + } + KeyEventOutcome::Pending => { + return Ok(EventOutcome::Ok("Waiting for next key...".into())); + } + KeyEventOutcome::NotMatched => { + // fall through to button handling + } + } + } + } + + Ok(EventOutcome::Ok(String::new())) +} diff --git a/client/src/pages/login/mod.rs b/client/src/pages/login/mod.rs index 162e08b..716c3cc 100644 --- a/client/src/pages/login/mod.rs +++ b/client/src/pages/login/mod.rs @@ -3,7 +3,9 @@ pub mod state; pub mod ui; pub mod logic; +pub mod event; pub use state::*; pub use ui::render_login; pub use logic::*; +pub use event::*; diff --git a/client/src/pages/login/state.rs b/client/src/pages/login/state.rs index ab6cd07..fc099a9 100644 --- a/client/src/pages/login/state.rs +++ b/client/src/pages/login/state.rs @@ -127,6 +127,8 @@ impl DataProvider for LoginState { pub struct LoginFormState { pub state: LoginState, pub editor: FormEditor, + pub focus_outside_canvas: bool, + pub focused_button_index: usize, } // manual debug because FormEditor doesnt implement debug @@ -150,7 +152,12 @@ impl LoginFormState { pub fn new() -> Self { let state = LoginState::default(); let editor = FormEditor::new(state.clone()); - Self { state, editor } + Self { + state, + editor, + focus_outside_canvas: false, + focused_button_index: 0, + } } // === Delegates to LoginState fields === diff --git a/client/src/pages/login/ui.rs b/client/src/pages/login/ui.rs index 9ba0f0e..e5a3110 100644 --- a/client/src/pages/login/ui.rs +++ b/client/src/pages/login/ui.rs @@ -80,7 +80,7 @@ pub fn render_login( // Login Button let login_button_index = 0; - let login_active = if app_state.ui.focus_outside_canvas { + let login_active = if login_page.focus_outside_canvas { app_state.focused_button_index == login_button_index } else { false diff --git a/client/src/pages/register/event.rs b/client/src/pages/register/event.rs new file mode 100644 index 0000000..d32542f --- /dev/null +++ b/client/src/pages/register/event.rs @@ -0,0 +1,76 @@ +// src/pages/register/event.rs +use anyhow::Result; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use canvas::{keymap::KeyEventOutcome, AppMode as CanvasMode}; +use canvas::DataProvider; +use crate::{ + state::app::state::AppState, + pages::register::RegisterFormState, + modes::handlers::event::EventOutcome, +}; + +/// Handles all Register page-specific events. +/// Return a non-empty Ok(message) only when the page actually consumed the key, +/// otherwise return Ok("") to let global handling proceed. +pub fn handle_register_event( + event: Event, + app_state: &mut AppState, + register_page: &mut RegisterFormState, +)-> Result { + if let Event::Key(key_event) = event { + let key_code = key_event.code; + let modifiers = key_event.modifiers; + + // From buttons (outside) back into the canvas (ReadOnly) with Up/k from the left-most button + if register_page.focus_outside_canvas + && register_page.focused_button_index == 0 + && matches!(key_code, KeyCode::Up | KeyCode::Char('k')) + && modifiers.is_empty() + { + register_page.focus_outside_canvas = false; + // Keep global in sync for now (cursor styling elsewhere still reads it) + app_state.ui.focus_outside_canvas = false; + register_page.editor.set_mode(CanvasMode::ReadOnly); + return Ok(EventOutcome::Ok(String::new())); + } + + // Focus handoff: inside canvas → buttons + if !register_page.focus_outside_canvas { + let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1); + let at_last = register_page.editor.current_field() >= last_idx; + if at_last + && matches!( + (key_code, modifiers), + (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) + ) + { + register_page.focus_outside_canvas = true; + register_page.focused_button_index = 0; // focus "Register" button + // Keep global in sync for now + app_state.ui.focus_outside_canvas = true; + app_state.focused_button_index = 0; + register_page.editor.set_mode(CanvasMode::ReadOnly); + return Ok(EventOutcome::Ok("Focus moved to buttons".into())); + } + } + + // Forward to canvas if focus is inside + if !register_page.focus_outside_canvas { + match register_page.handle_key_event(key_event) { + KeyEventOutcome::Consumed(Some(msg)) => { + return Ok(EventOutcome::Ok(msg)); + } + KeyEventOutcome::Consumed(None) => { + return Ok(EventOutcome::Ok("Register input updated".into())); + } + KeyEventOutcome::Pending => { + return Ok(EventOutcome::Ok("Waiting for next key...".into())); + } + KeyEventOutcome::NotMatched => { + // fall through + } + } + } + } + Ok(EventOutcome::Ok(String::new())) +} diff --git a/client/src/pages/register/logic.rs b/client/src/pages/register/logic.rs index ee7663e..6c216ac 100644 --- a/client/src/pages/register/logic.rs +++ b/client/src/pages/register/logic.rs @@ -54,6 +54,8 @@ pub async fn back_to_login( buffer_state.update_history(AppView::Login); // Reset focus state + register_state.focus_outside_canvas = false; + register_state.focused_button_index = 0; app_state.ui.focus_outside_canvas = false; app_state.focused_button_index = 0; diff --git a/client/src/pages/register/mod.rs b/client/src/pages/register/mod.rs index 8ec204d..d95e14a 100644 --- a/client/src/pages/register/mod.rs +++ b/client/src/pages/register/mod.rs @@ -5,8 +5,10 @@ pub mod ui; pub mod state; pub mod logic; pub mod suggestions; +pub mod event; // pub use state::*; pub use ui::render_register; pub use logic::*; pub use state::*; +pub use event::*; diff --git a/client/src/pages/register/state.rs b/client/src/pages/register/state.rs index aefe612..6c87d6b 100644 --- a/client/src/pages/register/state.rs +++ b/client/src/pages/register/state.rs @@ -146,6 +146,8 @@ impl DataProvider for RegisterState { pub struct RegisterFormState { pub state: RegisterState, pub editor: FormEditor, + pub focus_outside_canvas: bool, + pub focused_button_index: usize, } impl Default for RegisterFormState { @@ -174,7 +176,12 @@ impl RegisterFormState { pub fn new() -> Self { let state = RegisterState::default(); let editor = FormEditor::new(state.clone()); - Self { state, editor } + Self { + state, + editor, + focus_outside_canvas: false, + focused_button_index: 0, + } } // === Delegates to RegisterState === diff --git a/client/src/pages/register/ui.rs b/client/src/pages/register/ui.rs index e61908c..0098ce5 100644 --- a/client/src/pages/register/ui.rs +++ b/client/src/pages/register/ui.rs @@ -80,8 +80,9 @@ pub fn render_register( // Register Button let register_button_index = 0; - let register_active = app_state.ui.focus_outside_canvas - && app_state.focused_button_index == register_button_index; + let register_active = + register_page.focus_outside_canvas + && register_page.focused_button_index == register_button_index; let mut register_style = Style::default().fg(theme.fg); let mut register_border = Style::default().fg(theme.border); if register_active { @@ -104,8 +105,9 @@ pub fn render_register( // Return Button let return_button_index = 1; - let return_active = app_state.ui.focus_outside_canvas - && app_state.focused_button_index == return_button_index; + let return_active = + register_page.focus_outside_canvas + && register_page.focused_button_index == return_button_index; let mut return_style = Style::default().fg(theme.fg); let mut return_border = Style::default().fg(theme.border); if return_active {