feat: Add automation based on config file (#836)

* feat: Add automation based on config file

* docs: add configuration to the manpage & README

* update roadmap

---------

Co-authored-by: Adam Perkowski <adas1per@protonmail.com>
Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
Jeevitha Kannan K S 2024-11-08 00:51:37 +05:30 committed by GitHub
parent e8e1a36a63
commit 9f0863729f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 101 additions and 2 deletions

View File

@ -100,6 +100,28 @@ Note that crates installed using `cargo install` require manual updating with `c
</details> </details>
## 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 ## 💖 Support
If you find Linutil helpful, please consider giving it a ⭐️ to show your support! If you find Linutil helpful, please consider giving it a ⭐️ to show your support!

28
core/src/config.rs Normal file
View File

@ -0,0 +1,28 @@
use serde::Deserialize;
use std::path::Path;
use std::process;
#[derive(Deserialize)]
pub struct Config {
pub auto_execute: Vec<String>,
}
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);
}
}
}
}

View File

@ -1,3 +1,4 @@
mod config;
mod inner; mod inner;
use std::rc::Rc; use std::rc::Rc;
@ -6,6 +7,7 @@ pub use ego_tree;
use ego_tree::Tree; use ego_tree::Tree;
use std::path::PathBuf; use std::path::PathBuf;
pub use config::Config;
pub use inner::{get_tabs, TabList}; pub use inner::{get_tabs, TabList};
#[derive(Clone, Hash, Eq, PartialEq)] #[derive(Clone, Hash, Eq, PartialEq)]
@ -34,3 +36,16 @@ 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

@ -13,7 +13,7 @@
## Milestones ## Milestones
### Q4 2024 ### Q4 2024
- [x] Finish the foundation of the project's CLI - [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 - [ ] Add an option for logging script executions
### Q1 2025 ### Q1 2025

View File

@ -22,6 +22,10 @@ curl -fsSL https://christitus.com/linux | sh
curl -fsSL https://christitus.com/linuxdev | sh curl -fsSL https://christitus.com/linuxdev | sh
.SH OPTIONS .SH OPTIONS
.TP
\fB\-c\fR, \fB\-\-config\fR \fI<path>\fR
Path to the configuration file.
.TP .TP
\fB\-t\fR, \fB\-\-theme\fR \fI<theme>\fR \fB\-t\fR, \fB\-\-theme\fR \fI<theme>\fR
Set the theme to use in the TUI. Set the theme to use in the TUI.

View File

@ -9,6 +9,7 @@ mod theme;
use std::{ use std::{
io::{self, stdout}, io::{self, stdout},
path::PathBuf,
time::Duration, time::Duration,
}; };
@ -30,6 +31,8 @@ use state::AppState;
// Linux utility toolbox // Linux utility toolbox
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct Args { struct Args {
#[arg(short, long, help = "Path to the configuration file")]
config: Option<PathBuf>,
#[arg(short, long, value_enum)] #[arg(short, long, value_enum)]
#[arg(default_value_t = Theme::Default)] #[arg(default_value_t = Theme::Default)]
#[arg(help = "Set the theme to use in the application")] #[arg(help = "Set the theme to use in the application")]
@ -52,6 +55,7 @@ fn main() -> io::Result<()> {
let args = Args::parse(); let args = Args::parse();
let mut state = AppState::new( let mut state = AppState::new(
args.config,
args.theme, args.theme,
args.override_validation, args.override_validation,
args.size_bypass, args.size_bypass,

View File

@ -8,7 +8,7 @@ use crate::{
theme::Theme, theme::Theme,
}; };
use linutil_core::{ego_tree::NodeId, ListNode, TabList}; use linutil_core::{ego_tree::NodeId, Config, ListNode, TabList};
#[cfg(feature = "tips")] #[cfg(feature = "tips")]
use rand::Rng; use rand::Rng;
use ratatui::{ use ratatui::{
@ -19,6 +19,7 @@ use ratatui::{
widgets::{Block, Borders, List, ListState, Paragraph}, widgets::{Block, Borders, List, ListState, Paragraph},
Frame, Frame,
}; };
use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
const MIN_WIDTH: u16 = 100; const MIN_WIDTH: u16 = 100;
@ -87,6 +88,7 @@ enum SelectedItem {
impl AppState { impl AppState {
pub fn new( pub fn new(
config_path: Option<PathBuf>,
theme: Theme, theme: Theme,
override_validation: bool, override_validation: bool,
size_bypass: bool, size_bypass: bool,
@ -95,6 +97,8 @@ impl AppState {
let tabs = linutil_core::get_tabs(!override_validation); 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 mut state = Self { let mut state = Self {
theme, theme,
focus: Focus::List, focus: Focus::List,
@ -113,9 +117,31 @@ impl AppState {
}; };
state.update_items(); state.update_items();
if let Some(auto_execute_commands) = auto_execute_commands {
state.handle_initial_auto_execute(&auto_execute_commands);
}
state 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]> { fn get_list_item_shortcut(&self) -> Box<[Shortcut]> {
if self.selected_item_is_dir() { if self.selected_item_is_dir() {
Box::new([Shortcut::new("Go to selected dir", ["l", "Right", "Enter"])]) Box::new([Shortcut::new("Go to selected dir", ["l", "Right", "Enter"])])