From 2699238bb1fa6b2347a87ebf4083d3aba71366ff Mon Sep 17 00:00:00 2001 From: iff Date: Wed, 11 Jun 2025 12:10:30 +0200 Subject: [PATCH] feat: config file --- Cargo.lock | 2 + config.md | 24 +++ core/Cargo.toml | 4 + core/src/args.rs | 2 +- core/src/config.rs | 67 +++++++ core/src/data.rs | 288 +++++++++++++++++++++++++++++ core/src/init.rs | 17 ++ core/src/main.rs | 9 +- core/src/modes.rs | 3 +- core/src/rules.rs | 2 +- core/src/shell.rs | 295 +++--------------------------- core/src/style.rs | 6 +- core/src/suggestions.rs | 5 +- core/src/system.rs | 3 +- module-request-ai/src/requests.rs | 3 +- module-runtime-rules/src/main.rs | 8 +- 16 files changed, 452 insertions(+), 286 deletions(-) create mode 100644 config.md create mode 100644 core/src/config.rs create mode 100644 core/src/data.rs create mode 100644 core/src/init.rs diff --git a/Cargo.lock b/Cargo.lock index e38a03f..f7f67d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,7 +912,9 @@ dependencies = [ "pay-respects-utils", "regex-lite", "rust-i18n", + "serde", "sys-locale", + "toml", ] [[package]] diff --git a/config.md b/config.md new file mode 100644 index 0000000..7c6ad7f --- /dev/null +++ b/config.md @@ -0,0 +1,24 @@ +# Configuration File + +Configuration file for `pay-respects` is located at: + +- `$HOME/.config/pay-respects/config.toml` (*nix) +- `%APPDATA%/pay-respects/config.toml` (Windows) + +## Options + +All available options are listed in the following example file: +```toml +# maximum time in milliseconds for getting previous output +timeout = 3000 +# your preferred command for privileges +sudo = "run0" + +[package_manager] +# preferred package manager +package_manager = "pacman" + +# preferred installation method, can be limited with the package manager +# available options are: System, User, Temp +install_method = "System" +``` diff --git a/core/Cargo.toml b/core/Cargo.toml index ab84460..9f3379a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,6 +22,10 @@ askama = "0.13" inquire = "0.7" +# config file +toml = { version = "0.8" } +serde = { version = "1.0", features = ["derive"] } + pay-respects-parser = { version = "0.3", path = "../parser" } pay-respects-utils = { version ="0.1", path = "../utils"} itertools = "0.14.0" diff --git a/core/src/args.rs b/core/src/args.rs index 8eb5058..98780ca 100644 --- a/core/src/args.rs +++ b/core/src/args.rs @@ -1,4 +1,4 @@ -use crate::shell::{initialization, Init}; +use crate::{init::Init, shell::initialization}; use colored::Colorize; pub enum Status { diff --git a/core/src/config.rs b/core/src/config.rs new file mode 100644 index 0000000..6a09ea8 --- /dev/null +++ b/core/src/config.rs @@ -0,0 +1,67 @@ +use serde::Deserialize; + +#[allow(dead_code)] +#[derive(Deserialize)] +#[derive(Default)] +pub struct Config { + pub sudo: Option, + #[serde(default)] + pub timeout: Timeout, + #[serde(default)] + pub package_manager: PackageManagerConfig, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +#[derive(Default)] +pub struct PackageManagerConfig { + pub package_manager: Option, + #[serde(default)] + pub install_method: InstallMethod, +} + + + +#[derive(Deserialize)] +pub struct Timeout(pub u64); +impl Default for Timeout { + fn default() -> Self { + Timeout(3000) + } +} + +#[derive(Deserialize)] +#[derive(Default)] +pub enum InstallMethod { + #[default] + System, + User, + Temp, +} + +pub fn load_config() -> Config { + let path = config_path(); + let exists = std::path::Path::new(&path).exists(); + if exists { + let content = std::fs::read_to_string(&path).expect("Failed to read config file"); + let config: Config = toml::from_str(&content).unwrap_or_else(|_| { + eprintln!( + "Failed to parse config file at {}. Using default configuration.", + path + ); + Config::default() + }); + return config; + } + Config::default() +} + +fn config_path() -> String { + #[cfg(windows)] + let xdg_config_home = std::env::var("APPDATA").unwrap(); + #[cfg(not(windows))] + let xdg_config_home = std::env::var("XDG_CONFIG_HOME") + .unwrap_or_else(|_| std::env::var("HOME").unwrap() + "/.config"); + + format!("{}/pay-respects/config.toml", xdg_config_home) +} diff --git a/core/src/data.rs b/core/src/data.rs new file mode 100644 index 0000000..85f49e4 --- /dev/null +++ b/core/src/data.rs @@ -0,0 +1,288 @@ +use pay_respects_utils::evals::split_command; +use pay_respects_utils::files::get_path_files; +use pay_respects_utils::files::path_env_sep; + +use itertools::Itertools; + +use std::process::exit; + +use std::collections::HashMap; + +#[cfg(windows)] +use pay_respects_utils::files::path_convert; + +use crate::config::load_config; +use crate::config::Config; +use crate::shell::alias_map; +use crate::shell::builtin_commands; +use crate::shell::expand_alias_multiline; +use crate::shell::get_error; +use crate::shell::get_shell; +use crate::shell::last_command; +use crate::shell::run_mode; + +pub const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"]; + +#[derive(PartialEq)] +pub enum Mode { + Suggestion, + Echo, + NoConfirm, + Cnf, +} +pub struct Data { + pub shell: String, + pub env: Option, + pub command: String, + pub suggest: Option, + pub candidates: Vec, + pub split: Vec, + pub alias: Option>, + pub privilege: Option, + pub error: String, + pub executables: Vec, + pub modules: Vec, + pub fallbacks: Vec, + pub config: Config, + pub mode: Mode, +} + +impl Data { + pub fn init() -> Data { + let shell = get_shell(); + let command = last_command(&shell).trim().to_string(); + let alias = alias_map(&shell); + let mode = run_mode(); + let (mut executables, modules, fallbacks); + let lib_dir = { + if let Ok(lib_dir) = std::env::var("_PR_LIB") { + Some(lib_dir) + } else { + option_env!("_DEF_PR_LIB").map(|dir| dir.to_string()) + } + }; + + #[cfg(debug_assertions)] + eprintln!("lib_dir: {:?}", lib_dir); + + if lib_dir.is_none() { + (executables, modules, fallbacks) = { + let path_executables = get_path_files(); + let mut executables = vec![]; + let mut modules = vec![]; + let mut fallbacks = vec![]; + for exe in path_executables { + if exe.starts_with("_pay-respects-module-") { + modules.push(exe.to_string()); + } else if exe.starts_with("_pay-respects-fallback-") { + fallbacks.push(exe.to_string()); + } else { + executables.push(exe.to_string()); + } + } + modules.sort_unstable(); + fallbacks.sort_unstable(); + if alias.is_some() { + let alias = alias.as_ref().unwrap(); + for command in alias.keys() { + if executables.contains(command) { + continue; + } + executables.push(command.to_string()); + } + } + + (executables, modules, fallbacks) + }; + } else { + (executables, modules, fallbacks) = { + let mut modules = vec![]; + let mut fallbacks = vec![]; + let lib_dir = lib_dir.unwrap(); + let mut executables = get_path_files(); + if alias.is_some() { + let alias = alias.as_ref().unwrap(); + for command in alias.keys() { + if executables.contains(command) { + continue; + } + executables.push(command.to_string()); + } + } + + let path = lib_dir.split(path_env_sep()).collect::>(); + + for p in path { + #[cfg(windows)] + let p = path_convert(p); + + let files = match std::fs::read_dir(p) { + Ok(files) => files, + Err(_) => continue, + }; + for file in files { + let file = file.unwrap(); + let file_name = file.file_name().into_string().unwrap(); + let file_path = file.path(); + + if file_name.starts_with("_pay-respects-module-") { + modules.push(file_path.to_string_lossy().to_string()); + } else if file_name.starts_with("_pay-respects-fallback-") { + fallbacks.push(file_path.to_string_lossy().to_string()); + } + } + } + + modules.sort_unstable(); + fallbacks.sort_unstable(); + + (executables, modules, fallbacks) + }; + } + + let builtins = builtin_commands(&shell); + executables.extend(builtins.clone()); + executables = executables.iter().unique().cloned().collect(); + let config = load_config(); + + let mut init = Data { + shell, + env: None, + command, + suggest: None, + candidates: vec![], + alias, + split: vec![], + privilege: None, + error: "".to_string(), + executables, + modules, + fallbacks, + config, + mode, + }; + + init.split(); + init.extract_env(); + init.expand_command(); + if init.mode != Mode::Cnf { + init.update_error(None); + } + + #[cfg(debug_assertions)] + { + eprintln!("/// data initialization"); + eprintln!("shell: {}", init.shell); + eprintln!("env: {:?}", init.env); + eprintln!("command: {}", init.command); + eprintln!("error: {}", init.error); + eprintln!("modules: {:?}", init.modules); + eprintln!("fallbacks: {:?}", init.fallbacks); + } + + init + } + + pub fn expand_command(&mut self) { + if self.alias.is_none() { + return; + } + let alias = self.alias.as_ref().unwrap(); + if let Some(command) = expand_alias_multiline(alias, &self.command) { + #[cfg(debug_assertions)] + eprintln!("expand_command: {}", command); + self.update_command(&command); + } + } + + pub fn expand_suggest(&mut self) { + if self.alias.is_none() { + return; + } + let alias = self.alias.as_ref().unwrap(); + if let Some(suggest) = expand_alias_multiline(alias, self.suggest.as_ref().unwrap()) { + #[cfg(debug_assertions)] + eprintln!("expand_suggest: {}", suggest); + self.update_suggest(&suggest); + } + } + + pub fn split(&mut self) { + self.extract_privilege(); + let split = split_command(&self.command); + #[cfg(debug_assertions)] + eprintln!("split: {:?}", split); + if split.is_empty() { + eprintln!("{}", t!("empty-command")); + exit(1); + } + self.split = split; + } + + pub fn extract_privilege(&mut self) { + let command = { + let first = self.command.split_whitespace().next(); + if let Some(first) = first { + first.to_string() + } else { + return; + } + }; + if let Some(sudo) = self.config.sudo.as_ref() { + if command == *sudo { + self.privilege = Some(command.to_string()); + self.command = self.command.replacen(sudo, "", 1); + } + return; + } + if PRIVILEGE_LIST.contains(&command.as_str()) { + self.privilege = Some(command.to_string()); + self.command = self.command.replacen(&self.split[0], "", 1); + } + } + + pub fn extract_env(&mut self) { + let mut envs = vec![]; + loop { + let mut char = self.split[0].char_indices(); + char.next(); + let offset = char.offset(); + if self.split[0][offset..].contains("=") { + envs.push(self.split.remove(0)); + } else { + break; + } + } + if !envs.is_empty() { + self.env = Some(envs.join(" ")); + self.command = self.split.join(" "); + } + } + + pub fn update_error(&mut self, error: Option) { + if let Some(error) = error { + self.error = error + .to_lowercase() + .split_whitespace() + .collect::>() + .join(" "); + } else { + self.error = get_error(&self.shell, &self.command, self); + } + } + + pub fn update_command(&mut self, command: &str) { + self.command = command.to_string(); + self.split(); + } + + pub fn update_suggest(&mut self, suggest: &str) { + let split = split_command(suggest); + if PRIVILEGE_LIST.contains(&split[0].as_str()) { + self.suggest = Some(suggest.replacen(&split[0], "", 1)); + self.privilege = Some(split[0].clone()) + } else { + self.suggest = Some(suggest.to_string()); + }; + } +} diff --git a/core/src/init.rs b/core/src/init.rs new file mode 100644 index 0000000..973a503 --- /dev/null +++ b/core/src/init.rs @@ -0,0 +1,17 @@ +pub struct Init { + pub shell: String, + pub binary_path: String, + pub alias: String, + pub cnf: bool, +} + +impl Init { + pub fn new() -> Init { + Init { + shell: String::from(""), + binary_path: String::from(""), + alias: String::from("f"), + cnf: true, + } + } +} diff --git a/core/src/main.rs b/core/src/main.rs index 306e9ca..e693c8e 100644 --- a/core/src/main.rs +++ b/core/src/main.rs @@ -18,6 +18,9 @@ use std::env; use sys_locale::get_locale; mod args; +mod config; +mod data; +mod init; mod modes; mod rules; mod shell; @@ -51,7 +54,7 @@ fn main() -> Result<(), std::io::Error> { init.ok().unwrap() }; - use shell::Mode::*; + use data::Mode::*; match data.mode { Suggestion => modes::suggestion(&mut data), Echo => modes::echo(&mut data), @@ -62,7 +65,7 @@ fn main() -> Result<(), std::io::Error> { Ok(()) } -fn init() -> Result { +fn init() -> Result { let locale = { let sys_locale = { // use terminal locale if available @@ -95,5 +98,5 @@ fn init() -> Result { _ => {} } - Ok(shell::Data::init()) + Ok(data::Data::init()) } diff --git a/core/src/modes.rs b/core/src/modes.rs index 6c89d7a..814b625 100644 --- a/core/src/modes.rs +++ b/core/src/modes.rs @@ -6,7 +6,8 @@ use ui::Color; use pay_respects_utils::evals::best_matches_path; use pay_respects_utils::files::best_match_file; -use crate::shell::{shell_evaluated_commands, Data}; +use crate::data::Data; +use crate::shell::shell_evaluated_commands; use crate::style::highlight_difference; use crate::suggestions; use crate::suggestions::suggest_candidates; diff --git a/core/src/rules.rs b/core/src/rules.rs index 03b15b0..7722235 100644 --- a/core/src/rules.rs +++ b/core/src/rules.rs @@ -1,4 +1,4 @@ -use crate::shell::Data; +use crate::data::Data; use pay_respects_parser::parse_rules; use pay_respects_utils::evals::*; diff --git a/core/src/shell.rs b/core/src/shell.rs index 37c8ced..bc8ee90 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -1,9 +1,4 @@ -use pay_respects_utils::evals::split_command; -use pay_respects_utils::files::get_path_files; -use pay_respects_utils::files::path_env_sep; - use askama::Template; -use itertools::Itertools; use std::process::{exit, Stdio}; @@ -15,269 +10,19 @@ use std::time::Duration; #[cfg(windows)] use pay_respects_utils::files::path_convert; -pub const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"]; +use crate::data::{Data, Mode}; +use crate::init::Init; -#[derive(PartialEq)] -pub enum Mode { - Suggestion, - Echo, - NoConfirm, - Cnf, -} -pub struct Init { - pub shell: String, - pub binary_path: String, - pub alias: String, - pub cnf: bool, -} - -impl Init { - pub fn new() -> Init { - Init { - shell: String::from(""), - binary_path: String::from(""), - alias: String::from("f"), - cnf: true, - } - } -} - -pub struct Data { - pub shell: String, - pub env: Option, - pub command: String, - pub suggest: Option, - pub candidates: Vec, - pub split: Vec, - pub alias: Option>, - pub privilege: Option, - pub error: String, - pub executables: Vec, - pub modules: Vec, - pub fallbacks: Vec, - pub mode: Mode, -} - -impl Data { - pub fn init() -> Data { - let shell = get_shell(); - let command = last_command(&shell).trim().to_string(); - let alias = alias_map(&shell); - let mode = run_mode(); - let (mut executables, modules, fallbacks); - let lib_dir = { - if let Ok(lib_dir) = std::env::var("_PR_LIB") { - Some(lib_dir) - } else { - option_env!("_DEF_PR_LIB").map(|dir| dir.to_string()) - } - }; - - #[cfg(debug_assertions)] - eprintln!("lib_dir: {:?}", lib_dir); - - if lib_dir.is_none() { - (executables, modules, fallbacks) = { - let path_executables = get_path_files(); - let mut executables = vec![]; - let mut modules = vec![]; - let mut fallbacks = vec![]; - for exe in path_executables { - if exe.starts_with("_pay-respects-module-") { - modules.push(exe.to_string()); - } else if exe.starts_with("_pay-respects-fallback-") { - fallbacks.push(exe.to_string()); - } else { - executables.push(exe.to_string()); - } - } - modules.sort_unstable(); - fallbacks.sort_unstable(); - if alias.is_some() { - let alias = alias.as_ref().unwrap(); - for command in alias.keys() { - if executables.contains(command) { - continue; - } - executables.push(command.to_string()); - } - } - - (executables, modules, fallbacks) - }; - } else { - (executables, modules, fallbacks) = { - let mut modules = vec![]; - let mut fallbacks = vec![]; - let lib_dir = lib_dir.unwrap(); - let mut executables = get_path_files(); - if alias.is_some() { - let alias = alias.as_ref().unwrap(); - for command in alias.keys() { - if executables.contains(command) { - continue; - } - executables.push(command.to_string()); - } - } - - let path = lib_dir.split(path_env_sep()).collect::>(); - - for p in path { - #[cfg(windows)] - let p = path_convert(p); - - let files = match std::fs::read_dir(p) { - Ok(files) => files, - Err(_) => continue, - }; - for file in files { - let file = file.unwrap(); - let file_name = file.file_name().into_string().unwrap(); - let file_path = file.path(); - - if file_name.starts_with("_pay-respects-module-") { - modules.push(file_path.to_string_lossy().to_string()); - } else if file_name.starts_with("_pay-respects-fallback-") { - fallbacks.push(file_path.to_string_lossy().to_string()); - } - } - } - - modules.sort_unstable(); - fallbacks.sort_unstable(); - - (executables, modules, fallbacks) - }; - } - - let builtins = builtin_commands(&shell); - executables.extend(builtins.clone()); - executables = executables.iter().unique().cloned().collect(); - - let mut init = Data { - shell, - env: None, - command, - suggest: None, - candidates: vec![], - alias, - split: vec![], - privilege: None, - error: "".to_string(), - executables, - modules, - fallbacks, - mode, - }; - - init.split(); - init.extract_env(); - init.expand_command(); - if init.mode != Mode::Cnf { - init.update_error(None); - } - - #[cfg(debug_assertions)] - { - eprintln!("/// data initialization"); - eprintln!("shell: {}", init.shell); - eprintln!("env: {:?}", init.env); - eprintln!("command: {}", init.command); - eprintln!("error: {}", init.error); - eprintln!("modules: {:?}", init.modules); - eprintln!("fallbacks: {:?}", init.fallbacks); - } - - init - } - - pub fn expand_command(&mut self) { - if self.alias.is_none() { - return; - } - let alias = self.alias.as_ref().unwrap(); - if let Some(command) = expand_alias_multiline(alias, &self.command) { - #[cfg(debug_assertions)] - eprintln!("expand_command: {}", command); - self.update_command(&command); - } - } - - pub fn expand_suggest(&mut self) { - if self.alias.is_none() { - return; - } - let alias = self.alias.as_ref().unwrap(); - if let Some(suggest) = expand_alias_multiline(alias, self.suggest.as_ref().unwrap()) { - #[cfg(debug_assertions)] - eprintln!("expand_suggest: {}", suggest); - self.update_suggest(&suggest); - } - } - - pub fn split(&mut self) { - let mut split = split_command(&self.command); - if PRIVILEGE_LIST.contains(&split[0].as_str()) { - self.command = self.command.replacen(&split[0], "", 1).trim().to_string(); - self.privilege = Some(split.remove(0)) - } - #[cfg(debug_assertions)] - eprintln!("split: {:?}", split); - if split.is_empty() { - eprintln!("{}", t!("empty-command")); - exit(1); - } - self.split = split; - } - - pub fn extract_env(&mut self) { - let mut envs = vec![]; - loop { - let mut char = self.split[0].char_indices(); - char.next(); - let offset = char.offset(); - if self.split[0][offset..].contains("=") { - envs.push(self.split.remove(0)); - } else { - break; - } - } - if !envs.is_empty() { - self.env = Some(envs.join(" ")); - self.command = self.split.join(" "); - } - } - - pub fn update_error(&mut self, error: Option) { - if let Some(error) = error { - self.error = error - .to_lowercase() - .split_whitespace() - .collect::>() - .join(" "); - } else { - self.error = get_error(&self.shell, &self.command); - } - } - - pub fn update_command(&mut self, command: &str) { - self.command = command.to_string(); - self.split(); - } - - pub fn update_suggest(&mut self, suggest: &str) { - let split = split_command(suggest); - if PRIVILEGE_LIST.contains(&split[0].as_str()) { - self.suggest = Some(suggest.replacen(&split[0], "", 1)); - self.privilege = Some(split[0].clone()) - } else { - self.suggest = Some(suggest.to_string()); - }; - } -} +const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"]; pub fn elevate(data: &mut Data, command: &mut String) { + if is_privileged(command, data) { + return; + } + if data.config.sudo.is_some() { + *command = format!("{} {}", data.config.sudo.as_ref().unwrap(), command); + return; + } for privilege in PRIVILEGE_LIST.iter() { if data.executables.contains(&privilege.to_string()) { *command = format!("{} {}", privilege, command); @@ -286,6 +31,13 @@ pub fn elevate(data: &mut Data, command: &mut String) { } } +pub fn is_privileged(command: &str, data: &Data) -> bool { + if data.config.sudo.is_some() { + return command == data.config.sudo.as_ref().unwrap(); + } + PRIVILEGE_LIST.contains(&command) +} + pub fn add_candidates_no_dup( command: &str, candidates: &mut Vec, @@ -302,13 +54,15 @@ pub fn add_candidates_no_dup( } } -pub fn get_error(shell: &str, command: &str) -> String { +pub fn get_error(shell: &str, command: &str, data: &Data) -> String { let error_msg = std::env::var("_PR_ERROR_MSG"); let error = if let Ok(error_msg) = error_msg { std::env::remove_var("_PR_ERROR_MSG"); error_msg } else { - error_output_threaded(shell, command) + let timeout = data.config.timeout.0; + eprintln!("time out is: {}", timeout); + error_output_threaded(shell, command, timeout) }; error .to_lowercase() @@ -317,7 +71,7 @@ pub fn get_error(shell: &str, command: &str) -> String { .join(" ") } -pub fn error_output_threaded(shell: &str, command: &str) -> String { +pub fn error_output_threaded(shell: &str, command: &str, timeout: u64) -> String { let (sender, receiver) = channel(); thread::scope(|s| { @@ -334,7 +88,7 @@ pub fn error_output_threaded(shell: &str, command: &str) -> String { .expect("failed to send output"); }); - match receiver.recv_timeout(Duration::from_secs(3)) { + match receiver.recv_timeout(Duration::from_millis(timeout)) { Ok(output) => match output.stderr.is_empty() { true => String::from_utf8_lossy(&output.stdout).to_string(), false => String::from_utf8_lossy(&output.stderr).to_string(), @@ -630,7 +384,8 @@ pub fn get_shell() -> String { } } -fn builtin_commands(shell: &str) -> Vec { +#[allow(unused_variables)] +pub fn builtin_commands(shell: &str) -> Vec { // TODO: add the commands for each shell // these should cover most of the builtin commands // (maybe with false positives) diff --git a/core/src/style.rs b/core/src/style.rs index 6186d41..f358179 100644 --- a/core/src/style.rs +++ b/core/src/style.rs @@ -1,5 +1,5 @@ -use crate::shell::Data; -use crate::shell::PRIVILEGE_LIST; +use crate::data::Data; +use crate::shell::is_privileged; use colored::*; use pay_respects_utils::evals::split_command; @@ -19,7 +19,7 @@ pub fn highlight_difference(data: &Data, suggested_command: &str) -> Option Result<(), std::io::Error> { } rules::runtime_match(&executable, &shell, &last_command, &error_msg, &executables); - rules::runtime_match("_PR_GENERAL", &shell, &last_command, &error_msg, &executables); + rules::runtime_match( + "_PR_GENERAL", + &shell, + &last_command, + &error_msg, + &executables, + ); Ok(()) }