\fR
Set the theme to use in the TUI.
@@ -36,18 +48,6 @@ 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/tui/Cargo.toml b/tui/Cargo.toml
index e01af32f..5094b815 100644
--- a/tui/Cargo.toml
+++ b/tui/Cargo.toml
@@ -14,18 +14,23 @@ default = ["tips"]
tips = ["rand"]
[dependencies]
-clap = { version = "4.5.20", features = ["derive"] }
-oneshot = "0.1.8"
+clap = { version = "4.5.20", features = ["derive", "std"], default-features = false }
+oneshot = { version = "0.1.8", features = ["std"], default-features = false }
portable-pty = "0.8.1"
-ratatui = "0.29.0"
+ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false }
tui-term = "0.2.0"
temp-dir = "0.1.14"
-time = { version = "0.3.36", features = ["local-offset", "macros", "formatting"] }
+time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false }
+unicode-width = { version = "0.2.0", default-features = false }
rand = { version = "0.8.5", optional = true }
-linutil_core = { path = "../core", version = "24.9.28" }
+linutil_core = { version = "24.9.28", path = "../core" }
tree-sitter-highlight = "0.24.3"
tree-sitter-bash = "0.23.1"
-unicode-width = "0.2.0"
+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]]
name = "linutil"
diff --git a/tui/src/confirmation.rs b/tui/src/confirmation.rs
index 1993c3f5..f9ae42ad 100644
--- a/tui/src/confirmation.rs
+++ b/tui/src/confirmation.rs
@@ -1,6 +1,6 @@
use crate::{float::FloatContent, hint::Shortcut, theme};
use ratatui::{
- crossterm::event::{KeyCode, KeyEvent},
+ crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
layout::Alignment,
prelude::*,
widgets::{Block, Borders, Clear, List},
@@ -86,6 +86,19 @@ impl FloatContent for ConfirmPrompt {
frame.render_widget(List::new(paths_text), inner_area);
}
+ fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
+ match event.kind {
+ MouseEventKind::ScrollDown => {
+ self.scroll_down();
+ }
+ MouseEventKind::ScrollUp => {
+ self.scroll_up();
+ }
+ _ => {}
+ }
+ false
+ }
+
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
use ConfirmStatus::*;
use KeyCode::{Char, Down, Esc, Up};
diff --git a/tui/src/float.rs b/tui/src/float.rs
index ab9394a5..d9c8d6de 100644
--- a/tui/src/float.rs
+++ b/tui/src/float.rs
@@ -1,6 +1,6 @@
use crate::{hint::Shortcut, theme::Theme};
use ratatui::{
- crossterm::event::{KeyCode, KeyEvent},
+ crossterm::event::{KeyCode, KeyEvent, MouseEvent},
layout::{Constraint, Direction, Layout, Rect},
Frame,
};
@@ -8,6 +8,7 @@ use ratatui::{
pub trait FloatContent {
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;
fn get_shortcut_list(&self) -> (&str, Box<[Shortcut]>);
}
@@ -52,6 +53,10 @@ impl Float {
self.content.draw(frame, popup_area, theme);
}
+ pub fn handle_mouse_event(&mut self, event: &MouseEvent) {
+ self.content.handle_mouse_event(event);
+ }
+
// Returns true if the floating window is finished.
pub fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
match key.code {
diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs
index b583df9c..18074ddb 100644
--- a/tui/src/floating_text.rs
+++ b/tui/src/floating_text.rs
@@ -1,7 +1,7 @@
use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
use linutil_core::Command;
use ratatui::{
- crossterm::event::{KeyCode, KeyEvent},
+ crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
layout::Rect,
style::{Color, Style, Stylize},
text::{Line, Span, Text},
@@ -200,6 +200,17 @@ impl<'a> FloatContent for FloatingText<'a> {
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 {
diff --git a/tui/src/main.rs b/tui/src/main.rs
index f5d2b7cd..536b10c0 100644
--- a/tui/src/main.rs
+++ b/tui/src/main.rs
@@ -3,6 +3,7 @@ mod filter;
mod float;
mod floating_text;
mod hint;
+mod root;
mod running_command;
pub mod state;
mod theme;
@@ -12,7 +13,7 @@ use clap::Parser;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
- event::{self, DisableMouseCapture, Event, KeyEventKind},
+ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind},
style::ResetColor,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
@@ -61,6 +62,8 @@ fn main() -> io::Result<()> {
);
stdout().execute(EnterAlternateScreen)?;
+ stdout().execute(EnableMouseCapture)?;
+
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
@@ -90,15 +93,22 @@ fn run(
// It's guaranteed that the `read()` won't block when the `poll()`
// function returns `true`
- if let Event::Key(key) = event::read()? {
- // We are only interested in Press and Repeat events
- if key.kind != KeyEventKind::Press && key.kind != KeyEventKind::Repeat {
- continue;
- }
+ match event::read()? {
+ Event::Key(key) => {
+ if key.kind != KeyEventKind::Press && key.kind != KeyEventKind::Repeat {
+ continue;
+ }
- if !state.handle_key(&key) {
- return Ok(());
+ if !state.handle_key(&key) {
+ return Ok(());
+ }
}
+ Event::Mouse(mouse_event) => {
+ if !state.handle_mouse(&mouse_event) {
+ return Ok(());
+ }
+ }
+ _ => {}
}
}
}
diff --git a/tui/src/root.rs b/tui/src/root.rs
new file mode 100644
index 00000000..1b02b938
--- /dev/null
+++ b/tui/src/root.rs
@@ -0,0 +1,17 @@
+use crate::floating_text::FloatingText;
+
+#[cfg(unix)]
+use nix::unistd::Uid;
+
+const ROOT_WARNING: &str = "WARNING: You are running this utility as root!\n
+This means you have full system access and commands can potentially damage your system if used incorrectly.\n
+Please proceed with caution and make sure you understand what each script does before executing it.";
+
+#[cfg(unix)]
+pub fn check_root_status() -> Option {
+ (Uid::effective().is_root()).then_some(FloatingText::new(
+ ROOT_WARNING.into(),
+ "Root User Warning",
+ true,
+ ))
+}
diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs
index 368ed6ca..e675c7b4 100644
--- a/tui/src/running_command.rs
+++ b/tui/src/running_command.rs
@@ -5,7 +5,7 @@ use portable_pty::{
ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem,
};
use ratatui::{
- crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
+ crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind},
layout::{Rect, Size},
style::{Style, Stylize},
text::{Line, Span},
@@ -105,6 +105,18 @@ impl FloatContent for RunningCommand {
frame.render_widget(pseudo_term, area);
}
+ fn handle_mouse_event(&mut self, event: &MouseEvent) -> bool {
+ match event.kind {
+ MouseEventKind::ScrollUp => {
+ self.scroll_offset = self.scroll_offset.saturating_add(1);
+ }
+ MouseEventKind::ScrollDown => {
+ self.scroll_offset = self.scroll_offset.saturating_sub(1);
+ }
+ _ => {}
+ }
+ true
+ }
/// Handle key events of the running command "window". Returns true when the "window" should be
/// closed
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
diff --git a/tui/src/state.rs b/tui/src/state.rs
index 692ef273..d767d81b 100644
--- a/tui/src/state.rs
+++ b/tui/src/state.rs
@@ -4,6 +4,7 @@ use crate::{
float::{Float, FloatContent},
floating_text::FloatingText,
hint::{create_shortcut_list, Shortcut},
+ root::check_root_status,
running_command::RunningCommand,
theme::Theme,
};
@@ -12,8 +13,8 @@ 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},
+ crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind},
+ layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect},
style::{Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListState, Paragraph},
@@ -41,6 +42,8 @@ P* - privileged *
";
pub struct AppState {
+ /// Areas of tabs
+ areas: Option,
/// Selected theme
theme: Theme,
/// Currently focused area
@@ -79,6 +82,11 @@ pub struct ListEntry {
pub has_children: bool,
}
+pub struct Areas {
+ tab_list: Rect,
+ list: Rect,
+}
+
enum SelectedItem {
UpDir,
Directory,
@@ -100,6 +108,7 @@ impl AppState {
let auto_execute_commands = config_path.map(|path| Config::from_file(&path).auto_execute);
let mut state = Self {
+ areas: None,
theme,
focus: Focus::List,
tabs,
@@ -116,6 +125,11 @@ impl AppState {
skip_confirmation,
};
+ #[cfg(unix)]
+ if let Some(root_warning) = check_root_status() {
+ state.spawn_float(root_warning, 60, 40);
+ }
+
state.update_items();
if let Some(auto_execute_commands) = auto_execute_commands {
state.handle_initial_auto_execute(&auto_execute_commands);
@@ -315,6 +329,11 @@ impl AppState {
.split(horizontal[0]);
frame.render_widget(label, left_chunks[0]);
+ self.areas = Some(Areas {
+ tab_list: left_chunks[1],
+ list: horizontal[1],
+ });
+
let tabs = self
.tabs
.iter()
@@ -469,6 +488,59 @@ impl AppState {
frame.render_widget(keybind_para, vertical[1]);
}
+ pub fn handle_mouse(&mut self, event: &MouseEvent) -> bool {
+ if !self.drawable {
+ return true;
+ }
+
+ if matches!(self.focus, Focus::TabList | Focus::List) {
+ let position = Position::new(event.column, event.row);
+ let mouse_in_tab_list = self.areas.as_ref().unwrap().tab_list.contains(position);
+ let mouse_in_list = self.areas.as_ref().unwrap().list.contains(position);
+
+ match event.kind {
+ MouseEventKind::Moved => {
+ if mouse_in_list {
+ self.focus = Focus::List
+ } else if mouse_in_tab_list {
+ self.focus = Focus::TabList
+ }
+ }
+ MouseEventKind::ScrollDown => {
+ if mouse_in_tab_list {
+ if self.current_tab.selected().unwrap() != self.tabs.len() - 1 {
+ self.current_tab.select_next();
+ }
+ self.refresh_tab();
+ } else if mouse_in_list {
+ self.selection.select_next()
+ }
+ }
+ MouseEventKind::ScrollUp => {
+ if mouse_in_tab_list {
+ if self.current_tab.selected().unwrap() != 0 {
+ self.current_tab.select_previous();
+ }
+ self.refresh_tab();
+ } else if mouse_in_list {
+ self.selection.select_previous()
+ }
+ }
+ _ => {}
+ }
+ }
+ match &mut self.focus {
+ Focus::FloatingWindow(float) => {
+ float.content.handle_mouse_event(event);
+ }
+ Focus::ConfirmationPrompt(confirm) => {
+ confirm.content.handle_mouse_event(event);
+ }
+ _ => {}
+ }
+ true
+ }
+
pub fn handle_key(&mut self, key: &KeyEvent) -> bool {
// This should be defined first to allow closing
// the application even when not drawable ( If terminal is small )