diff --git a/README.md b/README.md index f8aa0ea3..1031e625 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,28 @@ Note that crates installed using `cargo install` require manual updating with `c +## Configuration + +Linutil supports configuration through a TOML config file. Path to the file can be specified with `--config` (or `-c`). + +Available options: +- `auto_execute` - a list of commands to execute automatically (can be combined with `--skip-confirmation`) + +Example config: +```toml +# example_config.toml + +auto_execute = [ + "Fastfetch", + "Alacritty", + "Kitty" +] +``` + +```bash +linutil --config /path/to/example_config.toml +``` + ## 💖 Support If you find Linutil helpful, please consider giving it a ⭐️ to show your support! diff --git a/core/src/config.rs b/core/src/config.rs new file mode 100644 index 00000000..d4f5e5c5 --- /dev/null +++ b/core/src/config.rs @@ -0,0 +1,28 @@ +use serde::Deserialize; +use std::path::Path; +use std::process; + +#[derive(Deserialize)] +pub struct Config { + pub auto_execute: Vec, +} + +impl Config { + pub fn from_file(path: &Path) -> Self { + let content = match std::fs::read_to_string(path) { + Ok(content) => content, + Err(e) => { + eprintln!("Failed to read config file {}: {}", path.display(), e); + process::exit(1); + } + }; + + match toml::from_str(&content) { + Ok(config) => config, + Err(e) => { + eprintln!("Failed to parse config file: {}", e); + process::exit(1); + } + } + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index b7cd631e..b5488135 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,4 @@ +mod config; mod inner; use std::rc::Rc; @@ -5,6 +6,7 @@ use std::rc::Rc; use ego_tree::Tree; use std::path::PathBuf; +pub use config::Config; pub use inner::get_tabs; #[derive(Clone, Hash, Eq, PartialEq)] @@ -33,3 +35,16 @@ pub struct ListNode { pub command: Command, pub task_list: String, } + +impl Tab { + pub fn find_command(&self, name: &str) -> Option> { + self.tree.root().descendants().find_map(|node| { + let value = node.value(); + if value.name == name && !node.has_children() { + Some(value.clone()) + } else { + None + } + }) + } +} diff --git a/man/linutil.1 b/man/linutil.1 index d31cf879..2a01435b 100644 --- a/man/linutil.1 +++ b/man/linutil.1 @@ -22,6 +22,10 @@ curl -fsSL https://christitus.com/linux | sh curl -fsSL https://christitus.com/linuxdev | sh .SH OPTIONS +.TP +\fB\-c\fR, \fB\-\-config\fR \fI\fR +Path to the configuration file. + .TP \fB\-t\fR, \fB\-\-theme\fR \fI\fR Set the theme to use in the TUI. diff --git a/tui/src/main.rs b/tui/src/main.rs index 801e3b1d..5cbefffe 100644 --- a/tui/src/main.rs +++ b/tui/src/main.rs @@ -9,6 +9,7 @@ mod theme; use std::{ io::{self, stdout}, + path::PathBuf, time::Duration, }; @@ -26,6 +27,8 @@ use state::AppState; // Linux utility toolbox #[derive(Debug, Parser)] struct Args { + #[arg(short, long, help = "Path to the configuration file")] + config: Option, #[arg(short, long, value_enum)] #[arg(default_value_t = Theme::Default)] #[arg(help = "Set the theme to use in the application")] @@ -38,7 +41,7 @@ struct Args { fn main() -> io::Result<()> { let args = Args::parse(); - let mut state = AppState::new(args.theme, args.override_validation); + let mut state = AppState::new(args.theme, args.override_validation, args.config); stdout().execute(EnterAlternateScreen)?; enable_raw_mode()?; diff --git a/tui/src/state.rs b/tui/src/state.rs index 4e398ee7..04a5f887 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -9,7 +9,7 @@ use crate::{ }; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ego_tree::NodeId; -use linutil_core::{ListNode, Tab}; +use linutil_core::{Config, ListNode, Tab}; #[cfg(feature = "tips")] use rand::Rng; use ratatui::{ @@ -19,6 +19,7 @@ use ratatui::{ widgets::{Block, Borders, List, ListState, Paragraph}, Frame, }; +use std::path::PathBuf; use std::rc::Rc; use temp_dir::TempDir; @@ -79,10 +80,12 @@ pub struct ListEntry { } impl AppState { - pub fn new(theme: Theme, override_validation: bool) -> Self { + pub fn new(theme: Theme, override_validation: bool, config_path: Option) -> Self { let (temp_dir, tabs) = linutil_core::get_tabs(!override_validation); let root_id = tabs[0].tree.root().id(); + let auto_execute_commands = config_path.map(|path| Config::from_file(&path).auto_execute); + let mut state = Self { _temp_dir: temp_dir, theme, @@ -100,9 +103,31 @@ impl AppState { }; state.update_items(); + if let Some(auto_execute_commands) = auto_execute_commands { + state.handle_initial_auto_execute(&auto_execute_commands); + } + state } + fn handle_initial_auto_execute(&mut self, auto_execute_commands: &[String]) { + self.selected_commands = auto_execute_commands + .iter() + .filter_map(|name| self.tabs.iter().find_map(|tab| tab.find_command(name))) + .collect(); + + if !self.selected_commands.is_empty() { + let cmd_names: Vec<_> = self + .selected_commands + .iter() + .map(|node| node.name.as_str()) + .collect(); + + let prompt = ConfirmPrompt::new(&cmd_names); + self.focus = Focus::ConfirmationPrompt(Float::new(Box::new(prompt), 40, 40)); + } + } + fn get_list_item_shortcut(&self) -> Box<[Shortcut]> { if self.selected_item_is_dir() { Box::new([Shortcut::new("Go to selected dir", ["l", "Right", "Enter"])])