-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdefaults.zig
More file actions
288 lines (264 loc) · 16.1 KB
/
defaults.zig
File metadata and controls
288 lines (264 loc) · 16.1 KB
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
//! atty's shipped defaults.
//!
//! Every knob the user's `src/config.zig` can override has its
//! default value here. The resolver in `src/config_resolver.zig`
//! merges: each `pub const` the user declares wins; everything else
//! falls through to this file.
//!
//! Style guide: **every subsystem is a struct with per-field defaults**,
//! even if it has just one knob today. Adding a sibling knob upstream
//! becomes "new field in the struct" — user configs pick up the new
//! default automatically without needing to migrate from `xxx_yyy`
//! flat decls to a `xxx { .yyy }` grouped decl. The only exception is
//! `modules`, which is a heterogeneous tuple and can't be a struct
//! field. **Users do not edit this file**; edit `src/config.zig`.
const atty = @import("atty");
// ───── Modules ────────────────────────────────────────────────────────────
//
// The default tuple is dependency-free: history reads/writes your
// shell's own ~/.bash_history / ~/.zsh_history file — no `atuin`
// binary required, no daemon. Atty's `bin.atty.sh` prebuilt
// distribution runs out of the box.
//
// Want atuin? Override `modules` in your `src/config.zig` — see
// `src/config.def.zig` for the documented opt-in.
pub const modules = .{
atty.modules.guardrail.configure(.{}),
atty.modules.history.configure(.{}),
};
// ───── Proxy tunables ─────────────────────────────────────────────────────
pub const Proxy = struct {
/// poll() timeout — drives onTick cadence. Lower = more responsive
/// suggestion expiry; higher = lower idle CPU.
tick_interval_ms: i32 = 100,
};
pub const proxy: Proxy = .{};
// ───── Ghost overlay ──────────────────────────────────────────────────────
pub const Ghost = struct {
/// Overlay style. Matches fish + zsh-autosuggestions: dim only.
style: atty.Style = atty.style.presets.muted,
/// How many alternative suggestions to render below the prompt
/// as a numbered pick-list, in addition to the inline ghost.
/// 0 disables the feature (default). Capped at 9 — the default
/// bindings are Ctrl+1..Ctrl+9 / Esc+1..Esc+9, and most
/// terminals can't address more than that without chord keys.
list_count: u8 = 2,
/// Style of the rendered list entries — applied to both the
/// 1-based index prefix (`1: `, `2: `, …) and the entry text.
list_style: atty.Style = atty.style.presets.muted,
};
pub const ghost: Ghost = .{};
// ───── Terminal protocol negotiation ──────────────────────────────────────
pub const Terminal = struct {
/// Push the kitty keyboard protocol's `disambiguate` flag on
/// startup (and pop on exit). When on, terminals that support it
/// (Ghostty/kitty/foot/WezTerm/…) emit distinct CSI-u sequences
/// for keys that would otherwise collide with control bytes:
///
/// Ctrl+Shift+I → \x1b[105;6u (instead of \t)
/// Ctrl+Tab → \x1b[9;5u
/// Ctrl+9 → \x1b[57;5u (had no legacy encoding)
///
/// atty intercepts CSI-u sequences in the stdin path: matched
/// bindings fire, unmatched ones are dropped (never forwarded to
/// the shell). Without that drop, the shell would see the raw
/// CSI-u bytes as input — most shells don't speak the protocol
/// and would echo them as mojibake. The unaffected legacy keys
/// (Ctrl+D/Ctrl+C/arrows/etc.) pass through unchanged.
enable_kitty_keyboard: bool = true,
};
pub const terminal: Terminal = .{};
// ───── Mouse ──────────────────────────────────────────────────────────────
pub const Mouse = struct {
/// Master switch for atty's mouse handling. When true, the
/// proxy emits the SGR-1006 DECSET enable trio on startup,
/// parses incoming CSI < mouse events from stdin, and
/// dispatches them through `onMouseClick` to modules. When
/// false, atty doesn't intercept — TUIs like vim/lazygit
/// still get their own mouse via their own DECSET.
///
/// Default: false. Mouse-aware modules are opt-in per #304
/// design.
enabled: bool = false,
};
pub const mouse: Mouse = .{};
// ───── Keymap ─────────────────────────────────────────────────────────────
pub const Keymap = struct {
/// dwm-style bindings array. Right / End / Ctrl+F / Ctrl+Tab
/// accept the ghost suggestion; Ctrl+Shift+I toggles incognito
/// (resolved via `keymap.key()` to the kitty-keyboard CSI-u byte
/// sequence — requires `Terminal.enable_kitty_keyboard = true`,
/// default on). Alt+i is bound as a fallback so it still works
/// on terminals that don't speak the protocol. Ctrl+Tab also
/// requires kitty kbd; on legacy terminals the kernel collapses
/// it to plain Tab and there's no portable way to disambiguate.
bindings: []const atty.keymap.Binding = &.{
// Labels + descriptions feed the Alt+H help cheat-sheet.
// First non-empty entry per action wins; dual-encoded
// siblings (legacy + kitty kbd) leave the second blank to
// avoid double-listing. Empty label OR description hides
// the entry from help entirely.
.{ .bytes = atty.keymap.key("Right"), .action = .ghost_accept, .label = "Right", .description = "accept the full ghost suggestion" },
.{ .bytes = atty.keymap.key("End"), .action = .ghost_accept },
.{ .bytes = atty.keymap.key("Ctrl+F"), .action = .ghost_accept },
.{ .bytes = atty.keymap.key("Ctrl+Tab"), .action = .ghost_accept },
// Ctrl+Right — accept ONE word of the ghost suggestion
// (fish's section-by-section partial-accept). Useful when
// the LATER part of the ghost is correct but the middle
// needs editing — successive presses walk through.
.{ .bytes = atty.keymap.key("Ctrl+Right"), .action = .ghost_accept_word, .label = "Ctrl+Right", .description = "accept one word of the ghost suggestion" },
.{ .bytes = atty.keymap.key("Ctrl+Shift+I"), .action = .incognito_toggle, .label = "Ctrl+Shift+I", .description = "toggle incognito (no atuin/history recording)" },
// Alt+i — DUAL binding (legacy ESC+letter + kitty kbd CSI-u).
// Same reason as the Esc bindings below: keymap matching
// happens BEFORE the CSI-u → legacy translation in the
// stdin path, so a terminal that has pushed the kitty kbd
// disambiguate flag (Ghostty/kitty/foot/WezTerm) emits
// `\x1b[105;3u` for Alt+i rather than the legacy `\x1bi`
// — without the CSI-u sibling here, the binding silently
// misses. Same dual-form pattern applies to every Alt+letter
// LLM binding below.
.{ .bytes = atty.keymap.key("Alt+i"), .action = .incognito_toggle },
.{ .bytes = "\x1b[105;3u", .action = .incognito_toggle },
.{ .bytes = atty.keymap.key("Ctrl+Shift+D"), .action = .delete_history_match, .label = "Ctrl+Shift+D", .description = "delete the current ghost match from history" },
// Alt+Shift+W — dump security_guard warn events into
// scrollback + clear the buffer. Dual-encoded: legacy
// terminals emit `\x1b` + uppercase `W` for Alt+Shift+W;
// terminals that push the kitty kbd disambiguate flag
// (Ghostty / kitty / foot / WezTerm) emit `\x1b[87;4u`
// (87 = 'W', modifier 4 = shift+alt). The keymap parser
// doesn't grok `Alt+Shift+<letter>` so the raw bytes are
// spelled out here.
.{ .bytes = "\x1bW", .action = .security_guard_show_warnings, .label = "Alt+Shift+W", .description = "dump security_guard warn events to scrollback" },
.{ .bytes = "\x1b[87;4u", .action = .security_guard_show_warnings },
// LLM-module bindings live on the module itself (see
// `atty.modules.llm.default_bindings`). The dispatcher's
// `allDefaultBindings()` comptime-collects them into the
// global keymap whenever the user's `modules` tuple includes
// the LLM module — so a user who doesn't enable LLM gets
// none of those keys, and a user who wants to rebind one
// can still list an override in their own `Keymap.bindings`
// (user wins via first-match scan).
// Esc — bare-key shortcut for the same cancel action. The
// module's onAction handler is gated on AI-mode-or-
// pending-work, so a stray Esc outside AI mode falls
// through to the shell (vim users still get their bare
// Esc). Bytes match exact only — \x1b alone (lone Esc),
// not the lead byte of CSI sequences like arrow keys.
//
// Two bindings because the kitty keyboard protocol
// (enabled by default — see `Terminal.enable_kitty_keyboard`)
// re-encodes Esc as the CSI-u sequence `\x1b[27u`. Keymap
// matching happens BEFORE the CSI-u → legacy translation
// in the shell-input path, so binding only the legacy
// form misses Esc presses on a kitty-kbd terminal.
.{ .bytes = atty.keymap.key("Esc"), .action = .llm_exec_cancel },
.{ .bytes = "\x1b[27u", .action = .llm_exec_cancel },
// Ghost pick — kitty kbd CSI-u + Esc+digit legacy fallback.
// No-ops when `ghost.list_count == 0` (default) or N exceeds
// the rendered list length.
.{ .bytes = atty.keymap.key("Ctrl+1"), .action = .{ .ghost_pick = 1 } },
.{ .bytes = atty.keymap.key("Ctrl+2"), .action = .{ .ghost_pick = 2 } },
.{ .bytes = atty.keymap.key("Ctrl+3"), .action = .{ .ghost_pick = 3 } },
.{ .bytes = atty.keymap.key("Ctrl+4"), .action = .{ .ghost_pick = 4 } },
.{ .bytes = atty.keymap.key("Ctrl+5"), .action = .{ .ghost_pick = 5 } },
.{ .bytes = atty.keymap.key("Ctrl+6"), .action = .{ .ghost_pick = 6 } },
.{ .bytes = atty.keymap.key("Ctrl+7"), .action = .{ .ghost_pick = 7 } },
.{ .bytes = atty.keymap.key("Ctrl+8"), .action = .{ .ghost_pick = 8 } },
.{ .bytes = atty.keymap.key("Ctrl+9"), .action = .{ .ghost_pick = 9 } },
.{ .bytes = atty.keymap.key("Esc+1"), .action = .{ .ghost_pick = 1 } },
.{ .bytes = atty.keymap.key("Esc+2"), .action = .{ .ghost_pick = 2 } },
.{ .bytes = atty.keymap.key("Esc+3"), .action = .{ .ghost_pick = 3 } },
.{ .bytes = atty.keymap.key("Esc+4"), .action = .{ .ghost_pick = 4 } },
.{ .bytes = atty.keymap.key("Esc+5"), .action = .{ .ghost_pick = 5 } },
.{ .bytes = atty.keymap.key("Esc+6"), .action = .{ .ghost_pick = 6 } },
.{ .bytes = atty.keymap.key("Esc+7"), .action = .{ .ghost_pick = 7 } },
.{ .bytes = atty.keymap.key("Esc+8"), .action = .{ .ghost_pick = 8 } },
.{ .bytes = atty.keymap.key("Esc+9"), .action = .{ .ghost_pick = 9 } },
},
};
pub const keymap: Keymap = .{};
// ───── Bottom status bar ──────────────────────────────────────────────────
pub const StatusBar = struct {
/// Reserve rows at the bottom of the terminal for the indicator
/// (via DECSTBM). Off by default — costs `reserve_rows` of vertical
/// screen real estate.
enabled: bool = false,
/// How many bottom rows to reserve. The last row holds the status
/// text; the topmost reserved row holds the hint / error
/// notification (when active); rows in between are blank visual
/// padding. Default of 3 gives:
/// rows-2: hint / error notification
/// rows-1: blank padding (visual breathing room)
/// rows : status bar
/// Bumping to 4+ adds more padding above the hint; dropping to
/// 2 collapses hint and status onto adjacent rows; 1 disables
/// the hint surface entirely (no room above status).
reserve_rows: u16 = 3,
/// Style for the bar's default segments (base text + module
/// contributions). Per-segment overrides (like incognito_style)
/// take precedence inside their own segment.
style: atty.Style = atty.style.presets.muted,
/// Base text shown when no module contributes anything. Empty
/// means the bar stays blank until a module sets something.
base_text: []const u8 = "",
/// Style for the 🔒 incognito segment specifically. Defaults to
/// muted red (dim + red) so it pops against the rest of the bar.
incognito_style: atty.Style = .{ .dim = true, .fg = 1 },
/// Style for one-shot hints (LLM explanations and similar
/// informational content). Defaults to muted italic so the hint
/// row reads as a distinct annotation, not just more status
/// text. Override per taste — `.italic = true, .fg = 39` for a
/// cool cyan slant, etc.
hint_style: atty.Style = atty.style.presets.muted_italic,
/// How long a one-shot hint (from a module's `provideHintText`)
/// stays visible above the status text. The LLM module uses
/// this to flash an explanation of the command it just
/// injected. Drop to 0 to disable hint rendering.
hint_ttl_ms: u32 = 30_000,
/// Style for error notifications surfaced via a module's
/// `provideErrorText` hook. Painted on the same row as the
/// regular hint, in muted red with a leading ⚠ glyph so it
/// reads as a notification rather than informational text.
/// Errors take precedence over regular hints while active.
error_style: atty.Style = .{ .dim = true, .fg = 1 },
/// How long an error notification stays visible. Defaults to
/// twice the hint TTL so users have time to notice and read.
/// Drop to 0 to disable error rendering entirely.
error_ttl_ms: u32 = 60_000,
};
pub const statusbar: StatusBar = .{};
// ───── Subprocess-context tracking ────────────────────────────────────────
pub const Subprocess = struct {
/// Path to the `ssh` binary used for `ssh -G <args>` resolution.
/// Override on NixOS, Guix, or anywhere `ssh` lives outside the
/// default `$PATH` lookup.
ssh_binary: []const u8 = "ssh",
/// When true (default), the subprocess parser forks `ssh -G <args>`
/// to resolve `~/.ssh/config` aliases, Match blocks, ProxyJump,
/// `-F` files, `-i` identity → CanonicalDomains transformations,
/// etc. — using ssh's OWN parser rather than re-implementing one
/// in Zig. Adds a short blocking call (~100ms) per `ssh` invocation
/// at `;C` time; the user is already waiting for ssh to connect
/// so it's not user-visible.
///
/// Disable to fall back to a regex extraction of `user@host` from
/// the typed command line — loses alias resolution but doesn't
/// fork+exec. Useful when ssh is slow / unavailable / inside a
/// container without `ssh -G` support.
use_ssh_g: bool = true,
/// Surface a `→ ssh:host` (or `→ k8s:context/ns/pod`, etc.)
/// segment in the status bar while a recognised subprocess is
/// active. Adds at most one ANSI repaint per `;C` / `;D` edge.
show_in_statusbar: bool = true,
/// Style for the subprocess segment in the status bar. Defaults
/// to muted cyan to distinguish from the `🔒` incognito segment
/// (muted red) and the regular module contributions.
segment_style: atty.Style = .{ .dim = true, .fg = 6 },
/// Per-target incognito list. Matches against the resolved
/// `Frame.name` (e.g. `prod@bastion.example.com`, `prod/apps/db`,
/// `nginx`, `sudo`). Commands typed inside a matching target are
/// dropped — same effect as the user manually toggling
/// Ctrl+Shift+I before stepping in. Empty by default.
incognito_targets: []const []const u8 = &.{},
};
pub const subprocess: Subprocess = .{};