Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,16 @@ jobs:
- name: Check packages
run: moon check --deny-warn

- name: Check packages (wasm)
run: moon check --target wasm --deny-warn

- name: Run tests
run: moon test --deny-warn

- name: Run tests (wasm)
if: runner.os != 'Windows'
run: moon test --target wasm --deny-warn

- name: Generate interfaces
if: runner.os != 'Windows'
run: moon info
Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ pane system, or scrollback buffer.

## Status

This module is early and native-target focused. Public APIs are still being
shaped around small terminal programs and the examples in this repository.
This module is early. Public APIs are still being shaped around small
terminal programs and the examples in this repository. It targets `native`
and `wasm` (see "Wasm backend" below).

## Packages

Expand All @@ -23,7 +24,8 @@ operations.
Use `Tty` when an operation needs a real terminal handle:

- process stdio: `Tty::stdio()`
- controlling terminal: `Tty::open()`
- controlling terminal: `@tty/open.open()` (native-only sub-package
`moonbit-community/tty/open`, which also adapts raw fds via `Terminal`)
- custom handles: `Tty::new(input, output)` with `Reader` and `Writer` traits
for async files, stdio, and OS pipes
- raw mode: `Tty::get_state`, `State::make_raw`, `Tty::set_state`,
Expand Down Expand Up @@ -151,6 +153,32 @@ The examples are manual validation tools, not framework APIs:
- `examples/agent` demonstrates a Codex-like primary-screen transcript, input
composer, delayed queued input, and shell command execution.

## Wasm backend

On the `wasm` target (run with `moonrun`), terminal syscalls have no libc to
call into, so the package spawns a small native **sidecar** process on first
use: it inherits the program's stdio, performs `termios`/`ioctl`/console
calls on the shared terminal, and is reached over loopback TCP with a
compact binary protocol (see `sidecar/PROTOCOL.md` and the "Wasm Backend"
section of `docs/architecture.md`). The sidecar restores the terminal even
if the program dies while in raw mode.

Differences from native:

- syscall-backed operations are `async` on wasm: `isatty`,
`Tty::window_size`, `Tty::enter_raw_mode` / `Tty::leave_raw_mode` (which
take and return nothing — `State`, `get_state`, `set_state` are
native-only; the sidecar keeps the state), and `Tty::new` / `Tty::stdio`.
- `isatty` identifies process stdio handles only; other handles report
`false`.
- `@tty/open` (controlling terminal) is not available on wasm.
- On Windows, the sidecar streams raw console `INPUT_RECORD`s, so mouse,
focus and resize events keep native fidelity.
- Supported wasm hosts: linux/x86_64, macos/aarch64, windows/x86_64. The
sidecar binaries are embedded (`sidecar_binaries_wasm.mbt`, ~540 KB of
source) and regenerated with `cd tools && moon run build_sidecar` (uses
`zig cc`, downloading zig on demand).

## Design Boundaries

- `tty` owns terminal handles, platform state, raw mode, terminal size, cursor
Expand Down
38 changes: 36 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ operations:

- `Tty`, a narrow handle that coordinates one terminal input stream and output
stream for terminal capabilities and request/response protocols
- `Tty::stdio` and `Tty::open` convenience constructors for process stdio and
`/dev/tty` style handles where supported
- `Tty::stdio`, the convenience constructor for process stdio
(`@tty/open.open()` in the native-only `open` sub-package covers `/dev/tty`
and Windows console devices)
- `Reader` and `Writer`, terminal-handle traits that extend async I/O with
descriptor and close operations for `Tty::new`
(`moonbitlang/async` files, stdio handles, and OS pipes implement them)
Expand Down Expand Up @@ -227,6 +228,39 @@ their own source order, but `Tty::read_event` does not guarantee a strict total
order between native resize/focus notifications and terminal byte sequences;
this matches the Unix signal-backed resize model.

## Wasm Backend

The `wasm` target (run under `moonrun`) has no libc surface, so terminal
syscalls are proxied to a small native **sidecar** executable
(`sidecar/tty_sidecar.c`), spawned on first use with the parent's
stdin/stdout/stderr inherited and reached over loopback TCP with a compact
binary protocol (`sidecar/PROTOCOL.md`). Terminals are addressed by stdio
slot (0/1/2) because wasm host handles are opaque and cannot cross the
process boundary; terminal state itself never crosses the wire (`ENTER_RAW`
captures and restores inside the sidecar, which also restores the terminal
if the program dies while raw).

