chore: rearrange directories

This commit is contained in:
iff 2024-12-08 16:52:53 +01:00
parent d5fb7462e0
commit 4c9aac45a8
46 changed files with 11 additions and 18 deletions

90
core/src/args.rs Normal file
View file

@ -0,0 +1,90 @@
use crate::shell::{initialization, Init};
use colored::Colorize;
pub enum Status {
Continue,
Exit, // version, help, etc.
Error,
}
pub fn handle_args() -> Status {
use Status::*;
let args = std::env::args().collect::<Vec<String>>();
if args.len() <= 1 {
return Continue;
}
let mut init = Init::new();
let mut index = 1;
while index < args.len() {
match args[index].as_str() {
"-h" | "--help" => {
print_help();
return Exit;
}
"-v" | "--version" => {
print_version();
return Exit;
}
"-a" | "--alias" => {
if args.len() > index + 1 {
if args[index + 1].starts_with('-') {
init.alias = String::from("f");
} else {
init.alias = args[index + 1].clone();
index += 1;
}
} else {
init.alias = String::from("f");
}
init.auto_alias = true;
index += 1;
}
"--noncf" => {
init.cnf = false;
index += 1
}
_ => {
init.shell = args[index].clone();
index += 1
}
}
}
if init.shell.is_empty() {
eprintln!("{}", t!("no-shell"));
return Error;
}
init.binary_path = args[0].clone();
initialization(&mut init);
Exit
}
fn print_help() {
println!(
"{}",
t!(
"help",
usage = "pay-respects <shell> [--alias [<alias>]] [--noncf]",
eval = "Bash / Zsh / Fish".bold(),
eval_examples = r#"
eval "$(pay-respects bash --alias)"
eval "$(pay-respects zsh --alias)"
pay-respects fish --alias | source
"#,
manual = "Nushell / PowerShell".bold(),
manual_examples = r#"
pay-respects nushell [--alias <alias>]
pay-respects pwsh [--alias <alias>] [--nocnf]
"#
)
);
}
fn print_version() {
println!(
"version: {}",
option_env!("CARGO_PKG_VERSION").unwrap_or("unknown")
);
}

86
core/src/main.rs Normal file
View file

@ -0,0 +1,86 @@
// pay-respects: Press F to correct your command
// Copyright (C) 2023 iff
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use sys_locale::get_locale;
mod args;
mod modes;
mod rules;
mod shell;
mod style;
mod suggestions;
mod system;
#[macro_use]
extern crate rust_i18n;
i18n!("i18n", fallback = "en", minify_key = true);
fn main() -> Result<(), std::io::Error> {
colored::control::set_override(true);
let init = init();
let mut data = if let Err(status) = init {
match status {
args::Status::Exit => {
return Ok(());
}
args::Status::Error => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid input",
));
}
_ => {
unreachable!()
}
}
} else {
init.ok().unwrap()
};
data.expand_command();
use shell::Mode::*;
match data.mode {
Suggestion => modes::suggestion(&mut data),
Cnf => modes::cnf(&mut data),
}
Ok(())
}
fn init() -> Result<shell::Data, args::Status> {
let locale = {
let sys_locale = get_locale().unwrap_or("en-US".to_string());
if sys_locale.len() < 2 {
"en-US".to_string()
} else {
sys_locale
}
};
rust_i18n::set_locale(&locale[0..2]);
let status = args::handle_args();
match status {
args::Status::Exit => {
return Err(status);
}
args::Status::Error => {
return Err(status);
}
_ => {}
}
Ok(shell::Data::init())
}

150
core/src/modes.rs Normal file
View file

