// 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())) } } } }