Skip to content
greyskysoulPublic

About

A minicom-style cross-platform serial terminal TUI (Windows + Linux) with robust YMODEM file transfer, HEX mode, VT/ANSI rendering, session capture, and a --bare headless pass-through for embedded firmware flashing and everyday serial debugging.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

PyCom

A minicom-style, cross-platform serial terminal TUI with robust YMODEM file transfer.

Built for embedded firmware flashing (STM32 & other ymodem bootloaders) and everyday serial debugging.

简体中文 · English

CI PyPI version Python versions License: MIT GitHub release GitHub stars

Screenshots

Main terminal
Main terminal — device ANSI/VT output with scrollback and live TX/RX counters

HEX mode with ASCII pane
HEX mode — hex bytes on the left, printable ASCII (grey dots for the rest) in the right-hand pane

Main menu popup Options screen
Ctrl+A menu popup and the options screen

Features

  • Full-screen terminal UI — device ANSI/VT output is rendered correctly, with scrollable history.
  • Light / dark themes — the One Half palette; one theme ships both variants, switchable in Options (“Auto” follows the terminal background via OSC 11). Extra themes are plain JSON files.
  • Ctrl+A prefix-key + overlay menu — familiar minicom interaction model.
  • YMODEM send / receive — CRC-16-CCITT, configurable 128/1024-byte blocks, timeout retransmission, progress display, cancellable.
  • ZMODEM transfer — lrzsz-compatible engine (pick it via Ctrl+A S/R).
  • Live port / baudrate / parity editing with persistent configuration.
  • Session capture (logging), local echo, line-ending conversion, HEX display, and more.
  • Overlays (menu / connection / options / confirm dialogs) auto-adapt to small windows (compact full-screen layout).
  • When the window is too small to be usable (< 20 columns or < 5 rows), prints a hint and exits cleanly.
  • --bare headless serial pass-through: stdin → serial port, serial RX → stdout, for driving by external processes such as AI agents.
  • Windows (Windows Terminal recommended) and Linux.

Installation

python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux
source .venv/bin/activate
pip install -e ".[dev]"

Or install from PyPI:

pip install pycom

Usage

pycom                          # start, then press Ctrl+A Z for the menu / auto connection dialog
pycom --port COM3 --baud 115200
pycom /dev/ttyUSB0 -b 921600
# send a string right after connecting (supports \n \r \t \xHH escapes)
pycom -p COM3 -s "AT\r"
# send a script file line by line (# starts a comment line)
pycom -p COM3 -f boot.txt
# exit automatically after 5 seconds without receiving any byte (-e supports fractional seconds)
pycom -p COM3 -s "AT\r" -e 5
# start with 16-hex receive/send mode (HEX) enabled
pycom -p COM3 --hex
# disable mouse capture (wheel/click is left to the host terminal)
pycom -p COM3 --no-mouse
# compatibility mode for a bare Linux virtual console (init3) / extreme terminals:
# force English, 16 colors, no animations, no mouse
pycom -p /dev/ttyUSB0 --compat
# force the UI language for this run (zh | en); PYCOM_LANG=... also works
pycom --lang en
# headless pure serial pass-through (--bare): hides all UI, requires a port.
# stdin bytes → serial, serial RX → stdout; hand the terminal to an AI agent, etc.:
pycom --bare -p COM3 -b 115200

Data bits / stop bits / flow control use the full names --data-bits/--stop-bits/--flow. -s/-f mean "send string/script after connecting" and require -p/--port.

--bare is a UI-less pure pass-through mode: it only uses the connection parameters (-p/-b/--parity/...) and cannot be combined with -s/-f/-e/--hex.

