From 1aa1dfe3823c1534aab93164d17f9619c66dd445 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Sun, 10 Nov 2024 16:29:14 +0530 Subject: [PATCH 1/7] Use success and fail colors and reorder imports Use theme color instead of using ratatui::Color for running_command success and fail + search preview text color + min tui warning color, add colors for confirmation prompt, fix inverted success and fail colors --- tui/src/confirmation.rs | 17 +++++++++++------ tui/src/filter.rs | 5 +++-- tui/src/float.rs | 9 ++++----- tui/src/floating_text.rs | 21 ++++++++------------- tui/src/hint.rs | 3 +-- tui/src/main.rs | 12 +++++------- tui/src/running_command.rs | 11 ++++++----- tui/src/state.rs | 6 +++--- tui/src/theme.rs | 11 +++++++++-- 9 files changed, 50 insertions(+), 45 deletions(-) diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs index 96ab06ca..6e130e5a 100644 --- a/tui/src/confirmation.rs +++ b/tui/src/confirmation.rs @@ -1,13 +1,11 @@ -use std::borrow::Cow; - -use crate::{float::FloatContent, hint::Shortcut}; - +use crate::{float::FloatContent, hint::Shortcut, theme}; use ratatui::{ crossterm::event::{KeyCode, KeyEvent}, layout::Alignment, prelude::*, widgets::{Block, Borders, Clear, List}, }; +use std::borrow::Cow; pub enum ConfirmStatus { Confirm, @@ -57,12 +55,19 @@ impl ConfirmPrompt { } impl FloatContent for ConfirmPrompt { - fn draw(&mut self, frame: &mut Frame, area: Rect) { + fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &theme::Theme) { let block = Block::default() .borders(Borders::ALL) .border_set(ratatui::symbols::border::ROUNDED) .title(" Confirm selections ") - .title_bottom(" [y] to continue, [n] to abort ") + .title_bottom(Line::from(vec![ + Span::styled(" [", Style::default()), + Span::styled("y", Style::default().fg(theme.success_color())), + Span::styled("] to continue ", Style::default()), + Span::styled("[", Style::default()), + Span::styled("n", Style::default().fg(theme.fail_color())), + Span::styled("] to abort ", Style::default()), + ])) .title_alignment(Alignment::Center) .title_style(Style::default().bold()) .style(Style::default()); diff --git a/tui/src/filter.rs b/tui/src/filter.rs index f44e89a1..2fdce073 100644 --- a/tui/src/filter.rs +++ b/tui/src/filter.rs @@ -3,7 +3,7 @@ use linutil_core::{ego_tree::NodeId, Tab}; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, layout::{Position, Rect}, - style::{Color, Style}, + style::Style, text::Span, widgets::{Block, Borders, Paragraph}, Frame, @@ -144,7 +144,8 @@ impl Filter { frame.set_cursor_position(Position::new(x, y)); if let Some(preview) = &self.completion_preview { - let preview_span = Span::styled(preview, Style::default().fg(Color::DarkGray)); + let preview_span = + Span::styled(preview, Style::default().fg(theme.search_preview_color())); let preview_paragraph = Paragraph::new(preview_span).style(Style::default()); let preview_area = Rect::new( x, diff --git a/tui/src/float.rs b/tui/src/float.rs index 993684b0..ab9394a5 100644 --- a/tui/src/float.rs +++ b/tui/src/float.rs @@ -1,13 +1,12 @@ +use crate::{hint::Shortcut, theme::Theme}; use ratatui::{ crossterm::event::{KeyCode, KeyEvent}, layout::{Constraint, Direction, Layout, Rect}, Frame, }; -use crate::hint::Shortcut; - pub trait FloatContent { - fn draw(&mut self, frame: &mut Frame, area: Rect); + fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &Theme); fn handle_key_event(&mut self, key: &KeyEvent) -> bool; fn is_finished(&self) -> bool; fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>); @@ -48,9 +47,9 @@ impl Float { .split(hor_float)[1] } - pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect) { + pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect, theme: &Theme) { let popup_area = self.floating_window(parent_area); - self.content.draw(frame, popup_area); + self.content.draw(frame, popup_area, theme); } // Returns true if the floating window is finished. diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs index c307b854..c534064b 100644 --- a/tui/src/floating_text.rs +++ b/tui/src/floating_text.rs @@ -1,13 +1,6 @@ -use std::{ - borrow::Cow, - collections::VecDeque, - io::{Cursor, Read as _, Seek, SeekFrom, Write as _}, -}; - -use crate::{float::FloatContent, hint::Shortcut}; - +use crate::{float::FloatContent, hint::Shortcut, theme::Theme}; +use ansi_to_tui::IntoText; use linutil_core::Command; - use ratatui::{ crossterm::event::{KeyCode, KeyEvent}, layout::Rect, @@ -16,9 +9,11 @@ use ratatui::{ widgets::{Block, Borders, Clear, List}, Frame, }; - -use ansi_to_tui::IntoText; - +use std::{ + borrow::Cow, + collections::VecDeque, + io::{Cursor, Read as _, Seek, SeekFrom, Write as _}, +}; use textwrap::wrap; use tree_sitter_bash as hl_bash; use tree_sitter_highlight::{self as hl, HighlightEvent}; @@ -209,7 +204,7 @@ impl FloatingText { } impl FloatContent for FloatingText { - fn draw(&mut self, frame: &mut Frame, area: Rect) { + fn draw(&mut self, frame: &mut Frame, area: Rect, _theme: &Theme) { self.frame_height = area.height as usize; // Define the Block with a border and background color diff --git a/tui/src/hint.rs b/tui/src/hint.rs index 82c265c8..2ab4b99d 100644 --- a/tui/src/hint.rs +++ b/tui/src/hint.rs @@ -1,9 +1,8 @@ -use std::borrow::Cow; - use ratatui::{ style::{Style, Stylize}, text::{Line, Span}, }; +use std::borrow::Cow; pub struct Shortcut { pub key_sequences: Vec>, diff --git a/tui/src/main.rs b/tui/src/main.rs index 7a9f4067..f5d2b7cd 100644 --- a/tui/src/main.rs +++ b/tui/src/main.rs @@ -7,15 +7,8 @@ mod running_command; pub mod state; mod theme; -use std::{ - io::{self, stdout}, - path::PathBuf, - time::Duration, -}; - use crate::theme::Theme; use clap::Parser; - use ratatui::{ backend::CrosstermBackend, crossterm::{ @@ -27,6 +20,11 @@ use ratatui::{ Terminal, }; use state::AppState; +use std::{ + io::{self, stdout}, + path::PathBuf, + time::Duration, +}; // Linux utility toolbox #[derive(Debug, Parser)] diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs index 779a0b3c..368ed6ca 100644 --- a/tui/src/running_command.rs +++ b/tui/src/running_command.rs @@ -1,4 +1,4 @@ -use crate::{float::FloatContent, hint::Shortcut}; +use crate::{float::FloatContent, hint::Shortcut, theme::Theme}; use linutil_core::Command; use oneshot::{channel, Receiver}; use portable_pty::{ @@ -7,7 +7,7 @@ use portable_pty::{ use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, layout::{Rect, Size}, - style::{Color, Style, Stylize}, + style::{Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders}, Frame, @@ -22,6 +22,7 @@ use tui_term::{ vt100::{self, Screen}, widget::PseudoTerminal, }; + pub struct RunningCommand { /// A buffer to save all the command output (accumulates, until the command exits) buffer: Arc>>, @@ -42,7 +43,7 @@ pub struct RunningCommand { } impl FloatContent for RunningCommand { - fn draw(&mut self, frame: &mut Frame, area: Rect) { + fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) { // Calculate the inner size of the terminal area, considering borders let inner_size = Size { width: area.width - 2, // Adjust for border width @@ -64,13 +65,13 @@ impl FloatContent for RunningCommand { Line::from( Span::default() .content("SUCCESS!") - .style(Style::default().fg(Color::Green).reversed()), + .style(Style::default().fg(theme.success_color()).reversed()), ) } else { Line::from( Span::default() .content("FAILED!") - .style(Style::default().fg(Color::Red).reversed()), + .style(Style::default().fg(theme.fail_color()).reversed()), ) }; diff --git a/tui/src/state.rs b/tui/src/state.rs index 5ee34079..7b31deca 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -229,7 +229,7 @@ impl AppState { MIN_HEIGHT, )) .alignment(Alignment::Center) - .style(Style::default().fg(ratatui::style::Color::Red).bold()) + .style(Style::default().fg(self.theme.fail_color()).bold()) .wrap(ratatui::widgets::Wrap { trim: true }); let centered_layout = Layout::default() @@ -461,8 +461,8 @@ impl AppState { frame.render_stateful_widget(disclaimer_list, list_chunks[1], &mut self.selection); match &mut self.focus { - Focus::FloatingWindow(float) => float.draw(frame, chunks[1]), - Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1]), + Focus::FloatingWindow(float) => float.draw(frame, chunks[1], &self.theme), + Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1], &self.theme), _ => {} } diff --git a/tui/src/theme.rs b/tui/src/theme.rs index d87e87ee..9e10ce70 100644 --- a/tui/src/theme.rs +++ b/tui/src/theme.rs @@ -72,14 +72,14 @@ impl Theme { pub fn success_color(&self) -> Color { match self { - Theme::Default => Color::Rgb(199, 55, 44), + Theme::Default => Color::Rgb(5, 255, 55), Theme::Compatible => Color::Green, } } pub fn fail_color(&self) -> Color { match self { - Theme::Default => Color::Rgb(5, 255, 55), + Theme::Default => Color::Rgb(199, 55, 44), Theme::Compatible => Color::Red, } } @@ -91,6 +91,13 @@ impl Theme { } } + pub fn search_preview_color(&self) -> Color { + match self { + Theme::Default => Color::DarkGray, + Theme::Compatible => Color::DarkGray, + } + } + pub fn unfocused_color(&self) -> Color { match self { Theme::Default => Color::Gray, From 3b7e859af80bfa1f453928a74c47efd897e26934 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Sun, 10 Nov 2024 16:34:16 +0530 Subject: [PATCH 2/7] Remove redundant code in themes Removed redundant match statement with a function --- tui/src/theme.rs | 77 ++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/tui/src/theme.rs b/tui/src/theme.rs index 9e10ce70..3897b18b 100644 --- a/tui/src/theme.rs +++ b/tui/src/theme.rs @@ -14,95 +14,70 @@ pub enum Theme { } impl Theme { - pub fn dir_color(&self) -> Color { + fn get_color_variant(&self, default: Color, compatible: Color) -> Color { match self { - Theme::Default => Color::Blue, - Theme::Compatible => Color::Blue, + Theme::Default => default, + Theme::Compatible => compatible, } } + fn get_icon_variant(&self, default: &'static str, compatible: &'static str) -> &'static str { + match self { + Theme::Default => default, + Theme::Compatible => compatible, + } + } + + pub fn dir_color(&self) -> Color { + self.get_color_variant(Color::Blue, Color::Blue) + } + pub fn cmd_color(&self) -> Color { - match self { - Theme::Default => Color::Rgb(204, 224, 208), - Theme::Compatible => Color::LightGreen, - } + self.get_color_variant(Color::Rgb(204, 224, 208), Color::LightGreen) } pub fn multi_select_disabled_color(&self) -> Color { - match self { - Theme::Default => Color::DarkGray, - Theme::Compatible => Color::DarkGray, - } + self.get_color_variant(Color::DarkGray, Color::DarkGray) } pub fn tab_color(&self) -> Color { - match self { - Theme::Default => Color::Rgb(255, 255, 85), - Theme::Compatible => Color::Yellow, - } + self.get_color_variant(Color::Rgb(255, 255, 85), Color::Yellow) } pub fn dir_icon(&self) -> &'static str { - match self { - Theme::Default => "  ", - Theme::Compatible => "[DIR]", - } + self.get_icon_variant("  ", "[DIR]") } pub fn cmd_icon(&self) -> &'static str { - match self { - Theme::Default => "  ", - Theme::Compatible => "[CMD]", - } + self.get_icon_variant("  ", "[CMD]") } pub fn tab_icon(&self) -> &'static str { - match self { - Theme::Default => " ", - Theme::Compatible => ">> ", - } + self.get_icon_variant(" ", ">> ") } pub fn multi_select_icon(&self) -> &'static str { - match self { - Theme::Default => "", - Theme::Compatible => "*", - } + self.get_icon_variant("", "*") } pub fn success_color(&self) -> Color { - match self { - Theme::Default => Color::Rgb(5, 255, 55), - Theme::Compatible => Color::Green, - } + self.get_color_variant(Color::Rgb(5, 255, 55), Color::Green) } pub fn fail_color(&self) -> Color { - match self { - Theme::Default => Color::Rgb(199, 55, 44), - Theme::Compatible => Color::Red, - } + self.get_color_variant(Color::Rgb(199, 55, 44), Color::Red) } pub fn focused_color(&self) -> Color { - match self { - Theme::Default => Color::LightBlue, - Theme::Compatible => Color::LightBlue, - } + self.get_color_variant(Color::LightBlue, Color::LightBlue) } pub fn search_preview_color(&self) -> Color { - match self { - Theme::Default => Color::DarkGray, - Theme::Compatible => Color::DarkGray, - } + self.get_color_variant(Color::DarkGray, Color::DarkGray) } pub fn unfocused_color(&self) -> Color { - match self { - Theme::Default => Color::Gray, - Theme::Compatible => Color::Gray, - } + self.get_color_variant(Color::Gray, Color::Gray) } } From 190c26cd76bd535ac88231ed61d157dea33aacbe Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Sun, 10 Nov 2024 16:52:03 +0530 Subject: [PATCH 3/7] Fix scroll beyond list, color bleeding and refact in confirmation.rs Remove unnecessary usage of pub in ConfirmPropmt struct fields, simplify numbering, prevent scrolling beyond list, fix color bleeding --- tui/src/confirmation.rs | 50 +++++++++++++++++++---------------------- tui/src/state.rs | 4 ++-- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs index 6e130e5a..64b160e0 100644 --- a/tui/src/confirmation.rs +++ b/tui/src/confirmation.rs @@ -14,35 +14,30 @@ pub enum ConfirmStatus { } pub struct ConfirmPrompt { - pub names: Box<[String]>, - pub status: ConfirmStatus, + inner_area_height: usize, + names: Box<[String]>, scroll: usize, + pub status: ConfirmStatus, } impl ConfirmPrompt { - pub fn new(names: &[&str]) -> Self { - let max_count_str = format!("{}", names.len()); + pub fn new(names: Vec<&str>) -> Self { let names = names .iter() .zip(1..) - .map(|(name, n)| { - let count_str = format!("{n}"); - let space_str = (0..(max_count_str.len() - count_str.len())) - .map(|_| ' ') - .collect::(); - format!("{space_str}{n}. {name}") - }) + .map(|(name, n)| format!(" {n}. {name}")) .collect(); Self { + inner_area_height: 0, names, - status: ConfirmStatus::None, scroll: 0, + status: ConfirmStatus::None, } } pub fn scroll_down(&mut self) { - if self.scroll < self.names.len() - 1 { + if self.scroll + self.inner_area_height < self.names.len() - 1 { self.scroll += 1; } } @@ -72,9 +67,11 @@ impl FloatContent for ConfirmPrompt { .title_style(Style::default().bold()) .style(Style::default()); - frame.render_widget(block.clone(), area); - let inner_area = block.inner(area); + self.inner_area_height = inner_area.height as usize; + + frame.render_widget(Clear, area); + frame.render_widget(block, area); let paths_text = self .names @@ -86,26 +83,25 @@ impl FloatContent for ConfirmPrompt { }) .collect::(); - frame.render_widget(Clear, inner_area); frame.render_widget(List::new(paths_text), inner_area); } fn handle_key_event(&mut self, key: &KeyEvent) -> bool { - use KeyCode::*; + use ConfirmStatus::*; + use KeyCode::{Char, Down, Esc, Up}; self.status = match key.code { - Char('y') | Char('Y') => ConfirmStatus::Confirm, - Char('n') | Char('N') | Esc | Char('q') => ConfirmStatus::Abort, - Char('j') => { + Char('y') | Char('Y') => Confirm, + Char('n') | Char('N') | Esc | Char('q') => Abort, + Char('j') | Char('J') | Down => { self.scroll_down(); - ConfirmStatus::None + None } - Char('k') => { + Char('k') | Char('K') | Up => { self.scroll_up(); - ConfirmStatus::None + None } - _ => ConfirmStatus::None, + _ => None, }; - false } @@ -123,8 +119,8 @@ impl FloatContent for ConfirmPrompt { Box::new([ Shortcut::new("Continue", ["Y", "y"]), Shortcut::new("Abort", ["N", "n", "q", "Esc"]), - Shortcut::new("Scroll up", ["k"]), - Shortcut::new("Scroll down", ["j"]), + Shortcut::new("Scroll up", ["k", "Up"]), + Shortcut::new("Scroll down", ["j", "Down"]), Shortcut::new("Close linutil", ["CTRL-c"]), ]), ) diff --git a/tui/src/state.rs b/tui/src/state.rs index 7b31deca..ed320a2d 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -137,7 +137,7 @@ impl AppState { .map(|node| node.name.as_str()) .collect(); - let prompt = ConfirmPrompt::new(&cmd_names); + let prompt = ConfirmPrompt::new(cmd_names); self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); } } @@ -798,7 +798,7 @@ impl AppState { .map(|node| node.name.as_str()) .collect::>(); - let prompt = ConfirmPrompt::new(&cmd_names[..]); + let prompt = ConfirmPrompt::new(cmd_names); self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); } } From 0b4f33c761f27a2efb12628b60cb6b6afc89cd28 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Sun, 10 Nov 2024 18:46:35 +0530 Subject: [PATCH 4/7] Implement case insensitive, fix word disappearing bug Use regex for case insesitive finding, implement String instead of char, fix word disappearing by recalculating the render x for preview text --- Cargo.lock | 2 +- tui/Cargo.toml | 2 +- tui/src/filter.rs | 120 +++++++++++++++++++++++++--------------------- 3 files changed, 68 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86b9ef04..4a05c1b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,13 +449,13 @@ dependencies = [ "portable-pty", "rand", "ratatui", + "regex", "temp-dir", "textwrap", "time", "tree-sitter-bash", "tree-sitter-highlight", "tui-term", - "unicode-width 0.2.0", "zips", ] diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 4051d3b3..efc46293 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -21,7 +21,6 @@ ratatui = "0.29.0" tui-term = "0.2.0" temp-dir = "0.1.14" time = { version = "0.3.36", features = ["local-offset", "macros", "formatting"] } -unicode-width = "0.2.0" rand = { version = "0.8.5", optional = true } linutil_core = { path = "../core", version = "24.9.28" } tree-sitter-highlight = "0.24.3" @@ -30,6 +29,7 @@ textwrap = "0.16.1" anstyle = "1.0.8" ansi-to-tui = "7.0.0" zips = "0.1.7" +regex = { version = "1.3", default-features = false, features = ["std"] } [[bin]] name = "linutil" diff --git a/tui/src/filter.rs b/tui/src/filter.rs index 2fdce073..99d9bcf5 100644 --- a/tui/src/filter.rs +++ b/tui/src/filter.rs @@ -8,7 +8,7 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, Frame, }; -use unicode_width::UnicodeWidthChar; +use regex::RegexBuilder; pub enum SearchAction { None, @@ -17,7 +17,7 @@ pub enum SearchAction { } pub struct Filter { - search_input: Vec, + search_input: String, in_search_mode: bool, input_position: usize, items: Vec, @@ -27,7 +27,7 @@ pub struct Filter { impl Filter { pub fn new() -> Self { Self { - search_input: vec![], + search_input: String::new(), in_search_mode: false, input_position: 0, items: vec![], @@ -62,47 +62,45 @@ impl Filter { .collect(); } else { self.items.clear(); - - let query_lower = self.search_input.iter().collect::().to_lowercase(); - for tab in tabs.iter() { - let mut stack = vec![tab.tree.root().id()]; - while let Some(node_id) = stack.pop() { - let node = tab.tree.get(node_id).unwrap(); - - if node.value().name.to_lowercase().contains(&query_lower) - && !node.has_children() - { - self.items.push(ListEntry { - node: node.value().clone(), - id: node.id(), - has_children: false, - }); + if let Ok(regex) = self.regex_builder(®ex::escape(&self.search_input)) { + for tab in tabs { + let mut stack = vec![tab.tree.root().id()]; + while let Some(node_id) = stack.pop() { + let node = tab.tree.get(node_id).unwrap(); + if regex.is_match(&node.value().name) && !node.has_children() { + self.items.push(ListEntry { + node: node.value().clone(), + id: node.id(), + has_children: false, + }); + } + stack.extend(node.children().map(|child| child.id())); } - - stack.extend(node.children().map(|child| child.id())); } + self.items + .sort_unstable_by(|a, b| a.node.name.cmp(&b.node.name)); + } else { + self.search_input.clear(); } - self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name)); } - self.update_completion_preview(); } fn update_completion_preview(&mut self) { - if self.search_input.is_empty() { - self.completion_preview = None; - return; - } - - let input = self.search_input.iter().collect::().to_lowercase(); - self.completion_preview = self.items.iter().find_map(|item| { - let item_name_lower = item.node.name.to_lowercase(); - if item_name_lower.starts_with(&input) { - Some(item_name_lower[input.len()..].to_string()) + self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() { + None + } else { + let pattern = format!("(?i)^{}", regex::escape(&self.search_input)); + if let Ok(regex) = self.regex_builder(&pattern) { + self.items.iter().find_map(|item| { + regex + .find(&item.node.name) + .map(|mat| item.node.name[mat.end()..].to_string()) + }) } else { None } - }); + } } pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) { @@ -110,8 +108,10 @@ impl Filter { let display_text = if !self.in_search_mode && self.search_input.is_empty() { Span::raw("Press / to search") } else { - let input_text = self.search_input.iter().collect::(); - Span::styled(input_text, Style::default().fg(theme.focused_color())) + Span::styled( + &self.search_input, + Style::default().fg(theme.focused_color()), + ) }; let search_color = if self.in_search_mode { @@ -135,25 +135,16 @@ impl Filter { // Render cursor in search bar if self.in_search_mode { - let cursor_position: usize = self.search_input[..self.input_position] - .iter() - .map(|c| c.width().unwrap_or(1)) - .sum(); - let x = area.x + cursor_position as u16 + 1; + let x = area.x + self.input_position as u16 + 1; let y = area.y + 1; frame.set_cursor_position(Position::new(x, y)); if let Some(preview) = &self.completion_preview { + let preview_x = area.x + self.search_input.len() as u16 + 1; let preview_span = Span::styled(preview, Style::default().fg(theme.search_preview_color())); - let preview_paragraph = Paragraph::new(preview_span).style(Style::default()); - let preview_area = Rect::new( - x, - y, - (preview.len() as u16).min(area.width - cursor_position as u16 - 1), - 1, - ); - frame.render_widget(preview_paragraph, preview_area); + let preview_area = Rect::new(preview_x, y, preview.len() as u16, 1); + frame.render_widget(Paragraph::new(preview_span), preview_area); } } } @@ -220,14 +211,35 @@ impl Filter { } } + fn regex_builder(&self, pattern: &str) -> Result { + RegexBuilder::new(pattern).case_insensitive(true).build() + } + fn complete_search(&mut self) -> SearchAction { - if let Some(completion) = self.completion_preview.take() { - self.search_input.extend(completion.chars()); - self.input_position = self.search_input.len(); - self.update_completion_preview(); - SearchAction::Update - } else { + if self.completion_preview.is_none() { SearchAction::None + } else { + let pattern = format!("(?i)^{}", self.search_input); + if let Ok(regex) = self.regex_builder(&pattern) { + self.search_input = self + .items + .iter() + .find_map(|item| { + if regex.is_match(&item.node.name) { + Some(item.node.name.clone()) + } else { + None + } + }) + .unwrap_or_default(); + + self.completion_preview = None; + self.input_position = self.search_input.len(); + + SearchAction::Update + } else { + SearchAction::None + } } } From 7e96651bbe4f497b9f8e6e3bb1392ef358a14adf Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Mon, 11 Nov 2024 07:20:59 +0530 Subject: [PATCH 5/7] Revert "Remove redundant code in themes" This reverts commit 3b7e859af80bfa1f453928a74c47efd897e26934. --- tui/src/theme.rs | 79 +++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/tui/src/theme.rs b/tui/src/theme.rs index 3897b18b..9e10ce70 100644 --- a/tui/src/theme.rs +++ b/tui/src/theme.rs @@ -14,70 +14,95 @@ pub enum Theme { } impl Theme { - fn get_color_variant(&self, default: Color, compatible: Color) -> Color { - match self { - Theme::Default => default, - Theme::Compatible => compatible, - } - } - - fn get_icon_variant(&self, default: &'static str, compatible: &'static str) -> &'static str { - match self { - Theme::Default => default, - Theme::Compatible => compatible, - } - } - pub fn dir_color(&self) -> Color { - self.get_color_variant(Color::Blue, Color::Blue) + match self { + Theme::Default => Color::Blue, + Theme::Compatible => Color::Blue, + } } pub fn cmd_color(&self) -> Color { - self.get_color_variant(Color::Rgb(204, 224, 208), Color::LightGreen) + match self { + Theme::Default => Color::Rgb(204, 224, 208), + Theme::Compatible => Color::LightGreen, + } } pub fn multi_select_disabled_color(&self) -> Color { - self.get_color_variant(Color::DarkGray, Color::DarkGray) + match self { + Theme::Default => Color::DarkGray, + Theme::Compatible => Color::DarkGray, + } } pub fn tab_color(&self) -> Color { - self.get_color_variant(Color::Rgb(255, 255, 85), Color::Yellow) + match self { + Theme::Default => Color::Rgb(255, 255, 85), + Theme::Compatible => Color::Yellow, + } } pub fn dir_icon(&self) -> &'static str { - self.get_icon_variant("  ", "[DIR]") + match self { + Theme::Default => "  ", + Theme::Compatible => "[DIR]", + } } pub fn cmd_icon(&self) -> &'static str { - self.get_icon_variant("  ", "[CMD]") + match self { + Theme::Default => "  ", + Theme::Compatible => "[CMD]", + } } pub fn tab_icon(&self) -> &'static str { - self.get_icon_variant(" ", ">> ") + match self { + Theme::Default => " ", + Theme::Compatible => ">> ", + } } pub fn multi_select_icon(&self) -> &'static str { - self.get_icon_variant("", "*") + match self { + Theme::Default => "", + Theme::Compatible => "*", + } } pub fn success_color(&self) -> Color { - self.get_color_variant(Color::Rgb(5, 255, 55), Color::Green) + match self { + Theme::Default => Color::Rgb(5, 255, 55), + Theme::Compatible => Color::Green, + } } pub fn fail_color(&self) -> Color { - self.get_color_variant(Color::Rgb(199, 55, 44), Color::Red) + match self { + Theme::Default => Color::Rgb(199, 55, 44), + Theme::Compatible => Color::Red, + } } pub fn focused_color(&self) -> Color { - self.get_color_variant(Color::LightBlue, Color::LightBlue) + match self { + Theme::Default => Color::LightBlue, + Theme::Compatible => Color::LightBlue, + } } pub fn search_preview_color(&self) -> Color { - self.get_color_variant(Color::DarkGray, Color::DarkGray) + match self { + Theme::Default => Color::DarkGray, + Theme::Compatible => Color::DarkGray, + } } pub fn unfocused_color(&self) -> Color { - self.get_color_variant(Color::Gray, Color::Gray) + match self { + Theme::Default => Color::Gray, + Theme::Compatible => Color::Gray, + } } } From 35159b8393d51a326caf959cb6fb4ed47b217534 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Mon, 11 Nov 2024 08:00:03 +0530 Subject: [PATCH 6/7] Reference instead of passing the vector --- tui/src/confirmation.rs | 2 +- tui/src/state.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs index 64b160e0..1993c3f5 100644 --- a/tui/src/confirmation.rs +++ b/tui/src/confirmation.rs @@ -21,7 +21,7 @@ pub struct ConfirmPrompt { } impl ConfirmPrompt { - pub fn new(names: Vec<&str>) -> Self { + pub fn new(names: &[&str]) -> Self { let names = names .iter() .zip(1..) diff --git a/tui/src/state.rs b/tui/src/state.rs index ed320a2d..396123a9 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -137,7 +137,7 @@ impl AppState { .map(|node| node.name.as_str()) .collect(); - let prompt = ConfirmPrompt::new(cmd_names); + let prompt = ConfirmPrompt::new(&cmd_names); self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); } } @@ -798,7 +798,7 @@ impl AppState { .map(|node| node.name.as_str()) .collect::>(); - let prompt = ConfirmPrompt::new(cmd_names); + let prompt = ConfirmPrompt::new(&cmd_names); self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); } } From 2d1f5dbc80fe18fcf767082a046b306fe78234a3 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Mon, 11 Nov 2024 13:23:36 +0530 Subject: [PATCH 7/7] Revert regex and String implementation Use Vec for search_input to prevent panics when using multi-byte characters, use lowercase conversion instead of regex, Added comments for clarity --- Cargo.lock | 2 +- tui/Cargo.toml | 2 +- tui/src/filter.rs | 129 +++++++++++++++++++++++----------------------- 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a05c1b0..86b9ef04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,13 +449,13 @@ dependencies = [ "portable-pty", "rand", "ratatui", - "regex", "temp-dir", "textwrap", "time", "tree-sitter-bash", "tree-sitter-highlight", "tui-term", + "unicode-width 0.2.0", "zips", ] diff --git a/tui/Cargo.toml b/tui/Cargo.toml index efc46293..9510cab6 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -29,7 +29,7 @@ textwrap = "0.16.1" anstyle = "1.0.8" ansi-to-tui = "7.0.0" zips = "0.1.7" -regex = { version = "1.3", default-features = false, features = ["std"] } +unicode-width = "0.2.0" [[bin]] name = "linutil" diff --git a/tui/src/filter.rs b/tui/src/filter.rs index 99d9bcf5..60a64223 100644 --- a/tui/src/filter.rs +++ b/tui/src/filter.rs @@ -8,7 +8,7 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, Frame, }; -use regex::RegexBuilder; +use unicode_width::UnicodeWidthChar; pub enum SearchAction { None, @@ -17,17 +17,19 @@ pub enum SearchAction { } pub struct Filter { - search_input: String, + // Use Vec to handle multi-byte characters like emojis + search_input: Vec, in_search_mode: bool, input_position: usize, items: Vec, + // No complex string manipulation is done with completion_preview so we can use String unlike search_input completion_preview: Option, } impl Filter { pub fn new() -> Self { Self { - search_input: String::new(), + search_input: vec![], in_search_mode: false, input_position: 0, items: vec![], @@ -62,26 +64,25 @@ impl Filter { .collect(); } else { self.items.clear(); - if let Ok(regex) = self.regex_builder(®ex::escape(&self.search_input)) { - for tab in tabs { - let mut stack = vec![tab.tree.root().id()]; - while let Some(node_id) = stack.pop() { - let node = tab.tree.get(node_id).unwrap(); - if regex.is_match(&node.value().name) && !node.has_children() { - self.items.push(ListEntry { - node: node.value().clone(), - id: node.id(), - has_children: false, - }); - } - stack.extend(node.children().map(|child| child.id())); + let query_lower = self.search_input.iter().collect::().to_lowercase(); + for tab in tabs { + let mut stack = vec![tab.tree.root().id()]; + while let Some(node_id) = stack.pop() { + let node = tab.tree.get(node_id).unwrap(); + if node.value().name.to_lowercase().contains(&query_lower) + && !node.has_children() + { + self.items.push(ListEntry { + node: node.value().clone(), + id: node.id(), + has_children: false, + }); } + stack.extend(node.children().map(|child| child.id())); } - self.items - .sort_unstable_by(|a, b| a.node.name.cmp(&b.node.name)); - } else { - self.search_input.clear(); } + self.items + .sort_unstable_by(|a, b| a.node.name.cmp(&b.node.name)); } self.update_completion_preview(); } @@ -90,16 +91,15 @@ impl Filter { self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() { None } else { - let pattern = format!("(?i)^{}", regex::escape(&self.search_input)); - if let Ok(regex) = self.regex_builder(&pattern) { - self.items.iter().find_map(|item| { - regex - .find(&item.node.name) - .map(|mat| item.node.name[mat.end()..].to_string()) - }) - } else { - None - } + let input = self.search_input.iter().collect::().to_lowercase(); + self.items.iter().find_map(|item| { + let item_name_lower = &item.node.name.to_lowercase(); + if item_name_lower.starts_with(&input) { + Some(item_name_lower[input.len()..].to_string()) + } else { + None + } + }) } } @@ -108,10 +108,8 @@ impl Filter { let display_text = if !self.in_search_mode && self.search_input.is_empty() { Span::raw("Press / to search") } else { - Span::styled( - &self.search_input, - Style::default().fg(theme.focused_color()), - ) + let input_text = self.search_input.iter().collect::(); + Span::styled(input_text, Style::default().fg(theme.focused_color())) }; let search_color = if self.in_search_mode { @@ -135,15 +133,31 @@ impl Filter { // Render cursor in search bar if self.in_search_mode { - let x = area.x + self.input_position as u16 + 1; + // Calculate the visual width of search input so that completion preview can be displayed after the search input + let search_input_size: u16 = self + .search_input + .iter() + .map(|c| c.width().unwrap_or(1) as u16) + .sum(); + + let cursor_position: u16 = self.search_input[..self.input_position] + .iter() + .map(|c| c.width().unwrap_or(1) as u16) + .sum(); + let x = area.x + cursor_position + 1; let y = area.y + 1; frame.set_cursor_position(Position::new(x, y)); if let Some(preview) = &self.completion_preview { - let preview_x = area.x + self.search_input.len() as u16 + 1; + let preview_x = area.x + search_input_size + 1; let preview_span = Span::styled(preview, Style::default().fg(theme.search_preview_color())); - let preview_area = Rect::new(preview_x, y, preview.len() as u16, 1); + let preview_area = Rect::new( + preview_x, + y, + (preview.len() as u16).min(area.width - search_input_size - 1), // Ensure the completion preview stays within the search bar bounds + 1, + ); frame.render_widget(Paragraph::new(preview_span), preview_area); } } @@ -211,35 +225,22 @@ impl Filter { } } - fn regex_builder(&self, pattern: &str) -> Result { - RegexBuilder::new(pattern).case_insensitive(true).build() - } - fn complete_search(&mut self) -> SearchAction { - if self.completion_preview.is_none() { - SearchAction::None - } else { - let pattern = format!("(?i)^{}", self.search_input); - if let Ok(regex) = self.regex_builder(&pattern) { - self.search_input = self - .items - .iter() - .find_map(|item| { - if regex.is_match(&item.node.name) { - Some(item.node.name.clone()) - } else { - None - } - }) - .unwrap_or_default(); - - self.completion_preview = None; - self.input_position = self.search_input.len(); - - SearchAction::Update - } else { - SearchAction::None + if self.completion_preview.is_some() { + let input = &self.search_input.iter().collect::().to_lowercase(); + if let Some(search_completion) = self + .items + .iter() + .find(|item| item.node.name.to_lowercase().starts_with(input)) + { + self.search_input = search_completion.node.name.chars().collect(); } + + self.input_position = self.search_input.len(); + self.completion_preview = None; + SearchAction::Update + } else { + SearchAction::None } }