mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-24 22:14:28 +00:00
Merge db0f6c5857
into 76f8e6438b
This commit is contained in:
commit
e442d20a10
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -449,13 +449,13 @@ dependencies = [
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"rand",
|
"rand",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"regex",
|
||||||
"temp-dir",
|
"temp-dir",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"time",
|
"time",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"tree-sitter-highlight",
|
"tree-sitter-highlight",
|
||||||
"tui-term",
|
"tui-term",
|
||||||
"unicode-width 0.2.0",
|
|
||||||
"zips",
|
"zips",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ ratatui = "0.29.0"
|
||||||
tui-term = "0.2.0"
|
tui-term = "0.2.0"
|
||||||
temp-dir = "0.1.14"
|
temp-dir = "0.1.14"
|
||||||
time = { version = "0.3.36", features = ["local-offset", "macros", "formatting"] }
|
time = { version = "0.3.36", features = ["local-offset", "macros", "formatting"] }
|
||||||
unicode-width = "0.2.0"
|
|
||||||
rand = { version = "0.8.5", optional = true }
|
rand = { version = "0.8.5", optional = true }
|
||||||
linutil_core = { path = "../core", version = "24.9.28" }
|
linutil_core = { path = "../core", version = "24.9.28" }
|
||||||
tree-sitter-highlight = "0.24.3"
|
tree-sitter-highlight = "0.24.3"
|
||||||
|
@ -30,6 +29,7 @@ textwrap = "0.16.1"
|
||||||
anstyle = "1.0.8"
|
anstyle = "1.0.8"
|
||||||
ansi-to-tui = "7.0.0"
|
ansi-to-tui = "7.0.0"
|
||||||
zips = "0.1.7"
|
zips = "0.1.7"
|
||||||
|
regex = { version = "1.3", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "linutil"
|
name = "linutil"
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use std::borrow::Cow;
|
use crate::{float::FloatContent, hint::Shortcut, theme};
|
||||||
|
|
||||||
use crate::{float::FloatContent, hint::Shortcut};
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent},
|
crossterm::event::{KeyCode, KeyEvent},
|
||||||
layout::Alignment,
|
layout::Alignment,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
widgets::{Block, Borders, Clear, List},
|
widgets::{Block, Borders, Clear, List},
|
||||||
};
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub enum ConfirmStatus {
|
pub enum ConfirmStatus {
|
||||||
Confirm,
|
Confirm,
|
||||||
|
@ -16,35 +14,30 @@ pub enum ConfirmStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfirmPrompt {
|
pub struct ConfirmPrompt {
|
||||||
pub names: Box<[String]>,
|
inner_area_height: usize,
|
||||||
pub status: ConfirmStatus,
|
names: Box<[String]>,
|
||||||
scroll: usize,
|
scroll: usize,
|
||||||
|
pub status: ConfirmStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfirmPrompt {
|
impl ConfirmPrompt {
|
||||||
pub fn new(names: &[&str]) -> Self {
|
pub fn new(names: Vec<&str>) -> Self {
|
||||||
let max_count_str = format!("{}", names.len());
|
|
||||||
let names = names
|
let names = names
|
||||||
.iter()
|
.iter()
|
||||||
.zip(1..)
|
.zip(1..)
|
||||||
.map(|(name, n)| {
|
.map(|(name, n)| format!(" {n}. {name}"))
|
||||||
let count_str = format!("{n}");
|
|
||||||
let space_str = (0..(max_count_str.len() - count_str.len()))
|
|
||||||
.map(|_| ' ')
|
|
||||||
.collect::<String>();
|
|
||||||
format!("{space_str}{n}. {name}")
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
inner_area_height: 0,
|
||||||
names,
|
names,
|
||||||
status: ConfirmStatus::None,
|
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
|
status: ConfirmStatus::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_down(&mut self) {
|
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;
|
self.scroll += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,19 +50,28 @@ impl ConfirmPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FloatContent for 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()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_set(ratatui::symbols::border::ROUNDED)
|
.border_set(ratatui::symbols::border::ROUNDED)
|
||||||
.title(" Confirm selections ")
|
.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_alignment(Alignment::Center)
|
||||||
.title_style(Style::default().bold())
|
.title_style(Style::default().bold())
|
||||||
.style(Style::default());
|
.style(Style::default());
|
||||||
|
|
||||||
frame.render_widget(block.clone(), area);
|
|
||||||
|
|
||||||
let inner_area = block.inner(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
|
let paths_text = self
|
||||||
.names
|
.names
|
||||||
|
@ -81,26 +83,25 @@ impl FloatContent for ConfirmPrompt {
|
||||||
})
|
})
|
||||||
.collect::<Text>();
|
.collect::<Text>();
|
||||||
|
|
||||||
frame.render_widget(Clear, inner_area);
|
|
||||||
frame.render_widget(List::new(paths_text), inner_area);
|
frame.render_widget(List::new(paths_text), inner_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
||||||
use KeyCode::*;
|
use ConfirmStatus::*;
|
||||||
|
use KeyCode::{Char, Down, Esc, Up};
|
||||||
self.status = match key.code {
|
self.status = match key.code {
|
||||||
Char('y') | Char('Y') => ConfirmStatus::Confirm,
|
Char('y') | Char('Y') => Confirm,
|
||||||
Char('n') | Char('N') | Esc | Char('q') => ConfirmStatus::Abort,
|
Char('n') | Char('N') | Esc | Char('q') => Abort,
|
||||||
Char('j') => {
|
Char('j') | Char('J') | Down => {
|
||||||
self.scroll_down();
|
self.scroll_down();
|
||||||
ConfirmStatus::None
|
None
|
||||||
}
|
}
|
||||||
Char('k') => {
|
Char('k') | Char('K') | Up => {
|
||||||
self.scroll_up();
|
self.scroll_up();
|
||||||
ConfirmStatus::None
|
None
|
||||||
}
|
}
|
||||||
_ => ConfirmStatus::None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ use linutil_core::{ego_tree::NodeId, Tab};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||||
layout::{Position, Rect},
|
layout::{Position, Rect},
|
||||||
style::{Color, Style},
|
style::Style,
|
||||||
text::Span,
|
text::Span,
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use unicode_width::UnicodeWidthChar;
|
use regex::RegexBuilder;
|
||||||
|
|
||||||
pub enum SearchAction {
|
pub enum SearchAction {
|
||||||
None,
|
None,
|
||||||
|
@ -17,7 +17,7 @@ pub enum SearchAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Filter {
|
pub struct Filter {
|
||||||
search_input: Vec<char>,
|
search_input: String,
|
||||||
in_search_mode: bool,
|
in_search_mode: bool,
|
||||||
input_position: usize,
|
input_position: usize,
|
||||||
items: Vec<ListEntry>,
|
items: Vec<ListEntry>,
|
||||||
|
@ -27,7 +27,7 @@ pub struct Filter {
|
||||||
impl Filter {
|
impl Filter {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
search_input: vec![],
|
search_input: String::new(),
|
||||||
in_search_mode: false,
|
in_search_mode: false,
|
||||||
input_position: 0,
|
input_position: 0,
|
||||||
items: vec![],
|
items: vec![],
|
||||||
|
@ -62,47 +62,45 @@ impl Filter {
|
||||||
.collect();
|
.collect();
|
||||||
} else {
|
} else {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
|
if let Ok(regex) = self.regex_builder(®ex::escape(&self.search_input)) {
|
||||||
let query_lower = self.search_input.iter().collect::<String>().to_lowercase();
|
for tab in tabs {
|
||||||
for tab in tabs.iter() {
|
|
||||||
let mut stack = vec![tab.tree.root().id()];
|
let mut stack = vec![tab.tree.root().id()];
|
||||||
while let Some(node_id) = stack.pop() {
|
while let Some(node_id) = stack.pop() {
|
||||||
let node = tab.tree.get(node_id).unwrap();
|
let node = tab.tree.get(node_id).unwrap();
|
||||||
|
if regex.is_match(&node.value().name) && !node.has_children() {
|
||||||
if node.value().name.to_lowercase().contains(&query_lower)
|
|
||||||
&& !node.has_children()
|
|
||||||
{
|
|
||||||
self.items.push(ListEntry {
|
self.items.push(ListEntry {
|
||||||
node: node.value().clone(),
|
node: node.value().clone(),
|
||||||
id: node.id(),
|
id: node.id(),
|
||||||
has_children: false,
|
has_children: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.extend(node.children().map(|child| child.id()));
|
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));
|
||||||
|
} else {
|
||||||
|
self.search_input.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_completion_preview();
|
self.update_completion_preview();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_completion_preview(&mut self) {
|
fn update_completion_preview(&mut self) {
|
||||||
if self.search_input.is_empty() {
|
self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() {
|
||||||
self.completion_preview = None;
|
None
|
||||||
return;
|
} else {
|
||||||
}
|
let pattern = format!("(?i)^{}", regex::escape(&self.search_input));
|
||||||
|
if let Ok(regex) = self.regex_builder(&pattern) {
|
||||||
let input = self.search_input.iter().collect::<String>().to_lowercase();
|
self.items.iter().find_map(|item| {
|
||||||
self.completion_preview = self.items.iter().find_map(|item| {
|
regex
|
||||||
let item_name_lower = item.node.name.to_lowercase();
|
.find(&item.node.name)
|
||||||
if item_name_lower.starts_with(&input) {
|
.map(|mat| item.node.name[mat.end()..].to_string())
|
||||||
Some(item_name_lower[input.len()..].to_string())
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
|
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() {
|
let display_text = if !self.in_search_mode && self.search_input.is_empty() {
|
||||||
Span::raw("Press / to search")
|
Span::raw("Press / to search")
|
||||||
} else {
|
} else {
|
||||||
let input_text = self.search_input.iter().collect::<String>();
|
Span::styled(
|
||||||
Span::styled(input_text, Style::default().fg(theme.focused_color()))
|
&self.search_input,
|
||||||
|
Style::default().fg(theme.focused_color()),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let search_color = if self.in_search_mode {
|
let search_color = if self.in_search_mode {
|
||||||
|
@ -135,24 +135,16 @@ impl Filter {
|
||||||
|
|
||||||
// Render cursor in search bar
|
// Render cursor in search bar
|
||||||
if self.in_search_mode {
|
if self.in_search_mode {
|
||||||
let cursor_position: usize = self.search_input[..self.input_position]
|
let x = area.x + self.input_position as u16 + 1;
|
||||||
.iter()
|
|
||||||
.map(|c| c.width().unwrap_or(1))
|
|
||||||
.sum();
|
|
||||||
let x = area.x + cursor_position as u16 + 1;
|
|
||||||
let y = area.y + 1;
|
let y = area.y + 1;
|
||||||
frame.set_cursor_position(Position::new(x, y));
|
frame.set_cursor_position(Position::new(x, y));
|
||||||
|
|
||||||
if let Some(preview) = &self.completion_preview {
|
if let Some(preview) = &self.completion_preview {
|
||||||
let preview_span = Span::styled(preview, Style::default().fg(Color::DarkGray));
|
let preview_x = area.x + self.search_input.len() as u16 + 1;
|
||||||
let preview_paragraph = Paragraph::new(preview_span).style(Style::default());
|
let preview_span =
|
||||||
let preview_area = Rect::new(
|
Span::styled(preview, Style::default().fg(theme.search_preview_color()));
|
||||||
x,
|
let preview_area = Rect::new(preview_x, y, preview.len() as u16, 1);
|
||||||
y,
|
frame.render_widget(Paragraph::new(preview_span), preview_area);
|
||||||
(preview.len() as u16).min(area.width - cursor_position as u16 - 1),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
frame.render_widget(preview_paragraph, preview_area);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,16 +211,37 @@ impl Filter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn regex_builder(&self, pattern: &str) -> Result<regex::Regex, regex::Error> {
|
||||||
|
RegexBuilder::new(pattern).case_insensitive(true).build()
|
||||||
|
}
|
||||||
|
|
||||||
fn complete_search(&mut self) -> SearchAction {
|
fn complete_search(&mut self) -> SearchAction {
|
||||||
if let Some(completion) = self.completion_preview.take() {
|
if self.completion_preview.is_none() {
|
||||||
self.search_input.extend(completion.chars());
|
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();
|
self.input_position = self.search_input.len();
|
||||||
self.update_completion_preview();
|
|
||||||
SearchAction::Update
|
SearchAction::Update
|
||||||
} else {
|
} else {
|
||||||
SearchAction::None
|
SearchAction::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_search(&mut self) {
|
pub fn clear_search(&mut self) {
|
||||||
self.search_input.clear();
|
self.search_input.clear();
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
|
use crate::{hint::Shortcut, theme::Theme};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent},
|
crossterm::event::{KeyCode, KeyEvent},
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::hint::Shortcut;
|
|
||||||
|
|
||||||
pub trait FloatContent {
|
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_key_event(&mut self, key: &KeyEvent) -> bool;
|
||||||
fn is_finished(&self) -> bool;
|
fn is_finished(&self) -> bool;
|
||||||
fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>);
|
fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>);
|
||||||
|
@ -48,9 +47,9 @@ impl<Content: FloatContent + ?Sized> Float<Content> {
|
||||||
.split(hor_float)[1]
|
.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);
|
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.
|
// Returns true if the floating window is finished.
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
use std::{
|
use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
|
||||||
borrow::Cow,
|
use ansi_to_tui::IntoText;
|
||||||
collections::VecDeque,
|
|
||||||
io::{Cursor, Read as _, Seek, SeekFrom, Write as _},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{float::FloatContent, hint::Shortcut};
|
|
||||||
|
|
||||||
use linutil_core::Command;
|
use linutil_core::Command;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent},
|
crossterm::event::{KeyCode, KeyEvent},
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
@ -16,9 +9,11 @@ use ratatui::{
|
||||||
widgets::{Block, Borders, Clear, List},
|
widgets::{Block, Borders, Clear, List},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use std::{
|
||||||
use ansi_to_tui::IntoText;
|
borrow::Cow,
|
||||||
|
collections::VecDeque,
|
||||||
|
io::{Cursor, Read as _, Seek, SeekFrom, Write as _},
|
||||||
|
};
|
||||||
use textwrap::wrap;
|
use textwrap::wrap;
|
||||||
use tree_sitter_bash as hl_bash;
|
use tree_sitter_bash as hl_bash;
|
||||||
use tree_sitter_highlight::{self as hl, HighlightEvent};
|
use tree_sitter_highlight::{self as hl, HighlightEvent};
|
||||||
|
@ -209,7 +204,7 @@ impl FloatingText {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FloatContent for 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;
|
self.frame_height = area.height as usize;
|
||||||
|
|
||||||
// Define the Block with a border and background color
|
// Define the Block with a border and background color
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
style::{Style, Stylize},
|
style::{Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
};
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub struct Shortcut {
|
pub struct Shortcut {
|
||||||
pub key_sequences: Vec<Span<'static>>,
|
pub key_sequences: Vec<Span<'static>>,
|
||||||
|
|
|
@ -7,15 +7,8 @@ mod running_command;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
use std::{
|
|
||||||
io::{self, stdout},
|
|
||||||
path::PathBuf,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
crossterm::{
|
crossterm::{
|
||||||
|
@ -27,6 +20,11 @@ use ratatui::{
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
use std::{
|
||||||
|
io::{self, stdout},
|
||||||
|
path::PathBuf,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
// Linux utility toolbox
|
// Linux utility toolbox
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{float::FloatContent, hint::Shortcut};
|
use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
|
||||||
use linutil_core::Command;
|
use linutil_core::Command;
|
||||||
use oneshot::{channel, Receiver};
|
use oneshot::{channel, Receiver};
|
||||||
use portable_pty::{
|
use portable_pty::{
|
||||||
|
@ -7,7 +7,7 @@ use portable_pty::{
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||||
layout::{Rect, Size},
|
layout::{Rect, Size},
|
||||||
style::{Color, Style, Stylize},
|
style::{Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders},
|
widgets::{Block, Borders},
|
||||||
Frame,
|
Frame,
|
||||||
|
@ -22,6 +22,7 @@ use tui_term::{
|
||||||
vt100::{self, Screen},
|
vt100::{self, Screen},
|
||||||
widget::PseudoTerminal,
|
widget::PseudoTerminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct RunningCommand {
|
pub struct RunningCommand {
|
||||||
/// A buffer to save all the command output (accumulates, until the command exits)
|
/// A buffer to save all the command output (accumulates, until the command exits)
|
||||||
buffer: Arc<Mutex<Vec<u8>>>,
|
buffer: Arc<Mutex<Vec<u8>>>,
|
||||||
|
@ -42,7 +43,7 @@ pub struct RunningCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FloatContent for 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
|
// Calculate the inner size of the terminal area, considering borders
|
||||||
let inner_size = Size {
|
let inner_size = Size {
|
||||||
width: area.width - 2, // Adjust for border width
|
width: area.width - 2, // Adjust for border width
|
||||||
|
@ -64,13 +65,13 @@ impl FloatContent for RunningCommand {
|
||||||
Line::from(
|
Line::from(
|
||||||
Span::default()
|
Span::default()
|
||||||
.content("SUCCESS!")
|
.content("SUCCESS!")
|
||||||
.style(Style::default().fg(Color::Green).reversed()),
|
.style(Style::default().fg(theme.success_color()).reversed()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Line::from(
|
Line::from(
|
||||||
Span::default()
|
Span::default()
|
||||||
.content("FAILED!")
|
.content("FAILED!")
|
||||||
.style(Style::default().fg(Color::Red).reversed()),
|
.style(Style::default().fg(theme.fail_color()).reversed()),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -137,7 +137,7 @@ impl AppState {
|
||||||
.map(|node| node.name.as_str())
|
.map(|node| node.name.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let prompt = ConfirmPrompt::new(&cmd_names);
|
let prompt = ConfirmPrompt::new(cmd_names);
|
||||||
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
|
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,7 @@ impl AppState {
|
||||||
MIN_HEIGHT,
|
MIN_HEIGHT,
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Center)
|
.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 });
|
.wrap(ratatui::widgets::Wrap { trim: true });
|
||||||
|
|
||||||
let centered_layout = Layout::default()
|
let centered_layout = Layout::default()
|
||||||
|
@ -461,8 +461,8 @@ impl AppState {
|
||||||
frame.render_stateful_widget(disclaimer_list, list_chunks[1], &mut self.selection);
|
frame.render_stateful_widget(disclaimer_list, list_chunks[1], &mut self.selection);
|
||||||
|
|
||||||
match &mut self.focus {
|
match &mut self.focus {
|
||||||
Focus::FloatingWindow(float) => float.draw(frame, chunks[1]),
|
Focus::FloatingWindow(float) => float.draw(frame, chunks[1], &self.theme),
|
||||||
Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1]),
|
Focus::ConfirmationPrompt(prompt) => prompt.draw(frame, chunks[1], &self.theme),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -798,7 +798,7 @@ impl AppState {
|
||||||
.map(|node| node.name.as_str())
|
.map(|node| node.name.as_str())
|
||||||
.collect::<Vec<_>>();
|
.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));
|
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,88 +14,70 @@ pub enum Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
pub fn dir_color(&self) -> Color {
|
fn get_color_variant(&self, default: Color, compatible: Color) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Theme::Default => Color::Blue,
|
Theme::Default => default,
|
||||||
Theme::Compatible => Color::Blue,
|
Theme::Compatible => compatible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_icon_variant(&self, default: &'static str, compatible: &'static str) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Theme::Default => default,
|
||||||
|
Theme::Compatible => compatible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dir_color(&self) -> Color {
|
||||||
|
self.get_color_variant(Color::Blue, Color::Blue)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cmd_color(&self) -> Color {
|
pub fn cmd_color(&self) -> Color {
|
||||||
match self {
|
self.get_color_variant(Color::Rgb(204, 224, 208), Color::LightGreen)
|
||||||
Theme::Default => Color::Rgb(204, 224, 208),
|
|
||||||
Theme::Compatible => Color::LightGreen,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multi_select_disabled_color(&self) -> Color {
|
pub fn multi_select_disabled_color(&self) -> Color {
|
||||||
match self {
|
self.get_color_variant(Color::DarkGray, Color::DarkGray)
|
||||||
Theme::Default => Color::DarkGray,
|
|
||||||
Theme::Compatible => Color::DarkGray,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab_color(&self) -> Color {
|
pub fn tab_color(&self) -> Color {
|
||||||
match self {
|
self.get_color_variant(Color::Rgb(255, 255, 85), Color::Yellow)
|
||||||
Theme::Default => Color::Rgb(255, 255, 85),
|
|
||||||
Theme::Compatible => Color::Yellow,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dir_icon(&self) -> &'static str {
|
pub fn dir_icon(&self) -> &'static str {
|
||||||
match self {
|
self.get_icon_variant(" ", "[DIR]")
|
||||||
Theme::Default => " ",
|
|
||||||
Theme::Compatible => "[DIR]",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cmd_icon(&self) -> &'static str {
|
pub fn cmd_icon(&self) -> &'static str {
|
||||||
match self {
|
self.get_icon_variant(" ", "[CMD]")
|
||||||
Theme::Default => " ",
|
|
||||||
Theme::Compatible => "[CMD]",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab_icon(&self) -> &'static str {
|
pub fn tab_icon(&self) -> &'static str {
|
||||||
match self {
|
self.get_icon_variant(" ", ">> ")
|
||||||
Theme::Default => " ",
|
|
||||||
Theme::Compatible => ">> ",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn multi_select_icon(&self) -> &'static str {
|
pub fn multi_select_icon(&self) -> &'static str {
|
||||||
match self {
|
self.get_icon_variant("", "*")
|
||||||
Theme::Default => "",
|
|
||||||
Theme::Compatible => "*",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn success_color(&self) -> Color {
|
pub fn success_color(&self) -> Color {
|
||||||
match self {
|
self.get_color_variant(Color::Rgb(5, 255, 55), Color::Green)
|
||||||
Theme::Default => Color::Rgb(199, 55, 44),
|
|
||||||
Theme::Compatible => Color::Green,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fail_color(&self) -> Color {
|
pub fn fail_color(&self) -> Color {
|
||||||
match self {
|
self.get_color_variant(Color::Rgb(199, 55, 44), Color::Red)
|
||||||
Theme::Default => Color::Rgb(5, 255, 55),
|
|
||||||
Theme::Compatible => Color::Red,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focused_color(&self) -> Color {
|
pub fn focused_color(&self) -> Color {
|
||||||
match self {
|
self.get_color_variant(Color::LightBlue, Color::LightBlue)
|
||||||
Theme::Default => Color::LightBlue,
|
|
||||||
Theme::Compatible => Color::LightBlue,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn search_preview_color(&self) -> Color {
|
||||||
|
self.get_color_variant(Color::DarkGray, Color::DarkGray)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfocused_color(&self) -> Color {
|
pub fn unfocused_color(&self) -> Color {
|
||||||
match self {
|
self.get_color_variant(Color::Gray, Color::Gray)
|
||||||
Theme::Default => Color::Gray,
|
|
||||||
Theme::Compatible => Color::Gray,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user