mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2025-03-03 21:37:12 +00:00
* fix: getting locked out when running script
* 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
* Remove redundant code in themes
Removed redundant match statement with a function
* 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
* Implement case insensitive, fix word disappearing bug
Use regex for case insesitive finding, implement String instead of char<Vec>, fix word disappearing by recalculating the render x for preview text
* Revert "Remove redundant code in themes"
This reverts commit 3b7e859af8
.
* Reference instead of passing the vector
* Revert regex and String implementation
Use Vec<char> for search_input to prevent panics when using multi-byte characters, use lowercase conversion instead of regex, Added comments for clarity
* 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.
* Fix conflicts
* 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
* Update tui/src/filter.rs
Co-authored-by: Liam <33645555+lj3954@users.noreply.github.com>
* 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`
* Cleanup
Use prelude for ratatui imports. Use const for theme functions, add
missing hints
* Update deps, remove unused temp-dir
* Add accidentally deleted preview.tape
Add labels + Wait 2sec after program ends
* Add fields to config files
Skip Confirmation, Bypass Size
* Remove accidentally commited config file
---------
Co-authored-by: Liam <33645555+lj3954@users.noreply.github.com>
241 lines
7.7 KiB
Rust
241 lines
7.7 KiB
Rust
use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
|
|
use linutil_core::Command;
|
|
use ratatui::{
|
|
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
|
|
prelude::*,
|
|
symbols::border,
|
|
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
|
};
|
|
use tree_sitter_bash as hl_bash;
|
|
use tree_sitter_highlight::{self as hl, HighlightEvent};
|
|
|
|
macro_rules! style {
|
|
($r:literal, $g:literal, $b:literal) => {{
|
|
Style::new().fg(Color::Rgb($r, $g, $b))
|
|
}};
|
|
}
|
|
|
|
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
|
|
("comment", style!(92, 131, 75)), // green
|
|
("embedded", style!(206, 145, 120)), // blue (string expansions)
|
|
("constant", style!(79, 193, 255)), // dark blue
|
|
("keyword", style!(197, 134, 192)), // magenta
|
|
("number", style!(181, 206, 168)), // light green
|
|
];
|
|
|
|
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,
|
|
}
|
|
|
|
impl<'a> FloatingText<'a> {
|
|
pub fn new(text: String, title: &str, wrap_words: bool) -> Self {
|
|
let processed_text = Text::from(text);
|
|
|
|
Self {
|
|
inner_area_size: (0, 0),
|
|
mode_title: title.to_string(),
|
|
processed_text,
|
|
scroll: (0, 0),
|
|
wrap_words,
|
|
}
|
|
}
|
|
|
|
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 processed_text = Self::get_highlighted_string(&src).unwrap_or_else(|| Text::from(src));
|
|
|
|
Self {
|
|
inner_area_size: (0, 0),
|
|
mode_title: title.to_string(),
|
|
processed_text,
|
|
scroll: (0, 0),
|
|
wrap_words,
|
|
}
|
|
}
|
|
|
|
fn get_highlighted_string(s: &str) -> Option<Text<'a>> {
|
|
let matched_tokens = SYNTAX_HIGHLIGHT_STYLES
|
|
.iter()
|
|
.map(|hl| hl.0)
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut lines = Vec::with_capacity(s.lines().count());
|
|
let mut current_line = Vec::new();
|
|
let mut style_stack = vec![Style::default()];
|
|
|
|
let mut hl_conf = hl::HighlightConfiguration::new(
|
|
hl_bash::LANGUAGE.into(),
|
|
"bash",
|
|
hl_bash::HIGHLIGHT_QUERY,
|
|
"",
|
|
"",
|
|
)
|
|
.ok()?;
|
|
|
|
hl_conf.configure(&matched_tokens);
|
|
|
|
let mut hl = hl::Highlighter::new();
|
|
let events = hl.highlight(&hl_conf, s.as_bytes(), None, |_| None).ok()?;
|
|
|
|
for event in events {
|
|
match event.ok()? {
|
|
HighlightEvent::HighlightStart(h) => {
|
|
style_stack.push(SYNTAX_HIGHLIGHT_STYLES.get(h.0)?.1);
|
|
}
|
|
|
|
HighlightEvent::HighlightEnd => {
|
|
style_stack.pop();
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
if lines.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(Text::from(lines))
|
|
}
|
|
|
|
fn scroll_down(&mut self) {
|
|
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) {
|
|
self.scroll.0 = self.scroll.0.saturating_sub(1);
|
|
}
|
|
|
|
fn scroll_left(&mut self) {
|
|
self.scroll.1 = self.scroll.1.saturating_sub(1);
|
|
}
|
|
|
|
fn scroll_right(&mut self) {
|
|
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<'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(border::ROUNDED)
|
|
.title(self.mode_title.as_str())
|
|
.title_alignment(Alignment::Center)
|
|
.title_style(Style::default().reversed())
|
|
.style(Style::default());
|
|
|
|
let inner_area = block.inner(area);
|
|
self.inner_area_size = (inner_area.width as usize, inner_area.height as usize);
|
|
|
|
frame.render_widget(Clear, area);
|
|
frame.render_widget(block, area);
|
|
|
|
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)
|
|
};
|
|
|
|
frame.render_widget(paragraph, inner_area);
|
|
}
|
|
|
|
fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
|
|
match event.kind {
|
|
MouseEventKind::ScrollDown => self.scroll_down(),
|
|
MouseEventKind::ScrollUp => self.scroll_up(),
|
|
MouseEventKind::ScrollLeft => self.scroll_left(),
|
|
MouseEventKind::ScrollRight => self.scroll_right(),
|
|
_ => {}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
|
use KeyCode::{Char, Down, Left, Right, Up};
|
|
match key.code {
|
|
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
|
|
}
|
|
|
|
fn is_finished(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>) {
|
|
(
|
|
&self.mode_title,
|
|
Box::new([
|
|
Shortcut::new("Scroll down", ["j", "Down"]),
|
|
Shortcut::new("Scroll up", ["k", "Up"]),
|
|
Shortcut::new("Scroll left", ["h", "Left"]),
|
|
Shortcut::new("Scroll right", ["l", "Right"]),
|
|
Shortcut::new("Close window", ["Enter", "p", "q", "d", "g"]),
|
|
]),
|
|
)
|
|
}
|
|
}
|