@ -0,0 +1,150 @@
use crate::shell::Data;
use crate::suggestions::suggest_candidates;
use crate::system;
use crate::{shell, suggestions};
use colored::Colorize;
use inquire::*;
use pay_respects_utils::evals::best_match_path;
use std::path::Path;
pub fn suggestion(data: &mut Data) {
let shell = data.shell.clone();
let mut last_command;
loop {
last_command = data.command.clone();
suggest_candidates(data);
if data.candidates.is_empty() {
break;
};
for candidate in &mut data.candidates {
shell::shell_syntax(&shell, candidate);
}
suggestions::select_candidate(data);
let execution = suggestions::confirm_suggestion(data);
if execution.is_ok() {
return;
} else {
data.update_command(&data.suggest.clone().unwrap());
let msg = Some(
execution
.err()
.unwrap()
.split_whitespace()
.collect::<Vec<&str>>()
.join(" "),
);
data.update_error(msg);
let retry_message = format!("{}...", t!("retry"));
eprintln!("\n{}\n", retry_message.cyan().bold());
}
}
eprintln!("{}: {}\n", t!("no-suggestion"), last_command.red());
eprintln!(
"{}\n{}",
t!("contribute"),
option_env!("CARGO_PKG_REPOSITORY").unwrap_or("https://github.com/iffse/pay-respects/")
);
}
pub fn cnf(data: &mut Data) {
let shell = data.shell.clone();
let mut split_command = data.split.clone();
let executable = split_command[0].as_str();
let shell_msg = format!("{}:", shell);
eprintln!(
"{} {}: {}\n",
shell_msg.bold().red(),
t!("command-not-found"),
executable
);
let best_match = best_match_path(executable, &data.executables);
if best_match.is_some() {
let best_match = best_match.unwrap();
split_command[0] = best_match;
let suggest = split_command.join(" ");
data.candidates.push(suggest.clone());
suggestions::select_candidate(data);
let status = suggestions::confirm_suggestion(data);
if status.is_err() {
data.update_command(&suggest);
let msg = Some(
status
.err()
.unwrap()
.split_whitespace()
.collect::<Vec<&str>>()
.join(" "),
);
data.update_error(msg);
suggestion(data);
}
} else {
let package_manager = match system::get_package_manager(data) {
Some(package_manager) => match package_manager.as_str() {
"apt" => {
let cnf_dirs = [
"/usr/lib/",
"/data/data/com.termux/files/usr/libexec/termux/",
];
let mut package_manager = package_manager;
for bin_dir in &cnf_dirs {
let bin = format!("{}{}", bin_dir, "command-not-found");
if Path::new(&bin).exists() {
package_manager = bin;
}
}
package_manager
}
_ => package_manager,
},
None => {
return;
}
};
#[cfg(debug_assertions)]
eprintln!("package_manager: {}", package_manager);
let packages = match system::get_packages(data, &package_manager, executable) {
Some(packages) => packages,
None => {
eprintln!("{}: {}", "pay-respects".red(), t!("package-not-found"));
return;
}
};
#[cfg(debug_assertions)]
eprintln!("packages: {:?}", packages);
let style = ui::Styled::default();
let render_config = ui::RenderConfig::default().with_prompt_prefix(style);
let msg = format!("{}:", t!("install-package")).bold().blue();
let confirm = format!("[{}]", t!("confirm-yes")).green();
let hint = format!("{} {} {}", "[↑/↓]".blue(), confirm, "[Ctrl+C]".red());
eprintln!("{}", msg);
eprintln!("{}", hint);
let package = Select::new("\n", packages)
.with_vim_mode(true)
.without_help_message()
.with_render_config(render_config)
.without_filtering()
.prompt()
.unwrap();
// retry after installing package
if system::install_package(data, &package_manager, &package) {
let _ = suggestions::run_suggestion(data, &data.command);
}
}
}

185
core/src/replaces.rs Normal file
View file

