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
8const FISH_PICKER_FUNCTION: &str = r#"function try-rs-picker
9    set -l picker_args --inline-picker
10
11    if set -q TRY_RS_PICKER_HEIGHT
12        if string match -qr '^[0-9]+$' -- "$TRY_RS_PICKER_HEIGHT"
13            set picker_args $picker_args --inline-height $TRY_RS_PICKER_HEIGHT
14        end
15    end
16
17    if status --is-interactive
18        printf "\n"
19    end
20
21    set command (command try-rs $picker_args | string collect)
22    set command_status $status
23
24    if test $command_status -eq 0; and test -n "$command"
25        eval $command
26    end
27
28    if status --is-interactive
29        printf "\033[A"
30        commandline -f repaint
31    end
32end
33"#;
34
35/// Returns the shell integration script content for the given shell type.
36/// This is used by --setup-stdout to print the content to stdout.
37pub fn get_shell_content(shell: &Shell) -> String {
38    let completions = get_completions_script(shell);
39    match shell {
40        Shell::Fish => {
41            format!(
42                r#"function try-rs
43    # Pass flags/options directly to stdout without capturing
44    for arg in $argv
45        if string match -q -- '-*' $arg
46            command try-rs $argv
47            return
48        end
49    end
50
51    # Captures the output of the binary (stdout) which is the "cd" command
52    # The TUI is rendered on stderr, so it doesn't interfere.
53    set command (command try-rs $argv | string collect)
54    set command_status $status
55
56    if test $command_status -eq 0; and test -n "$command"
57        eval $command
58    end
59end
60
61{picker_function}
62
63{completions}"#,
64                picker_function = FISH_PICKER_FUNCTION,
65            )
66        }
67        Shell::Zsh => {
68            format!(
69                r#"try-rs() {{
70    # Pass flags/options directly to stdout without capturing
71    for arg in "$@"; do
72        case "$arg" in
73            -*) command try-rs "$@"; return ;;
74        esac
75    done
76
77    # Captures the output of the binary (stdout) which is the "cd" command
78    # The TUI is rendered on stderr, so it doesn't interfere.
79    local output
80    output=$(command try-rs "$@")
81
82    if [ -n "$output" ]; then
83        eval "$output"
84    fi
85}}
86
87{completions}"#
88            )
89        }
90        Shell::Bash => {
91            format!(
92                r#"try-rs() {{
93    # Pass flags/options directly to stdout without capturing
94    for arg in "$@"; do
95        case "$arg" in
96            -*) command try-rs "$@"; return ;;
97        esac
98    done
99
100    # Captures the output of the binary (stdout) which is the "cd" command
101    # The TUI is rendered on stderr, so it doesn't interfere.
102    local output
103    output=$(command try-rs "$@")
104
105    if [ -n "$output" ]; then
106        eval "$output"
107    fi
108}}
109
110{completions}"#
111            )
112        }
113        Shell::PowerShell => {
114            format!(
115                r#"# try-rs integration for PowerShell
116function try-rs {{
117    # Pass flags/options directly to stdout without capturing
118    foreach ($a in $args) {{
119        if ($a -like '-*') {{
120            & try-rs.exe @args
121            return
122        }}
123    }}
124
125    # Captures the output of the binary (stdout) which is the "cd" or editor command
126    # The TUI is rendered on stderr, so it doesn't interfere.
127    $command = (try-rs.exe @args)
128
129    if ($command) {{
130        Invoke-Expression $command
131    }}
132}}
133
134{completions}"#
135            )
136        }
137        Shell::NuShell => {
138            format!(
139                r#"def --env --wrapped try-rs [
140    name_or_url?: string@__try_rs_complete
141    ...args
142] {{
143    let all_args = (if $name_or_url == null {{ [] }} else {{ [$name_or_url] }} | append $args)
144
145    # Pass flags/options directly to stdout without capturing
146    if ($all_args | any {{ |arg| $arg | str starts-with '-' }}) {{
147        ^try-rs ...$all_args
148        return
149    }}
150
151    # Capture output. Stderr (TUI) goes directly to terminal.
152    let output = (^try-rs ...$all_args | str trim)
153
154    if ($output | is-not-empty) {{
155        if ($output | str starts-with "cd ") {{
156            # Grabs the path out of stdout returned by the binary and removes the single quotes
157            let path = ($output | str replace --regex '^cd ' '' | str replace --all "'" "" | str replace --all '"' "")
158            if ($path | path exists) {{
159                cd $path
160            }}
161        }} else {{
162            # If it's not a cd command, it's likely an editor command
163            nu -c $output
164        }}
165    }}
166}}
167
168{completions}"#,
169                completions = get_completions_script(shell),
170            )
171        }
172    }
173}
174
175/// Returns the tab completion script for the given shell.
176/// This provides dynamic completion of directory names from the tries_path.
177pub fn get_completions_script(shell: &Shell) -> String {
178    match shell {
179        Shell::Fish => {
180            r#"# try-rs tab completion for directory names
181function __try_rs_get_tries_path
182    # Check TRY_PATH environment variable first
183    if set -q TRY_PATH
184        # Check if contains comma
185        if echo "$TRY_PATH" | command grep -q ","
186            for path in (string split "," $TRY_PATH)
187                printf '%s\n' (string trim $path)
188            end
189        else
190            printf '%s\n' $TRY_PATH
191        end
192        return
193    end
194    
195    # Try to read from config file
196    set -l config_paths "$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml"
197    for config_path in $config_paths
198        if test -f $config_path
199            # Try tries_path (supports single or multiple paths with comma)
200            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)
201            if test -n "$tries_path"
202                # Check if it contains comma (multiple paths)
203                if echo "$tries_path" | command grep -q ","
204                    for path in (string split "," $tries_path)
205                        printf '%s\n' (string trim $path)
206                    end
207                else
208                    printf '%s\n' $tries_path
209                end
210                return
211            end
212        end
213    end
214    
215    # Default path
216    printf '%s\n' "$HOME/work/tries"
217end
218
219function __try_rs_complete_directories
220    for tries_path in (__try_rs_get_tries_path)
221        if test -d $tries_path
222            command ls -1 $tries_path 2>/dev/null | while read -l dir
223                if test -d "$tries_path/$dir"
224                    echo $dir
225                end
226            end
227        end
228    end
229end
230
231complete -f -c try-rs -n '__fish_use_subcommand' -a '(__try_rs_complete_directories)' -d 'Try directory'
232"#.to_string()
233        }
234        Shell::Zsh => {
235            r#"# try-rs tab completion for directory names
236_try_rs_get_tries_path() {
237    # Check TRY_PATH environment variable first
238    if [[ -n "${TRY_PATH}" ]]; then
239        if [[ "${TRY_PATH}" == *","* ]]; then
240            echo "${TRY_PATH}" | tr ',' '\n'
241        else
242            echo "${TRY_PATH}"
243        fi
244        return
245    fi
246    
247    # Try to read from config file
248    local config_paths=("$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml")
249    for config_path in "${config_paths[@]}"; do
250        if [[ -f "$config_path" ]]; then
251            # Try tries_path (supports single or multiple paths with comma)
252            local tries_path=$(grep -E '^[[:space:]]*tries_path[[:space:]]*=' "$config_path" 2>/dev/null | sed 's/.*=[[:space:]]*"\?\([^"]*\)"\?.*/\1/' | sed "s|~|$HOME|" | tr -d '[:space:]')
253            if [[ -n "$tries_path" ]]; then
254                if [[ "$tries_path" == *","* ]]; then
255                    echo "$tries_path" | tr ',' '\n'
256                else
257                    echo "$tries_path"
258                fi
259                return
260            fi
261        fi
262    done
263    
264    # Default path
265    echo "$HOME/work/tries"
266}
267
268_try_rs_complete() {
269    local cur="${COMP_WORDS[COMP_CWORD]}"
270    local tries_paths=$(_try_rs_get_tries_path)
271    local -a dirs=()
272    
273    # Split by comma if multiple paths
274    IFS=',' read -ra PATH_ARRAY <<< "$tries_paths"
275    
276    for tries_path in "${PATH_ARRAY[@]}"; do
277        # Trim whitespace
278        tries_path=$(echo "$tries_path" | xargs)
279        
280        if [[ -d "$tries_path" ]]; then
281            # Get list of directories
282            while IFS= read -r dir; do
283                dirs+=("$dir")
284            done < <(ls -1 "$tries_path" 2>/dev/null | while read -r dir; do
285                if [[ -d "$tries_path/$dir" ]]; then
286                    echo "$dir"
287                fi
288            done)
289        fi
290    done
291    
292    COMPREPLY=($(compgen -W "${dirs[*]}" -- "$cur"))
293}
294
295complete -o default -F _try_rs_complete try-rs
296"#.to_string()
297        }
298        Shell::Bash => {
299            r#"# try-rs tab completion for directory names
300_try_rs_get_tries_path() {
301    # Check TRY_PATH environment variable first
302    if [[ -n "${TRY_PATH}" ]]; then
303        if [[ "${TRY_PATH}" == *","* ]]; then
304            echo "${TRY_PATH}" | tr ',' '\n'
305        else
306            echo "${TRY_PATH}"
307        fi
308        return
309    fi
310    
311    # Try to read from config file
312    local config_paths=("$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml")
313    for config_path in "${config_paths[@]}"; do
314        if [[ -f "$config_path" ]]; then
315            # Try tries_path (supports single or multiple paths with comma)
316            local tries_path=$(grep -E '^[[:space:]]*tries_path[[:space:]]*=' "$config_path" 2>/dev/null | sed 's/.*=[[:space:]]*"\?\([^"]*\)"\?.*/\1/' | sed "s|~|$HOME|" | tr -d '[:space:]')
317            if [[ -n "$tries_path" ]]; then
318                if [[ "$tries_path" == *","* ]]; then
319                    echo "$tries_path" | tr ',' '\n'
320                else
321                    echo "$tries_path"
322                fi
323                return
324            fi
325        fi
326    done
327    
328    # Default path
329    echo "$HOME/work/tries"
330}
331
332_try_rs_complete() {
333    local cur="${COMP_WORDS[COMP_CWORD]}"
334    local tries_paths=$(_try_rs_get_tries_path)
335    local dirs=""
336    
337    # Split by comma if multiple paths
338    IFS=',' read -ra PATH_ARRAY <<< "$tries_paths"
339    
340    for tries_path in "${PATH_ARRAY[@]}"; do
341        # Trim whitespace
342        tries_path=$(echo "$tries_path" | xargs)
343        
344        if [[ -d "$tries_path" ]]; then
345            # Get list of directories
346            while IFS= read -r dir; do
347                if [[ -d "$tries_path/$dir" ]]; then
348                    dirs="$dirs $dir"
349                fi
350            done < <(ls -1 "$tries_path" 2>/dev/null)
351        fi
352    done
353    
354    COMPREPLY=($(compgen -W "$dirs" -- "$cur"))
355}
356
357complete -o default -F _try_rs_complete try-rs
358"#.to_string()
359        }
360        Shell::PowerShell => {
361            r#"# try-rs tab completion for directory names
362Register-ArgumentCompleter -CommandName try-rs -ScriptBlock {
363    param($wordToComplete, $commandAst, $cursorPosition)
364    
365    # Get tries path from environment variable or default
366    $triesPaths = $env:TRY_PATH
367    if (-not $triesPaths) {
368        # Try to read from config file
369        $configPaths = @(
370            "$env:USERPROFILE/.config/try-rs/config.toml",
371            "$env:USERPROFILE/.try-rs/config.toml"
372        )
373        foreach ($configPath in $configPaths) {
374            if (Test-Path $configPath) {
375                $content = Get-Content $configPath -Raw
376                # Try tries_path (supports single or multiple paths with comma)
377                if ($content -match 'tries_path\s*=\s*["'']?([^"'']+)["'']?') {
378                    $triesPaths = $matches[1].Replace('~', $env:USERPROFILE).Trim()
379                    break
380                }
381            }
382        }
383    }
384    
385    # Default path
386    if (-not $triesPaths) {
387        $triesPaths = "$env:USERPROFILE/work/tries"
388    }
389    
390    # Split by comma if multiple paths
391    $pathArray = $triesPaths -split ','
392    
393    # Get directories from all paths
394    foreach ($triesPath in $pathArray) {
395        $triesPath = $triesPath.Trim()
396        if (Test-Path $triesPath) {
397            Get-ChildItem -Path $triesPath -Directory | 
398                Where-Object { $_.Name -like "$wordToComplete*" } |
399                ForEach-Object { 
400                    [System.Management.Automation.CompletionResult]::new(
401                        $_.Name, 
402                        $_.Name, 
403                        'ParameterValue', 
404                        $_.Name
405                    )
406                }
407        }
408    }
409}
410"#.to_string()
411        }
412        Shell::NuShell => {
413            r#"# try-rs tab completion for directory names
414# Add this to your Nushell config or env file
415
416export def __try_rs_get_tries_paths [] {
417    # Check TRY_PATH environment variable first
418    if ($env.TRY_PATH? | is-not-empty) {
419        return ($env.TRY_PATH | split row "," | each { |s| $s | str trim })
420    }
421    
422    # Try to read from config file
423    let config_paths = [
424        ($env.HOME | path join ".config" "try-rs" "config.toml"),
425        ($env.HOME | path join ".try-rs" "config.toml")
426    ]
427    
428    for config_path in $config_paths {
429        if ($config_path | path exists) {
430            let content = (open $config_path | str trim)
431            # Try tries_path (supports single or multiple paths with comma)
432            if ($content =~ 'tries_path\\s*=\\s*"?([^"]+)"?') {
433                let path = ($content | parse -r 'tries_path\\s*=\\s*"?([^"]+)"?' | get capture0.0? | default "")
434                if ($path | is-not-empty) {
435                    # Check if contains comma (multiple paths)
436                    if ($path | str contains ",") {
437                        return ($path | split row "," | each { |s| ($s | str trim | str replace "~" $env.HOME) })
438                    else
439                        return ([($path | str replace "~" $env.HOME)])
440                    }
441                }
442            }
443        }
444    }
445    
446    # Default path
447    [($env.HOME | path join "work" "tries")]
448}
449
450export def __try_rs_complete [context: string] {
451    let tries_paths = (__try_rs_get_tries_paths)
452    
453    mut all_dirs = []
454    for tries_path in $tries_paths {
455        if ($tries_path | path exists) {
456            let dirs = (ls $tries_path | where type == "dir" | get name | path basename)
457            $all_dirs = ($all_dirs | append $dirs)
458        }
459    }
460    $all_dirs
461}
462"#.to_string()
463        }
464    }
465}
466
467/// Returns only the completion script (for --completions flag)
468pub fn get_completion_script_only(shell: &Shell) -> String {
469    let completions = get_completions_script(shell);
470    match shell {
471        Shell::NuShell => {
472            // For NuShell, we need to provide a different format when used standalone
473            r#"# try-rs tab completion for directory names
474# Add this to your Nushell config
475
476def __try_rs_get_tries_path [] {
477    if ($env.TRY_PATH? | is-not-empty) {
478        return $env.TRY_PATH
479    }
480    
481    let config_paths = [
482        ($env.HOME | path join ".config" "try-rs" "config.toml"),
483        ($env.HOME | path join ".try-rs" "config.toml")
484    ]
485    
486    for config_path in $config_paths {
487        if ($config_path | path exists) {
488            let content = (open $config_path | str trim)
489            if ($content =~ 'tries_path\\s*=\\s*"?([^"]+)"?') {
490                let path = ($content | parse -r 'tries_path\\s*=\\s*"?([^"]+)"?' | get capture0.0? | default "")
491                if ($path | is-not-empty) {
492                    return ($path | str replace "~" $env.HOME)
493                }
494            }
495        }
496    }
497    
498    ($env.HOME | path join "work" "tries")
499}
500
501def __try_rs_complete [context: string] {
502    let tries_path = (__try_rs_get_tries_path)
503    
504    if ($tries_path | path exists) {
505        ls $tries_path | where type == "dir" | get name | path basename
506    } else {
507        []
508    }
509}
510
511# Register completion
512export extern try-rs [
513    name_or_url?: string@__try_rs_complete
514    destination?: string
515    --setup: string
516    --setup-stdout: string
517    --completions: string
518    --shallow-clone(-s)
519    --worktree(-w): string
520]
521"#.to_string()
522        }
523        _ => completions,
524    }
525}
526
527pub fn get_shell_integration_path(shell: &Shell) -> PathBuf {
528    let config_dir = match shell {
529        Shell::Fish => get_base_config_dir(),
530        _ => get_config_dir(),
531    };
532
533    match shell {
534        Shell::Fish => get_fish_functions_dir().join("try-rs.fish"),
535        Shell::Zsh => config_dir.join("try-rs.zsh"),
536        Shell::Bash => config_dir.join("try-rs.bash"),
537        Shell::PowerShell => config_dir.join("try-rs.ps1"),
538        Shell::NuShell => config_dir.join("try-rs.nu"),
539    }
540}
541
542fn get_fish_functions_dir() -> PathBuf {
543    if let Ok(output) = std::process::Command::new("fish")
544        .args(["-c", "echo $__fish_config_dir"])
545        .output()
546    {
547        if output.status.success() {
548            let output_str = String::from_utf8_lossy(&output.stdout);
549            let path = PathBuf::from(output_str.trim()).join("functions");
550            if path.exists() || path.parent().map(|p| p.exists()).unwrap_or(false) {
551                return path;
552            }
553        }
554    }
555    get_base_config_dir().join("fish").join("functions")
556}
557
558fn write_fish_picker_function() -> Result<PathBuf> {
559    let file_path = get_fish_functions_dir().join("try-rs-picker.fish");
560    if let Some(parent) = file_path.parent()
561        && !parent.exists()
562    {
563        fs::create_dir_all(parent)?;
564    }
565    fs::write(&file_path, FISH_PICKER_FUNCTION)?;
566    eprintln!(
567        "Fish picker function file created at: {}",
568        file_path.display()
569    );
570    Ok(file_path)
571}
572
573pub fn is_shell_integration_configured(shell: &Shell) -> bool {
574    get_shell_integration_path(shell).exists()
575}
576
577/// Appends a source command to an RC file if not already present.
578fn append_source_to_rc(rc_path: &std::path::Path, source_cmd: &str) -> Result<()> {
579    if rc_path.exists() {
580        let content = fs::read_to_string(rc_path)?;
581        // Check for either the exact source command or our marker comment
582        if !content.contains(source_cmd) && !content.contains("# try-rs integration") {
583            let mut file = fs::OpenOptions::new().append(true).open(rc_path)?;
584            writeln!(file, "\n# try-rs integration")?;
585            writeln!(file, "{}", source_cmd)?;
586            eprintln!("Added configuration to {}", rc_path.display());
587        } else {
588            eprintln!("Configuration already present in {}", rc_path.display());
589        }
590    } else {
591        eprintln!(
592            "You need to add the following line to {}:",
593            rc_path.display()
594        );
595        eprintln!("{}", source_cmd);
596    }
597    Ok(())
598}
599
600/// Writes the shell integration file and returns its path.
601fn write_shell_integration(shell: &Shell) -> Result<std::path::PathBuf> {
602    let file_path = get_shell_integration_path(shell);
603    if let Some(parent) = file_path.parent()
604        && !parent.exists()
605    {
606        fs::create_dir_all(parent)?;
607    }
608    fs::write(&file_path, get_shell_content(shell))?;
609    eprintln!(
610        "{:?} function file created at: {}",
611        shell,
612        file_path.display()
613    );
614    Ok(file_path)
615}
616
617/// Sets up shell integration for the given shell.
618pub fn setup_shell(shell: &Shell) -> Result<()> {
619    let file_path = write_shell_integration(shell)?;
620    let home_dir = dirs::home_dir().expect("Could not find home directory");
621
622    match shell {
623        Shell::Fish => {
624            let _picker_path = write_fish_picker_function()?;
625            let fish_config_path = home_dir.join(".config").join("fish").join("config.fish");
626            eprintln!(
627                "You may need to restart your shell or run 'source {}' to apply changes.",
628                file_path.display()
629            );
630            eprintln!(
631                "Optional: append the following to {} to bind Ctrl+T:",
632                fish_config_path.display()
633            );
634            eprintln!("bind \\ct try-rs-picker");
635            eprintln!("bind -M insert \\ct try-rs-picker");
636        }
637        Shell::Zsh => {
638            let source_cmd = format!("source '{}'", file_path.display());
639            append_source_to_rc(&home_dir.join(".zshrc"), &source_cmd)?;
640        }
641        Shell::Bash => {
642            let source_cmd = format!("source '{}'", file_path.display());
643            append_source_to_rc(&home_dir.join(".bashrc"), &source_cmd)?;
644        }
645        Shell::PowerShell => {
646            let profile_path_ps7 = home_dir
647                .join("Documents")
648                .join("PowerShell")
649                .join("Microsoft.PowerShell_profile.ps1");
650            let profile_path_ps5 = home_dir
651                .join("Documents")
652                .join("WindowsPowerShell")
653                .join("Microsoft.PowerShell_profile.ps1");
654            let profile_path = if profile_path_ps7.exists() {
655                profile_path_ps7
656            } else if profile_path_ps5.exists() {
657                profile_path_ps5
658            } else {
659                profile_path_ps7
660            };
661
662            if let Some(parent) = profile_path.parent()
663                && !parent.exists()
664            {
665                fs::create_dir_all(parent)?;
666            }
667
668            let source_cmd = format!(". '{}'", file_path.display());
669            if profile_path.exists() {
670                append_source_to_rc(&profile_path, &source_cmd)?;
671            } else {
672                let mut file = fs::File::create(&profile_path)?;
673                writeln!(file, "# try-rs integration")?;
674                writeln!(file, "{}", source_cmd)?;
675                eprintln!(
676                    "PowerShell profile created and configured at: {}",
677                    profile_path.display()
678                );
679            }
680
681            eprintln!(
682                "You may need to restart your shell or run '. {}' to apply changes.",
683                profile_path.display()
684            );
685            eprintln!(
686                "If you get an error about running scripts, you may need to run: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned"
687            );
688        }
689        Shell::NuShell => {
690            let nu_config_path = dirs::config_dir()
691                .expect("Could not find config directory")
692                .join("nushell")
693                .join("config.nu");
694            let source_cmd = format!("source '{}'", file_path.display());
695            if nu_config_path.exists() {
696                append_source_to_rc(&nu_config_path, &source_cmd)?;
697            } else {
698                eprintln!("Could not find config.nu at {}", nu_config_path.display());
699                eprintln!("Please add the following line manually:");
700                eprintln!("{}", source_cmd);
701            }
702        }
703    }
704
705    Ok(())
706}
707
708/// Generates a standalone completion script for the given shell.
709pub fn generate_completions(shell: &Shell) -> Result<()> {
710    let script = get_completion_script_only(shell);
711    print!("{}", script);
712    Ok(())
713}
714
715pub fn get_installed_shells() -> Vec<Shell> {
716    let mut shells = Vec::new();
717    for shell in [
718        Shell::Fish,
719        Shell::Zsh,
720        Shell::Bash,
721        Shell::PowerShell,
722        Shell::NuShell,
723    ] {
724        if is_shell_installed(&shell) {
725            shells.push(shell);
726        }
727    }
728    shells
729}
730
731fn is_shell_installed(shell: &Shell) -> bool {
732    let shell_name = match shell {
733        Shell::Fish => "fish",
734        Shell::Zsh => "zsh",
735        Shell::Bash => "bash",
736        Shell::PowerShell => "pwsh",
737        Shell::NuShell => "nu",
738    };
739
740    let output = std::process::Command::new("whereis")
741        .arg(shell_name)
742        .output();
743
744    match output {
745        Ok(out) => {
746            let result = String::from_utf8_lossy(&out.stdout);
747            let trimmed = result.trim();
748            !trimmed.is_empty()
749                && !trimmed.ends_with(':')
750                && trimmed.starts_with(&format!("{}: ", shell_name))
751        }
752        Err(_) => false,
753    }
754}
755
756pub fn clear_shell_setup() -> Result<()> {
757    let installed_shells = get_installed_shells();
758
759    if installed_shells.is_empty() {
760        eprintln!("No supported shells found on this system.");
761        return Ok(());
762    }
763
764    eprintln!("Detected shells: {:?}\n", installed_shells);
765    
766    eprintln!("Files to be removed:");
767
768    for shell in &installed_shells {
769        let paths = get_shell_config_paths(shell);
770
771        for path in &paths {
772            eprintln!("  - {}", path.display());
773        }
774
775        match shell {
776            Shell::Fish => {
777                let fish_functions = get_fish_functions_dir();
778                eprintln!(
779                    "  - {}",
780                    fish_functions.join("try-rs-picker.fish").display()
781                );
782            }
783            _ => {}
784        }
785    }
786
787    eprintln!("\nRemoving files...");
788
789    for shell in &installed_shells {
790        clear_shell_config(shell)?;
791    }
792
793    eprintln!("\nDone! Shell integration removed.");
794    Ok(())
795}
796
797fn clear_shell_config(shell: &Shell) -> Result<()> {
798    let integration_file = get_shell_integration_path(shell);
799    if integration_file.exists() {
800        fs::remove_file(&integration_file)?;
801        eprintln!("Removed integration file: {}", integration_file.display());
802    }
803
804    if let Shell::Fish = shell {
805        let picker_path = get_fish_functions_dir().join("try-rs-picker.fish");
806        if picker_path.exists() {
807            fs::remove_file(&picker_path)?;
808            eprintln!("Removed picker file: {}", picker_path.display());
809        }
810    }
811
812    // Clean up RC files instead of deleting them
813    let home_dir = dirs::home_dir().expect("Could not find home directory");
814    let rc_files = match shell {
815        Shell::Zsh => vec![home_dir.join(".zshrc")],
816        Shell::Bash => vec![home_dir.join(".bashrc")],
817        Shell::NuShell => vec![dirs::config_dir()
818            .expect("Could not find config directory")
819            .join("nushell")
820            .join("config.nu")],
821        Shell::PowerShell => {
822             let profile_path_ps7 = home_dir
823                .join("Documents")
824                .join("PowerShell")
825                .join("Microsoft.PowerShell_profile.ps1");
826            let profile_path_ps5 = home_dir
827                .join("Documents")
828                .join("WindowsPowerShell")
829                .join("Microsoft.PowerShell_profile.ps1");
830            vec![profile_path_ps7, profile_path_ps5]
831        },
832        _ => vec![],
833    };
834
835    for rc_path in rc_files {
836        if rc_path.exists() {
837            remove_source_from_rc(&rc_path)?;
838        }
839    }
840
841    Ok(())
842}
843
844fn remove_source_from_rc(rc_path: &std::path::Path) -> Result<()> {
845    let content = fs::read_to_string(rc_path)?;
846    if content.contains("try-rs") {
847        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
848        let initial_count = lines.len();
849        
850        // Remove lines containing try-rs integration marker or typical source commands
851        lines.retain(|line| {
852            !line.contains("# try-rs integration") && 
853            !(line.contains("source") && line.contains("try-rs")) &&
854            !(line.contains(".") && line.contains("try-rs") && rc_path.extension().map_or(false, |ext| ext == "ps1"))
855        });
856
857        if lines.len() < initial_count {
858            let mut new_content = lines.join("\n");
859            if !new_content.is_empty() && !new_content.ends_with('\n') {
860                new_content.push('\n');
861            }
862            fs::write(rc_path, new_content)?;
863            eprintln!("Cleaned up integration lines from {}", rc_path.display());
864        }
865    }
866    Ok(())
867}
868
869fn get_shell_config_paths(shell: &Shell) -> Vec<PathBuf> {
870    let mut paths = Vec::new();
871    let config_dir = get_base_config_dir();
872
873    match shell {
874        Shell::Fish => {
875            let fish_functions = get_fish_functions_dir();
876            paths.push(fish_functions.join("try-rs.fish"));
877        }
878        Shell::Zsh => {
879            paths.push(config_dir.join("try-rs.zsh"));
880        }
881        Shell::Bash => {
882            paths.push(config_dir.join("try-rs.bash"));
883        }
884        Shell::PowerShell => {
885            paths.push(config_dir.join("try-rs.ps1"));
886        }
887        Shell::NuShell => {
888            paths.push(config_dir.join("try-rs.nu"));
889        }
890    }
891
892    paths
893}
894