oops-cli 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.ja.md +14 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +21 -0
- data/README.ja.md +236 -0
- data/README.md +236 -0
- data/exe/oops +5 -0
- data/lib/oops/ansi.rb +52 -0
- data/lib/oops/cli.rb +186 -0
- data/lib/oops/clipboard.rb +218 -0
- data/lib/oops/config.rb +72 -0
- data/lib/oops/doctor.rb +133 -0
- data/lib/oops/formatter.rb +115 -0
- data/lib/oops/marker.rb +88 -0
- data/lib/oops/paths.rb +28 -0
- data/lib/oops/prompt_sp.rb +74 -0
- data/lib/oops/record.rb +41 -0
- data/lib/oops/recorder.rb +416 -0
- data/lib/oops/secrets.rb +85 -0
- data/lib/oops/session_log.rb +213 -0
- data/lib/oops/shell/oops.bash +336 -0
- data/lib/oops/shell/oops.zsh +102 -0
- data/lib/oops/shell_init.rb +26 -0
- data/lib/oops/stream_parser.rb +347 -0
- data/lib/oops/tty.rb +19 -0
- data/lib/oops/version.rb +3 -0
- data/lib/oops.rb +17 -0
- metadata +71 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# oops: bash hooks (bash 3.2 or later). Put this line in ~/.bashrc (or
|
|
2
|
+
# ~/.bash_profile, if that is where your settings live), after whatever puts
|
|
3
|
+
# oops's Ruby on PATH and as early as possible after that:
|
|
4
|
+
#
|
|
5
|
+
# eval "$(oops init bash)"
|
|
6
|
+
#
|
|
7
|
+
# On a terminal that is not recorded yet, the shell is started again under
|
|
8
|
+
# `oops record` (a pty relay). Inside the recorded shell the hooks write
|
|
9
|
+
# invisible APC markers around every command line:
|
|
10
|
+
#
|
|
11
|
+
# ESC _ oops;start;<seq>;<epoch>;<base64 cwd>;<base64 cmd> ESC \
|
|
12
|
+
# ESC _ oops;end;<seq>;<exit> ESC \
|
|
13
|
+
#
|
|
14
|
+
# "Recorded" means $OOPS_TTY (set by the relay) names this very terminal, so
|
|
15
|
+
# shells started from a recorded one on another tty (tmux, GUI terminals)
|
|
16
|
+
# get recorded on their own, and `exec bash` on the same tty is not recorded
|
|
17
|
+
# twice. Markers go straight to the terminal device: inside `{ ...; } >file`
|
|
18
|
+
# the DEBUG trap runs with stdout redirected.
|
|
19
|
+
#
|
|
20
|
+
# The preexec hook uses the bash-preexec technique (DEBUG trap armed by the
|
|
21
|
+
# last PROMPT_COMMAND entry), inlined and reduced to what oops needs.
|
|
22
|
+
if [[ $- == *i* ]] && [ -t 0 ] && [ -t 1 ] && [ -z "${OOPS_DISABLE:-}" ] &&
|
|
23
|
+
[ -z "${BASH_EXECUTION_STRING:-}" ]; then
|
|
24
|
+
__oops_bin=__OOPS_BIN__
|
|
25
|
+
__oops_tty=$(tty 2>/dev/null || :)
|
|
26
|
+
|
|
27
|
+
# True when bash was started to run a script (`bash -i script`). bash 4+
|
|
28
|
+
# already has the script in $0 while the rc file runs; bash 3.2 still has
|
|
29
|
+
# "bash" there, so look at the shell's own argv.
|
|
30
|
+
__oops_runs_script() {
|
|
31
|
+
local args word skip= after_dashes= found= noglob=
|
|
32
|
+
case ${0##*/} in
|
|
33
|
+
bash | -bash | sh | -sh) ;;
|
|
34
|
+
*) if [ -f "$0" ] && ! [ "$0" -ef "$BASH" ]; then return 0; fi ;;
|
|
35
|
+
esac
|
|
36
|
+
args=$(ps -ww -o args= -p $$ 2>/dev/null) || return 1
|
|
37
|
+
case $args in
|
|
38
|
+
"$0 "*) args=${args#"$0 "} ;;
|
|
39
|
+
*) return 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
case $- in *f*) noglob=1 ;; esac
|
|
42
|
+
set -f
|
|
43
|
+
for word in $args; do
|
|
44
|
+
if [ -n "$after_dashes" ]; then
|
|
45
|
+
found=1
|
|
46
|
+
break
|
|
47
|
+
elif [ -n "$skip" ]; then
|
|
48
|
+
skip=
|
|
49
|
+
continue
|
|
50
|
+
fi
|
|
51
|
+
case $word in
|
|
52
|
+
--rcfile | --init-file | -o | +o | -O | +O) skip=1 ;;
|
|
53
|
+
--) after_dashes=1 ;;
|
|
54
|
+
-* | +*) ;;
|
|
55
|
+
*)
|
|
56
|
+
found=1
|
|
57
|
+
break
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
[ -n "$noglob" ] || set +f
|
|
62
|
+
[ -n "$found" ]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Without a tty name (tty(1) failed) this terminal cannot be told apart
|
|
66
|
+
# from a recorded one, so the shell is left alone: no restart, no hooks.
|
|
67
|
+
if [ -n "$__oops_tty" ] && [ "${OOPS_TTY:-}" != "$__oops_tty" ]; then
|
|
68
|
+
if __oops_runs_script; then
|
|
69
|
+
: # not a prompt to record; run the script as asked
|
|
70
|
+
elif command -v "$__oops_bin" >/dev/null 2>&1; then
|
|
71
|
+
# execfail: if the exec fails anyway, keep this shell running.
|
|
72
|
+
__oops_execfail=$(shopt -p execfail || :)
|
|
73
|
+
shopt -s execfail
|
|
74
|
+
if shopt -q login_shell; then
|
|
75
|
+
exec "$__oops_bin" record -- "$BASH" -l
|
|
76
|
+
else
|
|
77
|
+
exec "$__oops_bin" record -- "$BASH"
|
|
78
|
+
fi
|
|
79
|
+
eval "$__oops_execfail"
|
|
80
|
+
unset __oops_execfail
|
|
81
|
+
else
|
|
82
|
+
echo "oops: cannot run $__oops_bin; recording disabled" >&2
|
|
83
|
+
fi
|
|
84
|
+
elif [ -n "$__oops_tty" ]; then
|
|
85
|
+
# Our PROMPT_COMMAND entries. Each keeps $? and $_ for the entries after
|
|
86
|
+
# it: a hook returns the status, and `&& __oops_ok "$_"` (run only when
|
|
87
|
+
# it is 0) puts $_ back. A non-zero status inside an && list does not
|
|
88
|
+
# trigger `set -e`.
|
|
89
|
+
__oops_e_pre='__oops_precmd "$_" && __oops_ok "$_"'
|
|
90
|
+
__oops_e_pass='__oops_pass "$_" && __oops_ok "$_"'
|
|
91
|
+
__oops_e_arm='__oops_arm'
|
|
92
|
+
# The DEBUG trap is read and set here, not inside __oops_install: in a
|
|
93
|
+
# function the DEBUG trap reads as unset, and bash puts back the one the
|
|
94
|
+
# function started with when it returns.
|
|
95
|
+
__oops_e_install='__oops_install "$(trap -p DEBUG)" && __oops_ok; [ -z "${__oops_trap:-}" ] || trap -- "$__oops_trap" DEBUG'
|
|
96
|
+
|
|
97
|
+
__oops_ok() { return 0; }
|
|
98
|
+
__oops_b64() { printf '%s' "$1" | base64 | tr -d '\n'; }
|
|
99
|
+
|
|
100
|
+
# $1: $BASH_COMMAND, the first simple command of the line. The full line
|
|
101
|
+
# is the newest history entry, but only if the history grew since the
|
|
102
|
+
# prompt; otherwise the line was not saved (HISTCONTROL, HISTIGNORE,
|
|
103
|
+
# history off) and the entry belongs to an older command.
|
|
104
|
+
__oops_preexec() {
|
|
105
|
+
local line num
|
|
106
|
+
line=$(HISTTIMEFORMAT='' builtin history 1)
|
|
107
|
+
num=${line#"${line%%[0-9]*}"}
|
|
108
|
+
num=${num%%[!0-9]*}
|
|
109
|
+
if [ -n "$num" ] && [ "$num" != "$__oops_hist" ]; then
|
|
110
|
+
line=${line#*[0-9][* ] }
|
|
111
|
+
else
|
|
112
|
+
line=$1
|
|
113
|
+
fi
|
|
114
|
+
__OOPS_SEQ=$((__OOPS_SEQ + 1))
|
|
115
|
+
__oops_open=$__OOPS_SEQ
|
|
116
|
+
printf '\033_oops;start;%s;%s;%s;%s\033\\' "$__OOPS_SEQ" "$(date +%s)" \
|
|
117
|
+
"$(__oops_b64 "$PWD")" "$(__oops_b64 "${line:0:8192}")" 2>/dev/null >"$__oops_tty" || :
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# The DEBUG trap runs before every simple command; only the first one
|
|
121
|
+
# after a prompt starts a record. $1 is handed back as the status (for a
|
|
122
|
+
# chained trap) and $2 restores $_, which calling a function overwrites.
|
|
123
|
+
# READLINE_LINE is set while a `bind -x` widget (fzf, atuin) runs; such
|
|
124
|
+
# commands are not the user's line. bash 3.2 does not set it, so there a
|
|
125
|
+
# widget's command takes the record of the line typed after it.
|
|
126
|
+
__oops_debug() {
|
|
127
|
+
if [ -n "$__oops_armed" ] && [ -z "${COMP_LINE:-}" ] && [ -z "${READLINE_LINE+x}" ] &&
|
|
128
|
+
[ "${BASH_SUBSHELL:-0}" = 0 ]; then
|
|
129
|
+
case $BASH_COMMAND in
|
|
130
|
+
__oops_precmd*) ;; # empty line: the prompt is drawn again
|
|
131
|
+
*)
|
|
132
|
+
case $__oops_tail in
|
|
133
|
+
*"$BASH_COMMAND"*) ;; # entries after a rewrap, see __oops_wrap
|
|
134
|
+
*)
|
|
135
|
+
__oops_armed=
|
|
136
|
+
__oops_tail=
|
|
137
|
+
__oops_preexec "$BASH_COMMAND"
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
;;
|
|
141
|
+
esac
|
|
142
|
+
fi
|
|
143
|
+
return "$1"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# First PROMPT_COMMAND entry: $? must be read before anything else runs.
|
|
147
|
+
__oops_precmd() {
|
|
148
|
+
local exit_status=$?
|
|
149
|
+
__oops_armed=
|
|
150
|
+
if [ -n "$__oops_open" ]; then
|
|
151
|
+
printf '\033_oops;end;%s;%s\033\\' "$__oops_open" "$exit_status" 2>/dev/null >"$__oops_tty" || :
|
|
152
|
+
__oops_open=
|
|
153
|
+
fi
|
|
154
|
+
__oops_status=$exit_status
|
|
155
|
+
return "$exit_status"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Stands in for an entry of ours that is no longer in its place.
|
|
159
|
+
__oops_pass() { return "${__oops_status:-0}"; }
|
|
160
|
+
|
|
161
|
+
__oops_pc_is_array() {
|
|
162
|
+
case $(declare -p PROMPT_COMMAND 2>/dev/null) in
|
|
163
|
+
'declare -a'*) return 0 ;;
|
|
164
|
+
esac
|
|
165
|
+
return 1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# True when __oops_precmd is the first entry and __oops_arm the last.
|
|
169
|
+
__oops_wrapped() {
|
|
170
|
+
local nl=$'\n' i last=
|
|
171
|
+
case ${PROMPT_COMMAND:-} in
|
|
172
|
+
"$__oops_e_pre$nl"*"$nl$__oops_e_arm") return 0 ;;
|
|
173
|
+
esac
|
|
174
|
+
__oops_pc_is_array || return 1
|
|
175
|
+
case ${PROMPT_COMMAND:-} in "$__oops_e_pre$nl"*) ;; *) return 1 ;; esac
|
|
176
|
+
for i in "${!PROMPT_COMMAND[@]}"; do last=$i; done
|
|
177
|
+
[ "${PROMPT_COMMAND[$last]}" = "$__oops_e_arm" ]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Puts __oops_precmd first and __oops_arm last in PROMPT_COMMAND (an
|
|
181
|
+
# array in bash 5.1+); our entries found elsewhere become __oops_pass, so
|
|
182
|
+
# text around them stays valid. $1 is the entry running now: the entries
|
|
183
|
+
# after it still run in this prompt, before __oops_arm, so their text is
|
|
184
|
+
# kept in __oops_tail and skipped by the DEBUG trap.
|
|
185
|
+
__oops_wrap() {
|
|
186
|
+
local cur=$1 nl=$'\n' sep=$' \t\n;' i elem first= seen= tail=
|
|
187
|
+
if __oops_pc_is_array; then
|
|
188
|
+
for i in "${!PROMPT_COMMAND[@]}"; do
|
|
189
|
+
[ -n "$first" ] || first=$i
|
|
190
|
+
elem=${PROMPT_COMMAND[$i]}
|
|
191
|
+
if [ -n "$seen" ]; then
|
|
192
|
+
tail="$tail$nl$elem"
|
|
193
|
+
else
|
|
194
|
+
case $elem in *"$cur"*) seen=1 tail=${elem#*"$cur"} ;; esac
|
|
195
|
+
fi
|
|
196
|
+
elem=${elem//"$__oops_e_install"/"$__oops_e_pass"}
|
|
197
|
+
elem=${elem//"$__oops_e_pre"/"$__oops_e_pass"}
|
|
198
|
+
PROMPT_COMMAND[$i]=${elem//"$__oops_e_arm"/"$__oops_e_pass"}
|
|
199
|
+
done
|
|
200
|
+
PROMPT_COMMAND[$first]="$__oops_e_pre$nl${PROMPT_COMMAND[$first]}"
|
|
201
|
+
PROMPT_COMMAND+=("$__oops_e_arm")
|
|
202
|
+
else
|
|
203
|
+
elem=${PROMPT_COMMAND:-}
|
|
204
|
+
case $elem in *"$cur"*) tail=${elem#*"$cur"} ;; esac
|
|
205
|
+
elem=${elem//"$__oops_e_install"/"$__oops_e_pass"}
|
|
206
|
+
elem=${elem//"$__oops_e_pre"/"$__oops_e_pass"}
|
|
207
|
+
elem=${elem//"$__oops_e_arm"/"$__oops_e_pass"}
|
|
208
|
+
PROMPT_COMMAND="$__oops_e_pre$nl$elem$nl$__oops_e_arm"
|
|
209
|
+
fi
|
|
210
|
+
case $tail in *[!"$sep"]*) __oops_tail=$tail ;; *) __oops_tail= ;; esac
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
__oops_history_number() {
|
|
214
|
+
local line
|
|
215
|
+
line=$(HISTTIMEFORMAT='' builtin history 1)
|
|
216
|
+
line=${line#"${line%%[0-9]*}"}
|
|
217
|
+
__oops_hist=${line%%[!0-9]*}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Last PROMPT_COMMAND entry: the next simple command is the user's. A
|
|
221
|
+
# sourced rc file may have put entries before or after ours; wrap again.
|
|
222
|
+
__oops_arm() {
|
|
223
|
+
__oops_tail=
|
|
224
|
+
__oops_wrapped || __oops_wrap "$__oops_e_arm"
|
|
225
|
+
__oops_history_number
|
|
226
|
+
__oops_armed=1
|
|
227
|
+
return 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Runs at the first prompt, after the rest of the rc file, so hooks and
|
|
231
|
+
# DEBUG traps set up later in it are kept. $1: `trap -p DEBUG`.
|
|
232
|
+
__oops_install() {
|
|
233
|
+
local prev=$1
|
|
234
|
+
if __oops_bp_present; then
|
|
235
|
+
__oops_use_bp
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
__oops_wrap "$__oops_e_install"
|
|
239
|
+
case $prev in
|
|
240
|
+
*__oops_debug*) __oops_trap= ;;
|
|
241
|
+
'') __oops_trap='__oops_debug 0 "$_"' ;;
|
|
242
|
+
*)
|
|
243
|
+
eval "set -- $prev"
|
|
244
|
+
__oops_trap="__oops_debug \"\$?\" \"\$_\" && __oops_ok \"\$_\"; $3"
|
|
245
|
+
;;
|
|
246
|
+
esac
|
|
247
|
+
__oops_history_number
|
|
248
|
+
__oops_armed=1
|
|
249
|
+
return 0
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# --- bash-preexec ---
|
|
253
|
+
#
|
|
254
|
+
# bash-preexec (behind atuin, Ghostty's bash integration and others) needs
|
|
255
|
+
# the same two things as the hooks above: the DEBUG trap, and its
|
|
256
|
+
# `__bp_interactive_mode` entry as the last PROMPT_COMMAND entry. Both of
|
|
257
|
+
# us wrapping PROMPT_COMMAND on every prompt broke recording (our arm
|
|
258
|
+
# entry used up its flag, and our entries piled up, which made its string
|
|
259
|
+
# clean-up crawl in bash 3.2). So when it is loaded, oops stops using its own
|
|
260
|
+
# DEBUG trap and PROMPT_COMMAND entries and registers with its arrays.
|
|
261
|
+
|
|
262
|
+
__oops_bp_present() { declare -F __bp_install >/dev/null 2>&1; }
|
|
263
|
+
|
|
264
|
+
# bash-preexec runs this with the line from history. When the line was
|
|
265
|
+
# not saved (HISTCONTROL, HISTIGNORE) that is an older line, and oops
|
|
266
|
+
# has nothing better than a placeholder: the DEBUG trap's $BASH_COMMAND
|
|
267
|
+
# is out of reach here.
|
|
268
|
+
__oops_bp_preexec() { __oops_preexec "(not saved in history)"; }
|
|
269
|
+
|
|
270
|
+
# bash-preexec restores $? before it calls each precmd function.
|
|
271
|
+
__oops_bp_precmd() {
|
|
272
|
+
__oops_precmd
|
|
273
|
+
local exit_status=$?
|
|
274
|
+
__oops_history_number
|
|
275
|
+
return "$exit_status"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Drops our first-prompt entry from PROMPT_COMMAND; it has done its job.
|
|
279
|
+
# ":" is what bash-preexec cleans away itself.
|
|
280
|
+
__oops_forget_install_entry() {
|
|
281
|
+
local i
|
|
282
|
+
if __oops_pc_is_array; then
|
|
283
|
+
for i in "${!PROMPT_COMMAND[@]}"; do
|
|
284
|
+
PROMPT_COMMAND[$i]=${PROMPT_COMMAND[$i]//"$__oops_e_install"/:}
|
|
285
|
+
done
|
|
286
|
+
else
|
|
287
|
+
PROMPT_COMMAND=${PROMPT_COMMAND:-}
|
|
288
|
+
PROMPT_COMMAND=${PROMPT_COMMAND//"$__oops_e_install"/:}
|
|
289
|
+
fi
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
__oops_use_bp() {
|
|
293
|
+
__oops_bp=1
|
|
294
|
+
__oops_armed=
|
|
295
|
+
case " ${precmd_functions[*]:-} " in
|
|
296
|
+
*" __oops_bp_precmd "*) ;;
|
|
297
|
+
*) precmd_functions=(__oops_bp_precmd ${precmd_functions[@]+"${precmd_functions[@]}"}) ;;
|
|
298
|
+
esac
|
|
299
|
+
case " ${preexec_functions[*]:-} " in
|
|
300
|
+
*" __oops_bp_preexec "*) ;;
|
|
301
|
+
*) preexec_functions+=(__oops_bp_preexec) ;;
|
|
302
|
+
esac
|
|
303
|
+
__oops_forget_install_entry
|
|
304
|
+
__oops_history_number
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Once per shell process; sourcing ~/.bashrc again must not redo this.
|
|
308
|
+
if [ -z "${__oops_hooked:-}" ]; then
|
|
309
|
+
__oops_hooked=1
|
|
310
|
+
__oops_open=
|
|
311
|
+
__oops_armed=
|
|
312
|
+
__oops_tail=
|
|
313
|
+
__oops_hist=
|
|
314
|
+
__oops_trap=
|
|
315
|
+
case ${__OOPS_SEQ:-} in '' | *[!0-9]*) __OOPS_SEQ=0 ;; esac
|
|
316
|
+
# Same pid as the shell that set __OOPS_PID: this shell replaced it
|
|
317
|
+
# with `exec`, so that command (still open in the relay) is done.
|
|
318
|
+
if [ "${__OOPS_PID:-}" = "$$" ] && [ "$__OOPS_SEQ" != 0 ]; then
|
|
319
|
+
printf '\033_oops;end;%s;%s\033\\' "$__OOPS_SEQ" 0 2>/dev/null >"$__oops_tty" || :
|
|
320
|
+
fi
|
|
321
|
+
# Exported so a nested shell on this tty continues the numbering.
|
|
322
|
+
export __OOPS_SEQ
|
|
323
|
+
export __OOPS_PID=$$
|
|
324
|
+
fi
|
|
325
|
+
if __oops_bp_present; then
|
|
326
|
+
__oops_use_bp
|
|
327
|
+
else
|
|
328
|
+
# Again after a sourced rc file replaced PROMPT_COMMAND.
|
|
329
|
+
case ${PROMPT_COMMAND:-} in
|
|
330
|
+
*__oops_precmd* | *__oops_install*) ;;
|
|
331
|
+
*) PROMPT_COMMAND="${PROMPT_COMMAND:-}"$'\n'"$__oops_e_install" ;;
|
|
332
|
+
esac
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
unset -f __oops_runs_script
|
|
336
|
+
fi
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# oops: zsh hooks. Put this line in ~/.zshrc, after whatever puts oops's Ruby
|
|
2
|
+
# on PATH and as early as possible after that:
|
|
3
|
+
#
|
|
4
|
+
# eval "$(oops init zsh)"
|
|
5
|
+
#
|
|
6
|
+
# On a terminal that is not recorded yet, the shell is started again under
|
|
7
|
+
# `oops record` (a pty relay). Inside the recorded shell the hooks write
|
|
8
|
+
# invisible APC markers around every command:
|
|
9
|
+
#
|
|
10
|
+
# ESC _ oops;start;<seq>;<epoch>;<base64 cwd>;<base64 cmd> ESC \
|
|
11
|
+
# ESC _ oops;end;<seq>;<exit> ESC \
|
|
12
|
+
#
|
|
13
|
+
# "Recorded" means $OOPS_TTY (set by the relay) names this very terminal, so
|
|
14
|
+
# shells started from a recorded one on another tty (tmux, GUI terminals)
|
|
15
|
+
# get recorded on their own, and `exec zsh` on the same tty is not recorded
|
|
16
|
+
# twice. Markers go straight to the terminal device, so a redirected stdout
|
|
17
|
+
# never receives them.
|
|
18
|
+
# `zsh -ic 'cmd'` and `zsh -i script` are left alone so the command runs.
|
|
19
|
+
if [[ -o interactive && -t 0 && -t 1 && -n "${TTY:-}" && -z "${OOPS_DISABLE:-}" &&
|
|
20
|
+
-z "${ZSH_EXECUTION_STRING:-}" && -z "${ZSH_SCRIPT:-}" ]]; then
|
|
21
|
+
__oops_bin=__OOPS_BIN__
|
|
22
|
+
if [[ "${OOPS_TTY:-}" != "$TTY" ]]; then
|
|
23
|
+
# A failed exec ends zsh, so check first; without the binary the shell
|
|
24
|
+
# simply runs unrecorded.
|
|
25
|
+
if command -v "$__oops_bin" >/dev/null 2>&1; then
|
|
26
|
+
# Restart this same zsh. A bare name (a login shell's "-zsh") could
|
|
27
|
+
# find a different zsh on PATH, so a login shell uses $SHELL, the
|
|
28
|
+
# login shell it was started as, and other bare names go through
|
|
29
|
+
# `command -v`.
|
|
30
|
+
__oops_zsh=${${ZSH_ARGZERO#-}:-zsh}
|
|
31
|
+
if [[ "$__oops_zsh" != */* ]]; then
|
|
32
|
+
if [[ "$ZSH_ARGZERO" == -* && "${SHELL:-}" == */"$__oops_zsh" && -x "$SHELL" ]]; then
|
|
33
|
+
__oops_zsh=$SHELL
|
|
34
|
+
else
|
|
35
|
+
__oops_zsh=${$(command -v -- "$__oops_zsh" 2>/dev/null):-$__oops_zsh}
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
38
|
+
# Ghostty injects its shell integration through ZDOTDIR and removes the
|
|
39
|
+
# injection again, so the restarted shell would not get it. Note
|
|
40
|
+
# whether this one has it (the integration sets _ghostty_state), so
|
|
41
|
+
# the restarted shell can load it again; see the end of this block.
|
|
42
|
+
if (( $+_ghostty_state )); then export __OOPS_GHOSTTY=1; else unset __OOPS_GHOSTTY; fi
|
|
43
|
+
exec "$__oops_bin" record -- "$__oops_zsh"
|
|
44
|
+
else
|
|
45
|
+
print -ru2 -- "oops: cannot run $__oops_bin; recording disabled"
|
|
46
|
+
fi
|
|
47
|
+
else
|
|
48
|
+
zmodload zsh/datetime 2>/dev/null
|
|
49
|
+
autoload -Uz add-zsh-hook
|
|
50
|
+
|
|
51
|
+
# The ";" before "}" keeps this valid with IGNORE_CLOSE_BRACES.
|
|
52
|
+
__oops_b64() { printf '%s' "$1" | base64 | tr -d '\n'; }
|
|
53
|
+
|
|
54
|
+
# $1 is the line as typed (empty if history is off; $3 is the full text
|
|
55
|
+
# that runs). Very long lines are cut so the marker stays small.
|
|
56
|
+
__oops_preexec() {
|
|
57
|
+
emulate -L zsh
|
|
58
|
+
local cmd=${1:-$3}
|
|
59
|
+
__OOPS_SEQ=$(( __OOPS_SEQ + 1 ))
|
|
60
|
+
__oops_open=$__OOPS_SEQ
|
|
61
|
+
printf '\033_oops;start;%s;%s;%s;%s\033\\' "$__OOPS_SEQ" "${EPOCHSECONDS:-$(date +%s)}" \
|
|
62
|
+
"$(__oops_b64 "$PWD")" "$(__oops_b64 "${cmd[1,8192]}")" 2>/dev/null >"$TTY"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# $? must be read before anything else runs; it is handed back unchanged.
|
|
66
|
+
__oops_precmd() {
|
|
67
|
+
local exit_status=$?
|
|
68
|
+
emulate -L zsh
|
|
69
|
+
if [[ -n "$__oops_open" ]]; then
|
|
70
|
+
printf '\033_oops;end;%s;%s\033\\' "$__oops_open" "$exit_status" 2>/dev/null >"$TTY"
|
|
71
|
+
__oops_open=
|
|
72
|
+
fi
|
|
73
|
+
return $exit_status
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Once per shell process; sourcing ~/.zshrc again must not redo this.
|
|
77
|
+
if [[ -z "${__oops_hooked:-}" ]]; then
|
|
78
|
+
__oops_hooked=1
|
|
79
|
+
__oops_open=
|
|
80
|
+
case "${__OOPS_SEQ:-}" in ('' | *[!0-9]*) __OOPS_SEQ=0 ;; esac
|
|
81
|
+
# Same pid as the shell that set __OOPS_PID: this shell replaced it
|
|
82
|
+
# with `exec`, so that command (still open in the relay) is done.
|
|
83
|
+
if [[ "${__OOPS_PID:-}" == "$$" && "$__OOPS_SEQ" != 0 ]]; then
|
|
84
|
+
printf '\033_oops;end;%s;%s\033\\' "$__OOPS_SEQ" 0 2>/dev/null >"$TTY"
|
|
85
|
+
fi
|
|
86
|
+
# Exported so a nested shell on this tty continues the numbering.
|
|
87
|
+
export __OOPS_SEQ
|
|
88
|
+
export __OOPS_PID=$$
|
|
89
|
+
fi
|
|
90
|
+
add-zsh-hook preexec __oops_preexec
|
|
91
|
+
add-zsh-hook precmd __oops_precmd
|
|
92
|
+
|
|
93
|
+
# The shell this one replaced had Ghostty's integration (see above); load
|
|
94
|
+
# it here too, as Ghostty documents for a manual setup. It does nothing
|
|
95
|
+
# when it is already loaded. This comes after the hooks above, so
|
|
96
|
+
# __oops_precmd stays the first precmd hook and sees $?.
|
|
97
|
+
if [[ -n "${__OOPS_GHOSTTY:-}" && -n "${GHOSTTY_RESOURCES_DIR:-}" &&
|
|
98
|
+
-r "$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration" ]]; then
|
|
99
|
+
builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration"
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Oops
|
|
2
|
+
# The snippets printed by `oops init zsh|bash`, meant for
|
|
3
|
+
# `eval "$(oops init zsh)"` in the rc file, after whatever puts oops's Ruby
|
|
4
|
+
# on PATH and as early as possible after that. The templates live in
|
|
5
|
+
# lib/oops/shell/; the absolute path of the oops executable is written into
|
|
6
|
+
# them, so a later change to PATH cannot break the hooks.
|
|
7
|
+
module ShellInit
|
|
8
|
+
SHELLS = %w[zsh bash].freeze
|
|
9
|
+
TEMPLATE_DIR = File.expand_path("shell", __dir__)
|
|
10
|
+
BIN_PLACEHOLDER = "__OOPS_BIN__".freeze
|
|
11
|
+
|
|
12
|
+
def self.snippet(shell, bin: "oops")
|
|
13
|
+
raise ArgumentError, "unsupported shell: #{shell}" unless SHELLS.include?(shell)
|
|
14
|
+
|
|
15
|
+
template = File.read(File.join(TEMPLATE_DIR, "oops.#{shell}"), encoding: Encoding::UTF_8)
|
|
16
|
+
template.gsub(BIN_PLACEHOLDER) { shell_quote(bin) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# One single-quoted word for both shells: ' becomes '\''.
|
|
20
|
+
def self.shell_quote(text)
|
|
21
|
+
"'#{text.to_s.gsub("'") { "'\\''" }}'"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private_class_method :shell_quote
|
|
25
|
+
end
|
|
26
|
+
end
|