mirror of
https://github.com/TECHNOFAB11/pay-respects.git
synced 2025-12-12 14:30:10 +01:00
chore: rearrange directories
This commit is contained in:
parent
d5fb7462e0
commit
4c9aac45a8
46 changed files with 11 additions and 18 deletions
90
core/src/args.rs
Normal file
90
core/src/args.rs
Normal 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
86
core/src/main.rs
Normal 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
150
core/src/modes.rs
Normal 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
185
core/src/replaces.rs
Normal 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, ¤t_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
17
core/src/rules.rs
Normal 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
623
core/src/shell.rs
Normal 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
66
core/src/style.rs
Normal 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
193
core/src/suggestions.rs
Normal 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
204
core/src/system.rs
Normal 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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue