lacy 1.8.11 → 1.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.github/FUNDING.yml +3 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  7. package/.github/SECURITY.md +32 -0
  8. package/.github/assets/logo-horizontal-dark.png +0 -0
  9. package/.github/assets/logo-horizontal-dark.svg +17 -0
  10. package/.github/assets/logo-horizontal.png +0 -0
  11. package/.github/assets/logo-horizontal.svg +17 -0
  12. package/.github/assets/logo.png +0 -0
  13. package/.github/assets/logo.svg +12 -0
  14. package/.github/assets/social-preview.png +0 -0
  15. package/.github/assets/social-preview.svg +50 -0
  16. package/.github/dependabot.yml +21 -0
  17. package/.github/workflows/ci.yml +80 -0
  18. package/.github/workflows/dependabot-auto-merge.yml +32 -0
  19. package/CHANGELOG.md +366 -0
  20. package/CLAUDE.md +340 -0
  21. package/CONTRIBUTING.md +141 -0
  22. package/LICENSE +110 -0
  23. package/README.md +201 -31
  24. package/RELEASING.md +148 -0
  25. package/STYLE.md +202 -0
  26. package/assets/hero.jpeg +0 -0
  27. package/assets/mode-indicators.jpeg +0 -0
  28. package/assets/real-time-indicator.jpeg +0 -0
  29. package/assets/supported-tools.jpeg +0 -0
  30. package/bin/lacy +1028 -0
  31. package/docs/ADDING-BACKENDS.md +124 -0
  32. package/docs/DEVTO-ARTICLE.md +94 -0
  33. package/docs/DOCS.md +68 -0
  34. package/docs/GROWTH-STRATEGY.md +119 -0
  35. package/docs/HN-RESPONSES.md +122 -0
  36. package/docs/LAUNCH-COPY-FINAL.md +105 -0
  37. package/docs/MARKETING.md +411 -0
  38. package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
  39. package/docs/UGC_VIDEO_SCRIPT.md +114 -0
  40. package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
  41. package/docs/demo-color-transition.gif +0 -0
  42. package/docs/demo-full.gif +0 -0
  43. package/docs/demo-indicator.gif +0 -0
  44. package/docs/launch-thread-may6.sh +158 -0
  45. package/docs/videos/README.md +189 -0
  46. package/docs/videos/generate_frames.py +510 -0
  47. package/docs/videos/generate_frames_v2.py +729 -0
  48. package/docs/videos/generate_short.py +328 -0
  49. package/docs/videos/generate_short_v2.py +526 -0
  50. package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
  51. package/docs/videos/lacy-shell-demo.mp4 +0 -0
  52. package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
  53. package/docs/videos/lacy-shell-short.mp4 +0 -0
  54. package/install.sh +1009 -0
  55. package/lacy.plugin.bash +75 -0
  56. package/lacy.plugin.fish +43 -0
  57. package/lacy.plugin.zsh +65 -0
  58. package/lib/animations.zsh +3 -0
  59. package/lib/bash/completions.bash +40 -0
  60. package/lib/bash/execute.bash +233 -0
  61. package/lib/bash/init.bash +40 -0
  62. package/lib/bash/keybindings.bash +134 -0
  63. package/lib/bash/prompt.bash +85 -0
  64. package/lib/commands/info.sh +25 -0
  65. package/lib/config.zsh +3 -0
  66. package/lib/constants.zsh +3 -0
  67. package/lib/core/animations.sh +271 -0
  68. package/lib/core/commands.sh +297 -0
  69. package/lib/core/config.sh +340 -0
  70. package/lib/core/constants.sh +366 -0
  71. package/lib/core/context.sh +260 -0
  72. package/lib/core/detection.sh +417 -0
  73. package/lib/core/mcp.sh +741 -0
  74. package/lib/core/modes.sh +123 -0
  75. package/lib/core/preheat.sh +496 -0
  76. package/lib/core/spinner.sh +174 -0
  77. package/lib/core/telemetry.sh +99 -0
  78. package/lib/detection.zsh +3 -0
  79. package/lib/execute.zsh +3 -0
  80. package/lib/fish/config.fish +66 -0
  81. package/lib/fish/detection.fish +90 -0
  82. package/lib/fish/execute.fish +105 -0
  83. package/lib/fish/keybindings.fish +42 -0
  84. package/lib/fish/prompt.fish +30 -0
  85. package/lib/keybindings.zsh +3 -0
  86. package/lib/mcp.zsh +3 -0
  87. package/lib/modes.zsh +3 -0
  88. package/lib/preheat.zsh +3 -0
  89. package/lib/prompt.zsh +3 -0
  90. package/lib/spinner.zsh +3 -0
  91. package/lib/zsh/completions.zsh +60 -0
  92. package/lib/zsh/execute.zsh +294 -0
  93. package/lib/zsh/init.zsh +26 -0
  94. package/lib/zsh/keybindings.zsh +551 -0
  95. package/lib/zsh/prompt.zsh +90 -0
  96. package/package.json +42 -27
  97. package/packages/lacy/README.md +61 -0
  98. package/packages/lacy/commands/info.sh +25 -0
  99. package/{index.mjs → packages/lacy/index.mjs} +247 -20
  100. package/packages/lacy/package-lock.json +71 -0
  101. package/packages/lacy/package.json +42 -0
  102. package/script/release.ts +487 -0
  103. package/squirrel.toml +36 -0
  104. package/tests/test_bash.bash +163 -0
  105. package/tests/test_core.sh +607 -0
  106. package/tests/test_gemini.sh +119 -0
  107. package/tests/test_gemini_mcp.sh +126 -0
  108. package/tests/test_preheat_server.zsh +446 -0
  109. package/uninstall.sh +52 -0
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env zsh
2
+
3
+ # Keybinding setup for Lacy Shell
4
+ #
5
+ # ============================================================================
6
+ # Plugin Coexistence: region_highlight & POSTDISPLAY
7
+ # ============================================================================
8
+ #
9
+ # This file manages two ZLE features that are shared with other plugins
10
+ # (notably zsh-autosuggestions): `region_highlight` and `POSTDISPLAY`.
11
+ #
12
+ # region_highlight
13
+ # ----------------
14
+ # An array of highlight specs applied to the input buffer + POSTDISPLAY.
15
+ # Multiple plugins write to it (autosuggestions for gray suggestion text,
16
+ # syntax-highlighting for colorized input, etc.).
17
+ #
18
+ # Problem: Lacy needs to highlight the first word (green/magenta) on every
19
+ # keystroke, which requires removing the previous first-word
20
+ # highlight. Naively resetting `region_highlight=()` destroys
21
+ # highlights from other plugins — causing autosuggestion text to
22
+ # turn white (default fg) instead of staying gray.
23
+ #
24
+ # Solution: Tag every Lacy highlight entry with `memo=lacy` (a ZSH 5.8+
25
+ # region_highlight feature that is ignored by the renderer but lets
26
+ # plugins identify their own entries). On each pre-redraw, strip
27
+ # only memo=lacy entries:
28
+ #
29
+ # region_highlight=("${(@)region_highlight:#*memo=lacy*}")
30
+ #
31
+ # This preserves highlights from autosuggestions, syntax-highlighting,
32
+ # and any other plugin.
33
+ #
34
+ # POSTDISPLAY
35
+ # -----------
36
+ # Text rendered after BUFFER (the user's input). Both Lacy (ghost text
37
+ # suggestions after a reroute candidate fails) and zsh-autosuggestions
38
+ # (history-based suggestions) write to POSTDISPLAY.
39
+ #
40
+ # Problem: When both plugins set POSTDISPLAY in the same redraw cycle, the
41
+ # last writer wins. If autosuggestions runs after Lacy's pre-redraw
42
+ # hook (via add-zle-hook-widget, which coexists with our zle -N
43
+ # registration), it overwrites Lacy's ghost text with an empty
44
+ # string (no history match for an empty buffer).
45
+ #
46
+ # Solution: When Lacy's ghost text is active, call _zsh_autosuggest_clear
47
+ # (if available) before setting POSTDISPLAY. This tells
48
+ # autosuggestions to stop managing POSTDISPLAY for this cycle.
49
+ # When the user starts typing (BUFFER becomes non-empty), Lacy
50
+ # clears its ghost text and autosuggestions resumes normally.
51
+ #
52
+ # Right Arrow / Tab (suggestion accept)
53
+ # --------------------------------------
54
+ # Both Lacy and autosuggestions use right arrow / tab to accept suggestions.
55
+ # Lacy's widgets (_lacy_forward_char_or_accept, _lacy_expand_or_accept)
56
+ # check for Lacy ghost text first. If present, they accept it into BUFFER.
57
+ # If not, they fall through to `forward-char` / `expand-or-complete`
58
+ # (WITHOUT the dot prefix) so that autosuggestions' widget wrappers still
59
+ # fire and can accept their own suggestions.
60
+ #
61
+ # Key detail: `zle .forward-char` (dot prefix) calls the raw ZSH builtin,
62
+ # bypassing any widget wrapping. `zle forward-char` (no dot) calls the
63
+ # named widget, which autosuggestions may have replaced with its wrapper.
64
+ # We use the no-dot form so autosuggestions works when Lacy has no ghost text.
65
+ #
66
+ # ============================================================================
67
+
68
+ # Interrupt state and input type are initialized in constants.sh
69
+
70
+ # Ghost text suggestion (shown as POSTDISPLAY after a reroute candidate fails)
71
+ LACY_SHELL_SUGGESTION=""
72
+ LACY_SHELL_OWN_POSTDISPLAY=false # true when Lacy is managing POSTDISPLAY
73
+
74
+ # ============================================================================
75
+ # Real-time Shell/Agent Indicator
76
+ # ============================================================================
77
+
78
+ # Check if input will go to shell or agent
79
+ # Delegates to centralized detection in detection.zsh
80
+ lacy_shell_detect_input_type() {
81
+ lacy_shell_classify_input "$1"
82
+ }
83
+
84
+ # Update the indicator based on current input (called on every keystroke)
85
+ lacy_shell_update_input_indicator() {
86
+ [[ "$LACY_SHELL_ENABLED" != true ]] && return
87
+ [[ "$LACY_SHELL_PROMPT_INITIALIZED" != true ]] && return
88
+ [[ -z "$LACY_SHELL_BASE_PS1" ]] && return
89
+
90
+ local input_type=$(lacy_shell_detect_input_type "$BUFFER")
91
+
92
+ # Only update prompt if type changed (avoids flickering)
93
+ if [[ "$input_type" != "$LACY_SHELL_INPUT_TYPE" ]]; then
94
+ LACY_SHELL_INPUT_TYPE="$input_type"
95
+
96
+ # Build new PS1 with colored indicator
97
+ # Colors chosen for maximum distinction (see constants.zsh)
98
+ local indicator
99
+ case "$input_type" in
100
+ "shell")
101
+ indicator="%F{${LACY_COLOR_SHELL}}${LACY_INDICATOR_CHAR}%f"
102
+ ;;
103
+ "agent")
104
+ indicator="%F{${LACY_COLOR_AGENT}}${LACY_INDICATOR_CHAR}%f"
105
+ ;;
106
+ *)
107
+ indicator="%F{${LACY_COLOR_NEUTRAL}}${LACY_INDICATOR_CHAR}%f"
108
+ ;;
109
+ esac
110
+
111
+ # Update prompt with indicator (appended after prompt, before cursor)
112
+ PS1="${LACY_SHELL_BASE_PS1}${indicator} "
113
+ local _lacy_need_reset=true
114
+ fi
115
+
116
+ # Highlight the first word in the buffer based on classification.
117
+ # Runs on every pre-redraw (not just type changes) because the
118
+ # first word boundaries shift as the user types.
119
+ # Remove only our previous highlight (tagged with "memo=lacy") —
120
+ # preserve highlights from zsh-autosuggestions and other plugins.
121
+ region_highlight=("${(@)region_highlight:#*memo=lacy*}")
122
+ if [[ -n "$BUFFER" ]]; then
123
+ # Find start of first word (skip leading whitespace)
124
+ local i=0
125
+ while (( i < ${#BUFFER} )) && [[ "${BUFFER:$i:1}" == [[:space:]] ]]; do
126
+ (( i++ ))
127
+ done
128
+ # Find end of first word
129
+ local j=$i
130
+ while (( j < ${#BUFFER} )) && [[ "${BUFFER:$j:1}" != [[:space:]] ]]; do
131
+ (( j++ ))
132
+ done
133
+ if (( j > i )); then
134
+ case "$input_type" in
135
+ "shell")
136
+ region_highlight+=("$i $j fg=${LACY_COLOR_SHELL},bold memo=lacy")
137
+ ;;
138
+ "agent")
139
+ region_highlight+=("$i $j fg=${LACY_COLOR_AGENT},bold memo=lacy")
140
+ ;;
141
+ esac
142
+ fi
143
+ fi
144
+
145
+ # Ghost text suggestion — show inline placeholder when buffer is empty.
146
+ # See file header for POSTDISPLAY coexistence design with zsh-autosuggestions.
147
+ if [[ -n "$LACY_SHELL_SUGGESTION" ]]; then
148
+ if [[ -z "$BUFFER" ]]; then
149
+ # Clear autosuggestions' POSTDISPLAY before writing ours.
150
+ # Without this, autosuggestions' pre-redraw hook (registered via
151
+ # add-zle-hook-widget) runs after ours and overwrites POSTDISPLAY
152
+ # with "" (no history match for empty input), making ghost text
153
+ # invisible. _zsh_autosuggest_clear tells it to stop for this cycle.
154
+ (( $+functions[_zsh_autosuggest_clear] )) && _zsh_autosuggest_clear
155
+ POSTDISPLAY="$LACY_SHELL_SUGGESTION"
156
+ LACY_SHELL_OWN_POSTDISPLAY=true
157
+ region_highlight+=("${#BUFFER} $((${#BUFFER} + ${#POSTDISPLAY})) fg=${LACY_COLOR_NEUTRAL} memo=lacy")
158
+ else
159
+ # User started typing — clear ghost text, autosuggestions resumes
160
+ LACY_SHELL_SUGGESTION=""
161
+ POSTDISPLAY=""
162
+ LACY_SHELL_OWN_POSTDISPLAY=false
163
+ fi
164
+ elif [[ "$LACY_SHELL_OWN_POSTDISPLAY" == true ]]; then
165
+ # Suggestion was cleared externally (precmd) — clean up POSTDISPLAY
166
+ POSTDISPLAY=""
167
+ LACY_SHELL_OWN_POSTDISPLAY=false
168
+ fi
169
+
170
+ # Defer reset-prompt to AFTER all highlights and POSTDISPLAY are set,
171
+ # since reset-prompt triggers an immediate render.
172
+ if [[ "$_lacy_need_reset" == true ]]; then
173
+ zle && zle reset-prompt
174
+ fi
175
+ }
176
+
177
+ # ZLE widget that runs before each redraw
178
+ lacy_shell_line_pre_redraw() {
179
+ lacy_shell_update_input_indicator
180
+ }
181
+
182
+ # ZLE widget that runs when a new line of input starts — set up ghost text
183
+ # before the first pre-redraw so it's visible on the very first render.
184
+ # Same POSTDISPLAY coexistence pattern as in lacy_shell_update_input_indicator.
185
+ lacy_shell_line_init() {
186
+ if [[ -n "$LACY_SHELL_SUGGESTION" && -z "$BUFFER" ]]; then
187
+ # Suppress autosuggestions before claiming POSTDISPLAY (see file header)
188
+ (( $+functions[_zsh_autosuggest_clear] )) && _zsh_autosuggest_clear
189
+ POSTDISPLAY="$LACY_SHELL_SUGGESTION"
190
+ LACY_SHELL_OWN_POSTDISPLAY=true
191
+ region_highlight+=("${#BUFFER} $((${#BUFFER} + ${#POSTDISPLAY})) fg=${LACY_COLOR_NEUTRAL} memo=lacy")
192
+ fi
193
+ }
194
+
195
+ # Register hooks
196
+ zle -N zle-line-pre-redraw lacy_shell_line_pre_redraw
197
+ zle -N zle-line-init lacy_shell_line_init
198
+
199
+ # Set up all keybindings
200
+ lacy_shell_setup_keybindings() {
201
+ # Only add our custom bindings - don't touch existing terminal shortcuts
202
+
203
+ # Primary mode toggle - Ctrl+Space (most universal)
204
+ bindkey '^@' lacy_shell_toggle_mode_widget # Ctrl+Space: Toggle mode
205
+
206
+ # Alternative keybindings
207
+ bindkey '^T' lacy_shell_toggle_mode_widget # Ctrl+T: Toggle mode (backup)
208
+
209
+ # Direct mode switches (Ctrl+X prefix)
210
+ # bindkey '^X^A' lacy_shell_agent_mode_widget # Ctrl+X Ctrl+A: Agent mode
211
+ # bindkey '^X^S' lacy_shell_shell_mode_widget # Ctrl+X Ctrl+S: Shell mode
212
+ # bindkey '^X^U' lacy_shell_auto_mode_widget # Ctrl+X Ctrl+U: Auto mode
213
+ # bindkey '^X^H' lacy_shell_help_widget # Ctrl+X Ctrl+H: Help
214
+
215
+ # Terminal scrolling keybindings
216
+ # bindkey '^[[5~' lacy_shell_scroll_up_widget # Page Up: Scroll up
217
+ # bindkey '^[[6~' lacy_shell_scroll_down_widget # Page Down: Scroll down
218
+ # bindkey '^Y' lacy_shell_scroll_up_line_widget # Ctrl+Y: Scroll up one line
219
+ # bindkey '^E' lacy_shell_scroll_down_line_widget # Ctrl+E: Scroll down one line
220
+
221
+ # Override Ctrl+D behavior
222
+ bindkey '^D' lacy_shell_delete_char_or_quit_widget # Ctrl+D: Quit if buffer empty
223
+
224
+ # Fix Command+Delete on macOS: send ^U, which ZSH defaults to kill-whole-line.
225
+ # Rebind to backward-kill-line so only text before the cursor is deleted.
226
+ bindkey '^U' backward-kill-line
227
+
228
+ # Ghost text suggestion accept (right arrow, tab)
229
+ bindkey '^[[C' _lacy_forward_char_or_accept # Right arrow
230
+ bindkey '^[OC' _lacy_forward_char_or_accept # Right arrow (alt sequence)
231
+ bindkey '^I' _lacy_expand_or_accept # Tab
232
+ }
233
+
234
+ # Widget to toggle mode
235
+ lacy_shell_toggle_mode_widget() {
236
+ lacy_shell_toggle_mode
237
+ zle reset-prompt
238
+ }
239
+
240
+ # Widget to switch to agent mode
241
+ lacy_shell_agent_mode_widget() {
242
+ lacy_shell_set_mode "agent"
243
+ zle reset-prompt
244
+ }
245
+
246
+ # Widget to switch to shell mode
247
+ lacy_shell_shell_mode_widget() {
248
+ lacy_shell_set_mode "shell"
249
+ zle reset-prompt
250
+ }
251
+
252
+ # Widget to switch to auto mode
253
+ lacy_shell_auto_mode_widget() {
254
+ lacy_shell_set_mode "auto"
255
+ zle reset-prompt
256
+ }
257
+
258
+ # Widget to show help
259
+ lacy_shell_help_widget() {
260
+ echo ""
261
+ echo "Lacy Shell"
262
+ echo ""
263
+ echo "Modes:"
264
+ echo " Shell Normal shell execution"
265
+ echo " Agent AI-powered assistance"
266
+ echo " Auto Smart detection"
267
+ echo ""
268
+ echo "Keys:"
269
+ echo " Ctrl+Space Toggle mode"
270
+ echo " Ctrl+D Quit"
271
+ echo " Ctrl+C (2x) Quit"
272
+ echo ""
273
+ echo "Commands:"
274
+ echo " ask \"text\" Query AI"
275
+ echo " quit_lacy Exit"
276
+ echo ""
277
+ zle reset-prompt
278
+ }
279
+
280
+ # Widget to clear/cancel current input (was quit)
281
+ lacy_shell_quit_widget() {
282
+ # Clear the current line buffer
283
+ BUFFER=""
284
+ # Reset the prompt
285
+ zle reset-prompt
286
+ }
287
+
288
+ # Widget for Ctrl+D - quit if buffer empty, else delete char
289
+ lacy_shell_delete_char_or_quit_widget() {
290
+ if [[ -z "$BUFFER" ]]; then
291
+ # Buffer is empty - request deferred quit and consume Ctrl-D safely
292
+ LACY_SHELL_DEFER_QUIT=true
293
+ BUFFER=" :"
294
+ zle .accept-line
295
+ else
296
+ # Buffer has content - normal delete char behavior
297
+ zle delete-char-or-list
298
+ fi
299
+ }
300
+
301
+
302
+ # Scrolling widgets
303
+ lacy_shell_scroll_up_widget() {
304
+ # Scroll terminal buffer up (page)
305
+ zle -I
306
+ if [[ "$TERM_PROGRAM" == "iTerm.app" ]]; then
307
+ printf '\e]1337;ScrollPageUp\a'
308
+ elif [[ "$TERM" == "xterm"* ]] || [[ "$TERM" == "screen"* ]]; then
309
+ # Send shift+page up for terminal scrollback
310
+ printf '\e[5;2~'
311
+ else
312
+ # Generic terminal: try to scroll with tput
313
+ tput rin 5 2>/dev/null || printf '\e[5S'
314
+ fi
315
+ }
316
+
317
+ lacy_shell_scroll_down_widget() {
318
+ # Scroll terminal buffer down (page)
319
+ zle -I
320
+ if [[ "$TERM_PROGRAM" == "iTerm.app" ]]; then
321
+ printf '\e]1337;ScrollPageDown\a'
322
+ elif [[ "$TERM" == "xterm"* ]] || [[ "$TERM" == "screen"* ]]; then
323
+ # Send shift+page down for terminal scrollback
324
+ printf '\e[6;2~'
325
+ else
326
+ # Generic terminal: try to scroll with tput
327
+ tput ri 5 2>/dev/null || printf '\e[5T'
328
+ fi
329
+ }
330
+
331
+ lacy_shell_scroll_up_line_widget() {
332
+ # Scroll terminal buffer up (single line)
333
+ zle -I
334
+ if [[ "$TERM_PROGRAM" == "iTerm.app" ]]; then
335
+ printf '\e]1337;ScrollLineUp\a'
336
+ elif [[ "$TERM" == "xterm"* ]] || [[ "$TERM" == "screen"* ]]; then
337
+ printf '\eOA'
338
+ else
339
+ # Generic terminal: scroll one line
340
+ tput rin 1 2>/dev/null || printf '\e[S'
341
+ fi
342
+ }
343
+
344
+ lacy_shell_scroll_down_line_widget() {
345
+ # Scroll terminal buffer down (single line)
346
+ zle -I
347
+ if [[ "$TERM_PROGRAM" == "iTerm.app" ]]; then
348
+ printf '\e]1337;ScrollLineDown\a'
349
+ elif [[ "$TERM" == "xterm"* ]] || [[ "$TERM" == "screen"* ]]; then
350
+ printf '\eOB'
351
+ else
352
+ # Generic terminal: scroll one line
353
+ tput ri 1 2>/dev/null || printf '\e[T'
354
+ fi
355
+ }
356
+
357
+ # Enhanced execute line widget that shows mode info
358
+ lacy_shell_execute_line_widget() {
359
+ local input="$BUFFER"
360
+
361
+ # If buffer is empty, just accept line normally
362
+ if [[ -z "$input" ]]; then
363
+ zle accept-line
364
+ return
365
+ fi
366
+
367
+ # Silent execution - mode shows in prompt
368
+
369
+ # Accept the line for normal processing
370
+ zle accept-line
371
+ }
372
+
373
+ # Interrupt handler for double Ctrl-C quit
374
+ lacy_shell_interrupt_handler() {
375
+ # Don't handle if disabled
376
+ if [[ "$LACY_SHELL_ENABLED" != true ]]; then
377
+ return 130
378
+ fi
379
+
380
+ # Get current time in milliseconds (portable method)
381
+ local current_time
382
+ if command -v gdate >/dev/null 2>&1; then
383
+ # macOS with GNU date installed
384
+ current_time=$(gdate +%s%3N)
385
+ elif [[ "$OSTYPE" == "darwin"* ]]; then
386
+ # macOS without GNU date - use python for milliseconds
387
+ current_time=$(python3 -c 'import time; print(int(time.time() * 1000))')
388
+ else
389
+ # Linux and other systems with GNU date
390
+ current_time=$(date +%s%3N)
391
+ fi
392
+
393
+ local time_diff=$(( current_time - LACY_SHELL_LAST_INTERRUPT_TIME ))
394
+
395
+ # Check if this is a double Ctrl+C within threshold
396
+ if [[ $time_diff -lt $LACY_SHELL_EXIT_TIMEOUT_MS ]]; then
397
+ # Double Ctrl+C detected - quit Lacy Shell
398
+ LACY_SHELL_QUITTING=true
399
+
400
+ # Remove precmd hooks IMMEDIATELY to prevent redraw
401
+ precmd_functions=(${precmd_functions:#lacy_shell_precmd})
402
+ precmd_functions=(${precmd_functions:#lacy_shell_update_prompt})
403
+
404
+ echo ""
405
+ lacy_shell_quit
406
+ return 130
407
+ else
408
+ # Single Ctrl+C - show hint
409
+ LACY_SHELL_LAST_INTERRUPT_TIME=$current_time
410
+ echo ""
411
+ lacy_print_color "$LACY_COLOR_NEUTRAL" "$LACY_MSG_CTRL_C_HINT"
412
+ return 130
413
+ fi
414
+ }
415
+
416
+ # Set up the interrupt handler
417
+ lacy_shell_setup_interrupt_handler() {
418
+ TRAPINT() {
419
+ # CRITICAL: Only intercept SIGINT when ZLE (the line editor) is active,
420
+ # i.e., the user is at the prompt. When a foreground child process is
421
+ # running (e.g., `lash`, `vim`, `python`), we must NOT intercept SIGINT
422
+ # — let it propagate to the child's process group normally. Without this
423
+ # guard, Ctrl+C, paste, and other keyboard shortcuts break in child
424
+ # processes because SIGINT never reaches them.
425
+ if [[ -z "$ZLE_STATE" ]]; then
426
+ # No ZLE active — a child process is running. Use default behavior.
427
+ return $(( 128 + $1 ))
428
+ fi
429
+
430
+ # Don't handle if already disabled
431
+ if [[ "$LACY_SHELL_ENABLED" != true ]]; then
432
+ return $(( 128 + $1 ))
433
+ fi
434
+
435
+ # Get current time
436
+ local current_time
437
+ if command -v gdate >/dev/null 2>&1; then
438
+ current_time=$(gdate +%s%3N)
439
+ elif [[ "$OSTYPE" == "darwin"* ]]; then
440
+ current_time=$(python3 -c 'import time; print(int(time.time() * 1000))')
441
+ else
442
+ current_time=$(date +%s%3N)
443
+ fi
444
+
445
+ local time_diff=$(( current_time - LACY_SHELL_LAST_INTERRUPT_TIME ))
446
+
447
+ if [[ $time_diff -lt $LACY_SHELL_EXIT_TIMEOUT_MS ]]; then
448
+ # Double Ctrl+C - quit
449
+ lacy_shell_quit
450
+ # After quitting, force prompt redraw (best-effort)
451
+ zle -I 2>/dev/null
452
+ zle -R 2>/dev/null
453
+ zle reset-prompt 2>/dev/null
454
+ # Remove this trap itself after quit
455
+ unfunction TRAPINT 2>/dev/null
456
+ return 130
457
+ else
458
+ # Single Ctrl+C
459
+ LACY_SHELL_LAST_INTERRUPT_TIME=$current_time
460
+ echo ""
461
+ lacy_print_color "$LACY_COLOR_NEUTRAL" "$LACY_MSG_CTRL_C_HINT"
462
+ return 130
463
+ fi
464
+ }
465
+ }
466
+
467
+ # EOF handler setup for Ctrl-D
468
+ lacy_shell_setup_eof_handler() {
469
+ # Prevent Ctrl-D from exiting the shell at all
470
+ # The widget will handle quitting lacy shell
471
+ setopt IGNORE_EOF
472
+ # Note: we intentionally do NOT export IGNOREEOF to the environment.
473
+ # Exporting it would leak into child processes (lash, vim, python, etc.)
474
+ # and alter their EOF handling behavior. The ZSH setopt above is sufficient
475
+ # for the interactive shell itself.
476
+ IGNOREEOF=1000
477
+ }
478
+
479
+ # Cleanup all keybindings
480
+ lacy_shell_cleanup_keybindings() {
481
+ # Restore keybindings we override
482
+ bindkey '^D' delete-char-or-list
483
+ bindkey '^@' set-mark-command
484
+ bindkey '^T' transpose-chars
485
+ bindkey '^U' kill-whole-line
486
+
487
+ # Restore suggestion accept bindings
488
+ bindkey '^[[C' forward-char
489
+ bindkey '^[OC' forward-char
490
+ bindkey '^I' expand-or-complete
491
+
492
+ # Remove hooks
493
+ zle -D zle-line-pre-redraw 2>/dev/null
494
+ zle -D zle-line-init 2>/dev/null
495
+
496
+ # Remove custom widgets
497
+ zle -D lacy_shell_toggle_mode_widget 2>/dev/null
498
+ zle -D lacy_shell_delete_char_or_quit_widget 2>/dev/null
499
+ zle -D _lacy_forward_char_or_accept 2>/dev/null
500
+ zle -D _lacy_expand_or_accept 2>/dev/null
501
+ }
502
+
503
+ # Accept ghost text suggestion into buffer.
504
+ # Called by right arrow and tab widgets below.
505
+ _lacy_try_accept_suggestion() {
506
+ if [[ -n "$LACY_SHELL_SUGGESTION" && -z "$BUFFER" ]]; then
507
+ BUFFER="$LACY_SHELL_SUGGESTION"
508
+ CURSOR=${#BUFFER}
509
+ LACY_SHELL_SUGGESTION=""
510
+ POSTDISPLAY=""
511
+ LACY_SHELL_OWN_POSTDISPLAY=false
512
+ return 0 # consumed — caller should NOT fall through
513
+ fi
514
+ return 1 # no ghost text — caller should fall through to default widget
515
+ }
516
+
517
+ # Right arrow: accept Lacy ghost text if present, otherwise delegate to
518
+ # forward-char (no dot prefix — lets autosuggestions' wrapper accept its
519
+ # own suggestion). See file header for why the dot prefix matters.
520
+ _lacy_forward_char_or_accept() {
521
+ _lacy_try_accept_suggestion || zle forward-char
522
+ }
523
+
524
+ # Tab: accept Lacy ghost text if present, otherwise delegate to
525
+ # expand-or-complete (no dot prefix — same reason as above).
526
+ _lacy_expand_or_accept() {
527
+ _lacy_try_accept_suggestion || zle expand-or-complete
528
+ }
529
+
530
+ # Register widgets
531
+ zle -N lacy_shell_toggle_mode_widget
532
+ zle -N lacy_shell_delete_char_or_quit_widget
533
+ zle -N _lacy_forward_char_or_accept
534
+ zle -N _lacy_expand_or_accept
535
+
536
+ # Alternative keybindings that don't conflict with system shortcuts
537
+ lacy_shell_setup_safe_keybindings() {
538
+ # Use Alt-based bindings that are less likely to conflict
539
+ bindkey '^[^M' lacy_shell_toggle_mode_widget # Alt+Enter
540
+ bindkey '^[1' lacy_shell_shell_mode_widget # Alt+1
541
+ bindkey '^[2' lacy_shell_agent_mode_widget # Alt+2
542
+ bindkey '^[3' lacy_shell_auto_mode_widget # Alt+3
543
+ bindkey '^[h' lacy_shell_help_widget # Alt+H
544
+
545
+ echo "Using safe keybindings:"
546
+ echo " Alt+Enter: Toggle mode"
547
+ echo " Alt+1: Shell mode"
548
+ echo " Alt+2: Agent mode"
549
+ echo " Alt+3: Auto mode"
550
+ echo " Alt+H: Help"
551
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env zsh
2
+
3
+ # Prompt handling for Lacy Shell
4
+ # - Real-time colored indicator: green (shell) vs magenta (agent)
5
+ # - Mode indicator in right prompt
6
+
7
+ # Store original prompts (captured later, after user's profile loads)
8
+ LACY_SHELL_ORIGINAL_PS1=""
9
+ LACY_SHELL_ORIGINAL_RPS1=""
10
+ LACY_SHELL_BASE_PS1=""
11
+ LACY_SHELL_PROMPT_INITIALIZED=false
12
+
13
+ # Setup prompt integration (called during init, but defers actual setup)
14
+ lacy_shell_setup_prompt() {
15
+ # Don't capture PS1 yet - wait for first precmd when user's prompt is ready
16
+ LACY_SHELL_PROMPT_INITIALIZED=false
17
+ }
18
+
19
+ # Actually initialize the prompt (called on first precmd)
20
+ lacy_shell_init_prompt_once() {
21
+ [[ "$LACY_SHELL_PROMPT_INITIALIZED" == true ]] && return
22
+
23
+ # Now capture the user's fully-loaded prompt
24
+ LACY_SHELL_ORIGINAL_PS1="$PS1"
25
+ LACY_SHELL_ORIGINAL_RPS1="$RPS1"
26
+ LACY_SHELL_BASE_PS1="$PS1"
27
+
28
+ # Initialize with neutral indicator (appended after prompt, before cursor)
29
+ LACY_SHELL_INPUT_TYPE="neutral"
30
+ PS1="${LACY_SHELL_BASE_PS1}%F{${LACY_COLOR_NEUTRAL}}${LACY_INDICATOR_CHAR}%f "
31
+
32
+ # Set right prompt with mode indicator
33
+ lacy_shell_update_rprompt
34
+
35
+ LACY_SHELL_PROMPT_INITIALIZED=true
36
+ }
37
+
38
+ # Update right prompt with mode indicator
39
+ lacy_shell_update_rprompt() {
40
+ local mode_text mode_color
41
+ case "$LACY_SHELL_CURRENT_MODE" in
42
+ "shell")
43
+ mode_text="SHELL"
44
+ mode_color="$LACY_COLOR_SHELL"
45
+ ;;
46
+ "agent")
47
+ mode_text="AGENT"
48
+ mode_color="$LACY_COLOR_AGENT"
49
+ ;;
50
+ "auto")
51
+ mode_text="AUTO"
52
+ mode_color="$LACY_COLOR_AUTO"
53
+ ;;
54
+ *)
55
+ mode_text="?"
56
+ mode_color="$LACY_COLOR_NEUTRAL"
57
+ ;;
58
+ esac
59
+
60
+ RPS1="%F{${mode_color}}${mode_text}%f %F{${LACY_COLOR_NEUTRAL}}[Ctrl+Space]%f"
61
+ }
62
+
63
+ # Update prompt (called by precmd)
64
+ lacy_shell_update_prompt() {
65
+ # Initialize on first call
66
+ lacy_shell_init_prompt_once
67
+
68
+ # Reset to neutral indicator (appended after prompt, before cursor)
69
+ LACY_SHELL_INPUT_TYPE="neutral"
70
+ PS1="${LACY_SHELL_BASE_PS1}%F{${LACY_COLOR_NEUTRAL}}${LACY_INDICATOR_CHAR}%f "
71
+
72
+ # Update mode in right prompt
73
+ lacy_shell_update_rprompt
74
+ }
75
+
76
+ # Restore original prompt
77
+ lacy_shell_restore_prompt() {
78
+ if [[ -n "$LACY_SHELL_ORIGINAL_PS1" ]]; then
79
+ PS1="$LACY_SHELL_ORIGINAL_PS1"
80
+ fi
81
+ if [[ -n "$LACY_SHELL_ORIGINAL_RPS1" ]]; then
82
+ RPS1="$LACY_SHELL_ORIGINAL_RPS1"
83
+ else
84
+ RPS1=""
85
+ fi
86
+ }
87
+
88
+ # Stubs for removed features
89
+ lacy_shell_remove_top_bar() { :; }
90
+ lacy_shell_show_top_bar_message() { :; }