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) -> &'static str {
11    match shell {
12        Shell::Fish => {
13            r#"function try-rs
14    # Pass flags/options directly to stdout without capturing
15    for arg in $argv
16        if string match -q -- '-*' $arg
17            command try-rs $argv
18            return
19        end
20    end
21
22    # Captures the output of the binary (stdout) which is the "cd" command
23    # The TUI is rendered on stderr, so it doesn't interfere.
24    set command (command try-rs $argv | string collect)
25
26    if test -n "$command"
27        eval $command
28    end
29end
30"#
31        }
32        Shell::Zsh => {
33            r#"try-rs() {
34    # Pass flags/options directly to stdout without capturing
35    for arg in "$@"; do
36        case "$arg" in
37            -*) command try-rs "$@"; return ;;
38        esac
39    done
40
41    # Captures the output of the binary (stdout) which is the "cd" command
42    # The TUI is rendered on stderr, so it doesn't interfere.
43    local output
44    output=$(command try-rs "$@")
45
46    if [ -n "$output" ]; then
47        eval "$output"
48    fi
49}
50"#
51        }
52        Shell::Bash => {
53            r#"try-rs() {
54    # Pass flags/options directly to stdout without capturing
55    for arg in "$@"; do
56        case "$arg" in
57            -*) command try-rs "$@"; return ;;
58        esac
59    done
60
61    # Captures the output of the binary (stdout) which is the "cd" command
62    # The TUI is rendered on stderr, so it doesn't interfere.
63    local output
64    output=$(command try-rs "$@")
65
66    if [ -n "$output" ]; then
67        eval "$output"
68    fi
69}
70"#
71        }
72        Shell::PowerShell => {
73            r#"# try-rs integration for PowerShell
74function try-rs {
75    # Pass flags/options directly to stdout without capturing
76    foreach ($a in $args) {
77        if ($a -like '-*') {
78            & try-rs.exe @args
79            return
80        }
81    }
82
83    # Captures the output of the binary (stdout) which is the "cd" or editor command
84    # The TUI is rendered on stderr, so it doesn't interfere.
85    $command = (try-rs.exe @args)
86
87    if ($command) {
88        Invoke-Expression $command
89    }
90}
91"#
92        }
93        Shell::NuShell => {
94            r#"def --wrapped try-rs [...args] {
95    # Pass flags/options directly to stdout without capturing
96    for arg in $args {
97        if ($arg | str starts-with '-') {
98            ^try-rs.exe ...$args
99            return
100        }
101    }
102
103    # Capture output. Stderr (TUI) goes directly to terminal.
104    let output = (try-rs.exe ...$args)
105
106    if ($output | is-not-empty) {
107
108        # Grabs the path out of stdout returned by the binary and removes the single quotes
109        let $path = ($output | split row ' ').1 | str replace --all "'" ''
110        cd $path
111    }
112}
113"#
114        }
115    }
116}
117
118pub fn get_shell_integration_path(shell: &Shell) -> PathBuf {
119    let config_dir = match shell {
120        Shell::Fish => get_base_config_dir(),
121        _ => get_config_dir(),
122    };
123
124    match shell {
125        Shell::Fish => config_dir
126            .join("fish")
127            .join("functions")
128            .join("try-rs.fish"),
129        Shell::Zsh => config_dir.join("try-rs.zsh"),
130        Shell::Bash => config_dir.join("try-rs.bash"),
131        Shell::PowerShell => config_dir.join("try-rs.ps1"),
132        Shell::NuShell => config_dir.join("try-rs.nu"),
133    }
134}
135
136pub fn is_shell_integration_configured(shell: &Shell) -> bool {
137    get_shell_integration_path(shell).exists()
138}
139
140/// Appends a source command to an RC file if not already present.
141fn append_source_to_rc(rc_path: &std::path::Path, source_cmd: &str) -> Result<()> {
142    if rc_path.exists() {
143        let content = fs::read_to_string(rc_path)?;
144        if !content.contains(source_cmd) {
145            let mut file = fs::OpenOptions::new().append(true).open(rc_path)?;
146            writeln!(file, "\n# try-rs integration")?;
147            writeln!(file, "{}", source_cmd)?;
148            eprintln!("Added configuration to {}", rc_path.display());
149        } else {
150            eprintln!("Configuration already present in {}", rc_path.display());
151        }
152    } else {
153        eprintln!("You need to add the following line to {}:", rc_path.display());
154        eprintln!("{}", source_cmd);
155    }
156    Ok(())
157}
158
159/// Writes the shell integration file and returns its path.
160fn write_shell_integration(shell: &Shell) -> Result<std::path::PathBuf> {
161    let file_path = get_shell_integration_path(shell);
162    if let Some(parent) = file_path.parent()
163        && !parent.exists()
164    {
165        fs::create_dir_all(parent)?;
166    }
167    fs::write(&file_path, get_shell_content(shell))?;
168    eprintln!("{:?} function file created at: {}", shell, file_path.display());
169    Ok(file_path)
170}
171
172/// Sets up shell integration for the given shell.
173pub fn setup_shell(shell: &Shell) -> Result<()> {
174    let file_path = write_shell_integration(shell)?;
175    let home_dir = dirs::home_dir().expect("Could not find home directory");
176
177    match shell {
178        Shell::Fish => {
179            eprintln!(
180                "You may need to restart your shell or run 'source {}' to apply changes.",
181                file_path.display()
182            );
183        }
184        Shell::Zsh => {
185            let source_cmd = format!("source '{}'", file_path.display());
186            append_source_to_rc(&home_dir.join(".zshrc"), &source_cmd)?;
187        }
188        Shell::Bash => {
189            let source_cmd = format!("source '{}'", file_path.display());
190            append_source_to_rc(&home_dir.join(".bashrc"), &source_cmd)?;
191        }
192        Shell::PowerShell => {
193            let profile_path_ps7 = home_dir
194                .join("Documents")
195                .join("PowerShell")
196                .join("Microsoft.PowerShell_profile.ps1");
197            let profile_path_ps5 = home_dir
198                .join("Documents")
199                .join("WindowsPowerShell")
200                .join("Microsoft.PowerShell_profile.ps1");
201            let profile_path = if profile_path_ps7.exists() {
202                profile_path_ps7
203            } else if profile_path_ps5.exists() {
204                profile_path_ps5
205            } else {
206                profile_path_ps7
207            };
208
209            if let Some(parent) = profile_path.parent()
210                && !parent.exists()
211            {
212                fs::create_dir_all(parent)?;
213            }
214
215            let source_cmd = format!(". '{}'", file_path.display());
216            if profile_path.exists() {
217                append_source_to_rc(&profile_path, &source_cmd)?;
218            } else {
219                let mut file = fs::File::create(&profile_path)?;
220                writeln!(file, "# try-rs integration")?;
221                writeln!(file, "{}", source_cmd)?;
222                eprintln!(
223                    "PowerShell profile created and configured at: {}",
224                    profile_path.display()
225                );
226            }
227
228            eprintln!(
229                "You may need to restart your shell or run '. {}' to apply changes.",
230                profile_path.display()
231            );
232            eprintln!(
233                "If you get an error about running scripts, you may need to run: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned"
234            );
235        }
236        Shell::NuShell => {
237            let nu_config_path = dirs::config_dir()
238                .expect("Could not find config directory")
239                .join("nushell")
240                .join("config.nu");
241            let source_cmd = format!("source '{}'", file_path.display());
242            if nu_config_path.exists() {
243                append_source_to_rc(&nu_config_path, &source_cmd)?;
244            } else {
245                eprintln!("Could not find config.nu at {}", nu_config_path.display());
246                eprintln!("Please add the following line manually:");
247                eprintln!("{}", source_cmd);
248            }
249        }
250    }
251
252    Ok(())
253}