Skip to content

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:

termapy --ports=COM4

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:57 shows the three ports that were plugged in when --watch started, all closed.
  • 15:58:01 — something opened COM7 (blank marker, state column flipped to open).
  • 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:

termapy --watch > watch.log
less -S watch.log

--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.

termapy --info          # every port
termapy --info=COM4     # just one

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:

termapy --ports --vid 0403 --json | jq '.[].device'

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:

termapy --ports DEMO --json
termapy --info DEMO

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:

$ termapy my_device --cli -e "AT+VER"
Firmware v1.4.2
$ echo $?
0

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:

$ termapy --cli --demo -e "AT+INFO"
Bassomatic v77 v1.0
Uptime: 0h 0m 0s
Free memory: 29135 bytes

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:

$ termapy --cli --demo -e "/notacommand"
  Error: Unknown command: notacommand
$ echo $?
1

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).