A small readline alternative for Zig.
  • Zig 94.2%
  • Python 4.6%
  • Shell 1.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker 436e493985
All checks were successful
test / test (push) Successful in 30s
Merge branch 'dev'
2026-10-10 12:55:40 +02:00
.forgejo/workflows Zig 0.17: minimum version, README, and CI pinned to /opt/zig-0.17.0 2026-10-06 01:58:59 +02:00
examples demo: --history <path> loads history at start and saves on exit 2026-10-08 14:40:31 +02:00
scripts demo: --history <path> loads history at start and saves on exit 2026-10-08 14:40:31 +02:00
src complete: preset start is the word ending at the cursor 2026-10-08 14:40:31 +02:00
.devkit.conf Add devkit config 2026-09-04 09:37:46 +02:00
.gitignore scripts: setup-tools, the one every repo shares 2026-09-22 21:06:15 +02:00
build.zig build: the run step's arguments on Zig 0.17 2026-10-06 01:57:39 +02:00
build.zig.zon Zig 0.17: minimum version, README, and CI pinned to /opt/zig-0.17.0 2026-10-06 01:58:59 +02:00
CLAUDE.md Initial commit: project instructions and gitignore 2026-09-03 09:54:20 +02:00
context.md context, DESIGN: session 3 2026-10-10 12:54:19 +02:00
DESIGN.md context, DESIGN: session 3 2026-10-10 12:54:19 +02:00
LICENSE Add MIT license 2026-09-04 09:23:08 +02:00
README.md README: history load/save, completion start and context 2026-10-08 14:41:35 +02:00

caret

A small readline alternative for Zig, built for interactive REPL prompts. No libc, caller-supplied allocator, one blocking call.

  • Line editing with Emacs keys, kill/yank, transpose, undo/redo
  • History with prefix-aware navigation, saved and loaded through std.Io readers and writers
  • Tab completion with a cycling menu under the prompt
  • Multiline input driven by a completeness callback, with a built-in bracket balancer that understands strings, comments and char literals
  • Bracket-pair highlighting while editing; the submitted line is left plain in scrollback, as is a line with the completion menu open
  • Bracketed paste
  • Plain-line fallback when stdin is a pipe or file
  • UTF-8 with East Asian wide character widths
  • macOS, Linux, Windows 10 1809+

Requires Zig 0.17.

Usage

const caret = @import("caret");

var ed = try caret.Editor.init(allocator, .{
    .prompt = "> ",
    .continuation = "  ",
    .is_complete = caret.Balance.isComplete(.scheme),
    .match = .scheme,
    .completer = complete,
});
defer ed.deinit();

while (try ed.readLine()) |line| {
    defer allocator.free(line);
    // ...
}

readLine returns an owned copy of the line, null at end of input (Ctrl-D on an empty line, or a closed pipe), and error.Interrupted on Ctrl-C.

A completer fills in candidates for the word before the cursor:

fn complete(_: ?*anyopaque, text: []const u8, cursor: usize, out: *caret.Completions) !void {
    const word = text[out.start..cursor];
    for (symbols) |s| if (std.mem.startsWith(u8, s, word)) try out.add(s);
}

One candidate completes outright. Several insert the first and show all of them under the prompt; Tab and Shift-Tab step through them.

out.start is preset to the start of the word ending at the cursor, or to the cursor itself after a space or bracket. The completer sees the whole text, so it can look before the word for context, say only library names inside (import (scheme _, and move out.start when its idea of a word differs, such as Scheme's string->list.

History

Lines are added as they are submitted; blanks and an immediate repeat are skipped, and the oldest is dropped past history_max (1000). The caller opens the file:

var r = file.reader(io, &buf);
try ed.history.load(&r.interface);   // at start
...
var w = file.writer(io, &buf);
try ed.history.save(&w.interface);   // on exit; flushes

One entry per line, oldest first; a newline inside an entry is written \n and a backslash \\.

Multiline

Enter submits when is_complete says the text is done, otherwise it inserts a newline. Balance describes a syntax, bracket pairs, string quotes, comments and a char-literal prefix, and comes with scheme and c_like presets. A stray or mismatched closer counts as complete so a typo submits rather than trapping you. Ctrl-J, Ctrl-O, Alt-Enter and Shift-Enter always insert a newline.

Pasted text goes in verbatim through bracketed paste, and when stdin is not a terminal lines are read plainly and joined until complete, so a piped file of forms works the same as typing them.

Keys

Key Action
Left, Right, Ctrl-B, Ctrl-F move by character
Alt-B, Alt-F, Ctrl-Left, Ctrl-Right move by word
Home, End, Ctrl-A, Ctrl-E line start and end
Ctrl-Home, Ctrl-End, Alt-<, Alt-> buffer start and end
Up, Down previous/next line, then history
Ctrl-P, Ctrl-N history
Backspace, Delete, Ctrl-H delete
Ctrl-K, Ctrl-U kill to end and start of line
Ctrl-W, Alt-Backspace, Alt-D, Ctrl-Delete kill word
Ctrl-Y yank
Ctrl-T transpose
Ctrl-_ , Ctrl-Z undo
Alt-/ , Ctrl-Shift-Z redo
Tab, Shift-Tab complete, cycle
Ctrl-J, Ctrl-O, Alt-Enter, Shift-Enter newline
Ctrl-L clear screen
Ctrl-D delete forward, or end of input on an empty line
Ctrl-C interrupt

The keymap is a value in the config; copy Keymap.emacs and bind or unbind to change it. On macOS Terminal.app, turn on "Use Option as Meta key" for the Alt chords.

Build

zig build test   # unit tests, no terminal needed
zig build run    # demo REPL, examples/repl.zig
zig build run -- --history /tmp/caret_history   # demo with a history file
zig build pty    # drive the demo through a pseudo-terminal (POSIX, python3)

See DESIGN.md for the layering and the decisions behind it.

License

MIT — see LICENSE.