@ -0,0 +1,185 @@
use pay_respects_utils::evals::*;
fn tag(name: &str, x: i32) -> String {
format!("{{{}{}}}", name, x)
}
pub fn eval_placeholder(
string: &str,
start: &str,
end: &str,
) -> (std::ops::Range<usize>, std::ops::Range<usize>) {
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, opt_list: &mut Vec<(String, String)>) {
let mut replace_tag = 0;
let tag_name = "opts";
while suggest.contains(" {{opt::") {
let (placeholder, args) = eval_placeholder(suggest, " {{opt::", "}}");
let opt = &suggest[args.to_owned()];
let regex = opt.trim();
let current_tag = tag(tag_name, replace_tag);
opt_list.push((current_tag.clone(), opt_regex(regex, last_command)));
suggest.replace_range(placeholder, &current_tag);
replace_tag += 1;
}
}
pub fn cmd_reg(suggest: &mut String, last_command: &str) {
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: &[String]) {
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::<i32>().unwrap_or(0);
if start_index < 0 {
start_index += split_command.len() as i32;
};
let mut end_index;
let parsed_end = end.parse::<i32>();
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 + 1;
} else {
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::<usize>().unwrap_or(0);
let command = &split_command[range];
suggest.replace_range(placeholder, command);
}
}
}
pub fn typo(suggest: &mut String, split_command: &[String], executables: &[String], shell: &str) {
while suggest.contains("{{typo") {
let (placeholder, args) = eval_placeholder(suggest, "{{typo", "}}");
let index = if suggest.contains('[') {
let split = suggest[args.to_owned()]
.split(&['[', ']'])
.collect::<Vec<&str>>();
let command_index = split[1];
if !command_index.contains(':') {
let command_index = command_index.parse::<i32>().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::<i32>().unwrap_or(0);
let start_index = if start < 0 {
split_command.len() as i32 + start
} else {
start
};
let end = end.parse::<i32>();
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;
split.split(',').collect::<Vec<&str>>()
} else {
unreachable!("Typo suggestion must have a match list");
};
let match_list = match_list
.iter()
.map(|s| s.trim().to_string())
.collect::<Vec<String>>();
let command = if match_list[0].starts_with("{{shell") {
let function = match_list.join(",");
let (_, args) = eval_placeholder(&function, "{{shell", "}}");
let function = &function[args.to_owned()].trim_matches(|c| c == '(' || c == ')');
suggest_typo(
&split_command[index],
eval_shell_command(shell, function),
executables,
)
} else {
suggest_typo(&split_command[index], match_list, executables)
};
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"));
}
}

17
core/src/rules.rs Normal file
View file

@ -0,0 +1,17 @@
use crate::shell::Data;
use pay_respects_parser::parse_rules;
use pay_respects_utils::evals::*;
pub fn match_pattern(executable: &str, data: &Data) -> Option<Vec<String>> {
let error_msg = &data.error;
let shell = &data.shell;
let last_command = &data.command;
let executables = &data.executables;
let mut candidates = vec![];
parse_rules!("rules");
if candidates.is_empty() {
None
} else {
Some(candidates)
}
}

623
core/src/shell.rs Normal file
View file

@ -0,0 +1,623 @@
use pay_respects_utils::evals::split_command;
use pay_respects_utils::files::get_path_files;
use std::process::exit;
use std::collections::HashMap;
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;
pub const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"];
pub enum Mode {
Suggestion,
Cnf,
}
pub struct Init {
pub shell: String,
pub binary_path: String,
pub alias: String,
pub auto_alias: bool,
pub cnf: bool,
}
impl Init {
pub fn new() -> Init {
Init {
shell: String::from(""),
binary_path: String::from(""),
alias: String::from("f"),
auto_alias: false,
cnf: true,
}
}
}
pub struct Data {
pub shell: 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 (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());
}
}
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)
};
let mut init = Data {
shell,
command,
suggest: None,
candidates: vec![],
alias,
split: vec![],
privilege: None,
error: "".to_string(),
executables,
modules,
fallbacks,
mode,
};
init.split();
init.update_error(None);
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);
self.privilege = Some(split.remove(0))
}
self.split = split;
}
pub fn update_error(&mut self, error: Option<String>) {
if let Some(error) = error {
self.error = error;
} 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());
};
}
}
pub fn elevate(data: &mut Data, command: &mut String) {
for privilege in PRIVILEGE_LIST.iter() {
if data.executables.contains(&privilege.to_string()) {
*command = format!("{} {}", privilege, command);
break;
}
}
}
pub fn add_candidates_no_dup(
command: &str,
candidates: &mut Vec<String>,
new_candidates: &Vec<String>,
) {
for candidate in new_candidates {
let candidate = candidate.trim();
if candidate != command && !candidates.contains(&candidate.to_string()) {
candidates.push(candidate.to_string());
}
}
}
pub fn get_error(shell: &str, command: &str) -> 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 {
command_output_threaded(shell, command)
};
error.split_whitespace().collect::<Vec<&str>>().join(" ")
}
pub fn command_output_threaded(shell: &str, command: &str) -> String {
let (sender, receiver) = channel();
let _shell = shell.to_owned();
let _command = command.to_owned();
thread::spawn(move || {
sender
.send(
std::process::Command::new(_shell)
.arg("-c")
.arg(_command)
.env("LC_ALL", "C")
.output()
.expect("failed to execute process"),
)
.expect("failed to send output");
});
match receiver.recv_timeout(Duration::from_secs(3)) {
Ok(output) => match output.stderr.is_empty() {
true => String::from_utf8_lossy(&output.stdout).to_lowercase(),
false => String::from_utf8_lossy(&output.stderr).to_lowercase(),
},
Err(_) => {
use colored::*;
eprintln!("Timeout while executing command: {}", command.red());
exit(1);
}
}
}
pub fn command_output(shell: &str, command: &str) -> String {
let output = std::process::Command::new(shell)
.arg("-c")
.arg(command)
.env("LC_ALL", "C")
.output()
.expect("failed to execute process");
match output.stdout.is_empty() {
false => String::from_utf8_lossy(&output.stdout).to_lowercase(),
true => String::from_utf8_lossy(&output.stderr).to_lowercase(),
}
}
pub fn module_output(data: &Data, module: &str) -> Option<Vec<String>> {
let shell = &data.shell;
let executable = &data.split[0];
let last_command = &data.command;
let error_msg = &data.error;
let executables = data.executables.clone().join(",");
let output = std::process::Command::new(shell)
.arg("-c")
.arg(module)
.env("_PR_COMMAND", executable)
.env("_PR_SHELL", shell)
.env("_PR_LAST_COMMAND", last_command)
.env("_PR_ERROR_MSG", error_msg)
.env("_PR_EXECUTABLES", executables)
.output()
.expect("failed to execute process");
if !output.stderr.is_empty() {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
if output.stdout.is_empty() {
return None;
}
let break_holder = "<_PR_BR>";
Some(
String::from_utf8_lossy(&output.stdout)[..output.stdout.len() - break_holder.len()]
.split("<_PR_BR>")
.map(|s| s.trim().to_string())
.collect::<Vec<String>>(),
)
}
pub fn last_command(shell: &str) -> String {
let last_command = match std::env::var("_PR_LAST_COMMAND") {
Ok(command) => command,
Err(_) => {
eprintln!(
"{}",
t!(
"no-env-setup",
var = "_PR_LAST_COMMAND",
help = "pay-respects -h"
)
);
exit(1);
}
};
match shell {
"bash" => {
let first_line = last_command.lines().next().unwrap().trim();
first_line.split_once(' ').unwrap().1.to_string()
}
"zsh" => last_command,
"fish" => last_command,
"nu" => last_command,
_ => last_command,
}
}
pub fn run_mode() -> Mode {
match std::env::var("_PR_MODE") {
Ok(mode) => match mode.as_str() {
"suggestion" => Mode::Suggestion,
"cnf" => Mode::Cnf,
_ => Mode::Suggestion,
},
Err(_) => Mode::Suggestion,
}
}
pub fn alias_map(shell: &str) -> Option<HashMap<String, String>> {
let env = std::env::var("_PR_ALIAS");
if env.is_err() {
return None;
}
let env = env.unwrap();
if env.is_empty() {
return None;
}
let mut alias_map = HashMap::new();
match shell {
"bash" => {
for line in env.lines() {
let alias = line.replace("alias ", "");
let (alias, command) = alias.split_once('=').unwrap();
let command = command.trim().trim_matches('\'');
alias_map.insert(alias.to_string(), command.to_string());
}
}
"zsh" => {
for line in env.lines() {
let (alias, command) = line.split_once('=').unwrap();
let command = command.trim().trim_matches('\'');
alias_map.insert(alias.to_string(), command.to_string());
}
}
"fish" => {
for line in env.lines() {
let alias = line.replace("alias ", "");
let (alias, command) = alias.split_once(' ').unwrap();
let command = command.trim().trim_matches('\'');
alias_map.insert(alias.to_string(), command.to_string());
}
}
_ => {
unreachable!("Unsupported shell: {}", shell);
}
}
Some(alias_map)
}
pub fn expand_alias(map: &HashMap<String, String>, command: &str) -> Option<String> {
let (command, args) = if let Some(split) = command.split_once(' ') {
(split.0, split.1)
} else {
(command, "")
};
map.get(command)
.map(|expand| format!("{} {}", expand, args))
}
pub fn expand_alias_multiline(map: &HashMap<String, String>, command: &str) -> Option<String> {
let lines = command.lines().collect::<Vec<&str>>();
let mut expanded = String::new();
let mut expansion = false;
for line in lines {
if let Some(expand) = expand_alias(map, line) {
expanded = format!("{}\n{}", expanded, expand);
expansion = true;
} else {
expanded = format!("{}\n{}", expanded, line);
}
}
if expansion {
Some(expanded)
} else {
None
}
}
pub fn initialization(init: &mut Init) {
let last_command;
let shell_alias;
let alias = &init.alias;
let auto_alias = init.auto_alias;
let cnf = init.cnf;
let binary_path = &init.binary_path;
match init.shell.as_str() {
"bash" => {
last_command = "$(history 2)";
shell_alias = "$(alias)"
}
"zsh" => {
last_command = "$(fc -ln -1)";
shell_alias = "$(alias)"
}
"fish" => {
last_command = "$(history | head -n 1)";
shell_alias = "$(alias)";
}
"nu" | "nush" | "nushell" => {
last_command = "(history | last).command";
shell_alias = "\"\"";
init.shell = "nu".to_string();
}
"pwsh" | "powershell" => {
last_command = "Get-History | Select-Object -Last 1 | ForEach-Object {$_.CommandLine}";
shell_alias = ";";
init.shell = "pwsh".to_string();
}
_ => {
println!("Unknown shell: {}", init.shell);
return;
}
}
let shell = &init.shell;
if init.shell == "nu" {
let init = format!(
r#"
def --env {} [] {{
let dir = (with-env {{ _PR_LAST_COMMAND: {}, _PR_SHELL: nu }} {{ {} }})
cd $dir
}}
"#,
init.alias, last_command, init.binary_path
);
println!("{}", init);
return;
}
let mut initialize = match shell.as_str() {
"bash" | "zsh" | "fish" => format!(
"\
eval $(_PR_LAST_COMMAND=\"{}\" \
_PR_ALIAS=\"{}\" \
_PR_SHELL=\"{}\" \
\"{}\")",
last_command, shell_alias, shell, binary_path
),
"pwsh" | "powershell" => format!(
r#"& {{
try {{
# fetch command and error from session history only when not in cnf mode
if ($env:_PR_MODE -ne 'cnf') {{
$env:_PR_LAST_COMMAND = ({});
$err = Get-Error;
if ($env:_PR_LAST_COMMAND -eq $err.InvocationInfo.Line) {{
$env:_PR_ERROR_MSG = $err.Exception.Message
}}
}}
$env:_PR_SHELL = '{}';
&'{}';
}}
finally {{
# restore mode from cnf
if ($env:_PR_MODE -eq 'cnf') {{
$env:_PR_MODE = $env:_PR_PWSH_ORIGIN_MODE;
$env:_PR_PWSH_ORIGIN_MODE = $null;
}}
}}
}}
"#,
last_command, shell, binary_path
),
_ => {
println!("Unsupported shell: {}", shell);
return;
}
};
if !auto_alias {
println!("{}", initialize);
return;
}
match shell.as_str() {
"bash" | "zsh" => {
initialize = format!(r#"alias {}='{}'"#, alias, initialize);
}
"fish" => {
initialize = format!(
r#"
function {} -d "Terminal command correction"
eval $({})
end
"#,
alias, initialize
);
}
"pwsh" => {
initialize = format!(
"function {} {{\n{}",
alias,
initialize.split_once("\n").unwrap().1,
);
}
_ => {
println!("Unsupported shell: {}", shell);
return;
}
}
if cnf {
match shell.as_str() {
"bash" => {
initialize = format!(
r#"
command_not_found_handle() {{
eval $(_PR_LAST_COMMAND="_ $@" _PR_SHELL="{}" _PR_MODE="cnf" "{}")
}}
{}
"#,
shell, binary_path, initialize
);
}
"zsh" => {
initialize = format!(
r#"
command_not_found_handler() {{
eval $(_PR_LAST_COMMAND="$@" _PR_SHELL="{}" _PR_MODE="cnf" "{}")
}}
{}
"#,
shell, binary_path, initialize
);
}
"fish" => {
initialize = format!(
r#"
function fish_command_not_found --on-event fish_command_not_found
eval $(_PR_LAST_COMMAND="$argv" _PR_SHELL="{}" _PR_MODE="cnf" "{}")
end
{}
"#,
shell, binary_path, initialize
);
}
"pwsh" => {
initialize = format!(
r#"{}
$ExecutionContext.InvokeCommand.CommandNotFoundAction =
{{
param(
[string]
$commandName,
[System.Management.Automation.CommandLookupEventArgs]
$eventArgs
)
# powershell does not support run command with specific environment variables
# but you must set global variables. so we are memorizing the current mode and the alias function will reset it later.
$env:_PR_PWSH_ORIGIN_MODE=$env:_PR_MODE;
$env:_PR_MODE='cnf';
# powershell may search command with prefix 'get-' or '.\' first when this hook is hit, strip them
$env:_PR_LAST_COMMAND=$commandName -replace '^get-|\.\\','';
$eventArgs.Command = (Get-Command {});
$eventArgs.StopSearch = $True;
}}
"#,
initialize, alias
)
}
_ => {
println!("Unsupported shell: {}", shell);
return;
}
}
}
println!("{}", initialize);
}
pub fn get_shell() -> String {
match std::env::var("_PR_SHELL") {
Ok(shell) => shell,
Err(_) => {
eprintln!(
"{}",
t!("no-env-setup", var = "_PR_SHELL", help = "pay-respects -h")
);
std::process::exit(1);
}
}
}
pub fn shell_syntax(shell: &str, command: &mut String) {
#[allow(clippy::single_match)]
match shell {
"nu" => {
*command = command.replace(" && ", " and ");
}
_ => {}
}
}
pub fn shell_evaluated_commands(shell: &str, command: &str) -> Option<String> {
let lines = command
.lines()
.map(|line| line.trim().trim_end_matches(['\\', ';', '|', '&']))
.collect::<Vec<&str>>();
let mut dirs = Vec::new();
for line in lines {
if let Some(dir) = line.strip_prefix("cd ") {
dirs.push(dir.to_string());
}
}
let cd_dir = dirs.join("");
if cd_dir.is_empty() {
return None;
}
#[allow(clippy::single_match)]
match shell {
"nu" => Some(cd_dir),
_ => Some(format!("cd {}", cd_dir)),
}
}

