web-ui/README.md
A non-terminal UI for Fresh, driven end-to-end by the real Rust Editor — no
mock model. See docs/internal/UNIFIED_SCENE_DESIGN.md (design + phased plan) and
docs/internal/NON_TERMINAL_UI_RESEARCH.md (original research).
The guiding principle: the TUI and the web must not re-implement the same logic. Everything semantic — which menus/items exist, what's enabled/checked, accelerators, status segments, the settings tree, etc. — is derived once in the core; each frontend only renders it.
crates/fresh-editor/src/view/scene.rs (Editor::menu_view(),
tab_bar_view(), status_view(), palette_view(), popups_view(),
file_explorer_view(), trust_dialog_view(), widgets_view(),
context_menu_view(), keybinding_editor_view(), settings_view()): menu bar
Editor::handle_key / handle_mouse (and shared hit→action dispatch for
settings/widgets/keybindings); the page re-renders from the editor's new state.browser (web-ui/index.html) ──HTTP──► fresh::webui bridge ──► real Editor
chrome = native HTML from GET /state runs Editor::render (piece tree,
scene.rs projections POST /key into a cell buffer, highlighter,
buffer = real highlighted POST /mouse reads the pipeline's handle_key, …)
CELLS (SVG) POST /action layout caches + cells
key/mouse ─► POST POST /resize
The bridge (crates/fresh-editor/src/webui/mod.rs) runs the actual
Editor::render once into an in-memory Buffer. Editor::suppress_chrome_cells
makes the pipeline compute chrome layout/geometry/semantics but not draw
chrome cells, so the cell buffer carries pane interiors only. The bridge then
serializes the scene.rs projections (chrome) and slices the rendered cells
(buffer interiors). Nothing is re-implemented — layout, highlighting, tabs,
scrollbars, split borders and item state all come from the core; only the final
drawing is re-targeted. The TUI keeps suppress_chrome_cells = false, so its
rendering is unchanged.
Each poll runs editor_tick (drains async LSP/plugin/file events, steps
animations), so frames advance without user input, exactly like the TUI loop.
cargo run -p fresh-editor --example webui_server -- 127.0.0.1:8137 \
crates/fresh-editor/src/view/scene.rs # or any file(s)
# then open http://127.0.0.1:8137 and type — edits go through the real editor.
⚠️ The bridge binds plain localhost HTTP and hosts a live editor with filesystem access. It's a local-development prototype, not for exposure on a shared interface.
test/drive.mjs drives the real bridge in headless Chromium: it asserts the
buffer interior is the pipeline's real syntax-highlighted cells while all chrome
is native HTML (no cell-drawn chrome), and that key / mouse / menu / palette /
settings / widget interactions run through the real Editor. 50 assertions
across the chrome surfaces, plus screenshots.
# 1) start the bridge (see above) on :8141
# 2) run the driver
CHROMIUM=/path/to/chrome UI_URL=http://127.0.0.1:8141 node web-ui/test/drive.mjs
(Defaults: CHROMIUM=/opt/pw-browsers/chromium-1194/chrome-linux/chrome,
UI_URL=http://127.0.0.1:8141, SHOTS=/tmp/pw/shots.)
A Rust web/TUI parity test (crates/fresh-editor/tests/scene_parity.rs) drives
one Editor and asserts the chrome the web scene reports also appears in the
TUI's cell rendering — so the two renderers can't diverge on what the chrome is.