use std::collections::BTreeMap;
pub type ValueProvider = fn(partial: &str) -> Vec<String>;
#[derive(Clone)]
pub enum Node {
Leaf {
options: Vec<String>,
flags: std::collections::HashSet<String>,
value_providers: BTreeMap<String, ValueProvider>,
},
Group { children: BTreeMap<String, Node> },
}
impl std::fmt::Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Node::Leaf { options, flags, value_providers } => {
f.debug_struct("Leaf")
.field("options", options)
.field("flags", flags)
.field("value_providers", &value_providers.keys().collect::<Vec<_>>())
.finish()
}
Node::Group { children } => {
f.debug_struct("Group").field("children", children).finish()
}
}
}
}
impl Node {
pub fn leaf(options: &[&str]) -> Self {
Node::Leaf {
options: options.iter().map(|s| s.to_string()).collect(),
flags: std::collections::HashSet::new(),
value_providers: BTreeMap::new(),
}
}
pub fn leaf_with_flags(options: &[&str], flags: &[&str]) -> Self {
Node::Leaf {
options: options.iter().chain(flags.iter()).map(|s| s.to_string()).collect(),
flags: flags.iter().map(|s| s.to_string()).collect(),
value_providers: BTreeMap::new(),
}
}
pub fn with_value_provider(mut self, option: &str, provider: ValueProvider) -> Self {
if let Node::Leaf { ref mut value_providers, .. } = self {
value_providers.insert(option.to_string(), provider);
}
self
}
pub fn is_flag(&self, option: &str) -> bool {
match self {
Node::Leaf { flags, .. } => flags.contains(option),
_ => false,
}
}
pub fn group(children: Vec<(&str, Node)>) -> Self {
Node::Group {
children: children.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
}
}
pub fn empty_group() -> Self {
Node::Group { children: BTreeMap::new() }
}
pub fn with_child(mut self, name: &str, child: Node) -> Self {
match &mut self {
Node::Group { children } => { children.insert(name.to_string(), child); }
Node::Leaf { .. } => panic!("cannot add child to leaf node"),
}
self
}
pub fn child_names(&self) -> Vec<&str> {
match self {
Node::Group { children } => children.keys().map(|k| k.as_str()).collect(),
Node::Leaf { .. } => Vec::new(),
}
}
pub fn child(&self, name: &str) -> Option<&Node> {
match self {
Node::Group { children } => children.get(name),
Node::Leaf { .. } => None,
}
}
pub fn options(&self) -> Vec<&str> {
match self {
Node::Leaf { options, .. } => options.iter().map(|s| s.as_str()).collect(),
Node::Group { .. } => Vec::new(),
}
}
}
pub struct CommandTree {
pub app_name: String,
pub root: Node,
pub hidden: std::collections::HashSet<String>,
pub global_value_providers: BTreeMap<String, ValueProvider>,
}
impl CommandTree {
pub fn new(app_name: &str) -> Self {
CommandTree {
app_name: app_name.to_string(),
root: Node::empty_group(),
hidden: std::collections::HashSet::new(),
global_value_providers: BTreeMap::new(),
}
}
pub fn command(mut self, name: &str, node: Node) -> Self {
self.root = self.root.with_child(name, node);
self
}
pub fn group(self, name: &str, node: Node) -> Self {
self.command(name, node)
}
pub fn hidden_command(mut self, name: &str, node: Node) -> Self {
self.hidden.insert(name.to_string());
self.command(name, node)
}
pub fn global_value_provider(mut self, option: &str, provider: ValueProvider) -> Self {
self.global_value_providers.insert(option.to_string(), provider);
self
}
}
pub fn complete(tree: &CommandTree, words: &[&str]) -> Vec<String> {
if words.len() <= 1 {
let mut cmds: Vec<String> = tree.root.child_names().iter()
.filter(|s| !tree.hidden.contains(&s.to_string()))
.map(|s| s.to_string())
.collect();
cmds.sort_by(|a, b| {
let a_flag = a.starts_with('-');
let b_flag = b.starts_with('-');
a_flag.cmp(&b_flag).then_with(|| a.cmp(b))
});
return cmds;
}
let partial = words.last().unwrap_or(&"");
let completed = &words[1..words.len() - 1];
let at_root = completed.is_empty();
let mut node = &tree.root;
let mut remaining_start = 0;
for (i, &word) in completed.iter().enumerate() {
match node.child(word) {
Some(child) => { node = child; remaining_start = i + 1; }
None => break, }
}
let remaining = &completed[remaining_start..];
if let Some(&prev_word) = completed.last() {
if let Some(provider) = tree.global_value_providers.get(prev_word) {
return provider(partial);
}
}
match node {
Node::Group { children } => {
let mut candidates: Vec<String> = children.keys()
.filter(|k| k.starts_with(partial))
.filter(|k| !at_root || !partial.is_empty() || !tree.hidden.contains(k.as_str()))
.map(|k| k.to_string())
.collect();
candidates.sort_by(|a, b| {
a.starts_with('-').cmp(&b.starts_with('-')).then_with(|| a.cmp(b))
});
candidates
}
Node::Leaf { options, flags, value_providers } => {
if let Some(&prev_word) = remaining.last() {
if prev_word.starts_with("--") && !flags.contains(prev_word) {
if let Some(provider) = value_providers.get(prev_word) {
return provider(partial);
}
if let Some(provider) = tree.global_value_providers.get(prev_word) {
return provider(partial);
}
return Vec::new();
}
}
if partial.starts_with('-') || partial.is_empty() {
let mut candidates: Vec<String> = options.iter()
.filter(|o| o.starts_with(partial))
.map(|o| o.to_string())
.collect();
for global_opt in tree.global_value_providers.keys() {
if global_opt.starts_with(partial) && !candidates.contains(global_opt) {
candidates.push(global_opt.clone());
}
}
candidates.sort();
candidates
} else {
Vec::new()
}
}
}
}
pub fn print_bash_script(app_name: &str) {
let env_var = format!("_{}_COMPLETE", app_name.to_uppercase().replace('-', "_"));
let completer = std::env::args_os()
.next()
.and_then(|p| {
let path = std::path::PathBuf::from(&p);
if path.components().count() > 1 {
std::env::current_dir().ok().map(|cwd| cwd.join(path))
} else {
Some(path)
}
})
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| app_name.to_string());
print!(r#"_{app}_complete() {{
COMP_WORDBREAKS="${{COMP_WORDBREAKS//:}}"
local line="${{COMP_LINE:0:$COMP_POINT}}"
local -a words=()
local word=""
local in_quote=""
local i=0
while [ $i -lt ${{#line}} ]; do
local ch="${{line:$i:1}}"
if [ -n "$in_quote" ]; then
if [ "$ch" = "$in_quote" ]; then
in_quote=""
else
word+="$ch"
fi
elif [ "$ch" = "'" ] || [ "$ch" = '"' ]; then
in_quote="$ch"
elif [ "$ch" = " " ] || [ "$ch" = $'\t' ]; then
if [ -n "$word" ]; then
words+=("$word")
word=""
fi
else
word+="$ch"
fi
i=$((i + 1))
done
words+=("$word")
local IFS=$'\n'
COMPREPLY=($({env_var}=bash _COMP_SHELL_PID=$$ "{completer}" -- "${{words[@]}}" 2>/dev/null))
}}
if [[ "${{BASH_VERSINFO[0]}}" -eq 4 && "${{BASH_VERSINFO[1]}}" -ge 4 || "${{BASH_VERSINFO[0]}}" -gt 4 ]]; then
complete -o default -o bashdefault -o nosort -F _{app}_complete {app}
else
complete -o default -o bashdefault -F _{app}_complete {app}
fi
"#,
app = app_name,
env_var = env_var,
completer = completer,
);
}
pub fn handle_complete_env(app_name: &str, tree: &CommandTree) -> bool {
let env_var = format!("_{}_COMPLETE", app_name.to_uppercase().replace('-', "_"));
let is_ours = std::env::var(&env_var).ok().as_deref() == Some("bash");
let is_legacy = std::env::var("COMPLETE").ok().as_deref() == Some("bash");
if !is_ours && !is_legacy {
return false;
}
let args: Vec<String> = std::env::args().collect();
let words_start = args.iter().position(|a| a == "--").map(|i| i + 1).unwrap_or(1);
let words: Vec<&str> = args[words_start..].iter().map(|s| s.as_str()).collect();
let input_key = words[1..].join(" ");
let tap_count = tap_detect(app_name, &input_key);
let expanded = tap_count >= 3 && tap_count % 2 == 1;
let candidates = if expanded {
complete_expanded(tree, &words)
} else {
complete(tree, &words)
};
for candidate in candidates {
println!("{}", candidate);
}
true
}
fn tap_detect(app_name: &str, input_key: &str) -> u32 {
use std::io::Write;
let ppid = std::env::var("_COMP_SHELL_PID")
.or_else(|_| std::env::var("PPID"))
.unwrap_or_else(|_| "0".to_string());
let tap_file = std::path::PathBuf::from(format!("/tmp/.{}_tap_{}", app_name, ppid));
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut count = 1u32;
if let Ok(content) = std::fs::read_to_string(&tap_file) {
let mut parts = content.splitn(3, ' ');
let prev_time: u64 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let prev_count: u32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let prev_key = parts.next().unwrap_or("").trim();
if prev_key == input_key && now.saturating_sub(prev_time) < 5 {
count = prev_count + 1;
}
}
if let Ok(mut f) = std::fs::File::create(&tap_file) {
let _ = write!(f, "{} {} {}", now, count, input_key);
}
count
}
pub fn complete_expanded(tree: &CommandTree, words: &[&str]) -> Vec<String> {
let partial = if words.len() > 1 { words.last().unwrap_or(&"") } else { &"" };
let completed = if words.len() > 2 { &words[1..words.len() - 1] } else { &[] };
if !completed.is_empty() || !partial.is_empty() {
return complete(tree, words);
}
let mut results = Vec::new();
if let Node::Group { children } = &tree.root {
for (name, node) in children {
if name == "help" || name.starts_with('-') {
continue;
}
match node {
Node::Group { children: sub } if !sub.is_empty() => {
for sub_name in sub.keys() {
results.push(format!("{} {}", name, sub_name));
}
}
_ => {
if !tree.hidden.contains(name.as_str()) {
results.push(name.to_string());
}
}
}
}
}
results
}