aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/commands/init.rs
blob: 77abc4f40863405f2a4a1e5ae9b2f3fb32fb6cf4 (plain) (blame)
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__"));
    }
}