- Zig 98.7%
- Objective-C 0.7%
- Python 0.2%
- Shell 0.2%
- Makefile 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docker | ||
| docs | ||
| macos | ||
| packaging | ||
| scripts | ||
| src | ||
| windows | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| build.zig | ||
| build.zig.zon | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Makefile | ||
| README.md | ||
| STYLE.md | ||
zelnet(1)
A modern, idiomatic-Zig unified IBM terminal client — a clean-room replacement for
tn5250that also drivestn3270mainframe screens, with the IBM i Host Server data layer (IFS, SQL, CL, data queues, source export) folded into the same single binary.
NAME
zelnet — unified IBM terminal client (TN5250 / TN3270 / plain telnet) and IBM i Host Server client
SYNOPSIS
zelnet [options] <host> [port]
zelnet --tn3270 [--model N] [options] <host> [port]
zelnet --nvt [options] <host> [port]
zelnet <verb> [args ...]
zelnet --demo | --probe <host> [port] | --version
where <verb> is one of auth ifs sql xfer cmd dq pgm src.
DESCRIPTION
zelnet is one statically-linked Zig 0.16 binary with two faces:
- an interactive full-screen terminal for both IBM screen protocols, and
- a set of scriptable IBM i Host Server verbs — the ACS data layer (clean-room) — that share the terminal's EBCDIC codec and TLS transport.
It detects its launch context: started from a shell (a controlling TTY) it runs
the terminal CLI; double-clicked from Finder or open(1) (no TTY) it opens a
native GUI window (macOS AppKit / Windows Win32) when built with -Dgui.
--gui / --cli override the heuristic.
TN5250 (IBM i / midrange, AS/400 lineage) — RFC 1205 / 2877 / 4777, GDS 12A0
records, plain (row, col) buffer addressing, field attributes, colour, DBCS
(double-byte) via Shift-Out/Shift-In, and concealed password fields. Negotiates
IBM-3179-2 (24×80 colour), or IBM-3477-FC (27×132) for a wide display.
TN3270 (IBM z/OS mainframe) — RFC 1576 / 2355, 12/14-bit buffer addressing,
and the extended 3270 data stream: colour, extended highlighting (blink /
reverse / underscore / intensify), Query Reply, and alternate screen sizes for
models 2–5 (24×80, 32×80, 43×80, 27×132). Negotiates IBM-3278-2-E …
IBM-3278-5-E. (TN3270E — the RFC 2355 option-40 5-byte header — is staged
but not yet negotiated; see CAVEATS.)
NVT (plain telnet, RFC 854) — the floor every telnet session starts on.
--nvt is a byte-level pass-through like telnet(1): the enclosing terminal
does the rendering while zelnet answers negotiation (remote ECHO, SGA, the
local TERM as TERMINAL-TYPE, NAWS window reports; Ctrl-] disconnects).
The default 5250 path degrades into NVT automatically when the host
negotiates an option no block-mode host uses (WILL ECHO — Cisco/BBS,
DO NAWS — Unix telnetd), so zelnet some-switch 23 just works. Block mode
is only ever entered when the server explicitly negotiates it, so the
detection cannot false-positive.
TLS is implicit ("telnets"): on by default for terminal port 992 and for
every Host Server port. The certificate is verified against the OS trust store
by default; --self-signed and --no-verify relax that for hosts whose cert is
not publicly chained.
OPTIONS
Terminal-client options (the zelnet <host> [port] form):
| Option | Description |
|---|---|
<host> [port] |
Host to connect to; port defaults to 23. |
--tls / --no-tls |
Force the transport. Default is auto: TLS iff port == 992. |
--no-verify |
Encrypt but skip certificate verification. |
--self-signed |
Accept a valid self-signed certificate (the IBM i default cert). |
--tn3270 |
Speak TN3270 (mainframe). Default is TN5250. |
--nvt |
Speak plain telnet (NVT pass-through): Cisco, BBSes, Unix telnetd. |
--model N |
TN3270 screen model 2/3/4/5 (alternate size + -E type). Default 2. |
--demo |
Render a captured pub400 sign-on transcript offline (no network). |
--probe |
Non-interactive, credential-free connectivity round-trip (diagnostic). |
--login |
Interactive sign-on using ZELNET_USER / ZELNET_PW. |
--play <script> |
Scripted TN5250 session: sign on, then drive and assert panels. With --nvt: a headless expect/send gate (expect <t>; send <t>; type <t>; enter; pause <ms>; dump). |
--drive <script> |
Scripted TN3270 session (implies --tn3270). |
--record <file> |
Write an asciicast of a scripted session (--play / --drive) — the release reel. |
--gui / --cli |
Force the GUI or terminal launch mode. |
--version, -v |
Print the build banner and version, then exit. |
In an interactive session: Tab / Shift-Tab move between fields, Enter transmits, F1–F24 are the function keys, PgUp / PgDn are Roll, F3 or a lone Esc exits/goes back, Ctrl-C quits. Password fields are non-display (typed characters are concealed).
COMMANDS
The IBM i Host Server verbs. Credentials come from the environment only,
never the command line (see ENVIRONMENT). Each verb signs on, runs, prints a
result, and exits non-zero on any failure — so they compose into CI gates
(zelnet cmd "…" || exit 1).
| Command | Description |
|---|---|
auth [host] |
Sign on and print the system VRM, server level, and password level. |
ifs ls <path> |
List an IFS directory (the file server). |
ifs get <remote> <local> |
Download a file from the IFS. |
ifs put <local> <remote> |
Upload a file to the IFS. |
ifs mkdir <path> / rmdir <path> |
Create / remove a directory. |
ifs rm <path> |
Delete a file. |
ifs mv <src> <dst> |
Rename / move a path. |
ifs stat <path> |
Show a path's type, size, attributes, and timestamps. |
sql "<SELECT ...>" |
Run a query and print the rows. Non-SELECT runs as EXECUTE IMMEDIATE and reports rows affected. |
xfer <table|"SELECT ..."> <file.csv> |
Data Transfer: export a table or query result to CSV. |
cmd "<CL COMMAND>" |
Run a CL command on the remote-command server and print its messages. |
dq write <lib/queue> <data> |
Write to a data queue. |
dq writek <lib/q> <key> <data> |
Write a keyed entry. |
dq read|peek <lib/q> [wait] |
Read (removing) or peek the next entry; wait seconds to block. |
dq readk <lib/q> <EQ|NE|LT|LE|GT|GE> <key> |
Read by key comparator. |
dq clear <lib/q> / attr <lib/q> |
Clear a queue / show its attributes. |
pgm rtvsysval <SYSVAL> |
Program-call QWCRSVAL for a system value (e.g. QMODEL, QCCSID, QDATE). |
src export <lib> [--out <dir>] [--srcfiles QDDSSRC,QRPGLESRC,QCLSRC] [--filter PART*] |
"Reverse deploy": pull a library's source members into a local Git layout (<out>/<srcfile>/<member>.<type>). |
ENVIRONMENT
| Variable | Meaning |
|---|---|
ZELNET_USER / ZELNET_PW |
Credentials for the Host Server verbs and --login / --play. Never passed on the command line. |
ZELNET_HOST |
Default host for the verbs. Defaults to pub400.com. |
ZELNET_ALLOW_PLAINTEXT=1 |
Required to permit --no-tls on a Host Server verb. Without it, zelnet refuses to send your password substitute and data in the clear. |
ZELNET_DUMP=1 |
Hex-dump every inbound 3270 record (wire debugging). |
EXAMPLES
Terminal sessions:
zelnet pub400.com 992 # IBM i over TLS (auto-on for 992), then sign on
zelnet pub400.com 23 # plain telnet
zelnet --self-signed host # IBM i with its default self-signed cert
zelnet --tn3270 --model 4 host 23 # a 43×80 mainframe screen
zelnet --demo # offline render of a captured sign-on
zelnet --probe pub400.com 992 # connectivity check, no credentials
IBM i Host Server (credentials in the environment):
export ZELNET_USER=you ZELNET_PW=secret ZELNET_HOST=pub400.com
zelnet auth # sign on, print V7R5M0 etc.
zelnet ifs ls /home/YOU # list a directory
zelnet sql "SELECT SCHEMA_NAME FROM QSYS2.SYSSCHEMAS FETCH FIRST 10 ROWS ONLY"
zelnet cmd "DSPSYSVAL SYSVAL(QDATE)" # run a CL command
zelnet xfer "QSYS2.SYSSCHEMAS" /tmp/schemas.csv # export to CSV
zelnet pgm rtvsysval QMODEL # program-call a system value
zelnet src export MYLIB --out ./MYLIB --filter PART* # pull source into Git
Scripted / headless (CI):
zelnet --play signon.zts pub400.com 992 # drive + assert a 5250 panel flow
zelnet --drive logon.z3270 mainframe 23 # drive a 3270 panel flow
EXIT STATUS
0 on success. Any sign-on, connection, or operation failure prints a
diagnostic and exits non-zero, so a verb can gate a script or CI step.
TLS AND SECURITY
- By default TLS verifies the server certificate against the OS trust store
(
pub400.com's publicly-trusted cert validates cleanly).--self-signedaccepts a valid self-signed cert;--no-verifyencrypts without authenticating the peer. - Implicit TLS runs on terminal port
992and on the Host Server ports — sign-on9476, file9473, database9471, command9475, data-queue9472(the84xxcounterparts are plaintext). - The Host Server verbs refuse
--no-tlsunlessZELNET_ALLOW_PLAINTEXT=1is set, so a stray flag can never silently leak your password substitute and data.
BUILDING
Requires Zig 0.16. Thin make wrappers around zig build:
make build # native CLI binary -> zig-out/bin/zelnet
make run ARGS="pub400.com 992"
make gui # native GUI (macOS AppKit / Windows Win32, -Dgui)
make app # assemble zig-out/zelnet.app (macOS bundle)
make ci # what CI runs: fmt-check + tests (Debug + ReleaseSafe)
make test # unit tests
make smoke # live pub400.com sign-on smoke (needs ZELNET_USER / ZELNET_PW)
make dist # cross-compile every platform -> dist/
Or drive zig build directly: zig build -Doptimize=ReleaseSafe,
zig build run -- <host> [port], zig build -Dgui, and zig build check
(the CI-parity preflight: zig fmt --check + all unit tests). Cross-compile
targets: x86_64-windows, x86_64-linux-musl, aarch64-linux-musl,
aarch64-macos, x86_64-macos.
DISTRIBUTION AND RELEASING
src/VERSION is the single source of truth and carries a -dev suffix while
developing. The in-cluster pipeline publishes to pkg.sean.farm:
- Every push is cross-compiled and uploaded per-commit to
https://pkg.sean.farm/zelnet/builds/<short-sha>/(aMANIFESTlists the artifacts) — click-testable release candidates. - A release commit drops the
-devsuffix (and adds aCHANGELOG.mdentry). The pipeline then promotes that commit's already-tested build — no rebuild, so what was tested is what ships — tohttps://pkg.sean.farm/zelnet/releases/v<x.y.z>/and the Forgejo releases page, tagging in-cluster.
CAVEATS
- This is unstable software, written in an unstable language, by unstable people. Opus 4.8 spat most of it out over a handful of days; human eyes have barely been cast upon the code, but it seems to work. For now.
- I cannot recommend using it for anything important. Will it eat your computer? Maybe.
- TN3270E is staged, not wired.
zelnetcurrently refuses TN3270E (telnet option 40, the RFC 2355 5-byte data-stream header + LU binding/responses) and speaks basic TN3270 (LU2) with the extended-Edata stream. Full-screen TSO/E logon that depends on TN3270E is not yet available.
SEE ALSO
tn5250(1), x3270(1).
RFC 1205 / 2877 / 4777 (TN5250), RFC 1576 / 2355 (TN3270 / TN3270E),
RFC 854 (Telnet).
docs/HOSTSERVER.md — the Host Server wire protocol, with
live pub400.com verification notes.