diff --git a/client/config.toml b/client/config.toml index 5db709b..f432d72 100644 --- a/client/config.toml +++ b/client/config.toml @@ -52,10 +52,12 @@ delete_char_forward = ["delete"] delete_char_backward = ["backspace"] next_field = ["enter"] prev_field = ["backtab"] -suggestion_down = ["shift+tab"] -suggestion_up = ["tab"] move_left = ["left"] move_right = ["right"] +suggestion_down = ["shift+tab"] +suggestion_up = ["tab"] +select_suggestion = ["enter"] +exit_suggestion_mode = ["esc"] [keybindings.command] exit_command_mode = ["ctrl+g", "esc"] diff --git a/client/src/components/common/autocomplete.rs b/client/src/components/common/autocomplete.rs index 8d0f8ff..db618b1 100644 --- a/client/src/components/common/autocomplete.rs +++ b/client/src/components/common/autocomplete.rs @@ -2,9 +2,10 @@ use crate::config::colors::themes::Theme; use ratatui::{ + buffer::Buffer, layout::Rect, - style::{Modifier, Style}, - widgets::{Block, Borders, List, ListItem, ListState}, + style::{Color, Modifier, Style}, + widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame, }; @@ -19,25 +20,27 @@ pub fn render_autocomplete_dropdown( if suggestions.is_empty() { return; // Don't render if no suggestions } + // --- Render Background --- + struct GrayBackground; + impl Widget for GrayBackground { + fn render(self, area: Rect, buf: &mut Buffer) { + buf.set_style(area, Style::default().bg(Color::DarkGray)); // Set background + } + } + f.render_widget(GrayBackground, area); + // --- Render Suggestions List (without block/border) --- let items: Vec = suggestions .iter() - .map(|s| ListItem::new(s.as_str())) + .map(|s| ListItem::new(s.as_str()).style(Style::default().fg(theme.fg))) // Set default text color .collect(); let list = List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(ratatui::widgets::BorderType::Plain) - .border_style(Style::default().fg(theme.accent)) // Highlight border - .style(Style::default().bg(theme.bg).fg(theme.fg)), - ) .highlight_style( Style::default() .add_modifier(Modifier::BOLD) - .bg(theme.highlight) // Highlight background for selected item - .fg(theme.bg), // Text color for selected item + .bg(theme.highlight) + .fg(theme.bg), ) .highlight_symbol("> "); // Symbol for selected item diff --git a/client/src/functions/modes/edit/auth_e.rs b/client/src/functions/modes/edit/auth_e.rs index 86e2ceb..9581028 100644 --- a/client/src/functions/modes/edit/auth_e.rs +++ b/client/src/functions/modes/edit/auth_e.rs @@ -299,13 +299,13 @@ pub async fn execute_edit_action( } // --- Autocomplete Actions --- - "suggestion_down" | "suggestion_up" | "select_suggestion" | "hide_suggestions" => { + "suggestion_down" | "suggestion_up" | "select_suggestion" | "exit_suggestion_mode" => { // Attempt to downcast to RegisterState if let Some(register_state) = (state as &mut dyn Any).downcast_mut::() { // Only handle if it's the role field (index 4) and suggestions are shown (except for hide) if register_state.current_field() == 4 { match action { - "suggestion_down" if register_state.show_role_suggestions => { + "suggestion_down" if register_state.in_suggestion_mode => { let max_index = register_state.role_suggestions.len().saturating_sub(1); let current_index = register_state.selected_suggestion_index.unwrap_or(0); register_state.selected_suggestion_index = Some(if current_index >= max_index { 0 } else { current_index + 1 }); @@ -317,7 +317,7 @@ pub async fn execute_edit_action( register_state.selected_suggestion_index = Some(if current_index == 0 { max_index } else { current_index.saturating_sub(1) }); Ok("Suggestion changed up".to_string()) } - "select_suggestion" if register_state.show_role_suggestions => { + "select_suggestion" if register_state.in_suggestion_mode => { if let Some(selected_index) = register_state.selected_suggestion_index { if let Some(selected_role) = register_state.role_suggestions.get(selected_index) { register_state.role = selected_role.clone(); @@ -335,9 +335,10 @@ pub async fn execute_edit_action( Ok("No suggestion selected".to_string()) } } - "hide_suggestions" => { + "exit_suggestion_mode" => { // Handle Esc register_state.show_role_suggestions = false; register_state.selected_suggestion_index = None; + register_state.in_suggestion_mode = false; Ok("Suggestions hidden".to_string()) } _ => Ok("".to_string()) // Action doesn't apply in this state (e.g., suggestions not shown) diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 94cb622..2b23641 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -80,6 +80,29 @@ pub async fn handle_edit_event( // Edit-specific actions if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) { + // --- Special Handling for Tab/Shift+Tab in Role Field --- + if app_state.ui.show_register && register_state.current_field() == 4 { + match action { + "suggestion_up" | "suggestion_down" => { // Mapped to Tab/Shift+Tab + if !register_state.in_suggestion_mode { + // Enter suggestion mode + register_state.update_role_suggestions(); // Populate suggestions + if !register_state.role_suggestions.is_empty() { + register_state.in_suggestion_mode = true; + register_state.show_role_suggestions = true; + register_state.selected_suggestion_index = Some(0); // Select first + return Ok("Suggestions shown".to_string()); // Consume the event + } else { + return Ok("No suggestions available".to_string()); // Consume, do nothing else + } + } + // If already in suggestion mode, fall through to execute the action via auth_e + } + _ => {} // Other actions fall through + } + } + // --- End Special Handling --- + return if app_state.ui.show_login { auth_e::execute_edit_action( action, @@ -115,6 +138,12 @@ pub async fn handle_edit_event( // Character insertion if let KeyCode::Char(_) = key.code { + // If in suggestion mode, exit it before inserting char + if app_state.ui.show_register && register_state.in_suggestion_mode { + register_state.in_suggestion_mode = false; + register_state.show_role_suggestions = false; + register_state.selected_suggestion_index = None; + } let is_role_field = app_state.ui.show_register && register_state.current_field() == 4; // --- End Autocomplete Trigger --- @@ -149,15 +178,16 @@ pub async fn handle_edit_event( total_count ).await }; - - // After character insertion/deletion, update suggestions if it was the role field - if is_role_field { - register_state.update_role_suggestions(); - } } // Handle Backspace/Delete for Autocomplete Trigger if matches!(key.code, KeyCode::Backspace | KeyCode::Delete) { + // If in suggestion mode, exit it before deleting char + if app_state.ui.show_register && register_state.in_suggestion_mode { + register_state.in_suggestion_mode = false; + register_state.show_role_suggestions = false; + register_state.selected_suggestion_index = None; + } let is_role_field = app_state.ui.show_register && register_state.current_field() == 4; let action_str = if key.code == KeyCode::Backspace { "backspace" } else { "delete_char" }; @@ -177,9 +207,6 @@ pub async fn handle_edit_event( Ok("Action not applicable here".to_string()) // Placeholder }?; - if is_role_field { - register_state.update_role_suggestions(); - } return Ok(result); } diff --git a/client/src/state/pages/auth.rs b/client/src/state/pages/auth.rs index 396653f..1aae56a 100644 --- a/client/src/state/pages/auth.rs +++ b/client/src/state/pages/auth.rs @@ -4,8 +4,10 @@ use lazy_static::lazy_static; lazy_static! { pub static ref AVAILABLE_ROLES: Vec = vec![ - "accountant".to_string(), "admin".to_string(), + "moderator".to_string(), + "accountant".to_string(), + "viewer".to_string(), ]; } @@ -37,6 +39,7 @@ pub struct RegisterState { pub show_role_suggestions: bool, pub role_suggestions: Vec, pub selected_suggestion_index: Option, + pub in_suggestion_mode: bool, } impl AuthState { @@ -71,6 +74,7 @@ impl RegisterState { show_role_suggestions: false, role_suggestions: Vec::new(), selected_suggestion_index: None, + in_suggestion_mode: false, } } @@ -78,9 +82,7 @@ impl RegisterState { pub fn update_role_suggestions(&mut self) { let current_input = self.role.to_lowercase(); if current_input.is_empty() { - self.role_suggestions = Vec::new(); // Or show all? For now, clear. - self.show_role_suggestions = false; - self.selected_suggestion_index = None; + self.role_suggestions = AVAILABLE_ROLES.to_vec(); } else { self.role_suggestions = AVAILABLE_ROLES .iter() @@ -88,7 +90,6 @@ impl RegisterState { .cloned() .collect(); self.show_role_suggestions = !self.role_suggestions.is_empty(); - self.selected_suggestion_index = if self.show_role_suggestions { Some(0) } else { None }; // Default to first suggestion selected } } } @@ -269,7 +270,7 @@ impl CanvasState for RegisterState { fn get_suggestions(&self) -> Option<&[String]> { // Only show suggestions for the role field (index 4) when requested - if self.current_field == 4 && self.show_role_suggestions { + if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions { Some(&self.role_suggestions) } else { None @@ -277,8 +278,7 @@ impl CanvasState for RegisterState { } fn get_selected_suggestion_index(&self) -> Option { - // Only return index if suggestions are shown for the role field - if self.current_field == 4 && self.show_role_suggestions { + if self.current_field == 4 && self.in_suggestion_mode && self.show_role_suggestions { self.selected_suggestion_index } else { None