Compare commits

..

4 Commits

Author SHA1 Message Date
Jeevitha Kannan K S
c16bfd7a12
Merge 2d1f5dbc80 into 76f8e6438b 2024-11-11 07:55:47 +00:00
Jeevitha Kannan K S
2d1f5dbc80
Revert regex and String implementation
Use Vec<char> for search_input to prevent panics when using multi-byte characters, use lowercase conversion instead of regex, Added comments for clarity
2024-11-11 13:23:36 +05:30
Jeevitha Kannan K S
35159b8393
Reference instead of passing the vector 2024-11-11 08:00:03 +05:30
Jeevitha Kannan K S
7e96651bbe
Revert "Remove redundant code in themes"
This reverts commit 3b7e859af8.
2024-11-11 07:20:59 +05:30
6 changed files with 122 additions and 96 deletions

2
Cargo.lock generated
View File

@ -449,13 +449,13 @@ dependencies = [
"portable-pty", "portable-pty",
"rand", "rand",
"ratatui", "ratatui",
"regex",
"temp-dir", "temp-dir",
"textwrap", "textwrap",
"time", "time",
"tree-sitter-bash", "tree-sitter-bash",
"tree-sitter-highlight", "tree-sitter-highlight",
"tui-term", "tui-term",
"unicode-width 0.2.0",
"zips", "zips",
] ]

View File

@ -29,7 +29,7 @@ textwrap = "0.16.1"
anstyle = "1.0.8" anstyle = "1.0.8"
ansi-to-tui = "7.0.0" ansi-to-tui = "7.0.0"
zips = "0.1.7" zips = "0.1.7"
regex = { version = "1.3", default-features = false, features = ["std"] } unicode-width = "0.2.0"
[[bin]] [[bin]]
name = "linutil" name = "linutil"

View File

