- Zig 99.6%
- Nix 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| examples | ||
| LICENSES | ||
| src | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
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.