Command-line flags¶
Termapy is a TUI first, but a few things are easier from the shell: listing
what's plugged in, watching for changes, and inspecting a chip without
launching the UI. Every flag in the inventory sections below prints
plain text to stdout and exits; none of them load the TUI. (The
session flags at the end -- --cli, --run, -e -- start a session
instead.)
Because these flags skip the Textual import, they start fast — no config loading, no widget tree, no config directory scan. Handy when you just want to answer "what's plugged in?" from the shell.
termapy --ports # one-line-per-port table
termapy --watch # live log of plug/unplug/open/close
termapy --info # full per-port chip dump
termapy --info=COM4 # just COM4
termapy --usb # the whole USB tree, serial ports marked
--ports: what's plugged in right now¶
A single-line-per-port table, same schema as the in-app port picker.
$ termapy --ports
PORT MFG VENDOR DESCRIPTION PRODUCT CHIP SPEED VID:PID SERIAL # DRIVER LOCATION IF
---- ---- --------- ----------------- ----------------- ------------------------------- ---------- --------- ------------------ ------- -------- --
COM3 MSFT Microchip USB Serial Device MPLAB PICkit 5 Microchip MPLAB PICkit 5 (VCOM) High-Speed 04D8:9036 020026702RYN040952 usbser 1-8.4 1
COM4 FTDI FTDI USB Serial Port FT230X Basic UART FTDI FT230X / FT231X / FT234XD Full-Speed 0403:6015 D20JSV68A FTSER2K 1-8.2
COM7 FTDI FTDI USB Serial Port FT232R USB UART FTDI FT232R / FT245R Full-Speed 0403:6001 BG03U7VTA FTSER2K 1-8.3
Three columns name the device, each from a different source: DESCRIPTION is the name the driver gives the port (often just "USB Serial Device"), PRODUCT is what the device calls itself (its USB product string), and CHIP is termapy's own table of known chips.
Columns you don't need are dropped automatically: on a machine where no
port has a serial number, the SERIAL # column disappears. On narrow
terminals, columns drop in this order until the row fits: speed, chip,
VID:PID, driver, vendor, PRODUCT, SERIAL #, IF, location. PORT, MFG and
DESCRIPTION always stay.
Filter to a single port by name:
Exits 0 on match, 1 on "not found" or "no ports" (script-friendly).
--ports lists physical ports only — the ones the OS reports via
pyserial's comports(). Termapy also accepts pyserial URL handlers
(loop://, socket://host:port, rfc2217://host:port, hwgrep://...)
as the port config value, but those are connection methods rather
than inventory, so they don't appear here. See
Serial ports for the full list.
--watch: live log of port events¶
Polls every 0.5 s and prints a log line for every change — plug,
unplug, open, close, chip re-EEPROM. Baseline rows show what's
present at startup; Ctrl+C to exit.
$ termapy --watch
[15:57:57] monitoring 3 port(s); Ctrl+C to exit
[15:57:57] COM3 closed MSFT USB Serial Device Microchip MPLAB PICkit 5 (VCOM) High-Speed 04D8:9036 020026702RYN040952
[15:57:57] COM4 closed FTDI USB Serial Port FTDI FT230X / FT231X / FT234XD Full-Speed 0403:6015 D20JSV68A
[15:57:57] COM7 closed FTDI USB Serial Port FTDI FT232R / FT245R Full-Speed 0403:6001 BG03U7VTA
[15:58:01] COM7 open FTDI USB Serial Port FTDI FT232R / FT245R Full-Speed 0403:6001 BG03U7VTA
[15:58:02] - COM7
[15:58:06] + COM7
[15:58:06] COM7 closed FTDI USB Serial Port FTDI FT232R / FT245R Full-Speed 0403:6001 BG03U7VTA
[15:58:13] COM4 open FTDI USB Serial Port FTDI FT230X / FT231X / FT234XD Full-Speed 0403:6015 D20JSV68A
In that capture:
- Baseline at
15:57:57shows the three ports that were plugged in when--watchstarted, all closed. 15:58:01— something opened COM7 (blank marker, state column flipped toopen).15:58:02/15:58:06— COM7 was unplugged (-), then plugged back in (+); the line after the+shows the post-plug state.15:58:13— a process opened COM4 (another blank-marker state transition).
Reading the event column¶
The first column after the timestamp is a single-character event marker:
| Marker | Meaning |
|---|---|
| blank | Baseline snapshot, or open/close transition (see state column) |
+ |
Port appeared (new plug-in). Followed by a full state line. |
- |
Port disappeared (unplug). Only port name is known. |
~ |
Serial number or VID:PID changed on an existing port (re-EEPROM) |
The state column carries open / closed on its own — no extra verb
needed. When a port toggles from closed to open (another process grabbed
it), the marker stays blank and the state column flips. Plug events
"punch out" of the stream visually because the + / - line is
sparse.
Why you'd use it¶
--watch is the fastest way to answer "which port is this cable?"
without guessing. Start termapy --watch, then plug the cable in:
the + line tells you exactly which COM number showed up. Unplug it
and the - line confirms. No ambiguity, no poking at Device Manager.
It's also handy for diagnosing flaky connectors — if a cable
repeatedly appears and disappears on its own, you'll see a stream of
+ / - events without touching anything.
Output width is ~140 columns and doesn't reflow to fit your terminal.
For long sessions, pipe to a file and less -S later:
--info: chip details for one or every port¶
Full multi-line per-port dump — same data the in-app /port.chip
command produces, but no TUI.
Example, one known and one unknown chip:
$ termapy --info
COM3
Description : USB Serial Device (COM3)
Manufacturer: Microsoft
Serial # : 020026702RYN040952
Location : 1-8.3:x.1
VID:PID : 04D8:000A
Model : unknown
USB Speed : unknown (chip not in lookup table)
Permissions : n/a
In Use : no
(chip 04D8:000A not in termapy's lookup table -- please report at https://github.com/hucker/termapy/issues so we can add it)
COM4
Description : USB Serial Port (COM4)
Manufacturer : FTDI
Serial # : D20JSV68A
VID:PID : 0403:6015
Model : FTDI FT230X / FT231X / FT234XD
USB Speed : USB Full-Speed (1 ms min latency)
Latency Timer: n/a (Windows - check Device Manager)
Max Baud : 3,000,000 baud
Permissions : n/a
In Use : no
The COM3 block shows what an unrecognized chip looks like: the VID:PID is always printed so you can identify it manually, and termapy nudges you to open an issue so the lookup table can grow.
Exits 0 if at least one port was printed, 1 if the named port wasn't found or no ports are connected.
Filtering and JSON output¶
--ports accepts these script-friendly filters (AND together):
| Flag | Effect |
|---|---|
--vid HEX |
Only USB devices matching this VID. Hex with or without 0x. |
--pid HEX |
Only USB devices matching this PID. |
--mfg STR |
Manufacturer substring match (case-insensitive). |
--sn STR |
Exact serial-number match (case-insensitive). |
--json |
Emit a JSON array instead of the column table; also for --chips and --usb. |
Example:
Each --ports --json record has a stable schema:
{
"device": "COM4",
"manufacturer": "FTDI",
"manufacturer_raw": "Future Technology Devices, Inc.",
"vendor": "FTDI",
"description": "USB Serial Converter",
"product": "FT232R USB UART",
"chip": "FTDI FT232R",
"speed": "Full-Speed",
"vid": 1027,
"pid": 24577,
"vid_pid": "0403:6001",
"serial_number": "AL01ABCD",
"in_use": false,
"driver": "ftdi_sio",
"location": "Hub_#0009.Port_#0004",
"interface_number": null
}
Three vendor-related fields, intentionally separate:
manufacturer_raw— the literal string the device descriptor or driver INF reports.manufacturer— the same string folded to a column-friendly short form (e.g."Future Technology Devices..."→"FTDI","Silicon Labs"→"SiLabs").vendor— the silicon vendor resolved from the VID per USB-IF assignment. Independent of what the descriptor / INF reports.
These often agree. They can disagree when a device uses a generic
driver — a Microchip USB-serial chip running on Microsoft's
usbser.sys reports manufacturer_raw = "Microsoft" (driver INF) but
vendor = "Microchip" (VID 0x04D8). Engineers diagnosing devices
benefit from seeing all three.
Numeric fields stay numeric; missing values are null (not omitted).
vid_pid is the canonical lowercase-hex form for literal matching.
driver is the kernel module name on Linux (ftdi_sio, cdc_acm,
ch341, cp210x, ...) and the Windows service name (FTDIBUS,
usbser, silabser, ...). On macOS driver is null for now.
Hardware-free CI: --ports DEMO¶
DEMO and DEMO_FAIL are virtual port names that the OS doesn't
enumerate but termapy can connect to. Naming one explicitly returns
a synthesized record so CI pipelines can exercise the CLI without
plugging in real hardware:
Bare --ports (no filter) does NOT include DEMO in the listing --
it only appears when you specifically ask for it, the same way
pyserial's loop:// URL handler is reachable but not enumerated.
--kill: stop leftover sessions¶
Every running termapy -- a TUI, a CLI, an MCP server an editor started, a
--web server -- leaves a breadcrumb in the app state folder
(%LOCALAPPDATA%\termapy\running\ on Windows) naming its config and the
port it has open. --kill reads them:
$ termapy --kill
PID MODE CONFIG PORT STARTED
----- ---- ------ ---- -------------------
21552 mcp m3_bin COM4 2026-10-27 16:38:45
28172 cli demo DEMO 2026-10-31 08:56:24
Bare --kill only lists. Give it a target to stop sessions, after a yes:
termapy --kill all # every termapy, say before an update
termapy --kill COM4 # whichever session holds COM4
termapy --kill m3_bin # the sessions on that config (m3* picks several)
Inside termapy, /app.running lists the same sessions and
/app.kill <target> stops them: /app.kill others every session but the
one you type it in, /app.kill all that one too. When a target picks the
session you type it in, that one goes last, through /exit, so it saves its
history.
With no terminal to answer on (a pipe), the answer is no. A stopped session
writes no Session End to its log. A session started before breadcrumbs
existed is not listed: end that one from Task Manager or kill, once.
Other flags worth knowing¶
| Flag | What it does |
|---|---|
--chips |
Dump the full USB-serial chip lookup table (VID:PID → model). |
--chips=ftdi |
Filter the table (case-insensitive substring match). |
--usb |
Whole USB tree with serial ports marked (Windows and Linux); pairs with --json. |
--check |
Validate your config, print JSON status, exit. |
--version |
Print the installed version and exit. |
--demo |
Set up (if needed) and start the demo device config. |
--board BOARD |
Start a dev board's starter config, creating it on first use from the attached board (see Dev boards). |
--new NAME PORT [SETTING ...] |
Create config NAME for PORT and start it connected (see Quick setup). |
--vt100 |
Start in VT100 passthrough mode (see VT100 mode). |
--no-color |
Plain output without ANSI color (implied by -e). |
--cfg-dir PATH |
Override the default config directory for this run. |
--cli |
Plain-text mode instead of the TUI. Default form is an interactive REPL; pair with --run or --exec for one-shot. With no config, shows a welcome banner listing available ports -- use /port.connect <name> to pick one. |
--run SCRIPT |
Run a .run script headlessly, then exit. |
-e, --exec CMD |
Run a single command and exit (CLI mode). Implies --cli and --no-color. See One-shot exec below. |
--silent |
Output level: nothing (script reads CmdResult.value only). |
--quiet |
Output level: command results only. |
--verbose |
Output level: results + data + progress chatter. |
Any REPL command also accepts --json to render its result as a JSON
envelope -- termapy --cli -e "/port.list --json" pipes cleanly to jq.
See Scripting.
termapy --help has the full list.
One-shot exec: -e / --exec¶
Run a single command, print its output to stdout, exit with status 0 (success) or 1 (failure):
termapy --cli my_device -e "AT+VER" # send a device command
termapy --cli my_device -e "/help port" # run a slash command
termapy --cli my_device --exec "/proto.crc.calc crc16-modbus 01 03 00 00 00 02"
One command per invocation: a second -e, or a line break inside one,
is refused. Several commands go in a .run file, run with --run.
Worked example¶
The general shape is termapy <cfg> --cli -e "<command>". For a
device whose cfg is my_device, asking for the firmware version
might look like:
One line from the device, exit 0, no banner, no echo prefix, no
ANSI -- ready to feed into awk, grep, or jq.
If you don't have a cfg set up yet, swap the cfg name for --demo
to use the bundled simulator (no hardware needed); the rest of the
shape is identical:
Exit code contract¶
Exit 0 if termapy successfully dispatched the command, 1 if the
dispatch itself failed. Termapy's dispatch is what's checked, not
the device's response. An unknown slash command exits 1 because
termapy can detect it locally:
But a device-side error text (e.g. the simulator's
"ERROR: Unknown command 'AT+NOPE'") still exits 0 -- termapy sent
the command and got a reply. Reading device errors from exit codes
requires a device profile that parses the response, or you can
grep -q the stdout yourself.
Quoting. A single-arg flag means multi-token commands must be
quoted, the same way git commit -m "msg" and bash -c "cmd" work --
the shell tokenizes before argparse sees anything:
termapy --cli cfg -e "start_system a=23 b=24" # MUST be quoted
termapy --cli cfg -e="start_system a=23 b=24" # = form also works
termapy --cli cfg -e start_system a=23 b=24 # WRONG: shell splits into 4 tokens
Suppressed chrome. Connect-time autorun (on_connect_cmd,
the help banner), the "Connected: ..." banner, and the
input-echo prefix (> AT+VER)
are all suppressed in --exec -- captured stdout contains only the
command's output. --exec also implies --no-color so ANSI
escapes never reach captured stdout. -e runs exactly one command;
if your one-shot needs init steps, write a .run file and use
--run.
Pair with request_mode=true in cfg to emit a JSON envelope
({cmd, success, error, elapsed_s, result}) instead of plain text.
Each envelope is one line, so a consumer can read stdout line by line;
json_style (/term.json) set to indent or color pretty-prints it
for a human instead.
Mutual exclusion. --exec and --run can't both be set; passing
both exits with an argparse error.
Try without hardware: TERMAPY_DEMO_FLEET¶
Want to see what --ports, --watch, or --info look like without any
USB-serial adapters plugged in? Set TERMAPY_DEMO_FLEET=1 and termapy
will enumerate a fixed three-port synthetic fleet (FTDI FT232R on COM3,
Silicon Labs CP2102 on COM4, Microsoft USB Serial on COM7) instead of
calling the OS. It is a switch, not a boolean: any non-empty value
turns it on.
TERMAPY_DEMO_FLEET=1 termapy --ports # fake three-port table
TERMAPY_DEMO_FLEET=1 termapy --info=COM4 # fake CP2102 info dump
Useful for docs, screenshots, trying the tool before you own hardware,
and cross-platform bug reports. This is a sibling to the DEMO port
(which fakes an open serial connection) and DEMO_FAIL (which makes
opens deterministically fail).