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 => config_dir
472 .join("fish")
473 .join("functions")
474 .join("try-rs.fish"),
475 Shell::Zsh => config_dir.join("try-rs.zsh"),
476 Shell::Bash => config_dir.join("try-rs.bash"),
477 Shell::PowerShell => config_dir.join("try-rs.ps1"),
478 Shell::NuShell => config_dir.join("try-rs.nu"),
479 }
480}
481
482fn write_fish_picker_function() -> Result<PathBuf> {
483 let file_path = get_base_config_dir()
484 .join("fish")
485 .join("functions")
486 .join("try-rs-picker.fish");
487 if let Some(parent) = file_path.parent()
488 && !parent.exists()
489 {
490 fs::create_dir_all(parent)?;
491 }
492 fs::write(&file_path, FISH_PICKER_FUNCTION)?;
493 eprintln!("Fish picker function file created at: {}", file_path.display());
494 Ok(file_path)
495}
496
497pub fn is_shell_integration_configured(shell: &Shell) -> bool {
498 get_shell_integration_path(shell).exists()
499}
500
501fn append_source_to_rc(rc_path: &std::path::Path, source_cmd: &str) -> Result<()> {
503 if rc_path.exists() {
504 let content = fs::read_to_string(rc_path)?;
505 if !content.contains(source_cmd) {
506 let mut file = fs::OpenOptions::new().append(true).open(rc_path)?;
507 writeln!(file, "\n# try-rs integration")?;
508 writeln!(file, "{}", source_cmd)?;
509 eprintln!("Added configuration to {}", rc_path.display());
510 } else {
511 eprintln!("Configuration already present in {}", rc_path.display());
512 }
513 } else {
514 eprintln!(
515 "You need to add the following line to {}:",
516 rc_path.display()
517 );
518 eprintln!("{}", source_cmd);
519 }
520 Ok(())
521}
522
523fn write_shell_integration(shell: &Shell) -> Result<std::path::PathBuf> {
525 let file_path = get_shell_integration_path(shell);
526 if let Some(parent) = file_path.parent()
527 && !parent.exists()
528 {
529 fs::create_dir_all(parent)?;
530 }
531 fs::write(&file_path, get_shell_content(shell))?;
532 eprintln!(
533 "{:?} function file created at: {}",
534 shell,
535 file_path.display()
536 );
537 Ok(file_path)
538}
539
540pub fn setup_shell(shell: &Shell) -> Result<()> {
542 let file_path = write_shell_integration(shell)?;
543 let home_dir = dirs::home_dir().expect("Could not find home directory");
544
545 match shell {
546 Shell::Fish => {
547 let _picker_path = write_fish_picker_function()?;
548 let fish_config_path = get_base_config_dir().join("fish").join("config.fish");
549 eprintln!(
550 "You may need to restart your shell or run 'source {}' to apply changes.",
551 file_path.display()
552 );
553 eprintln!(
554 "Optional: append the following to {} to bind Ctrl+T:",
555 fish_config_path.display()
556 );
557 eprintln!("bind \\ct try-rs-picker");
558 eprintln!("bind -M insert \\ct try-rs-picker");
559 }
560 Shell::Zsh => {
561 let source_cmd = format!("source '{}'", file_path.display());
562 append_source_to_rc(&home_dir.join(".zshrc"), &source_cmd)?;
563 }
564 Shell::Bash => {
565 let source_cmd = format!("source '{}'", file_path.display());
566 append_source_to_rc(&home_dir.join(".bashrc"), &source_cmd)?;
567 }
568 Shell::PowerShell => {
569 let profile_path_ps7 = home_dir
570 .join("Documents")
571 .join("PowerShell")
572 .join("Microsoft.PowerShell_profile.ps1");
573 let profile_path_ps5 = home_dir
574 .join("Documents")
575 .join("WindowsPowerShell")
576 .join("Microsoft.PowerShell_profile.ps1");
577 let profile_path = if profile_path_ps7.exists() {
578 profile_path_ps7
579 } else if profile_path_ps5.exists() {
580 profile_path_ps5
581 } else {
582 profile_path_ps7
583 };
584
585 if let Some(parent) = profile_path.parent()
586 && !parent.exists()
587 {
588 fs::create_dir_all(parent)?;
589 }
590
591 let source_cmd = format!(". '{}'", file_path.display());
592 if profile_path.exists() {
593 append_source_to_rc(&profile_path, &source_cmd)?;
594 } else {
595 let mut file = fs::File::create(&profile_path)?;
596 writeln!(file, "# try-rs integration")?;
597 writeln!(file, "{}", source_cmd)?;
598 eprintln!(
599 "PowerShell profile created and configured at: {}",
600 profile_path.display()
601 );
602 }
603
604 eprintln!(
605 "You may need to restart your shell or run '. {}' to apply changes.",
606 profile_path.display()
607 );
608 eprintln!(
609 "If you get an error about running scripts, you may need to run: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned"
610 );
611 }
612 Shell::NuShell => {
613 let nu_config_path = dirs::config_dir()
614 .expect("Could not find config directory")
615 .join("nushell")
616 .join("config.nu");
617 let source_cmd = format!("source '{}'", file_path.display());
618 if nu_config_path.exists() {
619 append_source_to_rc(&nu_config_path, &source_cmd)?;
620 } else {
621 eprintln!("Could not find config.nu at {}", nu_config_path.display());
622 eprintln!("Please add the following line manually:");
623 eprintln!("{}", source_cmd);
624 }
625 }
626 }
627
628 Ok(())
629}
630
631pub fn generate_completions(shell: &Shell) -> Result<()> {
633 let script = get_completion_script_only(shell);
634 print!("{}", script);
635 Ok(())
636}