diff options
Diffstat (limited to 'src/dynamic/shells/bash.rs')
-rw-r--r-- | src/dynamic/shells/bash.rs | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/src/dynamic/shells/bash.rs b/src/dynamic/shells/bash.rs new file mode 100644 index 0000000..43c128e --- /dev/null +++ b/src/dynamic/shells/bash.rs @@ -0,0 +1,121 @@ +use unicode_xid::UnicodeXID as _; + +/// Bash completions +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Bash; + +impl crate::dynamic::Completer for Bash { + fn file_name(&self, name: &str) -> String { + format!("{name}.bash") + } + fn write_registration( + &self, + name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let escaped_name = name.replace('-', "_"); + debug_assert!( + escaped_name.chars().all(|c| c.is_xid_continue()), + "`name` must be an identifier, got `{escaped_name}`" + ); + let mut upper_name = escaped_name.clone(); + upper_name.make_ascii_uppercase(); + + let completer = shlex::quote(completer); + + let script = r#" +_clap_complete_NAME() { + export IFS=$'\013' + export _CLAP_COMPLETE_INDEX=${COMP_CWORD} + export _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE} + if compopt +o nospace 2> /dev/null; then + export _CLAP_COMPLETE_SPACE=false + else + export _CLAP_COMPLETE_SPACE=true + fi + COMPREPLY=( $("COMPLETER" complete --shell bash -- "${COMP_WORDS[@]}") ) + if [[ $? != 0 ]]; then + unset COMPREPLY + elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then + compopt -o nospace + fi +} +complete -o nospace -o bashdefault -F _clap_complete_NAME BIN +"# + .replace("NAME", &escaped_name) + .replace("BIN", bin) + .replace("COMPLETER", &completer) + .replace("UPPER", &upper_name); + + writeln!(buf, "{script}")?; + Ok(()) + } + fn write_complete( + &self, + cmd: &mut clap::Command, + args: Vec<std::ffi::OsString>, + current_dir: Option<&std::path::Path>, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let index: usize = std::env::var("_CLAP_COMPLETE_INDEX") + .ok() + .and_then(|i| i.parse().ok()) + .unwrap_or_default(); + let _comp_type: CompType = std::env::var("_CLAP_COMPLETE_COMP_TYPE") + .ok() + .and_then(|i| i.parse().ok()) + .unwrap_or_default(); + let _space: Option<bool> = std::env::var("_CLAP_COMPLETE_SPACE") + .ok() + .and_then(|i| i.parse().ok()); + let ifs: Option<String> = std::env::var("IFS").ok().and_then(|i| i.parse().ok()); + let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; + + for (i, (completion, _)) in completions.iter().enumerate() { + if i != 0 { + write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; + } + write!(buf, "{}", completion.to_string_lossy())?; + } + Ok(()) + } +} + +/// Type of completion attempted that caused a completion function to be called +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +enum CompType { + /// Normal completion + Normal, + /// List completions after successive tabs + Successive, + /// List alternatives on partial word completion + Alternatives, + /// List completions if the word is not unmodified + Unmodified, + /// Menu completion + Menu, +} + +impl std::str::FromStr for CompType { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "9" => Ok(Self::Normal), + "63" => Ok(Self::Successive), + "33" => Ok(Self::Alternatives), + "64" => Ok(Self::Unmodified), + "37" => Ok(Self::Menu), + _ => Err(format!("unsupported COMP_TYPE `{}`", s)), + } + } +} + +impl Default for CompType { + fn default() -> Self { + Self::Normal + } +} |