diff --git a/README.md b/README.md index e6c54db5..78bd1d0e 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,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 aa0693c1..986d9ac1 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,4 @@ +mod config; mod inner; use std::rc::Rc; @@ -6,6 +7,7 @@ pub use ego_tree; use ego_tree::Tree; use std::path::PathBuf; +pub use config::Config; pub use inner::{get_tabs, TabList}; #[derive(Clone, Hash, Eq, PartialEq)] @@ -34,3 +36,16 @@ pub struct ListNode { pub task_list: String, pub multi_select: bool, } + +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/docs/roadmap.md b/docs/roadmap.md index cc334348..71a7aaf5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -13,7 +13,7 @@ ## Milestones ### Q4 2024 - [x] Finish the foundation of the project's CLI -- [ ] Implement CLI arguments and configuration support +- [x] Implement CLI arguments and configuration support - [ ] Add an option for logging script executions ### Q1 2025 diff --git a/man/linutil.1 b/man/linutil.1 index 5150b351..f846614f 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 d8f7d0d1..7a9f4067 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, }; @@ -30,6 +31,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")] @@ -52,6 +55,7 @@ fn main() -> io::Result<()> { let args = Args::parse(); let mut state = AppState::new( + args.config, args.theme, args.override_validation, args.size_bypass, diff --git a/tui/src/state.rs b/tui/src/state.rs index a164e174..5ee34079 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -8,7 +8,7 @@ use crate::{ theme::Theme, }; -use linutil_core::{ego_tree::NodeId, ListNode, TabList}; +use linutil_core::{ego_tree::NodeId, Config, ListNode, TabList}; #[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; const MIN_WIDTH: u16 = 100; @@ -87,6 +88,7 @@ enum SelectedItem { impl AppState { pub fn new( + config_path: Option, theme: Theme, override_validation: bool, size_bypass: bool, @@ -95,6 +97,8 @@ impl AppState { let tabs = linutil_core::get_tabs(!override_validation); let root_id = tabs[0].tree.root().id(); + let auto_execute_commands = config_path.map(|path| Config::from_file(&path).auto_execute); + let mut state = Self { theme, focus: Focus::List, @@ -113,9 +117,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"])])