This commit is contained in:
Jeevitha Kannan K S 2024-11-12 15:37:45 +00:00 committed by GitHub
commit caf795fc8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 270 additions and 407 deletions

49
Cargo.lock generated
View File

@ -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"
@ -395,8 +383,6 @@ dependencies = [
name = "linutil_tui"
version = "24.9.28"
dependencies = [
"ansi-to-tui",
"anstyle",
"clap",
"linutil_core",
"nix 0.29.0",
@ -405,13 +391,11 @@ dependencies = [
"rand",
"ratatui",
"temp-dir",
"textwrap",
"time",
"tree-sitter-bash",
"tree-sitter-highlight",
"tui-term",
"unicode-width 0.2.0",
"zips",
]
[[package]]
@ -460,12 +444,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"
@ -505,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"
@ -940,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"
@ -1328,14 +1290,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",
]

View File

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

View File

@ -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, MouseEvent, MouseEventKind},
layout::Alignment,
prelude::*,
widgets::{Block, Borders, Clear, List},
};
use std::borrow::Cow;
pub enum ConfirmStatus {
Confirm,
@ -16,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());
let names = names
.iter()
.zip(1..)
.map(|(name, n)| {
let count_str = format!("{n}");
let space_str = (0..(max_count_str.len() - count_str.len()))
.map(|_| ' ')
.collect::<String>();
format!("{space_str}{n}. {name}")
})
.map(|(name, n)| format!(" {n}. {name}"))
.collect();
Self {
inner_area_height: 0,
names,
status: ConfirmStatus::None,
scroll: 0,
status: ConfirmStatus::None,
}
}
pub fn scroll_down(&mut self) {
if self.scroll < self.names.len() - 1 {
if self.scroll + self.inner_area_height < self.names.len() - 1 {
self.scroll += 1;
}
}
@ -57,19 +50,28 @@ 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());
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
@ -81,7 +83,6 @@ impl FloatContent for ConfirmPrompt {
})
.collect::<Text>();
frame.render_widget(Clear, inner_area);
frame.render_widget(List::new(paths_text), inner_area);
}
@ -99,21 +100,21 @@ impl FloatContent for ConfirmPrompt {
}
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
}
@ -131,8 +132,8 @@ impl FloatContent for ConfirmPrompt {
Box::new([
Shortcut::new("Continue", ["Y", "y"]),
Shortcut::new("Abort", ["N", "n", "q", "Esc"]),
Shortcut::new("Scroll up", ["k"]),
Shortcut::new("Scroll down", ["j"]),
Shortcut::new("Scroll up", ["k", "Up"]),
Shortcut::new("Scroll down", ["j", "Down"]),
Shortcut::new("Close linutil", ["CTRL-c"]),
]),
)

View File

