Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

hqtui — Python

Fetch latest main and run the full demo (from any directory):

# Vanilla: installed toolchain
curl -fsSL https://hqtui.com/demo.sh | sh -s -- --system python
# Mise: pinned toolchain
curl -fsSL https://hqtui.com/demo.sh | sh -s -- --mise python

Both update a private cache before running, print the exact commit, and leave your checkout alone. Append --sim or --snapshot directly. Review the launcher before executing it. In an updated checkout, mise run demo:python also updates; mise run demo-local:python and the direct example commands below run local source without updating.

High Quality Terminal UI for Python. btop-grade dashboards with a one-import API, dark by default, standard library only — nothing in dependencies, nothing to pin.

This is a native port of the TypeScript reference implementation, not a binding. There is no Node in the picture at runtime or at install time.

Not on PyPI yet — it ships in the monorepo. To run it now:

git clone https://github.com/profullstack/hqtui
cd hqtui/ports/python && python examples/dashboard.py

To use it from your own project, install the checkout in editable mode:

pip install -e /path/to/hqtui/ports/python

Hello, terminal

from hqtui import App, Panel

app = App()
app.render(lambda f: f.ui.panel(
    Panel(title="Hello"),
    lambda p: p.text("Hello, terminal."),
))
app.start()

App() already gives you a dark theme, truecolor with automatic 256/16 fallback, mouse tracking, the alternate screen, resize handling, 30fps adaptive rendering (15 over SSH), and a terminal that is restored however the process dies.

Handlers close over your state

Python keeps the reference implementation's shape, because Python closures capture by reference and the runtime has a garbage collector. The Rust port has to invert this; Python does not:

import hqtui.widgets as w
from hqtui import App, ScrollHandlers

state = {"selected": 0}
app = App()

app.on("key", lambda e: state.update(
    selected=state["selected"] + (1 if e.name == "down" else -1 if e.name == "up" else 0)
))

app.render(lambda f: f.ui.table(
    w.TableOptions(
        rows=[w.TableRow(("1", "systemd"))],
        columns=[w.TableColumn("PID"), w.TableColumn("NAME")],
        selected=state["selected"],
    ),
    # The wheel acts on whatever is under the pointer.
    ScrollHandlers(on_select_row=lambda row: state.update(selected=row)),
))
app.start()

Run the examples to see it working:

python -m examples.hello          # the smallest app
python -m examples.dashboard      # full native ten-screen demo
python -m examples.dashboard --sim # generated sample telemetry
python -m examples.dashboard_mini # small library example
python -m examples.screenshot     # renders to stdout, no TTY needed

Testing without a terminal

from hqtui import Panel, render_to_screen

screen = render_to_screen(80, 24, "dark", lambda ui: ui.panel(
    Panel(title="CPU"), lambda p: p.text("72%")))
assert screen.contains("72%")

render_to_screen also gives you .ansi() for a colored screenshot, .cell(x, y) for structural assertions, and .regions so a test can prove a widget is actually reachable by a click.

How this stays honest

Every port replays a shared corpus of fixtures generated from the TypeScript implementation: the same widget arguments must produce the same cells, the same colors and the same escape bytes.

python -m unittest discover      # 13 conformance groups, ~9k reference cells

That covers colors and all 256 palette quantizations, Unicode widths and grapheme clustering, the layout solver, the framebuffer, the diff encoder's exact output bytes, Braille rasterisation, every border style, all 53 widget scenes and 10 whole-screen layouts.

Where this differs from the reference, and why

Three places, all documented in the source at the point they matter:

The framebuffer uses array, not lists. Four array("L") planes instead of a list of cell objects. This matters more here than it does in the reference: a list of boxed integers for a 200x50 screen is ten thousand objects per frame, and the whole design exists to avoid exactly that.

NaN does not propagate the same way. math.floor(nan) raises where JavaScript's Math.floor returns NaN, so round_half_up passes NaN and the infinities through, and clamp01 turns NaN into 0. Both reach the same empty state the reference draws, without the exception.

Tables take strings. The reference is generic over a row type and reads cells by key. TableRow(("1", "systemd")) keeps the widget out of the business of formatting someone else's objects.

Everything else — layout, widgets, colors, glyph selection, escape output — is identical, and the conformance suite is what says so.

Terminal handling

Python has the easiest job of the four ports: termios, tty, signal, select and os.get_terminal_size are all standard library and correctly implemented per platform. There is no FFI (as in Rust) and no subprocess.

One thing that falls out of that: poll_input waits on the file descriptor itself with select, rather than on a clock. The other ports have to time the Escape-key ambiguity against their own frame budget.

Windows has no POSIX terminal layer here, so the interactive App does not run there — but everything above it (the framebuffer, widgets, encoder and headless renderer) is pure computation and works fine.

License

MIT.