feat: shell completions

This commit is contained in:
Cyborus 2025-02-06 21:02:45 -05:00
parent 3bc1c07659
commit 8103a29ac0
No known key found for this signature in database
4 changed files with 210 additions and 13 deletions

36
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -233,9 +233,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.15"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc"
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
dependencies = [
"clap_builder",
"clap_derive",
@ -243,9 +243,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.15"
version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
dependencies = [
"anstream",
"anstyle",
@ -255,10 +255,19 @@ dependencies = [
]
[[package]]
name = "clap_derive"
version = "4.5.13"
name = "clap_complete"
version = "4.5.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck",
"proc-macro2",
@ -268,9 +277,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
@ -605,6 +614,7 @@ dependencies = [
"base64ct",
"cfg-if",
"clap",
"clap_complete",
"comrak",
"crossterm",
"directories",
@ -1864,12 +1874,12 @@ dependencies = [
[[package]]
name = "terminal_size"
version = "0.3.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
dependencies = [
"rustix",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]

View file

@ -22,6 +22,7 @@ auth-git2 = "0.5.4"
base64ct = { version = "1.6.0", features = ["std"] }
cfg-if = "1.0.0"
clap = { version = "4.5.11", features = ["derive"] }
clap_complete = "4.5.44"
comrak = "0.26.0"
crossterm = "0.27.0"
directories = "5.0.1"

183
src/completion.rs Normal file
View file

@ -0,0 +1,183 @@
use std::io::Write;
use clap::{ArgAction, Args, CommandFactory, ValueEnum};
use eyre::OptionExt;
#[derive(Args, Clone, Debug)]
pub struct CompletionCommand {
shell: Shell,
#[clap(long)]
bin_name: Option<String>,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum Shell {
Bash,
Elvish,
Fish,
PowerShell,
Zsh,
Nushell,
}
impl CompletionCommand {
pub fn run(self) {
use clap_complete::Shell as CCShell;
use Shell::*;
let mut cmd = crate::App::command();
let app_name = self.bin_name.as_deref().unwrap_or("fj");
let mut writer = std::io::stdout();
match self.shell {
Bash => clap_complete::generate(CCShell::Bash, &mut cmd, app_name, &mut writer),
Elvish => clap_complete::generate(CCShell::Elvish, &mut cmd, app_name, &mut writer),
Fish => clap_complete::generate(CCShell::Fish, &mut cmd, app_name, &mut writer),
PowerShell => clap_complete::generate(CCShell::PowerShell, &mut cmd, app_name, &mut writer),
Zsh => clap_complete::generate(CCShell::Zsh, &mut cmd, app_name, &mut writer),
Nushell => clap_complete::generate(NushellCompletion, &mut cmd, app_name, &mut writer),
}
}
}
// Heavily inspired by clap_complete_nushell
// but rewritten/modified since I'm not a fan of its completions
struct NushellCompletion;
impl clap_complete::Generator for NushellCompletion {
fn file_name(&self, name: &str) -> String {
format!("{name}.nu")
}
fn generate(&self, cmd: &clap::Command, buf: &mut dyn Write) {
generate_command(cmd, buf).expect("failed to generate nushell completions");
}
}
fn generate_command(cmd: &clap::Command, buf: &mut dyn Write) -> eyre::Result<()> {
writeln!(buf, "module completions {{")?;
generate_subcommand(cmd, buf)?;
writeln!(buf, "}}")?;
writeln!(buf, "export use completions *")?;
Ok(())
}
fn generate_subcommand(cmd: &clap::Command, buf: &mut dyn Write) -> eyre::Result<()> {
let name = cmd.get_bin_name().ok_or_eyre("no bin name")?;
writeln!(buf, " export extern \"{name}\" [")?;
let mut args = cmd.get_arguments().collect::<Vec<_>>();
args.sort_by_key(|arg| arg.is_positional());
// positional arguments
for arg in cmd.get_arguments() {
if !arg.is_positional() {
continue;
}
write!(buf, " ")?;
let id = arg.get_id().as_str();
if matches!(arg.get_action(), ArgAction::Append) {
write!(buf, "...{id}")?;
} else {
write!(buf, "{id}")?;
if !arg.is_required_set() {
write!(buf, "?")?;
}
}
arg_type(name, arg, buf)?;
writeln!(buf)?;
}
// subcommand completion
if cmd.get_subcommands().next().is_some() { // basically `!is_empty`
writeln!(buf, " rest?: string@\"complete-subcommand {name}\",")?;
}
// flag arguments
for arg in cmd.get_arguments() {
match (arg.get_long(), arg.get_short()) {
(Some(long), Some(short)) => write!(buf, " --{long}(-{short})")?,
(Some(long), None) => write!(buf, " --{long}")?,
(None, Some(short)) => write!(buf, " -{short}")?,
(None, None) => continue,
}
arg_type(name, arg, buf)?;
writeln!(buf)?;
}
writeln!(buf, " ]")?;
writeln!(buf)?;
// argument completions
for arg in cmd.get_arguments() {
let possible_values = arg.get_possible_values();
if possible_values.is_empty() {
continue;
}
writeln!(buf, " def \"complete-value {name} {}\" [] {{", arg.get_id().as_str())?;
writeln!(buf, " [")?;
for possible_value in &possible_values {
write!(buf, " {{ value: \"{}\"", possible_value.get_name())?;
if let Some(help) = possible_value.get_help() {
write!(buf, ", description: \"{help}\"")?;
}
writeln!(buf, " }},")?;
}
writeln!(buf, " ]")?;
writeln!(buf, " }}")?;
writeln!(buf)?;
}
// subcommand completion
if cmd.get_subcommands().count() != 0 {
writeln!(buf, " def \"complete-subcommand {name}\" [] {{")?;
writeln!(buf, " [")?;
for subcommand in cmd.get_subcommands() {
write!(buf, " {{ value: \"{}\"", subcommand.get_name())?;
if let Some(about) = subcommand.get_about() {
write!(buf, ", description: \"{about}\"")?;
}
writeln!(buf, " }},")?;
}
writeln!(buf, " ]")?;
writeln!(buf, " }}")?;
writeln!(buf)?;
}
for subcommand in cmd.get_subcommands() {
generate_subcommand(subcommand, buf)?;
}
Ok(())
}
fn arg_type(cmd_name: &str, arg: &clap::Arg, buf: &mut dyn Write) -> eyre::Result<()> {
use clap::ValueHint;
let takes_values = arg.get_num_args().map(|r| r.takes_values()).unwrap_or_default();
if takes_values {
let type_name = match arg.get_value_hint() {
ValueHint::Unknown => "string",
ValueHint::Other => "string",
ValueHint::AnyPath => "path",
ValueHint::FilePath => "path",
ValueHint::DirPath => "path",
ValueHint::ExecutablePath => "path",
ValueHint::CommandName => "string",
ValueHint::CommandString => "path",
ValueHint::CommandWithArguments => "string",
ValueHint::Username => "string",
ValueHint::Hostname => "string",
ValueHint::Url => "string",
ValueHint::EmailAddress => "string",
_ => "string",
};
write!(buf, ": {type_name}")?;
}
let possible_values = arg.get_possible_values();
if !possible_values.is_empty() {
write!(buf, "@\"complete-value {cmd_name} {}\"", arg.get_id().as_str())?;
}
Ok(())
}

View file

@ -16,6 +16,7 @@ mod user;
mod version;
mod whoami;
mod wiki;
mod completion;
pub const USER_AGENT: &str = concat!(
env!("CARGO_PKG_NAME"),
@ -50,6 +51,7 @@ pub enum Command {
Release(release::ReleaseCommand),
User(user::UserCommand),
Version(version::VersionCommand),
Completion(completion::CompletionCommand),
}
#[tokio::main]
@ -72,6 +74,7 @@ async fn main() -> eyre::Result<()> {
Command::Release(subcommand) => subcommand.run(&mut keys, host_name).await?,
Command::User(subcommand) => subcommand.run(&mut keys, host_name).await?,
Command::Version(command) => command.run().await?,
Command::Completion(subcommand) => subcommand.run(),
}
keys.save().await?;