You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Line anchors are against current main and will drift.
TL;DR
Rio has publicly wanted WASM plugins ("in the future you'll define the tab system with a WASM plugin"). This is a concrete, minimally-invasive design to get there, plus an offer to build it in phases.
The core idea: let a pane be backed by something other than a PTY. A plugin pane runs a sandboxed WebAssembly component that renders by emitting a serializable draw-command list (translated into sugarloaf primitives on the render thread) and receives input events β using Rio's existing per-pane render loop, event bus, and input dispatch. Nothing about the windowing, tab/split, or layout system changes.
The enabling step β generalizing Context into a pane kind β is useful to Rio independent of plugins (it's what any non-terminal pane needs: a dashboard, a help view, a settings surface).
I'd like to shape the plugin API with you rather than drop a finished fork β open questions are at the end.
Why this fits Rio cleanly
Reading the code, almost everything above a single pane is already pane-agnostic:
ContextGrid (the per-tab Taffy layout) and ContextManager (tabs/splits/switch/close) operate on opaque panes β layout/mod.rs, context/mod.rs.
Renderer::run iterates grid.contexts_mut() per pane and only needs each pane's render snapshot + rich_text_id β frontends/rioterm/src/renderer/mod.rs (~271, pane loop ~304).
The user-event bus RioEvent + EventProxy already carries per-route_id "this pane changed, redraw" messages into the loop β rio-backend/src/event/mod.rs, dispatched in application.rsuser_event (~212).
The PTY assumption is concentrated in one type: Context (context/mod.rs ~49), created by one factory: ContextManager::create_context (~225).
Design
1. Pane-kind generalization (the enabling refactor)
Make Context carry a backend rather than unconditional PTY fields:
Everything above Context stays as-is. This is the bulk of the value and lands as a standalone PR. (Leaks to fix, all bounded β see below.)
2. Plugin host + lifecycle (reuse the Machine pattern)
A PluginHost (wasmtime + component-model linker + instance pool) lives on Screen, peer to context_manager/renderer. Each plugin instance runs as a Send worker thread holding an EventProxy clone β exactly like the PTY reader Machine (rio-backend/src/performer/mod.rs ~65). It signals redraw via event_proxy.send_event(RioEvent::Te
10BC8
rminalDamaged(route_id) | PluginRender(route_id), window_id); the existing user_event path already marks the route dirty and requests a frame. No new event loop.
3. Rendering: a serializable draw-command IR (the rich path)
Sugarloaf is &mut, single-threaded, non-Send GPU state β a WASM guest can't hold it. So the contract is a serializable draw-command list the guest emits each frame and the host replays on the render thread. That's not a workaround; it's exactly what the component model is for.
The command set mirrors sugarloaf's existing immediate-mode API (sugarloaf/src/sugarloaf.rs): rect / rounded_rect / quad (per-corner radii) / line / arc / polygon / triangle, proportional text via text_mut().draw, and the real bitmap path GraphicData + push_image_overlay (the kitty-graphics route, sugarloaf/src/components/.../graphics.rs). The host clamps every coordinate to the pane's layout_rect (no scissor is exposed) and offsets by its origin β identical to how command_palette / island / scrollbar already draw (renderer/mod.rs ~580β620). A plugin pane that wants the full rect skips pushing its grid into frame_grids; otherwise it composites over the grid.
Injection point: one plugin.render(&mut sugarloaf, pane_rect, &commands) call in Renderer::run's pane loop, beside the existing overlays. The existing render_with_grids presents it with no pipeline change.
(There's also a near-free text-only path β a plugin can drive a Crosswords grid via the Handler trait and "draw" with cells β but the draw-command IR above is the general one.)
4. Input + invocation
Input already funnels through Screen::process_key_event (screen/mod.rs ~629) ending at messenger.send_write(bytes) (~669). Branch on pane kind: for a Plugin, forward decoded key/mouse/resize/focus to the plugin worker over a channel (mirroring Messenger). Add an Action::PluginOpen(name) to the config binding enum (bindings/mod.rs ~324) and its dispatch (screen/mod.rs ~1067) β the standard extension seam.
5. The WASM contract (WIT, component model)
A small wit interface, so plugins can be written in any language (wit-bindgen): hostβguest render(size) -> draw-commands, event(input), tick; guestβhost capabilities (timers, persistent state, optionally a gated net import via wasmtime-wasi). Sandboxed by default; capabilities opt-in per plugin in config.
Scope / invasiveness
Real change concentrates in ~3 files (context/mod.rs, a new plugin-worker module mirroring performer, screen/mod.rs + bindings/mod.rs for input/action) plus five bounded PTY-assumption fixes:
Context::drop unconditionally kill_pids the shell (context/mod.rs ~65) β make kind-aware (create_dead_context ~153 shows the no-PTY shape).
A couple of renderer paths lock terminal directly (renderer/mod.rs ~342) β fine if plugins reuse Crosswords; otherwise make the snapshot terminal-optional.
Pre-existing route-id keying bug (application.rs ~888): per-pane lookup should use get_by_route_id, not Taffy NodeId.
Native macOS tabs become real OS windows (context/mod.rs ~1114) β plugin panes likely restricted to Rio's own split/tab model at first.
create_context is the single PTY factory (~225) β add a sibling create_plugin_context.
Open questions (where your call shapes it)
Runtime: wasmtime (component model) vs. wasmi β preference? I've prototyped with wasmtime + WIT.
Draw IR: immediate-mode each frame, or host-side cached command lists with dirty signaling? (Affects the wit shape.)
API surface: which sugarloaf primitives to expose first β start text + rects + image overlay, grow later?
Security/capabilities: default-deny + per-plugin opt-in (net/fs/clipboard)? How declared in config?
Config: how should a plugin pane be declared/launched β a binding Action, a config block, a rio plugin run CLI?
Grid-vs-canvas: should plugins always overlay the grid, or be allowed to fully replace a pane's grid?
Naming (optional, take it or leave it)
Your crates are carioca landmarks (sugarloaf, corcovado, copa). If you want a theme for the plugin layer: a plugin = a bloco (a Carnaval street-party group β independent, anyone starts one, hundreds run side by side; and bloco also literally means "block/module"), hosted by Carnaval. Purely a suggestion.
What I'm offering
To build this in phases as reviewable PRs: (1) the pane-kind refactor on its own, (2) the plugin host + worker + a trivial reference plugin (a static draw-command demo), (3) the wit contract + a real plugin. Happy to start with (1) so you can judge the refactor in isolation. Would you want this contributed, and how would you like the plugin API shaped?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Line anchors are against current
mainand will drift.TL;DR
Rio has publicly wanted WASM plugins ("in the future you'll define the tab system with a WASM plugin"). This is a concrete, minimally-invasive design to get there, plus an offer to build it in phases.
The core idea: let a pane be backed by something other than a PTY. A plugin pane runs a sandboxed WebAssembly component that renders by emitting a serializable draw-command list (translated into
sugarloafprimitives on the render thread) and receives input events β using Rio's existing per-pane render loop, event bus, and input dispatch. Nothing about the windowing, tab/split, or layout system changes.The enabling step β generalizing
Contextinto a pane kind β is useful to Rio independent of plugins (it's what any non-terminal pane needs: a dashboard, a help view, a settings surface).I'd like to shape the plugin API with you rather than drop a finished fork β open questions are at the end.
Why this fits Rio cleanly
Reading the code, almost everything above a single pane is already pane-agnostic:
ContextGrid(the per-tab Taffy layout) andContextManager(tabs/splits/switch/close) operate on opaque panes βlayout/mod.rs,context/mod.rs.Renderer::runiteratesgrid.contexts_mut()per pane and only needs each pane's render snapshot +rich_text_idβfrontends/rioterm/src/renderer/mod.rs(~271, pane loop ~304).RioEvent+EventProxyalready carries per-route_id"this pane changed, redraw" messages into the loop βrio-backend/src/event/mod.rs, dispatched inapplication.rsuser_event(~212).The PTY assumption is concentrated in one type:
Context(context/mod.rs~49), created by one factory:ContextManager::create_context(~225).Design
1. Pane-kind generalization (the enabling refactor)
Make
Contextcarry a backend rather than unconditional PTY fields:Everything above
Contextstays as-is. This is the bulk of the value and lands as a standalone PR. (Leaks to fix, all bounded β see below.)2. Plugin host + lifecycle (reuse the
Machinepattern)A
PluginHost(wasmtime + component-model linker + instance pool) lives onScreen, peer tocontext_manager/renderer. Each plugin instance runs as aSendworker thread holding anEventProxyclone β exactly like the PTY readerMachine(rio-backend/src/performer/mod.rs~65). It signals redraw viaevent_proxy.send_event(RioEvent::Te 10BC8 rminalDamaged(route_id) | PluginRender(route_id), window_id); the existinguser_eventpath already marks the route dirty and requests a frame. No new event loop.3. Rendering: a serializable draw-command IR (the rich path)
Sugarloafis&mut, single-threaded, non-SendGPU state β a WASM guest can't hold it. So the contract is a serializable draw-command list the guest emits each frame and the host replays on the render thread. That's not a workaround; it's exactly what the component model is for.The command set mirrors
sugarloaf's existing immediate-mode API (sugarloaf/src/sugarloaf.rs):rect/rounded_rect/quad(per-corner radii) /line/arc/polygon/triangle, proportionaltextviatext_mut().draw, and the real bitmap pathGraphicData+push_image_overlay(the kitty-graphics route,sugarloaf/src/components/.../graphics.rs). The host clamps every coordinate to the pane'slayout_rect(no scissor is exposed) and offsets by its origin β identical to howcommand_palette/island/scrollbaralready draw (renderer/mod.rs~580β620). A plugin pane that wants the full rect skips pushing its grid intoframe_grids; otherwise it composites over the grid.Injection point: one
plugin.render(&mut sugarloaf, pane_rect, &commands)call inRenderer::run's pane loop, beside the existing overlays. The existingrender_with_gridspresents it with no pipeline change.(There's also a near-free text-only path β a plugin can drive a
Crosswordsgrid via theHandlertrait and "draw" with cells β but the draw-command IR above is the general one.)4. Input + invocation
Input already funnels through
Screen::process_key_event(screen/mod.rs~629) ending atmessenger.send_write(bytes)(~669). Branch on pane kind: for aPlugin, forward decoded key/mouse/resize/focus to the plugin worker over a channel (mirroringMessenger). Add anAction::PluginOpen(name)to the config binding enum (bindings/mod.rs~324) and its dispatch (screen/mod.rs~1067) β the standard extension seam.5. The WASM contract (WIT, component model)
A small
witinterface, so plugins can be written in any language (wit-bindgen): hostβguestrender(size) -> draw-commands,event(input),tick; guestβhost capabilities (timers, persistent state, optionally a gatednetimport via wasmtime-wasi). Sandboxed by default; capabilities opt-in per plugin in config.Scope / invasiveness
Real change concentrates in ~3 files (
context/mod.rs, a new plugin-worker module mirroringperformer,screen/mod.rs+bindings/mod.rsfor input/action) plus five bounded PTY-assumption fixes:Context::dropunconditionallykill_pids the shell (context/mod.rs~65) β make kind-aware (create_dead_context~153 shows the no-PTY shape).terminaldirectly (renderer/mod.rs~342) β fine if plugins reuseCrosswords; otherwise make the snapshot terminal-optional.application.rs~888): per-pane lookup should useget_by_route_id, not TaffyNodeId.context/mod.rs~1114) β plugin panes likely restricted to Rio's own split/tab model at first.create_contextis the single PTY factory (~225) β add a siblingcreate_plugin_context.Open questions (where your call shapes it)
witshape.)sugarloafprimitives to expose first β start text + rects + image overlay, grow later?Action, a config block, ario plugin runCLI?Naming (optional, take it or leave it)
Your crates are carioca landmarks (
sugarloaf,corcovado,copa). If you want a theme for the plugin layer: a plugin = a bloco (a Carnaval street-party group β independent, anyone starts one, hundreds run side by side; and bloco also literally means "block/module"), hosted by Carnaval. Purely a suggestion.What I'm offering
To build this in phases as reviewable PRs: (1) the pane-kind refactor on its own, (2) the plugin host + worker + a trivial reference plugin (a static draw-command demo), (3) the
witcontract + a real plugin. Happy to start with (1) so you can judge the refactor in isolation. Would you want this contributed, and how would you like the plugin API shaped?All reactions