Built-in modules
- Built-in modules
Atuin (src/modules/atuin.zig)
Fish/zsh-autosuggestion-style ghost text driven by your Atuin history.
How it works
Suggest path:
- Every keystroke updates the line model;
onInputcopies the current buffer into the worker’s one-slot mailbox and signals. - The worker calls
atuin search --search-mode prefix --filter-mode global --limit 1 --cmd-only <query>(newest match wins; no--reverse— that flag flips the default and gives the oldest match instead). provideGhostTextreads the latest result under a mutex; if it still starts with the current input, the trailing portion is rendered after the cursor.onTickoptionally expires the suggestion aftersuggestion_ttl_msof keyboard inactivity (0= disabled, suggestion persists until it no longer prefix-matches — fish-style).
Record path:
onLineCommitfires when the user presses Enter on a non-empty, certain line (the proxy snapshots the pre-submit buffer fromLineState.lastCommitted()).- The module pushes the line into the worker’s record mailbox.
- The worker shells out to
atuin history start <cmd>(we don’t capture the entry ID, so there’s nohistory end— entries land with no exit code or duration; atuin handles that gracefully). - After
sync_after_recordsrecords orsync_interval_msms,atuin syncruns — on a detachedstd.Thread, so the worker never blocks on the network. One final sync also runs on detach.
Accept path:
Right-arrow / End / Ctrl-F (configured in bindings[]) replace the
keystroke with the current ghost-overlay text before line state sees
the CSI — see the Keymap docs.
Configuration
pub const Atuin = atty.modules.atuin.configure(.{
.backend = .subprocess,
.atuin_binary = "atuin",
.search_mode = .prefix,
.filter_mode = .global,
.suggestion_ttl_ms = 0,
.max_query = 256,
.max_result = 512,
.record = true,
.sync_after_records = 10,
.sync_interval_ms = 60_000,
.sync_on_detach = true,
});
| Field | Default | Values |
|---|---|---|
backend |
.subprocess |
.subprocess, .socket (stub) |
atuin_binary |
"atuin" |
path to atuin executable |
search_mode |
.prefix |
.prefix, .full_text, .fuzzy |
filter_mode |
.global |
.global, .host, .session, .directory |
suggestion_ttl_ms |
0 | ms of idleness before suggestion fades; 0 disables |
max_query, max_result |
256 / 512 | comptime mailbox sizes |
record |
true |
shell out to atuin history start on Enter |
sync_after_records |
10 | sync after N records; 0 disables |
sync_interval_ms |
60000 | sync if at least this much time elapsed; 0 disables |
sync_on_detach |
true |
one last sync on shutdown |
Backends
.subprocess— shells out toatuin search. Robust, used today..socket— talks to the Atuin daemon socket. Stub; the symbols are wired throughconfigureso swapping backends is a one-field change once Atuin’s IPC protocol stabilises. When.subprocessis selected, the socket path is comptime-eliminated from the binary, and vice versa.
Status bar segment
Atuin contributes "atuin" as its statusText segment (always-on
label). Future iterations will surface queued-record count and
last-sync age. The segment is omitted entirely if the user has
disabled the status bar (statusbar.enabled = false).
deleteHistoryMatch
Not implemented — atuin history delete needs an entry ID, and atty
doesn’t capture IDs at record time (one CLI call per commit, not
two). If you need to delete from atuin’s store, do it via the CLI
directly. The delete_history_match action still affects the
history module if you have both wired in your modules tuple.
Performance
onInputdoes onememcpy+ a cv-signal, no I/O. Zero allocations.- The worker thread runs at most one
atuin searchper keystroke (coalesced — typing 5 chars quickly results in 1–2 lookups, not 5). provideGhostTextis a mutex+memcpy, no allocations beyond the per-dispatchctx.scratch.
Guardrail (src/modules/guardrail.zig)
Confirmation prompt for dangerous commands.
How it works
When the user presses Enter:
- Match the current line against a configurable rule list (substring or prefix).
- If a rule fires:
- Swallow the Enter (don’t forward to shell).
- Print a one-line warning banner to stderr.
- Enter “armed” state.
- The next Enter passes through.
- Any non-Enter keystroke disarms (so editing the command doesn’t accidentally double-press past the guard).
Default rules
| Name | Match | Reason |
|---|---|---|
rm-rf-root |
substring rm -rf / |
rm -rf on a root-ish path |
rm-rf-tilde |
substring rm -rf ~ |
rm -rf on home |
dd-raw-device |
prefix dd |
dd writing to a raw device |
mkfs |
prefix mkfs |
filesystem creation |
fork-bomb |
substring :(){ :\|:& };: |
classic fork bomb |
curl-pipe-sh |
substring \| sh |
curl … | sh |
curl-pipe-bash |
substring \| bash |
curl … | bash |
chmod-world |
substring chmod 777 / |
world-writable root path |
Custom rules
pub const Guardrail = atty.modules.guardrail.configure(.{
.rules = &.{
.{
.name = "git-force-push-main",
.kind = .{ .substring = "git push --force" },
.reason = "force-pushing to a shared branch",
},
},
.warning_style = atty.style.presets.danger, // bold red
});
Passing .rules replaces the default list entirely; merge manually
if you want both. warning_style takes an atty.Style — same type
the ghost overlay uses, so a single palette can drive the whole
proxy. Default style is .{ .dim = true, .italic = true }.
Limitations
- The match runs against our line model, which approximates what the
user typed. Vi-mode users navigating with
hjklflip the buffer intouncertainstate — in that case the guardrail doesn’t fire. Reasonable default: if we can’t tell what the user typed, we don’t try to second-guess them. - Substring matches are literal — there’s no regex engine. If you need one, write a new module; do not add it here. A pathological regex on the input hot path would be a strict regression.
History (src/modules/history.zig)
Shell-native command history with fish-style ghost suggestions —
no daemon, no shell plugin. Reads and writes the file your shell
already uses (~/.bash_history, ~/.zsh_history, ~/.history),
so commands typed through atty are visible to everything else
that reads the file (and vice-versa).
How it works
Init. Resolves the history file from $HISTFILE if set,
otherwise ~/.zsh_history / ~/.bash_history / ~/.history
based on $SHELL. Reads the tail (up to 1 MiB) and loads the
most recent capacity entries into a ring kept in memory.
Record. onLineCommit appends each committed line to the
history file via a single atomic O_APPEND write (≤ PIPE_BUF =
4096 B; max_line caps lines below that). The line also goes
into the in-memory ring, evicting the oldest entry if at capacity.
For zsh the extended-history prefix : <unix_ts>:0; is prepended;
bash and others get a bare line.
Suggest. provideGhostText walks the ring newest-first and
returns the first entry that prefix-matches the current input.
Composing with Atuin
Put History after Atuin in config.modules:
pub const modules = .{ Guardrail, Atuin, History };
provideGhostText is “first non-null wins” across modules — atuin
gets to suggest first, history fills the gap when atuin is empty,
not installed, or hasn’t synced recently.
Configuration
pub const History = atty.modules.history.configure(.{
.path = "", // "" = auto-detect from $HISTFILE / $SHELL
.format = .auto, // .auto, .bash, .zsh_extended, .plain
.record = true,
.suggest = true,
.capacity = 5_000,
.max_line = 4_096,
.suggestion_ttl_ms = 5_000,
.match = .prefix, // .substring is reserved for later
});
| Field | Default | Notes |
|---|---|---|
path |
"" |
"" auto-detects from $HISTFILE then $SHELL |
format |
.auto |
.bash, .zsh_extended, .plain |
record |
true |
Append on Enter |
suggest |
true |
Serve ghost-text suggestions |
capacity |
5000 | Ring size; oldest evicted past this |
max_line |
4096 | Anything longer is dropped (likely pasted garbage) |
suggestion_ttl_ms |
5000 | TTL on cached ghost match |
match |
.prefix |
.substring not yet wired |
deleteHistoryMatch
Implements the optional deleteHistoryMatch hook. When the user
fires Action.delete_history_match (default Ctrl+Shift+D) with the
target line in their buffer, the module:
- Walks the in-memory ring and removes every entry whose payload equals the line — duplicates included.
- Rewrites the on-disk history file via the temp + rename trick so a crash mid-write can’t corrupt the existing file.
Errors are swallowed: a read-only or missing parent directory means the ring stays filtered in this session but the file isn’t updated.
Limitations
- No exit-code tracking. Records fire on Enter, before the
shell completes the command. Atuin’s official shell plugin uses
PROMPT_COMMANDfor that; we don’t. Entries land in the file with no exit code attached (bash format has no slot for one; zsh extended-history’s duration field is set to0). - Tab completion isn’t followed. Same
uncertainlimitation as every other input-only module — see the line-state section. - Substring-mode ghost isn’t implemented yet — the ghost renderer
only paints the tail past the query position, which is wrong for
non-prefix hits. Set
.match = .prefix(the default).