Skip to content

Contributing

Setting up

git clone <repo> && cd turbodesk
uv sync
make test

Python 3.12 or later. Everything runs through uv.

The toolchain

make test        # 2,447 tests
make lint        # ruff, ty, pyrefly, mypy --strict, zuban --strict
make format      # ruff format + --fix

Five type checkers, because they disagree and the disagreements are where the bugs are. ty passed on code where mypy found eight real errors. mypy --strict finds one more than zuban --strict. zuban can check examples/ where mypy cannot, because two __main__.py files collide on module name.

Tests

A pyramid, and the directory decides the marker:

tests/a_unit/          pure functions, no I/O, no event loop   (~0.3 s)
tests/b_integration/   widgets and apps through a UI
tests/c_e2e/           real programs on a pty
uv run pytest -m unit
uv run pytest -m "not slow"     # skips pty and tmux tests

Conventions the existing tests follow:

  • Name the behaviour. test_a_listbox_scrolls_to_keep_the_selection_in_view.
  • Arrange, act, assert, separated by blank lines.
  • Parametrize when it removes duplication.
  • Assert on rendered output wherever that is possible.

House rules

Never silence a linter. No # noqa, no # type: ignore. If a rule is wrong for this codebase, ignore it in ruff.toml with a one-line reason next to it. A growing list of per-line suppressions means the types or the structure are wrong.

Comments say why. The code already says what.

Keyword-only arguments for anything after the required ones on a public function. PLR0917 is deliberately not ignored, and it is what keeps this true.

Immutable by default. NamedTuples and frozen dataclasses for values; mutable dataclasses only where something is genuinely assigned after construction (a Task, a future's result).

Adding a widget

A widget is a function returning a View. There is no base class. Writing a component builds one end to end and ends with the checklist a pull request has to pass.

The two conventions that matter: the caller owns the state, and return a View unless the caller needs more than pixels. Put pure helpers (width arithmetic, key tables, formatting) at module level so they can be unit-tested without a UI. column_widths in widgets/table.py and less_keys.action_for are the pattern.

Adding to the core

The bar is higher. Before adding to view.py or runtime.py, check whether an application can do it. Prefer letting it, until a second application wants the same thing.

Both ported applications needed screen navigation; they needed different shapes, so turbodesk has open_modal and no Screen class. One application would have produced an abstraction the second did not want.

The one amendment: a component named in the catalogue in notes/turbodesk-roadmap.md may be promoted on one caller, provided it ships with a demo that exercises it on its own. Waiting for the second caller is what keeps a library honest, and it also keeps it permanently one demo behind, which is the wrong trade now that the component set is a goal in itself.

The notes

notes/ holds the design record: a Pythonic-design review, a plan and verdict per port, and lessons-learned.md. They are worth reading before proposing a change, because several obvious-looking ideas are in there with a reason they were rejected.

notes/turbodesk-roadmap.md is the current list of what is missing, in the order it blocks things. If you are looking for something to work on, start there.

If you find something that costs an hour to rediscover, add it.

Documentation

Built with Zensical:

The docs are their own project under docs/, with sources in docs/src/ and their own Makefile:

make docs            # or: cd docs && make build
make docs-serve      # live reload
make docs-check      # strict — warnings become errors

Zensical validates internal links. make docs reports a bad cross-reference as a warning; make docs-check fails the build on it, which is what CI should run.