@tty-pt/axil-tty
v1.3.2
Published
AXIL PTY multiplexer module
Maintainers
Readme
axil-tty
WebSocket-to-PTY bridge module for axil — browser terminal sessions over a login shell.
A dynamic module for axil that adds:
- WebSocket-to-PTY bridging (browser terminal sessions)
- Telnet negotiation (NAWS window resize, ECHO, SGA)
- Browser terminal JS/CSS assets (
axil-tty.js,axil-tty.css,demo.js) - Login-shell
shhandler — spawns a shell over a WebSocket
Contents
Features
- WebSocket↔PTY bridging — browser terminal sessions wired to a PTY on the server.
- Telnet negotiation — NAWS window resize, ECHO, and SGA for a proper
terminal experience.
WILL ECHOis negotiated once per session, and every PTY keeps the line discipline's ownECHO, so the client stays a pipe: the driver echoes each keystroke as it is typed, owns line editing and^?erase, and turns the guest's\ninto the\r\na terminal needs. - Login-shell handler — the first NAWS on a connection spawns a login shell
over the WebSocket, via
axil_tty_shell/axil_tty_exec. Anshcommand is also registered, but see the caveat below: over a WebSocket axil never runscmd_parse, so it is only reachable from a host module's own command pipeline (axil-nd), not by a client typingsh. - Browser assets —
axil-tty.js,axil-tty.css, anddemo.jsserved fromhtdocs/(installed undershare/axil/htdocs).
Install
Prebuilt packages are distributed from tty.pt for Linux (APT
/ Alpine / Arch / Fedora-RHEL), macOS (Homebrew), Windows (winget / MSYS2), and
OpenBSD. Follow the [installation instructions]
(https://github.com/tty-pt/ci/blob/main/docs/install.md) and use libaxil-tty
as the package name. The module installs as libaxil-tty.so in the axil module
directory, and the NPM terminal widget is published as @tty-pt/libaxil-tty.
Build from source
The module builds with a plain make (the shared [mk include.mk]
(https://github.com/tty-pt/mk) is expected as a sibling directory):
make # builds lib/libaxil-tty.so (and the lib/axil-tty.so link)
make test # run the in-tree test suite (./test.sh)
sudo make install # module + htdocs → $(PREFIX), default /usr/localDependencies: libaxil, libcorm, libxylem packages (from the tty.pt
repo) provide the headers and libraries.
Quick start
Start axil with the module loaded:
axil -d -A -p 8080 -m axil-tty
# run from the repo's lib/ so `-m axil-tty` resolves, or pass the installed
# module path, e.g. -m axil-tty once installed, or -m /path/to/lib/axil-ttyIf the libaxil-tty package is installed, -m axil-tty resolves from
anywhere (the module lives in the axil module directory); the working-directory
hint above only matters when running from a source checkout.
Command-line options (inherited from axil)
| Option | Description |
|--------|-------------|
| -m PATH | Load module from PATH (colon-separated list) |
| -A | Auto-authenticate all connections |
| -p PORT | HTTP/WS listen port |
| -C PATH | Change directory before starting |
| -d | Don't detach (run in foreground) |
Open http://localhost:8080/tty for a terminal connected to a login shell.
The module serves its page and WebSocket upgrade on /tty; the server root is
not claimed by this module.
Browser terminal
Install the NPM package:
npm install @tty-pt/libaxil-ttyJavaScript/TypeScript API:
import { create } from "@tty-pt/libaxil-tty";
// Create terminal instance
const term = create(document.getElementById("terminal"), {
proto: "ws", // or "wss" for secure
port: 4201,
// "/tty" is the default, matching the route the module serves
url: `ws://${location.hostname}:${location.port}/tty`,
// Every key is optional: `sub` is merged over the defaults, so you only
// override what you need and onOpen/onClose/onMessage/cols/rows keep their
// default implementations. The returned instance is a copy — mutating it
// does not write back into this object.
sub: {
onOpen: (term, ws) => {
console.log("Connected to server");
},
onClose: () => {
console.log("Disconnected, reconnecting...");
},
onMessage: (ev, arr) => {
// Return true to continue default processing
return true;
},
cols: 80,
rows: 25,
},
debug: false,
});See types/axil-tty.d.ts for full TypeScript definitions.
C API
#include <ttypt/axil-tty.h>
// Spawn a login shell on a WebSocket connection
call_axil_tty_shell(fd);
// Spawn a specific command
char *argv[] = { "/bin/bash", NULL };
call_axil_tty_exec(fd, argv);
// Is this connection ours? True only for a client on GET:/tty.
call_axil_tty_owns(fd);Two modules on one process
axil dispatches on_axil_tick to every loaded module, and axil never routes
WebSocket frames through on_axil_parse — frames reach a module only after it
calls axil_fd_watch() and pulls them with axil_ws_read(). So if another
module (axil-nd) is loaded alongside this one, both ticks would drain the same
socket and split its frame stream.
axil_tty_owns(fd) is the arbitration point: axil-tty sets it for connections
on its own GET:/tty route, and a host module should skip any fd for which it
returns true. axil-nd does exactly this, and forwards the frames it reads to
axil_tty_input() so this module's PTY bridge and NAWS handling still apply.
Note that the gate on on_axil_connect is defensive rather than load-bearing:
loaded as a DT_NEEDED dependency of another module, axil dispatches
on_axil_connect to the primary module only (verified with an instrumented
build).
WebSocket upgrade
axil does not auto-upgrade WebSocket requests. Modules must opt in
explicitly. axil-tty registers a GET:/tty handler that detects the upgrade
and calls axil_ws_upgrade(fd):
static int
handle_tty(socket_t fd, char *body)
{
char key[ENV_VALUE_LEN] = {0};
if (axil_env_get(fd, key, "HTTP_SEC_WEBSOCKET_KEY") == 0) {
axil_ws_upgrade(fd); /* performs handshake, calls axil_connect() */
return 0;
}
serve_htdocs(fd, "index.html");
return 0;
}After axil_ws_upgrade succeeds, axil_connect() fires — axil-tty's
on_axil_connect handler sets up the PTY and Telnet negotiation.
Testing
./test.sh # or: make testThe suite boots axil with the module on a random port and verifies the
WebSocket handshake (computed Sec-WebSocket-Accept), the telnet IAC
negotiation (DO NAWS, WILL ECHO, WONT SGA), that a partial line comes
back echoed before Enter — the line discipline echoing each keystroke as it
arrives, which is the whole feature — that PTY output (echo AXIL_TEST) streams
back over the socket CRLF-terminated, and that the browser assets are served.
make builds the C module only; the bundle the browser loads is bun run build.
Neither installs — sudo make install is what puts the module and htdocs/ under
$(PREFIX), and axil -m axil-tty loads that copy, so a rebuilt bundle is not
what you are testing until it is installed. For a loop that needs no sudo:
bun run build && AXIL_HTDOCS=./htdocs axil -d -A -p 8080 -m ./lib/axil-ttyDocumentation
- Man pages:
man axil_tty_shellandman axil_tty_exec(generated bymake docsfrom the Doxygen-annotated header). - API:
include/ttypt/axil-tty.h - Version history:
CHANGELOG.md
License
BSD 2-Clause License. Copyright (c) 2026, tty-pt. See LICENSE.