66
core/src/style.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::shell::PRIVILEGE_LIST;
use colored::*;
use pay_respects_utils::evals::split_command;
// to_string() is necessary here, otherwise there won't be color in the output
#[warn(clippy::unnecessary_to_owned)]
pub fn highlight_difference(
shell: &str,
suggested_command: &str,
last_command: &str,
) -> Option<String> {
// let replaced_newline = suggested_command.replace('\n', r" {{newline}} ");
let mut split_suggested_command = split_command(suggested_command);
let split_last_command = split_command(last_command);
if split_suggested_command == split_last_command {
return None;
}
if split_suggested_command.is_empty() {
return None;
}
let privileged = PRIVILEGE_LIST.contains(&split_suggested_command[0].as_str());
let mut old_entries = Vec::new();
for command in &split_suggested_command {
if command.is_empty() {
continue;
}
for old in split_last_command.clone() {
if command == &old {
old_entries.push(command.clone());
break;
}
}
}
// let mut highlighted = suggested_command.to_string();
'next: for entry in split_suggested_command.iter_mut() {
if entry == "\n" {
continue;
}
for old in &old_entries {
if old == entry {
*entry = entry.blue().to_string();
continue 'next;
}
}
*entry = entry.red().bold().to_string();
}
if privileged
&& (suggested_command.contains("&&")
|| suggested_command.contains("||")
|| suggested_command.contains('>'))
{
split_suggested_command[1] =
format!("{} -c \"", shell).red().bold().to_string() + &split_suggested_command[1];
let len = split_suggested_command.len() - 1;
split_suggested_command[len] =
split_suggested_command[len].clone() + "\"".red().bold().to_string().as_str();
}
let highlighted = split_suggested_command.join(" ");
Some(highlighted.replace(" \n ", "\n"))
}

