\fR
Set the theme to use in the TUI.
@@ -32,10 +36,18 @@ Possible values:
.br
Defaults to \fIdefault\fR.
+.TP
+\fB\-y\fR, \fB\-\-skip\-confirmation\fR
+Skip confirmation prompt before executing commands.
+
.TP
\fB\-\-override\-validation\fR
Show all available entries, disregarding compatibility checks. (\fBUNSAFE\fR)
+.TP
+\fB\-\-size\-bypass\fR
+Bypass the terminal size limit
+
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help.
diff --git a/overrides/main.html b/overrides/main.html
deleted file mode 100644
index 0cebb4ac..00000000
--- a/overrides/main.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% extends "base.html" %}
-
-{% block header %}
- {{ super() }}
-
- Announcement: This documentation is still in progress.
-
-{% endblock %}
-
-{% block footer %}
- {# Empty block to override the footer #}
-{% endblock %}
\ No newline at end of file
diff --git a/start.sh b/start.sh
index 3c412fb6..71e87583 100755
--- a/start.sh
+++ b/start.sh
@@ -43,7 +43,7 @@ check $? "Downloading linutil"
chmod +x "$temp_file"
check $? "Making linutil executable"
-"$temp_file"
+"$temp_file" "$@"
check $? "Executing linutil"
rm -f "$temp_file"
diff --git a/startdev.sh b/startdev.sh
index 5aad12ad..8e27bbf0 100755
--- a/startdev.sh
+++ b/startdev.sh
@@ -69,7 +69,7 @@ check $? "Downloading linutil"
chmod +x "$TMPFILE"
check $? "Making linutil executable"
-"$TMPFILE"
+"$TMPFILE" "$@"
check $? "Executing linutil"
rm -f "$TMPFILE"
diff --git a/tui/Cargo.toml b/tui/Cargo.toml
index 337dc5c7..4051d3b3 100644
--- a/tui/Cargo.toml
+++ b/tui/Cargo.toml
@@ -15,18 +15,18 @@ tips = ["rand"]
[dependencies]
clap = { version = "4.5.20", features = ["derive"] }
-crossterm = "0.28.1"
-ego-tree = { workspace = true }
oneshot = "0.1.8"
portable-pty = "0.8.1"
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"
tree-sitter-bash = "0.23.1"
+textwrap = "0.16.1"
anstyle = "1.0.8"
ansi-to-tui = "7.0.0"
zips = "0.1.7"
diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs
index d883fd2f..96ab06ca 100644
--- a/tui/src/confirmation.rs
+++ b/tui/src/confirmation.rs
@@ -2,8 +2,8 @@ use std::borrow::Cow;
use crate::{float::FloatContent, hint::Shortcut};
-use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
+ crossterm::event::{KeyCode, KeyEvent},
layout::Alignment,
prelude::*,
widgets::{Block, Borders, Clear, List},
@@ -60,6 +60,7 @@ impl FloatContent for ConfirmPrompt {
fn draw(&mut self, frame: &mut Frame, area: Rect) {
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_alignment(Alignment::Center)
diff --git a/tui/src/filter.rs b/tui/src/filter.rs
index 898fee74..f44e89a1 100644
--- a/tui/src/filter.rs
+++ b/tui/src/filter.rs
@@ -1,8 +1,7 @@
use crate::{state::ListEntry, theme::Theme};
-use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
-use ego_tree::NodeId;
-use linutil_core::Tab;
+use linutil_core::{ego_tree::NodeId, Tab};
use ratatui::{
+ crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
layout::{Position, Rect},
style::{Color, Style},
text::Span,
@@ -123,7 +122,12 @@ impl Filter {
//Create the search bar widget
let search_bar = Paragraph::new(display_text)
- .block(Block::default().borders(Borders::ALL).title(" Search "))
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_set(ratatui::symbols::border::ROUNDED)
+ .title(" Search "),
+ )
.style(Style::default().fg(search_color));
//Render the search bar (First chunk of the screen)
diff --git a/tui/src/float.rs b/tui/src/float.rs
index 7b569752..993684b0 100644
--- a/tui/src/float.rs
+++ b/tui/src/float.rs
@@ -1,5 +1,5 @@
-use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
+ crossterm::event::{KeyCode, KeyEvent},
layout::{Constraint, Direction, Layout, Rect},
Frame,
};
diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs
index be22958c..c307b854 100644
--- a/tui/src/floating_text.rs
+++ b/tui/src/floating_text.rs
@@ -8,9 +8,8 @@ use crate::{float::FloatContent, hint::Shortcut};
use linutil_core::Command;
-use crossterm::event::{KeyCode, KeyEvent};
-
use ratatui::{
+ crossterm::event::{KeyCode, KeyEvent},
layout::Rect,
style::{Style, Stylize},
text::Line,
@@ -20,16 +19,19 @@ use ratatui::{
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: Vec,
+ 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,
}
@@ -108,12 +110,6 @@ fn get_highlighted_string(s: &str) -> Option {
Some(output)
}
-macro_rules! max_width {
- ($($lines:tt)+) => {{
- $($lines)+.iter().fold(0, |accum, val| accum.max(val.len()))
- }}
-}
-
#[inline]
fn get_lines(s: &str) -> Vec<&str> {
s.lines().collect::>()
@@ -125,57 +121,56 @@ fn get_lines_owned(s: &str) -> Vec {
}
impl FloatingText {
- pub fn new(text: String, title: &str) -> Self {
- let src = get_lines(&text)
- .into_iter()
- .map(|s| s.to_string())
- .collect::>();
+ 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)
+ };
- let max_line_width = max_width!(src);
Self {
- src,
+ src: text,
+ wrapped_lines,
mode_title: title.to_string(),
max_line_width,
v_scroll: 0,
h_scroll: 0,
+ wrap_words,
frame_height: 0,
}
}
pub fn from_command(command: &Command, title: String) -> Option {
- let (max_line_width, src) = match command {
- Command::Raw(cmd) => {
- // just apply highlights directly
- (max_width!(get_lines(cmd)), Some(cmd.clone()))
- }
- Command::LocalFile { file, .. } => {
- // have to read from tmp dir to get cmd src
- let raw = std::fs::read_to_string(file)
- .map_err(|_| format!("File not found: {:?}", file))
- .unwrap();
+ 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,
+ }?;
- (max_width!(get_lines(&raw)), Some(raw))
- }
-
- // If command is a folder, we don't display a preview
- Command::None => (0usize, None),
- };
-
- let src = get_lines_owned(&get_highlighted_string(&src?)?);
+ 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,
})
}
fn scroll_down(&mut self) {
let visible_lines = self.frame_height.saturating_sub(2);
- if self.v_scroll + visible_lines < self.src.len() {
+ if self.v_scroll + visible_lines < self.wrapped_lines.len() {
self.v_scroll += 1;
}
}
@@ -197,6 +192,20 @@ impl FloatingText {
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()))
+ };
+ }
+ }
}
impl FloatContent for FloatingText {
@@ -206,6 +215,7 @@ impl FloatContent for FloatingText {
// 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_alignment(ratatui::layout::Alignment::Center)
.title_style(Style::default().reversed())
@@ -217,13 +227,22 @@ impl FloatContent for FloatingText {
// Calculate the inner area to ensure text is not drawn over the border
let inner_area = block.inner(area);
- let Rect { height, .. } = inner_area;
+ let Rect { width, height, .. } = inner_area;
+
+ self.update_wrapping(width as usize);
+
let lines = self
- .src
+ .wrapped_lines
.iter()
.skip(self.v_scroll)
.take(height as usize)
- .flat_map(|l| l.into_text().unwrap())
+ .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
diff --git a/tui/src/hint.rs b/tui/src/hint.rs
index b5f096ca..82c265c8 100644
--- a/tui/src/hint.rs
+++ b/tui/src/hint.rs
@@ -71,8 +71,8 @@ impl Shortcut {
}
fn to_spans(&self) -> Vec> {
- let mut ret: Vec<_> = self
- .key_sequences
+ let description = Span::styled(self.desc, Style::default().italic());
+ self.key_sequences
.iter()
.flat_map(|seq| {
[
@@ -81,8 +81,7 @@ impl Shortcut {
Span::default().content("] "),
]
})
- .collect();
- ret.push(Span::styled(self.desc, Style::default().italic()));
- ret
+ .chain(std::iter::once(description))
+ .collect()
}
}
diff --git a/tui/src/main.rs b/tui/src/main.rs
index 801e3b1d..7a9f4067 100644
--- a/tui/src/main.rs
+++ b/tui/src/main.rs
@@ -9,36 +9,58 @@ mod theme;
use std::{
io::{self, stdout},
+ path::PathBuf,
time::Duration,
};
use crate::theme::Theme;
use clap::Parser;
-use crossterm::{
- event::{self, DisableMouseCapture, Event, KeyEventKind},
- style::ResetColor,
- terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
- ExecutableCommand,
+
+use ratatui::{
+ backend::CrosstermBackend,
+ crossterm::{
+ event::{self, DisableMouseCapture, Event, KeyEventKind},
+ style::ResetColor,
+ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+ ExecutableCommand,
+ },
+ Terminal,
};
-use ratatui::{backend::CrosstermBackend, Terminal};
use state::AppState;
// Linux utility toolbox
#[derive(Debug, Parser)]
struct Args {
+ #[arg(short, long, help = "Path to the configuration file")]
+ config: Option,
#[arg(short, long, value_enum)]
#[arg(default_value_t = Theme::Default)]
#[arg(help = "Set the theme to use in the application")]
theme: Theme,
+ #[arg(
+ short = 'y',
+ long,
+ help = "Skip confirmation prompt before executing commands"
+ )]
+ skip_confirmation: bool,
#[arg(long, default_value_t = false)]
#[clap(help = "Show all available options, disregarding compatibility checks (UNSAFE)")]
override_validation: bool,
+ #[arg(long, default_value_t = false)]
+ #[clap(help = "Bypass the terminal size limit")]
+ size_bypass: bool,
}
fn main() -> io::Result<()> {
let args = Args::parse();
- let mut state = AppState::new(args.theme, args.override_validation);
+ let mut state = AppState::new(
+ args.config,
+ args.theme,
+ args.override_validation,
+ args.size_bypass,
+ args.skip_confirmation,
+ );
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs
index f2471778..779a0b3c 100644
--- a/tui/src/running_command.rs
+++ b/tui/src/running_command.rs
@@ -1,11 +1,11 @@
use crate::{float::FloatContent, hint::Shortcut};
-use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use linutil_core::Command;
use oneshot::{channel, Receiver};
use portable_pty::{
ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem,
};
use ratatui::{
+ crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
layout::{Rect, Size},
style::{Color, Style, Stylize},
text::{Line, Span},
@@ -17,11 +17,11 @@ use std::{
sync::{Arc, Mutex},
thread::JoinHandle,
};
+use time::{macros::format_description, OffsetDateTime};
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>>,
@@ -37,6 +37,7 @@ pub struct RunningCommand {
writer: Box,
/// Only set after the process has ended
status: Option,
+ log_path: Option,
scroll_offset: usize,
}
@@ -53,6 +54,7 @@ impl FloatContent for RunningCommand {
// Display a block indicating the command is running
Block::default()
.borders(Borders::ALL)
+ .border_set(ratatui::symbols::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"))
@@ -78,9 +80,20 @@ impl FloatContent for RunningCommand {
.style(Style::default()),
);
- Block::default()
+ let mut block = Block::default()
.borders(Borders::ALL)
- .title_top(title_line.centered())
+ .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
};
// Process the buffer and create the pseudo-terminal widget
@@ -109,6 +122,11 @@ impl FloatContent for RunningCommand {
KeyCode::PageDown => {
self.scroll_offset = self.scroll_offset.saturating_sub(10);
}
+ KeyCode::Char('l') if self.is_finished() => {
+ if let Ok(log_path) = self.save_log() {
+ self.log_path = Some(log_path);
+ }
+ }
// Pass other key events to the terminal
_ => self.handle_passthrough_key_event(key),
}
@@ -132,6 +150,7 @@ impl FloatContent for RunningCommand {
Shortcut::new("Close window", ["Enter", "q"]),
Shortcut::new("Scroll up", ["Page up"]),
Shortcut::new("Scroll down", ["Page down"]),
+ Shortcut::new("Save log", ["l"]),
]),
)
} else {
@@ -235,6 +254,7 @@ impl RunningCommand {
pty_master: pair.master,
writer,
status: None,
+ log_path: None,
scroll_offset: 0,
}
}
@@ -253,7 +273,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, 200);
+ let mut parser = vt100::Parser::new(size.height, size.width, 1000);
let mutex = self.buffer.lock();
let buffer = mutex.as_ref().unwrap();
parser.process(buffer);
@@ -282,6 +302,24 @@ impl RunningCommand {
}
}
+ fn save_log(&self) -> std::io::Result {
+ let mut log_path = std::env::temp_dir();
+ let date_format = format_description!("[year]-[month]-[day]-[hour]-[minute]-[second]");
+ log_path.push(format!(
+ "linutil_log_{}.log",
+ OffsetDateTime::now_local()
+ .unwrap_or(OffsetDateTime::now_utc())
+ .format(&date_format)
+ .unwrap()
+ ));
+
+ let mut file = std::fs::File::create(&log_path)?;
+ let buffer = self.buffer.lock().unwrap();
+ file.write_all(&buffer)?;
+
+ Ok(log_path.to_string_lossy().into_owned())
+ }
+
/// Convert the KeyEvent to pty key codes, and send them to the virtual terminal
fn handle_passthrough_key_event(&mut self, key: &KeyEvent) {
let input_bytes = match key.code {
diff --git a/tui/src/state.rs b/tui/src/state.rs
index a07bf178..5ee34079 100644
--- a/tui/src/state.rs
+++ b/tui/src/state.rs
@@ -7,20 +7,20 @@ use crate::{
running_command::RunningCommand,
theme::Theme,
};
-use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
-use ego_tree::NodeId;
-use linutil_core::{ListNode, Tab};
+
+use linutil_core::{ego_tree::NodeId, Config, ListNode, TabList};
#[cfg(feature = "tips")]
use rand::Rng;
use ratatui::{
+ crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
layout::{Alignment, Constraint, Direction, Flex, Layout},
style::{Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListState, Paragraph},
Frame,
};
+use std::path::PathBuf;
use std::rc::Rc;
-use temp_dir::TempDir;
const MIN_WIDTH: u16 = 100;
const MIN_HEIGHT: u16 = 25;
@@ -31,6 +31,7 @@ D - disk modifications (ex. partitioning) (privileged)
FI - flatpak installation
FM - file modification
I - installation (privileged)
+K - kernel modifications (privileged)
MP - package manager actions
SI - full system installation
SS - systemd actions (privileged)
@@ -40,14 +41,12 @@ P* - privileged *
";
pub struct AppState {
- /// This must be passed to retain the temp dir until the end of the program
- _temp_dir: TempDir,
/// Selected theme
theme: Theme,
/// Currently focused area
pub focus: Focus,
/// List of tabs
- tabs: Vec,
+ tabs: TabList,
/// Current tab
current_tab: ListState,
/// This stack keeps track of our "current directory". You can think of it as `pwd`. but not
@@ -62,6 +61,8 @@ pub struct AppState {
drawable: bool,
#[cfg(feature = "tips")]
tip: String,
+ size_bypass: bool,
+ skip_confirmation: bool,
}
pub enum Focus {
@@ -86,12 +87,19 @@ enum SelectedItem {
}
impl AppState {
- pub fn new(theme: Theme, override_validation: bool) -> Self {
- let (temp_dir, tabs) = linutil_core::get_tabs(!override_validation);
+ 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);
let root_id = tabs[0].tree.root().id();
+ let auto_execute_commands = config_path.map(|path| Config::from_file(&path).auto_execute);
+
let mut state = Self {
- _temp_dir: temp_dir,
theme,
focus: Focus::List,
tabs,
@@ -104,12 +112,36 @@ impl AppState {
drawable: false,
#[cfg(feature = "tips")]
tip: get_random_tip(),
+ size_bypass,
+ skip_confirmation,
};
state.update_items();
+ if let Some(auto_execute_commands) = auto_execute_commands {
+ state.handle_initial_auto_execute(&auto_execute_commands);
+ }
+
state
}
+ 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)))
+ .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));
+ }
+ }
+
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"])])
@@ -153,12 +185,10 @@ impl AppState {
hints.push(Shortcut::new("Select item below", ["j", "Down"]));
hints.push(Shortcut::new("Next theme", ["t"]));
hints.push(Shortcut::new("Previous theme", ["T"]));
-
- if self.is_current_tab_multi_selectable() {
- hints.push(Shortcut::new("Toggle multi-selection mode", ["v"]));
+ hints.push(Shortcut::new("Multi-selection mode", ["v"]));
+ if self.multi_select {
hints.push(Shortcut::new("Select multiple commands", ["Space"]));
}
-
hints.push(Shortcut::new("Next tab", ["Tab"]));
hints.push(Shortcut::new("Previous tab", ["Shift-Tab"]));
hints.push(Shortcut::new("Important actions guide", ["g"]));
@@ -188,7 +218,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 !self.size_bypass
+ && (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH)
+ {
let warning = Paragraph::new(format!(
"Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}",
terminal_size.width,
@@ -215,19 +247,19 @@ impl AppState {
self.drawable = true;
}
- let label_block =
- Block::default()
- .borders(Borders::all())
- .border_set(ratatui::symbols::border::Set {
- top_left: " ",
- top_right: " ",
- bottom_left: " ",
- bottom_right: " ",
- vertical_left: " ",
- vertical_right: " ",
- horizontal_top: "*",
- horizontal_bottom: "*",
- });
+ 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 = Paragraph::new(Line::from(vec![
@@ -251,7 +283,8 @@ impl AppState {
let keybinds_block = Block::default()
.title(format!(" {} ", keybind_scope))
- .borders(Borders::all());
+ .borders(Borders::ALL)
+ .border_set(ratatui::symbols::border::ROUNDED);
let keybinds = create_shortcut_list(shortcuts, keybind_render_width);
let n_lines = keybinds.len() as u16;
@@ -295,7 +328,11 @@ impl AppState {
};
let list = List::new(tabs)
- .block(Block::default().borders(Borders::ALL))
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_set(ratatui::symbols::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);
@@ -330,7 +367,12 @@ impl AppState {
let (indicator, style) = if is_selected {
(self.theme.multi_select_icon(), Style::default().bold())
} else {
- ("", Style::new())
+ let ms_style = if self.multi_select && !node.multi_select {
+ Style::default().fg(self.theme.multi_select_disabled_color())
+ } else {
+ Style::new()
+ };
+ ("", ms_style)
};
if *has_children {
Line::from(format!(
@@ -340,6 +382,7 @@ impl AppState {
indicator
))
.style(self.theme.dir_color())
+ .patch_style(style)
} else {
Line::from(format!(
"{} {} {}",
@@ -357,13 +400,21 @@ impl AppState {
|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())
+ 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)
}
},
));
@@ -393,6 +444,7 @@ impl AppState {
.block(
Block::default()
.borders(Borders::ALL & !Borders::RIGHT)
+ .border_set(ratatui::symbols::border::ROUNDED)
.title(title)
.title_bottom(bottom_title),
)
@@ -402,6 +454,7 @@ impl AppState {
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),
);
@@ -479,6 +532,13 @@ impl AppState {
// enabled, need to clear it to prevent state corruption
if !self.multi_select {
self.selected_commands.clear()
+ } else {
+ // Prevents non multi_selectable cmd from being pushed into the selected list
+ if let Some(node) = self.get_selected_node() {
+ if !node.multi_select {
+ self.selected_commands.retain(|cmd| cmd.name != node.name);
+ }
+ }
}
}
ConfirmStatus::Confirm => self.handle_confirm_command(),
@@ -556,41 +616,31 @@ impl AppState {
}
fn toggle_multi_select(&mut self) {
- if self.is_current_tab_multi_selectable() {
- self.multi_select = !self.multi_select;
- if !self.multi_select {
- self.selected_commands.clear();
- }
+ self.multi_select = !self.multi_select;
+ if !self.multi_select {
+ self.selected_commands.clear();
}
}
fn toggle_selection(&mut self) {
- if let Some(command) = self.get_selected_node() {
- if self.selected_commands.contains(&command) {
- self.selected_commands.retain(|c| c != &command);
- } else {
- self.selected_commands.push(command);
+ if let Some(node) = self.get_selected_node() {
+ if node.multi_select {
+ if self.selected_commands.contains(&node) {
+ self.selected_commands.retain(|c| c != &node);
+ } else {
+ self.selected_commands.push(node);
+ }
}
}
}
- pub fn is_current_tab_multi_selectable(&self) -> bool {
- let index = self.current_tab.selected().unwrap_or(0);
- self.tabs
- .get(index)
- .map_or(false, |tab| tab.multi_selectable)
- }
-
fn update_items(&mut self) {
self.filter.update_items(
&self.tabs,
self.current_tab.selected().unwrap(),
self.visit_stack.last().unwrap().0,
);
- if !self.is_current_tab_multi_selectable() {
- self.multi_select = false;
- self.selected_commands.clear();
- }
+
let len = self.filter.item_list().len();
if len > 0 {
let current = self.selection.selected().unwrap_or(0);
@@ -709,7 +759,8 @@ impl AppState {
fn enable_description(&mut self) {
if let Some(command_description) = self.get_selected_description() {
if !command_description.is_empty() {
- let description = FloatingText::new(command_description, "Command Description");
+ let description =
+ FloatingText::new(command_description, "Command Description", true);
self.spawn_float(description, 80, 80);
}
}
@@ -738,14 +789,18 @@ impl AppState {
}
}
- let cmd_names = self
- .selected_commands
- .iter()
- .map(|node| node.name.as_str())
- .collect::>();
+ 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));
+ let prompt = ConfirmPrompt::new(&cmd_names[..]);
+ self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
+ }
}
SelectedItem::None => {}
}
@@ -795,7 +850,7 @@ impl AppState {
fn toggle_task_list_guide(&mut self) {
self.spawn_float(
- FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide"),
+ FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide", true),
80,
80,
);
diff --git a/tui/src/theme.rs b/tui/src/theme.rs
index 8337645a..d87e87ee 100644
--- a/tui/src/theme.rs
+++ b/tui/src/theme.rs
@@ -28,6 +28,13 @@ impl Theme {
}
}
+ pub fn multi_select_disabled_color(&self) -> Color {
+ match self {
+ Theme::Default => Color::DarkGray,
+ Theme::Compatible => Color::DarkGray,
+ }
+ }
+
pub fn tab_color(&self) -> Color {
match self {
Theme::Default => Color::Rgb(255, 255, 85),
diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs
index 992b83aa..385c1134 100644
--- a/xtask/src/docgen.rs
+++ b/xtask/src/docgen.rs
@@ -11,7 +11,7 @@ pub fn userguide() -> Result {
let mut md = String::new();
md.push_str("\n# Walkthrough\n");
- let tabs = linutil_core::get_tabs(false).1;
+ let tabs = linutil_core::get_tabs(false);
for tab in tabs {
#[cfg(debug_assertions)]
@@ -24,7 +24,7 @@ pub fn userguide() -> Result {
#[cfg(debug_assertions)]
println!(" Directory: {}", entry.name);
- if entry.name != "root".to_string() {
+ if entry.name != "root" {
md.push_str(&format!("\n### {}\n\n", entry.name));
}
@@ -36,18 +36,16 @@ pub fn userguide() -> Result {
current_dir
));
} */ // Commenting this for now, might be a good idea later
- } else {
- if !entry.description.is_empty() {
- #[cfg(debug_assertions)]
- println!(" Entry: {}", entry.name);
- #[cfg(debug_assertions)]
- println!(" Description: {}", entry.description);
+ } else if !entry.description.is_empty() {
+ #[cfg(debug_assertions)]
+ println!(" Entry: {}", entry.name);
+ #[cfg(debug_assertions)]
+ println!(" Description: {}", entry.description);
- md.push_str(&format!("- **{}**: {}\n", entry.name, entry.description));
- } /* else {
- md.push_str(&format!("- **{}**\n", entry.name));
- } */ // https://github.com/ChrisTitusTech/linutil/pull/753
- }
+ md.push_str(&format!("- **{}**: {}\n", entry.name, entry.description));
+ } /* else {
+ md.push_str(&format!("- **{}**\n", entry.name));
+ } */ // https://github.com/ChrisTitusTech/linutil/pull/753
}
}