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        echo $TRY_PATH
185        return
186    end
187    
188    # Try to read from config file
189    set -l config_paths "$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml"
190    for config_path in $config_paths
191        if test -f $config_path
192            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)
193            if test -n "$tries_path"
194                echo $tries_path
195                return
196            end
197        end
198    end
199    
200    # Default path
201    echo "$HOME/work/tries"
202end
203
204function __try_rs_complete_directories
205    set -l tries_path (__try_rs_get_tries_path)
206    
207    if test -d $tries_path
208        # List directories in tries_path, filtering by current token
209        command ls -1 $tries_path 2>/dev/null | while read -l dir
210            if test -d "$tries_path/$dir"
211                echo $dir
212            end
213        end
214    end
215end
216
217complete -f -c try-rs -n '__fish_use_subcommand' -a '(__try_rs_complete_directories)' -d 'Try directory'
218"#.to_string()
219        }
220        Shell::Zsh => {
221            r#"# try-rs tab completion for directory names
222_try_rs_get_tries_path() {
223    # Check TRY_PATH environment variable first
224    if [[ -n "${TRY_PATH}" ]]; then
225        echo "${TRY_PATH}"
226        return
227    fi
228    
229    # Try to read from config file
230    local config_paths=("$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml")
231    for config_path in "${config_paths[@]}"; do
232        if [[ -f "$config_path" ]]; then
233            local tries_path=$(grep -E '^\s*tries_path\s*=' "$config_path" 2>/dev/null | sed 's/.*=\s*"\?\([^"]*\)"\?.*/\1/' | sed "s|~|$HOME|" | tr -d '[:space:]')
234            if [[ -n "$tries_path" ]]; then
235                echo "$tries_path"
236                return
237            fi
238        fi
239    done
240    
241    # Default path
242    echo "$HOME/work/tries"
243}
244
245_try_rs_complete() {
246    local cur="${COMP_WORDS[COMP_CWORD]}"
247    local tries_path=$(_try_rs_get_tries_path)
248    local -a dirs=()
249    
250    if [[ -d "$tries_path" ]]; then
251        # Get list of directories
252        while IFS= read -r dir; do
253            dirs+=("$dir")
254        done < <(ls -1 "$tries_path" 2>/dev/null | while read -r dir; do
255            if [[ -d "$tries_path/$dir" ]]; then
256                echo "$dir"
257            fi
258        done)
259    fi
260    
261    COMPREPLY=($(compgen -W "${dirs[*]}" -- "$cur"))
262}
263
264complete -o default -F _try_rs_complete try-rs
265"#.to_string()
266        }
267        Shell::Bash => {
268            r#"# try-rs tab completion for directory names
269_try_rs_get_tries_path() {
270    # Check TRY_PATH environment variable first
271    if [[ -n "${TRY_PATH}" ]]; then
272        echo "${TRY_PATH}"
273        return
274    fi
275    
276    # Try to read from config file
277    local config_paths=("$HOME/.config/try-rs/config.toml" "$HOME/.try-rs/config.toml")
278    for config_path in "${config_paths[@]}"; do
279        if [[ -f "$config_path" ]]; then
280            local tries_path=$(grep -E '^[[:space:]]*tries_path[[:space:]]*=' "$config_path" 2>/dev/null | sed 's/.*=[[:space:]]*"\?\([^"]*\)"\?.*/\1/' | sed "s|~|$HOME|" | tr -d '[:space:]')
281            if [[ -n "$tries_path" ]]; then
282                echo "$tries_path"
283                return
284            fi
285        fi
286    done
287    
288    # Default path
289    echo "$HOME/work/tries"
290}
291
292_try_rs_complete() {
293    local cur="${COMP_WORDS[COMP_CWORD]}"
294    local tries_path=$(_try_rs_get_tries_path)
295    local dirs=""
296    
297    if [[ -d "$tries_path" ]]; then
298        # Get list of directories
299        while IFS= read -r dir; do
300            if [[ -d "$tries_path/$dir" ]]; then
301                dirs="$dirs $dir"
302            fi
303        done < <(ls -1 "$tries_path" 2>/dev/null)
304    fi
305    
306    COMPREPLY=($(compgen -W "$dirs" -- "$cur"))
307}
308
309complete -o default -F _try_rs_complete try-rs
310"#.to_string()
311        }
312        Shell::PowerShell => {
313            r#"# try-rs tab completion for directory names
314Register-ArgumentCompleter -CommandName try-rs -ScriptBlock {
315    param($wordToComplete, $commandAst, $cursorPosition)
316    
317    # Get tries path from environment variable or default
318    $triesPath = $env:TRY_PATH
319    if (-not $triesPath) {
320        # Try to read from config file
321        $configPaths = @(
322            "$env:USERPROFILE/.config/try-rs/config.toml",
323            "$env:USERPROFILE/.try-rs/config.toml"
324        )
325        foreach ($configPath in $configPaths) {
326            if (Test-Path $configPath) {
327                $content = Get-Content $configPath -Raw
328                if ($content -match 'tries_path\s*=\s*["'']?([^"'']+)["'']?') {
329                    $triesPath = $matches[1].Replace('~', $env:USERPROFILE).Trim()
330                    break
331                }
332            }
333        }
334    }
335    
336    # Default path
337    if (-not $triesPath) {
338        $triesPath = "$env:USERPROFILE/work/tries"
339    }
340    
341    # Get directories
342    if (Test-Path $triesPath) {
343        Get-ChildItem -Path $triesPath -Directory | 
344            Where-Object { $_.Name -like "$wordToComplete*" } |
345            ForEach-Object { 
346                [System.Management.Automation.CompletionResult]::new(
347                    $_.Name, 
348                    $_.Name, 
349                    'ParameterValue', 
350                    $_.Name
351                )
352            }
353    }
354}
355"#.to_string()
356        }
357        Shell::NuShell => {
358            r#"# try-rs tab completion for directory names
359# Add this to your Nushell config or env file
360
361export def __try_rs_get_tries_path [] {
362    # Check TRY_PATH environment variable first
363    if ($env.TRY_PATH? | is-not-empty) {
364        return $env.TRY_PATH
365    }
366    
367    # Try to read from config file
368    let config_paths = [
369        ($env.HOME | path join ".config" "try-rs" "config.toml"),
370        ($env.HOME | path join ".try-rs" "config.toml")
371    ]
372    
373    for config_path in $config_paths {
374        if ($config_path | path exists) {
375            let content = (open $config_path | str trim)
376            if ($content =~ 'tries_path\\s*=\\s*"?([^"]+)"?') {
377                let path = ($content | parse -r 'tries_path\\s*=\\s*"?([^"]+)"?' | get capture0.0? | default "")
378                if ($path | is-not-empty) {
379                    return ($path | str replace "~" $env.HOME)
380                }
381            }
382        }
383    }
384    
385    # Default path
386    ($env.HOME | path join "work" "tries")
387}
388
389export def __try_rs_complete [context: string] {
390    let tries_path = (__try_rs_get_tries_path)
391    
392    if ($tries_path | path exists) {
393        ls $tries_path | where type == "dir" | get name | path basename
394    } else {
395        []
396    }
397}
398"#.to_string()
399        }
400    }
401}
402
403/// Returns only the completion script (for --completions flag)
404pub fn get_completion_script_only(shell: &Shell) -> String {
405    let completions = get_completions_script(shell);
406    match shell {
407        Shell::NuShell => {
408            // For NuShell, we need to provide a different format when used standalone
409            r#"# try-rs tab completion for directory names
410# Add this to your Nushell config
411
412def __try_rs_get_tries_path [] {
413    if ($env.TRY_PATH? | is-not-empty) {
414        return $env.TRY_PATH
415    }
416    
417    let config_paths = [
418        ($env.HOME | path join ".config" "try-rs" "config.toml"),
419        ($env.HOME | path join ".try-rs" "config.toml")
420    ]
421    
422    for config_path in $config_paths {
423        if ($config_path | path exists) {
424            let content = (open $config_path | str trim)
425            if ($content =~ 'tries_path\\s*=\\s*"?([^"]+)"?') {
426                let path = ($content | parse -r 'tries_path\\s*=\\s*"?([^"]+)"?' | get capture0.0? | default "")
427                if ($path | is-not-empty) {
428                    return ($path | str replace "~" $env.HOME)
429                }
430            }
431        }
432    }
433    
434    ($env.HOME | path join "work" "tries")
435}
436
437def __try_rs_complete [context: string] {
438    let tries_path = (__try_rs_get_tries_path)
439    
440    if ($tries_path | path exists) {
441        ls $tries_path | where type == "dir" | get name | path basename
442    } else {
443        []
444    }
445}
446
447# Register completion
448export extern try-rs [
449    name_or_url?: string@__try_rs_complete
450    destination?: string
451    --setup: string
452    --setup-stdout: string
453    --completions: string
454    --shallow-clone(-s)
455    --worktree(-w): string
456]
457"#.to_string()
458        }
459        _ => completions,
460    }
461}
462
463pub fn get_shell_integration_path(shell: &Shell) -> PathBuf {
464    let config_dir = match shell {
465        Shell::Fish => get_base_config_dir(),
466        _ => get_config_dir(),
467    };
468
469    match shell {
470        Shell::Fish => get_fish_functions_dir().join("try-rs.fish"),
471        Shell::Zsh => config_dir.join("try-rs.zsh"),
472        Shell::Bash => config_dir.join("try-rs.bash"),
473        Shell::PowerShell => config_dir.join("try-rs.ps1"),
474        Shell::NuShell => config_dir.join("try-rs.nu"),
475    }
476}
477
478fn get_fish_functions_dir() -> PathBuf {
479    if let Ok(output) = std::process::Command::new("fish")
480        .args(["-c", "echo $__fish_config_dir"])
481        .output()
482    {
483        if output.status.success() {
484            let output_str = String::from_utf8_lossy(&output.stdout);
485            let path = PathBuf::from(output_str.trim()).join("functions");
486            if path.exists() || path.parent().map(|p| p.exists()).unwrap_or(false) {
487                return path;
488            }
489        }
490    }
491    get_base_config_dir().join("fish").join("functions")
492}
493
494fn write_fish_picker_function() -> Result<PathBuf> {
495    let file_path = get_fish_functions_dir().join("try-rs-picker.fish");
496    if let Some(parent) = file_path.parent()
497        && !parent.exists()
498    {
499        fs::create_dir_all(parent)?;
500    }
501    fs::write(&file_path, FISH_PICKER_FUNCTION)?;
502    eprintln!(
503        "Fish picker function file created at: {}",
504        file_path.display()
505    );
506    Ok(file_path)
507}
508
509pub fn is_shell_integration_configured(shell: &Shell) -> bool {
510    get_shell_integration_path(shell).exists()
511}
512
513/// Appends a source command to an RC file if not already present.
514fn append_source_to_rc(rc_path: &std::path::Path, source_cmd: &str) -> Result<()> {
515    if rc_path.exists() {
516        let content = fs::read_to_string(rc_path)?;
517        // Check for either the exact source command or our marker comment
518        if !content.contains(source_cmd) && !content.contains("# try-rs integration") {
519            let mut file = fs::OpenOptions::new().append(true).open(rc_path)?;
520            writeln!(file, "\n# try-rs integration")?;
521            writeln!(file, "{}", source_cmd)?;
522            eprintln!("Added configuration to {}", rc_path.display());
523        } else {
524            eprintln!("Configuration already present in {}", rc_path.display());
525        }
526    } else {
527        eprintln!(
528            "You need to add the following line to {}:",
529            rc_path.display()
530        );
531        eprintln!("{}", source_cmd);
532    }
533    Ok(())
534}
535
536/// Writes the shell integration file and returns its path.
537fn write_shell_integration(shell: &Shell) -> Result<std::path::PathBuf> {
538    let file_path = get_shell_integration_path(shell);
539    if let Some(parent) = file_path.parent()
540        && !parent.exists()
541    {
542        fs::create_dir_all(parent)?;
543    }
544    fs::write(&file_path, get_shell_content(shell))?;
545    eprintln!(
546        "{:?} function file created at: {}",
547        shell,
548        file_path.display()
549    );
550    Ok(file_path)
551}
552
553/// Sets up shell integration for the given shell.
554pub fn setup_shell(shell: &Shell) -> Result<()> {
555    let file_path = write_shell_integration(shell)?;
556    let home_dir = dirs::home_dir().expect("Could not find home directory");
557
558    match shell {
559        Shell::Fish => {
560            let _picker_path = write_fish_picker_function()?;
561            let fish_config_path = home_dir.join(".config").join("fish").join("config.fish");
562            eprintln!(
563                "You may need to restart your shell or run 'source {}' to apply changes.",
564                file_path.display()
565            );
566            eprintln!(
567                "Optional: append the following to {} to bind Ctrl+T:",
568                fish_config_path.display()
569            );
570            eprintln!("bind \\ct try-rs-picker");
571            eprintln!("bind -M insert \\ct try-rs-picker");
572        }
573        Shell::Zsh => {
574            let source_cmd = format!("source '{}'", file_path.display());
575            append_source_to_rc(&home_dir.join(".zshrc"), &source_cmd)?;
576        }
577        Shell::Bash => {
578            let source_cmd = format!("source '{}'", file_path.display());
579            append_source_to_rc(&home_dir.join(".bashrc"), &source_cmd)?;
580        }
581        Shell::PowerShell => {
582            let profile_path_ps7 = home_dir
583                .join("Documents")
584                .join("PowerShell")
585                .join("Microsoft.PowerShell_profile.ps1");
586            let profile_path_ps5 = home_dir
587                .join("Documents")
588                .join("WindowsPowerShell")
589                .join("Microsoft.PowerShell_profile.ps1");
590            let profile_path = if profile_path_ps7.exists() {
591                profile_path_ps7
592            } else if profile_path_ps5.exists() {
593                profile_path_ps5
594            } else {
595                profile_path_ps7
596            };
597
598            if let Some(parent) = profile_path.parent()
599                && !parent.exists()
600            {
601                fs::create_dir_all(parent)?;
602            }
603
604            let source_cmd = format!(". '{}'", file_path.display());
605            if profile_path.exists() {
606                append_source_to_rc(&profile_path, &source_cmd)?;
607            } else {
608                let mut file = fs::File::create(&profile_path)?;
609                writeln!(file, "# try-rs integration")?;
610                writeln!(file, "{}", source_cmd)?;
611                eprintln!(
612                    "PowerShell profile created and configured at: {}",
613                    profile_path.display()
614                );
615            }
616
617            eprintln!(
618                "You may need to restart your shell or run '. {}' to apply changes.",
619                profile_path.display()
620            );
621            eprintln!(
622                "If you get an error about running scripts, you may need to run: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned"
623            );
624        }
625        Shell::NuShell => {
626            let nu_config_path = dirs::config_dir()
627                .expect("Could not find config directory")
628                .join("nushell")
629                .join("config.nu");
630            let source_cmd = format!("source '{}'", file_path.display());
631            if nu_config_path.exists() {
632                append_source_to_rc(&nu_config_path, &source_cmd)?;
633            } else {
634                eprintln!("Could not find config.nu at {}", nu_config_path.display());
635                eprintln!("Please add the following line manually:");
636                eprintln!("{}", source_cmd);
637            }
638        }
639    }
640
641    Ok(())
642}
643
644/// Generates a standalone completion script for the given shell.
645pub fn generate_completions(shell: &Shell) -> Result<()> {
646    let script = get_completion_script_only(shell);
647    print!("{}", script);
648    Ok(())
649}
650
651pub fn get_installed_shells() -> Vec<Shell> {
652    let mut shells = Vec::new();
653    for shell in [
654        Shell::Fish,
655        Shell::Zsh,
656        Shell::Bash,
657        Shell::PowerShell,
658        Shell::NuShell,
659    ] {
660        if is_shell_installed(&shell) {
661            shells.push(shell);
662        }
663    }
664    shells
665}
666
667fn is_shell_installed(shell: &Shell) -> bool {
668    let shell_name = match shell {
669        Shell::Fish => "fish",
670        Shell::Zsh => "zsh",
671        Shell::Bash => "bash",
672        Shell::PowerShell => "pwsh",
673        Shell::NuShell => "nu",
674    };
675
676    let output = std::process::Command::new("whereis")
677        .arg(shell_name)
678        .output();
679
680    match output {
681        Ok(out) => {
682            let result = String::from_utf8_lossy(&out.stdout);
683            let trimmed = result.trim();
684            !trimmed.is_empty()
685                && !trimmed.ends_with(':')
686                && trimmed.starts_with(&format!("{}: ", shell_name))
687        }
688        Err(_) => false,
689    }
690}
691
692pub fn clear_shell_setup() -> Result<()> {
693    let installed_shells = get_installed_shells();
694
695    if installed_shells.is_empty() {
696        eprintln!("No supported shells found on this system.");
697        return Ok(());
698    }
699
700    eprintln!("Detected shells: {:?}\n", installed_shells);
701    
702    eprintln!("Files to be removed:");
703
704    for shell in &installed_shells {
705        let paths = get_shell_config_paths(shell);
706
707        for path in &paths {
708            eprintln!("  - {}", path.display());
709        }
710
711        match shell {
712            Shell::Fish => {
713                let fish_functions = get_fish_functions_dir();
714                eprintln!(
715                    "  - {}",
716                    fish_functions.join("try-rs-picker.fish").display()
717                );
718            }
719            _ => {}
720        }
721    }
722
723    eprintln!("\nRemoving files...");
724
725    for shell in &installed_shells {
726        clear_shell_config(shell)?;
727    }
728
729    eprintln!("\nDone! Shell integration removed.");
730    Ok(())
731}
732
733fn clear_shell_config(shell: &Shell) -> Result<()> {
734    let integration_file = get_shell_integration_path(shell);
735    if integration_file.exists() {
736        fs::remove_file(&integration_file)?;
737        eprintln!("Removed integration file: {}", integration_file.display());
738    }
739
740    if let Shell::Fish = shell {
741        let picker_path = get_fish_functions_dir().join("try-rs-picker.fish");
742        if picker_path.exists() {
743            fs::remove_file(&picker_path)?;
744            eprintln!("Removed picker file: {}", picker_path.display());
745        }
746    }
747
748    // Clean up RC files instead of deleting them
749    let home_dir = dirs::home_dir().expect("Could not find home directory");
750    let rc_files = match shell {
751        Shell::Zsh => vec![home_dir.join(".zshrc")],
752        Shell::Bash => vec![home_dir.join(".bashrc")],
753        Shell::NuShell => vec![dirs::config_dir()
754            .expect("Could not find config directory")
755            .join("nushell")
756            .join("config.nu")],
757        Shell::PowerShell => {
758             let profile_path_ps7 = home_dir
759                .join("Documents")
760                .join("PowerShell")
761                .join("Microsoft.PowerShell_profile.ps1");
762            let profile_path_ps5 = home_dir
763                .join("Documents")
764                .join("WindowsPowerShell")
765                .join("Microsoft.PowerShell_profile.ps1");
766            vec![profile_path_ps7, profile_path_ps5]
767        },
768        _ => vec![],
769    };
770
771    for rc_path in rc_files {
772        if rc_path.exists() {
773            remove_source_from_rc(&rc_path)?;
774        }
775    }
776
777    Ok(())
778}
779
780fn remove_source_from_rc(rc_path: &std::path::Path) -> Result<()> {
781    let content = fs::read_to_string(rc_path)?;
782    if content.contains("try-rs") {
783        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
784        let initial_count = lines.len();
785        
786        // Remove lines containing try-rs integration marker or typical source commands
787        lines.retain(|line| {
788            !line.contains("# try-rs integration") && 
789            !(line.contains("source") && line.contains("try-rs")) &&
790            !(line.contains(".") && line.contains("try-rs") && rc_path.extension().map_or(false, |ext| ext == "ps1"))
791        });
792
793        if lines.len() < initial_count {
794            let mut new_content = lines.join("\n");
795            if !new_content.is_empty() && !new_content.ends_with('\n') {
796                new_content.push('\n');
797            }
798            fs::write(rc_path, new_content)?;
799            eprintln!("Cleaned up integration lines from {}", rc_path.display());
800        }
801    }
802    Ok(())
803}
804
805fn get_shell_config_paths(shell: &Shell) -> Vec<PathBuf> {
806    let mut paths = Vec::new();
807    let config_dir = get_base_config_dir();
808
809    match shell {
810        Shell::Fish => {
811            let fish_functions = get_fish_functions_dir();
812            paths.push(fish_functions.join("try-rs.fish"));
813        }
814        Shell::Zsh => {
815            paths.push(config_dir.join("try-rs.zsh"));
816        }
817        Shell::Bash => {
818            paths.push(config_dir.join("try-rs.bash"));
819        }
820        Shell::PowerShell => {
821            paths.push(config_dir.join("try-rs.ps1"));
822        }
823        Shell::NuShell => {
824            paths.push(config_dir.join("try-rs.nu"));
825        }
826    }
827
828    paths
829}
830