A minimal dumb-terminal program for talking to serial ports — a Zig 0.16 port of picocom by Nick Patavalis.
  • Zig 99.6%
  • Nix 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 145827a617
Add a Ziggy configuration file
picocom has no configuration file. This adds one, because the settings a
serial device needs are a property of the device rather than of the
moment: the same adapter wants the same baud rate and flow control every
time, and typing them on every invocation is what shell aliases end up
papering over.

The file lives at $XDG_CONFIG_HOME/zettacom/config.ziggy and has a
defaults block plus any number of named profiles. A profile carries a
glob that is tested against the port path, the first match in file order
winning, so opening a device picks up its settings without naming them.
A profile without a glob is reachable by name with --profile. Settings
resolve as built-in defaults, then defaults, then profile, then the
command line, so a flag always wins over the file. Having no file at all
is not an error; --config reads a specific one and --no-config reads
none.

This required a change to how arguments are parsed. The parser now
produces cli.Overrides -- Options with every field optional -- rather
than Options directly, because otherwise a bare invocation is
indistinguishable from an explicit flag that happens to match the
default, and the file could never fill a gap. Overrides is generated
from Options so the two cannot drift; note that @Type is gone in Zig
0.16, so the generation uses @Struct with parallel arrays.

Two things about the ziggy dependency are worth recording. It is pinned
to 0b61581, the last commit targeting Zig 0.16; everything after it
requires 0.17-dev, and the library there uses reflection APIs (notably
Type.Struct.field_names) that 0.16 does not have. And config.zig
deserializes into an arena it owns rather than letting ziggy allocate
one, which sidesteps a leak on ziggy's parse-error path that was fixed
only after this pin.

Parse failures report a location, so an unknown setting reads as
"config.ziggy: 3:5: unknown setting". There is deliberately no
.ziggy-schema file: the Zig types are the schema and ziggy validates
against them as it deserializes, and a schema whose syntax could not be
verified here would be worse than none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcT82cZh7Kpd3cLuSEA5xp
2026-09-02 18:06:40 -05:00
examples Add a Ziggy configuration file 2026-09-02 18:06:40 -05:00
LICENSES Port picocom to Zig 0.16 2026-09-02 11:47:15 -05:00
src Add a Ziggy configuration file 2026-09-02 18:06:40 -05:00
.gitignore Port picocom to Zig 0.16 2026-09-02 11:47:15 -05:00
build.zig Add a Ziggy configuration file 2026-09-02 18:06:40 -05:00
build.zig.zon Add a Ziggy configuration file 2026-09-02 18:06:40 -05:00
flake.lock Port picocom to Zig 0.16 2026-09-02 11:47:15 -05:00
flake.nix Port picocom to Zig 0.16 2026-09-02 11:47:15 -05:00
README.md Add a Ziggy configuration file 2026-09-02 18:06:40 -05:00
REUSE.toml Port picocom to Zig 0.16 2026-09-02 11:47:15 -05:00

zettacom

A minimal dumb-terminal program for talking to serial ports — a Zig 0.16 port of picocom 2024-07 by Nick Patavalis.

It keeps picocom's command-line interface, its [C-a]-prefixed command keys and its output format, so existing habits and scripts carry over.

Building

nix develop          # Zig 0.16, plus reuse for the licence lint
zig build            # -> zig-out/bin/zettacom
zig build test       # unit tests
zig build check      # type-check without emitting a binary
zig build run -- /dev/ttyUSB0

The binary links no libc: everything goes through raw Linux syscalls in src/sys/linux.zig, so the result is a static executable with no shared-library dependencies.

Build options

Option Default Meaning
-Dversion=<string> 0.1.0-dev Version reported by --version
-Dtty-queue-max=<bytes> 0 (unlimited) Cap on the output queue, picocom's TTY_Q_SZ
-Duucp-lock-dir=<path> unset Use UUCP LCK.. lock files instead of flock(2)

Locking defaults to flock(2), as picocom's shipped Makefile does. Set -Duucp-lock-dir=/var/lock only if you need to interoperate with programs that use UUCP lock files (minicom, cu, pppd); enabling both conventions at once is normally a mistake.

Dependencies

Two, both pinned in build.zig.zon: stanza for line editing and ziggy for the configuration file. ziggy is pinned to 0b61581, the last commit that targets Zig 0.16 — everything after it requires 0.17-dev.

Configuration