Extreme environments (Linux init3 / bare virtual console). A Linux virtual console (TERM=linux) only supports 8/16 colors, its font has no CJK glyphs, and it has no mouse reporting — the rich UI then renders garbled. PyCom detects this and automatically enables compatibility mode: English UI, a dedicated high-contrast ANSI theme using only the base 8 colours (the Linux console ignores bright backgrounds, which would otherwise leave fields background-less), animations off and mouse capture off. Force it manually with --compat, or opt out of the automatic detection with --no-compat. --lang en (or the PYCOM_LANG=en environment variable) forces the language for a single run without touching the saved preference. You can also simplify rendering yourself via Textual's environment variables: TEXTUAL_COLOR_SYSTEM=standard, TEXTUAL_ANIMATIONS=none and/or NO_COLOR=1 (the latter also strips the device's ANSI colors). In compatibility mode the checkbox/radio markers fall back to ASCII [ ]/[x] and the dropdown/collapsible arrows to v/^/>, since console fonts often lack the glyphs (○/●/▼/▲/▶).

Key bindings (Ctrl+A prefix)

Keys Action
Ctrl+A Z Open the main menu (floating popup)
Ctrl+A P Serial parameters (connection)
Ctrl+A S Send file (choose YMODEM/ZMODEM first)
Ctrl+A R Receive file (choose YMODEM/ZMODEM first)
Ctrl+A L Toggle session capture
Ctrl+A C Clear screen
Ctrl+A H Toggle 16-hex receive/send (HEX)
Ctrl+A O Options
Ctrl+A Y Language
Ctrl+A A About
Ctrl+A X Quit
Esc (in prefix) Cancel prefix

Local echo, auto-wrap, etc. live in the Ctrl+A O options overlay (off by default) rather than occupying prefix shortcuts.

HEX mode (Ctrl+A H, persistent): received bytes are shown as hex text on the left, with a right-hand ASCII pane — printable ASCII as characters, everything else as grey dots. A multi-line hex input area appears at the bottom (only valid characters, auto space-separated per byte, wrapping at 4/8/16/32 bytes per line depending on window width). Keys no longer send directly — press Enter in the input area (or click the bottom "Send" button) to parse the input as bytes. Toggling via shortcut auto-focuses the input area.

Virtual loopback device (debugging): start with --enable-debug, then LOOPBACK appears at the end of the port list in Ctrl+A P. No real port needed — every byte sent is echoed back (pure loopback), ideal for testing TX/RX and HEX display without hardware.

Mouse capture is on by default (needed for scroll-back and in-app selection); start with --no-mouse to hand wheel/click back to the host terminal.

Development

python tools/check.py   # same entry point as CI (see below)

It runs ruff check / ruff format --check / mypy / pytest, and then the packaging checks that mirror the CI build job: the version in pyproject.toml must match pycom.__version__, and the wheel must build and ship every runtime resource (app.tcss, the bundled themes) — a wheel that is missing one of them installs cleanly but the app cannot start. Pass --skip-build to skip that last step while iterating.

Packaging

pip install pyinstaller
pyinstaller packaging/pycom.spec    # produces the onedir layout dist/pycom/ (size-optimized)

Size optimizations (already in packaging/pycom.spec):

  • onedir layout: avoids onefile's per-launch self-extraction overhead, easier to inspect/remove unused runtime files on embedded devices
  • exclude ssl/network modules: a serial terminal doesn't need SSL — saves ~6MB (libcrypto/libssl)
  • exclude unused Textual widgets and stdlib extension modules (_decimal/_lzma/_bz2/_zstd, etc.)
  • strip + UPX: effective on Linux and Python 3.12; Windows + Python 3.14 auto-skips UPX due to CFG-enabled binaries

CI (.github/workflows/ci.yml) runs lint/type/tests and builds artifacts on Windows and Ubuntu (CI installs UPX automatically). Pushing a v* tag (e.g. v0.1.0) automatically creates a GitHub Release from the build artifacts.