@ -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,
@ -17,10 +17,12 @@ pub enum SearchAction {
}
pub struct Filter {
// Use Vec<char> to handle multi-byte characters like emojis
search_input: Vec<char>,
in_search_mode: bool,
input_position: usize,
items: Vec<ListEntry>,
// No complex string manipulation is done with completion_preview so we can use String unlike search_input
completion_preview: Option<String>,
}
@ -62,13 +64,11 @@ impl Filter {
.collect();
} else {
self.items.clear();
let query_lower = self.search_input.iter().collect::<String>().to_lowercase();
for tab in tabs.iter() {
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()
{
@ -78,31 +78,26 @@ impl Filter {
has_children: false,
});
}
stack.extend(node.children().map(|child| child.id()));
}
}
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
self.items
.sort_unstable_by(|a, b| a.node.name.cmp(&b.node.name));
}
self.update_completion_preview();
}
fn update_completion_preview(&mut self) {
if self.search_input.is_empty() {
self.completion_preview = None;
return;
}
let input = self.search_input.iter().collect::<String>().to_lowercase();
self.completion_preview = self.items.iter().find_map(|item| {
let item_name_lower = item.node.name.to_lowercase();
if item_name_lower.starts_with(&input) {
Some(item_name_lower[input.len()..].to_string())
} else {
self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() {
None
} else {
let input = self.search_input.iter().collect::<String>().to_lowercase();
self.items.iter().find_map(|item| {
let item_name_lower = item.node.name.to_lowercase();
(item_name_lower.starts_with(&input))
.then_some(item_name_lower[input.len()..].to_string())
})
}
});
}
pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
@ -135,24 +130,32 @@ impl Filter {
// Render cursor in search bar
if self.in_search_mode {
let cursor_position: usize = self.search_input[..self.input_position]
// 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))
.map(|c| c.width().unwrap_or(1) as u16)
.sum();
let x = area.x + cursor_position as u16 + 1;
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_span = Span::styled(preview, Style::default().fg(Color::DarkGray));
let preview_paragraph = Paragraph::new(preview_span).style(Style::default());
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(
x,
preview_x,
y,
(preview.len() as u16).min(area.width - cursor_position as u16 - 1),
(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(preview_paragraph, preview_area);
frame.render_widget(Paragraph::new(preview_span), preview_area);
}
}
}
@ -220,10 +223,18 @@ impl Filter {
}
fn complete_search(&mut self) -> SearchAction {
if let Some(completion) = self.completion_preview.take() {
self.search_input.extend(completion.chars());
if self.completion_preview.is_some() {
let input = &self.search_input.iter().collect::<String>().to_lowercase();
if let Some(search_completion) = self
.items
.iter()
.find(|item| item.node.name.to_lowercase().starts_with(input))
{
self.search_input = search_completion.node.name.chars().collect();
}
self.input_position = self.search_input.len();
self.update_completion_preview();
self.completion_preview = None;
SearchAction::Update
} else {
SearchAction::None

View File

@ -1,13 +1,12 @@
use crate::{hint::Shortcut, theme::Theme};
use ratatui::{
crossterm::event::{KeyCode, KeyEvent, MouseEvent},
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 handle_mouse_event(&mut self, key: &MouseEvent) -> bool;
fn is_finished(&self) -> bool;
@ -49,9 +48,9 @@ impl<Content: FloatContent + ?Sized> Float<Content> {
.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);
}
pub fn handle_mouse_event(&mut self, event: &MouseEvent) {

View File

@ -1,48 +1,23 @@
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 linutil_core::Command;
use ratatui::{
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
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 ansi_to_tui::IntoText;
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<String>,
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
@ -53,7 +28,61 @@ const SYNTAX_HIGHLIGHT_STYLES: [(&str, anstyle::Style); 8] = [
("number", style!(181, 206, 168)), // light green
];
fn get_highlighted_string(s: &str) -> Option<String> {
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",
@ -63,24 +92,13 @@ fn get_highlighted_string(s: &str) -> Option<String> {
)
.ok()?;
let matched_tokens = &SYNTAX_HIGHLIGHT_STYLES
.iter()
.map(|hl| hl.0)
.collect::<Vec<_>>();
hl_conf.configure(matched_tokens);
hl_conf.configure(&matched_tokens);
let mut hl = hl::Highlighter::new();
let mut style_stack = vec![anstyle::Style::new()];
let src = s.as_bytes();
let events = hl.highlight(&hl_conf, src, None, |_| None).ok()?;
let mut buf = Cursor::new(vec![]);
let events = hl.highlight(&hl_conf, s.as_bytes(), None, |_| None).ok()?;
for event in events {
match event.unwrap() {
match event.ok()? {
HighlightEvent::HighlightStart(h) => {
style_stack.push(SYNTAX_HIGHLIGHT_STYLES.get(h.0)?.1);
}
@ -90,197 +108,96 @@ fn get_highlighted_string(s: &str) -> Option<String> {
}
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 style = *style_stack.last()?;
let content = &s[start..end];
let mut output = String::new();
zip_result!(
buf.seek(SeekFrom::Start(0)),
buf.read_to_string(&mut output),
)?;
Some(output)
}
#[inline]
fn get_lines(s: &str) -> Vec<&str> {
s.lines().collect::<Vec<_>>()
}
#[inline]
fn get_lines_owned(s: &str) -> Vec<String> {
get_lines(s).iter().map(|s| s.to_string()).collect()
}
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()
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 {
get_lines_owned(&text)
};
Self {
src: text,
wrapped_lines,
mode_title: title.to_string(),
max_line_width,
v_scroll: 0,
h_scroll: 0,
wrap_words,
frame_height: 0,
current_line.push(Span::styled(part.to_owned(), style));
}
}
}
}
}
pub fn from_command(command: &Command, title: String) -> Option<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,
}?;
// 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));
}
let max_line_width = 80;
let wrapped_lines = get_lines_owned(&get_highlighted_string(&src)?);
if lines.is_empty() {
return None;
}
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()
let visible_length = self.inner_area_size.0.saturating_sub(1);
let max_scroll = if self.wrap_words {
0
} else {
get_lines_owned(&get_highlighted_string(&self.src).unwrap_or(self.src.clone()))
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 {
fn draw(&mut self, frame: &mut Frame, area: Rect) {
self.frame_height = area.height as usize;
// Define the Block with a border and background color
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)
.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);
frame.render_widget(Clear, area);
frame.render_widget(block, area);
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())]
let paragraph = if self.wrap_words {
Paragraph::new(self.processed_text.clone())
.scroll(self.scroll)
.wrap(Wrap { trim: false })
} 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::<VecDeque<_>>();
Paragraph::new(self.processed_text.clone()).scroll(self.scroll)
};
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::<Vec<_>>();
// Create list widget
let list = List::new(lines)
.block(Block::default())
.highlight_style(Style::default().reversed());
// Render the list inside the bordered area
frame.render_widget(list, inner_area);
frame.render_widget(paragraph, inner_area);
}
fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
@ -295,12 +212,12 @@ impl FloatContent for FloatingText {
}
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

View File

@ -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<Span<'static>>,

View File

@ -8,15 +8,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::{
@ -28,6 +21,11 @@ use ratatui::{
Terminal,
};
use state::AppState;
use std::{
io::{self, stdout},
path::PathBuf,
time::Duration,
};
// Linux utility toolbox
#[derive(Debug, Parser)]

View File

@ -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<FloatingText> {
pub fn check_root_status<'a>() -> Option<FloatingText<'a>> {
(Uid::effective().is_root()).then_some(FloatingText::new(
ROOT_WARNING.into(),
"Root User Warning",

View File

@ -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,8 +7,8 @@ use portable_pty::{
use ratatui::{
crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind},
layout::{Rect, Size},
style::{Color, Style, Stylize},
text::{Line, Span},
style::{Style, Stylize},
text::Line,
widgets::{Block, Borders},
Frame,
};
@ -19,9 +19,10 @@ use std::{
};
use time::{macros::format_description, OffsetDateTime};
use tui_term::{
vt100::{self, Screen},
vt100::{Parser, Screen},
widget::PseudoTerminal,
};
pub struct RunningCommand {
/// A buffer to save all the command output (accumulates, until the command exits)
buffer: Arc<Mutex<Vec<u8>>>,
@ -42,13 +43,7 @@ pub struct RunningCommand {
}
impl FloatContent for RunningCommand {
fn draw(&mut self, frame: &mut Frame, area: Rect) {
// 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,
};
fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
// Define the block for the terminal display
let block = if !self.is_finished() {
// Display a block indicating the command is running
@ -60,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(Color::Green).reversed()),
let title_line = if self.get_exit_status().success() {
Line::styled(
"SUCCESS! Press <ENTER> to close this window",
Style::default().fg(theme.success_color()).reversed(),
)
} else {
Line::from(
Span::default()
.content("FAILED!")
.style(Style::default().fg(Color::Red).reversed()),
Line::styled(
"FAILED! Press <ENTER> to close this window",
Style::default().fg(theme.fail_color()).reversed(),
)
};
title_line.push_span(
Span::default()
.content(" Press <ENTER> 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);
@ -179,7 +165,7 @@ impl FloatContent for RunningCommand {
}
impl RunningCommand {
pub fn new(commands: Vec<Command>) -> Self {
pub fn new(commands: &[&Command]) -> Self {
let pty_system = NativePtySystem::default();
// Build the command based on the provided Command enum variant
@ -199,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
}
@ -285,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);

View File

@ -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::{
@ -243,7 +243,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()
@ -480,8 +480,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),
_ => {}
}
@ -820,13 +820,11 @@ 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) {
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);
}
}
}
fn enable_description(&mut self) {
if let Some(command_description) = self.get_selected_description() {
@ -870,7 +868,7 @@ impl AppState {
.map(|node| node.name.as_str())
.collect::<Vec<_>>();
let prompt = ConfirmPrompt::new(&cmd_names[..]);
let prompt = ConfirmPrompt::new(&cmd_names);
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
}
}
@ -879,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();
}

View File

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