Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ jobs:
- name: Smoke test source distribution
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py

- name: Smoke-test installed web frontend
run: |
uv run --isolated --no-project --with dist/*.whl tests/web_smoke.py
uv run --isolated --no-project --with dist/*.tar.gz tests/web_smoke.py

- name: Upload distributions as artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ jobs:
- name: Smoke-test source distribution
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py

- name: Smoke-test installed web frontend
run: |
uv run --isolated --no-project --with dist/*.whl tests/web_smoke.py
uv run --isolated --no-project --with dist/*.tar.gz tests/web_smoke.py

- name: Upload distributions
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to ctui are documented in this file.
The project follows [Semantic Versioning](https://semver.org/). Dates use the
ISO 8601 format.

## [Unreleased]

### Added

- Asyncio browser frontend included in standard installs via `--web` and
`CtuiApp.run_web()`, with packaged HTML/CSS/JavaScript and WebSockets.
- Shared browser views of one app session, existing custom-widget rendering,
completion, per-view help/confirmation/input popups, and optional authenticated
HTTPS access using supplied certificates.
- Browser-session integration tests, installed web artifact checks, and a
runnable custom-layout web tutorial.

## [1.0.0] - 2026-09-13

ctui 1.0 establishes the first stable public API for building typed command
Expand Down
108 changes: 106 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# ControlThings User Interface

`ctui` is an event-driven Python framework for command tools that work both as
full-screen terminal interfaces and traditional command-line programs. Write
full-screen terminal interfaces, traditional command-line programs, and optional
browser interfaces. Write
ordinary typed functions; ctui supplies parsing, validation, async execution,
completion, history, layout, and automatic CLI routing.

Expand Down Expand Up @@ -46,7 +47,7 @@ an event loop can use `await app.run_async()`. Test without a terminal using

## Automatic command-line mode

Every `CtuiApp` supports both interfaces without application-specific argument
Every `CtuiApp` supports these terminal interfaces without application-specific argument
parsing. Calling the program without arguments opens the full-screen UI:

```bash
Expand Down Expand Up @@ -89,6 +90,93 @@ Error: first must be float: 'wrong'
In the full-screen UI, an invalid command is restored to the input field and the
cursor moves to the beginning of the argument that needs correction.

## Browser frontend

Browser support is included in the standard installation. Serve the same
application's commands and supported custom widgets in a browser:

```bash
python -m pip install ctui
python my_tool.py --web
python my_tool.py --web --web-port 0
```

No flags still opens the full-screen terminal interface. `-c`/`--command` and
`-f`/`--file` still select CLI execution. `--web` selects browser-only mode and
cannot be combined with CLI batches. The terminal prints the browser address
and waits; Ctrl-C stops the server. Port 8080 is the default; port 0 chooses an
available port. Use different ports to start independent application sessions.

The startup URL includes a random session token in its fragment. Open that URL
to authenticate; the browser removes the fragment after exchanging it for an
HttpOnly session cookie. Treat the startup URL as a credential. Additional tabs
at the same address share app state and output. Draft commands, completion,
scrolling, focus, and confirmation/help dialogs belong to each tab. `help` opens
a scrollable popup and preserves the output window. `exit` or Ctrl-Q stops the
whole session. Closing all tabs leaves the server and submitted commands running;
other views receive completed results. Unanswered dialogs are cancelled when their
requesting tab disconnects.

The browser uses packaged HTML, CSS, and JavaScript with no frontend build step,
external assets, or JavaScript framework. WebSocket updates and commands run on
the same asyncio loop as app lifecycle hooks. Existing async commands can overlap;
append results use current output when each command finishes. Each view has a
bounded, coalescing update queue so a slow browser does not hold up other views.
Reconnecting views receive the current layout and output.

Typing `exit` asks for confirmation in both terminal and browser UIs. An approved
exit stops the shared application session. In browser mode, the submitting tab
attempts to close; if browser policy blocks it, a message tells you to close it
manually. Other tabs disconnect. Noninteractive CLI use requires `exit confirm`.

### Custom layouts and lifecycle

Reuse `compose()` and `ctui.widgets`: Vertical, Horizontal, Frame, Label, TextArea,
Window, Button, and ProgressBar. Shared buffer text, frame titles, callable
labels/footer text, visibility, and dimensions are translated into browser
widgets. Buttons invoke the same handlers; editable custom text areas update
shared state. Call `self.app.invalidate()` after changing application-owned
labels or progress. Text-buffer changes trigger web redraws automatically.

Web mode runs `on_start`, builds widgets and the runtime, then calls `on_ready`.
`self.app` supports `invalidate()`, `exit()`, and `create_background_task()`;
terminal renderer internals are unavailable. Shutdown cancels pending work
before `on_stop` and service cleanup. In an existing event loop, use
`await app.run_web(port=8080)` instead of `run()`.

CTUI's message, confirmation, and text-input dialogs work from browser-triggered
commands and callbacks. Native browser keyboard reservations and text selection
still apply. Arbitrary third-party prompt-toolkit controls, custom floating
containers, and renderer-specific extensions need a browser adapter; unsupported
controls produce an explicit startup error rather than a partial interface.

Try the complete example:

```bash
uv run examples/15_web_frontend.py --web --web-port 0
```

### Optional remote access and HTTPS

Loopback HTTP is the default. To listen on other interfaces, explicitly specify
a bind address and supply a certificate and private key:

```bash
python my_tool.py --web --web-host 0.0.0.0 --web-port 8443 \
--web-cert server-cert.pem --web-key server-key.pem
```

Open the printed address using the server's reachable hostname/IP in place of
localhost when binding all interfaces. Use a certificate trusted by the browser
and valid for that hostname/IP. HTTPS automatically uses secure WebSockets;
remote plaintext binds and incomplete certificate/key pairs are rejected.
Automatic certificate issuance and reverse-proxy configuration are not included.

To reuse a supplied token, set an environment variable containing at least 24
characters and pass `--web-token-env VARIABLE_NAME`. All connections still
require authentication. Browser origins are checked and loopback hosts are
validated. TLS encrypts traffic; the session token authorizes app access.

## Help and interface guidance

In the full-screen UI, `help` opens a scrollable popup and preserves the main
Expand Down Expand Up @@ -423,3 +511,19 @@ uv sync
uv run python -m unittest discover -s tests -v
uv run examples/filesystem.py
```

## Browser verification for contributors

The portable unittest suite includes real HTTP/WebSocket integration tests. Additional Playwright checks exercise browser behavior
and all 16 tutorial apps through their actual `--web` entry points:

```bash
uv run --with playwright python -m playwright install firefox
CTUI_BROWSER=firefox uv run --with playwright tests/browser_smoke.py
CTUI_BROWSER=firefox uv run --with playwright tests/browser_tutorials.py
```

Use `CTUI_BROWSER=chromium` for Chromium; `CTUI_CHROMIUM_EXECUTABLE` can select
a system installation. To repeat one tutorial, append its filename, such as
`15_web_frontend.py`, to `browser_tutorials.py`. These checks use temporary
application storage and close their servers; they do not require real devices.
2 changes: 2 additions & 0 deletions RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Remove or move any previous `dist/` directory, then run:
uv build
uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
uv run --isolated --no-project --with dist/*.whl tests/web_smoke.py
uv run --isolated --no-project --with dist/*.tar.gz tests/web_smoke.py
```

Inspect both archives. They should contain the license; the source archive
Expand Down
37 changes: 37 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,40 @@ relevant entries per task, broadening for architectural work. Preserve decision
rationale and supersession history instead of compressing away API constraints.
This refines D11's instruction placement and startup reading, not its requirement
for durable project continuity.

## D15 — Asyncio browser frontend

Frontend design recorded October 1 during development on `feat/web-frontend`.
The optional-dependency choice was made without owner approval and is superseded
by the owner's October 5 direction: aiohttp is a required dependency so a standard
installation supports browser mode without extra installation flags.

One CtuiApp definition supplies three mutually exclusive startup modes: no
arguments opens the existing terminal UI, -c/--command and -f/--file run CLI
operations, and --web starts only the browser frontend. Print a connection URL
and keep the process running. A process owns one live session; separate
processes/ports own separate sessions. Multiple tabs at the same address share
app state/output while drafts, focus, scrolling and dialogs stay per-view.
Owner direction, October 5: typed `exit` requires confirmation in both interactive
frontends. On approval the shared session stops; the submitting browser tab
attempts to close, with a manual-close message if browser policy prevents it.
Existing explicit confirmation tokens remain available to noninteractive callers.

Submitted commands continue after their tab disconnects; unanswered confirmation
or input dialogs cancel on disconnect, preserving explicit approval.

Use packaged HTML/CSS and small JavaScript modules/functions with no frontend
framework or build step. aiohttp is a required dependency for HTTP/1.1 and
WebSockets on the existing asyncio loop. Loopback HTTP is the default; explicit
remote binds require supplied TLS certificate/key files and authenticated access.
HTTPS uses secure WebSockets. Automatic certificate issuance is deferred.

Preserve the existing supported compose() API using a browser adapter over its
prompt-toolkit tree. Compatible Button/Frame/ProgressBar subclasses retain
widget identity through upstream container conversion. This avoids forcing
existing apps to define a second layout or migrating the supported terminal API.
An independent declarative widget model is not introduced in this change.
Arbitrary third-party controls and custom floats require future adapters and
fail explicitly. Implementation/lifecycle, queue bounds, session credentials,
and limitations live in the application/web/layout/dialog docstrings and README.
Help remains a per-view scrollable popup that preserves the main output.
Loading
Loading