Skip to main content

flag_rs/
shell.rs

1//! Shell completion script generation
2//!
3//! This module provides functionality to generate shell completion scripts
4//! for Bash, Zsh, and Fish shells. The generated scripts integrate with the
5//! dynamic completion system to provide TAB completions at runtime.
6
7use crate::command::Command;
8
9/// Supported shell types for completion generation
10///
11/// This enum represents the shells for which we can generate completion scripts.
12///
13/// # Examples
14///
15/// ```
16/// use flag_rs::shell::Shell;
17/// use flag_rs::Command;
18///
19/// let cmd = Command::new("myapp");
20///
21/// // Generate Bash completion script
22/// let bash_script = cmd.generate_completion(Shell::Bash);
23///
24/// // Generate Zsh completion script
25/// let zsh_script = cmd.generate_completion(Shell::Zsh);
26///
27/// // Generate Fish completion script
28/// let fish_script = cmd.generate_completion(Shell::Fish);
29/// ```
30#[derive(Debug, Clone, Copy)]
31pub enum Shell {
32    /// Bash shell (most common on Linux)
33    Bash,
34    /// Zsh shell (default on macOS)
35    Zsh,
36    /// Fish shell (modern alternative shell)
37    Fish,
38}
39
40impl Command {
41    /// Generates a completion script for the specified shell
42    ///
43    /// The generated script should be saved to the appropriate location
44    /// for your shell to load it automatically.
45    ///
46    /// # Arguments
47    ///
48    /// * `shell` - The shell to generate completions for
49    ///
50    /// # Returns
51    ///
52    /// A string containing the shell completion script
53    ///
54    /// # Shell-specific installation
55    ///
56    /// ## Bash
57    /// Save to `/etc/bash_completion.d/myapp` or source from `.bashrc`:
58    /// ```bash
59    /// myapp completion bash > ~/.myapp-completion.bash
60    /// echo "source ~/.myapp-completion.bash" >> ~/.bashrc
61    /// ```
62    ///
63    /// ## Zsh
64    /// Save to a directory in your `$fpath`:
65    /// ```bash
66    /// myapp completion zsh > ~/.zsh/completions/_myapp
67    /// ```
68    ///
69    /// ## Fish
70    /// Save to Fish's completion directory:
71    /// ```bash
72    /// myapp completion fish > ~/.config/fish/completions/myapp.fish
73    /// ```
74    pub fn generate_completion(&self, shell: Shell) -> String {
75        match shell {
76            Shell::Bash => self.generate_bash_completion(),
77            Shell::Zsh => self.generate_zsh_completion(),
78            Shell::Fish => self.generate_fish_completion(),
79        }
80    }
81
82    fn generate_bash_completion(&self) -> String {
83        let name = self.name();
84        let upper = name.to_uppercase();
85        format!(
86            r#"# Bash completion for {name}
87_{name}_complete() {{
88    local cur prev words cword
89    _get_comp_words_by_ref -n : cur prev words cword
90
91    # Call our binary with special completion env var
92    local IFS=$'\n'
93    local response
94    response=$({upper}_COMPLETE=bash "${{words[0]}}" __complete "${{words[@]:1:$((cword-1))}}" "$cur" 2>/dev/null)
95
96    if [[ -n "$response" ]]; then
97        # Use printf to handle each line separately
98        local lines=()
99        local help_messages=()
100        while IFS= read -r line; do
101            if [[ "$line" == _activehelp_* ]]; then
102                # Extract help message
103                help_messages+=("${{line#_activehelp_ }}")
104            else
105                lines+=("$line")
106            fi
107        done <<< "$response"
108        COMPREPLY=( "${{lines[@]}}" )
109
110        # Display help messages if any
111        if [[ ${{#help_messages[@]}} -gt 0 ]]; then
112            printf '\n'
113            for msg in "${{help_messages[@]}}"; do
114                printf '%s\n' "$msg"
115            done
116            printf '\n'
117        fi
118    fi
119}}
120
121complete -F _{name}_complete {name}
122"#
123        )
124    }
125
126    fn generate_zsh_completion(&self) -> String {
127        let name = self.name();
128        let upper = name.to_uppercase();
129        format!(
130            r#"#compdef -P {name}
131# Zsh completion for {name}
132
133_{name}_complete() {{
134    local -a completions
135    local IFS=$'\n'
136
137    # Get the actual command from the command line
138    local cmd="${{words[1]}}"
139    if [[ "$cmd" != /* ]] && ! command -v "$cmd" &>/dev/null; then
140        # If not found in PATH, try relative path
141        if [[ -x "$cmd" ]]; then
142            cmd="./$cmd"
143        fi
144    fi
145
146    # Build completion arguments
147    local -a comp_line
148    comp_line=("__complete")
149
150    # Add all words except the command name
151    local i
152    for (( i = 2; i < CURRENT; i++ )); do
153        comp_line+=("${{words[$i]}}")
154    done
155
156    # Add the current word being completed
157    comp_line+=("${{words[CURRENT]}}")
158
159    # Call the command with completion environment variable
160    local response
161    response=$({upper}_COMPLETE=zsh "$cmd" "${{comp_line[@]}}" 2>/dev/null)
162
163    if [[ -n "$response" ]]; then
164        local -a values
165        local -a descriptions
166        local -a help_messages
167        local line
168
169        # Parse response lines
170        while IFS= read -r line; do
171            if [[ "$line" == _activehelp_::* ]]; then
172                # ActiveHelp message
173                help_messages+=("${{line#_activehelp_::}}")
174            elif [[ "$line" == *:* ]]; then
175                # Line has description
176                values+=("${{line%%:*}}")
177                descriptions+=("${{line#*:}}")
178            else
179                # No description
180                values+=("$line")
181                descriptions+=("")
182            fi
183        done <<< "$response"
184
185        # Display ActiveHelp messages if any
186        if [[ ${{#help_messages[@]}} -gt 0 ]]; then
187            local formatted_help=()
188            for msg in "${{help_messages[@]}}"; do
189                formatted_help+=("-- $msg --")
190            done
191            compadd -x "${{(j: :)formatted_help}}"
192        fi
193
194        # Add completions with descriptions
195        if [[ ${{#descriptions[@]}} -gt 0 ]] && [[ -n "${{descriptions[*]// }}" ]]; then
196            compadd -Q -d descriptions -a values
197        else
198            compadd -Q -a values
199        fi
200    fi
201}}
202
203compdef _{name}_complete {name}
204"#
205        )
206    }
207
208    fn generate_fish_completion(&self) -> String {
209        let name = self.name();
210        let upper = name.to_uppercase();
211        // Regular string (not raw): `\t` MUST be a literal tab to match the
212        // `_activehelp_\t<msg>` separator that completion_format.rs emits.
213        // A raw `r#"..."#` would produce literal backslash-t and silently
214        // break ActiveHelp matching in fish.
215        format!(
216            "# Fish completion for {name}
217function __{name}_complete
218    set -l cmd (commandline -opc)
219    set -l cursor (commandline -C)
220    set -l current (commandline -ct)
221
222    # Call our binary with special completion env var
223    set -l response (env {upper}_COMPLETE=fish $cmd[1] __complete $cmd[2..-1] $current 2>/dev/null)
224
225    # Process response and handle ActiveHelp
226    set -l help_messages
227    for line in $response
228        if string match -q '_activehelp_*' -- $line
229            # Extract help message
230            set -a help_messages (string replace '_activehelp_\t' '' -- $line)
231        else
232            echo $line
233        end
234    end
235
236    # Display help messages if any
237    if test (count $help_messages) -gt 0
238        for msg in $help_messages
239            echo \"ยป $msg\" >&2
240        end
241    end
242end
243
244complete -c {name} -f -a '(__{name}_complete)'
245"
246        )
247    }
248}