Rust · 12113 bytes Raw Blame History
1 use crate::cli::{Hook, Shell};
2
3 use super::Result;
4
5 /// Generate shell integration code.
6 pub fn run(shell: Shell, cmd: String, hook: Hook, no_cmd: bool) -> Result<()> {
7 let output = match shell {
8 Shell::Bash => generate_bash(&cmd, &hook, no_cmd),
9 Shell::Zsh => generate_zsh(&cmd, &hook, no_cmd),
10 Shell::Fish => generate_fish(&cmd, &hook, no_cmd),
11 Shell::Fortsh => generate_fortsh(&cmd, &hook, no_cmd),
12 };
13
14 println!("{}", output);
15 Ok(())
16 }
17
18 fn generate_bash(cmd: &str, hook: &Hook, no_cmd: bool) -> String {
19 let mut output = String::new();
20
21 // Hook function
22 output.push_str(r#"
23 # gump hook - called after directory changes
24 __gump_hook() {
25 command gump add -- "$PWD"
26 }
27 "#);
28
29 // Hook trigger based on type
30 match hook {
31 Hook::Prompt => {
32 output.push_str(r#"
33 # Update on every prompt
34 if [[ "${PROMPT_COMMAND:-}" != *'__gump_hook'* ]]; then
35 PROMPT_COMMAND="__gump_hook;${PROMPT_COMMAND#;}"
36 fi
37 "#);
38 }
39 Hook::Pwd => {
40 output.push_str(r#"
41 # Update when directory changes
42 __gump_oldpwd="$PWD"
43 __gump_pwd_hook() {
44 if [[ "$PWD" != "$__gump_oldpwd" ]]; then
45 __gump_oldpwd="$PWD"
46 __gump_hook
47 fi
48 }
49 if [[ "${PROMPT_COMMAND:-}" != *'__gump_pwd_hook'* ]]; then
50 PROMPT_COMMAND="__gump_pwd_hook;${PROMPT_COMMAND#;}"
51 fi
52 "#);
53 }
54 }
55
56 // Command aliases (unless --no-cmd)
57 if !no_cmd {
58 output.push_str(&format!(
59 r#"
60 # Jump function
61 {cmd}() {{
62 if [[ $# -eq 0 ]]; then
63 builtin cd ~ && __gump_hook
64 elif [[ $# -eq 1 && "$1" == "-" ]]; then
65 builtin cd - && __gump_hook
66 elif [[ $# -eq 1 && -d "$1" ]]; then
67 # Explicit path - cd directly without fuzzy matching
68 builtin cd -- "$1" && __gump_hook
69 else
70 local result
71 # Try CWD first, then database
72 result=$(command gump query --cwd -- "$@" 2>/dev/null)
73 if [[ -z "$result" ]]; then
74 result=$(command gump query -- "$@" 2>/dev/null)
75 fi
76 if [[ -n "$result" ]]; then
77 builtin cd -- "$result" && __gump_hook
78 else
79 echo "gump: no match found" >&2
80 return 1
81 fi
82 fi
83 }}
84
85 # Interactive mode with fzf
86 {cmd}i() {{
87 local result
88 result=$(command gump query --all -- "$@" | fzf --height=40% --reverse)
89 if [[ -n "$result" ]]; then
90 builtin cd -- "$result" && __gump_hook
91 fi
92 }}
93 "#,
94 cmd = cmd
95 ));
96 }
97
98 // The magic: command_not_found_handle for no-prefix jumping
99 output.push_str(r#"
100 # No-prefix directory jumping
101 command_not_found_handle() {
102 # Check if it's a local directory first (exact match)
103 if [[ -d "$1" ]]; then
104 builtin cd -- "$1" && __gump_hook
105 return 0
106 fi
107
108 # Fuzzy match against current directory contents
109 local result
110 result=$(command gump query --cwd -- "$@" 2>/dev/null)
111 if [[ -n "$result" ]]; then
112 builtin cd -- "$result" && __gump_hook
113 return 0
114 fi
115
116 # Query gump database
117 result=$(command gump query -- "$@" 2>/dev/null)
118 if [[ -n "$result" ]]; then
119 builtin cd -- "$result" && __gump_hook
120 return 0
121 fi
122
123 # Fallback to default "command not found" behavior
124 echo "bash: $1: command not found" >&2
125 return 127
126 }
127 "#);
128
129 output
130 }
131
132 fn generate_zsh(cmd: &str, hook: &Hook, no_cmd: bool) -> String {
133 let mut output = String::new();
134
135 // Hook function
136 output.push_str(r#"
137 # gump hook - called after directory changes
138 __gump_hook() {
139 command gump add -- "$PWD"
140 }
141 "#);
142
143 // Hook trigger
144 match hook {
145 Hook::Prompt => {
146 output.push_str(r#"
147 # Update on every prompt
148 [[ -n "${precmd_functions[(r)__gump_hook]}" ]] || precmd_functions+=(__gump_hook)
149 "#);
150 }
151 Hook::Pwd => {
152 output.push_str(r#"
153 # Update when directory changes (chpwd hook)
154 [[ -n "${chpwd_functions[(r)__gump_hook]}" ]] || chpwd_functions+=(__gump_hook)
155 "#);
156 }
157 }
158
159 // Command aliases
160 if !no_cmd {
161 output.push_str(&format!(
162 r#"
163 # Jump function
164 {cmd}() {{
165 if [[ $# -eq 0 ]]; then
166 builtin cd ~
167 elif [[ $# -eq 1 && "$1" == "-" ]]; then
168 builtin cd -
169 elif [[ $# -eq 1 && -d "$1" ]]; then
170 # Explicit path - cd directly without fuzzy matching
171 builtin cd -- "$1"
172 else
173 local result
174 # Try CWD first, then database
175 result=$(command gump query --cwd -- "$@" 2>/dev/null)
176 if [[ -z "$result" ]]; then
177 result=$(command gump query -- "$@" 2>/dev/null)
178 fi
179 if [[ -n "$result" ]]; then
180 builtin cd -- "$result"
181 else
182 echo "gump: no match found" >&2
183 return 1
184 fi
185 fi
186 }}
187
188 # Interactive mode with fzf
189 {cmd}i() {{
190 local result
191 result=$(command gump query --all -- "$@" | fzf --height=40% --reverse)
192 if [[ -n "$result" ]]; then
193 builtin cd -- "$result"
194 fi
195 }}
196 "#,
197 cmd = cmd
198 ));
199 }
200
201 // ZLE widget for no-prefix jumping
202 output.push_str(r#"
203 # ZLE widget to intercept commands before execution
204 __gump_accept_line() {
205 local first_word="${BUFFER%% *}"
206
207 # Skip if buffer is empty
208 if [[ -z "$BUFFER" ]]; then
209 zle .accept-line
210 return
211 fi
212
213 # Skip paths (starting with . / or ~)
214 if [[ "$first_word" == "."* ]] || \
215 [[ "$first_word" == "/"* ]] || \
216 [[ "$first_word" == "~"* ]]; then
217 zle .accept-line
218 return
219 fi
220
221 # Skip if command exists (whence checks builtins, functions, aliases, and PATH)
222 if whence "$first_word" >/dev/null 2>&1; then
223 zle .accept-line
224 return
225 fi
226
227 # Check if it's a local directory (exact match)
228 if [[ -d "$first_word" ]]; then
229 BUFFER="cd ${(q)first_word}"
230 zle .accept-line
231 return
232 fi
233
234 # Fuzzy match against current directory contents
235 local result
236 result=$(command gump query --cwd -- $=BUFFER 2>/dev/null)
237 if [[ -n "$result" ]]; then
238 BUFFER="cd ${(q)result}"
239 zle .accept-line
240 return
241 fi
242
243 # Query gump database
244 result=$(command gump query -- $=BUFFER 2>/dev/null)
245 if [[ -n "$result" ]]; then
246 BUFFER="cd ${(q)result}"
247 fi
248
249 zle .accept-line
250 }
251
252 zle -N accept-line __gump_accept_line
253 "#);
254
255 output
256 }
257
258 fn generate_fish(cmd: &str, hook: &Hook, no_cmd: bool) -> String {
259 let mut output = String::new();
260
261 // Hook function based on type
262 match hook {
263 Hook::Prompt => {
264 output.push_str(r#"
265 # Update on every prompt
266 function __gump_hook --on-event fish_prompt
267 command gump add -- $PWD
268 end
269 "#);
270 }
271 Hook::Pwd => {
272 output.push_str(r#"
273 # Update when directory changes
274 function __gump_hook --on-variable PWD
275 command gump add -- $PWD
276 end
277 "#);
278 }
279 }
280
281 // Command aliases
282 if !no_cmd {
283 output.push_str(&format!(
284 r#"
285 # Jump function
286 function {cmd} --description "Jump to a directory"
287 if test (count $argv) -eq 0
288 cd ~
289 else if test (count $argv) -eq 1 -a "$argv[1]" = "-"
290 cd -
291 else if test (count $argv) -eq 1 -a -d "$argv[1]"
292 # Explicit path - cd directly without fuzzy matching
293 cd $argv[1]
294 else
295 # Try CWD first, then database
296 set -l result (command gump query --cwd -- $argv 2>/dev/null)
297 if test -z "$result"
298 set result (command gump query -- $argv 2>/dev/null)
299 end
300 if test -n "$result"
301 cd $result
302 else
303 echo "gump: no match found" >&2
304 return 1
305 end
306 end
307 end
308
309 # Interactive mode with fzf
310 function {cmd}i --description "Jump to a directory (interactive)"
311 set -l result (command gump query --all -- $argv | fzf --height=40% --reverse)
312 if test -n "$result"
313 cd $result
314 end
315 end
316 "#,
317 cmd = cmd
318 ));
319 }
320
321 // Intercept Enter key to check for gump jumps before execution
322 output.push_str(r#"
323 # No-prefix directory jumping via Enter key interception
324 function __gump_execute
325 set -l cmd (commandline -b)
326 set -l first_word (string split ' ' -- $cmd)[1]
327
328 # Skip if empty
329 if test -z "$first_word"
330 commandline -f execute
331 return
332 end
333
334 # Skip if command exists
335 if type -q "$first_word"
336 commandline -f execute
337 return
338 end
339
340 # Skip if starts with path characters
341 if string match -qr '^[./~]' -- "$first_word"
342 commandline -f execute
343 return
344 end
345
346 # Check if it's a local directory (exact match)
347 if test -d "$first_word"
348 commandline -r "cd $first_word"
349 commandline -f execute
350 return
351 end
352
353 # Fuzzy match against current directory contents
354 set -l result (command gump query --cwd -- $cmd 2>/dev/null)
355 if test -n "$result"
356 commandline -r "cd \"$result\""
357 commandline -f execute
358 return
359 end
360
361 # Query gump database
362 set -l result (command gump query -- $cmd 2>/dev/null)
363 if test -n "$result"
364 commandline -r "cd \"$result\""
365 commandline -f execute
366 return
367 end
368
369 # Not a gump match, execute normally
370 commandline -f execute
371 end
372
373 bind \r __gump_execute
374 bind \n __gump_execute
375 "#);
376
377 output
378 }
379
380 fn generate_fortsh(cmd: &str, hook: &Hook, no_cmd: bool) -> String {
381 let mut output = String::new();
382
383 // Hook function
384 output.push_str(r#"
385 # gump hook - called after directory changes
386 __gump_hook() {
387 command gump add -- "$PWD"
388 }
389 "#);
390
391 // Hook helper for pwd mode (defined before trap, used later)
392 if matches!(hook, Hook::Pwd) {
393 output.push_str(r#"
394 # Track directory changes
395 __gump_oldpwd="$PWD"
396 __gump_pwd_hook() {
397 if [[ "$PWD" != "$__gump_oldpwd" ]]; then
398 __gump_oldpwd="$PWD"
399 __gump_hook
400 fi
401 }
402 "#);
403 }
404
405 // Command aliases (unless --no-cmd)
406 if !no_cmd {
407 output.push_str(&format!(
408 r#"
409 # Jump function
410 {cmd}() {{
411 if [[ $# -eq 0 ]]; then
412 command cd ~ && __gump_hook
413 elif [[ $# -eq 1 && "$1" == "-" ]]; then
414 command cd - && __gump_hook
415 elif [ $# -eq 1 ] && [ -d "$1" ]; then
416 command cd "$1" && __gump_hook
417 else
418 local result
419 result=$(command gump query --cwd -- "$@" 2>/dev/null)
420 if [[ -z "$result" ]]; then
421 result=$(command gump query -- "$@" 2>/dev/null)
422 fi
423 if [[ -n "$result" ]]; then
424 command cd "$result" && __gump_hook
425 else
426 echo "gump: no match found" >&2
427 return 1
428 fi
429 fi
430 }}
431
432 # Interactive mode with fzf
433 {cmd}i() {{
434 local result
435 result=$(command gump query --all -- "$@" | fzf --height=40% --reverse)
436 if [[ -n "$result" ]]; then
437 command cd "$result" && __gump_hook
438 fi
439 }}
440 "#,
441 cmd = cmd
442 ));
443 }
444
445 // command_not_found_handle for no-prefix jumping
446 output.push_str(r#"
447 # No-prefix directory jumping
448 command_not_found_handle() {
449 # Check if it's a local directory first (exact match)
450 if [ -d "$1" ]; then
451 command cd "$1" && __gump_hook
452 return 0
453 fi
454
455 # Fuzzy match against current directory contents
456 local result
457 result=$(command gump query --cwd -- "$@" 2>/dev/null)
458 if [[ -n "$result" ]]; then
459 command cd "$result" && __gump_hook
460 return 0
461 fi
462
463 # Query gump database
464 result=$(command gump query -- "$@" 2>/dev/null)
465 if [[ -n "$result" ]]; then
466 command cd "$result" && __gump_hook
467 return 0
468 fi
469
470 # Fallback to default "command not found" behavior
471 echo "fortsh: $1: command not found" >&2
472 return 127
473 }
474 "#);
475
476 // Set trap LAST to avoid firing during function definitions above
477 match hook {
478 Hook::Prompt => {
479 output.push_str(r#"
480 # Update on every prompt (via DEBUG trap)
481 trap '__gump_hook' DEBUG
482 "#);
483 }
484 Hook::Pwd => {
485 output.push_str(r#"
486 # Update when directory changes (via DEBUG trap)
487 trap '__gump_pwd_hook' DEBUG
488 "#);
489 }
490 }
491
492 output
493 }
494