use std::collections::HashMap;
use clap::{ArgEnum, Parser};
use displaydoc::Display;
use nix::unistd::geteuid;
use owo_colors::OwoColorize;
use reqwest::Error;
use scraper::{Html, Selector};
use how_install_tealdeer as tealdeer;
#[derive(Parser)]
#[clap(
about = "A CLI for helping find how to install a given command",
long_about = "A CLI for helping find how to install a given command\n\n\
Credit to:\
\n - https://tldr.sh for descriptions\
\n - https://dbrgn.github.io/tealdeer/ for tldr console output\
\n - https://command-not-found.com/ for command install information"
)]
struct Args {
cmd: String,
#[clap(long, short)]
install: bool,
#[clap(short)]
yes: bool,
#[clap(long)]
no_tldr: bool,
#[clap(long, arg_enum, ignore_case = true)]
distro: Option<LinuxDistro>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum, Display)]
enum LinuxDistro {
Debian,
Ubuntu,
Alpine,
Arch,
Kali,
Centos,
Fedora,
Raspbian,
Docker,
}
fn error(msg: impl AsRef<str>) -> ! {
let msg = msg.as_ref();
eprintln!("{msg}");
std::process::exit(1)
}
fn confirm_install(command: &str) -> bool {
atty::isnt(atty::Stream::Stdout)
|| dialoguer::Confirm::new()
.with_prompt(format!("Install {command} using the above command?"))
.default(true)
.interact_opt()
.ok()
.flatten()
.unwrap_or(false)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let args = Args::parse();
let command = &args.cmd;
let url = format!("https://command-not-found.com/{}", command);
let response = reqwest::get(url).await?.text().await?;
let document = Html::parse_document(&response);
let selector = Selector::parse(".command-install:not(.d-none)").unwrap();
let dt_selector = Selector::parse("dt").unwrap();
let dd_selector = Selector::parse("dd").unwrap();
let os = sys_info::linux_os_release().expect("Failed to get Linux OS release info");
let command: HashMap<_, _> = document
.select(&selector)
.flat_map(|element| {
let name = element
.select(&dt_selector)
.next()
.unwrap()
.text()
.last()
.unwrap()
.trim()
.to_string();
let data_os_name = if let Some(data_os_name) = element.value().attr("data-os") {
data_os_name.to_owned()
} else {
String::from("")
};
const PREFIX: &str = "install-";
let id = if let Some(id) = element
.value()
.classes()
.find(|class| class.starts_with(PREFIX))
{
id[PREFIX.len()..].to_owned()
} else {
return vec![];
};
let command = element
.select(&dd_selector)
.next()
.unwrap()
.text()
.collect::<String>()
.trim()
.to_string();
vec![
(name, command.clone()),
(data_os_name, command.clone()),
(id, command),
]
})
.collect();
let command = if let Some(distro) = args.distro {
match command.get(&distro.to_string()) {
Some(command) => command,
None => error(format!("{cmd} not found for {distro}", cmd = args.cmd)),
}
} else {
command
.get(os.name())
.or_else(|| command.get(os.pretty_name()))
.or_else(|| command.get(os.id()))
.unwrap_or_else(|| {
error(format!(
"Failed to find install command for {:?} on OS {:?}",
args.cmd,
os.pretty_name(),
))
})
};
let maybe_sudo = if geteuid().is_root() { "" } else { "sudo " };
if !args.no_tldr && tealdeer::list().await.contains(&args.cmd) {
eprint!("{}", "TLDR".bold());
tealdeer::main(args.cmd.clone()).await;
}
eprint!("{}", "INSTALL\n ".bold());
println!("{maybe_sudo}{command}");
if args.yes || (args.install && confirm_install(&args.cmd)) {
let status = std::process::Command::new("bash")
.args(&["-c", command])
.status()
.unwrap();
std::process::exit(status.code().unwrap());
}
Ok(())
}