Compare commits

...

4 Commits

Author SHA1 Message Date
Jeevitha Kannan K S
da38626119
Merge 85719251d3 into 7147ed93e9 2024-11-12 15:39:52 -06:00
Jeevitha Kannan K S
85719251d3
Update deps, remove unused temp-dir 2024-11-13 03:09:38 +05:30
Jeevitha Kannan K S
06f13dc5f9
Cleanup
Use prelude for ratatui imports. Use const for theme functions, add
missing hints
2024-11-13 02:30:16 +05:30
Jeevitha Kannan K S
79aae9eb24
Rendering optimizations and function refactors
Handle `find_command` inside state itself -> `get_command_by_name`. Move tips to a seperate file for modularity. Pass the whole args to state instead of seperate args. Use const for float and confirmation prompt float sizes. Add the `longest_tab_length` to appstate struct so that it will not be calculated for each frame render use static str instead String for tips. Use function for spawning confirmprompt. Merge command list and task items list rendering a single widget instead of two. Remove redundant keys in handle_key. Optimize scrolling logic. Rename `toggle_task_list_guide` -> `enable_task_list_guide`
2024-11-13 00:59:46 +05:30
16 changed files with 300 additions and 410 deletions

29
Cargo.lock generated
View File

@ -390,7 +390,6 @@ dependencies = [
"portable-pty", "portable-pty",
"rand", "rand",
"ratatui", "ratatui",
"temp-dir",
"time", "time",
"tree-sitter-bash", "tree-sitter-bash",
"tree-sitter-highlight", "tree-sitter-highlight",
@ -721,18 +720,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.210" version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.210" version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -884,9 +883,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.77" version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -997,9 +996,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter" name = "tree-sitter"
version = "0.24.3" version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9871f16d6cf5c4757dcf30d5d2172a2df6987c510c017bbb7abfb7f9aa24d06" checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4"
dependencies = [ dependencies = [
"cc", "cc",
"regex", "regex",
@ -1010,9 +1009,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-bash" name = "tree-sitter-bash"
version = "0.23.1" version = "0.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aa5e1c6bd02c0053f3f68edcf5d8866b38a8640584279e30fca88149ce14dda" checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e"
dependencies = [ dependencies = [
"cc", "cc",
"tree-sitter-language", "tree-sitter-language",
@ -1020,9 +1019,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-highlight" name = "tree-sitter-highlight"
version = "0.24.3" version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48859aa39513716018d81904220960f415dbb72e071234a721304d20bf245e4c" checksum = "7f0f856de10d70a6d14d66db3648f7410c131cd49e989a863f15dda9acae6044"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"regex", "regex",
@ -1134,9 +1133,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "which" name = "which"
version = "6.0.3" version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b"
dependencies = [ dependencies = [
"either", "either",
"home", "home",

View File

@ -10,7 +10,7 @@ include = ["src/*.rs", "Cargo.toml", "tabs/**"]
[dependencies] [dependencies]
include_dir = "0.7.4" include_dir = "0.7.4"
temp-dir = "0.1.14" temp-dir = "0.1.14"
serde = { version = "1.0.205", features = ["derive"], default-features = false } serde = { version = "1.0.215", features = ["derive"], default-features = false }
toml = { version = "0.8.19", features = ["parse"], default-features = false } toml = { version = "0.8.19", features = ["parse"], default-features = false }
which = "6.0.3" which = "7.0.0"
ego-tree = "0.9.0" ego-tree = "0.9.0"

View File

@ -1,6 +1,5 @@
use serde::Deserialize; use serde::Deserialize;
use std::path::Path; use std::{path::Path, process};
use std::process;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Config { pub struct Config {

View File

@ -1,3 +1,7 @@
use crate::{Command, ListNode, Tab};
use ego_tree::{NodeMut, Tree};
use include_dir::{include_dir, Dir};
use serde::Deserialize;
use std::{ use std::{
fs::File, fs::File,
io::{BufRead, BufReader, Read}, io::{BufRead, BufReader, Read},
@ -6,11 +10,6 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
}; };
use crate::{Command, ListNode, Tab};
use ego_tree::{NodeMut, Tree};
use include_dir::{include_dir, Dir};
use serde::Deserialize;
use temp_dir::TempDir; use temp_dir::TempDir;
const TAB_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/tabs"); const TAB_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/tabs");

View File

@ -36,16 +36,3 @@ pub struct ListNode {
pub task_list: String, pub task_list: String,
pub multi_select: bool, pub multi_select: bool,
} }
impl Tab {
pub fn find_command(&self, name: &str) -> Option<Rc<ListNode>> {
self.tree.root().descendants().find_map(|node| {
let value = node.value();
if value.name == name && !node.has_children() {
Some(value.clone())
} else {
None
}
})
}
}

View File

@ -19,13 +19,12 @@ oneshot = { version = "0.1.8", features = ["std"], default-features = false }
portable-pty = "0.8.1" portable-pty = "0.8.1"
ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false } ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false }
tui-term = "0.2.0" tui-term = "0.2.0"
temp-dir = "0.1.14"
time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false } time = { version = "0.3.36", features = ["formatting", "local-offset", "macros"], default-features = false }
unicode-width = { version = "0.2.0", default-features = false } unicode-width = { version = "0.2.0", default-features = false }
rand = { version = "0.8.5", optional = true } rand = { version = "0.8.5", optional = true }
linutil_core = { version = "24.9.28", path = "../core" } linutil_core = { version = "24.9.28", path = "../core" }
tree-sitter-highlight = "0.24.3" tree-sitter-highlight = "0.24.4"
tree-sitter-bash = "0.23.1" tree-sitter-bash = "0.23.3"
nix = { version = "0.29.0", features = [ "user" ] } nix = { version = "0.29.0", features = [ "user" ] }
[[bin]] [[bin]]

View File

@ -3,7 +3,8 @@ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
layout::Alignment, layout::Alignment,
prelude::*, prelude::*,
widgets::{Block, Borders, Clear, List}, symbols::border,
widgets::{Block, Clear, List},
}; };
use std::borrow::Cow; use std::borrow::Cow;
@ -22,10 +23,17 @@ pub struct ConfirmPrompt {
impl ConfirmPrompt { impl ConfirmPrompt {
pub fn new(names: &[&str]) -> Self { pub fn new(names: &[&str]) -> Self {
let max_count_str = format!("{}", names.len());
let names = names let names = names
.iter() .iter()
.zip(1..) .zip(1..)
.map(|(name, n)| format!(" {n}. {name}")) .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}")
})
.collect(); .collect();
Self { Self {
@ -51,17 +59,15 @@ impl ConfirmPrompt {
impl FloatContent for ConfirmPrompt { impl FloatContent for ConfirmPrompt {
fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &theme::Theme) { fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &theme::Theme) {
let block = Block::default() let block = Block::bordered()
.borders(Borders::ALL) .border_set(border::ROUNDED)
.border_set(ratatui::symbols::border::ROUNDED)
.title(" Confirm selections ") .title(" Confirm selections ")
.title_bottom(Line::from(vec![ .title_bottom(Line::from(vec![
Span::styled(" [", Style::default()), Span::raw(" ["),
Span::styled("y", Style::default().fg(theme.success_color())), Span::styled("y", Style::default().fg(theme.success_color())),
Span::styled("] to continue ", Style::default()), Span::raw("] to continue ["),
Span::styled("[", Style::default()),
Span::styled("n", Style::default().fg(theme.fail_color())), Span::styled("n", Style::default().fg(theme.fail_color())),
Span::styled("] to abort ", Style::default()), Span::raw("] to abort "),
])) ]))
.title_alignment(Alignment::Center) .title_alignment(Alignment::Center)
.title_style(Style::default().bold()) .title_style(Style::default().bold())

View File

@ -2,11 +2,9 @@ use crate::{state::ListEntry, theme::Theme};
use linutil_core::{ego_tree::NodeId, Tab}; use linutil_core::{ego_tree::NodeId, Tab};
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
layout::{Position, Rect}, prelude::*,
style::Style, symbols::border,
text::Span, widgets::{Block, Paragraph},
widgets::{Block, Borders, Paragraph},
Frame,
}; };
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
@ -118,9 +116,8 @@ impl Filter {
//Create the search bar widget //Create the search bar widget
let search_bar = Paragraph::new(display_text) let search_bar = Paragraph::new(display_text)
.block( .block(
Block::default() Block::bordered()
.borders(Borders::ALL) .border_set(border::ROUNDED)
.border_set(ratatui::symbols::border::ROUNDED)
.title(" Search "), .title(" Search "),
) )
.style(Style::default().fg(search_color)); .style(Style::default().fg(search_color));

View File

@ -1,7 +1,7 @@
use crate::{hint::Shortcut, theme::Theme}; use crate::{hint::Shortcut, theme::Theme};
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, MouseEvent}, crossterm::event::{KeyCode, KeyEvent, MouseEvent},
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Layout, Rect},
Frame, Frame,
}; };
@ -29,22 +29,18 @@ impl<Content: FloatContent + ?Sized> Float<Content> {
} }
fn floating_window(&self, size: Rect) -> Rect { fn floating_window(&self, size: Rect) -> Rect {
let hor_float = Layout::default() let hor_float = Layout::horizontal([
.constraints([
Constraint::Percentage((100 - self.width_percent) / 2), Constraint::Percentage((100 - self.width_percent) / 2),
Constraint::Percentage(self.width_percent), Constraint::Percentage(self.width_percent),
Constraint::Percentage((100 - self.width_percent) / 2), Constraint::Percentage((100 - self.width_percent) / 2),
]) ])
.direction(Direction::Horizontal)
.split(size)[1]; .split(size)[1];
Layout::default() Layout::vertical([
.constraints([
Constraint::Percentage((100 - self.height_percent) / 2), Constraint::Percentage((100 - self.height_percent) / 2),
Constraint::Percentage(self.height_percent), Constraint::Percentage(self.height_percent),
Constraint::Percentage((100 - self.height_percent) / 2), Constraint::Percentage((100 - self.height_percent) / 2),
]) ])
.direction(Direction::Vertical)
.split(hor_float)[1] .split(hor_float)[1]
} }

View File

@ -2,11 +2,9 @@ use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
use linutil_core::Command; use linutil_core::Command;
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}, crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
layout::Rect, prelude::*,
style::{Color, Style, Stylize}, symbols::border,
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, Paragraph, Wrap}, widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame,
}; };
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};
@ -177,9 +175,9 @@ impl<'a> FloatContent for FloatingText<'a> {
fn draw(&mut self, frame: &mut Frame, area: Rect, _theme: &Theme) { fn draw(&mut self, frame: &mut Frame, area: Rect, _theme: &Theme) {
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_set(ratatui::symbols::border::ROUNDED) .border_set(border::ROUNDED)
.title(self.mode_title.as_str()) .title(self.mode_title.as_str())
.title_alignment(ratatui::layout::Alignment::Center) .title_alignment(Alignment::Center)
.title_style(Style::default().reversed()) .title_style(Style::default().reversed())
.style(Style::default()); .style(Style::default());

View File

@ -5,8 +5,8 @@ use ratatui::{
use std::borrow::Cow; use std::borrow::Cow;
pub struct Shortcut { pub struct Shortcut {
pub key_sequences: Vec<Span<'static>>, key_sequences: Vec<Span<'static>>,
pub desc: &'static str, desc: &'static str,
} }
fn add_spacing(list: Vec<Vec<Span>>) -> Line { fn add_spacing(list: Vec<Vec<Span>>) -> Line {
@ -18,7 +18,7 @@ fn add_spacing(list: Vec<Vec<Span>>) -> Line {
.collect() .collect()
} }
pub fn span_vec_len(span_vec: &[Span]) -> usize { fn span_vec_len(span_vec: &[Span]) -> usize {
span_vec.iter().rfold(0, |init, s| init + s.width()) span_vec.iter().rfold(0, |init, s| init + s.width())
} }
@ -38,7 +38,7 @@ pub fn create_shortcut_list(
let columns = (render_width as usize / (max_shortcut_width + 4)).max(1); let columns = (render_width as usize / (max_shortcut_width + 4)).max(1);
let rows = (shortcut_spans.len() + columns - 1) / columns; let rows = (shortcut_spans.len() + columns - 1) / columns;
let mut lines: Vec<Line<'static>> = Vec::new(); let mut lines: Vec<Line<'static>> = Vec::with_capacity(rows);
for row in 0..rows { for row in 0..rows {
let row_spans: Vec<_> = (0..columns) let row_spans: Vec<_> = (0..columns)
@ -73,13 +73,7 @@ impl Shortcut {
let description = Span::styled(self.desc, Style::default().italic()); let description = Span::styled(self.desc, Style::default().italic());
self.key_sequences self.key_sequences
.iter() .iter()
.flat_map(|seq| { .flat_map(|seq| [Span::raw("["), seq.clone(), Span::raw("] ")])
[
Span::default().content("["),
seq.clone(),
Span::default().content("] "),
]
})
.chain(std::iter::once(description)) .chain(std::iter::once(description))
.collect() .collect()
} }

View File

@ -8,6 +8,9 @@ mod running_command;
pub mod state; pub mod state;
mod theme; mod theme;
#[cfg(feature = "tips")]
mod tips;
use crate::theme::Theme; use crate::theme::Theme;
use clap::Parser; use clap::Parser;
use ratatui::{ use ratatui::{
@ -22,14 +25,14 @@ use ratatui::{
}; };
use state::AppState; use state::AppState;
use std::{ use std::{
io::{self, stdout}, io::{stdout, Result, Stdout},
path::PathBuf, path::PathBuf,
time::Duration, time::Duration,
}; };
// Linux utility toolbox // Linux utility toolbox
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct Args { pub struct Args {
#[arg(short, long, help = "Path to the configuration file")] #[arg(short, long, help = "Path to the configuration file")]
config: Option<PathBuf>, config: Option<PathBuf>,
#[arg(short, long, value_enum)] #[arg(short, long, value_enum)]
@ -50,16 +53,10 @@ struct Args {
size_bypass: bool, size_bypass: bool,
} }
fn main() -> io::Result<()> { fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
let mut state = AppState::new( let mut state = AppState::new(args);
args.config,
args.theme,
args.override_validation,
args.size_bypass,
args.skip_confirmation,
);
stdout().execute(EnterAlternateScreen)?; stdout().execute(EnterAlternateScreen)?;
stdout().execute(EnableMouseCapture)?; stdout().execute(EnableMouseCapture)?;
@ -80,10 +77,7 @@ fn main() -> io::Result<()> {
Ok(()) Ok(())
} }
fn run( fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: &mut AppState) -> Result<()> {
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
state: &mut AppState,
) -> io::Result<()> {
loop { loop {
terminal.draw(|frame| state.draw(frame)).unwrap(); terminal.draw(|frame| state.draw(frame)).unwrap();
// Wait for an event // Wait for an event

View File

@ -6,14 +6,13 @@ use portable_pty::{
}; };
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}, crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind},
layout::{Rect, Size}, prelude::*,
style::{Style, Stylize}, symbols::border,
text::Line, widgets::Block,
widgets::{Block, Borders},
Frame,
}; };
use std::{ use std::{
io::Write, fs::File,
io::{Result, Write},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::JoinHandle, thread::JoinHandle,
}; };
@ -47,9 +46,8 @@ impl FloatContent for RunningCommand {
// Define the block for the terminal display // Define the block for the terminal display
let block = if !self.is_finished() { let block = if !self.is_finished() {
// Display a block indicating the command is running // Display a block indicating the command is running
Block::default() Block::bordered()
.borders(Borders::ALL) .border_set(border::ROUNDED)
.border_set(ratatui::symbols::border::ROUNDED)
.title_top(Line::from("Running the command....").centered()) .title_top(Line::from("Running the command....").centered())
.title_style(Style::default().reversed()) .title_style(Style::default().reversed())
.title_bottom(Line::from("Press Ctrl-C to KILL the command")) .title_bottom(Line::from("Press Ctrl-C to KILL the command"))
@ -68,16 +66,15 @@ impl FloatContent for RunningCommand {
}; };
let log_path = if let Some(log_path) = &self.log_path { let log_path = if let Some(log_path) = &self.log_path {
Line::from(format!(" Log saved: {} ", log_path)).centered() Line::from(format!(" Log saved: {} ", log_path))
} else { } else {
Line::from(" Press 'l' to save command log ").centered() Line::from(" Press 'l' to save command log ")
}; };
Block::default() Block::bordered()
.borders(Borders::ALL) .border_set(border::ROUNDED)
.border_set(ratatui::symbols::border::ROUNDED)
.title_top(title_line.centered()) .title_top(title_line.centered())
.title_bottom(log_path) .title_bottom(log_path.centered())
}; };
// Calculate the inner size of the terminal area, considering borders // Calculate the inner size of the terminal area, considering borders
@ -300,7 +297,7 @@ impl RunningCommand {
} }
} }
fn save_log(&self) -> std::io::Result<String> { fn save_log(&self) -> Result<String> {
let mut log_path = std::env::temp_dir(); let mut log_path = std::env::temp_dir();
let date_format = format_description!("[year]-[month]-[day]-[hour]-[minute]-[second]"); let date_format = format_description!("[year]-[month]-[day]-[hour]-[minute]-[second]");
log_path.push(format!( log_path.push(format!(
@ -311,7 +308,7 @@ impl RunningCommand {
.unwrap() .unwrap()
)); ));
let mut file = std::fs::File::create(&log_path)?; let mut file = File::create(&log_path)?;
let buffer = self.buffer.lock().unwrap(); let buffer = self.buffer.lock().unwrap();
file.write_all(&buffer)?; file.write_all(&buffer)?;

View File

@ -7,24 +7,22 @@ use crate::{
root::check_root_status, root::check_root_status,
running_command::RunningCommand, running_command::RunningCommand,
theme::Theme, theme::Theme,
Args,
}; };
use linutil_core::{ego_tree::NodeId, Command, Config, ListNode, TabList}; use linutil_core::{ego_tree::NodeId, Command, Config, ListNode, TabList};
#[cfg(feature = "tips")]
use rand::Rng;
use ratatui::{ use ratatui::{
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind}, crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind},
layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect}, layout::Flex,
style::{Style, Stylize}, prelude::*,
text::{Line, Span, Text}, symbols::border,
widgets::{Block, Borders, List, ListState, Paragraph}, widgets::{Block, List, ListState, Paragraph},
Frame,
}; };
use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
const MIN_WIDTH: u16 = 100; const MIN_WIDTH: u16 = 100;
const MIN_HEIGHT: u16 = 25; const MIN_HEIGHT: u16 = 25;
const FLOAT_SIZE: u16 = 80;
const CONFIRM_PROMPT_FLOAT_SIZE: u16 = 40;
const TITLE: &str = concat!(" Linux Toolbox - ", env!("CARGO_PKG_VERSION"), " "); const TITLE: &str = concat!(" Linux Toolbox - ", env!("CARGO_PKG_VERSION"), " ");
const ACTIONS_GUIDE: &str = "List of important tasks performed by commands' names: const ACTIONS_GUIDE: &str = "List of important tasks performed by commands' names:
@ -47,11 +45,12 @@ pub struct AppState {
/// Selected theme /// Selected theme
theme: Theme, theme: Theme,
/// Currently focused area /// Currently focused area
pub focus: Focus, focus: Focus,
/// List of tabs /// List of tabs
tabs: TabList, tabs: TabList,
/// Current tab /// Current tab
current_tab: ListState, current_tab: ListState,
longest_tab_display_len: u16,
/// This stack keeps track of our "current directory". You can think of it as `pwd`. but not /// This stack keeps track of our "current directory". You can think of it as `pwd`. but not
/// just the current directory, all paths that took us here, so we can "cd .." /// just the current directory, all paths that took us here, so we can "cd .."
visit_stack: Vec<(NodeId, usize)>, visit_stack: Vec<(NodeId, usize)>,
@ -63,7 +62,7 @@ pub struct AppState {
selected_commands: Vec<Rc<ListNode>>, selected_commands: Vec<Rc<ListNode>>,
drawable: bool, drawable: bool,
#[cfg(feature = "tips")] #[cfg(feature = "tips")]
tip: String, tip: &'static str,
size_bypass: bool, size_bypass: bool,
skip_confirmation: bool, skip_confirmation: bool,
} }
@ -82,7 +81,7 @@ pub struct ListEntry {
pub has_children: bool, pub has_children: bool,
} }
pub struct Areas { struct Areas {
tab_list: Rect, tab_list: Rect,
list: Rect, list: Rect,
} }
@ -95,24 +94,27 @@ enum SelectedItem {
} }
impl AppState { impl AppState {
pub fn new( pub fn new(args: Args) -> Self {
config_path: Option<PathBuf>, let tabs = linutil_core::get_tabs(!args.override_validation);
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 root_id = tabs[0].tree.root().id();
let auto_execute_commands = config_path.map(|path| Config::from_file(&path).auto_execute); let auto_execute_commands = args
.config
.map(|path| Config::from_file(&path).auto_execute);
let longest_tab_display_len = tabs
.iter()
.map(|tab| tab.name.len() + args.theme.tab_icon().len())
.max()
.unwrap_or(22) as u16; // 22 is the length of "Linutil by Chris Titus" title
let mut state = Self { let mut state = Self {
areas: None, areas: None,
theme, theme: args.theme,
focus: Focus::List, focus: Focus::List,
tabs, tabs,
current_tab: ListState::default().with_selected(Some(0)), current_tab: ListState::default().with_selected(Some(0)),
longest_tab_display_len,
visit_stack: vec![(root_id, 0usize)], visit_stack: vec![(root_id, 0usize)],
selection: ListState::default().with_selected(Some(0)), selection: ListState::default().with_selected(Some(0)),
filter: Filter::new(), filter: Filter::new(),
@ -120,14 +122,14 @@ impl AppState {
selected_commands: Vec::new(), selected_commands: Vec::new(),
drawable: false, drawable: false,
#[cfg(feature = "tips")] #[cfg(feature = "tips")]
tip: get_random_tip(), tip: crate::tips::get_random_tip(),
size_bypass, size_bypass: args.size_bypass,
skip_confirmation, skip_confirmation: args.skip_confirmation,
}; };
#[cfg(unix)] #[cfg(unix)]
if let Some(root_warning) = check_root_status() { if let Some(root_warning) = check_root_status() {
state.spawn_float(root_warning, 60, 40); state.spawn_float(root_warning, FLOAT_SIZE, FLOAT_SIZE);
} }
state.update_items(); state.update_items();
@ -138,13 +140,27 @@ impl AppState {
state state
} }
fn find_command_by_name(&self, name: &str) -> Option<Rc<ListNode>> {
self.tabs.iter().find_map(|tab| {
tab.tree.root().descendants().find_map(|node| {
let node_value = node.value();
(node_value.name == name && !node.has_children()).then_some(node_value.clone())
})
})
}
fn handle_initial_auto_execute(&mut self, auto_execute_commands: &[String]) { fn handle_initial_auto_execute(&mut self, auto_execute_commands: &[String]) {
self.selected_commands = auto_execute_commands self.selected_commands = auto_execute_commands
.iter() .iter()
.filter_map(|name| self.tabs.iter().find_map(|tab| tab.find_command(name))) .filter_map(|name| self.find_command_by_name(name))
.collect(); .collect();
if !self.selected_commands.is_empty() { if !self.selected_commands.is_empty() {
self.spawn_confirmprompt();
}
}
fn spawn_confirmprompt(&mut self) {
let cmd_names: Vec<_> = self let cmd_names: Vec<_> = self
.selected_commands .selected_commands
.iter() .iter()
@ -152,8 +168,11 @@ impl AppState {
.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),
CONFIRM_PROMPT_FLOAT_SIZE,
CONFIRM_PROMPT_FLOAT_SIZE,
));
} }
fn get_list_item_shortcut(&self) -> Box<[Shortcut]> { fn get_list_item_shortcut(&self) -> Box<[Shortcut]> {
@ -221,6 +240,8 @@ impl AppState {
Shortcut::new("Previous theme", ["T"]), Shortcut::new("Previous theme", ["T"]),
Shortcut::new("Next tab", ["Tab"]), Shortcut::new("Next tab", ["Tab"]),
Shortcut::new("Previous tab", ["Shift-Tab"]), Shortcut::new("Previous tab", ["Shift-Tab"]),
Shortcut::new("Important actions guide", ["g"]),
Shortcut::new("Multi-selection mode", ["v"]),
]), ]),
), ),
@ -229,16 +250,18 @@ impl AppState {
} }
} }
pub fn draw(&mut self, frame: &mut Frame) { fn is_terminal_drawable(&mut self, terminal_size: Rect) -> bool {
let terminal_size = frame.area(); !self.size_bypass && (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH)
}
if !self.size_bypass pub fn draw(&mut self, frame: &mut Frame) {
&& (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH) let area = frame.area();
{ self.drawable = !self.is_terminal_drawable(area);
if !self.drawable {
let warning = Paragraph::new(format!( let warning = Paragraph::new(format!(
"Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}", "Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}",
terminal_size.width, area.width,
terminal_size.height, area.height,
MIN_WIDTH, MIN_WIDTH,
MIN_HEIGHT, MIN_HEIGHT,
)) ))
@ -253,18 +276,12 @@ impl AppState {
Constraint::Length(5), Constraint::Length(5),
Constraint::Fill(1), Constraint::Fill(1),
]) ])
.split(terminal_size); .split(area);
self.drawable = false;
return frame.render_widget(warning, centered_layout[1]); return frame.render_widget(warning, centered_layout[1]);
} else {
self.drawable = true;
} }
let label_block = Block::default() let label_block = Block::bordered().border_set(border::Set {
.borders(Borders::ALL)
.border_set(ratatui::symbols::border::ROUNDED)
.border_set(ratatui::symbols::border::Set {
top_left: " ", top_left: " ",
top_right: " ", top_right: " ",
bottom_left: " ", bottom_left: " ",
@ -274,59 +291,38 @@ impl AppState {
horizontal_top: "*", horizontal_top: "*",
horizontal_bottom: "*", horizontal_bottom: "*",
}); });
let str1 = "Linutil ";
let str2 = "by Chris Titus";
let label = Paragraph::new(Line::from(vec![ let label = Paragraph::new(Line::from(vec![
Span::styled(str1, Style::default().bold()), Span::styled("Linutil ", Style::default().bold()),
Span::styled(str2, Style::default().italic()), Span::styled("by Chris Titus", Style::default().italic()),
])) ]))
.block(label_block) .block(label_block)
.alignment(Alignment::Center); .centered();
let longest_tab_display_len = self
.tabs
.iter()
.map(|tab| tab.name.len() + self.theme.tab_icon().len())
.max()
.unwrap_or(0)
.max(str1.len() + str2.len());
let (keybind_scope, shortcuts) = self.get_keybinds(); let (keybind_scope, shortcuts) = self.get_keybinds();
let keybind_render_width = terminal_size.width - 2; let keybinds_block = Block::bordered()
let keybinds_block = Block::default()
.title(format!(" {} ", keybind_scope)) .title(format!(" {} ", keybind_scope))
.borders(Borders::ALL) .border_set(border::ROUNDED);
.border_set(ratatui::symbols::border::ROUNDED);
let keybind_render_width = keybinds_block.inner(area).width;
let keybinds = create_shortcut_list(shortcuts, keybind_render_width); let keybinds = create_shortcut_list(shortcuts, keybind_render_width);
let n_lines = keybinds.len() as u16; let keybind_len = keybinds.len() as u16;
let keybind_para = Paragraph::new(Text::from_iter(keybinds)).block(keybinds_block); let keybind_para = Paragraph::new(Text::from_iter(keybinds)).block(keybinds_block);
let vertical = Layout::default() let vertical =
.direction(Direction::Vertical) Layout::vertical([Constraint::Percentage(0), Constraint::Max(keybind_len + 2)])
.constraints([
Constraint::Percentage(0),
Constraint::Max(n_lines as u16 + 2),
])
.flex(Flex::Legacy) .flex(Flex::Legacy)
.margin(0) .split(area);
.split(frame.area());
let horizontal = Layout::default() let horizontal = Layout::horizontal([
.direction(Direction::Horizontal) Constraint::Min(self.longest_tab_display_len + 5),
.constraints([
Constraint::Min(longest_tab_display_len as u16 + 5),
Constraint::Percentage(100), Constraint::Percentage(100),
]) ])
.split(vertical[0]); .split(vertical[0]);
let left_chunks = Layout::default() let left_chunks =
.direction(Direction::Vertical) Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[0]);
.constraints([Constraint::Length(3), Constraint::Min(1)])
.split(horizontal[0]);
frame.render_widget(label, left_chunks[0]); frame.render_widget(label, left_chunks[0]);
self.areas = Some(Areas { self.areas = Some(Areas {
@ -346,36 +342,23 @@ impl AppState {
Style::new().fg(self.theme.tab_color()) Style::new().fg(self.theme.tab_color())
}; };
let list = List::new(tabs) let tab_list = List::new(tabs)
.block( .block(Block::bordered().border_set(border::ROUNDED))
Block::default()
.borders(Borders::ALL)
.border_set(ratatui::symbols::border::ROUNDED),
)
.highlight_style(tab_hl_style) .highlight_style(tab_hl_style)
.highlight_symbol(self.theme.tab_icon()); .highlight_symbol(self.theme.tab_icon());
frame.render_stateful_widget(list, left_chunks[1], &mut self.current_tab); frame.render_stateful_widget(tab_list, left_chunks[1], &mut self.current_tab);
let chunks = Layout::default() let chunks =
.direction(Direction::Vertical) Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[1]);
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
.split(horizontal[1]);
let list_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)].as_ref())
.split(chunks[1]);
self.filter.draw_searchbar(frame, chunks[0], &self.theme); self.filter.draw_searchbar(frame, chunks[0], &self.theme);
let mut items: Vec<Line> = Vec::new(); let mut items: Vec<Line> = Vec::with_capacity(self.filter.item_list().len());
let mut task_items: Vec<Line> = Vec::new();
if !self.at_root() { if !self.at_root() {
items.push( items.push(
Line::from(format!("{} ..", self.theme.dir_icon())).style(self.theme.dir_color()), Line::from(format!("{} ..", self.theme.dir_icon())).style(self.theme.dir_color()),
); );
task_items.push(Line::from(" ").style(self.theme.dir_color()));
} }
items.extend(self.filter.item_list().iter().map( items.extend(self.filter.item_list().iter().map(
@ -384,60 +367,37 @@ impl AppState {
}| { }| {
let is_selected = self.selected_commands.contains(node); let is_selected = self.selected_commands.contains(node);
let (indicator, style) = if is_selected { let (indicator, style) = if is_selected {
(self.theme.multi_select_icon(), Style::default().bold()) (self.theme.multi_select_icon(), Style::new().bold())
} else { } else {
let ms_style = if self.multi_select && !node.multi_select { let ms_style = if self.multi_select && !node.multi_select {
Style::default().fg(self.theme.multi_select_disabled_color()) Style::new().fg(self.theme.multi_select_disabled_color())
} else { } else {
Style::new() Style::new()
}; };
("", ms_style) ("", ms_style)
}; };
if *has_children { if *has_children {
Line::from(format!( Line::styled(
"{} {} {}", format!("{} {}", self.theme.dir_icon(), node.name,),
self.theme.dir_icon(), self.theme.dir_color(),
node.name, )
indicator
))
.style(self.theme.dir_color())
.patch_style(style) .patch_style(style)
} else { } else {
Line::from(format!( let left_content =
"{} {} {}", format!("{} {} {}", self.theme.cmd_icon(), node.name, indicator);
self.theme.cmd_icon(), let right_content = format!("{} ", node.task_list);
node.name, let center_space = " ".repeat(
indicator chunks[1].width as usize - left_content.len() - right_content.len(),
)) );
.style(self.theme.cmd_color()) Line::styled(
format!("{}{}{}", left_content, center_space, right_content),
self.theme.cmd_color(),
)
.patch_style(style) .patch_style(style)
} }
}, },
)); ));
task_items.extend(self.filter.item_list().iter().map(
|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())
.patch_style(ms_style)
} else {
Line::from(format!("{} ", node.task_list))
.alignment(Alignment::Right)
.style(self.theme.cmd_color())
.bold()
.patch_style(ms_style)
}
},
));
let style = if let Focus::List = self.focus { let style = if let Focus::List = self.focus {
Style::default().reversed() Style::default().reversed()
} else { } else {
@ -451,7 +411,10 @@ impl AppState {
}; };
#[cfg(feature = "tips")] #[cfg(feature = "tips")]
let bottom_title = Line::from(self.tip.as_str().bold().blue()).right_aligned(); let bottom_title = Line::from(format!(" {} ", self.tip))
.bold()
.blue()
.centered();
#[cfg(not(feature = "tips"))] #[cfg(not(feature = "tips"))]
let bottom_title = ""; let bottom_title = "";
@ -461,23 +424,14 @@ impl AppState {
let list = List::new(items) let list = List::new(items)
.highlight_style(style) .highlight_style(style)
.block( .block(
Block::default() Block::bordered()
.borders(Borders::ALL & !Borders::RIGHT) .border_set(border::ROUNDED)
.border_set(ratatui::symbols::border::ROUNDED)
.title(title) .title(title)
.title(task_list_title)
.title_bottom(bottom_title), .title_bottom(bottom_title),
) )
.scroll_padding(1); .scroll_padding(1);
frame.render_stateful_widget(list, list_chunks[0], &mut self.selection); frame.render_stateful_widget(list, chunks[1], &mut self.selection);
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),
);
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], &self.theme), Focus::FloatingWindow(float) => float.draw(frame, chunks[1], &self.theme),
@ -566,26 +520,10 @@ impl AppState {
// Handle key only when Tablist or List is focused // Handle key only when Tablist or List is focused
// Prevents exiting the application even when a command is running // Prevents exiting the application even when a command is running
// Add keys here which should work on both TabList and List // Add keys here which should work on both TabList and List
if matches!(self.focus, Focus::TabList | Focus::List) { if matches!(self.focus, Focus::TabList | Focus::List)
match key.code { && self.handle_tablist_and_list_keys(key)
KeyCode::Tab => { {
if self.current_tab.selected().unwrap() == self.tabs.len() - 1 { return true;
self.current_tab.select_first();
} else {
self.current_tab.select_next();
}
self.refresh_tab();
}
KeyCode::BackTab => {
if self.current_tab.selected().unwrap() == 0 {
self.current_tab.select(Some(self.tabs.len() - 1));
} else {
self.current_tab.select_previous();
}
self.refresh_tab();
}
_ => {}
}
} }
match &mut self.focus { match &mut self.focus {
@ -626,15 +564,9 @@ impl AppState {
Focus::TabList => match key.code { Focus::TabList => match key.code {
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.focus = Focus::List, KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.focus = Focus::List,
KeyCode::Char('j') | KeyCode::Down => self.scroll_tab_down(), KeyCode::Char('j') | KeyCode::Down => self.scroll_tab_down(),
KeyCode::Char('k') | KeyCode::Up => self.scroll_tab_up(), KeyCode::Char('k') | KeyCode::Up => self.scroll_tab_up(),
KeyCode::Char('/') => self.enter_search(),
KeyCode::Char('t') => self.theme.next(),
KeyCode::Char('T') => self.theme.prev(),
KeyCode::Char('g') => self.toggle_task_list_guide(),
_ => {} _ => {}
}, },
@ -645,11 +577,6 @@ impl AppState {
KeyCode::Char('d') | KeyCode::Char('D') => self.enable_description(), KeyCode::Char('d') | KeyCode::Char('D') => self.enable_description(),
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(), KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(),
KeyCode::Char('h') | KeyCode::Left => self.go_back(), KeyCode::Char('h') | KeyCode::Left => self.go_back(),
KeyCode::Char('/') => self.enter_search(),
KeyCode::Char('t') => self.theme.next(),
KeyCode::Char('T') => self.theme.prev(),
KeyCode::Char('g') => self.toggle_task_list_guide(),
KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(),
KeyCode::Char(' ') if self.multi_select => self.toggle_selection(), KeyCode::Char(' ') if self.multi_select => self.toggle_selection(),
_ => {} _ => {}
}, },
@ -659,32 +586,38 @@ impl AppState {
true true
} }
fn scroll_down(&mut self) { fn handle_tablist_and_list_keys(&mut self, key: &KeyEvent) -> bool {
let len = self.filter.item_list().len(); match key.code {
if len == 0 { KeyCode::Tab => self.scroll_tab_down(),
return; KeyCode::BackTab => self.scroll_tab_up(),
KeyCode::Char('/') => self.enter_search(),
KeyCode::Char('g') | KeyCode::Char('G') => self.enable_task_list_guide(),
KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(),
KeyCode::Char('t') => self.theme.next(),
KeyCode::Char('T') => self.theme.prev(),
_ => return false,
}
true
} }
let current = self.selection.selected().unwrap_or(0);
let max_index = if self.at_root() { len - 1 } else { len };
let next = if current + 1 > max_index {
0
} else {
current + 1
};
self.selection.select(Some(next)); fn scroll_down(&mut self) {
if let Some(selected) = self.selection.selected() {
if selected == self.filter.item_list().len() - 1 {
self.selection.select_first();
} else {
self.selection.select_next();
}
}
} }
fn scroll_up(&mut self) { fn scroll_up(&mut self) {
let len = self.filter.item_list().len(); if let Some(selected) = self.selection.selected() {
if len == 0 { if selected == 0 {
return; self.selection.select_last();
} else {
self.selection.select_previous();
}
} }
let current = self.selection.selected().unwrap_or(0);
let max_index = if self.at_root() { len - 1 } else { len };
let next = if current == 0 { max_index } else { current - 1 };
self.selection.select(Some(next));
} }
fn toggle_multi_select(&mut self) { fn toggle_multi_select(&mut self) {
@ -747,12 +680,13 @@ impl AppState {
fn get_selected_node(&self) -> Option<Rc<ListNode>> { fn get_selected_node(&self) -> Option<Rc<ListNode>> {
let mut selected_index = self.selection.selected().unwrap_or(0); let mut selected_index = self.selection.selected().unwrap_or(0);
if !self.at_root() && selected_index == 0 {
return None;
}
if !self.at_root() { if !self.at_root() {
if selected_index == 0 {
return None;
} else {
selected_index = selected_index.saturating_sub(1); selected_index = selected_index.saturating_sub(1);
} }
}
if let Some(item) = self.filter.item_list().get(selected_index) { if let Some(item) = self.filter.item_list().get(selected_index) {
if !item.has_children { if !item.has_children {
@ -793,13 +727,13 @@ impl AppState {
pub fn selected_item_is_dir(&self) -> bool { pub fn selected_item_is_dir(&self) -> bool {
let mut selected_index = self.selection.selected().unwrap_or(0); let mut selected_index = self.selection.selected().unwrap_or(0);
if !self.at_root() && selected_index == 0 {
return false;
}
if !self.at_root() { if !self.at_root() {
if selected_index == 0 {
return false;
} else {
selected_index = selected_index.saturating_sub(1); selected_index = selected_index.saturating_sub(1);
} }
}
self.filter self.filter
.item_list() .item_list()
@ -822,7 +756,7 @@ impl AppState {
if let Some(list_node) = self.get_selected_node() { if let Some(list_node) = self.get_selected_node() {
let preview_title = format!("[Preview] - {}", list_node.name.as_str()); let preview_title = format!("[Preview] - {}", list_node.name.as_str());
let preview = FloatingText::from_command(&list_node.command, &preview_title, false); let preview = FloatingText::from_command(&list_node.command, &preview_title, false);
self.spawn_float(preview, 80, 80); self.spawn_float(preview, FLOAT_SIZE, FLOAT_SIZE);
} }
} }
@ -831,11 +765,19 @@ impl AppState {
if !command_description.is_empty() { if !command_description.is_empty() {
let description = let description =
FloatingText::new(command_description, "Command Description", true); FloatingText::new(command_description, "Command Description", true);
self.spawn_float(description, 80, 80); self.spawn_float(description, FLOAT_SIZE, FLOAT_SIZE);
} }
} }
} }
fn enable_task_list_guide(&mut self) {
self.spawn_float(
FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide", true),
FLOAT_SIZE,
FLOAT_SIZE,
);
}
fn get_selected_item_type(&self) -> SelectedItem { fn get_selected_item_type(&self) -> SelectedItem {
if self.selected_item_is_up_dir() { if self.selected_item_is_up_dir() {
SelectedItem::UpDir SelectedItem::UpDir
@ -862,14 +804,7 @@ impl AppState {
if self.skip_confirmation { if self.skip_confirmation {
self.handle_confirm_command(); self.handle_confirm_command();
} else { } else {
let cmd_names = self self.spawn_confirmprompt();
.selected_commands
.iter()
.map(|node| node.name.as_str())
.collect::<Vec<_>>();
let prompt = ConfirmPrompt::new(&cmd_names);
self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40));
} }
} }
SelectedItem::None => {} SelectedItem::None => {}
@ -884,7 +819,7 @@ impl AppState {
.collect(); .collect();
let command = RunningCommand::new(&commands); let command = RunningCommand::new(&commands);
self.spawn_float(command, 80, 80); self.spawn_float(command, FLOAT_SIZE, FLOAT_SIZE);
self.selected_commands.clear(); self.selected_commands.clear();
} }
@ -918,44 +853,21 @@ impl AppState {
self.update_items(); self.update_items();
} }
fn toggle_task_list_guide(&mut self) {
self.spawn_float(
FloatingText::new(ACTIONS_GUIDE.to_string(), "Important Actions Guide", true),
80,
80,
);
}
fn scroll_tab_down(&mut self) { fn scroll_tab_down(&mut self) {
let len = self.tabs.len(); if self.current_tab.selected().unwrap() == self.tabs.len() - 1 {
let current = self.current_tab.selected().unwrap_or(0); self.current_tab.select_first();
let next = if current + 1 >= len { 0 } else { current + 1 }; } else {
self.current_tab.select_next();
self.current_tab.select(Some(next)); }
self.refresh_tab(); self.refresh_tab();
} }
fn scroll_tab_up(&mut self) { fn scroll_tab_up(&mut self) {
let len = self.tabs.len(); if self.current_tab.selected().unwrap() == 0 {
let current = self.current_tab.selected().unwrap_or(0); self.current_tab.select(Some(self.tabs.len() - 1));
let next = if current == 0 { len - 1 } else { current - 1 }; } else {
self.current_tab.select_previous();
self.current_tab.select(Some(next)); }
self.refresh_tab(); self.refresh_tab();
} }
} }
#[cfg(feature = "tips")]
const TIPS: &str = include_str!("../cool_tips.txt");
#[cfg(feature = "tips")]
fn get_random_tip() -> String {
let tips: Vec<&str> = TIPS.lines().collect();
if tips.is_empty() {
return "".to_string();
}
let mut rng = rand::thread_rng();
let random_index = rng.gen_range(0..tips.len());
format!(" {} ", tips[random_index])
}

View File

@ -14,91 +14,91 @@ pub enum Theme {
} }
impl Theme { impl Theme {
pub fn dir_color(&self) -> Color { pub const fn dir_color(&self) -> Color {
match self { match self {
Theme::Default => Color::Blue, Theme::Default => Color::Blue,
Theme::Compatible => Color::Blue, Theme::Compatible => Color::Blue,
} }
} }
pub fn cmd_color(&self) -> Color { pub const fn cmd_color(&self) -> Color {
match self { match self {
Theme::Default => Color::Rgb(204, 224, 208), Theme::Default => Color::Rgb(204, 224, 208),
Theme::Compatible => Color::LightGreen, Theme::Compatible => Color::LightGreen,
} }
} }
pub fn multi_select_disabled_color(&self) -> Color { pub const fn multi_select_disabled_color(&self) -> Color {
match self { match self {
Theme::Default => Color::DarkGray, Theme::Default => Color::DarkGray,
Theme::Compatible => Color::DarkGray, Theme::Compatible => Color::DarkGray,
} }
} }
pub fn tab_color(&self) -> Color { pub const fn tab_color(&self) -> Color {
match self { match self {
Theme::Default => Color::Rgb(255, 255, 85), Theme::Default => Color::Rgb(255, 255, 85),
Theme::Compatible => Color::Yellow, Theme::Compatible => Color::Yellow,
} }
} }
pub fn dir_icon(&self) -> &'static str { pub const fn dir_icon(&self) -> &'static str {
match self { match self {
Theme::Default => "", Theme::Default => "",
Theme::Compatible => "[DIR]", Theme::Compatible => "[DIR]",
} }
} }
pub fn cmd_icon(&self) -> &'static str { pub const fn cmd_icon(&self) -> &'static str {
match self { match self {
Theme::Default => "", Theme::Default => "",
Theme::Compatible => "[CMD]", Theme::Compatible => "[CMD]",
} }
} }
pub fn tab_icon(&self) -> &'static str { pub const fn tab_icon(&self) -> &'static str {
match self { match self {
Theme::Default => "", Theme::Default => "",
Theme::Compatible => ">> ", Theme::Compatible => ">> ",
} }
} }
pub fn multi_select_icon(&self) -> &'static str { pub const fn multi_select_icon(&self) -> &'static str {
match self { match self {
Theme::Default => "", Theme::Default => "",
Theme::Compatible => "*", Theme::Compatible => "*",
} }
} }
pub fn success_color(&self) -> Color { pub const fn success_color(&self) -> Color {
match self { match self {
Theme::Default => Color::Rgb(5, 255, 55), Theme::Default => Color::Rgb(5, 255, 55),
Theme::Compatible => Color::Green, Theme::Compatible => Color::Green,
} }
} }
pub fn fail_color(&self) -> Color { pub const fn fail_color(&self) -> Color {
match self { match self {
Theme::Default => Color::Rgb(199, 55, 44), Theme::Default => Color::Rgb(199, 55, 44),
Theme::Compatible => Color::Red, Theme::Compatible => Color::Red,
} }
} }
pub fn focused_color(&self) -> Color { pub const fn focused_color(&self) -> Color {
match self { match self {
Theme::Default => Color::LightBlue, Theme::Default => Color::LightBlue,
Theme::Compatible => Color::LightBlue, Theme::Compatible => Color::LightBlue,
} }
} }
pub fn search_preview_color(&self) -> Color { pub const fn search_preview_color(&self) -> Color {
match self { match self {
Theme::Default => Color::DarkGray, Theme::Default => Color::DarkGray,
Theme::Compatible => Color::DarkGray, Theme::Compatible => Color::DarkGray,
} }
} }
pub fn unfocused_color(&self) -> Color { pub const fn unfocused_color(&self) -> Color {
match self { match self {
Theme::Default => Color::Gray, Theme::Default => Color::Gray,
Theme::Compatible => Color::Gray, Theme::Compatible => Color::Gray,

13
tui/src/tips.rs Normal file
View File

@ -0,0 +1,13 @@
use rand::Rng;
const TIPS: &str = include_str!("../cool_tips.txt");
pub fn get_random_tip() -> &'static str {
let tips: Vec<&str> = TIPS.lines().collect();
if tips.is_empty() {
return "";
}
let random_index = rand::thread_rng().gen_range(0..tips.len());
tips[random_index]
}