Tech stack

  • Python ≥3.9, src layout, stdlib-first
  • Textual — terminal TUI framework (modal overlays / forms / file tree, native overlay menu support)
  • pyserial — serial port (including list_ports enumeration)
  • pyte — VT terminal emulation for the device RX byte stream (subclassing its Screen to capture scrolled-out content for history; LGPLv3)
  • In-house YMODEM engine (xfer/ymodem.py): CRC-16-CCITT, SOH/STX, 128/1024 blocks, configurable timeout/retry, duplicate-block tolerance, CAN-CAN abort, auto-retransmit on bad block, progress callbacks
  • In-house ZMODEM engine (xfer/zmodem.py): ZRQINIT/ZRINIT/ZFILE/ZDATA handshake, hex control headers + binary data subpackets with ZCRCW/ZACK, per-file session
  • Packaging: PyInstaller; testing: pytest (including Textual Pilot headless UI tests), ruff, mypy

Directory layout

src/pycom/
  app.py              main program (Ctrl+A prefix state machine, serial routing, transfer worker, capture)
  config.py           config data models + JSON persistence
  serialio.py         serial layer (background read thread, write lock, port enumeration, hot-plug)
  keys.py             key → byte mapping (line ending / backspace / arrow VT sequences…)
  termdisplay/
    vt.py             pyte terminal model (decode, scroll history, resize)
    view.py           TerminalView / StatusBar widgets
  xfer/ymodem.py      YMODEM bidirectional protocol engine (pure Python, unit-testable without serial)
  xfer/zmodem.py      ZMODEM transfer engine (pure Python, unit-testable without serial)
  screens/            connection, main menu, options, file/dir picker, transfer screens
  theme.py            theme-file loader (JSON, dark+light variants, user theme dir)
  resources/app.tcss  stylesheet (consumes theme variables only)
  resources/themes/   theme files (one-half.json = One Half, both variants)
tests/unit/           CRC/frame/block0, engine loopback (incl. error injection), keys, terminal model, Pilot UI
packaging/            PyInstaller launcher and spec

Configuration

The config file is JSON (%APPDATA%\pycom\config.json on Windows / ~/.config/pycom/config.json on Linux); edit it via Ctrl+A O and it saves during runtime.

Themes

Colours come from theme files: one theme ships both a dark and a light variant. Pick the theme and the mode (Auto = follow the terminal background / dark / light) in Ctrl+A O → Appearance.

To add your own theme, drop a JSON file into the themes/ folder next to the config file:

{
  "name": "my-theme",
  "label": "My Theme",
  "variants": {
    "dark":  { "primary": "#61afef", "variables": { "control-bg": "#3a4048" } },
    "light": { "primary": "#0184bc", "variables": { "control-bg": "#ffffff" } }
  }
}

primary is the only required key; unlisted variables keep their defaults (the full key list lives in the src/pycom/theme.py docstring). For an 8/16-colour terminal use "ansi": true with ANSI base-colour names such as ansi_red — that is how the built-in compatibility theme is written.

Known scope (Roadmap)

  • v1 included: YMODEM bidirectional transfer, ZMODEM transfer, capture log, line-ending/echo/decode/flow config, scrollback, HEX rendering
  • v1 not included: XMODEM/Kermit, ASCII send, macro scripts, dialing directory, split-pane multi-session
  • Recommended to run under Windows Terminal (full ConPTY/color support)

Interop testing

  1. Cross-validate with lrzsz on Linux: sz -Y file to PyCom receive; rz -Y to PyCom send
  2. ZMODEM: have lrzsz's rz receive a file that PyCom sends (Ctrl+A S, pick ZMODEM)
  3. Use socat(pty)/com0com virtual serial ports for end-to-end loopback on Windows/Linux
  4. STM32 bootloader flashing on hardware: verify at 115200/921600 each with a large file (SHA256 compare)

AI Disclosure

This project used an AI programming assistant (GitHub Copilot) during development to help write, review, and debug code. All code was manually reviewed and verified by automated tests (pytest / ruff / mypy).

About

A minicom-style cross-platform serial terminal TUI (Windows + Linux) with robust YMODEM file transfer, HEX mode, VT/ANSI rendering, session capture, and a --bare headless pass-through for embedded firmware flashing and everyday serial debugging.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages