From 309129ab662a9183ea52a3b7184c011961c6ec85 Mon Sep 17 00:00:00 2001 From: iff Date: Fri, 15 Nov 2024 15:43:04 +0100 Subject: [PATCH] feat!: runtime rules --- Cargo.lock | 2 +- Cargo.toml | 10 ++- src/main.rs | 5 ++ src/replaces.rs | 172 +++++++++++++++++++++++++++++++++++++++++++ src/runtime_rules.rs | 135 +++++++++++++++++++++++++++++++++ src/suggestions.rs | 19 ++++- 6 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 src/replaces.rs create mode 100644 src/runtime_rules.rs diff --git a/Cargo.lock b/Cargo.lock index df0809e..d299184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,7 +244,7 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pay-respects" -version = "0.4.17" +version = "0.4.18" dependencies = [ "colored", "pay-respects-parser", diff --git a/Cargo.toml b/Cargo.toml index 53fdd38..da747b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,15 @@ sys-locale = "0.3.2" rust-i18n = "3" regex-lite = "0.1" -pay-respects-parser = "0.2.2" +toml = { version = "0.7", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } + +pay-respects-parser = "0.2.3" +# pay-respects-parser = { path = "../pay-respects-parser" } + + +[features] +runtime-rules = ["dep:serde", "dep:toml"] [profile.release] strip = true diff --git a/src/main.rs b/src/main.rs index 72bbaf1..5c53432 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,11 @@ mod shell; mod style; mod suggestions; +#[cfg(feature = "runtime-rules")] +mod replaces; +#[cfg(feature = "runtime-rules")] +mod runtime_rules; + #[macro_use] extern crate rust_i18n; i18n!("i18n", fallback = "en", minify_key = true); diff --git a/src/replaces.rs b/src/replaces.rs new file mode 100644 index 0000000..fffd65e --- /dev/null +++ b/src/replaces.rs @@ -0,0 +1,172 @@ +use crate::suggestions::*; + +pub fn eval_placeholder( + string: &str, + start: &str, + end: &str, +) -> (std::ops::Range, std::ops::Range) { + let start_index = string.find(start).unwrap(); + let end_index = string[start_index..].find(end).unwrap() + start_index + end.len(); + + let placeholder = start_index..end_index; + + let args = start_index + start.len()..end_index - end.len(); + + (placeholder, args) +} + +pub fn opts(suggest: &mut String, last_command: &mut String) { + while suggest.contains("{{opt::") { + let (placeholder, args) = eval_placeholder(suggest, "{{opt::", "}}"); + + let opt = &suggest[args.to_owned()]; + let regex = opt.trim(); + + let command = opt_regex(regex, last_command); + suggest.replace_range(placeholder, &command) + } +} + +pub fn cmd_reg(suggest: &mut String, last_command: &mut String) { + while suggest.contains("{{cmd::") { + let (placeholder, args) = eval_placeholder(suggest, "{{cmd::", "}}"); + + let regex = suggest[args.to_owned()].trim(); + + let command = cmd_regex(regex, last_command); + suggest.replace_range(placeholder, &command) + } +} + +pub fn err(suggest: &mut String, error_msg: &str) { + while suggest.contains("{{err::") { + let (placeholder, args) = eval_placeholder(suggest, "{{err::", "}}"); + + let regex = suggest[args.to_owned()].trim(); + + let command = err_regex(regex, error_msg); + suggest.replace_range(placeholder, &command) + } +} + +pub fn command(suggest: &mut String, split_command: &Vec) { + while suggest.contains("{{command") { + let (placeholder, args) = eval_placeholder(suggest, "{{command", "}}"); + + let range = suggest[args.to_owned()].trim_matches(|c| c == '[' || c == ']'); + if let Some((start, end)) = range.split_once(':') { + let mut start_index = start.parse::().unwrap_or(0); + if start_index < 0 { + start_index = split_command.len() as i32 - start_index; + }; + let mut end_index; + let parsed_end = end.parse::(); + if parsed_end.is_err() { + end_index = split_command.len() as i32; + } else { + end_index = parsed_end.unwrap(); + if end_index < 0 { + end_index = split_command.len() as i32 - end_index + 1; + } else { + end_index = end_index + 1; + } + }; + + let command = split_command[start_index as usize..end_index as usize].join(" "); + + suggest.replace_range(placeholder, &command); + } else { + let range = range.parse::().unwrap_or(0); + let command = &split_command[range]; + + suggest.replace_range(placeholder, &command); + } + } +} + +pub fn typo(suggest: &mut String, split_command: &Vec) { + while suggest.contains("{{typo") { + let (placeholder, args) = eval_placeholder(suggest, "{{typo", "}}"); + + let index = if suggest.contains('[') { + let split = suggest[args.to_owned()] + .split(&['[', ']']) + .collect::>(); + let command_index = split[1]; + if !command_index.contains(':') { + let command_index = command_index.parse::().unwrap(); + + let index = if command_index < 0 { + split_command.len() as i32 - command_index + } else { + command_index + }; + index as usize..index as usize + 1 + } else { + let (start, end) = command_index.split_once(':').unwrap(); + let start = start.parse::().unwrap_or(0); + let start_index = if start < 0 { + split_command.len() as i32 - start + } else { + start + }; + let end = end.parse::(); + let end_index = if end.is_err() { + split_command.len() as i32 + } else { + let end = end.unwrap(); + if end < 0 { + split_command.len() as i32 - end + 1 + } else { + end + 1 + } + }; + + start_index as usize..end_index as usize + } + } else { + unreachable!("Typo suggestion must have a command index"); + }; + + let match_list; + if suggest.contains('(') { + let split = suggest[args.to_owned()] + .split_once("(") + .unwrap() + .1 + .rsplit_once(")") + .unwrap() + .0; + match_list = split.split(',').collect::>(); + } else { + unreachable!("Typo suggestion must have a match list"); + } + + let match_list = match_list + .iter() + .map(|s| s.trim().to_string()) + .collect::>(); + + let command; + // if match_list[0].starts_with("eval_shell_command(") { + // // FIXME + let match_list = match_list + .iter() + .map(|s| s.trim().to_string()) + .collect::>(); + command = suggest_typo(&split_command[index], match_list); + + suggest.replace_range(placeholder, &command); + } +} + +pub fn shell(suggest: &mut String, shell: &str) { + while suggest.contains("{{shell") { + let (placeholder, args) = eval_placeholder(suggest, "{{shell", "}}"); + let range = suggest[args.to_owned()].trim_matches(|c| c == '(' || c == ')'); + + let command = eval_shell_command(shell, range); + + suggest.replace_range(placeholder, &command.join("\n")); + } +} diff --git a/src/runtime_rules.rs b/src/runtime_rules.rs new file mode 100644 index 0000000..8587efc --- /dev/null +++ b/src/runtime_rules.rs @@ -0,0 +1,135 @@ +use crate::replaces; +use crate::suggestions::*; + +#[derive(serde::Deserialize)] +struct Rule { + match_err: Vec, +} + +#[derive(serde::Deserialize)] +struct MatchError { + pattern: Vec, + suggest: Vec, +} + +pub fn runtime_match( + executable: &str, + last_command: &str, + error_msg: &str, + shell: &str, +) -> Option { + let xdg_config_home = std::env::var("XDG_CONFIG_HOME") + .unwrap_or_else(|_| std::env::var("HOME").unwrap() + "/.config"); + let rule_dir = format!("{}/pay-respects/rules", xdg_config_home); + + let file = format!("{}/{}.toml", rule_dir, executable); + if !std::path::Path::new(&file).exists() { + return None; + } + + let file = std::fs::read_to_string(file).unwrap(); + let rule: Rule = toml::from_str(&file).unwrap(); + let split_command = split_command(&last_command); + + for match_err in rule.match_err { + for pattern in match_err.pattern { + if error_msg.contains(&pattern) { + 'suggest: for suggest in &match_err.suggest { + if suggest.starts_with('#') { + let mut lines = suggest.lines().collect::>(); + let mut conditions = String::new(); + for (i, line) in lines[0..].iter().enumerate() { + conditions.push_str(line); + if line.ends_with(']') { + lines = lines[i + 1..].to_vec(); + break; + } + } + let conditions = conditions + .trim_start_matches(['#', '[']) + .trim_end_matches(']') + .split(',') + .collect::>(); + + for condition in conditions { + let (mut condition, arg) = condition.split_once('(').unwrap(); + condition = condition.trim(); + let arg = arg.trim_start_matches('(').trim_end_matches(')'); + let reverse = match condition.starts_with('!') { + true => { + condition = condition.trim_start_matches('!'); + true + } + false => false, + }; + if eval_condition( + condition, + arg, + shell, + last_command, + error_msg, + &split_command, + ) == reverse + { + continue 'suggest; + } + } + + // replacing placeholders + let mut suggest = lines.join("\n").to_owned(); + if suggest.contains("{{command}}") { + suggest = suggest.replace("{{command}}", last_command); + } + eval_suggest(&suggest, last_command, error_msg, shell, &split_command); + return Some(suggest); + } + } + } + } + } + + None +} + +fn eval_condition( + condition: &str, + arg: &str, + shell: &str, + last_command: &str, + error_msg: &str, + split_command: &Vec, +) -> bool { + match condition { + "executable" => check_executable(shell, arg), + "err_contains" => error_msg.contains(arg), + "cmd_contains" => last_command.contains(arg), + "min_length" => split_command.len() >= arg.parse::().unwrap(), + "length" => split_command.len() == arg.parse::().unwrap(), + "max_length" => split_command.len() <= arg.parse::().unwrap() + 1, + _ => unreachable!("Unknown condition when evaluation condition: {}", condition), + } +} + +fn eval_suggest( + suggest: &str, + last_command: &str, + error_msg: &str, + shell: &str, + split_command: &Vec, +) -> Option { + let mut suggest = suggest.to_owned(); + if suggest.contains("{{command}}") { + suggest = suggest.replace("{{command}}", "{last_command}"); + } + + let mut last_command = last_command.to_owned(); + + replaces::opts(&mut suggest, &mut last_command); + replaces::cmd_reg(&mut suggest, &mut last_command); + replaces::err(&mut suggest, error_msg); + replaces::command(&mut suggest, split_command); + replaces::shell(&mut suggest, shell); + replaces::typo(&mut suggest, split_command); + + Some(suggest) +} diff --git a/src/suggestions.rs b/src/suggestions.rs index 4fa2395..91adbe7 100644 --- a/src/suggestions.rs +++ b/src/suggestions.rs @@ -44,6 +44,19 @@ pub fn suggest_command(shell: &str, last_command: &str, error_msg: &str) -> Opti } return Some(suggest); } + + #[cfg(feature = "runtime-rules")] + { + use crate::runtime_rules::runtime_match; + let suggest = runtime_match(executable, last_command, error_msg, shell); + if let Some(suggest) = suggest { + if PRIVILEGE_LIST.contains(&split_command[0].as_str()) { + return Some(format!("{} {}", split_command[0], suggest)); + } + return Some(suggest); + } + } + None } @@ -125,7 +138,7 @@ pub fn split_command(command: &str) -> Vec { split_command } -pub fn suggest_typo(typos: &[String], candidates: &[String]) -> String { +pub fn suggest_typo(typos: &[String], candidates: Vec) -> String { let mut path_files = Vec::new(); let mut suggestions = Vec::new(); for typo in typos { @@ -147,14 +160,14 @@ pub fn suggest_typo(typos: &[String], candidates: &[String]) -> String { } _ => {} } - } else if let Some(suggest) = find_similar(typo, candidates) { + } else if let Some(suggest) = find_similar(typo, &candidates) { suggestions.push(suggest); } } suggestions.join(" ") } -pub fn find_similar(typo: &str, candidates: &[String]) -> Option { +pub fn find_similar(typo: &str, candidates: &Vec) -> Option { let mut min_distance = 10; let mut min_distance_index = None; for (i, candidate) in candidates.iter().enumerate() {