Developing¶
Setup¶
Common tasks¶
make frontend # build the frontend (committed to src/terminux/web/static)
make frontend-dev # live Vite dev server, proxying to a running backend
make frontend-test # vitest unit tests (pure TS logic, no browser)
make test # vitest, then pytest: unit, integration, e2e
make lint # ruff + ty + pyrefly + mypy
make format # ruff (Python) + prettier (frontend)
make test-cov # pytest with HTML coverage report
For a live frontend loop: run uv run terminux --no-window in one terminal,
make frontend-dev in another. The Vite dev server proxies API/WS calls to the
running backend.
Testing tiers¶
The pytest suite has three tiers, selectable by marker:
unit: fast, isolated.integration: component interaction.e2e: drives the served UI with a real browser (Playwright, no pywebview).
The e2e tier needs the Playwright browser once:
Frontend logic is covered separately by vitest (make frontend-test), which
make test runs first.
Type checking¶
make lint runs four type checkers (ty, pyrefly, mypy) plus ruff.
mypy is in strict mode. The frontend has its own check:
Documentation¶
Docs are written in Markdown under docs/src/ and built with
Zensical. Configuration lives in
docs/zensical.toml.
make docs-serve # live preview with rebuild-on-save (http://127.0.0.1:8000)
make docs # static site → docs/site/
To add a page, create docs/src/<name>.md and add it to the nav array in
docs/zensical.toml.
Debugging knobs¶
--debug and --trace¶
Two CLI flags raise the noise level when something goes wrong:
| Flag | What it does |
|---|---|
--debug |
terminux.* loggers at DEBUG, uvicorn INFO with access logs, ms-precision timestamps on every line. Slow-I/O warnings (see below) are surfaced. |
--trace |
Everything --debug enables plus root logger at DEBUG: every save_state, save_scrollback, load_scrollback, and cwd lookup logs its individual duration. Very chatty; use only when actively chasing a freeze. |
uv run terminux --debug # daily-driver verbose
uv run terminux --debug 2>/tmp/term.log # also redirect to a file you can grep
uv run terminux --trace 2>/tmp/term.log # extreme mode for a known-reproducer
Slow-I/O warnings¶
Every disk-touching call on the server is wrapped in a time_op(name)
context manager (see src/terminux/diagnostics.py). Operations slower
than 200 ms log at WARNING regardless of the log level, so a slow
APFS volume, a wedged network mount, or a busy disk shows up as
slow save_state[12KB]: 1340ms before it escalates into a hang.
Wrapped sites:
save_stateandsave_scrollback(both end inos.fsync)load_scrollback(sync file read up to 2 MB)_libproc_cwdand the_lsof_cwdfallback on macOS
Event-loop watchdog¶
A daemon thread reads a heartbeat the asyncio loop is expected to bump
twice a second. If the loop falls more than 3 s behind, the watchdog
logs at WARNING and automatically calls
faulthandler.dump_traceback(all_threads=True) to stderr: the same
per-thread Python stack dump you'd get from py-spy dump. The dump
includes whichever syscall the offending thread is parked in (fsync,
subprocess.communicate, os.write, …), which is the exact evidence
needed to find the root cause. It re-warns every 10 s while the stall
persists.
The watchdog is started automatically by terminux.app.main(); no flag
required.
Manual stack dump¶
If you suspect a hang but the watchdog hasn't fired yet, send the process a signal yourself for the same dump on demand:
# macOS — SIGINFO is what Ctrl-T sends in a normal foreground terminal
kill -INFO <pid>
# Linux
kill -USR1 <pid>
<pid> comes from ps aux | grep terminux | grep -v grep. The dump
lands on the same stderr stream as the watchdog's output.
TERMINUX_PTY_LOG¶
When set in the environment, every PTY read is appended to <path> as a
one-line trace: <epoch.ms> <terminal-id-prefix> <byte_count> <repr of
first 120 bytes>. Useful for characterizing what an "idle" TUI actually
emits when you suspect a sidebar dot is reacting to noise rather than
real work. It is off by default and leaves no trace on disk in normal
runs.