From 668eeee197b327a1ddf32b0020afacb3d137a66e Mon Sep 17 00:00:00 2001 From: filipriec Date: Thu, 29 May 2025 16:11:41 +0200 Subject: [PATCH] navigation in the menu but needs refactoring --- client/src/modes/general.rs | 1 + .../src/modes/general/command_navigation.rs | 204 ++++++++++++++++++ client/src/modes/general/navigation.rs | 7 + client/src/modes/handlers/event.rs | 180 ++++++++-------- client/src/modes/handlers/mode_manager.rs | 12 +- client/src/ui/handlers/render.rs | 29 ++- client/src/ui/handlers/ui.rs | 14 +- 7 files changed, 329 insertions(+), 118 deletions(-) create mode 100644 client/src/modes/general/command_navigation.rs diff --git a/client/src/modes/general.rs b/client/src/modes/general.rs index d859ed8..4592c82 100644 --- a/client/src/modes/general.rs +++ b/client/src/modes/general.rs @@ -1,3 +1,4 @@ // src/client/modes/general.rs pub mod navigation; pub mod dialog; +pub mod command_navigation; diff --git a/client/src/modes/general/command_navigation.rs b/client/src/modes/general/command_navigation.rs new file mode 100644 index 0000000..c829b74 --- /dev/null +++ b/client/src/modes/general/command_navigation.rs @@ -0,0 +1,204 @@ +// src/modes/general/command_navigation.rs +use crossterm::event::{KeyEvent, KeyCode}; +use crate::config::binds::config::Config; +use crate::modes::handlers::event::EventOutcome; +use anyhow::Result; + +#[derive(Debug, Clone, PartialEq)] +pub enum NavigationType { + FindFile, + // Future: CommandPalette, BufferList, etc. +} + +pub struct NavigationState { + pub active: bool, + pub input: String, + pub options: Vec, + pub selected_index: Option, + pub filtered_options: Vec<(usize, String)>, // (original_index, filtered_string) + pub navigation_type: NavigationType, +} + +impl NavigationState { + pub fn new() -> Self { + Self { + active: false, + input: String::new(), + options: Vec::new(), + selected_index: None, + filtered_options: Vec::new(), + navigation_type: NavigationType::FindFile, + } + } + + pub fn activate_find_file(&mut self, options: Vec) { + self.active = true; + self.navigation_type = NavigationType::FindFile; + self.options = options; + self.input.clear(); + self.update_filtered_options(); + } + + pub fn deactivate(&mut self) { + self.active = false; + self.input.clear(); + self.options.clear(); + self.filtered_options.clear(); + self.selected_index = None; + } + + pub fn add_char(&mut self, c: char) { + self.input.push(c); + self.update_filtered_options(); + } + + pub fn remove_char(&mut self) { + self.input.pop(); + self.update_filtered_options(); + } + + pub fn move_up(&mut self) { + if self.filtered_options.is_empty() { + self.selected_index = None; + return; + } + + match self.selected_index { + Some(current) => { + if current == 0 { + self.selected_index = Some(self.filtered_options.len() - 1); + } else { + self.selected_index = Some(current - 1); + } + } + None => { + self.selected_index = Some(self.filtered_options.len() - 1); + } + } + } + + pub fn move_down(&mut self) { + if self.filtered_options.is_empty() { + self.selected_index = None; + return; + } + + match self.selected_index { + Some(current) => { + if current >= self.filtered_options.len() - 1 { + self.selected_index = Some(0); + } else { + self.selected_index = Some(current + 1); + } + } + None => { + self.selected_index = Some(0); + } + } + } + + pub fn get_selected_option(&self) -> Option<&str> { + self.selected_index + .and_then(|idx| self.filtered_options.get(idx)) + .map(|(_, option)| option.as_str()) + } + + fn update_filtered_options(&mut self) { + if self.input.is_empty() { + self.filtered_options = self.options + .iter() + .enumerate() + .map(|(i, opt)| (i, opt.clone())) + .collect(); + } else { + let input_lower = self.input.to_lowercase(); + self.filtered_options = self.options + .iter() + .enumerate() + .filter(|(_, opt)| opt.to_lowercase().contains(&input_lower)) + .map(|(i, opt)| (i, opt.clone())) + .collect(); + } + + // Reset selection to first item if current selection is invalid + if self.filtered_options.is_empty() { + self.selected_index = None; + } else if self.selected_index.map_or(true, |idx| idx >= self.filtered_options.len()) { + self.selected_index = Some(0); + } + } +} + +/// Handle navigation events within General mode +pub async fn handle_command_navigation_event( + navigation_state: &mut NavigationState, + key: KeyEvent, + config: &Config, +) -> Result { + if !navigation_state.active { + return Ok(EventOutcome::Ok(String::new())); + } + + match key.code { + KeyCode::Esc => { + navigation_state.deactivate(); + Ok(EventOutcome::Ok("Find File cancelled".to_string())) + } + KeyCode::Enter => { + if let Some(selected) = navigation_state.get_selected_option() { + let selected = selected.to_string(); + navigation_state.deactivate(); + match navigation_state.navigation_type { + NavigationType::FindFile => { + Ok(EventOutcome::Ok(format!("Selected file: {}", selected))) + } + } + } else { + Ok(EventOutcome::Ok("No file selected".to_string())) + } + } + KeyCode::Up => { + navigation_state.move_up(); + Ok(EventOutcome::Ok(String::new())) + } + KeyCode::Down => { + navigation_state.move_down(); + Ok(EventOutcome::Ok(String::new())) + } + KeyCode::Backspace => { + navigation_state.remove_char(); + Ok(EventOutcome::Ok(String::new())) + } + KeyCode::Char(c) => { + navigation_state.add_char(c); + Ok(EventOutcome::Ok(String::new())) + } + _ => { + // Check for general keybindings that might apply to navigation + if let Some(action) = config.get_general_action(key.code, key.modifiers) { + match action { + "move_up" => { + navigation_state.move_up(); + Ok(EventOutcome::Ok(String::new())) + } + "move_down" => { + navigation_state.move_down(); + Ok(EventOutcome::Ok(String::new())) + } + "select" => { + if let Some(selected) = navigation_state.get_selected_option() { + let selected = selected.to_string(); + navigation_state.deactivate(); + Ok(EventOutcome::Ok(format!("Selected file: {}", selected))) + } else { + Ok(EventOutcome::Ok("No file selected".to_string())) + } + } + _ => Ok(EventOutcome::Ok(String::new())), + } + } else { + Ok(EventOutcome::Ok(String::new())) + } + } + } +} diff --git a/client/src/modes/general/navigation.rs b/client/src/modes/general/navigation.rs index 607f5ff..7c464f2 100644 --- a/client/src/modes/general/navigation.rs +++ b/client/src/modes/general/navigation.rs @@ -11,6 +11,7 @@ use crate::state::pages::admin::AdminState; use crate::state::pages::canvas_state::CanvasState; use crate::ui::handlers::context::UiContext; use crate::modes::handlers::event::EventOutcome; +use crate::modes::general::command_navigation::{handle_command_navigation_event, NavigationState}; use anyhow::Result; pub async fn handle_navigation_event( @@ -25,7 +26,13 @@ pub async fn handle_navigation_event( command_mode: &mut bool, command_input: &mut String, command_message: &mut String, + navigation_state: &mut NavigationState, ) -> Result { + // Handle command navigation first if active + if navigation_state.active { + return handle_command_navigation_event(navigation_state, key, config).await; + } + if let Some(action) = config.get_general_action(key.code, key.modifiers) { match action { "move_up" => { diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 9041c3d..8087225 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -1,5 +1,5 @@ // src/modes/handlers/event.rs -use crossterm::event::Event; +use crossterm::event::{Event, KeyEvent}; use crossterm::cursor::SetCursorStyle; use crate::services::grpc_client::GrpcClient; use crate::services::auth::AuthClient; @@ -35,6 +35,7 @@ use crate::modes::{ canvas::{edit, read_only, common_mode}, general::{navigation, dialog}, }; +use crate::modes::general::command_navigation::{NavigationState, handle_command_navigation_event}; use crate::functions::modes::navigation::{admin_nav, add_table_nav}; use crate::config::binds::key_sequences::KeySequenceTracker; use tokio::sync::mpsc; @@ -43,7 +44,7 @@ use crate::tui::functions::common::register::RegisterResult; use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender; use crate::functions::modes::navigation::add_logic_nav; -use crossterm::event::{KeyCode, KeyModifiers}; +use crossterm::event::KeyCode; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EventOutcome { @@ -53,6 +54,15 @@ pub enum EventOutcome { ButtonSelected { context: UiContext, index: usize }, } +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, @@ -67,10 +77,7 @@ pub struct EventHandler { pub register_result_sender: mpsc::Sender, pub save_table_result_sender: SaveTableResultSender, pub save_logic_result_sender: SaveLogicResultSender, - pub find_file_palette_active: bool, - pub find_file_options: Vec, - pub find_file_selected_index: Option, - pub find_file_input: String, + pub navigation_state: NavigationState, } impl EventHandler { @@ -94,23 +101,18 @@ impl EventHandler { register_result_sender, save_table_result_sender, save_logic_result_sender, - find_file_palette_active: false, - find_file_options: vec![ - "src/main.rs".to_string(), - "src/lib.rs".to_string(), - "Cargo.toml".to_string(), - "README.md".to_string(), - "config.toml".to_string(), - "src/ui/handlers/ui.rs".to_string(), - "src/modes/handlers/event.rs".to_string(), - "another_file.txt".to_string(), - "yet_another_one.md".to_string(), - ], - find_file_selected_index: None, - find_file_input: String::new(), + navigation_state: NavigationState::new(), }) } + pub fn is_navigation_active(&self) -> bool { + self.navigation_state.active + } + + pub fn activate_find_file(&mut self, options: Vec) { + self.navigation_state.activate_find_file(options); + } + #[allow(clippy::too_many_arguments)] pub async fn handle_event( &mut self, @@ -130,26 +132,28 @@ impl EventHandler { total_count: u64, current_position: &mut u64, ) -> Result { - // Handle find file palette first - if self.find_file_palette_active { - if let Event::Key(key) = event { - if key.code == KeyCode::Esc { - self.find_file_palette_active = false; - self.find_file_input.clear(); - self.find_file_selected_index = None; - self.command_message = "Find File palette closed".to_string(); - return Ok(EventOutcome::Ok(self.command_message.clone())); + let mut current_mode = ModeManager::derive_mode(app_state, self, admin_state); + + // Handle active command navigation first + 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); } - if let KeyCode::Char(c) = key.code { - self.find_file_input.push(c); - } else if key.code == KeyCode::Backspace { - self.find_file_input.pop(); - } - return Ok(EventOutcome::Ok("Palette event consumed".to_string())); + app_state.update_mode(current_mode); + return Ok(outcome); } + app_state.update_mode(current_mode); + return Ok(EventOutcome::Ok(String::new())); } - let current_mode = ModeManager::derive_mode(app_state, self, admin_state); app_state.update_mode(current_mode); let current_view = { @@ -166,34 +170,29 @@ impl EventHandler { buffer_state.update_history(current_view); if app_state.ui.dialog.dialog_show { - if let Some(dialog_result) = dialog::handle_dialog_event( - &event, - config, - app_state, - login_state, - register_state, - buffer_state, - admin_state, - ).await { - return dialog_result; + 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 { + // Handle resize if needed } return Ok(EventOutcome::Ok(String::new())); } - if let Event::Key(key) = event { - let key_code = key.code; - let modifiers = key.modifiers; + 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" } - ); + 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" } - ); + let message = format!("Buffer {}", if app_state.ui.show_buffer_list { "shown" } else { "hidden" }); return Ok(EventOutcome::Ok(message)); } @@ -224,10 +223,9 @@ impl EventHandler { match current_mode { AppMode::General => { - if app_state.ui.show_admin - && auth_state.role.as_deref() == Some("admin") { + if app_state.ui.show_admin && auth_state.role.as_deref() == Some("admin") { if admin_nav::handle_admin_navigation( - key, + key_event, config, app_state, admin_state, @@ -243,7 +241,7 @@ impl EventHandler { let sender_clone = self.save_logic_result_sender.clone(); if add_logic_nav::handle_add_logic_navigation( - key, + key_event, config, app_state, &mut admin_state.add_logic_state, @@ -262,7 +260,7 @@ impl EventHandler { let sender_clone = self.save_table_result_sender.clone(); if add_table_nav::handle_add_table_navigation( - key, + key_event, config, app_state, &mut admin_state.add_table_state, @@ -275,7 +273,7 @@ impl EventHandler { } let nav_outcome = navigation::handle_navigation_event( - key, + key_event, config, form_state, app_state, @@ -286,7 +284,9 @@ impl EventHandler { &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 { @@ -300,24 +300,22 @@ impl EventHandler { format!("Intro Option {} selected", index) } UiContext::Login => { - let login_action_message = match index { + match index { 0 => { login::initiate_login(login_state, app_state, self.auth_client.clone(), self.login_result_sender.clone()) }, 1 => login::back_to_main(login_state, app_state, buffer_state).await, _ => "Invalid Login Option".to_string(), - }; - login_action_message + } } UiContext::Register => { - let register_action_message = match index { + match index { 0 => { register::initiate_registration(register_state, app_state, self.auth_client.clone(), self.register_result_sender.clone()) }, 1 => register::back_to_login(register_state, app_state, buffer_state).await, _ => "Invalid Login Option".to_string(), - }; - register_action_message + } } UiContext::Admin => { admin::handle_admin_selection(app_state, admin_state); @@ -331,7 +329,7 @@ impl EventHandler { } other => return other, } - }, + } AppMode::ReadOnly => { if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") @@ -426,7 +424,7 @@ impl EventHandler { let (_should_exit, message) = read_only::handle_read_only_event( app_state, - key, + key_event, config, form_state, login_state, @@ -442,7 +440,7 @@ impl EventHandler { &mut self.ideal_cursor_column, ).await?; return Ok(EventOutcome::Ok(message)); - }, + } AppMode::Highlight => { if config.get_highlight_action_for_key(key_code, modifiers) == Some("exit_highlight_mode") { @@ -461,7 +459,7 @@ impl EventHandler { } let (_should_exit, message) = read_only::handle_read_only_event( - app_state, key, config, form_state, login_state, + app_state, key_event, config, form_state, login_state, register_state, &mut admin_state.add_table_state, &mut admin_state.add_logic_state, @@ -500,7 +498,7 @@ impl EventHandler { } let edit_result = edit::handle_edit_event( - key, + key_event, config, form_state, login_state, @@ -551,10 +549,9 @@ impl EventHandler { return Err(e.into()); } } - }, + } AppMode::Command => { - // Handle immediate command mode actions if config.is_exit_command_mode(key_code, modifiers) { self.command_input.clear(); self.command_message.clear(); @@ -565,46 +562,54 @@ impl EventHandler { if config.is_command_execute(key_code, modifiers) { let outcome = command_mode::handle_command_event( - key, config, app_state, login_state, register_state, form_state, + key_event, config, app_state, login_state, register_state, form_state, &mut self.command_input, &mut self.command_message, grpc_client, command_handler, terminal, current_position, total_count, ).await?; 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); } - // Handle backspace if key_code == KeyCode::Backspace { self.command_input.pop(); self.key_sequence_tracker.reset(); return Ok(EventOutcome::Ok(String::new())); } - // Handle character input and sequences 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 { - self.find_file_palette_active = true; - self.find_file_input.clear(); - self.find_file_selected_index = if self.find_file_options.is_empty() { - None - } else { - Some(0) - }; + let options = vec![ + "src/main.rs".to_string(), + "src/lib.rs".to_string(), + "Cargo.toml".to_string(), + "README.md".to_string(), + "config.toml".to_string(), + "src/ui/handlers/ui.rs".to_string(), + "src/modes/handlers/event.rs".to_string(), + "another_file.txt".to_string(), + "yet_another_one.md".to_string(), + ]; + self.activate_find_file(options); self.command_mode = false; self.command_input.clear(); self.command_message = "Find File:".to_string(); self.key_sequence_tracker.reset(); + app_state.update_mode(AppMode::General); return Ok(EventOutcome::Ok("Find File palette activated".to_string())); } else { self.key_sequence_tracker.reset(); self.command_input.push('f'); - 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())); } @@ -623,11 +628,12 @@ impl EventHandler { return Ok(EventOutcome::Ok(String::new())); } - // Reset tracker for other keys 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; diff --git a/client/src/modes/handlers/mode_manager.rs b/client/src/modes/handlers/mode_manager.rs index 9819eaf..e02446d 100644 --- a/client/src/modes/handlers/mode_manager.rs +++ b/client/src/modes/handlers/mode_manager.rs @@ -23,7 +23,7 @@ impl ModeManager { event_handler: &EventHandler, admin_state: &AdminState, ) -> AppMode { - if event_handler.find_file_palette_active { + if event_handler.navigation_state.active { return AppMode::General; } @@ -82,14 +82,14 @@ impl ModeManager { } // Mode transition rules - pub fn can_enter_command_mode(current_mode: AppMode) -> bool { - !matches!(current_mode, AppMode::Edit) // Can't enter from Edit mode +pub fn can_enter_command_mode(current_mode: AppMode) -> bool { + !matches!(current_mode, AppMode::Edit) } - + pub fn can_enter_edit_mode(current_mode: AppMode) -> bool { - matches!(current_mode, AppMode::ReadOnly) // Only from ReadOnly + matches!(current_mode, AppMode::ReadOnly) } - + pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool { matches!(current_mode, AppMode::Edit | AppMode::Command | AppMode::Highlight) } diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index fe8e291..676b8a1 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -14,9 +14,9 @@ use crate::components::{ }; use crate::config::colors::themes::Theme; use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, // Added Rect - style::Style, // Added Style for render_find_file_palette - widgets::{Block, List, ListItem, Paragraph}, // Added for render_find_file_palette + layout::{Constraint, Direction, Layout, Rect}, + style::Style, + widgets::{Block, List, ListItem, Paragraph}, Frame, }; use crate::state::pages::canvas_state::CanvasState; @@ -29,6 +29,7 @@ use crate::state::app::buffer::BufferState; use crate::state::app::state::AppState; // AppState is needed for app_state.ui checks use crate::state::pages::admin::AdminState; use crate::state::app::highlight::HighlightState; +use crate::modes::general::command_navigation::NavigationState; // ++ New function to render the Find File Palette ++ fn render_find_file_palette( @@ -91,11 +92,8 @@ pub fn render_ui( event_handler_command_input: &str, // For normal command line event_handler_command_mode_active: bool, // Normal command line active? event_handler_command_message: &str, - // ++ Find File Palette specific state from EventHandler ++ - find_file_palette_active: bool, - find_file_options: &[String], - find_file_selected_index: Option, - find_file_palette_input: &str, // Input for the palette + // Navigation state replaces find file palette specific state + navigation_state: &NavigationState, // General app state total_count: u64, current_position: u64, @@ -108,10 +106,10 @@ pub fn render_ui( // Determine the height needed for the bottom bar (status + command/palette) let mut bottom_area_constraints: Vec = vec![Constraint::Length(1)]; // Status line - let command_palette_area_height = if find_file_palette_active { + let command_palette_area_height = if navigation_state.active { // Height for palette: 1 for input + number of visible options let max_visible_options = 7; // Example, can be adjusted - 1 + find_file_options.iter().take(max_visible_options).count().min(max_visible_options) as u16 + 1 + navigation_state.filtered_options.iter().take(max_visible_options).count().min(max_visible_options) as u16 } else if event_handler_command_mode_active { 1 // Height for normal command line } else { @@ -228,14 +226,14 @@ pub fn render_ui( // Render Find File Palette OR Normal Command Line if let Some(area) = command_render_area { - if find_file_palette_active { + if navigation_state.active { render_find_file_palette( f, area, theme, - find_file_palette_input, // Pass palette-specific input - find_file_options, - find_file_selected_index, + &navigation_state.input, + &navigation_state.filtered_options.iter().map(|(_, opt)| opt.clone()).collect::>(), + navigation_state.selected_index, ); } else if event_handler_command_mode_active { // Normal command line @@ -243,10 +241,9 @@ pub fn render_ui( f, area, event_handler_command_input, - true, // It's active + true, theme, event_handler_command_message, - // No palette-specific args for normal command line ); } } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index c83741e..e20cc61 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -209,16 +209,12 @@ pub async fn run_ui() -> Result<()> { &mut admin_state, &buffer_state, &theme, - event_handler.is_edit_mode, // is_event_handler_edit_mode + event_handler.is_edit_mode, &event_handler.highlight_state, - &event_handler.command_input, // event_handler_command_input - event_handler.command_mode, // event_handler_command_mode_active - &event_handler.command_message, // event_handler_command_message - // Find File Palette specific state - event_handler.find_file_palette_active, - &event_handler.find_file_options, - event_handler.find_file_selected_index, - &event_handler.find_file_input, + &event_handler.command_input, + event_handler.command_mode, + &event_handler.command_message, + &event_handler.navigation_state, // General app state app_state.total_count, app_state.current_position,