mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-24 14:11:55 +00:00
Compare commits
4 Commits
caf795fc8d
...
da38626119
Author | SHA1 | Date | |
---|---|---|---|
|
da38626119 | ||
|
85719251d3 | ||
|
06f13dc5f9 | ||
|
79aae9eb24 |
29
Cargo.lock
generated
29
Cargo.lock
generated
|
@ -390,7 +390,6 @@ dependencies = [
|
|||
"portable-pty",
|
||||
"rand",
|
||||
"ratatui",
|
||||
"temp-dir",
|
||||
"time",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-highlight",
|
||||
|
@ -721,18 +720,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -884,9 +883,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.77"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -997,9 +996,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.24.3"
|
||||
version = "0.24.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9871f16d6cf5c4757dcf30d5d2172a2df6987c510c017bbb7abfb7f9aa24d06"
|
||||
checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
|
@ -1010,9 +1009,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tree-sitter-bash"
|
||||
version = "0.23.1"
|
||||
version = "0.23.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aa5e1c6bd02c0053f3f68edcf5d8866b38a8640584279e30fca88149ce14dda"
|
||||
checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
|
@ -1020,9 +1019,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tree-sitter-highlight"
|
||||
version = "0.24.3"
|
||||
version = "0.24.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48859aa39513716018d81904220960f415dbb72e071234a721304d20bf245e4c"
|
||||
checksum = "7f0f856de10d70a6d14d66db3648f7410c131cd49e989a863f15dda9acae6044"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
|
@ -1134,9 +1133,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.3"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
|
||||
checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
|
|
|
@ -10,7 +10,7 @@ include = ["src/*.rs", "Cargo.toml", "tabs/**"]
|
|||
[dependencies]
|
||||
include_dir = "0.7.4"
|
||||
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 }
|
||||
which = "6.0.3"
|
||||
which = "7.0.0"
|
||||
ego-tree = "0.9.0"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
use std::{path::Path, process};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
|
|
|
@ -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::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read},
|
||||
|
@ -6,11 +10,6 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
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;
|
||||
|
||||
const TAB_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/tabs");
|
||||
|
|
|
@ -36,16 +36,3 @@ pub struct ListNode {
|
|||
pub task_list: String,
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,12 @@ oneshot = { version = "0.1.8", features = ["std"], default-features = false }
|
|||
portable-pty = "0.8.1"
|
||||
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 = ["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 = { version = "24.9.28", path = "../core" }
|
||||
tree-sitter-highlight = "0.24.3"
|
||||
tree-sitter-bash = "0.23.1"
|
||||
tree-sitter-highlight = "0.24.4"
|
||||
tree-sitter-bash = "0.23.3"
|
||||
nix = { version = "0.29.0", features = [ "user" ] }
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -3,7 +3,8 @@ use ratatui::{
|
|||
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
|
||||
layout::Alignment,
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Clear, List},
|
||||
symbols::border,
|
||||
widgets::{Block, Clear, List},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
@ -22,10 +23,17 @@ pub struct ConfirmPrompt {
|
|||
|
||||
impl ConfirmPrompt {
|
||||
pub fn new(names: &[&str]) -> Self {
|
||||
let max_count_str = format!("{}", names.len());
|
||||
let names = names
|
||||
.iter()
|
||||
.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();
|
||||
|
||||
Self {
|
||||
|
@ -51,17 +59,15 @@ impl ConfirmPrompt {
|
|||
|
||||
impl FloatContent for ConfirmPrompt {
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect, theme: &theme::Theme) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_set(ratatui::symbols::border::ROUNDED)
|
||||
let block = Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.title(" Confirm selections ")
|
||||
.title_bottom(Line::from(vec![
|
||||
Span::styled(" [", Style::default()),
|
||||
Span::raw(" ["),
|
||||
Span::styled("y", Style::default().fg(theme.success_color())),
|
||||
Span::styled("] to continue ", Style::default()),
|
||||
Span::styled("[", Style::default()),
|
||||
Span::raw("] to continue ["),
|
||||
Span::styled("n", Style::default().fg(theme.fail_color())),
|
||||
Span::styled("] to abort ", Style::default()),
|
||||
Span::raw("] to abort "),
|
||||
]))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title_style(Style::default().bold())
|
||||
|
|
|
@ -2,11 +2,9 @@ use crate::{state::ListEntry, theme::Theme};
|
|||
use linutil_core::{ego_tree::NodeId, Tab};
|
||||
use ratatui::{
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::{Position, Rect},
|
||||
style::Style,
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
|
@ -118,9 +116,8 @@ impl Filter {
|
|||
//Create the search bar widget
|
||||
let search_bar = Paragraph::new(display_text)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_set(ratatui::symbols::border::ROUNDED)
|
||||
Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.title(" Search "),
|
||||
)
|
||||
.style(Style::default().fg(search_color));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{hint::Shortcut, theme::Theme};
|
||||
use ratatui::{
|
||||
crossterm::event::{KeyCode, KeyEvent, MouseEvent},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
Frame,
|
||||
};
|
||||
|
||||
|
@ -29,23 +29,19 @@ impl<Content: FloatContent + ?Sized> Float<Content> {
|
|||
}
|
||||
|
||||
fn floating_window(&self, size: Rect) -> Rect {
|
||||
let hor_float = Layout::default()
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - self.width_percent) / 2),
|
||||
Constraint::Percentage(self.width_percent),
|
||||
Constraint::Percentage((100 - self.width_percent) / 2),
|
||||
])
|
||||
.direction(Direction::Horizontal)
|
||||
.split(size)[1];
|
||||
let hor_float = Layout::horizontal([
|
||||
Constraint::Percentage((100 - self.width_percent) / 2),
|
||||
Constraint::Percentage(self.width_percent),
|
||||
Constraint::Percentage((100 - self.width_percent) / 2),
|
||||
])
|
||||
.split(size)[1];
|
||||
|
||||
Layout::default()
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - self.height_percent) / 2),
|
||||
Constraint::Percentage(self.height_percent),
|
||||
Constraint::Percentage((100 - self.height_percent) / 2),
|
||||
])
|
||||
.direction(Direction::Vertical)
|
||||
.split(hor_float)[1]
|
||||
Layout::vertical([
|
||||
Constraint::Percentage((100 - self.height_percent) / 2),
|
||||
Constraint::Percentage(self.height_percent),
|
||||
Constraint::Percentage((100 - self.height_percent) / 2),
|
||||
])
|
||||
.split(hor_float)[1]
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect, theme: &Theme) {
|
||||
|
|
|
@ -2,11 +2,9 @@ use crate::{float::FloatContent, hint::Shortcut, theme::Theme};
|
|||
use linutil_core::Command;
|
||||
use ratatui::{
|
||||
crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind},
|
||||
layout::Rect,
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
use tree_sitter_bash as hl_bash;
|
||||
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) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_set(ratatui::symbols::border::ROUNDED)
|
||||
.border_set(border::ROUNDED)
|
||||
.title(self.mode_title.as_str())
|
||||
.title_alignment(ratatui::layout::Alignment::Center)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title_style(Style::default().reversed())
|
||||
.style(Style::default());
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ use ratatui::{
|
|||
use std::borrow::Cow;
|
||||
|
||||
pub struct Shortcut {
|
||||
pub key_sequences: Vec<Span<'static>>,
|
||||
pub desc: &'static str,
|
||||
key_sequences: Vec<Span<'static>>,
|
||||
desc: &'static str,
|
||||
}
|
||||
|
||||
fn add_spacing(list: Vec<Vec<Span>>) -> Line {
|
||||
|
@ -18,7 +18,7 @@ fn add_spacing(list: Vec<Vec<Span>>) -> Line {
|
|||
.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())
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ pub fn create_shortcut_list(
|
|||
let columns = (render_width as usize / (max_shortcut_width + 4)).max(1);
|
||||
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 {
|
||||
let row_spans: Vec<_> = (0..columns)
|
||||
|
@ -73,13 +73,7 @@ impl Shortcut {
|
|||
let description = Span::styled(self.desc, Style::default().italic());
|
||||
self.key_sequences
|
||||
.iter()
|
||||
.flat_map(|seq| {
|
||||
[
|
||||
Span::default().content("["),
|
||||
seq.clone(),
|
||||
Span::default().content("] "),
|
||||
]
|
||||
})
|
||||
.flat_map(|seq| [Span::raw("["), seq.clone(), Span::raw("] ")])
|
||||
.chain(std::iter::once(description))
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,9 @@ mod running_command;
|
|||
pub mod state;
|
||||
mod theme;
|
||||
|
||||
#[cfg(feature = "tips")]
|
||||
mod tips;
|
||||
|
||||
use crate::theme::Theme;
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
|
@ -22,14 +25,14 @@ use ratatui::{
|
|||
};
|
||||
use state::AppState;
|
||||
use std::{
|
||||
io::{self, stdout},
|
||||
io::{stdout, Result, Stdout},
|
||||
path::PathBuf,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
// Linux utility toolbox
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
pub struct Args {
|
||||
#[arg(short, long, help = "Path to the configuration file")]
|
||||
config: Option<PathBuf>,
|
||||
#[arg(short, long, value_enum)]
|
||||
|
@ -50,16 +53,10 @@ struct Args {
|
|||
size_bypass: bool,
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let mut state = AppState::new(
|
||||
args.config,
|
||||
args.theme,
|
||||
args.override_validation,
|
||||
args.size_bypass,
|
||||
args.skip_confirmation,
|
||||
);
|
||||
let mut state = AppState::new(args);
|
||||
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
stdout().execute(EnableMouseCapture)?;
|
||||
|
@ -80,10 +77,7 @@ fn main() -> io::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn run(
|
||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||
state: &mut AppState,
|
||||
) -> io::Result<()> {
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: &mut AppState) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| state.draw(frame)).unwrap();
|
||||
// Wait for an event
|
||||
|
|
|
@ -6,14 +6,13 @@ use portable_pty::{
|
|||
};
|
||||
use ratatui::{
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind},
|
||||
layout::{Rect, Size},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders},
|
||||
Frame,
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::Block,
|
||||
};
|
||||
use std::{
|
||||
io::Write,
|
||||
fs::File,
|
||||
io::{Result, Write},
|
||||
sync::{Arc, Mutex},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
@ -47,9 +46,8 @@ impl FloatContent for RunningCommand {
|
|||
// Define the block for the terminal display
|
||||
let block = if !self.is_finished() {
|
||||
// Display a block indicating the command is running
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_set(ratatui::symbols::border::ROUNDED)
|
||||
Block::bordered()
|
||||
.border_set(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"))
|
||||
|
@ -68,16 +66,15 @@ impl FloatContent for RunningCommand {
|
|||
};
|
||||
|
||||
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 {
|
||||
Line::from(" Press 'l' to save command log ").centered()
|
||||
Line::from(" Press 'l' to save command log ")
|
||||
};
|
||||
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_set(ratatui::symbols::border::ROUNDED)
|
||||
Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.title_top(title_line.centered())
|
||||
.title_bottom(log_path)
|
||||
.title_bottom(log_path.centered())
|
||||
};
|
||||
|
||||
// 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 date_format = format_description!("[year]-[month]-[day]-[hour]-[minute]-[second]");
|
||||
log_path.push(format!(
|
||||
|
@ -311,7 +308,7 @@ impl RunningCommand {
|
|||
.unwrap()
|
||||
));
|
||||
|
||||
let mut file = std::fs::File::create(&log_path)?;
|
||||
let mut file = File::create(&log_path)?;
|
||||
let buffer = self.buffer.lock().unwrap();
|
||||
file.write_all(&buffer)?;
|
||||
|
||||
|
|
462
tui/src/state.rs
462
tui/src/state.rs
|
@ -7,24 +7,22 @@ use crate::{
|
|||
root::check_root_status,
|
||||
running_command::RunningCommand,
|
||||
theme::Theme,
|
||||
Args,
|
||||
};
|
||||
|
||||
use linutil_core::{ego_tree::NodeId, Command, Config, ListNode, TabList};
|
||||
#[cfg(feature = "tips")]
|
||||
use rand::Rng;
|
||||
use ratatui::{
|
||||
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},
|
||||
Frame,
|
||||
layout::Flex,
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::{Block, List, ListState, Paragraph},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
const MIN_WIDTH: u16 = 100;
|
||||
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 ACTIONS_GUIDE: &str = "List of important tasks performed by commands' names:
|
||||
|
||||
|
@ -47,11 +45,12 @@ pub struct AppState {
|
|||
/// Selected theme
|
||||
theme: Theme,
|
||||
/// Currently focused area
|
||||
pub focus: Focus,
|
||||
focus: Focus,
|
||||
/// List of tabs
|
||||
tabs: TabList,
|
||||
/// Current tab
|
||||
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
|
||||
/// just the current directory, all paths that took us here, so we can "cd .."
|
||||
visit_stack: Vec<(NodeId, usize)>,
|
||||
|
@ -63,7 +62,7 @@ pub struct AppState {
|
|||
selected_commands: Vec<Rc<ListNode>>,
|
||||
drawable: bool,
|
||||
#[cfg(feature = "tips")]
|
||||
tip: String,
|
||||
tip: &'static str,
|
||||
size_bypass: bool,
|
||||
skip_confirmation: bool,
|
||||
}
|
||||
|
@ -82,7 +81,7 @@ pub struct ListEntry {
|
|||
pub has_children: bool,
|
||||
}
|
||||
|
||||
pub struct Areas {
|
||||
struct Areas {
|
||||
tab_list: Rect,
|
||||
list: Rect,
|
||||
}
|
||||
|
@ -95,24 +94,27 @@ enum SelectedItem {
|
|||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(
|
||||
config_path: Option<PathBuf>,
|
||||
theme: Theme,
|
||||
override_validation: bool,
|
||||
size_bypass: bool,
|
||||
skip_confirmation: bool,
|
||||
) -> Self {
|
||||
let tabs = linutil_core::get_tabs(!override_validation);
|
||||
pub fn new(args: Args) -> Self {
|
||||
let tabs = linutil_core::get_tabs(!args.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 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 {
|
||||
areas: None,
|
||||
theme,
|
||||
theme: args.theme,
|
||||
focus: Focus::List,
|
||||
tabs,
|
||||
current_tab: ListState::default().with_selected(Some(0)),
|
||||
longest_tab_display_len,
|
||||
visit_stack: vec![(root_id, 0usize)],
|
||||
selection: ListState::default().with_selected(Some(0)),
|
||||
filter: Filter::new(),
|
||||
|
@ -120,14 +122,14 @@ impl AppState {
|
|||
selected_commands: Vec::new(),
|
||||
drawable: false,
|
||||
#[cfg(feature = "tips")]
|
||||
tip: get_random_tip(),
|
||||
size_bypass,
|
||||
skip_confirmation,
|
||||
tip: crate::tips::get_random_tip(),
|
||||
size_bypass: args.size_bypass,
|
||||
skip_confirmation: args.skip_confirmation,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
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();
|
||||
|
@ -138,24 +140,41 @@ impl AppState {
|
|||
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]) {
|
||||
self.selected_commands = auto_execute_commands
|
||||
.iter()
|
||||
.filter_map(|name| self.tabs.iter().find_map(|tab| tab.find_command(name)))
|
||||
.filter_map(|name| self.find_command_by_name(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));
|
||||
self.spawn_confirmprompt();
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_confirmprompt(&mut self) {
|
||||
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),
|
||||
CONFIRM_PROMPT_FLOAT_SIZE,
|
||||
CONFIRM_PROMPT_FLOAT_SIZE,
|
||||
));
|
||||
}
|
||||
|
||||
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"])])
|
||||
|
@ -221,6 +240,8 @@ impl AppState {
|
|||
Shortcut::new("Previous theme", ["T"]),
|
||||
Shortcut::new("Next tab", ["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) {
|
||||
let terminal_size = frame.area();
|
||||
fn is_terminal_drawable(&mut self, terminal_size: Rect) -> bool {
|
||||
!self.size_bypass && (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH)
|
||||
}
|
||||
|
||||
if !self.size_bypass
|
||||
&& (terminal_size.height < MIN_HEIGHT || terminal_size.width < MIN_WIDTH)
|
||||
{
|
||||
pub fn draw(&mut self, frame: &mut Frame) {
|
||||
let area = frame.area();
|
||||
self.drawable = !self.is_terminal_drawable(area);
|
||||
if !self.drawable {
|
||||
let warning = Paragraph::new(format!(
|
||||
"Terminal size too small:\nWidth = {} Height = {}\n\nMinimum size:\nWidth = {} Height = {}",
|
||||
terminal_size.width,
|
||||
terminal_size.height,
|
||||
area.width,
|
||||
area.height,
|
||||
MIN_WIDTH,
|
||||
MIN_HEIGHT,
|
||||
))
|
||||
|
@ -253,80 +276,53 @@ impl AppState {
|
|||
Constraint::Length(5),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.split(terminal_size);
|
||||
.split(area);
|
||||
|
||||
self.drawable = false;
|
||||
return frame.render_widget(warning, centered_layout[1]);
|
||||
} else {
|
||||
self.drawable = true;
|
||||
}
|
||||
|
||||
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_block = Block::bordered().border_set(border::Set {
|
||||
top_left: " ",
|
||||
top_right: " ",
|
||||
bottom_left: " ",
|
||||
bottom_right: " ",
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: "*",
|
||||
horizontal_bottom: "*",
|
||||
});
|
||||
|
||||
let label = Paragraph::new(Line::from(vec![
|
||||
Span::styled(str1, Style::default().bold()),
|
||||
Span::styled(str2, Style::default().italic()),
|
||||
Span::styled("Linutil ", Style::default().bold()),
|
||||
Span::styled("by Chris Titus", Style::default().italic()),
|
||||
]))
|
||||
.block(label_block)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
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());
|
||||
.centered();
|
||||
|
||||
let (keybind_scope, shortcuts) = self.get_keybinds();
|
||||
|
||||
let keybind_render_width = terminal_size.width - 2;
|
||||
|
||||
let keybinds_block = Block::default()
|
||||
let keybinds_block = Block::bordered()
|
||||
.title(format!(" {} ", keybind_scope))
|
||||
.borders(Borders::ALL)
|
||||
.border_set(ratatui::symbols::border::ROUNDED);
|
||||
.border_set(border::ROUNDED);
|
||||
|
||||
let keybind_render_width = keybinds_block.inner(area).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 vertical = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Max(n_lines as u16 + 2),
|
||||
])
|
||||
.flex(Flex::Legacy)
|
||||
.margin(0)
|
||||
.split(frame.area());
|
||||
let vertical =
|
||||
Layout::vertical([Constraint::Percentage(0), Constraint::Max(keybind_len + 2)])
|
||||
.flex(Flex::Legacy)
|
||||
.split(area);
|
||||
|
||||
let horizontal = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Min(longest_tab_display_len as u16 + 5),
|
||||
Constraint::Percentage(100),
|
||||
])
|
||||
.split(vertical[0]);
|
||||
let horizontal = Layout::horizontal([
|
||||
Constraint::Min(self.longest_tab_display_len + 5),
|
||||
Constraint::Percentage(100),
|
||||
])
|
||||
.split(vertical[0]);
|
||||
|
||||
let left_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(1)])
|
||||
.split(horizontal[0]);
|
||||
let left_chunks =
|
||||
Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[0]);
|
||||
frame.render_widget(label, left_chunks[0]);
|
||||
|
||||
self.areas = Some(Areas {
|
||||
|
@ -346,36 +342,23 @@ impl AppState {
|
|||
Style::new().fg(self.theme.tab_color())
|
||||
};
|
||||
|
||||
let list = List::new(tabs)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_set(ratatui::symbols::border::ROUNDED),
|
||||
)
|
||||
let tab_list = List::new(tabs)
|
||||
.block(Block::bordered().border_set(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);
|
||||
frame.render_stateful_widget(tab_list, left_chunks[1], &mut self.current_tab);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.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]);
|
||||
let chunks =
|
||||
Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(horizontal[1]);
|
||||
|
||||
self.filter.draw_searchbar(frame, chunks[0], &self.theme);
|
||||
|
||||
let mut items: Vec<Line> = Vec::new();
|
||||
let mut task_items: Vec<Line> = Vec::new();
|
||||
let mut items: Vec<Line> = Vec::with_capacity(self.filter.item_list().len());
|
||||
|
||||
if !self.at_root() {
|
||||
items.push(
|
||||
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(
|
||||
|
@ -384,60 +367,37 @@ impl AppState {
|
|||
}| {
|
||||
let is_selected = self.selected_commands.contains(node);
|
||||
let (indicator, style) = if is_selected {
|
||||
(self.theme.multi_select_icon(), Style::default().bold())
|
||||
(self.theme.multi_select_icon(), Style::new().bold())
|
||||
} else {
|
||||
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 {
|
||||
Style::new()
|
||||
};
|
||||
("", ms_style)
|
||||
};
|
||||
if *has_children {
|
||||
Line::from(format!(
|
||||
"{} {} {}",
|
||||
self.theme.dir_icon(),
|
||||
node.name,
|
||||
indicator
|
||||
))
|
||||
.style(self.theme.dir_color())
|
||||
Line::styled(
|
||||
format!("{} {}", self.theme.dir_icon(), node.name,),
|
||||
self.theme.dir_color(),
|
||||
)
|
||||
.patch_style(style)
|
||||
} else {
|
||||
Line::from(format!(
|
||||
"{} {} {}",
|
||||
self.theme.cmd_icon(),
|
||||
node.name,
|
||||
indicator
|
||||
))
|
||||
.style(self.theme.cmd_color())
|
||||
let left_content =
|
||||
format!("{} {} {}", self.theme.cmd_icon(), node.name, indicator);
|
||||
let right_content = format!("{} ", node.task_list);
|
||||
let center_space = " ".repeat(
|
||||
chunks[1].width as usize - left_content.len() - right_content.len(),
|
||||
);
|
||||
Line::styled(
|
||||
format!("{}{}{}", left_content, center_space, right_content),
|
||||
self.theme.cmd_color(),
|
||||
)
|
||||
.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 {
|
||||
Style::default().reversed()
|
||||
} else {
|
||||
|
@ -451,7 +411,10 @@ impl AppState {
|
|||
};
|
||||
|
||||
#[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"))]
|
||||
let bottom_title = "";
|
||||
|
||||
|
@ -461,23 +424,14 @@ impl AppState {
|
|||
let list = List::new(items)
|
||||
.highlight_style(style)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL & !Borders::RIGHT)
|
||||
.border_set(ratatui::symbols::border::ROUNDED)
|
||||
Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.title(title)
|
||||
.title(task_list_title)
|
||||
.title_bottom(bottom_title),
|
||||
)
|
||||
.scroll_padding(1);
|
||||
frame.render_stateful_widget(list, list_chunks[0], &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);
|
||||
frame.render_stateful_widget(list, chunks[1], &mut self.selection);
|
||||
|
||||
match &mut self.focus {
|
||||
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
|
||||
// Prevents exiting the application even when a command is running
|
||||
// Add keys here which should work on both TabList and List
|
||||
if matches!(self.focus, Focus::TabList | Focus::List) {
|
||||
match key.code {
|
||||
KeyCode::Tab => {
|
||||
if self.current_tab.selected().unwrap() == self.tabs.len() - 1 {
|
||||
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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if matches!(self.focus, Focus::TabList | Focus::List)
|
||||
&& self.handle_tablist_and_list_keys(key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
match &mut self.focus {
|
||||
|
@ -626,15 +564,9 @@ impl AppState {
|
|||
|
||||
Focus::TabList => match key.code {
|
||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.focus = Focus::List,
|
||||
|
||||
KeyCode::Char('j') | KeyCode::Down => self.scroll_tab_down(),
|
||||
|
||||
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::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(),
|
||||
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(),
|
||||
_ => {}
|
||||
},
|
||||
|
@ -659,32 +586,38 @@ impl AppState {
|
|||
true
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self) {
|
||||
let len = self.filter.item_list().len();
|
||||
if len == 0 {
|
||||
return;
|
||||
fn handle_tablist_and_list_keys(&mut self, key: &KeyEvent) -> bool {
|
||||
match key.code {
|
||||
KeyCode::Tab => self.scroll_tab_down(),
|
||||
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,
|
||||
}
|
||||
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
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
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) {
|
||||
let len = self.filter.item_list().len();
|
||||
if len == 0 {
|
||||
return;
|
||||
if let Some(selected) = self.selection.selected() {
|
||||
if selected == 0 {
|
||||
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) {
|
||||
|
@ -747,11 +680,12 @@ impl AppState {
|
|||
fn get_selected_node(&self) -> Option<Rc<ListNode>> {
|
||||
let mut selected_index = self.selection.selected().unwrap_or(0);
|
||||
|
||||
if !self.at_root() && selected_index == 0 {
|
||||
return None;
|
||||
}
|
||||
if !self.at_root() {
|
||||
selected_index = selected_index.saturating_sub(1);
|
||||
if selected_index == 0 {
|
||||
return None;
|
||||
} else {
|
||||
selected_index = selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(item) = self.filter.item_list().get(selected_index) {
|
||||
|
@ -793,12 +727,12 @@ impl AppState {
|
|||
pub fn selected_item_is_dir(&self) -> bool {
|
||||
let mut selected_index = self.selection.selected().unwrap_or(0);
|
||||
|
||||
if !self.at_root() && selected_index == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.at_root() {
|
||||
selected_index = selected_index.saturating_sub(1);
|
||||
if selected_index == 0 {
|
||||
return false;
|
||||
} else {
|
||||
selected_index = selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
self.filter
|
||||
|
@ -822,7 +756,7 @@ impl AppState {
|
|||
if let Some(list_node) = self.get_selected_node() {
|
||||
let preview_title = format!("[Preview] - {}", list_node.name.as_str());
|
||||
let preview = FloatingText::from_command(&list_node.command, &preview_title, false);
|
||||
self.spawn_float(preview, 80, 80);
|
||||
self.spawn_float(preview, FLOAT_SIZE, FLOAT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -831,11 +765,19 @@ impl AppState {
|
|||
if !command_description.is_empty() {
|
||||
let description =
|
||||
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 {
|
||||
if self.selected_item_is_up_dir() {
|
||||
SelectedItem::UpDir
|
||||
|
@ -862,14 +804,7 @@ impl AppState {
|
|||
if self.skip_confirmation {
|
||||
self.handle_confirm_command();
|
||||
} else {
|
||||
let cmd_names = self
|
||||
.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));
|
||||
self.spawn_confirmprompt();
|
||||
}
|
||||
}
|
||||
SelectedItem::None => {}
|
||||
|
@ -884,7 +819,7 @@ impl AppState {
|
|||
.collect();
|
||||
|
||||
let command = RunningCommand::new(&commands);
|
||||
self.spawn_float(command, 80, 80);
|
||||
self.spawn_float(command, FLOAT_SIZE, FLOAT_SIZE);
|
||||
self.selected_commands.clear();
|
||||
}
|
||||
|
||||
|
@ -918,44 +853,21 @@ impl AppState {
|
|||
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) {
|
||||
let len = self.tabs.len();
|
||||
let current = self.current_tab.selected().unwrap_or(0);
|
||||
let next = if current + 1 >= len { 0 } else { current + 1 };
|
||||
|
||||
self.current_tab.select(Some(next));
|
||||
if self.current_tab.selected().unwrap() == self.tabs.len() - 1 {
|
||||
self.current_tab.select_first();
|
||||
} else {
|
||||
self.current_tab.select_next();
|
||||
}
|
||||
self.refresh_tab();
|
||||
}
|
||||
|
||||
fn scroll_tab_up(&mut self) {
|
||||
let len = self.tabs.len();
|
||||
let current = self.current_tab.selected().unwrap_or(0);
|
||||
let next = if current == 0 { len - 1 } else { current - 1 };
|
||||
|
||||
self.current_tab.select(Some(next));
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
#[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])
|
||||
}
|
||||
|
|
|
@ -14,91 +14,91 @@ pub enum Theme {
|
|||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn dir_color(&self) -> Color {
|
||||
pub const fn dir_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::Blue,
|
||||
Theme::Compatible => Color::Blue,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmd_color(&self) -> Color {
|
||||
pub const fn cmd_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::Rgb(204, 224, 208),
|
||||
Theme::Compatible => Color::LightGreen,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn multi_select_disabled_color(&self) -> Color {
|
||||
pub const fn multi_select_disabled_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::DarkGray,
|
||||
Theme::Compatible => Color::DarkGray,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_color(&self) -> Color {
|
||||
pub const fn tab_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::Rgb(255, 255, 85),
|
||||
Theme::Compatible => Color::Yellow,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dir_icon(&self) -> &'static str {
|
||||
pub const fn dir_icon(&self) -> &'static str {
|
||||
match self {
|
||||
Theme::Default => " ",
|
||||
Theme::Compatible => "[DIR]",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmd_icon(&self) -> &'static str {
|
||||
pub const fn cmd_icon(&self) -> &'static str {
|
||||
match self {
|
||||
Theme::Default => " ",
|
||||
Theme::Compatible => "[CMD]",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_icon(&self) -> &'static str {
|
||||
pub const fn tab_icon(&self) -> &'static str {
|
||||
match self {
|
||||
Theme::Default => " ",
|
||||
Theme::Compatible => ">> ",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn multi_select_icon(&self) -> &'static str {
|
||||
pub const fn multi_select_icon(&self) -> &'static str {
|
||||
match self {
|
||||
Theme::Default => "",
|
||||
Theme::Compatible => "*",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success_color(&self) -> Color {
|
||||
pub const fn success_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::Rgb(5, 255, 55),
|
||||
Theme::Compatible => Color::Green,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fail_color(&self) -> Color {
|
||||
pub const fn fail_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::Rgb(199, 55, 44),
|
||||
Theme::Compatible => Color::Red,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_color(&self) -> Color {
|
||||
pub const fn focused_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::LightBlue,
|
||||
Theme::Compatible => Color::LightBlue,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_preview_color(&self) -> Color {
|
||||
pub const fn search_preview_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::DarkGray,
|
||||
Theme::Compatible => Color::DarkGray,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unfocused_color(&self) -> Color {
|
||||
pub const fn unfocused_color(&self) -> Color {
|
||||
match self {
|
||||
Theme::Default => Color::Gray,
|
||||
Theme::Compatible => Color::Gray,
|
||||
|
|
13
tui/src/tips.rs
Normal file
13
tui/src/tips.rs
Normal 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]
|
||||
}
|
Loading…
Reference in New Issue
Block a user