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
8pub 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
134pub 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
373pub fn get_completion_script_only(shell: &Shell) -> String {
375 let completions = get_completions_script(shell);
376 match shell {
377 Shell::NuShell => {
378 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
455fn 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
477fn 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
494pub 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
577pub fn generate_completions(shell: &Shell) -> Result<()> {
579 let script = get_completion_script_only(shell);
580 print!("{}", script);
581 Ok(())
582}