Skip to content

Multi-mode feature parity

Ryll is one binary with three delivery modes: a GUI desktop window (egui/eframe), a headless mode (no window; stdout, metrics, and an optional control socket), and a web mode (browser client over WebRTC via --web). Features usually land in one mode first and reach the others later — or never, when a mode physically can't support them. This page is the honest ledger of that drift: a per-feature matrix of what is reachable in each mode today.

Use it two ways:

  • Choosing a mode. "Can I do X in headless?" is answered by one cell lookup rather than by reading three documents or trying it.
  • Reviewing changes. A new feature that only works in the mode its author happened to test in is a parity regression waiting to be discovered by a user. The matrix makes that visible at review time.

The three modes

Mode Invocation Description
GUI (default) Desktop window with menus, status bar, panels. See Features.
Headless --headless No window. Frames are counted, stats print every 10 s, and an optional control socket (--control-socket) lets an external driver send input, take screenshots, and subscribe to events.
Web --web Serves a browser client over WebRTC. Shipped; the MVP scope was declared in PLAN-web-frontend.md and items outside it are marked below. See the web frontend operator guide.

How to read the matrix

Cell value Meaning
available Feature is fully reachable in this mode
partial Only some of the feature is reachable
missing Feature is not reachable today
n/a — intrinsic Feature physically cannot exist in this mode (justification in parentheses)

Notes on the annotations:

  • Source references (file.rs:line) point at the evidence for a "partial" or "missing" verdict. They are point-in-time: correct as of the audit that added them, and removed when the gap closes.
  • "Out of MVP scope" in the Web column means the feature was deliberately excluded from the shipped web MVP; the list lives in PLAN-web-frontend.md §Out of MVP scope. These are future work, not oversights.
  • Several headless cells are reachable only through the control socket, which requires launching with --control-socket <path>. Those cells say so explicitly.

Maintenance protocol

Reviewers are expected to keep this matrix honest:

  • When a feature is added, add a row.
  • When a mode gains or loses a feature, update the cell.
  • When a "missing"/"partial" gap is closed, update the cell and remove the source link.

