mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-21 12:59:41 +00:00
Compare commits
4 Commits
e1df2bee62
...
c16bfd7a12
Author | SHA1 | Date | |
---|---|---|---|
|
c16bfd7a12 | ||
|
2d1f5dbc80 | ||
|
35159b8393 | ||
|
7e96651bbe |
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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..)
|
||||
|
|
|
@ -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<char> to handle multi-byte characters like emojis
|
||||
search_input: Vec<char>,
|
||||
in_search_mode: bool,
|
||||
input_position: usize,
|
||||
items: Vec<ListEntry>,
|
||||
// No complex string manipulation is done with completion_preview so we can use String unlike search_input
|
||||
completion_preview: Option<String>,
|
||||
}
|
||||
|
||||
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::<String>().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::<String>().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::<String>();
|
||||
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<regex::Regex, regex::Error> {
|
||||
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::<String>().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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user