Compare commits

...

2 Commits

Author SHA1 Message Date
Jeevitha Kannan K S
0b4f33c761
Implement case insensitive, fix word disappearing bug
Use regex for case insesitive finding, implement String instead of char<Vec>, fix word disappearing by recalculating the render x for preview text
2024-11-10 21:48:05 +05:30
Jeevitha Kannan K S
190c26cd76
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
2024-11-10 21:47:55 +05:30
5 changed files with 93 additions and 85 deletions

2
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

@ -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::<String>();
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::<Text>();
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"]),
]),
)

View File

@ -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<char>,
search_input: String,
in_search_mode: bool,
input_position: usize,
items: Vec<ListEntry>,
@ -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::<String>().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(&regex::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::<String>().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::<String>();
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<regex::Regex, regex::Error> {
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
}
}
}

View File

@ -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::<Vec<_>>();
let prompt = ConfirmPrompt::new(&cmd_names[..]);
let prompt = ConfirmPrompt::new(cmd_names);
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
}
}