1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
|
use crate::commands::detect_shell;
pub async fn run(shell: String) -> eyre::Result<()> {
let integration = match shell.as_str() {
"zsh" => generate_zsh_integration(),
"bash" => generate_bash_integration(),
"fish" => generate_fish_integration(),
"auto" => generate_auto_integration()?,
_ => eyre::bail!("Unsupported shell: {}", shell),
};
println!("{}", integration);
Ok(())
}
fn generate_auto_integration() -> eyre::Result<&'static str> {
let shell = detect_shell();
match shell.as_deref() {
Some("zsh") => Ok(generate_zsh_integration()),
Some("bash") => Ok(generate_bash_integration()),
Some("fish") => Ok(generate_fish_integration()),
Some(s) => eyre::bail!("Unsupported shell: {}", s),
None => eyre::bail!("Could not detect shell"),
}
}
/// Generate the zsh integration function - pure function for easy testing
pub fn generate_zsh_integration() -> &'static str {
r#"
# TUI uses an alternate screen, so no explicit cleanup is needed.
_atuin_ai_cleanup() {
true
}
# Question mark at start of line - natural language mode.
# Named with 'self-' prefix so bracketed-paste-magic activates it during
# paste, allowing url-quote-magic to escape ? in pasted URLs via self-insert.
self-atuin-ai-question-mark() {
# If buffer is empty or just contains '?', trigger natural language mode
if [[ -z "$BUFFER" || "$BUFFER" == "?" ]]; then
BUFFER=""
local output
output=$(atuin ai inline --hook 3>&1 1>&2 2>&3)
# Clean up the inline viewport
_atuin_ai_cleanup
if [[ $output == __atuin_ai_print__:* ]]; then
zle -I
echo "${output#__atuin_ai_print__:}"
elif [[ $output == __atuin_ai_cancel__ ]]; then
zle reset-prompt
elif [[ $output == __atuin_ai_execute__:* ]]; then
RBUFFER=""
LBUFFER=${output#__atuin_ai_execute__:}
zle reset-prompt
zle accept-line
elif [[ $output == __atuin_ai_insert__:* ]]; then
RBUFFER=""
LBUFFER=${output#__atuin_ai_insert__:}
zle reset-prompt
elif [[ -n $output ]]; then
RBUFFER=""
LBUFFER=$output
zle reset-prompt
else
zle reset-prompt
fi
else
zle self-insert
fi
}
# Set up keybindings
zle -N self-atuin-ai-question-mark
bindkey '?' self-atuin-ai-question-mark # Question mark
"#
.trim()
}
/// Generate the bash integration function - pure function for easy testing
pub fn generate_bash_integration() -> &'static str {
r#"
# Question mark at start of line - natural language mode
_atuin_ai_question_mark() {
# If buffer is empty or just contains '?', trigger natural language mode
if [[ -z "$READLINE_LINE" || "$READLINE_LINE" == "?" ]]; then
READLINE_LINE=""
READLINE_POINT=0
local output
output=$(atuin ai inline --hook 3>&1 1>&2 2>&3)
if [[ $output == __atuin_ai_print__:* ]]; then
echo "${output#__atuin_ai_print__:}"
READLINE_LINE=""
READLINE_POINT=0
elif [[ $output == __atuin_ai_cancel__ ]]; then
READLINE_LINE=""
READLINE_POINT=0
elif [[ $output == __atuin_ai_execute__:* ]]; then
# Execute the command immediately
READLINE_LINE=${output#__atuin_ai_execute__:}
READLINE_POINT=${#READLINE_LINE}
# Note: We can't directly execute in bash bind -x, but we can
# use a workaround by binding to a macro that accepts the line
bind '"\C-x\C-a": accept-line'
bind -x '"\C-x\C-e": _atuin_ai_question_mark'
elif [[ $output == __atuin_ai_insert__:* ]]; then
# Insert the command for editing
READLINE_LINE=${output#__atuin_ai_insert__:}
READLINE_POINT=${#READLINE_LINE}
elif [[ -n $output ]]; then
# Default: insert for editing
READLINE_LINE=$output
READLINE_POINT=${#READLINE_LINE}
fi
else
# Not at empty prompt, just insert the question mark
READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}?${READLINE_LINE:READLINE_POINT}"
((READLINE_POINT++))
fi
}
# Set up keybindings
# Bash requires special handling: we use bind -x for the function,
# but need a two-step approach for execute mode
__atuin_ai_accept_line=""
_atuin_ai_question_mark_wrapper() {
_atuin_ai_question_mark
if [[ -n "$__atuin_ai_accept_line" ]]; then
__atuin_ai_accept_line=""
fi
}
bind -x '"?": _atuin_ai_question_mark'
"#
.trim()
}
/// Generate the fish integration function - pure function for easy testing
pub fn generate_fish_integration() -> &'static str {
r#"
# Question mark at start of line - natural language mode
function _atuin_ai_question_mark
set -l buf (commandline -b)
# If buffer is empty or just contains '?', trigger natural language mode
if test -z "$buf" -o "$buf" = "?"
commandline -r ""
# Run atuin ai inline, swapping stdout and stderr
set -l output (atuin ai inline --hook 3>&1 1>&2 2>&3 | string collect)
if string match --quiet '__atuin_ai_print__:*' "$output"
echo (string replace "__atuin_ai_print__:" "" -- "$output" | string collect)
commandline -f repaint
else if test "$output" = "__atuin_ai_cancel__"
commandline -f repaint
else if string match --quiet '__atuin_ai_execute__:*' "$output"
# Execute the command immediately
set -l cmd (string replace "__atuin_ai_execute__:" "" -- "$output" | string collect)
commandline -r "$cmd"
commandline -f repaint
commandline -f execute
else if string match --quiet '__atuin_ai_insert__:*' "$output"
# Insert the command for editing
set -l cmd (string replace "__atuin_ai_insert__:" "" -- "$output" | string collect)
commandline -r "$cmd"
commandline -f repaint
else if test -n "$output"
# Default: insert for editing
commandline -r "$output"
commandline -f repaint
else
commandline -f repaint
end
else
# Not at empty prompt, just insert the question mark
commandline -i "?"
end
end
# Set up keybindings
bind "?" _atuin_ai_question_mark
"#
.trim()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_zsh_integration() {
let result = generate_zsh_integration();
assert!(result.contains("self-atuin-ai-question-mark"));
assert!(result.contains("bindkey"));
assert!(result.contains("atuin ai inline --hook"));
assert!(result.contains("__atuin_ai_print__"));
assert!(result.contains("__atuin_ai_cancel__"));
assert!(result.contains("__atuin_ai_execute__"));
assert!(result.contains("__atuin_ai_insert__"));
assert!(result.contains("zle self-insert"));
}
#[test]
fn test_generate_bash_integration() {
let result = generate_bash_integration();
assert!(result.contains("_atuin_ai_question_mark"));
assert!(result.contains("bind"));
assert!(result.contains("READLINE_LINE"));
assert!(result.contains("atuin ai inline --hook"));
assert!(result.contains("__atuin_ai_print__"));
assert!(result.contains("__atuin_ai_cancel__"));
assert!(result.contains("__atuin_ai_execute__"));
assert!(result.contains("__atuin_ai_insert__"));
}
#[test]
fn test_generate_fish_integration() {
let result = generate_fish_integration();
assert!(result.contains("_atuin_ai_question_mark"));
assert!(result.contains("bind"));
assert!(result.contains("commandline"));
assert!(result.contains("atuin ai inline --hook"));
assert!(result.contains("__atuin_ai_print__"));
assert!(result.contains("__atuin_ai_cancel__"));
assert!(result.contains("__atuin_ai_execute__"));
assert!(result.contains("__atuin_ai_insert__"));
}
}
|