This document records the baseline; it does not propose fixes. There is no standing gap-closure plan — gaps get closed by mode-specific work (the web frontend plan closed the Web column's MVP set; the control socket work gave headless an interactive driving surface). Last full audit: the web frontend sweep; control-socket cells refreshed 2026-07-30.

Parity matrix

Feature GUI Headless Web
Connection
Connect via .vv file (--file) available available available
Connect via URL (--url) available available available
Connect via direct host:port (--direct) available available available
TLS / secure channel (inline CA from .vv) available available available
Password authentication available available available
Control socket
Control socket (--control-socket) n/a — intrinsic (requires --headless; combining with the GUI is a CLI error) available n/a — intrinsic (requires --headless; combining with --web is a CLI error)
Session state query (status verb) n/a — intrinsic available n/a — intrinsic
Event subscription (latency, agent_connected, paste_*, surface_drawn, digest_updated) n/a — intrinsic available (digest_updated needs the digest-decode build feature) n/a — intrinsic
Display
Display framebuffer (render SPICE draw ops) available partial (ryll/src/app.rs:3513-3516 — headless counts frames but never paints continuously; with --control-socket, a SurfaceMirror renders draw ops on demand for the screenshot verb, instantiated on first use) available (single monitor, MVP)
Multi-monitor (--monitors N) available partial (ryll/src/app.rs:3513-3516 — channel connects but frames are silently dropped; surfaces are never created in headless) missing (out of MVP scope; see PLAN-web-frontend.md §Out of MVP scope)
Full draw-op coverage (FILL, COPY, BLEND, etc.) available partial (same as display framebuffer — channel runs but output is discarded outside the control-socket mirror) available (inherited from shared framebuffer)
SPICE streaming / MJPEG available partial (decoded messages counted as frames, not rendered) available (inherited)
Display capabilities advertisement (COMPOSITE, MONITORS_CONFIG, etc.) available available available
Window auto-resize to surface size available n/a — intrinsic (no window; resize channel exists but resize_tx is never connected to a window, ryll/src/app.rs:3391) n/a — intrinsic (browser viewport is user-controlled)
Obey guest size hints toggle (--no-obey-guest-size / hamburger toggle) available n/a — intrinsic (no window; flag accepted in headless for CLI symmetry but ignored, ryll/src/main.rs:195) missing (out of MVP scope; the web frontend's "guest fits browser viewport" inverse is planned)
Guest resolution follows viewport (VDAgentMonitorsConfig) available missing (ryll/src/app.rs:3391_resize_tx is created but never used to drive resize logic in headless) available
Cursor
Server cursor rendering (SET/INIT) available missing (ryll/src/app.rs:3563CursorChannel runs, events arrive, but headless event loop has no handler for CursorImage/CursorPos events) available (datachannel CSS overlay; the host cursor is only hidden once a real SPICE shape has arrived, so a guest that never sends one leaves the browser's own pointer visible rather than none at all)
Cursor position tracking available missing (same as above) available (MVP)
Cursor hide/show available missing (same as above) available (MVP)
Default arrow fallback cursor available n/a — intrinsic (no display) available
Audio
Audio playback (PCM and Opus decoding) available partial (ryll/src/channels/playback.rs:156-292PlaybackChannel connects and decodes, but spawns a cpal thread that will fail silently if no audio device is present; no headless-safe "discard" path exists) available (MVP; Opus passthrough preferred, PCM→Opus fallback)
Volume control (slider + mute) available missing (ryll/src/app.rs:1843-1856VolumeControl Arc is wired in but no CLI flag, stdout interface, or control-socket verb exposes it in headless) missing (out of MVP scope; not listed in PLAN-web-frontend.md MVP)
Audio volume/mute persisted across reconnect available n/a — intrinsic (no GUI; VolumeControl is re-created on reconnect anyway) missing (out of MVP scope)
Keyboard / Mouse Input
Keyboard input (key down/up, scancodes) available available (via the control socket send_key verb — down/up/press with AT-set-1 scancodes; without --control-socket only cadence and --paste-text generate input) available (MVP; browser-side scancode table)
Mouse input (position and buttons) available missing (the control socket declares mouse verbs a non-goal until a test needs them; no other pointer path exists in headless) available (MVP)
Mouse mode negotiation (CLIENT/SERVER) available available (negotiation runs in MainChannel regardless of mode) available (both modes drive the guest pointer: absolute MouseMove in client mode, relative MouseMotion derived from consecutive browser positions in server mode. Pointer Lock is still absent, so the derived deltas drift under guest pointer acceleration and dead-zone at the window edge — see docs/web-frontend.md)
Extended key prefix (E0 for nav cluster) available available (the control socket send_key verb accepts 0xE0-prefixed 16-bit scancodes; cadence alone only sends space) available (browser shell builds its own AT table)
Modifier state tracking (Ctrl/Shift/Alt) available partial (ryll/src/channels/inputs.rs:77-82 — tracked internally but only used by paste state machine; cadence does not restore modifiers) available (MVP)
Cadence mode
Cadence mode (--cadence) available available missing (out of MVP scope; no test-automation use case in browser)
Paste-as-keystrokes
Paste-as-keystrokes (enable flag) available available (--enable-paste-as-keystrokes) missing (out of MVP scope; clipboard sync is in Future work list in PLAN-web-frontend.md)
Paste-as-keystrokes (runtime trigger via Ctrl+Alt+V) available available (via the control socket paste verb; Ctrl+Alt+V itself is GUI-only) missing (out of MVP scope)
Paste-as-keystrokes (headless --paste-text) n/a — intrinsic (GUI reads clipboard; --paste-text is headless-only per README) available missing (out of MVP scope)
Paste-as-keystrokes (inter-character delay --paste-char-delay-ms) available available (also settable per-request via the control socket paste verb's char_delay_ms) missing (out of MVP scope)
Paste-as-keystrokes (vdagent auto-disable) available (ryll/src/app.rs:1077-1078) available (AgentConnected event logged; enable_paste flag passed through) missing (out of MVP scope)
Paste character validation / error dialog available partial (ryll/src/app.rs:3553-3556 — PasteFailed logged and causes non-zero exit; control-socket clients get a paste_failed event; no interactive dialog) missing (out of MVP scope)
USB Redirection
USB redirection channel (protocol layer) available available (--usb-disk, --usb-disk-ro CLI flags work in headless) missing (out of MVP scope; see PLAN-web-frontend.md §Out of MVP scope)
USB interactive panel (Menu → USB) available n/a — intrinsic (no GUI) missing (out of MVP scope)
Connect physical USB device (Linux only) available missing (ryll/src/app.rs:3389usb_tx is created but usb_rx is never sent a ConnectPhysical command in headless; no CLI flag for physical devices) missing (out of MVP scope)
Connect virtual USB disk (--usb-disk) available available missing (out of MVP scope)
Connect read-only virtual USB disk (--usb-disk-ro) available available missing (out of MVP scope)
Runtime USB disk addition (file picker) available n/a — intrinsic (no GUI file picker) missing (out of MVP scope)
USB error reporting (bug report integration) available partial (ryll/src/app.rs:3527-3534 — errors logged; no zip written unless --pedantic) missing (out of MVP scope)
Folder Sharing (WebDAV)
WebDAV folder sharing channel (protocol layer) available available (--share-dir, --share-dir-ro CLI flags work in headless) missing (out of MVP scope; see PLAN-web-frontend.md §Out of MVP scope)
WebDAV interactive panel (Menu → Folders) available n/a — intrinsic (no GUI) missing (out of MVP scope)
Share directory (--share-dir) available available missing (out of MVP scope)
Read-only share (--share-dir-ro) available available missing (out of MVP scope)
Runtime directory picker (native file dialog) available n/a — intrinsic (no GUI) missing (out of MVP scope)
Statistics and Instrumentation
FPS tracking (sliding window from DisplayMark) available partial (ryll/src/app.rs:3516 — frame count incremented but FPS not computed; only raw count printed every 10 s) missing (out of MVP scope; no stats panel in browser MVP)
Bandwidth sparkline (rolling bytes/sec graph) available missing (ryll/src/app.rs:3392-3394byte_counter wired in but never sampled or reported in headless) missing (out of MVP scope)
Latency sparkline (inter-PING interval graph) available partial (no graph; raw latency samples are reachable as control-socket latency events, and bytes_in/bytes_out are tracked, ryll/src/app.rs:3518-3521) missing (out of MVP scope)
Periodic stats to stdout (headless only) n/a — intrinsic (GUI shows live stats in status bar) available (every 10 s: frames, bytes_in, bytes_out) n/a — intrinsic (browser context)
--latency-file latency measurements export missing (ryll/src/config.rs:65 — flag declared but never read or acted upon anywhere in the codebase) missing (same — flag is declared but unused; control-socket latency events are the working alternative) missing (out of MVP scope)
Channel state snapshots (for bug reports) available available missing (out of MVP scope)
Traffic ring buffer (always active) available available missing (out of MVP scope)
Protocol Gap Tracking
Protocol gap counter (Gaps: N status bar) available missing (ryll/src/app.rs:3563 — gap observer registered; warn_once fires; but headless event loop has no handler to surface the count) missing (out of MVP scope)
Protocol gap floating window (click Gaps button) available n/a — intrinsic (no GUI) missing (out of MVP scope)
--pedantic mode (auto bug-report per gap) available available (partial: ryll/src/app.rs:3419 — app_snapshot is an empty default, so pedantic zips omit surface/bandwidth/latency data) missing (out of MVP scope)
--pedantic-dir directory selection available available missing (out of MVP scope)
Bug Reports
Bug report dialog (F12 / Menu → Report) available n/a — intrinsic (no GUI) missing (out of MVP scope)
Bug report zip assembly (metadata, pcap, screenshot, metrics) available partial (ryll/src/app.rs:3419--pedantic zips have empty app_snapshot; no interactive zip generation in headless) missing (out of MVP scope)
Bug report display region selection available n/a — intrinsic (no GUI) missing (out of MVP scope)
Bug report trigger-time screenshot capture available n/a — intrinsic (no continuously-painted display) missing (out of MVP scope)
Runtime metrics in bug reports (Linux /proc) available available (sampled in metrics.rs; used by pedantic observer) missing (out of MVP scope)
Screenshot
Screenshot (F8 / Menu → Screenshot) available available (via the control socket screenshot verb — PNG or raw RGBA of any live surface; the backing SurfaceMirror is created on first use) missing (out of MVP scope; see PLAN-web-frontend.md §Out of MVP scope "Recording / capture")
Multi-surface screenshot (suffix -1, -2, …) available available (the screenshot verb takes a surface_id parameter; one call per surface) missing (out of MVP scope)
Live Traffic Viewer
Traffic viewer panel (F11 / Menu → Traffic) available n/a — intrinsic (no GUI) missing (out of MVP scope)
Traffic viewer channel filters available n/a — intrinsic (no GUI) missing (out of MVP scope)
Traffic viewer pause/resume available n/a — intrinsic (no GUI) missing (out of MVP scope)
Notifications
In-app notifications panel (bell / Menu bell) available missing (ryll/src/app.rs:3400-3402 — store exists and is populated, but no bell or side panel in headless; no stdout drain) missing (out of MVP scope)
SPICE_MSG_NOTIFY surfacing available partial (stored in notification store; printed nowhere in headless event loop unless captured by --pedantic) missing (out of MVP scope)
Gap observer notification feed available partial (registered in headless at ryll/src/app.rs:3431; notifications accumulate in store but are never printed to stdout) missing (out of MVP scope)
Reconnect / Lifecycle
Reconnect on disconnect (Reconnect button) available n/a — intrinsic (no GUI; headless exits when main channel disconnects) available (SPICE session held while PeerConnection is dead; browser auto-reconnects with 1 s/2 s/4 s/8 s/16 s backoff, max 5 attempts then a manual button)
State preserved across reconnect (disks, share dir, paste toggle) available n/a — intrinsic (headless does not reconnect) partial (SPICE session continuity is preserved — the bridge is reaped and rebuilt, leaving the SPICE channels untouched; per-tab UI state is browser-managed)
Notification history preserved across reconnect available n/a — intrinsic n/a — intrinsic (browser tab holds its own page state)
Disconnect dialog available n/a — intrinsic (no GUI) missing (not in MVP scope; browser tab closure is the UX)
Graceful Ctrl+C shutdown available available available (SIGTERM/Ctrl-C drains axum then explicitly closes the active bridge so DTLS/SRTP tears down cleanly before the process exits)
Cancel superseded connection attempt available n/a — intrinsic (headless does not reconnect) n/a — intrinsic (single-session MVP)
Window / Session Persistence
Window size/position persistence (eframe persistence) available n/a — intrinsic (no window) n/a — intrinsic (browser manages its own viewport)
Open side panels survive reconnect available n/a — intrinsic (no GUI) n/a — intrinsic (no side panels in web MVP)
Capture Mode
Protocol pcap capture (--capture) available available missing (out of MVP scope; see PLAN-web-frontend.md §Out of MVP scope "Recording / capture")
Display video capture (display.mp4 via openh264) available available (capture session is passed through and VideoWriter fires on DisplayMark events even in headless) missing (out of MVP scope)
Session metadata (metadata.json) available available missing (out of MVP scope)
Diagnostics / Verbose Logging
Verbose logging to stderr (-v) available available available (stdout logging works regardless of mode)
File logging to /tmp/ryll.log (-v) available available available
Intimate logging (--intimate, keystrokes/mouse) available available missing (no interactive input to log in web MVP that wouldn't also appear in the browser console)
Connection Source
VDAgent connection status (agent_connected flag) available available (AgentConnected event handled and logged; also a control-socket event and status field) available (wired through same channel path)
TCP keepalive on all channel sockets available available available (inherited from SpiceClient)
Authentication / Session Token
Per-launch URL token (web mode only) n/a — intrinsic (no HTTP server) n/a — intrinsic (no HTTP server) available (MVP; random 32-byte token printed to stdout)
TLS for browser-facing endpoint n/a — intrinsic n/a — intrinsic available (--web-tls-cert / --web-tls-key flags; axum-server with rustls)
Web frontend (--web mode)
HTTP server with per-launch URL token n/a — intrinsic n/a — intrinsic available (ryll/src/web/server.rs)
Embedded browser shell (HTML/JS/CSS via include_bytes!) n/a — intrinsic n/a — intrinsic available
WebRTC SDP signalling endpoint (POST /offer) n/a — intrinsic n/a — intrinsic available
Synthetic video stream over WebRTC (test pattern) n/a — intrinsic n/a — intrinsic available (SyntheticFrameSource)
Synthetic audio stream over WebRTC (440 Hz tone) n/a — intrinsic n/a — intrinsic available
Real SPICE display frames over WebRTC available (egui paint loop) n/a — intrinsic (no encoder driven from real frames) available (RealFrameSource over SurfaceMirror)
Real SPICE audio over WebRTC (Opus passthrough) available (cpal sink) n/a — intrinsic (no audio sink) available (Opus path; PCM-only servers fall back to silent audio with a warn)
Browser-side keyboard/mouse input over datachannel available (egui inputs) n/a — intrinsic (no input device) available
Browser-side cursor overlay (datachannel) available (egui overlay) n/a — intrinsic available

📝 Report an issue with this page