1use crate::complete::CompleteOptions;
2use heck::ToSnakeCase;
3
4pub fn complete_zsh(opts: &CompleteOptions) -> String {
5 let usage_bin = &opts.usage_bin;
6 let bin = &opts.bin;
7 let bin_snake = bin.to_snake_case();
8 let spec_variable = if let Some(cache_key) = &opts.cache_key {
9 format!("_usage_spec_{bin_snake}_{}", cache_key.to_snake_case())
10 } else {
11 format!("_usage_spec_{bin_snake}")
12 };
13 let generated_comment = if let Some(source_file) = &opts.source_file {
15 format!("# @generated by usage-cli from {source_file}")
16 } else {
17 "# @generated by usage-cli from usage spec".to_string()
18 };
19 let mut out = vec![format!(
20 r#"#compdef {bin}
21{generated_comment}
22local curcontext="$curcontext""#
23 )];
24
25 if let Some(_usage_cmd) = &opts.usage_cmd {
26 out.push(format!(
27 r#"
28# caching config
29_usage_{bin_snake}_cache_policy() {{
30 if [[ -z "${{lifetime}}" ]]; then
31 lifetime=$((60*60*4)) # 4 hours
32 fi
33 local -a oldp
34 oldp=( "$1"(Nms+${{lifetime}}) )
35 (( $#oldp ))
36}}"#
37 ));
38 }
39
40 out.push(format!(
41 r#"
42_{bin_snake}() {{
43 typeset -A opt_args
44 local curcontext="$curcontext" cache_policy
45
46 if ! type -p {usage_bin} &> /dev/null; then
47 echo >&2
48 echo "Error: {usage_bin} CLI not found. This is required for completions to work in {bin}." >&2
49 echo "See https://usage.jdx.dev for more information." >&2
50 return 1
51 fi"#,
52 ));
53
54 let file_write_logic = if let Some(usage_cmd) = &opts.usage_cmd {
56 if opts.cache_key.is_some() {
57 format!(
58 r#"if [[ ! -f "$spec_file" ]]; then
59 {usage_cmd} > "$spec_file"
60 fi"#
61 )
62 } else {
63 format!(r#"{usage_cmd} > "$spec_file""#)
64 }
65 } else if let Some(spec) = &opts.spec {
66 let heredoc = format!(
67 r#"cat > "$spec_file" <<'__USAGE_EOF__'
68{spec}
69__USAGE_EOF__"#,
70 spec = spec.to_string().trim()
71 );
72 if opts.cache_key.is_some() {
73 format!(
74 r#"if [[ ! -f "$spec_file" ]]; then
75 {heredoc}
76fi"#
77 )
78 } else {
79 heredoc.to_string()
80 }
81 } else {
82 String::new()
83 };
84
85 out.push(format!(
86 r#"
87 local spec_file="${{TMPDIR:-/tmp}}/usage_{spec_variable}.spec"
88 {file_write_logic}
89 _arguments "*: :(($(command {usage_bin} complete-word --shell zsh -f "$spec_file" -- "${{words[@]}}" )))"
90 return 0
91}}
92
93if [ "$funcstack[1]" = "_{bin_snake}" ]; then
94 _{bin_snake} "$@"
95else
96 compdef _{bin_snake} {bin}
97fi
98
99# vim: noet ci pi sts=0 sw=4 ts=4"#,
100 ));
101
102 out.join("\n")
103}
104
105#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::test::SPEC_KITCHEN_SINK;
113 use insta::assert_snapshot;
114
115 #[test]
116 fn test_complete_zsh() {
117 assert_snapshot!(complete_zsh(&CompleteOptions {
118 usage_bin: "usage".to_string(),
119 shell: "zsh".to_string(),
120 bin: "mycli".to_string(),
121 cache_key: None,
122 spec: None,
123 usage_cmd: Some("mycli complete --usage".to_string()),
124 include_bash_completion_lib: false,
125 source_file: None,
126 }));
127 assert_snapshot!(complete_zsh(&CompleteOptions {
128 usage_bin: "usage".to_string(),
129 shell: "zsh".to_string(),
130 bin: "mycli".to_string(),
131 cache_key: Some("1.2.3".to_string()),
132 spec: None,
133 usage_cmd: Some("mycli complete --usage".to_string()),
134 include_bash_completion_lib: false,
135 source_file: None,
136 }));
137 assert_snapshot!(complete_zsh(&CompleteOptions {
138 usage_bin: "usage".to_string(),
139 shell: "zsh".to_string(),
140 bin: "mycli".to_string(),
141 cache_key: None,
142 spec: Some(SPEC_KITCHEN_SINK.clone()),
143 usage_cmd: None,
144 include_bash_completion_lib: false,
145 source_file: None,
146 }));
147 }
148}