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

222
Cargo.lock generated
View file

@ -17,6 +17,12 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base62"
version = "2.0.3"
@ -45,6 +51,12 @@ dependencies = [
"serde",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.1"
@ -95,6 +107,31 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "curl"
version = "0.4.47"
@ -125,6 +162,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "dyn-clone"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "either"
version = "1.13.0"
@ -153,6 +196,24 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "glob"
version = "0.3.1"
@ -221,6 +282,23 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inquire"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
dependencies = [
"bitflags 2.6.0",
"crossterm",
"dyn-clone",
"fuzzy-matcher",
"fxhash",
"newline-converter",
"once_cell",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
@ -289,6 +367,16 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
@ -301,6 +389,27 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "newline-converter"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "normpath"
version = "1.3.0"
@ -334,12 +443,36 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "pay-respects"
version = "0.5.14"
dependencies = [
"colored",
"curl",
"inquire",
"pay-respects-parser",
"regex-lite",
"rust-i18n",
@ -387,6 +520,15 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "regex"
version = "1.11.1"
@ -527,6 +669,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.215"
@ -591,6 +739,36 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "1.0.1"
@ -691,6 +869,16 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "toml"
version = "0.7.8"
@ -773,6 +961,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
@ -795,6 +989,28 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
@ -804,6 +1020,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"

View file

@ -25,6 +25,8 @@ serde = { version = "1.0", features = ["derive"], optional = true }
curl = { version = "0.4", optional = true }
textwrap = { version = "0.16", features = ["terminal_size"], optional = true }
inquire = "0.7.5"
pay-respects-parser = "0.2.6"
# pay-respects-parser = { path = "../pay-respects-parser" }

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,11 +80,9 @@ 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;
}
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;
@ -93,6 +93,38 @@ pub fn cnf() {
}
let suggestion = split_command.join(" ");
let highlighted_suggestion = highlight_difference(&shell, &suggestion, &last_command).unwrap();
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"),
}
}