feat: prompt to install missing command

This commit is contained in:
iff 2024-11-27 20:38:37 +01:00
parent ab3534f0ef
commit c99c736ad5
7 changed files with 351 additions and 23 deletions

View file

@ -130,7 +130,7 @@ pub fn get_best_match_file(input: &str) -> Option<String> {
})
.collect::<Vec<String>>();
let best_match = find_similar(&exit_dir, &dir_files);
let best_match = find_similar(&exit_dir, &dir_files, Some(1));
best_match.as_ref()?;
input = format!("{}/{}", input, best_match.unwrap());

View file

@ -23,6 +23,7 @@ mod rules;
mod shell;
mod style;
mod suggestions;
mod system;
#[cfg(feature = "runtime-rules")]
mod replaces;

View file

@ -1,8 +1,10 @@
use crate::shell::{command_output, get_shell, PRIVILEGE_LIST};
use crate::style::highlight_difference;
use crate::suggestions::{split_command, suggest_typo};
use crate::suggestions::{best_match_path, split_command};
use crate::system;
use crate::{shell, suggestions};
use colored::Colorize;
use inquire::*;
pub fn suggestion() {
let shell = get_shell();
@ -78,21 +80,51 @@ pub fn cnf() {
false => split_command.first().expect(&t!("no-command")).as_str(),
};
let best_match = suggest_typo(&[executable.to_owned()], vec!["path".to_string()]);
if best_match == executable {
eprintln!("{}: command not found: {}", shell, executable);
return;
}
match PRIVILEGE_LIST.contains(&split_command[0].as_str()) {
true => {
split_command[1] = best_match;
let best_match = best_match_path(executable);
if best_match.is_some() {
let best_match = best_match.unwrap();
match PRIVILEGE_LIST.contains(&split_command[0].as_str()) {
true => {
split_command[1] = best_match;
}
false => {
split_command[0] = best_match;
}
}
false => {
split_command[0] = best_match;
}
}
let suggestion = split_command.join(" ");
let suggestion = split_command.join(" ");
let highlighted_suggestion = highlight_difference(&shell, &suggestion, &last_command).unwrap();
let _ = suggestions::confirm_suggestion(&shell, &suggestion, &highlighted_suggestion);
let highlighted_suggestion =
highlight_difference(&shell, &suggestion, &last_command).unwrap();
let _ = suggestions::confirm_suggestion(&shell, &suggestion, &highlighted_suggestion);
} else {
let package_manager = match system::get_package_manager(&shell) {
Some(package_manager) => package_manager,
None => {
eprintln!("no package manager found");
return;
}
};
let packages = match system::get_packages(&shell, &package_manager, executable) {
Some(packages) => packages,
None => {
eprintln!("no package found");
return;
}
};
let ans = Select::new("Select a package to install", packages).prompt();
let package = match ans {
Ok(package) => package,
Err(_) => {
eprintln!("no package selected");
return;
}
};
// retry after installing package
if system::install_package(&shell, &package_manager, &package) {
let _ = suggestions::confirm_suggestion(&shell, &last_command, &last_command);
}
}
}

View file

@ -64,7 +64,8 @@ pub fn suggest_command(shell: &str, last_command: &str, error_msg: &str) -> Opti
// skip for commands with no arguments,
// very likely to be an error showing the usage
if PRIVILEGE_LIST.contains(&split_command[0].as_str()) && split_command.len() > 2
|| !PRIVILEGE_LIST.contains(&split_command[0].as_str()) && split_command.len() > 1 {
|| !PRIVILEGE_LIST.contains(&split_command[0].as_str()) && split_command.len() > 1
{
let suggest = ai_suggestion(last_command, error_msg);
if let Some(suggest) = suggest {
let warn = format!("{}:", t!("ai-suggestion")).bold().blue();
@ -184,7 +185,7 @@ pub fn suggest_typo(typos: &[String], candidates: Vec<String>) -> String {
if path_files.is_empty() {
path_files = get_path_files();
};
if let Some(suggest) = find_similar(typo, &path_files) {
if let Some(suggest) = find_similar(typo, &path_files, Some(2)) {
suggestions.push(suggest);
} else {
suggestions.push(typo.to_string());
@ -199,7 +200,7 @@ pub fn suggest_typo(typos: &[String], candidates: Vec<String>) -> String {
}
_ => {}
}
} else if let Some(suggest) = find_similar(typo, &candidates) {
} else if let Some(suggest) = find_similar(typo, &candidates, Some(2)) {
suggestions.push(suggest);
} else {
suggestions.push(typo.to_string());
@ -208,8 +209,19 @@ pub fn suggest_typo(typos: &[String], candidates: Vec<String>) -> String {
suggestions.join(" ")
}
pub fn find_similar(typo: &str, candidates: &[String]) -> Option<String> {
let mut min_distance = { std::cmp::max(2, typo.chars().count() / 2 + 1) };
pub fn best_match_path(typo: &str) -> Option<String> {
let path_files = get_path_files();
find_similar(typo, &path_files, Some(3))
}
// higher the threshold, the stricter the comparison
// 1: anything
// 2: 50% similarity
// 3: 33% similarity
// ... etc
pub fn find_similar(typo: &str, candidates: &[String], threshold: Option<usize>) -> Option<String> {
let threshold = threshold.unwrap_or(2);
let mut min_distance = typo.chars().count() / threshold + 1;
let mut min_distance_index = None;
for (i, candidate) in candidates.iter().enumerate() {
if candidate.is_empty() {
@ -344,4 +356,3 @@ fn run_suggestion(shell: &str, command: &str) -> std::process::ExitStatus {
.wait()
.unwrap()
}

60
src/system.rs Normal file
View file

@ -0,0 +1,60 @@
use std::io::stderr;
use std::process::Command;
use std::process::Stdio;
pub fn get_package_manager(shell: &str) -> Option<String> {
let package_managers = vec!["pacman", "apt", "dnf"];
for package_manager in package_managers {
let success = Command::new(shell)
.arg("-c")
.arg(format!("command -v {}", package_manager))
.output()
.expect("failed to execute process")
.status
.success();
if success {
return Some(package_manager.to_string());
}
}
None
}
pub fn get_packages(shell: &str, package_manager: &str, executable: &str) -> Option<Vec<String>> {
match package_manager {
"pacman" => {
let result = Command::new(shell)
.arg("-c")
.arg(format!("pacman -Fq /usr/bin/{}", executable))
.output()
.expect("failed to execute process");
if result.status.success() {
let output = String::from_utf8_lossy(&result.stdout)
.lines()
.map(|line| line.split_whitespace().next().unwrap().to_string())
.collect();
Some(output)
} else {
None
}
}
_ => unreachable!("Unsupported package manager"),
}
}
pub fn install_package(shell: &str, package_manager: &str, package: &str) -> bool {
match package_manager {
"pacman" => Command::new(shell)
.arg("-c")
.arg(format!("sudo pacman -S {}", package))
.stdout(stderr())
.stderr(Stdio::inherit())
.spawn()
.expect("failed to execute process")
.wait()
.unwrap()
.success(),
_ => unreachable!("Unsupported package manager"),
}
}