@ -21,7 +21,7 @@ pub struct ConfirmPrompt {
} }
impl ConfirmPrompt { impl ConfirmPrompt {
pub fn new(names: Vec<&str>) -> Self { pub fn new(names: &[&str]) -> Self {
let names = names let names = names
.iter() .iter()
.zip(1..) .zip(1..)

View File

@ -8,7 +8,7 @@ use ratatui::{
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
Frame, Frame,
}; };
use regex::RegexBuilder; use unicode_width::UnicodeWidthChar;
pub enum SearchAction { pub enum SearchAction {
None, None,
@ -17,17 +17,19 @@ pub enum SearchAction {
} }
pub struct Filter { pub struct Filter {
search_input: String, // Use Vec<char> to handle multi-byte characters like emojis
search_input: Vec<char>,
in_search_mode: bool, in_search_mode: bool,
input_position: usize, input_position: usize,
items: Vec<ListEntry>, items: Vec<ListEntry>,
// No complex string manipulation is done with completion_preview so we can use String unlike search_input
completion_preview: Option<String>, completion_preview: Option<String>,
} }
impl Filter { impl Filter {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
search_input: String::new(), search_input: vec![],
in_search_mode: false, in_search_mode: false,
input_position: 0, input_position: 0,
items: vec![], items: vec![],
@ -62,26 +64,25 @@ impl Filter {
.collect(); .collect();
} else { } else {
self.items.clear(); self.items.clear();
if let Ok(regex) = self.regex_builder(&regex::escape(&self.search_input)) { let query_lower = self.search_input.iter().collect::<String>().to_lowercase();
for tab in tabs { for tab in tabs {
let mut stack = vec![tab.tree.root().id()]; let mut stack = vec![tab.tree.root().id()];
while let Some(node_id) = stack.pop() { while let Some(node_id) = stack.pop() {
let node = tab.tree.get(node_id).unwrap(); let node = tab.tree.get(node_id).unwrap();
if regex.is_match(&node.value().name) && !node.has_children() { if node.value().name.to_lowercase().contains(&query_lower)
self.items.push(ListEntry { && !node.has_children()
node: node.value().clone(), {
id: node.id(), self.items.push(ListEntry {
has_children: false, 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_unstable_by(|a, b| a.node.name.cmp(&b.node.name));
} }
self.update_completion_preview(); self.update_completion_preview();
} }
@ -90,16 +91,15 @@ impl Filter {
self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() { self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() {
None None
} else { } else {
let pattern = format!("(?i)^{}", regex::escape(&self.search_input)); let input = self.search_input.iter().collect::<String>().to_lowercase();
if let Ok(regex) = self.regex_builder(&pattern) { self.items.iter().find_map(|item| {
self.items.iter().find_map(|item| { let item_name_lower = &item.node.name.to_lowercase();
regex if item_name_lower.starts_with(&input) {
.find(&item.node.name) Some(item_name_lower[input.len()..].to_string())
.map(|mat| item.node.name[mat.end()..].to_string()) } else {
}) None
} else { }
None })
}
} }
} }
@ -108,10 +108,8 @@ impl Filter {
let display_text = if !self.in_search_mode && self.search_input.is_empty() { let display_text = if !self.in_search_mode && self.search_input.is_empty() {
Span::raw("Press / to search") Span::raw("Press / to search")
} else { } else {
Span::styled( let input_text = self.search_input.iter().collect::<String>();
&self.search_input, Span::styled(input_text, Style::default().fg(theme.focused_color()))
Style::default().fg(theme.focused_color()),
)
}; };
let search_color = if self.in_search_mode { let search_color = if self.in_search_mode {
@ -135,15 +133,31 @@ impl Filter {
// Render cursor in search bar // Render cursor in search bar
if self.in_search_mode { 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; let y = area.y + 1;
frame.set_cursor_position(Position::new(x, y)); frame.set_cursor_position(Position::new(x, y));
if let Some(preview) = &self.completion_preview { 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 = let preview_span =
Span::styled(preview, Style::default().fg(theme.search_preview_color())); 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); frame.render_widget(Paragraph::new(preview_span), preview_area);
} }
} }
@ -211,35 +225,22 @@ impl Filter {
} }
} }
fn regex_builder(&self, pattern: &str) -> Result<regex::Regex, regex::Error> {
RegexBuilder::new(pattern).case_insensitive(true).build()
}
fn complete_search(&mut self) -> SearchAction { fn complete_search(&mut self) -> SearchAction {
if self.completion_preview.is_none() { if self.completion_preview.is_some() {
SearchAction::None let input = &self.search_input.iter().collect::<String>().to_lowercase();
} else { if let Some(search_completion) = self
let pattern = format!("(?i)^{}", self.search_input); .items
if let Ok(regex) = self.regex_builder(&pattern) { .iter()
self.search_input = self .find(|item| item.node.name.to_lowercase().starts_with(input))
.items {
.iter() self.search_input = search_completion.node.name.chars().collect();
.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
} }
self.input_position = self.search_input.len();
self.completion_preview = None;
SearchAction::Update
} else {
SearchAction::None
} }
} }

View File

@ -137,7 +137,7 @@ impl AppState {
.map(|node| node.name.as_str()) .map(|node| node.name.as_str())
.collect(); .collect();
let prompt = ConfirmPrompt::new(cmd_names); let prompt = ConfirmPrompt::new(&cmd_names);
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
} }
} }
@ -798,7 +798,7 @@ impl AppState {
.map(|node| node.name.as_str()) .map(|node| node.name.as_str())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let prompt = ConfirmPrompt::new(cmd_names); let prompt = ConfirmPrompt::new(&cmd_names);
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
} }
} }

View File

@ -14,70 +14,95 @@ pub enum Theme {
} }
impl 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { pub fn multi_select_icon(&self) -> &'static str {
self.get_icon_variant("", "*") match self {
Theme::Default => "",
Theme::Compatible => "*",
}
} }
pub fn success_color(&self) -> Color { 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 { 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 { 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 { 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 { pub fn unfocused_color(&self) -> Color {
self.get_color_variant(Color::Gray, Color::Gray) match self {
Theme::Default => Color::Gray,
Theme::Compatible => Color::Gray,
}
} }
} }