From 2e9f8815d2603af9958ff6e3de2bda82204a074a Mon Sep 17 00:00:00 2001 From: filipriec Date: Tue, 15 Apr 2025 21:15:58 +0200 Subject: [PATCH] HIGHLIGHT MODE --- client/config.toml | 5 ++ client/src/components/auth/login.rs | 4 ++ client/src/components/auth/register.rs | 4 ++ client/src/components/form/form.rs | 4 ++ client/src/components/handlers/canvas.rs | 55 +++++++++++++++---- client/src/config/binds/config.rs | 10 ++++ client/src/config/colors/themes.rs | 4 ++ client/src/modes/handlers/event.rs | 66 +++++++++++++++++------ client/src/modes/handlers/mode_manager.rs | 11 +++- client/src/modes/highlight.rs | 2 + client/src/modes/highlight/highlight.rs | 62 +++++++++++++++++++++ client/src/modes/mod.rs | 2 + client/src/state/pages/form.rs | 8 ++- client/src/ui/handlers/render.rs | 12 ++++- client/src/ui/handlers/ui.rs | 6 +++ 15 files changed, 225 insertions(+), 30 deletions(-) create mode 100644 client/src/modes/highlight.rs create mode 100644 client/src/modes/highlight/highlight.rs diff --git a/client/config.toml b/client/config.toml index c5eba7f..a402cc4 100644 --- a/client/config.toml +++ b/client/config.toml @@ -49,6 +49,11 @@ move_line_start = ["0"] move_line_end = ["$"] move_first_line = ["gg"] move_last_line = ["x"] +highlight_mode = [v] +highlight_mode_full_line = [ctrl+v] + +[keybindings.highlight] +exit_highlight_mode = [esc] [keybindings.edit] exit_edit_mode = ["esc","ctrl+e"] diff --git a/client/src/components/auth/login.rs b/client/src/components/auth/login.rs index bc90623..599935f 100644 --- a/client/src/components/auth/login.rs +++ b/client/src/components/auth/login.rs @@ -20,6 +20,8 @@ pub fn render_login( login_state: &LoginState, app_state: &AppState, is_edit_mode: bool, + is_highlight_mode: bool, + highlight_anchor: Option<(usize, usize)>, ) { // Main container let block = Block::default() @@ -56,6 +58,8 @@ pub fn render_login( &[&login_state.username, &login_state.password], theme, is_edit_mode, + is_highlight_mode, + highlight_anchor, ); // --- ERROR MESSAGE --- diff --git a/client/src/components/auth/register.rs b/client/src/components/auth/register.rs index d0130a2..e28431a 100644 --- a/client/src/components/auth/register.rs +++ b/client/src/components/auth/register.rs @@ -22,6 +22,8 @@ pub fn render_register( state: &RegisterState, // Use RegisterState app_state: &AppState, is_edit_mode: bool, + is_highlight_mode: bool, + highlight_anchor: Option<(usize, usize)>, ) { let block = Block::default() .borders(Borders::ALL) @@ -64,6 +66,8 @@ pub fn render_register( &state.inputs().iter().map(|s| *s).collect::>(), // Pass inputs directly theme, is_edit_mode, + is_highlight_mode, + highlight_anchor, ); // --- HELP TEXT --- diff --git a/client/src/components/form/form.rs b/client/src/components/form/form.rs index d14b72c..4a0e900 100644 --- a/client/src/components/form/form.rs +++ b/client/src/components/form/form.rs @@ -18,6 +18,8 @@ pub fn render_form( inputs: &[&String], theme: &Theme, is_edit_mode: bool, + is_highlight_mode: bool, + highlight_anchor: Option<(usize, usize)>, total_count: u64, current_position: u64, ) { @@ -62,5 +64,7 @@ pub fn render_form( inputs, theme, is_edit_mode, + is_highlight_mode, + highlight_anchor, ); } diff --git a/client/src/components/handlers/canvas.rs b/client/src/components/handlers/canvas.rs index d0fe62b..23d1cd4 100644 --- a/client/src/components/handlers/canvas.rs +++ b/client/src/components/handlers/canvas.rs @@ -2,7 +2,7 @@ use ratatui::{ widgets::{Paragraph, Block, Borders}, layout::{Layout, Constraint, Direction, Rect}, - style::Style, + style::{Style, Modifier}, text::{Line, Span}, Frame, prelude::Alignment, @@ -19,6 +19,8 @@ pub fn render_canvas( inputs: &[&String], theme: &Theme, is_edit_mode: bool, + is_highlight_mode: bool, + highlight_anchor: Option<(usize, usize)>, ) -> Option { // Split area into columns let columns = Layout::default() @@ -75,19 +77,54 @@ pub fn render_canvas( // Render inputs and cursor for (i, input) in inputs.iter().enumerate() { let is_active = i == *current_field; - let input_display = Paragraph::new(input.as_str()) - .alignment(Alignment::Left) - .style(if is_active { - Style::default().fg(theme.highlight) - } else { - Style::default().fg(theme.fg) - }); + let current_cursor_pos = form_state.current_cursor_pos(); + let line = if is_highlight_mode && highlight_anchor.is_some() { + let (anchor_field, anchor_char) = highlight_anchor.unwrap(); + + if i == anchor_field && i == *current_field { // Highlight within the same field + let start = anchor_char.min(current_cursor_pos); + let end = anchor_char.max(current_cursor_pos); + let text = input.as_str(); + let len = text.chars().count(); + + // Ensure start and end are within bounds + let safe_start = start.min(len); + let safe_end = end.min(len); + + let before: String = text.chars().take(safe_start).collect(); + let highlighted: String = text.chars().skip(safe_start).take(safe_end - safe_start).collect(); + let after: String = text.chars().skip(safe_end).collect(); + + Line::from(vec![ + Span::styled(before, Style::default().fg(theme.fg)), + Span::styled( + highlighted, + Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD) + ), + Span::styled(after, Style::default().fg(theme.fg)), + ]) + } else { + // Field is not the anchor or not the current field during highlight + // Render normally for now (could extend to multi-line later) + Line::from(Span::styled(input.as_str(), Style::default().fg(theme.fg))) + } + } else { + // Not in highlight mode, render normally + Line::from(Span::styled( + input.as_str(), + if is_active { Style::default().fg(theme.highlight) } else { Style::default().fg(theme.fg) } + )) + }; + + let input_display = Paragraph::new(line) + .alignment(Alignment::Left); f.render_widget(input_display, input_rows[i]); if is_active { active_field_input_rect = Some(input_rows[i]); - let cursor_x = input_rows[i].x + form_state.current_cursor_pos() as u16; + // Cursor position calculation remains the same + let cursor_x = 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/config/binds/config.rs b/client/src/config/binds/config.rs index 7231622..3d701c7 100644 --- a/client/src/config/binds/config.rs +++ b/client/src/config/binds/config.rs @@ -32,6 +32,8 @@ pub struct ModeKeybindings { #[serde(default)] pub edit: HashMap>, #[serde(default)] + pub highlight: HashMap>, + #[serde(default)] pub command: HashMap>, #[serde(default)] pub common: HashMap>, @@ -75,6 +77,14 @@ impl Config { .or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers)) } + /// Gets an action for a key in Highlight mode, also checking common/global keybindings. + pub fn get_highlight_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { + self.get_action_for_key_in_mode(&self.keybindings.highlight, key, modifiers) + .or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers)) + .or_else(|| self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers)) + .or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers)) + } + /// Gets an action for a key in Command mode, also checking common keybindings. pub fn get_command_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> { self.get_action_for_key_in_mode(&self.keybindings.command, key, modifiers) diff --git a/client/src/config/colors/themes.rs b/client/src/config/colors/themes.rs index b285a17..e0c07dd 100644 --- a/client/src/config/colors/themes.rs +++ b/client/src/config/colors/themes.rs @@ -10,6 +10,7 @@ pub struct Theme { pub highlight: Color, pub warning: Color, pub border: Color, + pub highlight_bg: Color, } impl Theme { @@ -31,6 +32,7 @@ impl Theme { highlight: Color::Rgb(152, 251, 152), // Pastel green warning: Color::Rgb(255, 182, 193), // Pastel pink border: Color::Rgb(220, 220, 220), // Light gray border + highlight_bg: Color::Rgb(70, 70, 70), // Darker grey for highlight background } } @@ -44,6 +46,7 @@ impl Theme { highlight: Color::Rgb(50, 205, 50), // Bright green warning: Color::Rgb(255, 99, 71), // Bright red border: Color::Rgb(100, 100, 100), // Medium gray border + highlight_bg: Color::Rgb(180, 180, 180), // Lighter grey for highlight background } } @@ -57,6 +60,7 @@ impl Theme { highlight: Color::Rgb(0, 128, 0), // Green warning: Color::Rgb(255, 0, 0), // Red border: Color::Rgb(0, 0, 0), // Black border + highlight_bg: Color::Rgb(180, 180, 180), // Lighter grey for highlight background } } } diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index a2663b7..c69b80d 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -35,6 +35,7 @@ use crate::modes::{ common::{command_mode, commands::CommandHandler}, handlers::mode_manager::{ModeManager, AppMode}, canvas::{edit, read_only, common_mode}, + highlight::highlight, general::{navigation, dialog}, }; use crate::config::binds::key_sequences::KeySequenceTracker; @@ -52,6 +53,8 @@ pub struct EventHandler { pub command_input: String, pub command_message: String, pub is_edit_mode: bool, + pub is_highlight_mode: bool, + pub highlight_anchor: Option<(usize, usize)>, pub edit_mode_cooldown: bool, pub ideal_cursor_column: usize, pub key_sequence_tracker: KeySequenceTracker, @@ -65,6 +68,8 @@ impl EventHandler { command_input: String::new(), command_message: String::new(), is_edit_mode: false, + is_highlight_mode: false, + highlight_anchor: None, edit_mode_cooldown: false, ideal_cursor_column: 0, key_sequence_tracker: KeySequenceTracker::new(800), @@ -93,7 +98,6 @@ impl EventHandler { let current_mode = ModeManager::derive_mode(app_state, self); app_state.update_mode(current_mode); - // Determine the current view, including dynamic names let current_view = { let ui = &app_state.ui; if ui.show_intro { AppView::Intro } @@ -108,7 +112,6 @@ impl EventHandler { }; buffer_state.update_history(current_view); - // --- DIALOG MODALITY --- if app_state.ui.dialog.dialog_show { if let Some(dialog_result) = dialog::handle_dialog_event( &event, config, app_state, auth_state, login_state, register_state, buffer_state @@ -117,7 +120,6 @@ impl EventHandler { } return Ok(EventOutcome::Ok(String::new())); } - // --- END DIALOG MODALITY CHECK --- if let Event::Key(key) = event { let key_code = key.code; @@ -135,10 +137,10 @@ impl EventHandler { ); return Ok(EventOutcome::Ok(message)); } - // --- Buffer Switching (Check Global) --- + if !matches!(current_mode, AppMode::Edit | AppMode::Command) { if let Some(action) = config.get_action_for_key_in_mode( - &config.keybindings.global, key_code, modifiers // Check global bindings + &config.keybindings.global, key_code, modifiers ) { match action { "next_buffer" => { @@ -151,11 +153,10 @@ impl EventHandler { return Ok(EventOutcome::Ok("Switched to previous buffer".to_string())); } } - _ => {} // Other global actions could be handled here if needed + _ => {} } } } - // --- End Global UI Toggles --- match current_mode { AppMode::General => { @@ -174,7 +175,7 @@ impl EventHandler { ).await; match nav_outcome { Ok(EventOutcome::ButtonSelected { context, index }) => { - let mut message = String::from("Selected"); // Default message + let mut message = String::from("Selected"); match context { UiContext::Intro => { intro::handle_intro_selection(app_state, buffer_state, index); @@ -202,22 +203,35 @@ impl EventHandler { } UiContext::Admin => { admin::handle_admin_selection(app_state, admin_state); - message = format!("Admin Option {} selected", index); } UiContext::Dialog => { message = "Internal error: Unexpected dialog state".to_string(); } } - return Ok(EventOutcome::Ok(message)); // Return Ok with message + return Ok(EventOutcome::Ok(message)); } - other => return other, // Pass through Ok, Err, DataSaved directly + other => return other, } }, AppMode::ReadOnly => { - if config.is_enter_edit_mode_before(key_code, modifiers) && - ModeManager::can_enter_edit_mode(current_mode) { + if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode") + && ModeManager::can_enter_highlight_mode(current_mode) { + self.is_highlight_mode = true; + 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() }; + self.highlight_anchor = Some((current_field_index, current_cursor_pos)); + self.command_message = "-- HIGHLIGHT --".to_string(); + return Ok(EventOutcome::Ok(self.command_message.clone())); + } + + if config.get_read_only_action_for_key(key_code, modifiers) == 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(); @@ -225,8 +239,8 @@ impl EventHandler { return Ok(EventOutcome::Ok(self.command_message.clone())); } - if config.is_enter_edit_mode_after(key_code, modifiers) && - ModeManager::can_enter_edit_mode(current_mode) { + if config.get_read_only_action_for_key(key_code, modifiers) == 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 { @@ -306,8 +320,28 @@ impl EventHandler { return Ok(EventOutcome::Ok(message)); }, + AppMode::Highlight => { + if config.get_highlight_action_for_key(key_code, modifiers) == Some("exit_highlight_mode") { + self.is_highlight_mode = false; + self.highlight_anchor = None; + self.command_message = "Exited highlight mode".to_string(); + terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; + return Ok(EventOutcome::Ok(self.command_message.clone())); + } + + let (_should_exit, message) = read_only::handle_read_only_event( + app_state, key, config, form_state, login_state, + register_state, &mut self.key_sequence_tracker, + current_position, total_count, grpc_client, + &mut self.command_message, &mut self.edit_mode_cooldown, + &mut self.ideal_cursor_column, + ) + .await?; + return Ok(EventOutcome::Ok(message)); + } + AppMode::Edit => { - if config.is_exit_edit_mode(key_code, modifiers) { + if config.get_edit_action_for_key(key_code, modifiers) == Some("exit_edit_mode") { self.is_edit_mode = false; self.edit_mode_cooldown = true; diff --git a/client/src/modes/handlers/mode_manager.rs b/client/src/modes/handlers/mode_manager.rs index b601b06..2114e9c 100644 --- a/client/src/modes/handlers/mode_manager.rs +++ b/client/src/modes/handlers/mode_manager.rs @@ -7,6 +7,7 @@ pub enum AppMode { General, // For intro and admin screens ReadOnly, // Canvas read-only mode Edit, // Canvas edit mode + Highlight, // Cnavas highlight/visual mode Command, // Command mode overlay } @@ -19,6 +20,10 @@ impl ModeManager { return AppMode::Command; } + if event_handler.is_highlight_mode { + return AppMode::Highlight; + } + if app_state.ui.focus_outside_canvas { return AppMode::General; } @@ -50,6 +55,10 @@ impl ModeManager { } pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool { - matches!(current_mode, AppMode::Edit | AppMode::Command) + matches!(current_mode, AppMode::Edit | AppMode::Command | AppMode::Highlight) + } + + pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool { + matches!(current_mode, AppMode::ReadOnly) } } diff --git a/client/src/modes/highlight.rs b/client/src/modes/highlight.rs new file mode 100644 index 0000000..51de17a --- /dev/null +++ b/client/src/modes/highlight.rs @@ -0,0 +1,2 @@ +// src/client/modes/highlight.rs +pub mod highlight; diff --git a/client/src/modes/highlight/highlight.rs b/client/src/modes/highlight/highlight.rs new file mode 100644 index 0000000..c050be9 --- /dev/null +++ b/client/src/modes/highlight/highlight.rs @@ -0,0 +1,62 @@ +// src/modes/highlight/highlight.rs +// (This file is intentionally simple for now, reusing ReadOnly logic) + +use crate::config::binds::config::Config; +use crate::config::binds::key_sequences::KeySequenceTracker; +use crate::services::grpc_client::GrpcClient; +use crate::state::app::state::AppState; +use crate::state::pages::auth::{LoginState, RegisterState}; +use crate::state::pages::form::FormState; +use crate::modes::handlers::event::EventOutcome; +use crate::modes::read_only; // Import the ReadOnly handler +use crossterm::event::KeyEvent; + +/// Handles events when in Highlight mode. +/// Currently, it mostly delegates to the read_only handler for movement. +/// Exiting highlight mode is handled directly in the main event handler. +pub async fn handle_highlight_event( + app_state: &mut AppState, + key: KeyEvent, + config: &Config, + form_state: &mut FormState, + login_state: &mut LoginState, + register_state: &mut RegisterState, + key_sequence_tracker: &mut KeySequenceTracker, + current_position: &mut u64, + total_count: u64, + grpc_client: &mut GrpcClient, + command_message: &mut String, + edit_mode_cooldown: &mut bool, + ideal_cursor_column: &mut usize, +) -> Result> { + // Delegate movement and other actions to the read_only handler + // The rendering logic will use the highlight_anchor to draw the selection + let (should_exit, message) = read_only::handle_read_only_event( + app_state, + key, + config, + form_state, + login_state, + register_state, + key_sequence_tracker, + current_position, + total_count, + grpc_client, + command_message, // Pass the message buffer + edit_mode_cooldown, + ideal_cursor_column, + ) + .await?; + + // ReadOnly handler doesn't return EventOutcome directly, adapt if needed + // For now, assume Ok outcome unless ReadOnly signals an exit (which we ignore here) + if should_exit { + // This exit is likely for the whole app, let the main loop handle it + // We just return the message from read_only + Ok(EventOutcome::Ok(message)) + } else { + Ok(EventOutcome::Ok(message)) + } +} + + diff --git a/client/src/modes/mod.rs b/client/src/modes/mod.rs index 059f2a6..f00f653 100644 --- a/client/src/modes/mod.rs +++ b/client/src/modes/mod.rs @@ -3,8 +3,10 @@ pub mod handlers; pub mod canvas; pub mod general; pub mod common; +pub mod highlight; pub use handlers::*; pub use canvas::*; pub use general::*; pub use common::*; +pub use highlight::*; diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index 49531d0..de6adbf 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -6,8 +6,8 @@ use crate::state::pages::canvas_state::CanvasState; pub struct FormState { pub id: i64, - pub fields: Vec, // Use Vec for dynamic field names - pub values: Vec, // Store field values dynamically + pub fields: Vec, + pub values: Vec, pub current_field: usize, pub has_unsaved_changes: bool, pub current_cursor_pos: usize, @@ -33,6 +33,8 @@ impl FormState { area: Rect, theme: &Theme, is_edit_mode: bool, + is_highlight_mode: bool, + highlight_anchor: Option<(usize, usize)>, total_count: u64, current_position: u64, ) { @@ -48,6 +50,8 @@ impl FormState { &values, theme, is_edit_mode, + is_highlight_mode, + highlight_anchor, total_count, current_position, ); diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index cbaa8e4..cbbcdcb 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -33,6 +33,8 @@ pub fn render_ui( buffer_state: &BufferState, theme: &Theme, is_edit_mode: bool, + is_highlight_mode: bool, + highlight_anchor: Option<(usize, usize)>, total_count: u64, current_position: u64, current_dir: &str, @@ -91,7 +93,9 @@ pub fn render_ui( theme, register_state, app_state, - register_state.current_field < 4 + register_state.current_field < 4, + is_highlight_mode, + highlight_anchor, ); } else if app_state.ui.show_login { render_login( @@ -100,7 +104,9 @@ pub fn render_ui( theme, login_state, app_state, - login_state.current_field < 2 + login_state.current_field < 2, + is_highlight_mode, + highlight_anchor, ); } else if app_state.ui.show_admin { crate::components::admin::admin_panel::render_admin_panel( @@ -165,6 +171,8 @@ pub fn render_ui( &values, theme, is_edit_mode, + is_highlight_mode, + highlight_anchor, total_count, current_position, ); diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 7443fb5..285ace6 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -91,6 +91,8 @@ pub async fn run_ui() -> Result<(), Box> { &buffer_state, &theme, is_edit_mode, + event_handler.is_highlight_mode, + event_handler.highlight_anchor, app_state.total_count, app_state.current_position, &app_state.current_dir, @@ -108,6 +110,10 @@ pub async fn run_ui() -> Result<(), Box> { AppMode::Edit => { terminal.show_cursor()?; } + AppMode::Highlight => { + terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; + terminal.show_cursor()?; + } AppMode::ReadOnly => { if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;