193
core/src/suggestions.rs Normal file
View file

@ -0,0 +1,193 @@
use std::io::stderr;
use std::process::{exit, Stdio};
use std::time::{Duration, Instant};
use colored::Colorize;
use inquire::*;
use crate::rules::match_pattern;
use crate::shell::{add_candidates_no_dup, module_output, shell_evaluated_commands, Data};
use crate::style::highlight_difference;
pub fn suggest_candidates(data: &mut Data) {
let executable = &data.split[0];
let command = &data.command;
let privilege = &data.privilege;
let mut suggest_candidates = vec![];
let modules = &data.modules;
let fallbacks = &data.fallbacks;
if privilege.is_none() {
if let Some(candidates) = match_pattern("_PR_privilege", data) {
add_candidates_no_dup(command, &mut suggest_candidates, &candidates);
}
}
if let Some(candidates) = match_pattern(executable, data) {
add_candidates_no_dup(command, &mut suggest_candidates, &candidates);
}
if let Some(candidates) = match_pattern("_PR_general", data) {
add_candidates_no_dup(command, &mut suggest_candidates, &candidates);
}
#[cfg(debug_assertions)]
{
eprintln!("modules: {modules:?}");
eprintln!("fallbacks: {fallbacks:?}");
}
for module in modules {
let new_candidates = module_output(data, module);
if let Some(candidates) = new_candidates {
add_candidates_no_dup(command, &mut suggest_candidates, &candidates);
}
}
if !suggest_candidates.is_empty() {
data.candidates = suggest_candidates;
return;
}
for fallback in fallbacks {
let candidates = module_output(data, fallback);
eprintln!("fallback: {candidates:?}");
if candidates.is_some() {
add_candidates_no_dup(command, &mut suggest_candidates, &candidates.unwrap());
data.candidates = suggest_candidates;
return;
}
}
}
pub fn select_candidate(data: &mut Data) {
let candidates = &data.candidates;
#[cfg(debug_assertions)]
eprintln!("candidates: {candidates:?}");
if candidates.len() == 1 {
let suggestion = candidates[0].to_string();
let highlighted = highlight_difference(&data.shell, &suggestion, &data.command).unwrap();
eprintln!("{}\n", highlighted);
let confirm = format!("[{}]", t!("confirm-yes")).green();
eprintln!("{}: {} {}", t!("confirm"), confirm, "[Ctrl+C]".red());
std::io::stdin().read_line(&mut String::new()).unwrap();
data.update_suggest(&suggestion);
data.expand_suggest();
} else {
let mut highlight_candidates = candidates
.iter()
.map(|candidate| highlight_difference(&data.shell, candidate, &data.command).unwrap())
.collect::<Vec<String>>();
for candidate in highlight_candidates.iter_mut() {
let lines = candidate.lines().collect::<Vec<&str>>();
let mut formated = String::new();
for (j, line) in lines.iter().enumerate() {
if j == 0 {
formated = line.to_string();
} else {
formated = format!("{}\n {}", formated, line);
}
}
*candidate = formated;
}
let style = ui::Styled::default();
let render_config = ui::RenderConfig::default()
.with_prompt_prefix(style)
.with_answered_prompt_prefix(style)
.with_highlighted_option_prefix(style);
let msg = format!("{}", t!("multi-suggest", num = candidates.len()))
.bold()
.blue();
let confirm = format!("[{}]", t!("confirm-yes")).green();
let hint = format!("{} {} {}", "[↑/↓]".blue(), confirm, "[Ctrl+C]".red());
eprintln!("{}", msg);
eprintln!("{}", hint);
let ans = Select::new("\n", highlight_candidates.clone())
.with_page_size(1)
.with_vim_mode(true)
.without_filtering()
.without_help_message()
.with_render_config(render_config)
.prompt()
.unwrap();
let pos = highlight_candidates.iter().position(|x| x == &ans).unwrap();
let suggestion = candidates[pos].to_string();
data.update_suggest(&suggestion);
data.expand_suggest();
}
data.candidates.clear();
}
pub fn confirm_suggestion(data: &Data) -> Result<(), String> {
let shell = &data.shell;
let command = &data.suggest.clone().unwrap();
#[cfg(debug_assertions)]
eprintln!("running command: {command}");
let now = Instant::now();
let process = run_suggestion(data, command);
if process.success() {
let cd = shell_evaluated_commands(shell, command);
if let Some(cd) = cd {
println!("{}", cd);
}
Ok(())
} else {
if now.elapsed() > Duration::from_secs(3) {
exit(1);
}
suggestion_err(data, command)
}
}
pub fn run_suggestion(data: &Data, command: &str) -> std::process::ExitStatus {
let shell = &data.shell;
let privilege = &data.privilege;
match privilege {
Some(sudo) => std::process::Command::new(sudo)
.arg(shell)
.arg("-c")
.arg(command)
.stdout(stderr())
.stderr(Stdio::inherit())
.status()
.expect("failed to execute process"),
None => std::process::Command::new(shell)
.arg("-c")
.arg(command)
.stdout(stderr())
.stderr(Stdio::inherit())
.status()
.expect("failed to execute process"),
}
}
fn suggestion_err(data: &Data, command: &str) -> Result<(), String> {
let shell = &data.shell;
let privilege = &data.privilege;
let process = match privilege {
Some(sudo) => std::process::Command::new(sudo)
.arg(shell)
.arg("-c")
.arg(command)
.env("LC_ALL", "C")
.output()
.expect("failed to execute process"),
None => std::process::Command::new(shell)
.arg("-c")
.arg(command)
.env("LC_ALL", "C")
.output()
.expect("failed to execute process"),
};
let error_msg = match process.stderr.is_empty() {
true => String::from_utf8_lossy(&process.stdout).to_lowercase(),
false => String::from_utf8_lossy(&process.stderr).to_lowercase(),
};
Err(error_msg.to_string())
}

