1use crate::cli::Shell;
2
3#[must_use]
5pub fn get_hook_script(shell: Shell) -> &'static str {
6 match shell {
7 Shell::Bash => BASH_HOOK,
8 Shell::Zsh => ZSH_HOOK,
9 Shell::Fish => FISH_HOOK,
10 }
11}
12
13#[must_use]
15pub fn get_init_command(shell: Shell) -> &'static str {
16 match shell {
17 Shell::Bash => r#"eval "$(rec init bash)""#,
18 Shell::Zsh => r#"eval "$(rec init zsh)""#,
19 Shell::Fish => r"rec init fish | source",
20 }
21}
22
23pub const BASH_HOOK: &str = r#"
28# rec shell hooks for Bash
29# Add to ~/.bashrc: eval "$(rec init bash)"
30
31# Shell wrapper: automatically manages REC_RECORDING for start/stop
32rec() {
33 if [[ "${1:-}" == "start" ]]; then
34 command rec "$@" && export REC_RECORDING=1
35 elif [[ "${1:-}" == "stop" ]]; then
36 command rec "$@" && unset REC_RECORDING
37 else
38 command rec "$@"
39 fi
40}
41
42# Source bash-preexec if not already loaded
43if [[ -z "${bash_preexec_imported:-}" ]]; then
44 # bash-preexec is output first by rec init bash
45 :
46fi
47
48# Capture command before execution
49__rec_preexec() {
50 # $1 is the command (aliases expanded by bash-preexec)
51 if [[ -n "${REC_RECORDING:-}" ]]; then
52 rec _hook preexec "$1" 2>/dev/null || true
53 fi
54}
55
56# Capture exit code after command completes
57__rec_precmd() {
58 local exit_code=$?
59 if [[ -n "${REC_RECORDING:-}" ]]; then
60 rec _hook precmd "$exit_code" 2>/dev/null || true
61 fi
62}
63
64# Register hooks with bash-preexec
65preexec_functions+=(__rec_preexec)
66precmd_functions+=(__rec_precmd)
67
68# Recording indicator for prompt (optional, disable with REC_NO_PROMPT=1)
69__rec_prompt_indicator() {
70 if [[ -n "${REC_RECORDING:-}" && -z "${REC_NO_PROMPT:-}" ]]; then
71 printf '\[\e[31m\]● \[\e[0m\]'
72 fi
73}
74
75# Prepend indicator to PS1 if not disabled
76if [[ -z "${REC_NO_PROMPT:-}" ]]; then
77 PS1='$(__rec_prompt_indicator)'"${PS1}"
78fi
79"#;
80
81pub const ZSH_HOOK: &str = r#"
86# rec shell hooks for Zsh
87# Add to ~/.zshrc: eval "$(rec init zsh)"
88
89# Shell wrapper: automatically manages REC_RECORDING for start/stop
90rec() {
91 if [[ "${1:-}" == "start" ]]; then
92 command rec "$@" && export REC_RECORDING=1
93 elif [[ "${1:-}" == "stop" ]]; then
94 command rec "$@" && unset REC_RECORDING
95 else
96 command rec "$@"
97 fi
98}
99
100autoload -Uz add-zsh-hook
101
102# Capture command before execution
103# $1 = typed command, $2 = expanded aliases only, $3 = full expansion
104__rec_preexec() {
105 if [[ -n "${REC_RECORDING:-}" ]]; then
106 rec _hook preexec "$3" 2>/dev/null || true
107 fi
108}
109
110# Capture exit code after command completes
111__rec_precmd() {
112 local exit_code=$?
113 if [[ -n "${REC_RECORDING:-}" ]]; then
114 rec _hook precmd "$exit_code" 2>/dev/null || true
115 fi
116}
117
118# Register hooks
119add-zsh-hook preexec __rec_preexec
120add-zsh-hook precmd __rec_precmd
121
122# Recording indicator for prompt (optional, disable with REC_NO_PROMPT=1)
123__rec_prompt_indicator() {
124 if [[ -n "${REC_RECORDING:-}" && -z "${REC_NO_PROMPT:-}" ]]; then
125 print -n '%{\e[31m%}● %{\e[0m%}'
126 fi
127}
128
129# Prepend indicator to PROMPT if not disabled
130if [[ -z "${REC_NO_PROMPT:-}" ]]; then
131 PROMPT='$(__rec_prompt_indicator)'"${PROMPT}"
132fi
133"#;
134
135pub const FISH_HOOK: &str = r#"
140# rec shell hooks for Fish
141# Add to ~/.config/fish/config.fish: rec init fish | source
142
143# Shell wrapper: automatically manages REC_RECORDING for start/stop
144function rec --wraps=rec
145 if test (count $argv) -ge 1; and test "$argv[1]" = "start"
146 command rec $argv; and set -gx REC_RECORDING 1
147 else if test (count $argv) -ge 1; and test "$argv[1]" = "stop"
148 command rec $argv; and set -e REC_RECORDING
149 else
150 command rec $argv
151 end
152end
153
154# Capture command before execution
155# Note: $argv is the verbatim command line (aliases NOT expanded)
156function __rec_preexec --on-event fish_preexec
157 if set -q REC_RECORDING
158 rec _hook preexec "$argv" 2>/dev/null; or true
159 end
160end
161
162# Capture exit code after command completes
163function __rec_postexec --on-event fish_postexec
164 set -l exit_code $status
165 if set -q REC_RECORDING
166 rec _hook precmd "$exit_code" 2>/dev/null; or true
167 end
168end
169
170# Recording indicator for prompt
171# Call this from your fish_prompt function if you want the indicator
172function __rec_prompt_indicator
173 if set -q REC_RECORDING; and not set -q REC_NO_PROMPT
174 set_color red
175 echo -n "● "
176 set_color normal
177 end
178end
179
180# Fish doesn't allow dynamic prompt modification like Bash/Zsh
181# Users should add __rec_prompt_indicator to their fish_prompt function
182# Or we provide a wrapper (below) that they can use
183
184# Optional: Wrap existing fish_prompt to add indicator
185# Uncomment if you want automatic prompt modification:
186# functions -c fish_prompt __rec_original_fish_prompt 2>/dev/null
187# function fish_prompt
188# __rec_prompt_indicator
189# __rec_original_fish_prompt
190# end
191"#;
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_bash_hook_contains_preexec() {
199 assert!(BASH_HOOK.contains("__rec_preexec"));
200 assert!(BASH_HOOK.contains("rec _hook preexec"));
201 assert!(BASH_HOOK.contains("preexec_functions"));
202 }
203
204 #[test]
205 fn test_bash_hook_contains_precmd() {
206 assert!(BASH_HOOK.contains("__rec_precmd"));
207 assert!(BASH_HOOK.contains("rec _hook precmd"));
208 assert!(BASH_HOOK.contains("precmd_functions"));
209 }
210
211 #[test]
212 fn test_bash_hook_checks_rec_recording() {
213 assert!(BASH_HOOK.contains("REC_RECORDING"));
214 }
215
216 #[test]
217 fn test_bash_hook_suppresses_errors() {
218 assert!(BASH_HOOK.contains("2>/dev/null || true"));
219 }
220
221 #[test]
222 fn test_bash_hook_has_prompt_indicator() {
223 assert!(BASH_HOOK.contains("__rec_prompt_indicator"));
224 assert!(BASH_HOOK.contains(r"\[\e[31m\]"));
226 }
227
228 #[test]
229 fn test_zsh_hook_uses_add_zsh_hook() {
230 assert!(ZSH_HOOK.contains("add-zsh-hook"));
231 assert!(ZSH_HOOK.contains("add-zsh-hook preexec __rec_preexec"));
232 assert!(ZSH_HOOK.contains("add-zsh-hook precmd __rec_precmd"));
233 }
234
235 #[test]
236 fn test_zsh_hook_uses_dollar_3_for_expanded_command() {
237 assert!(ZSH_HOOK.contains("\"$3\""));
239 }
240
241 #[test]
242 fn test_zsh_hook_contains_rec_hook_calls() {
243 assert!(ZSH_HOOK.contains("rec _hook preexec"));
244 assert!(ZSH_HOOK.contains("rec _hook precmd"));
245 }
246
247 #[test]
248 fn test_zsh_hook_checks_rec_recording() {
249 assert!(ZSH_HOOK.contains("REC_RECORDING"));
250 }
251
252 #[test]
253 fn test_zsh_hook_suppresses_errors() {
254 assert!(ZSH_HOOK.contains("2>/dev/null || true"));
255 }
256
257 #[test]
258 fn test_zsh_hook_has_prompt_indicator() {
259 assert!(ZSH_HOOK.contains("__rec_prompt_indicator"));
260 assert!(ZSH_HOOK.contains(r"%{\e[31m%}"));
262 }
263
264 #[test]
265 fn test_fish_hook_uses_events() {
266 assert!(FISH_HOOK.contains("--on-event fish_preexec"));
267 assert!(FISH_HOOK.contains("--on-event fish_postexec"));
268 }
269
270 #[test]
271 fn test_fish_hook_contains_rec_hook_calls() {
272 assert!(FISH_HOOK.contains("rec _hook preexec"));
273 assert!(FISH_HOOK.contains("rec _hook precmd"));
274 }
275
276 #[test]
277 fn test_fish_hook_checks_rec_recording() {
278 assert!(FISH_HOOK.contains("REC_RECORDING"));
279 }
280
281 #[test]
282 fn test_fish_hook_suppresses_errors() {
283 assert!(FISH_HOOK.contains("2>/dev/null; or true"));
284 }
285
286 #[test]
287 fn test_fish_hook_has_prompt_indicator() {
288 assert!(FISH_HOOK.contains("__rec_prompt_indicator"));
289 assert!(FISH_HOOK.contains("set_color red"));
290 }
291
292 #[test]
293 fn test_all_hooks_check_rec_recording() {
294 assert!(BASH_HOOK.contains("REC_RECORDING"));
295 assert!(ZSH_HOOK.contains("REC_RECORDING"));
296 assert!(FISH_HOOK.contains("REC_RECORDING"));
297 }
298
299 #[test]
300 fn test_get_hook_script() {
301 assert!(get_hook_script(Shell::Bash).contains("bash-preexec"));
302 assert!(get_hook_script(Shell::Zsh).contains("add-zsh-hook"));
303 assert!(get_hook_script(Shell::Fish).contains("fish_preexec"));
304 }
305
306 #[test]
307 fn test_get_init_command() {
308 assert_eq!(get_init_command(Shell::Bash), r#"eval "$(rec init bash)""#);
309 assert_eq!(get_init_command(Shell::Zsh), r#"eval "$(rec init zsh)""#);
310 assert_eq!(get_init_command(Shell::Fish), r"rec init fish | source");
311 }
312
313 #[test]
314 fn test_hook_scripts_are_substantial() {
315 assert!(BASH_HOOK.lines().count() > 20);
317 assert!(ZSH_HOOK.lines().count() > 20);
318 assert!(FISH_HOOK.lines().count() > 20);
319
320 let total_lines =
322 BASH_HOOK.lines().count() + ZSH_HOOK.lines().count() + FISH_HOOK.lines().count();
323 assert!(
324 total_lines > 80,
325 "Expected 80+ lines of hook content, got {total_lines}"
326 );
327 }
328}