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