204
core/src/system.rs Normal file
View file

@ -0,0 +1,204 @@
use crate::shell::{command_output, elevate, Data};
use colored::Colorize;
use std::io::stderr;
use std::process::Command;
use std::process::Stdio;
pub fn get_package_manager(data: &mut Data) -> Option<String> {
let package_managers = vec![
"apt", "dnf", "emerge", "nix", "pacman", "yum",
// "zypper",
];
for package_manager in package_managers {
if data.executables.contains(&package_manager.to_string()) {
return Some(package_manager.to_string());
}
}
None
}
pub fn get_packages(
data: &mut Data,
package_manager: &str,
executable: &str,
) -> Option<Vec<String>> {
let shell = &data.shell.clone();
match package_manager {
"apt" => {
if !data.executables.contains(&"apt-file".to_string()) {
eprintln!(
"{}: apt-file is required to find packages",
"pay-respects".yellow()
);
return None;
}
let result = command_output(
shell,
&format!("apt-file find --regexp '.*/bin/{}$'", executable),
);
if result.is_empty() {
return None;
}
let packages: Vec<String> = result
.lines()
.map(|line| line.split_once(':').unwrap().0.to_string())
.collect();
if packages.is_empty() {
None
} else {
Some(packages)
}
}
"dnf" | "yum" => {
let result = command_output(
shell,
&format!("{} provides '/usr/bin/{}'", package_manager, executable),
);
if result.is_empty() {
return None;
}
let packages: Vec<String> = result
.lines()
.map(|line| line.split_whitespace().next().unwrap().to_string())
.collect();
if packages.is_empty() {
None
} else {
Some(packages)
}
}
"emerge" => {
if !data.executables.contains(&"e-file".to_string()) {
eprintln!(
"{}: pfl is required to find packages",
"pay-respects".yellow()
);
return None;
}
let result = command_output(shell, &format!("e-file /usr/bin/{}", executable));
if result.is_empty() {
return None;
}
let mut packages = vec![];
for line in result.lines() {
if !line.starts_with(" ") {
packages.push(line.to_string());
}
}
if packages.is_empty() {
None
} else {
Some(packages)
}
}
"nix" => {
if !data.executables.contains(&"nix-locate".to_string()) {
eprintln!(
"{}: nix-index is required to find packages",
"pay-respects".yellow()
);
return None;
}
let result = command_output(shell, &format!("nix-locate 'bin/{}'", executable));
if result.is_empty() {
return None;
}
let packages: Vec<String> = result
.lines()
.map(|line| {
line.split_whitespace()
.next()
.unwrap()
.rsplit_once('.')
.unwrap()
.0
.to_string()
})
.collect();
if packages.is_empty() {
None
} else {
Some(packages)
}
}
"pacman" => {
let result = if data.executables.contains(&"pkgfile".to_string()) {
command_output(shell, &format!("pkgfile -b {}", executable))
} else {
command_output(shell, &format!("pacman -Fq /usr/bin/{}", executable))
};
if result.is_empty() {
return None;
}
let packages: Vec<String> = result
.lines()
.map(|line| line.split_whitespace().next().unwrap().to_string())
.collect();
if packages.is_empty() {
None
} else {
Some(packages)
}
}
_ => match package_manager.ends_with("command-not-found") {
true => {
let result = command_output(shell, &format!("{} {}", package_manager, executable));
if result.is_empty() {
return None;
}
if result.contains("did you mean") || result.contains("is not installed") {
let packages = result
.lines()
.skip(1)
.map(|line| line.trim().to_string())
.collect();
return Some(packages);
}
None
}
false => unreachable!("Unsupported package manager"),
},
}
}
pub fn install_package(data: &mut Data, package_manager: &str, package: &str) -> bool {
let shell = &data.shell.clone();
let mut install = match package_manager {
"apt" | "dnf" | "pkg" | "yum" | "zypper" => {
format!("{} install {}", package_manager, package)
}
"emerge" => format!("emerge {}", package),
"nix" => format!("nix profile install nixpkgs#{}", package),
"pacman" => format!("pacman -S {}", package),
_ => match package_manager.ends_with("command-not-found") {
true => match package.starts_with("Command") {
false => package.to_string(),
true => {
let split = package.split_whitespace().collect::<Vec<&str>>();
let command = split[1];
let package = split[split.len() - 1];
let new_command = data.command.clone().replacen(command, package, 1);
data.update_command(&new_command);
format!("apt install {}", package)
}
},
false => unreachable!("Unsupported package manager"),
},
};
elevate(data, &mut install);
#[cfg(debug_assertions)]
eprintln!("install: {}", install);
let result = Command::new(shell)
.arg("-c")
.arg(install)
.stdout(stderr())
.stderr(Stdio::inherit())
.status()
.expect("failed to execute process");
result.success()
}