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.
- package/.claude/settings.local.json +26 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- package/.github/SECURITY.md +32 -0
- package/.github/assets/logo-horizontal-dark.png +0 -0
- package/.github/assets/logo-horizontal-dark.svg +17 -0
- package/.github/assets/logo-horizontal.png +0 -0
- package/.github/assets/logo-horizontal.svg +17 -0
- package/.github/assets/logo.png +0 -0
- package/.github/assets/logo.svg +12 -0
- package/.github/assets/social-preview.png +0 -0
- package/.github/assets/social-preview.svg +50 -0
- package/.github/dependabot.yml +21 -0
- package/.github/workflows/ci.yml +80 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/CHANGELOG.md +366 -0
- package/CLAUDE.md +340 -0
- package/CONTRIBUTING.md +141 -0
- package/LICENSE +110 -0
- package/README.md +201 -31
- package/RELEASING.md +148 -0
- package/STYLE.md +202 -0
- package/assets/hero.jpeg +0 -0
- package/assets/mode-indicators.jpeg +0 -0
- package/assets/real-time-indicator.jpeg +0 -0
- package/assets/supported-tools.jpeg +0 -0
- package/bin/lacy +1028 -0
- package/docs/ADDING-BACKENDS.md +124 -0
- package/docs/DEVTO-ARTICLE.md +94 -0
- package/docs/DOCS.md +68 -0
- package/docs/GROWTH-STRATEGY.md +119 -0
- package/docs/HN-RESPONSES.md +122 -0
- package/docs/LAUNCH-COPY-FINAL.md +105 -0
- package/docs/MARKETING.md +411 -0
- package/docs/NATURAL_LANGUAGE_DETECTION.md +204 -0
- package/docs/UGC_VIDEO_SCRIPT.md +114 -0
- package/docs/articles/devto-how-i-made-my-terminal-understand-english.md +117 -0
- package/docs/demo-color-transition.gif +0 -0
- package/docs/demo-full.gif +0 -0
- package/docs/demo-indicator.gif +0 -0
- package/docs/launch-thread-may6.sh +158 -0
- package/docs/videos/README.md +189 -0
- package/docs/videos/generate_frames.py +510 -0
- package/docs/videos/generate_frames_v2.py +729 -0
- package/docs/videos/generate_short.py +328 -0
- package/docs/videos/generate_short_v2.py +526 -0
- package/docs/videos/lacy-shell-demo-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-demo.mp4 +0 -0
- package/docs/videos/lacy-shell-short-v2.mp4 +0 -0
- package/docs/videos/lacy-shell-short.mp4 +0 -0
- package/install.sh +1009 -0
- package/lacy.plugin.bash +75 -0
- package/lacy.plugin.fish +43 -0
- package/lacy.plugin.zsh +65 -0
- package/lib/animations.zsh +3 -0
- package/lib/bash/completions.bash +40 -0
- package/lib/bash/execute.bash +233 -0
- package/lib/bash/init.bash +40 -0
- package/lib/bash/keybindings.bash +134 -0
- package/lib/bash/prompt.bash +85 -0
- package/lib/commands/info.sh +25 -0
- package/lib/config.zsh +3 -0
- package/lib/constants.zsh +3 -0
- package/lib/core/animations.sh +271 -0
- package/lib/core/commands.sh +297 -0
- package/lib/core/config.sh +340 -0
- package/lib/core/constants.sh +366 -0
- package/lib/core/context.sh +260 -0
- package/lib/core/detection.sh +417 -0
- package/lib/core/mcp.sh +741 -0
- package/lib/core/modes.sh +123 -0
- package/lib/core/preheat.sh +496 -0
- package/lib/core/spinner.sh +174 -0
- package/lib/core/telemetry.sh +99 -0
- package/lib/detection.zsh +3 -0
- package/lib/execute.zsh +3 -0
- package/lib/fish/config.fish +66 -0
- package/lib/fish/detection.fish +90 -0
- package/lib/fish/execute.fish +105 -0
- package/lib/fish/keybindings.fish +42 -0
- package/lib/fish/prompt.fish +30 -0
- package/lib/keybindings.zsh +3 -0
- package/lib/mcp.zsh +3 -0
- package/lib/modes.zsh +3 -0
- package/lib/preheat.zsh +3 -0
- package/lib/prompt.zsh +3 -0
- package/lib/spinner.zsh +3 -0
- package/lib/zsh/completions.zsh +60 -0
- package/lib/zsh/execute.zsh +294 -0
- package/lib/zsh/init.zsh +26 -0
- package/lib/zsh/keybindings.zsh +551 -0
- package/lib/zsh/prompt.zsh +90 -0
- package/package.json +42 -27
- package/packages/lacy/README.md +61 -0
- package/packages/lacy/commands/info.sh +25 -0
- package/{index.mjs → packages/lacy/index.mjs} +247 -20
- package/packages/lacy/package-lock.json +71 -0
- package/packages/lacy/package.json +42 -0
- package/script/release.ts +487 -0
- package/squirrel.toml +36 -0
- package/tests/test_bash.bash +163 -0
- package/tests/test_core.sh +607 -0
- package/tests/test_gemini.sh +119 -0
- package/tests/test_gemini_mcp.sh +126 -0
- package/tests/test_preheat_server.zsh +446 -0
- 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() { :; }
|