picocom has no configuration file. zettacom reads one, in Ziggy, from $XDG_CONFIG_HOME/zettacom/config.ziggy (or ~/.config/zettacom/config.ziggy when XDG_CONFIG_HOME is unset). Having no file is not an error.

.defaults = .{
    .baud = 115200,
},
.profiles = {
    "usb-serial": .{
        .match = "/dev/ttyUSB*",
        .baud = 9600,
        .flow = .rtscts,
    },
},

Settings resolve in this order, each overriding the one before:

built-in defaults  ->  defaults  ->  matching profile  ->  command line

A profile applies when its match glob accepts the port path, the first match in file order winning; a profile with no match is reachable only by name. Matching uses the port path exactly as typed, so a pattern written for /dev/serial/by-id/... will not fire when the same device is opened as /dev/ttyUSB0.

Flag Effect
--config <file> Read this file instead of the default location; a missing file is an error
--no-config Read no file at all
--profile <name> Use this profile and skip match entirely

examples/config.ziggy is a commented example covering every setting.

Errors point at the line: an unknown setting reports config.ziggy: 3:5: unknown setting, and a bad value says what was wrong with it. There is no .ziggy-schema file — the Zig types in src/config.zig are the schema, and ziggy validates against them as it deserializes, which is what produces those messages. A schema file would add editor tooling support and could be written later.

Differences from picocom

Line editing is stanza, not linenoise. The *** file:, *** baud: and *** hex: prompts get history with Ctrl-R search, bracketed paste, and Tab completion of file names — the last matching picocom's file_completion_cb. Editing is configured for emacs keys, as linenoise's were.

More than one port is an error. picocom takes the last remaining argument as the port, so picocom /dev/ttyUSB0 /dev/ttyACM0 silently opens the second one. zettacom rejects that instead.

No 512-byte argument truncation. picocom's split_quoted lexes into a fixed buffer and silently truncates any single argument longer than MAX_ARG_LEN. Here the buffer grows, so only the 32-argument cap remains, and it is reported.

--parity accepts m and s. Mark and space parity were reachable in picocom only through term_set_parity, not from the command line.

Terminals are restored explicitly, not via atexit. Zig's std.process.exit does not run atexit handlers, so term.Table.restoreAll is called from the exit paths instead.

Layout

src/
  main.zig          the program: options, poll loop, command keys   <- picocom.c
  cli.zig           argument parsing                                <- parse_args
  config.zig        the Ziggy configuration file                    (new)
  glob.zig          port-path matching for config profiles          (new)
  charmap.zig       --imap/--omap/--emap character mappings         <- do_map
  ttyq.zig          the output queue                                <- tty_q
  runcmd.zig        send/receive via fork+exec                      <- run_cmd
  prompt.zig        the interactive prompts, on stanza              <- read_*
  split.zig         shell-like command splitting                    <- split.c
  hex.zig           the *** hex: parser                             <- hex2bin
  fdio.zig          writing to a raw descriptor                     <- fdio.c
  lock.zig          flock and UUCP lock files
  term.zig          terminal settings framework                     <- term.c
  term/
    backend.zig     comptime backend selection + the contract
    types.zig       Parity, Flow, Modem, error set
    linux.zig       termios2 backend                    <- termios2.c, termbits2.h
    bsd.zig         placeholder, with porting notes
    windows.zig     placeholder, with porting notes
  sys.zig           OS primitives: the comptime seam
  sys/
    linux.zig       raw syscalls, no libc
    bsd.zig         placeholder, with porting notes
    windows.zig     placeholder, with porting notes

Only Linux is implemented. The bsd.zig and windows.zig files are not stubs that silently do nothing — they fail to compile with a message, and each documents what its port would actually involve.

Baud rates

Any rate the driver will accept can be requested, not just the ones with a Bxxxx constant, because the Linux backend uses the termios2 interface (TCGETS2/TCSETS2 with BOTHER). So -b 31250 for MIDI, or -b 76800, work directly. Set NO_CUSTOM_BAUD in the environment to restrict zettacom to the standard rates, as picocom does.

Note that a driver may quietly substitute a rate it prefers. zettacom always reads back what was actually applied and shows both when they differ:

*** baud: 31250 (38400) ***

Licence

GPL-2.0-or-later, as a translation of picocom must be. Copyright is held by Nick Patavalis for the original design and code, and by Jeffrey C. Ollie for this port. The tree is REUSE compliant; run reuse lint.