Skip to main content

try_rs/
shell.rs

1use crate::cli::Shell;
2use crate::config::{get_base_config_dir, get_config_dir};
3use anyhow::Result;
4use std::fs;
5use std::io::Write;
6use std::path::PathBuf;
7
8/// Returns the shell integration script content for the given shell type.
9/// This is used by --setup-stdout to print the content to stdout.
10pub fn get_shell_content(shell: &Shell) -> String {
11    let completions = get_completions_script(shell);
12    match shell {
13        Shell::Fish => {
14            format!(
15                r#"function try-rs
16    # Pass flags/options directly to stdout without capturing
17    for arg in $argv
18        if string match -q -- '-*' $arg
19            command try-rs $argv
20            return
21        end
22    end
23
24    # Captures the output of the binary (stdout) which is the "cd" command
25    # The TUI is rendered on stderr, so it doesn't interfere.
26    set command (command try-rs $argv | string collect)
27
28    if test -n "$command"
29        eval $command
30    end
31end
32
33{completions}"#
34            )
35        }
36        Shell::Zsh => {
37            format!(
38                r#"try-rs() {{
39    # Pass flags/options directly to stdout without capturing
40    for arg in "$@"; do
41        case "$arg" in
42            -*) command try-rs "$@"; return ;;
43        esac
44    done
45
46    # Captures the output of the binary (stdout) which is the "cd" command
47    # The TUI is rendered on stderr, so it doesn't interfere.
48    local output
49    output=$(command try-rs "$@")
50
51    if [ -n "$output" ]; then
52        eval "$output"
53    fi
54}}
55
56{completions}"#
57            )
58        }
59        Shell::Bash => {
60            format!(
61                r#"try-rs() {{
62    # Pass flags/options directly to stdout without capturing
63    for arg in "$@"; do
64        case "$arg" in
65            -*) command try-rs "$@"; return ;;
66        esac
67    done
68
69    # Captures the output of the binary (stdout) which is the "cd" command
70    # The TUI is rendered on stderr, so it doesn't interfere.
71    local output
72    output=$(command try-rs "$@")
73
74    if [ -n "$output" ]; then
75        eval "$output"
76    fi
77}}
78
79{completions}"#
80            )
81        }
82        Shell::PowerShell => {
83            format!(
84                r#"# try-rs integration for PowerShell
85function try-rs {{
86    # Pass flags/options directly to stdout without capturing
87    foreach ($a in $args) {{
88        if ($a -like '-*') {{
89            & try-rs.exe @args
90            return
91        }}
92    }}
93
94    # Captures the output of the binary (stdout) which is the "cd" or editor command
95    # The TUI is rendered on stderr, so it doesn't interfere.
96    $command = (try-rs.exe @args)
97
98    if ($command) {{
99        Invoke-Expression $command
100    }}
101}}
102
103{completions}"#
104            )
105        }
106        Shell::NuShell => {
107            format!(
108                r#"def --wrapped try-rs [...args] {{
109    # Pass flags/options directly to stdout without capturing
110    for arg in $args {{
111        if ($arg | str starts-with '-') {{
112            ^try-rs.exe ...$args
113            return
114        }}
115    }}
116
117    # Capture output. Stderr (TUI) goes directly to terminal.
118    let output = (try-rs.exe ...$args)
119
120    if ($output | is-not-empty) {{
121
122        # Grabs the path out of stdout returned by the binary and removes the single quotes
123        let $path = ($output | split row ' ').1 | str replace --all "'" ''
124        cd $path
125    }}
126}}
127
128{completions}"#
129            )
130        }
131    }
132}
133
134/// Returns the tab completion script for the given shell.
135/// This provides dynamic completion of directory names from the tries_path.
136pub fn get_completions_script(shell: &Shell) -> String {
137    match shell {
138        Shell::Fish => {
139            r#"# try-rs tab completion for directory names
140function __try_rs_get_tries_path
141    # Check TRY_PATH environment variable first
142    if set -q TRY_PATH
143        echo $TRY_PATH
144        return
145    end
146    
147    # Try to read from config file
148    set -l config_paths "$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml"
149    for config_path in $config_paths
150        if test -f $config_path
151            set -l tries_path (command grep -E '^\s*tries_path\s*=' $config_path 2>/dev/null | command sed 's/.*=\s*"\?\([^"]*\)"\?.*/\1/' | command sed "s|~|$HOME|" | string trim)
152            if test -n "$tries_path"
153                echo $tries_path
154                return
155            end
156        end
157    end
158    
159    # Default path
160    echo "$HOME/work/tries"
161end
162
163function __try_rs_complete_directories
164    set -l tries_path (__try_rs_get_tries_path)
165    
166    if test -d $tries_path
167        # List directories in tries_path, filtering by current token
168        command ls -1 $tries_path 2>/dev/null | while read -l dir
169            if test -d "$tries_path/$dir"
170                echo $dir
171            end
172        end
173    end
174end
175
176complete -f -c try-rs -n '__fish_use_subcommand' -a '(__try_rs_complete_directories)' -d 'Try directory'
177"#.to_string()
178        }
179        Shell::Zsh => {
180            r#"# try-rs tab completion for directory names
181_try_rs_get_tries_path() {
182    # Check TRY_PATH environment variable first
183    if [[ -n "${TRY_PATH}" ]]; then
184        echo "${TRY_PATH}"
185        return
186    fi
187    
188    # Try to read from config file
189    local config_paths=("$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml")
190    for config_path in "${config_paths[@]}"; do
191        if [[ -f "$config_path" ]]; then
192            local tries_path=$(grep -E '^\s*tries_path\s*=' "$config_path" 2>/dev/null | sed 's/.*=\s*"\?\([^"]*\)"\?.*/\1/' | sed "s|~|$HOME|" | tr -d '[:space:]')
193            if [[ -n "$tries_path" ]]; then
194                echo "$tries_path"
195                return
196            fi
197        fi
198    done
199    
200    # Default path
201    echo "$HOME/work/tries"
202}
203
204_try_rs_complete() {
205    local cur="${COMP_WORDS[COMP_CWORD]}"
206    local tries_path=$(_try_rs_get_tries_path)
207    local -a dirs=()
208    
209    if [[ -d "$tries_path" ]]; then
210        # Get list of directories
211        while IFS= read -r dir; do
212            dirs+=("$dir")
213        done < <(ls -1 "$tries_path" 2>/dev/null | while read -r dir; do
214            if [[ -d "$tries_path/$dir" ]]; then
215                echo "$dir"
216            fi
217        done)
218    fi
219    
220    COMPREPLY=($(compgen -W "${dirs[*]}" -- "$cur"))
221}
222
223complete -o default -F _try_rs_complete try-rs
224"#.to_string()
225        }
226        Shell::Bash => {
227            r#"# try-rs tab completion for directory names
228_try_rs_get_tries_path() {
229    # Check TRY_PATH environment variable first
230    if [[ -n "${TRY_PATH}" ]]; then
231        echo "${TRY_PATH}"
232        return
233    fi
234    
235    # Try to read from config file
236    local config_paths=("$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml")
237    for config_path in "${config_paths[@]}"; do
238        if [[ -f "$config_path" ]]; then
239            local tries_path=$(grep -E '^[[:space:]]*tries_path[[:space:]]*=' "$config_path" 2>/dev/null | sed 's/.*=[[:space:]]*"\?\([^"]*\)"\?.*/\1/' | sed "s|~|$HOME|" | tr -d '[:space:]')
240            if [[ -n "$tries_path" ]]; then
241                echo "$tries_path"
242                return
243            fi
244        fi
245    done
246    
247    # Default path
248    echo "$HOME/work/tries"
249}
250
251_try_rs_complete() {
252    local cur="${COMP_WORDS[COMP_CWORD]}"
253    local tries_path=$(_try_rs_get_tries_path)
254    local dirs=""
255    
256    if [[ -d "$tries_path" ]]; then
257        # Get list of directories
258        while IFS= read -r dir; do
259            if [[ -d "$tries_path/$dir" ]]; then
260                dirs="$dirs $dir"
261            fi
262        done < <(ls -1 "$tries_path" 2>/dev/null)
263    fi
264    
265    COMPREPLY=($(compgen -W "$dirs" -- "$cur"))
266}
267
268complete -o default -F _try_rs_complete try-rs
269"#.to_string()
270        }
271        Shell::PowerShell => {
272            r#"# try-rs tab completion for directory names
273Register-ArgumentCompleter -CommandName try-rs -ScriptBlock {
274    param($wordToComplete, $commandAst, $cursorPosition)
275    
276    # Get tries path from environment variable or default
277    $triesPath = $env:TRY_PATH
278    if (-not $triesPath) {
279        # Try to read from config file
280        $configPaths = @(
281            "$env:USERPROFILE/.config/try-rs/config.toml",
282            "$env:USERPROFILE/.try-rs/config.toml"
283        )
284        foreach ($configPath in $configPaths) {
285            if (Test-Path $configPath) {
286                $content = Get-Content $configPath -Raw
287                if ($content -match 'tries_path\s*=\s*["'']?([^"'']+)["'']?') {
288                    $triesPath = $matches[1].Replace('~', $env:USERPROFILE).Trim()
289                    break
290                }
291            }
292        }
293    }
294    
295    # Default path
296    if (-not $triesPath) {
297        $triesPath = "$env:USERPROFILE/work/tries"
298    }
299    
300    # Get directories
301    if (Test-Path $triesPath) {
302        Get-ChildItem -Path $triesPath -Directory | 
303            Where-Object { $_.Name -like "$wordToComplete*" } |
304            ForEach-Object { 
305                [System.Management.Automation.CompletionResult]::new(
306                    $_.Name, 
307                    $_.Name, 
308                    'ParameterValue', 
309                    $_.Name
310                )
311            }
312    }
313}
314"#.to_string()
315        }
316        Shell::NuShell => {
317            r#"# try-rs tab completion for directory names
318# Add this to your Nushell config or env file
319
320export def __try_rs_get_tries_path [] {
321    # Check TRY_PATH environment variable first
322    if ($env.TRY_PATH? | is-not-empty) {
323        return $env.TRY_PATH
324    }
325    
326    # Try to read from config file
327    let config_paths = [
328        ($env.HOME | path join ".config" "try-rs" "config.toml"),
329        ($env.HOME | path join ".try-rs" "config.toml")
330    ]
331    
332    for config_path in $config_paths {
333        if ($config_path | path exists) {
334            let content = (open $config_path | str trim)
335            if ($content =~ 'tries_path\\s*=\\s*"?([^"]+)"?') {
336                let path = ($content | parse -r 'tries_path\\s*=\\s*"?([^"]+)"?' | get capture0.0? | default "")
337                if ($path | is-not-empty) {
338                    return ($path | str replace "~" $env.HOME)
339                }
340            }
341        }
342    }
343    
344    # Default path
345    ($env.HOME | path join "work" "tries")
346}
347
348export def __try_rs_complete [context: string] {
349    let tries_path = (__try_rs_get_tries_path)
350    
351    if ($tries_path | path exists) {
352        ls $tries_path | where type == "dir" | get name | path basename
353    } else {
354        []
355    }
356}
357
358# Add completion to the try-rs command
359export extern try-rs [
360    name_or_url?: string@__try_rs_complete
361    destination?: string
362    --setup: string
363    --setup-stdout: string
364    --completions: string
365    --shallow-clone(-s)
366    --worktree(-w): string
367]
368"#.to_string()
369        }
370    }
371}
372
373/// Returns only the completion script (for --completions flag)
374pub fn get_completion_script_only(shell: &Shell) -> String {
375    let completions = get_completions_script(shell);
376    match shell {
377        Shell::NuShell => {
378            // For NuShell, we need to provide a different format when used standalone
379            r#"# try-rs tab completion for directory names
380# Add this to your Nushell config
381
382def __try_rs_get_tries_path [] {
383    if ($env.TRY_PATH? | is-not-empty) {
384        return $env.TRY_PATH
385    }
386    
387    let config_paths = [
388        ($env.HOME | path join ".config" "try-rs" "config.toml"),
389        ($env.HOME | path join ".try-rs" "config.toml")
390    ]
391    
392    for config_path in $config_paths {
393        if ($config_path | path exists) {
394            let content = (open $config_path | str trim)
395            if ($content =~ 'tries_path\\s*=\\s*"?([^"]+)"?') {
396                let path = ($content | parse -r 'tries_path\\s*=\\s*"?([^"]+)"?' | get capture0.0? | default "")
397                if ($path | is-not-empty) {
398                    return ($path | str replace "~" $env.HOME)
399                }
400            }
401        }
402    }
403    
404    ($env.HOME | path join "work" "tries")
405}
406
407def __try_rs_complete [context: string] {
408    let tries_path = (__try_rs_get_tries_path)
409    
410    if ($tries_path | path exists) {
411        ls $tries_path | where type == "dir" | get name | path basename
412    } else {
413        []
414    }
415}
416
417# Register completion
418export extern try-rs [
419    name_or_url?: string@__try_rs_complete
420    destination?: string
421    --setup: string
422    --setup-stdout: string
423    --completions: string
424    --shallow-clone(-s)
425    --worktree(-w): string
426]
427"#.to_string()
428        }
429        _ => completions,
430    }
431}
432
433pub fn get_shell_integration_path(shell: &Shell) -> PathBuf {
434    let config_dir = match shell {
435        Shell::Fish => get_base_config_dir(),
436        _ => get_config_dir(),
437    };
438
439    match shell {
440        Shell::Fish => config_dir
441            .join("fish")
442            .join("functions")
443            .join("try-rs.fish"),
444        Shell::Zsh => config_dir.join("try-rs.zsh"),
445        Shell::Bash => config_dir.join("try-rs.bash"),
446        Shell::PowerShell => config_dir.join("try-rs.ps1"),
447        Shell::NuShell => config_dir.join("try-rs.nu"),
448    }
449}
450
451pub fn is_shell_integration_configured(shell: &Shell) -> bool {
452    get_shell_integration_path(shell).exists()
453}
454
455/// Appends a source command to an RC file if not already present.
456fn append_source_to_rc(rc_path: &std::path::Path, source_cmd: &str) -> Result<()> {
457    if rc_path.exists() {
458        let content = fs::read_to_string(rc_path)?;
459        if !content.contains(source_cmd) {
460            let mut file = fs::OpenOptions::new().append(true).open(rc_path)?;
461            writeln!(file, "\n# try-rs integration")?;
462            writeln!(file, "{}", source_cmd)?;
463            eprintln!("Added configuration to {}", rc_path.display());
464        } else {
465            eprintln!("Configuration already present in {}", rc_path.display());
466        }
467    } else {
468        eprintln!(
469            "You need to add the following line to {}:",
470            rc_path.display()
471        );
472        eprintln!("{}", source_cmd);
473    }
474    Ok(())
475}
476
477/// Writes the shell integration file and returns its path.
478fn write_shell_integration(shell: &Shell) -> Result<std::path::PathBuf> {
479    let file_path = get_shell_integration_path(shell);
480    if let Some(parent) = file_path.parent()
481        && !parent.exists()
482    {
483        fs::create_dir_all(parent)?;
484    }
485    fs::write(&file_path, get_shell_content(shell))?;
486    eprintln!(
487        "{:?} function file created at: {}",
488        shell,
489        file_path.display()
490    );
491    Ok(file_path)
492}
493
494/// Sets up shell integration for the given shell.
495pub fn setup_shell(shell: &Shell) -> Result<()> {
496    let file_path = write_shell_integration(shell)?;
497    let home_dir = dirs::home_dir().expect("Could not find home directory");
498
499    match shell {
500        Shell::Fish => {
501            eprintln!(
502                "You may need to restart your shell or run 'source {}' to apply changes.",
503                file_path.display()
504            );
505        }
506        Shell::Zsh => {
507            let source_cmd = format!("source '{}'", file_path.display());
508            append_source_to_rc(&home_dir.join(".zshrc"), &source_cmd)?;
509        }
510        Shell::Bash => {
511            let source_cmd = format!("source '{}'", file_path.display());
512            append_source_to_rc(&home_dir.join(".bashrc"), &source_cmd)?;
513        }
514        Shell::PowerShell => {
515            let profile_path_ps7 = home_dir
516                .join("Documents")
517                .join("PowerShell")
518                .join("Microsoft.PowerShell_profile.ps1");
519            let profile_path_ps5 = home_dir
520                .join("Documents")
521                .join("WindowsPowerShell")
522                .join("Microsoft.PowerShell_profile.ps1");
523            let profile_path = if profile_path_ps7.exists() {
524                profile_path_ps7
525            } else if profile_path_ps5.exists() {
526                profile_path_ps5
527            } else {
528                profile_path_ps7
529            };
530
531            if let Some(parent) = profile_path.parent()
532                && !parent.exists()
533            {
534                fs::create_dir_all(parent)?;
535            }
536
537            let source_cmd = format!(". '{}'", file_path.display());
538            if profile_path.exists() {
539                append_source_to_rc(&profile_path, &source_cmd)?;
540            } else {
541                let mut file = fs::File::create(&profile_path)?;
542                writeln!(file, "# try-rs integration")?;
543                writeln!(file, "{}", source_cmd)?;
544                eprintln!(
545                    "PowerShell profile created and configured at: {}",
546                    profile_path.display()
547                );
548            }
549
550            eprintln!(
551                "You may need to restart your shell or run '. {}' to apply changes.",
552                profile_path.display()
553            );
554            eprintln!(
555                "If you get an error about running scripts, you may need to run: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned"
556            );
557        }
558        Shell::NuShell => {
559            let nu_config_path = dirs::config_dir()
560                .expect("Could not find config directory")
561                .join("nushell")
562                .join("config.nu");
563            let source_cmd = format!("source '{}'", file_path.display());
564            if nu_config_path.exists() {
565                append_source_to_rc(&nu_config_path, &source_cmd)?;
566            } else {
567                eprintln!("Could not find config.nu at {}", nu_config_path.display());
568                eprintln!("Please add the following line manually:");
569                eprintln!("{}", source_cmd);
570            }
571        }
572    }
573
574    Ok(())
575}
576
577/// Generates a standalone completion script for the given shell.
578pub fn generate_completions(shell: &Shell) -> Result<()> {
579    let script = get_completion_script_only(shell);
580    print!("{}", script);
581    Ok(())
582}