- `internal/sidecar` owns the wire protocol (`Request`/`Response` encode and
decode, handshake, cancellation-safe `EventStream`) and is pure enough to
be unit-tested on native; `internal/sidecar/livetest` exercises the real
executable end-to-end.
- The sidecar binaries for linux/x86_64, macos/aarch64 and windows/x86_64
are cross-compiled with `zig cc` by `tools/build_sidecar` (a MoonBit tool)
and embedded in the generated `sidecar_binaries_wasm.mbt`.
- Unix input still flows through the parent's own stdin and the shared ANSI
decoder; only resize notifications come from the sidecar (`SIGWINCH` is a
signal, which wasm cannot receive), coalesced on a non-blocking event
connection so a non-draining parent can never stall it.
- Windows input keeps native fidelity: with `WATCH` enabled the sidecar is
the console's sole `INPUT_RECORD` consumer and streams raw records to the
shared `internal/win32` record router (`StreamEventReader`); resizes arrive
as `WINDOW_BUFFER_SIZE_EVENT` records.
- Wasm API differences: syscall-backed operations (`isatty`,
`Tty::window_size`, raw-mode control) are `async`; `State` does not exist
(`enter_raw_mode`/`leave_raw_mode` return/take nothing);
`isatty` identifies process stdio handles only; `@tty/open` is
unavailable. `pkg.generated.mbti` records the native surface.

## Raw Mode

Raw mode is input-side terminal state, but root callers should access it through
Expand Down
1 change: 1 addition & 0 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ This board tracks implementation direction for `moonbit-community/tty`. Use
| TTY-23 | done | serialize root `Tty` output writes | `docs/plans/2026-06-15-output-write-lock.md`, `internal/lock/`, root package | a single `Tty::write`/`Tty::write_string` call lands atomically under concurrent writers via an internal `Lock`, with no public API change | `moon fmt`, `moon test internal/lock`, `moon test .`, `moon test`, `moon check`, `moon info`, `git diff --check` |
| TTY-24 | done | move Windows console input parsing state into `internal/win32.EventReader` | `docs/plans/2026-06-29-win32-event-reader.md`, `internal/win32`, root Windows input code | root `Tty` delegates native record parsing state to the internal Win32 reader while keeping public API unchanged | `moon fmt`, `moon test internal/win32`, `moon test . --filter "win32*"`, `moon check`, `moon info`, `git diff --check` |
| DEP-1 | in progress | upgrade `moonbitlang/async` 0.19.1 -> 0.19.4, working around the Windows console-open regression by opening `CONIN$`/`CONOUT$` directly and wrapping both platforms' terminal fd in `RawFdStream` | `docs/plans/2026-06-15-async-0.19.4-upgrade.md`, all workspace `moon.mod` files, `tty_open.c`, `tty_win32.mbt`, `tty_unix.mbt`, `io.mbt`, `isatty.mbt` | every module pins `0.19.4`, console devices bypass `@async/fs.open`, and CI stays green on all platforms; `.mbti` gains only the intended `RawFdStream` impls | `moon fmt --check`, `moon check --deny-warn`, `moon test --deny-warn`, `moon test` (`tests/`), `moon info`, `git diff --check` |
| WASM-1 | active | wasm backend via native syscall sidecar (zig-built, embedded), `open/` split, Windows INPUT_RECORD streaming | `docs/plans/2026-07-29-wasm-sidecar-backend.md`, `sidecar/`, `internal/sidecar/`, `internal/win32/`, root package, `open/`, `tools/` | root package builds and runs on `--target wasm` with isatty, window size, raw mode, resize events, and full-fidelity input; native surface unchanged except `Tty::open` → `@tty/open.open()` | `moon fmt`, `moon check`, `moon check --target wasm`, `moon test`, `moon test --target wasm`, `internal/sidecar/livetest`, pty smoke via `tests/probe` |

## Current Rules

Expand Down
Loading
Loading