From 1ff69c78460d386d7ca7bb7755c9697be5dd3928 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Wed, 6 Nov 2024 20:23:12 +0530 Subject: [PATCH 01/18] fix: getting locked out when running script --- tui/src/state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tui/src/state.rs b/tui/src/state.rs index 3670c779d..35a5b2962 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -186,7 +186,9 @@ impl AppState { pub fn draw(&mut self, frame: &mut Frame) { let terminal_size = frame.area(); - if terminal_size.width < MIN_WIDTH || terminal_size.height < MIN_HEIGHT { + if !matches!(self.focus, Focus::FloatingWindow(_)) && terminal_size.width < MIN_WIDTH + || terminal_size.height < MIN_HEIGHT + { let warning = Paragraph::new(format!( "Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}", terminal_size.width, 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 02/18] 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 96ab06ca9..6e130e5a3 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 f44e89a1a..2fdce073f 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 993684b0b..ab9394a52 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 c307b8542..c534064bc 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 82c265c89..2ab4b99d1 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 7a9f40677..f5d2b7cdc 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 779a0b3c8..368ed6cae 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 5ee340798..7b31deca2 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 d87e87ee1..9e10ce709 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 03/18] Remove redundant code in themes Removed redundant match statement with a function --- tui/src/theme.rs | 75 ++++++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 50 deletions(-) diff --git a/tui/src/theme.rs b/tui/src/theme.rs index 9e10ce709..3897b18b8 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, } } - pub fn cmd_color(&self) -> Color { + fn get_icon_variant(&self, default: &'static str, compatible: &'static str) -> &'static str { match self { - Theme::Default => Color::Rgb(204, 224, 208), - Theme::Compatible => Color::LightGreen, + 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 { + 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 04/18] 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 6e130e5a3..64b160e0d 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 7b31deca2..ed320a2dc 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 05/18] 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 86b9ef04c..4a05c1b0f 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 4051d3b34..efc462938 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 2fdce073f..99d9bcf5c 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 06/18] Revert "Remove redundant code in themes" This reverts commit 3b7e859af80bfa1f453928a74c47efd897e26934. --- tui/src/theme.rs | 75 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/tui/src/theme.rs b/tui/src/theme.rs index 3897b18b8..9e10ce709 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 { + pub fn dir_color(&self) -> Color { match self { - Theme::Default => default, - Theme::Compatible => compatible, + Theme::Default => Color::Blue, + Theme::Compatible => Color::Blue, } } - fn get_icon_variant(&self, default: &'static str, compatible: &'static str) -> &'static str { + pub fn cmd_color(&self) -> Color { match self { - Theme::Default => default, - Theme::Compatible => compatible, + Theme::Default => Color::Rgb(204, 224, 208), + Theme::Compatible => Color::LightGreen, } } - pub fn dir_color(&self) -> Color { - self.get_color_variant(Color::Blue, Color::Blue) - } - - pub fn cmd_color(&self) -> Color { - self.get_color_variant(Color::Rgb(204, 224, 208), 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 07/18] 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 64b160e0d..1993c3f5f 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 ed320a2dc..396123a9b 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 08/18] 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 4a05c1b0f..86b9ef04c 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 efc462938..9510cab69 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 99d9bcf5c..60a642239 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 } } From 10352c62549f4a4e492759c360b8796a7b98a1f8 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Mon, 11 Nov 2024 14:51:37 +0530 Subject: [PATCH 09/18] Replace ansi and text wrapping code with ratatui Replaced ansi related code for tree sitter highlight with direct ratatui::text. Cache the processed text in appstate to remove processing of text for every frame render.Create paragraph instead of list so that scroll and wrapping can be done without external crates. Add caps keys for handle_key_event. --- Cargo.lock | 73 -------- tui/Cargo.toml | 4 - tui/src/floating_text.rs | 350 +++++++++++++++------------------------ tui/src/state.rs | 8 +- 4 files changed, 139 insertions(+), 296 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86b9ef04c..36c3d9234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,19 +29,6 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "ansi-to-tui" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" -dependencies = [ - "nom", - "ratatui", - "simdutf8", - "smallvec", - "thiserror", -] - [[package]] name = "anstream" version = "0.6.15" @@ -441,8 +428,6 @@ dependencies = [ name = "linutil_tui" version = "24.9.28" dependencies = [ - "ansi-to-tui", - "anstyle", "clap", "linutil_core", "oneshot", @@ -450,13 +435,11 @@ dependencies = [ "rand", "ratatui", "temp-dir", - "textwrap", "time", "tree-sitter-bash", "tree-sitter-highlight", "tui-term", "unicode-width 0.2.0", - "zips", ] [[package]] @@ -505,12 +488,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "mio" version = "1.0.2" @@ -538,16 +515,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -907,24 +874,12 @@ dependencies = [ "libc", ] -[[package]] -name = "simdutf8" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" - [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - [[package]] name = "static_assertions" version = "1.1.0" @@ -991,17 +946,6 @@ dependencies = [ "libc", ] -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width 0.1.14", -] - [[package]] name = "thiserror" version = "1.0.64" @@ -1147,12 +1091,6 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -1390,14 +1328,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zips" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba09194204fda6b1e206faf9096a3c0658ddf7606560f6edce112da3fcc9b111" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 9510cab69..e01af32fc 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -25,10 +25,6 @@ rand = { version = "0.8.5", optional = true } linutil_core = { path = "../core", version = "24.9.28" } tree-sitter-highlight = "0.24.3" tree-sitter-bash = "0.23.1" -textwrap = "0.16.1" -anstyle = "1.0.8" -ansi-to-tui = "7.0.0" -zips = "0.1.7" unicode-width = "0.2.0" [[bin]] diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs index c534064bc..b583df9c3 100644 --- a/tui/src/floating_text.rs +++ b/tui/src/floating_text.rs @@ -1,43 +1,23 @@ 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, - style::{Style, Stylize}, - text::Line, - widgets::{Block, Borders, Clear, List}, + style::{Color, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Clear, Paragraph, Wrap}, Frame, }; -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}; -use zips::zip_result; - -pub struct FloatingText { - pub src: String, - wrapped_lines: Vec, - max_line_width: usize, - v_scroll: usize, - h_scroll: usize, - mode_title: String, - wrap_words: bool, - frame_height: usize, -} macro_rules! style { ($r:literal, $g:literal, $b:literal) => {{ - use anstyle::{Color, RgbColor, Style}; - Style::new().fg_color(Some(Color::Rgb(RgbColor($r, $g, $b)))) + Style::new().fg(Color::Rgb($r, $g, $b)) }}; } -const SYNTAX_HIGHLIGHT_STYLES: [(&str, anstyle::Style); 8] = [ +const SYNTAX_HIGHLIGHT_STYLES: [(&str, Style); 8] = [ ("function", style!(220, 220, 170)), // yellow ("string", style!(206, 145, 120)), // brown ("property", style!(156, 220, 254)), // light blue @@ -48,243 +28,185 @@ const SYNTAX_HIGHLIGHT_STYLES: [(&str, anstyle::Style); 8] = [ ("number", style!(181, 206, 168)), // light green ]; -fn get_highlighted_string(s: &str) -> Option { - let mut hl_conf = hl::HighlightConfiguration::new( - hl_bash::LANGUAGE.into(), - "bash", - hl_bash::HIGHLIGHT_QUERY, - "", - "", - ) - .ok()?; - - let matched_tokens = &SYNTAX_HIGHLIGHT_STYLES - .iter() - .map(|hl| hl.0) - .collect::>(); +pub struct FloatingText<'a> { + // Width, Height + inner_area_size: (usize, usize), + mode_title: String, + // Cache the text to avoid reprocessing it every frame + processed_text: Text<'a>, + // Vertical, Horizontal + scroll: (u16, u16), + wrap_words: bool, +} - hl_conf.configure(matched_tokens); +impl<'a> FloatingText<'a> { + pub fn new(text: String, title: &str, wrap_words: bool) -> Self { + let processed_text = Text::from(text); - let mut hl = hl::Highlighter::new(); + Self { + inner_area_size: (0, 0), + mode_title: title.to_string(), + processed_text, + scroll: (0, 0), + wrap_words, + } + } - let mut style_stack = vec![anstyle::Style::new()]; - let src = s.as_bytes(); + pub fn from_command(command: &Command, title: &str, wrap_words: bool) -> Self { + let src = match command { + Command::Raw(cmd) => Some(cmd.clone()), + Command::LocalFile { file, .. } => std::fs::read_to_string(file) + .map_err(|_| format!("File not found: {:?}", file)) + .ok(), + Command::None => None, + } + .unwrap(); - let events = hl.highlight(&hl_conf, src, None, |_| None).ok()?; + let processed_text = Self::get_highlighted_string(&src).unwrap_or_else(|| Text::from(src)); - let mut buf = Cursor::new(vec![]); + Self { + inner_area_size: (0, 0), + mode_title: title.to_string(), + processed_text, + scroll: (0, 0), + wrap_words, + } + } - for event in events { - match event.unwrap() { - HighlightEvent::HighlightStart(h) => { - style_stack.push(SYNTAX_HIGHLIGHT_STYLES.get(h.0)?.1); - } + fn get_highlighted_string(s: &str) -> Option> { + let matched_tokens = SYNTAX_HIGHLIGHT_STYLES + .iter() + .map(|hl| hl.0) + .collect::>(); - HighlightEvent::HighlightEnd => { - style_stack.pop(); - } + let mut lines = Vec::with_capacity(s.lines().count()); + let mut current_line = Vec::new(); + let mut style_stack = vec![Style::default()]; - HighlightEvent::Source { start, end } => { - let style = style_stack.last()?; - zip_result!( - write!(&mut buf, "{}", style), - buf.write_all(&src[start..end]), - write!(&mut buf, "{style:#}"), - )?; - } - } - } + let mut hl_conf = hl::HighlightConfiguration::new( + hl_bash::LANGUAGE.into(), + "bash", + hl_bash::HIGHLIGHT_QUERY, + "", + "", + ) + .ok()?; - let mut output = String::new(); + hl_conf.configure(&matched_tokens); - zip_result!( - buf.seek(SeekFrom::Start(0)), - buf.read_to_string(&mut output), - )?; + let mut hl = hl::Highlighter::new(); + let events = hl.highlight(&hl_conf, s.as_bytes(), None, |_| None).ok()?; - Some(output) -} + for event in events { + match event.ok()? { + HighlightEvent::HighlightStart(h) => { + style_stack.push(SYNTAX_HIGHLIGHT_STYLES.get(h.0)?.1); + } -#[inline] -fn get_lines(s: &str) -> Vec<&str> { - s.lines().collect::>() -} + HighlightEvent::HighlightEnd => { + style_stack.pop(); + } -#[inline] -fn get_lines_owned(s: &str) -> Vec { - get_lines(s).iter().map(|s| s.to_string()).collect() -} + HighlightEvent::Source { start, end } => { + let style = *style_stack.last()?; + let content = &s[start..end]; + + for part in content.split_inclusive('\n') { + if let Some(stripped) = part.strip_suffix('\n') { + // Push the text that is before '\n' and then start a new line + // After a new line clear the current line to start a new one + current_line.push(Span::styled(stripped.to_owned(), style)); + lines.push(Line::from(current_line.to_owned())); + current_line.clear(); + } else { + current_line.push(Span::styled(part.to_owned(), style)); + } + } + } + } + } -impl FloatingText { - pub fn new(text: String, title: &str, wrap_words: bool) -> Self { - let max_line_width = 80; - let wrapped_lines = if wrap_words { - wrap(&text, max_line_width) - .into_iter() - .map(|cow| cow.into_owned()) - .collect() - } else { - get_lines_owned(&text) - }; + // Makes sure last line of the file is pushed + // If no newline at the end of the file we need to push the last line + if !current_line.is_empty() { + lines.push(Line::from(current_line)); + } - Self { - src: text, - wrapped_lines, - mode_title: title.to_string(), - max_line_width, - v_scroll: 0, - h_scroll: 0, - wrap_words, - frame_height: 0, + if lines.is_empty() { + return None; } - } - pub fn from_command(command: &Command, title: String) -> Option { - let src = match command { - Command::Raw(cmd) => Some(cmd.clone()), - Command::LocalFile { file, .. } => std::fs::read_to_string(file) - .map_err(|_| format!("File not found: {:?}", file)) - .ok(), - Command::None => None, - }?; - - let max_line_width = 80; - let wrapped_lines = get_lines_owned(&get_highlighted_string(&src)?); - - Some(Self { - src, - wrapped_lines, - mode_title: title, - max_line_width, - h_scroll: 0, - v_scroll: 0, - wrap_words: false, - frame_height: 0, - }) + Some(Text::from(lines)) } fn scroll_down(&mut self) { - let visible_lines = self.frame_height.saturating_sub(2); - if self.v_scroll + visible_lines < self.wrapped_lines.len() { - self.v_scroll += 1; - } + let max_scroll = self + .processed_text + .lines + .len() + .saturating_sub(self.inner_area_size.1) as u16; + self.scroll.0 = (self.scroll.0 + 1).min(max_scroll); } fn scroll_up(&mut self) { - if self.v_scroll > 0 { - self.v_scroll -= 1; - } + self.scroll.0 = self.scroll.0.saturating_sub(1); } fn scroll_left(&mut self) { - if self.h_scroll > 0 { - self.h_scroll -= 1; - } + self.scroll.1 = self.scroll.1.saturating_sub(1); } fn scroll_right(&mut self) { - if self.h_scroll + 1 < self.max_line_width { - self.h_scroll += 1; - } - } - - fn update_wrapping(&mut self, width: usize) { - if self.max_line_width != width { - self.max_line_width = width; - self.wrapped_lines = if self.wrap_words { - wrap(&self.src, width) - .into_iter() - .map(|cow| cow.into_owned()) - .collect() - } else { - get_lines_owned(&get_highlighted_string(&self.src).unwrap_or(self.src.clone())) - }; - } + let visible_length = self.inner_area_size.0.saturating_sub(1); + let max_scroll = if self.wrap_words { + 0 + } else { + self.processed_text + .lines + .iter() + .map(|line| line.width()) + .max() + .unwrap_or(0) + .saturating_sub(visible_length) as u16 + }; + self.scroll.1 = (self.scroll.1 + 1).min(max_scroll); } } -impl FloatContent for FloatingText { +impl<'a> FloatContent for FloatingText<'a> { 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 let block = Block::default() .borders(Borders::ALL) .border_set(ratatui::symbols::border::ROUNDED) - .title(self.mode_title.clone()) + .title(self.mode_title.as_str()) .title_alignment(ratatui::layout::Alignment::Center) .title_style(Style::default().reversed()) .style(Style::default()); - frame.render_widget(Clear, area); - - frame.render_widget(block.clone(), area); - - // Calculate the inner area to ensure text is not drawn over the border let inner_area = block.inner(area); - let Rect { width, height, .. } = inner_area; + self.inner_area_size = (inner_area.width as usize, inner_area.height as usize); - self.update_wrapping(width as usize); - - let lines = self - .wrapped_lines - .iter() - .skip(self.v_scroll) - .take(height as usize) - .flat_map(|l| { - if self.wrap_words { - vec![Line::raw(l.clone())] - } else { - l.into_text().unwrap().lines - } - }) - .map(|line| { - let mut skipped = 0; - let mut spans = line - .into_iter() - .skip_while(|span| { - let skip = (skipped + span.content.len()) <= self.h_scroll; - if skip { - skipped += span.content.len(); - true - } else { - false - } - }) - .collect::>(); - - if spans.is_empty() { - Line::raw(Cow::Owned(String::new())) - } else { - if skipped < self.h_scroll { - let to_split = spans.pop_front().unwrap(); - let new_content = to_split.content.clone().into_owned() - [self.h_scroll - skipped..] - .to_owned(); - spans.push_front(to_split.content(Cow::Owned(new_content))); - } - - Line::from(Vec::from(spans)) - } - }) - .collect::>(); + frame.render_widget(Clear, area); + frame.render_widget(block, area); - // Create list widget - let list = List::new(lines) - .block(Block::default()) - .highlight_style(Style::default().reversed()); + let paragraph = if self.wrap_words { + Paragraph::new(self.processed_text.clone()) + .scroll(self.scroll) + .wrap(Wrap { trim: false }) + } else { + Paragraph::new(self.processed_text.clone()).scroll(self.scroll) + }; - // Render the list inside the bordered area - frame.render_widget(list, inner_area); + frame.render_widget(paragraph, inner_area); } fn handle_key_event(&mut self, key: &KeyEvent) -> bool { - use KeyCode::*; + use KeyCode::{Char, Down, Left, Right, Up}; match key.code { - Down | Char('j') => self.scroll_down(), - Up | Char('k') => self.scroll_up(), - Left | Char('h') => self.scroll_left(), - Right | Char('l') => self.scroll_right(), + Down | Char('j') | Char('J') => self.scroll_down(), + Up | Char('k') | Char('K') => self.scroll_up(), + Left | Char('h') | Char('H') => self.scroll_left(), + Right | Char('l') | Char('L') => self.scroll_right(), _ => {} } false diff --git a/tui/src/state.rs b/tui/src/state.rs index 396123a9b..692ef2733 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -748,11 +748,9 @@ impl AppState { fn enable_preview(&mut self) { if let Some(list_node) = self.get_selected_node() { - let mut preview_title = "[Preview] - ".to_string(); - preview_title.push_str(list_node.name.as_str()); - if let Some(preview) = FloatingText::from_command(&list_node.command, preview_title) { - self.spawn_float(preview, 80, 80); - } + let preview_title = format!("[Preview] - {}", list_node.name.as_str()); + let preview = FloatingText::from_command(&list_node.command, &preview_title, false); + self.spawn_float(preview, 80, 80); } } From 2f1f5fd5438a0f5a01deafa3cd46db689670b0eb Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Mon, 11 Nov 2024 22:23:28 +0530 Subject: [PATCH 10/18] Fix conflicts --- Cargo.lock | 28 ---------------------------- tui/Cargo.toml | 4 ---- tui/src/root.rs | 2 +- 3 files changed, 1 insertion(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60f37ffda..e87e2df9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,18 +29,6 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "ansi-to-tui" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" -dependencies = [ - "nom", - "ratatui", - "smallvec", - "thiserror", -] - [[package]] name = "anstyle" version = "1.0.8" @@ -495,16 +483,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -930,12 +908,6 @@ dependencies = [ "libc", ] -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" - [[package]] name = "thiserror" version = "1.0.64" diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 5094b8154..105a3b568 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -26,10 +26,6 @@ rand = { version = "0.8.5", optional = true } linutil_core = { version = "24.9.28", path = "../core" } tree-sitter-highlight = "0.24.3" tree-sitter-bash = "0.23.1" -textwrap = { version = "0.16.1", default-features = false } -anstyle = { version = "1.0.8", default-features = false } -ansi-to-tui = { version = "7.0.0", default-features = false } -zips = "0.1.7" nix = { version = "0.29.0", features = [ "user" ] } [[bin]] diff --git a/tui/src/root.rs b/tui/src/root.rs index 1b02b9386..46432d068 100644 --- a/tui/src/root.rs +++ b/tui/src/root.rs @@ -8,7 +8,7 @@ This means you have full system access and commands can potentially damage your Please proceed with caution and make sure you understand what each script does before executing it."; #[cfg(unix)] -pub fn check_root_status() -> Option { +pub fn check_root_status<'a>() -> Option> { (Uid::effective().is_root()).then_some(FloatingText::new( ROOT_WARNING.into(), "Root User Warning", From 02b06129e16dd5b1db8a48d24ccd8bb7ab2d0ef0 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Mon, 11 Nov 2024 23:40:41 +0530 Subject: [PATCH 11/18] Reference instead of borrowing commands, refact mut variables Reference instead of borrowing commands from state, Refactor draw function variables to immutable, calculate innersize from block instead of manual definition --- tui/src/running_command.rs | 61 ++++++++++++++------------------------ tui/src/state.rs | 8 ++--- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs index e675c7b4e..9a9ea7a95 100644 --- a/tui/src/running_command.rs +++ b/tui/src/running_command.rs @@ -8,7 +8,7 @@ use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}, layout::{Rect, Size}, style::{Style, Stylize}, - text::{Line, Span}, + text::Line, widgets::{Block, Borders}, Frame, }; @@ -19,7 +19,7 @@ use std::{ }; use time::{macros::format_description, OffsetDateTime}; use tui_term::{ - vt100::{self, Screen}, + vt100::{Parser, Screen}, widget::PseudoTerminal, }; @@ -44,12 +44,6 @@ pub struct RunningCommand { impl FloatContent for RunningCommand { 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 - height: area.height - 2, - }; - // Define the block for the terminal display let block = if !self.is_finished() { // Display a block indicating the command is running @@ -61,42 +55,33 @@ impl FloatContent for RunningCommand { .title_bottom(Line::from("Press Ctrl-C to KILL the command")) } else { // Display a block with the command's exit status - let mut title_line = if self.get_exit_status().success() { - Line::from( - Span::default() - .content("SUCCESS!") - .style(Style::default().fg(theme.success_color()).reversed()), + let title_line = if self.get_exit_status().success() { + Line::styled( + "SUCCESS! Press to close this window", + Style::default().fg(theme.success_color()).reversed(), ) } else { - Line::from( - Span::default() - .content("FAILED!") - .style(Style::default().fg(theme.fail_color()).reversed()), + Line::styled( + "FAILED! Press to close this window", + Style::default().fg(theme.fail_color()).reversed(), ) }; - title_line.push_span( - Span::default() - .content(" Press to close this window ") - .style(Style::default()), - ); + let log_path = if let Some(log_path) = &self.log_path { + Line::from(format!(" Log saved: {} ", log_path)).centered() + } else { + Line::from(" Press 'l' to save command log ").centered() + }; - let mut block = Block::default() + Block::default() .borders(Borders::ALL) .border_set(ratatui::symbols::border::ROUNDED) - .title_top(title_line.centered()); - - if let Some(log_path) = &self.log_path { - block = - block.title_bottom(Line::from(format!(" Log saved: {} ", log_path)).centered()); - } else { - block = - block.title_bottom(Line::from(" Press 'l' to save command log ").centered()); - } - - block + .title_top(title_line.centered()) + .title_bottom(log_path) }; + // Calculate the inner size of the terminal area, considering borders + let inner_size = block.inner(area).as_size(); // Process the buffer and create the pseudo-terminal widget let screen = self.screen(inner_size); let pseudo_term = PseudoTerminal::new(&screen).block(block); @@ -180,7 +165,7 @@ impl FloatContent for RunningCommand { } impl RunningCommand { - pub fn new(commands: Vec) -> Self { + pub fn new(commands: &[&Command]) -> Self { let pty_system = NativePtySystem::default(); // Build the command based on the provided Command enum variant @@ -200,10 +185,10 @@ impl RunningCommand { if let Some(parent_directory) = file.parent() { script.push_str(&format!("cd {}\n", parent_directory.display())); } - script.push_str(&executable); + script.push_str(executable); for arg in args { script.push(' '); - script.push_str(&arg); + script.push_str(arg); } script.push('\n'); // Ensures that each command is properly separated for execution preventing directory errors } @@ -286,7 +271,7 @@ impl RunningCommand { // Process the buffer with a parser with the current screen size // We don't actually need to create a new parser every time, but it is so much easier this // way, and doesn't cost that much - let mut parser = vt100::Parser::new(size.height, size.width, 1000); + let mut parser = Parser::new(size.height, size.width, 1000); let mutex = self.buffer.lock(); let buffer = mutex.as_ref().unwrap(); parser.process(buffer); diff --git a/tui/src/state.rs b/tui/src/state.rs index d767d81ba..485b13fcd 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -9,7 +9,7 @@ use crate::{ theme::Theme, }; -use linutil_core::{ego_tree::NodeId, Config, ListNode, TabList}; +use linutil_core::{ego_tree::NodeId, Command, Config, ListNode, TabList}; #[cfg(feature = "tips")] use rand::Rng; use ratatui::{ @@ -877,13 +877,13 @@ impl AppState { } fn handle_confirm_command(&mut self) { - let commands = self + let commands: Vec<&Command> = self .selected_commands .iter() - .map(|node| node.command.clone()) + .map(|node| &node.command) .collect(); - let command = RunningCommand::new(commands); + let command = RunningCommand::new(&commands); self.spawn_float(command, 80, 80); self.selected_commands.clear(); } From df81642c9eab661257340838a5e5f2ba175c6307 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Tue, 12 Nov 2024 21:07:43 +0530 Subject: [PATCH 12/18] Update tui/src/filter.rs Co-authored-by: Liam <33645555+lj3954@users.noreply.github.com> --- tui/src/filter.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tui/src/filter.rs b/tui/src/filter.rs index 60a642239..bc62bfa79 100644 --- a/tui/src/filter.rs +++ b/tui/src/filter.rs @@ -93,12 +93,9 @@ impl Filter { } else { 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 - } + let item_name_lower = item.node.name.to_lowercase(); + (item_name_lower.starts_with(&input)) + .then_some(item_name_lower[input.len()..].to_string()) }) } } From 79aae9eb24da0a3977d725aa5c61296ed83c4d01 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Wed, 13 Nov 2024 00:37:26 +0530 Subject: [PATCH 13/18] Rendering optimizations and function refactors Handle `find_command` inside state itself -> `get_command_by_name`. Move tips to a seperate file for modularity. Pass the whole args to state instead of seperate args. Use const for float and confirmation prompt float sizes. Add the `longest_tab_length` to appstate struct so that it will not be calculated for each frame render use static str instead String for tips. Use function for spawning confirmprompt. Merge command list and task items list rendering a single widget instead of two. Remove redundant keys in handle_key. Optimize scrolling logic. Rename `toggle_task_list_guide` -> `enable_task_list_guide` --- core/src/lib.rs | 13 -- tui/src/main.rs | 13 +- tui/src/state.rs | 460 +++++++++++++++++++---------------------------- tui/src/tips.rs | 14 ++ 4 files changed, 205 insertions(+), 295 deletions(-) create mode 100644 tui/src/tips.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index 986d9ac16..53fe988b4 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -36,16 +36,3 @@ pub struct ListNode { pub task_list: String, pub multi_select: bool, } - -impl Tab { - pub fn find_command(&self, name: &str) -> Option> { - self.tree.root().descendants().find_map(|node| { - let value = node.value(); - if value.name == name && !node.has_children() { - Some(value.clone()) - } else { - None - } - }) - } -} diff --git a/tui/src/main.rs b/tui/src/main.rs index 536b10c0b..1f03fc1bc 100644 --- a/tui/src/main.rs +++ b/tui/src/main.rs @@ -8,6 +8,9 @@ mod running_command; pub mod state; mod theme; +#[cfg(feature = "tips")] +mod tips; + use crate::theme::Theme; use clap::Parser; use ratatui::{ @@ -29,7 +32,7 @@ use std::{ // Linux utility toolbox #[derive(Debug, Parser)] -struct Args { +pub struct Args { #[arg(short, long, help = "Path to the configuration file")] config: Option, #[arg(short, long, value_enum)] @@ -53,13 +56,7 @@ struct Args { fn main() -> io::Result<()> { let args = Args::parse(); - let mut state = AppState::new( - args.config, - args.theme, - args.override_validation, - args.size_bypass, - args.skip_confirmation, - ); + let mut state = AppState::new(args); stdout().execute(EnterAlternateScreen)?; stdout().execute(EnableMouseCapture)?; diff --git a/tui/src/state.rs b/tui/src/state.rs index 485b13fcd..c80eb763c 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -7,24 +7,24 @@ use crate::{ root::check_root_status, running_command::RunningCommand, theme::Theme, + Args, }; - use linutil_core::{ego_tree::NodeId, Command, Config, ListNode, TabList}; -#[cfg(feature = "tips")] -use rand::Rng; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind}, layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect}, style::{Style, Stylize}, + symbols::border, text::{Line, Span, Text}, - widgets::{Block, Borders, List, ListState, Paragraph}, + widgets::{Block, List, ListState, Paragraph}, Frame, }; -use std::path::PathBuf; use std::rc::Rc; const MIN_WIDTH: u16 = 100; const MIN_HEIGHT: u16 = 25; +const FLOAT_SIZE: u16 = 80; +const CONFIRM_PROMPT_FLOAT_SIZE: u16 = 40; const TITLE: &str = concat!(" Linux Toolbox - ", env!("CARGO_PKG_VERSION"), " "); const ACTIONS_GUIDE: &str = "List of important tasks performed by commands' names: @@ -47,11 +47,12 @@ pub struct AppState { /// Selected theme theme: Theme, /// Currently focused area - pub focus: Focus, + focus: Focus, /// List of tabs tabs: TabList, /// Current tab current_tab: ListState, + longest_tab_display_len: u16, /// This stack keeps track of our "current directory". You can think of it as `pwd`. but not /// just the current directory, all paths that took us here, so we can "cd .." visit_stack: Vec<(NodeId, usize)>, @@ -63,7 +64,7 @@ pub struct AppState { selected_commands: Vec>, drawable: bool, #[cfg(feature = "tips")] - tip: String, + tip: &'static str, size_bypass: bool, skip_confirmation: bool, } @@ -82,7 +83,7 @@ pub struct ListEntry { pub has_children: bool, } -pub struct Areas { +struct Areas { tab_list: Rect, list: Rect, } @@ -95,24 +96,27 @@ enum SelectedItem { } impl AppState { - pub fn new( - config_path: Option, - theme: Theme, - override_validation: bool, - size_bypass: bool, - skip_confirmation: bool, - ) -> Self { - let tabs = linutil_core::get_tabs(!override_validation); + pub fn new(args: Args) -> Self { + let tabs = linutil_core::get_tabs(!args.override_validation); let root_id = tabs[0].tree.root().id(); - let auto_execute_commands = config_path.map(|path| Config::from_file(&path).auto_execute); + let auto_execute_commands = args + .config + .map(|path| Config::from_file(&path).auto_execute); + + let longest_tab_display_len = tabs + .iter() + .map(|tab| tab.name.len() + args.theme.tab_icon().len()) + .max() + .unwrap_or(22) as u16; // 22 is the length of "Linutil by Chris Titus" title let mut state = Self { areas: None, - theme, + theme: args.theme, focus: Focus::List, tabs, current_tab: ListState::default().with_selected(Some(0)), + longest_tab_display_len, visit_stack: vec![(root_id, 0usize)], selection: ListState::default().with_selected(Some(0)), filter: Filter::new(), @@ -120,14 +124,14 @@ impl AppState { selected_commands: Vec::new(), drawable: false, #[cfg(feature = "tips")] - tip: get_random_tip(), - size_bypass, - skip_confirmation, + tip: crate::tips::get_random_tip(), + size_bypass: args.size_bypass, + skip_confirmation: args.skip_confirmation, }; #[cfg(unix)] if let Some(root_warning) = check_root_status() { - state.spawn_float(root_warning, 60, 40); + state.spawn_float(root_warning, FLOAT_SIZE, FLOAT_SIZE); } state.update_items(); @@ -138,24 +142,41 @@ impl AppState { state } + fn find_command_by_name(&self, name: &str) -> Option> { + self.tabs.iter().find_map(|tab| { + tab.tree.root().descendants().find_map(|node| { + let node_value = node.value(); + (node_value.name == name && !node.has_children()).then_some(node_value.clone()) + }) + }) + } + fn handle_initial_auto_execute(&mut self, auto_execute_commands: &[String]) { self.selected_commands = auto_execute_commands .iter() - .filter_map(|name| self.tabs.iter().find_map(|tab| tab.find_command(name))) + .filter_map(|name| self.find_command_by_name(name)) .collect(); if !self.selected_commands.is_empty() { - let cmd_names: Vec<_> = self - .selected_commands - .iter() - .map(|node| node.name.as_str()) - .collect(); - - let prompt = ConfirmPrompt::new(&cmd_names); - self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); + self.spawn_confirmprompt(); } } + fn spawn_confirmprompt(&mut self) { + let cmd_names: Vec<_> = self + .selected_commands + .iter() + .map(|node| node.name.as_str()) + .collect(); + + let prompt = ConfirmPrompt::new(&cmd_names); + self.focus = Focus::ConfirmationPrompt(Float::new( + Box::new(prompt), + CONFIRM_PROMPT_FLOAT_SIZE, + CONFIRM_PROMPT_FLOAT_SIZE, + )); + } + fn get_list_item_shortcut(&self) -> Box<[Shortcut]> { if self.selected_item_is_dir() { Box::new([Shortcut::new("Go to selected dir", ["l", "Right", "Enter"])]) @@ -229,16 +250,18 @@ impl AppState { } } - pub fn draw(&mut self, frame: &mut Frame) { - let terminal_size = frame.area(); + fn is_terminal_drawable(&mut self, terminal_size: Rect) -> bool { + !self.size_bypass && (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH) + } - if !self.size_bypass - && (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH) - { + pub fn draw(&mut self, frame: &mut Frame) { + let area = frame.area(); + self.drawable = !self.is_terminal_drawable(area); + if !self.drawable { let warning = Paragraph::new(format!( "Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}", - terminal_size.width, - terminal_size.height, + area.width, + area.height, MIN_WIDTH, MIN_HEIGHT, )) @@ -253,80 +276,53 @@ impl AppState { Constraint::Length(5), Constraint::Fill(1), ]) - .split(terminal_size); + .split(area); - self.drawable = false; return frame.render_widget(warning, centered_layout[1]); - } else { - self.drawable = true; - } - - let label_block = Block::default() - .borders(Borders::ALL) - .border_set(ratatui::symbols::border::ROUNDED) - .border_set(ratatui::symbols::border::Set { - top_left: " ", - top_right: " ", - bottom_left: " ", - bottom_right: " ", - vertical_left: " ", - vertical_right: " ", - horizontal_top: "*", - horizontal_bottom: "*", - }); - let str1 = "Linutil "; - let str2 = "by Chris Titus"; + } + + let label_block = Block::bordered().border_set(border::Set { + top_left: " ", + top_right: " ", + bottom_left: " ", + bottom_right: " ", + vertical_left: " ", + vertical_right: " ", + horizontal_top: "*", + horizontal_bottom: "*", + }); + let label = Paragraph::new(Line::from(vec![ - Span::styled(str1, Style::default().bold()), - Span::styled(str2, Style::default().italic()), + Span::styled("Linutil ", Style::default().bold()), + Span::styled("by Chris Titus", Style::default().italic()), ])) .block(label_block) - .alignment(Alignment::Center); - - let longest_tab_display_len = self - .tabs - .iter() - .map(|tab| tab.name.len() + self.theme.tab_icon().len()) - .max() - .unwrap_or(0) - .max(str1.len() + str2.len()); + .centered(); let (keybind_scope, shortcuts) = self.get_keybinds(); - let keybind_render_width = terminal_size.width - 2; - - let keybinds_block = Block::default() + let keybinds_block = Block::bordered() .title(format!(" {} ", keybind_scope)) - .borders(Borders::ALL) - .border_set(ratatui::symbols::border::ROUNDED); + .border_set(border::ROUNDED); + let keybind_render_width = keybinds_block.inner(area).width; let keybinds = create_shortcut_list(shortcuts, keybind_render_width); - let n_lines = keybinds.len() as u16; - + let keybind_len = keybinds.len() as u16; let keybind_para = Paragraph::new(Text::from_iter(keybinds)).block(keybinds_block); - let vertical = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(0), - Constraint::Max(n_lines as u16 + 2), - ]) - .flex(Flex::Legacy) - .margin(0) - .split(frame.area()); - - let horizontal = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Min(longest_tab_display_len as u16 + 5), - Constraint::Percentage(100), - ]) - .split(vertical[0]); + let vertical = + Layout::vertical([Constraint::Percentage(0), Constraint::Max(keybind_len + 2)]) + .flex(Flex::Legacy) + .split(area); + + let horizontal = Layout::horizontal([ + Constraint::Min(self.longest_tab_display_len + 5), + Constraint::Percentage(100), + ]) + .split(vertical[0]); - let left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(1)]) - .split(horizontal[0]); + let left_chunks = + Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[0]); frame.render_widget(label, left_chunks[0]); self.areas = Some(Areas { @@ -346,36 +342,23 @@ impl AppState { Style::new().fg(self.theme.tab_color()) }; - let list = List::new(tabs) - .block( - Block::default() - .borders(Borders::ALL) - .border_set(ratatui::symbols::border::ROUNDED), - ) + let tab_list = List::new(tabs) + .block(Block::bordered().border_set(border::ROUNDED)) .highlight_style(tab_hl_style) .highlight_symbol(self.theme.tab_icon()); - frame.render_stateful_widget(list, left_chunks[1], &mut self.current_tab); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) - .split(horizontal[1]); + frame.render_stateful_widget(tab_list, left_chunks[1], &mut self.current_tab); - let list_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(70), Constraint::Percentage(30)].as_ref()) - .split(chunks[1]); + let chunks = + Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[1]); self.filter.draw_searchbar(frame, chunks[0], &self.theme); - let mut items: Vec = Vec::new(); - let mut task_items: Vec = Vec::new(); + let mut items: Vec = Vec::with_capacity(self.filter.item_list().len()); if !self.at_root() { items.push( Line::from(format!("{} ..", self.theme.dir_icon())).style(self.theme.dir_color()), ); - task_items.push(Line::from(" ").style(self.theme.dir_color())); } items.extend(self.filter.item_list().iter().map( @@ -384,60 +367,37 @@ impl AppState { }| { let is_selected = self.selected_commands.contains(node); let (indicator, style) = if is_selected { - (self.theme.multi_select_icon(), Style::default().bold()) + (self.theme.multi_select_icon(), Style::new().bold()) } else { let ms_style = if self.multi_select && !node.multi_select { - Style::default().fg(self.theme.multi_select_disabled_color()) + Style::new().fg(self.theme.multi_select_disabled_color()) } else { Style::new() }; ("", ms_style) }; if *has_children { - Line::from(format!( - "{} {} {}", - self.theme.dir_icon(), - node.name, - indicator - )) - .style(self.theme.dir_color()) + Line::styled( + format!("{} {}", self.theme.dir_icon(), node.name,), + self.theme.dir_color(), + ) .patch_style(style) } else { - Line::from(format!( - "{} {} {}", - self.theme.cmd_icon(), - node.name, - indicator - )) - .style(self.theme.cmd_color()) + let left_content = + format!("{} {} {}", self.theme.cmd_icon(), node.name, indicator); + let right_content = format!("{} ", node.task_list); + let center_space = " ".repeat( + chunks[1].width as usize - left_content.len() - right_content.len(), + ); + Line::styled( + format!("{}{}{}", left_content, center_space, right_content), + self.theme.cmd_color(), + ) .patch_style(style) } }, )); - task_items.extend(self.filter.item_list().iter().map( - |ListEntry { - node, has_children, .. - }| { - let ms_style = if self.multi_select && !node.multi_select { - Style::default().fg(self.theme.multi_select_disabled_color()) - } else { - Style::new() - }; - if *has_children { - Line::from(" ") - .style(self.theme.dir_color()) - .patch_style(ms_style) - } else { - Line::from(format!("{} ", node.task_list)) - .alignment(Alignment::Right) - .style(self.theme.cmd_color()) - .bold() - .patch_style(ms_style) - } - }, - )); - let style = if let Focus::List = self.focus { Style::default().reversed() } else { @@ -451,7 +411,10 @@ impl AppState { }; #[cfg(feature = "tips")] - let bottom_title = Line::from(self.tip.as_str().bold().blue()).right_aligned(); + let bottom_title = Line::from(format!(" {} ", self.tip)) + .bold() + .blue() + .centered(); #[cfg(not(feature = "tips"))] let bottom_title = ""; @@ -461,23 +424,14 @@ impl AppState { let list = List::new(items) .highlight_style(style) .block( - Block::default() - .borders(Borders::ALL & !Borders::RIGHT) - .border_set(ratatui::symbols::border::ROUNDED) + Block::bordered() + .border_set(border::ROUNDED) .title(title) + .title(task_list_title) .title_bottom(bottom_title), ) .scroll_padding(1); - frame.render_stateful_widget(list, list_chunks[0], &mut self.selection); - - let disclaimer_list = List::new(task_items).highlight_style(style).block( - Block::default() - .borders(Borders::ALL & !Borders::LEFT) - .border_set(ratatui::symbols::border::ROUNDED) - .title(task_list_title), - ); - - frame.render_stateful_widget(disclaimer_list, list_chunks[1], &mut self.selection); + frame.render_stateful_widget(list, chunks[1], &mut self.selection); match &mut self.focus { Focus::FloatingWindow(float) => float.draw(frame, chunks[1], &self.theme), @@ -566,26 +520,10 @@ impl AppState { // Handle key only when Tablist or List is focused // Prevents exiting the application even when a command is running // Add keys here which should work on both TabList and List - if matches!(self.focus, Focus::TabList | Focus::List) { - match key.code { - KeyCode::Tab => { - if self.current_tab.selected().unwrap() == self.tabs.len() - 1 { - self.current_tab.select_first(); - } else { - self.current_tab.select_next(); - } - self.refresh_tab(); - } - KeyCode::BackTab => { - if self.current_tab.selected().unwrap() == 0 { - self.current_tab.select(Some(self.tabs.len() - 1)); - } else { - self.current_tab.select_previous(); - } - self.refresh_tab(); - } - _ => {} - } + if matches!(self.focus, Focus::TabList | Focus::List) + && self.handle_tablist_and_list_keys(key) + { + return true; } match &mut self.focus { @@ -626,15 +564,9 @@ impl AppState { Focus::TabList => match key.code { KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.focus = Focus::List, - KeyCode::Char('j') | KeyCode::Down => self.scroll_tab_down(), - KeyCode::Char('k') | KeyCode::Up => self.scroll_tab_up(), - KeyCode::Char('/') => self.enter_search(), - KeyCode::Char('t') => self.theme.next(), - KeyCode::Char('T') => self.theme.prev(), - KeyCode::Char('g') => self.toggle_task_list_guide(), _ => {} }, @@ -645,11 +577,6 @@ impl AppState { KeyCode::Char('d') | KeyCode::Char('D') => self.enable_description(), KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(), KeyCode::Char('h') | KeyCode::Left => self.go_back(), - KeyCode::Char('/') => self.enter_search(), - KeyCode::Char('t') => self.theme.next(), - KeyCode::Char('T') => self.theme.prev(), - KeyCode::Char('g') => self.toggle_task_list_guide(), - KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(), KeyCode::Char(' ') if self.multi_select => self.toggle_selection(), _ => {} }, @@ -659,32 +586,38 @@ impl AppState { true } - fn scroll_down(&mut self) { - let len = self.filter.item_list().len(); - if len == 0 { - return; + fn handle_tablist_and_list_keys(&mut self, key: &KeyEvent) -> bool { + match key.code { + KeyCode::Tab => self.scroll_tab_down(), + KeyCode::BackTab => self.scroll_tab_up(), + KeyCode::Char('/') => self.enter_search(), + KeyCode::Char('g') | KeyCode::Char('G') => self.enable_task_list_guide(), + KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(), + KeyCode::Char('t') => self.theme.next(), + KeyCode::Char('T') => self.theme.prev(), + _ => return false, } - let current = self.selection.selected().unwrap_or(0); - let max_index = if self.at_root() { len - 1 } else { len }; - let next = if current + 1 > max_index { - 0 - } else { - current + 1 - }; + true + } - self.selection.select(Some(next)); + fn scroll_down(&mut self) { + if let Some(selected) = self.selection.selected() { + if selected == self.filter.item_list().len() - 1 { + self.selection.select_first(); + } else { + self.selection.select_next(); + } + } } fn scroll_up(&mut self) { - let len = self.filter.item_list().len(); - if len == 0 { - return; + if let Some(selected) = self.selection.selected() { + if selected == 0 { + self.selection.select_last(); + } else { + self.selection.select_previous(); + } } - let current = self.selection.selected().unwrap_or(0); - let max_index = if self.at_root() { len - 1 } else { len }; - let next = if current == 0 { max_index } else { current - 1 }; - - self.selection.select(Some(next)); } fn toggle_multi_select(&mut self) { @@ -747,11 +680,12 @@ impl AppState { fn get_selected_node(&self) -> Option> { let mut selected_index = self.selection.selected().unwrap_or(0); - if !self.at_root() && selected_index == 0 { - return None; - } if !self.at_root() { - selected_index = selected_index.saturating_sub(1); + if selected_index == 0 { + return None; + } else { + selected_index = selected_index.saturating_sub(1); + } } if let Some(item) = self.filter.item_list().get(selected_index) { @@ -793,12 +727,12 @@ impl AppState { pub fn selected_item_is_dir(&self) -> bool { let mut selected_index = self.selection.selected().unwrap_or(0); - if !self.at_root() && selected_index == 0 { - return false; - } - if !self.at_root() { - selected_index = selected_index.saturating_sub(1); + if selected_index == 0 { + return false; + } else { + selected_index = selected_index.saturating_sub(1); + } } self.filter @@ -822,7 +756,7 @@ impl AppState { if let Some(list_node) = self.get_selected_node() { let preview_title = format!("[Preview] - {}", list_node.name.as_str()); let preview = FloatingText::from_command(&list_node.command, &preview_title, false); - self.spawn_float(preview, 80, 80); + self.spawn_float(preview, FLOAT_SIZE, FLOAT_SIZE); } } @@ -831,11 +765,19 @@ impl AppState { if !command_description.is_empty() { let description = FloatingText::new(command_description, "Command Description", true); - self.spawn_float(description, 80, 80); + self.spawn_float(description, FLOAT_SIZE, FLOAT_SIZE); } } } + fn enable_task_list_guide(&mut self) { + self.spawn_float( + FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide", true), + FLOAT_SIZE, + FLOAT_SIZE, + ); + } + fn get_selected_item_type(&self) -> SelectedItem { if self.selected_item_is_up_dir() { SelectedItem::UpDir @@ -862,14 +804,7 @@ impl AppState { if self.skip_confirmation { self.handle_confirm_command(); } else { - let cmd_names = self - .selected_commands - .iter() - .map(|node| node.name.as_str()) - .collect::>(); - - let prompt = ConfirmPrompt::new(&cmd_names); - self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); + self.spawn_confirmprompt(); } } SelectedItem::None => {} @@ -884,7 +819,7 @@ impl AppState { .collect(); let command = RunningCommand::new(&commands); - self.spawn_float(command, 80, 80); + self.spawn_float(command, FLOAT_SIZE, FLOAT_SIZE); self.selected_commands.clear(); } @@ -918,44 +853,21 @@ impl AppState { self.update_items(); } - fn toggle_task_list_guide(&mut self) { - self.spawn_float( - FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide", true), - 80, - 80, - ); - } - fn scroll_tab_down(&mut self) { - let len = self.tabs.len(); - let current = self.current_tab.selected().unwrap_or(0); - let next = if current + 1 >= len { 0 } else { current + 1 }; - - self.current_tab.select(Some(next)); + if self.current_tab.selected().unwrap() == self.tabs.len() - 1 { + self.current_tab.select_first(); + } else { + self.current_tab.select_next(); + } self.refresh_tab(); } fn scroll_tab_up(&mut self) { - let len = self.tabs.len(); - let current = self.current_tab.selected().unwrap_or(0); - let next = if current == 0 { len - 1 } else { current - 1 }; - - self.current_tab.select(Some(next)); + if self.current_tab.selected().unwrap() == 0 { + self.current_tab.select(Some(self.tabs.len() - 1)); + } else { + self.current_tab.select_previous(); + } self.refresh_tab(); } } - -#[cfg(feature = "tips")] -const TIPS: &str = include_str!("../cool_tips.txt"); - -#[cfg(feature = "tips")] -fn get_random_tip() -> String { - let tips: Vec<&str> = TIPS.lines().collect(); - if tips.is_empty() { - return "".to_string(); - } - - let mut rng = rand::thread_rng(); - let random_index = rng.gen_range(0..tips.len()); - format!(" {} ", tips[random_index]) -} diff --git a/tui/src/tips.rs b/tui/src/tips.rs new file mode 100644 index 000000000..046b5cb61 --- /dev/null +++ b/tui/src/tips.rs @@ -0,0 +1,14 @@ +use rand::Rng; + +const TIPS: &str = include_str!("../cool_tips.txt"); + +pub fn get_random_tip() -> &'static str { + let tips: Vec<&str> = TIPS.lines().collect(); + if tips.is_empty() { + return ""; + } + + let mut rng = rand::thread_rng(); + let random_index = rng.gen_range(0..tips.len()); + tips[random_index] +} From 06f13dc5f9bcdd990e6d097739b8bda84637664b Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Wed, 13 Nov 2024 02:11:58 +0530 Subject: [PATCH 14/18] Cleanup Use prelude for ratatui imports. Use const for theme functions, add missing hints --- core/src/config.rs | 3 +-- core/src/inner.rs | 9 ++++----- tui/src/confirmation.rs | 24 +++++++++++++++--------- tui/src/filter.rs | 13 +++++-------- tui/src/float.rs | 30 +++++++++++++----------------- tui/src/floating_text.rs | 10 ++++------ tui/src/hint.rs | 16 +++++----------- tui/src/main.rs | 9 +++------ tui/src/running_command.rs | 31 ++++++++++++++----------------- tui/src/state.rs | 8 ++++---- tui/src/theme.rs | 26 +++++++++++++------------- tui/src/tips.rs | 3 +-- 12 files changed, 82 insertions(+), 100 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index d4f5e5c57..cf0407582 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,6 +1,5 @@ use serde::Deserialize; -use std::path::Path; -use std::process; +use std::{path::Path, process}; #[derive(Deserialize)] pub struct Config { diff --git a/core/src/inner.rs b/core/src/inner.rs index 9d2e71628..0fadae806 100644 --- a/core/src/inner.rs +++ b/core/src/inner.rs @@ -1,3 +1,7 @@ +use crate::{Command, ListNode, Tab}; +use ego_tree::{NodeMut, Tree}; +use include_dir::{include_dir, Dir}; +use serde::Deserialize; use std::{ fs::File, io::{BufRead, BufReader, Read}, @@ -6,11 +10,6 @@ use std::{ path::{Path, PathBuf}, rc::Rc, }; - -use crate::{Command, ListNode, Tab}; -use ego_tree::{NodeMut, Tree}; -use include_dir::{include_dir, Dir}; -use serde::Deserialize; use temp_dir::TempDir; const TAB_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/tabs"); diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs index f9ae42ad1..3a4e95785 100644 --- a/tui/src/confirmation.rs +++ b/tui/src/confirmation.rs @@ -3,7 +3,8 @@ use ratatui::{ crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, layout::Alignment, prelude::*, - widgets::{Block, Borders, Clear, List}, + symbols::border, + widgets::{Block, Clear, List}, }; use std::borrow::Cow; @@ -22,10 +23,17 @@ pub struct ConfirmPrompt { impl ConfirmPrompt { pub fn new(names: &[&str]) -> Self { + let max_count_str = format!("{}", names.len()); let names = names .iter() .zip(1..) - .map(|(name, n)| format!(" {n}. {name}")) + .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}") + }) .collect(); Self { @@ -51,17 +59,15 @@ impl ConfirmPrompt { impl FloatContent for ConfirmPrompt { 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) + let block = Block::bordered() + .border_set(border::ROUNDED) .title(" Confirm selections ") .title_bottom(Line::from(vec![ - Span::styled(" [", Style::default()), + Span::raw(" ["), Span::styled("y", Style::default().fg(theme.success_color())), - Span::styled("] to continue ", Style::default()), - Span::styled("[", Style::default()), + Span::raw("] to continue ["), Span::styled("n", Style::default().fg(theme.fail_color())), - Span::styled("] to abort ", Style::default()), + Span::raw("] to abort "), ])) .title_alignment(Alignment::Center) .title_style(Style::default().bold()) diff --git a/tui/src/filter.rs b/tui/src/filter.rs index bc62bfa79..734db164a 100644 --- a/tui/src/filter.rs +++ b/tui/src/filter.rs @@ -2,11 +2,9 @@ use crate::{state::ListEntry, theme::Theme}; use linutil_core::{ego_tree::NodeId, Tab}; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, - layout::{Position, Rect}, - style::Style, - text::Span, - widgets::{Block, Borders, Paragraph}, - Frame, + prelude::*, + symbols::border, + widgets::{Block, Paragraph}, }; use unicode_width::UnicodeWidthChar; @@ -118,9 +116,8 @@ impl Filter { //Create the search bar widget let search_bar = Paragraph::new(display_text) .block( - Block::default() - .borders(Borders::ALL) - .border_set(ratatui::symbols::border::ROUNDED) + Block::bordered() + .border_set(border::ROUNDED) .title(" Search "), ) .style(Style::default().fg(search_color)); diff --git a/tui/src/float.rs b/tui/src/float.rs index d9c8d6de8..9f29bff38 100644 --- a/tui/src/float.rs +++ b/tui/src/float.rs @@ -1,7 +1,7 @@ use crate::{hint::Shortcut, theme::Theme}; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, MouseEvent}, - layout::{Constraint, Direction, Layout, Rect}, + layout::{Constraint, Layout, Rect}, Frame, }; @@ -29,23 +29,19 @@ impl Float { } fn floating_window(&self, size: Rect) -> Rect { - let hor_float = Layout::default() - .constraints([ - Constraint::Percentage((100 - self.width_percent) / 2), - Constraint::Percentage(self.width_percent), - Constraint::Percentage((100 - self.width_percent) / 2), - ]) - .direction(Direction::Horizontal) - .split(size)[1]; + let hor_float = Layout::horizontal([ + Constraint::Percentage((100 - self.width_percent) / 2), + Constraint::Percentage(self.width_percent), + Constraint::Percentage((100 - self.width_percent) / 2), + ]) + .split(size)[1]; - Layout::default() - .constraints([ - Constraint::Percentage((100 - self.height_percent) / 2), - Constraint::Percentage(self.height_percent), - Constraint::Percentage((100 - self.height_percent) / 2), - ]) - .direction(Direction::Vertical) - .split(hor_float)[1] + Layout::vertical([ + Constraint::Percentage((100 - self.height_percent) / 2), + Constraint::Percentage(self.height_percent), + Constraint::Percentage((100 - self.height_percent) / 2), + ]) + .split(hor_float)[1] } pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect, theme: &Theme) { diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs index 18074ddb9..c2077de53 100644 --- a/tui/src/floating_text.rs +++ b/tui/src/floating_text.rs @@ -2,11 +2,9 @@ use crate::{float::FloatContent, hint::Shortcut, theme::Theme}; use linutil_core::Command; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, - layout::Rect, - style::{Color, Style, Stylize}, - text::{Line, Span, Text}, + prelude::*, + symbols::border, widgets::{Block, Borders, Clear, Paragraph, Wrap}, - Frame, }; use tree_sitter_bash as hl_bash; use tree_sitter_highlight::{self as hl, HighlightEvent}; @@ -177,9 +175,9 @@ impl<'a> FloatContent for FloatingText<'a> { fn draw(&mut self, frame: &mut Frame, area: Rect, _theme: &Theme) { let block = Block::default() .borders(Borders::ALL) - .border_set(ratatui::symbols::border::ROUNDED) + .border_set(border::ROUNDED) .title(self.mode_title.as_str()) - .title_alignment(ratatui::layout::Alignment::Center) + .title_alignment(Alignment::Center) .title_style(Style::default().reversed()) .style(Style::default()); diff --git a/tui/src/hint.rs b/tui/src/hint.rs index 2ab4b99d1..0f77f7a55 100644 --- a/tui/src/hint.rs +++ b/tui/src/hint.rs @@ -5,8 +5,8 @@ use ratatui::{ use std::borrow::Cow; pub struct Shortcut { - pub key_sequences: Vec>, - pub desc: &'static str, + key_sequences: Vec>, + desc: &'static str, } fn add_spacing(list: Vec>) -> Line { @@ -18,7 +18,7 @@ fn add_spacing(list: Vec>) -> Line { .collect() } -pub fn span_vec_len(span_vec: &[Span]) -> usize { +fn span_vec_len(span_vec: &[Span]) -> usize { span_vec.iter().rfold(0, |init, s| init + s.width()) } @@ -38,7 +38,7 @@ pub fn create_shortcut_list( let columns = (render_width as usize / (max_shortcut_width + 4)).max(1); let rows = (shortcut_spans.len() + columns - 1) / columns; - let mut lines: Vec> = Vec::new(); + let mut lines: Vec> = Vec::with_capacity(rows); for row in 0..rows { let row_spans: Vec<_> = (0..columns) @@ -73,13 +73,7 @@ impl Shortcut { let description = Span::styled(self.desc, Style::default().italic()); self.key_sequences .iter() - .flat_map(|seq| { - [ - Span::default().content("["), - seq.clone(), - Span::default().content("] "), - ] - }) + .flat_map(|seq| [Span::raw("["), seq.clone(), Span::raw("] ")]) .chain(std::iter::once(description)) .collect() } diff --git a/tui/src/main.rs b/tui/src/main.rs index 1f03fc1bc..ee0c3235f 100644 --- a/tui/src/main.rs +++ b/tui/src/main.rs @@ -25,7 +25,7 @@ use ratatui::{ }; use state::AppState; use std::{ - io::{self, stdout}, + io::{stdout, Result, Stdout}, path::PathBuf, time::Duration, }; @@ -53,7 +53,7 @@ pub struct Args { size_bypass: bool, } -fn main() -> io::Result<()> { +fn main() -> Result<()> { let args = Args::parse(); let mut state = AppState::new(args); @@ -77,10 +77,7 @@ fn main() -> io::Result<()> { Ok(()) } -fn run( - terminal: &mut Terminal>, - state: &mut AppState, -) -> io::Result<()> { +fn run(terminal: &mut Terminal>, state: &mut AppState) -> Result<()> { loop { terminal.draw(|frame| state.draw(frame)).unwrap(); // Wait for an event diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs index 9a9ea7a95..a3248c95d 100644 --- a/tui/src/running_command.rs +++ b/tui/src/running_command.rs @@ -6,14 +6,13 @@ use portable_pty::{ }; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}, - layout::{Rect, Size}, - style::{Style, Stylize}, - text::Line, - widgets::{Block, Borders}, - Frame, + prelude::*, + symbols::border, + widgets::Block, }; use std::{ - io::Write, + fs::File, + io::{Result, Write}, sync::{Arc, Mutex}, thread::JoinHandle, }; @@ -47,9 +46,8 @@ impl FloatContent for RunningCommand { // Define the block for the terminal display let block = if !self.is_finished() { // Display a block indicating the command is running - Block::default() - .borders(Borders::ALL) - .border_set(ratatui::symbols::border::ROUNDED) + Block::bordered() + .border_set(border::ROUNDED) .title_top(Line::from("Running the command....").centered()) .title_style(Style::default().reversed()) .title_bottom(Line::from("Press Ctrl-C to KILL the command")) @@ -68,16 +66,15 @@ impl FloatContent for RunningCommand { }; let log_path = if let Some(log_path) = &self.log_path { - Line::from(format!(" Log saved: {} ", log_path)).centered() + Line::from(format!(" Log saved: {} ", log_path)) } else { - Line::from(" Press 'l' to save command log ").centered() + Line::from(" Press 'l' to save command log ") }; - Block::default() - .borders(Borders::ALL) - .border_set(ratatui::symbols::border::ROUNDED) + Block::bordered() + .border_set(border::ROUNDED) .title_top(title_line.centered()) - .title_bottom(log_path) + .title_bottom(log_path.centered()) }; // Calculate the inner size of the terminal area, considering borders @@ -300,7 +297,7 @@ impl RunningCommand { } } - fn save_log(&self) -> std::io::Result { + fn save_log(&self) -> Result { let mut log_path = std::env::temp_dir(); let date_format = format_description!("[year]-[month]-[day]-[hour]-[minute]-[second]"); log_path.push(format!( @@ -311,7 +308,7 @@ impl RunningCommand { .unwrap() )); - let mut file = std::fs::File::create(&log_path)?; + let mut file = File::create(&log_path)?; let buffer = self.buffer.lock().unwrap(); file.write_all(&buffer)?; diff --git a/tui/src/state.rs b/tui/src/state.rs index c80eb763c..af29b5f03 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -12,12 +12,10 @@ use crate::{ use linutil_core::{ego_tree::NodeId, Command, Config, ListNode, TabList}; use ratatui::{ crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind}, - layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect}, - style::{Style, Stylize}, + layout::Flex, + prelude::*, symbols::border, - text::{Line, Span, Text}, widgets::{Block, List, ListState, Paragraph}, - Frame, }; use std::rc::Rc; @@ -242,6 +240,8 @@ impl AppState { Shortcut::new("Previous theme", ["T"]), Shortcut::new("Next tab", ["Tab"]), Shortcut::new("Previous tab", ["Shift-Tab"]), + Shortcut::new("Important actions guide", ["g"]), + Shortcut::new("Multi-selection mode", ["v"]), ]), ), diff --git a/tui/src/theme.rs b/tui/src/theme.rs index 9e10ce709..f09c6b537 100644 --- a/tui/src/theme.rs +++ b/tui/src/theme.rs @@ -14,91 +14,91 @@ pub enum Theme { } impl Theme { - pub fn dir_color(&self) -> Color { + pub const fn dir_color(&self) -> Color { match self { Theme::Default => Color::Blue, Theme::Compatible => Color::Blue, } } - pub fn cmd_color(&self) -> Color { + pub const fn cmd_color(&self) -> Color { match self { Theme::Default => Color::Rgb(204, 224, 208), Theme::Compatible => Color::LightGreen, } } - pub fn multi_select_disabled_color(&self) -> Color { + pub const fn multi_select_disabled_color(&self) -> Color { match self { Theme::Default => Color::DarkGray, Theme::Compatible => Color::DarkGray, } } - pub fn tab_color(&self) -> Color { + pub const fn tab_color(&self) -> Color { match self { Theme::Default => Color::Rgb(255, 255, 85), Theme::Compatible => Color::Yellow, } } - pub fn dir_icon(&self) -> &'static str { + pub const fn dir_icon(&self) -> &'static str { match self { Theme::Default => "  ", Theme::Compatible => "[DIR]", } } - pub fn cmd_icon(&self) -> &'static str { + pub const fn cmd_icon(&self) -> &'static str { match self { Theme::Default => "  ", Theme::Compatible => "[CMD]", } } - pub fn tab_icon(&self) -> &'static str { + pub const fn tab_icon(&self) -> &'static str { match self { Theme::Default => " ", Theme::Compatible => ">> ", } } - pub fn multi_select_icon(&self) -> &'static str { + pub const fn multi_select_icon(&self) -> &'static str { match self { Theme::Default => "", Theme::Compatible => "*", } } - pub fn success_color(&self) -> Color { + pub const fn success_color(&self) -> Color { match self { Theme::Default => Color::Rgb(5, 255, 55), Theme::Compatible => Color::Green, } } - pub fn fail_color(&self) -> Color { + pub const fn fail_color(&self) -> Color { match self { Theme::Default => Color::Rgb(199, 55, 44), Theme::Compatible => Color::Red, } } - pub fn focused_color(&self) -> Color { + pub const fn focused_color(&self) -> Color { match self { Theme::Default => Color::LightBlue, Theme::Compatible => Color::LightBlue, } } - pub fn search_preview_color(&self) -> Color { + pub const fn search_preview_color(&self) -> Color { match self { Theme::Default => Color::DarkGray, Theme::Compatible => Color::DarkGray, } } - pub fn unfocused_color(&self) -> Color { + pub const fn unfocused_color(&self) -> Color { match self { Theme::Default => Color::Gray, Theme::Compatible => Color::Gray, diff --git a/tui/src/tips.rs b/tui/src/tips.rs index 046b5cb61..17341c794 100644 --- a/tui/src/tips.rs +++ b/tui/src/tips.rs @@ -8,7 +8,6 @@ pub fn get_random_tip() -> &'static str { return ""; } - let mut rng = rand::thread_rng(); - let random_index = rng.gen_range(0..tips.len()); + let random_index = rand::thread_rng().gen_range(0..tips.len()); tips[random_index] } From 85719251d39878a15d2923a098a724b1febb5208 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Wed, 13 Nov 2024 03:09:38 +0530 Subject: [PATCH 15/18] Update deps, remove unused temp-dir --- Cargo.lock | 29 ++++++++++++++--------------- core/Cargo.toml | 4 ++-- tui/Cargo.toml | 5 ++--- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e87e2df9e..c82660684 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -390,7 +390,6 @@ dependencies = [ "portable-pty", "rand", "ratatui", - "temp-dir", "time", "tree-sitter-bash", "tree-sitter-highlight", @@ -721,18 +720,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -884,9 +883,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -997,9 +996,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9871f16d6cf5c4757dcf30d5d2172a2df6987c510c017bbb7abfb7f9aa24d06" +checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4" dependencies = [ "cc", "regex", @@ -1010,9 +1009,9 @@ dependencies = [ [[package]] name = "tree-sitter-bash" -version = "0.23.1" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa5e1c6bd02c0053f3f68edcf5d8866b38a8640584279e30fca88149ce14dda" +checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e" dependencies = [ "cc", "tree-sitter-language", @@ -1020,9 +1019,9 @@ dependencies = [ [[package]] name = "tree-sitter-highlight" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48859aa39513716018d81904220960f415dbb72e071234a721304d20bf245e4c" +checksum = "7f0f856de10d70a6d14d66db3648f7410c131cd49e989a863f15dda9acae6044" dependencies = [ "lazy_static", "regex", @@ -1134,9 +1133,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "which" -version = "6.0.3" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" dependencies = [ "either", "home", diff --git a/core/Cargo.toml b/core/Cargo.toml index f07d4b779..a38399199 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,7 +10,7 @@ include = ["src/*.rs", "Cargo.toml", "tabs/**"] [dependencies] include_dir = "0.7.4" temp-dir = "0.1.14" -serde = { version = "1.0.205", features = ["derive"], default-features = false } +serde = { version = "1.0.215", features = ["derive"], default-features = false } toml = { version = "0.8.19", features = ["parse"], default-features = false } -which = "6.0.3" +which = "7.0.0" ego-tree = "0.9.0" diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 105a3b568..9ad6044e3 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -19,13 +19,12 @@ oneshot = { version = "0.1.8", features = ["std"], default-features = false } portable-pty = "0.8.1" ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false } tui-term = "0.2.0" -temp-dir = "0.1.14" time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false } unicode-width = { version = "0.2.0", default-features = false } rand = { version = "0.8.5", optional = true } linutil_core = { version = "24.9.28", path = "../core" } -tree-sitter-highlight = "0.24.3" -tree-sitter-bash = "0.23.1" +tree-sitter-highlight = "0.24.4" +tree-sitter-bash = "0.23.3" nix = { version = "0.29.0", features = [ "user" ] } [[bin]] From f8c6ba1e5930222e5f262fc7fe4187666a7d1ad7 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Wed, 13 Nov 2024 19:46:00 +0530 Subject: [PATCH 16/18] Add accidentally deleted preview.tape Add labels + Wait 2sec after program ends --- .github/preview.tape | 93 +++++++++++++++++++++++++++++++++++ .github/workflows/preview.yml | 3 +- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 .github/preview.tape diff --git a/.github/preview.tape b/.github/preview.tape new file mode 100644 index 000000000..e56bd9bf2 --- /dev/null +++ b/.github/preview.tape @@ -0,0 +1,93 @@ +# VHS documentation +# +# Output: +# Output .gif Create a GIF output at the given +# Output .mp4 Create an MP4 output at the given +# Output .webm Create a WebM output at the given +# +# Require: +# Require Ensure a program is on the $PATH to proceed +# +# Settings: +# Set FontSize Set the font size of the terminal +# Set FontFamily Set the font family of the terminal +# Set Height Set the height of the terminal +# Set Width Set the width of the terminal +# Set LetterSpacing Set the font letter spacing (tracking) +# Set LineHeight Set the font line height +# Set LoopOffset % Set the starting frame offset for the GIF loop +# Set Theme Set the theme of the terminal +# Set Padding Set the padding of the terminal +# Set Framerate Set the framerate of the recording +# Set PlaybackSpeed Set the playback speed of the recording +# Set MarginFill Set the file or color the margin will be filled with. +# Set Margin Set the size of the margin. Has no effect if MarginFill isn't set. +# Set BorderRadius Set terminal border radius, in pixels. +# Set WindowBar Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight) +# Set WindowBarSize Set window bar size, in pixels. Default is 40. +# Set TypingSpeed