usage/complete/
zsh.rs

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 bin_snake = bin.to_snake_case();
14    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    // Build logic to write spec directly to file without storing in shell variables
55    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// fn render_args(cmds: &[&SchemaCmd]) -> String {
106//     format!("XX")
107// }
108
109#[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}