feat: config file

This commit is contained in:
iff
2025-06-11 12:10:30 +02:00
parent 6ba4591db9
commit 2699238bb1
16 changed files with 452 additions and 286 deletions

2
Cargo.lock generated
View File

@@ -912,7 +912,9 @@ dependencies = [
"pay-respects-utils",
"regex-lite",
"rust-i18n",
"serde",
"sys-locale",
"toml",
]
[[package]]

24
config.md Normal file
View File

@@ -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"
```

View File

@@ -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"

View File

@@ -1,4 +1,4 @@
use crate::shell::{initialization, Init};
use crate::{init::Init, shell::initialization};
use colored::Colorize;
pub enum Status {

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

@@ -0,0 +1,67 @@
use serde::Deserialize;
#[allow(dead_code)]
#[derive(Deserialize)]
#[derive(Default)]
pub struct Config {
pub sudo: Option<String>,
#[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<String>,
#[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)
}

288
core/src/data.rs Normal file
View File

@@ -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<String>,
pub command: String,
pub suggest: Option<String>,
pub candidates: Vec<String>,
pub split: Vec<String>,
pub alias: Option<HashMap<String, String>>,
pub privilege: Option<String>,
pub error: String,
pub executables: Vec<String>,
pub modules: Vec<String>,
pub fallbacks: Vec<String>,
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::<Vec<&str>>();
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<String>) {
if let Some(error) = error {
self.error = error
.to_lowercase()
.split_whitespace()
.collect::<Vec<&str>>()
.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());
};
}
}

17
core/src/init.rs Normal file
View File

@@ -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,
}
}
}

View File

@@ -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<shell::Data, args::Status> {
fn init() -> Result<data::Data, args::Status> {
let locale = {
let sys_locale = {
// use terminal locale if available
@@ -95,5 +98,5 @@ fn init() -> Result<shell::Data, args::Status> {
_ => {}
}
Ok(shell::Data::init())
Ok(data::Data::init())
}

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
use crate::shell::Data;
use crate::data::Data;
use pay_respects_parser::parse_rules;
use pay_respects_utils::evals::*;

View File

@@ -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<String>,
pub command: String,
pub suggest: Option<String>,
pub candidates: Vec<String>,
pub split: Vec<String>,
pub alias: Option<HashMap<String, String>>,
pub privilege: Option<String>,
pub error: String,
pub executables: Vec<String>,
pub modules: Vec<String>,
pub fallbacks: Vec<String>,
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::<Vec<&str>>();
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<String>) {
if let Some(error) = error {
self.error = error
.to_lowercase()
.split_whitespace()
.collect::<Vec<&str>>()
.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<String>,
@@ -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<String> {
#[allow(unused_variables)]
pub fn builtin_commands(shell: &str) -> Vec<String> {
// TODO: add the commands for each shell
// these should cover most of the builtin commands
// (maybe with false positives)

View File

@@ -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<Stri
return None;
}
let privileged = PRIVILEGE_LIST.contains(&split_suggested_command[0].as_str());
let privileged = is_privileged(&split_suggested_command[0], data);
let mut old_entries = Vec::new();
for command in &split_suggested_command {

View File

@@ -7,10 +7,9 @@ use colored::Colorize;
use inquire::*;
use ui::Color;
use crate::data::Data;
use crate::rules::match_pattern;
use crate::shell::{
add_candidates_no_dup, module_output, shell_evaluated_commands, shell_syntax, Data,
};
use crate::shell::{add_candidates_no_dup, module_output, shell_evaluated_commands, shell_syntax};
use crate::style::highlight_difference;
pub fn suggest_candidates(data: &mut Data) {

View File

@@ -1,5 +1,6 @@
use crate::data::Data;
use crate::shell::command_output_or_error;
use crate::shell::{command_output, elevate, Data};
use crate::shell::{command_output, elevate};
use colored::Colorize;
use std::io::stderr;
use std::process::Command;

View File

@@ -73,8 +73,7 @@ pub async fn ai_suggestion(last_command: &str, error_msg: &str, locale: &str) {
map.insert("error_msg", error_msg);
let user_locale = {
let locale = std::env::var("_PR_AI_LOCALE")
.unwrap_or_else(|_| locale.to_string());
let locale = std::env::var("_PR_AI_LOCALE").unwrap_or_else(|_| locale.to_string());
if locale.len() < 2 {
"en-US".to_string()
} else {

View File

@@ -35,6 +35,12 @@ fn main() -> 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(())
}