Built-in modules

Atuin (src/modules/atuin.zig)

Fish/zsh-autosuggestion-style ghost text driven by your Atuin history.

How it works

Suggest path:

  1. Every keystroke updates the line model; onInput copies the current buffer into the worker’s one-slot mailbox and signals.
  2. 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).
  3. provideGhostText reads the latest result under a mutex; if it still starts with the current input, the trailing portion is rendered after the cursor.
  4. onTick optionally expires the suggestion after suggestion_ttl_ms of keyboard inactivity (0 = disabled, suggestion persists until it no longer prefix-matches — fish-style).

Record path:

  1. onLineCommit fires when the user presses Enter on a non-empty, certain line (the proxy snapshots the pre-submit buffer from LineState.lastCommitted()).
  2. The module pushes the line into the worker’s record mailbox.
  3. The worker shells out to atuin history start <cmd> (we don’t capture the entry ID, so there’s no history end — entries land with no exit code or duration; atuin handles that gracefully).
  4. After sync_after_records records or sync_interval_ms ms, atuin sync runs — on a detached std.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

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


Guardrail (src/modules/guardrail.zig)

Confirmation prompt for dangerous commands.

How it works

When the user presses Enter:

  1. Match the current line against a configurable rule list (substring or prefix).
  2. If a rule fires:
    • Swallow the Enter (don’t forward to shell).
    • Print a one-line warning banner to stderr.
    • Enter “armed” state.
  3. The next Enter passes through.
  4. 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


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:

  1. Walks the in-memory ring and removes every entry whose payload equals the line — duplicates included.
  2. 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