Skip to content

zaius-labs/rvst

Repository files navigation

RVST

A native desktop engine for Svelte

Svelte is the language. Rust is the kernel. Your app owns every pixel.

Quick Start | How It Works | Native APIs | Templates | Configuration


What is RVST?

RVST is a new execution target for Svelte. Write components with Svelte 5, style with Tailwind or scoped CSS, and ship a native desktop app. No Electron. No webview. RVST replaces the browser engine entirely with a purpose-built Rust rendering stack.

Your Svelte code compiles to JavaScript as usual. RVST executes it in an embedded Deno runtime, maps the component tree to a Rust layout engine (Taffy), renders with a GPU vector graphics engine (Vello), and displays in a native window (winit). The result is a desktop app that starts instantly, uses minimal memory, and renders at native quality.

What you get:

  • Svelte 5 with runes, reactivity, scoped styles, component composition
  • Tailwind v4 with full utility class support and design tokens
  • CSS custom properties, @media queries, !important, :not(), :nth-child
  • Custom window chrome (traffic lights, Windows controls, or your own)
  • Native file system access from Svelte components
  • GPU-accelerated rendering with Vello
  • Icon fonts (Phosphor, Material Symbols, etc.)
  • Headless rendering and scene graph queries (RenderQuery)

Quick Start

Install

npm install -g @rvst/cli

This installs the rvst command globally. It downloads the correct Rust binary for your platform automatically.

Create a new app

rvst create my-app
cd my-app
npm install

Write your first component

<!-- src/App.svelte -->
<script>
  let count = $state(0);
</script>

<div class="app">
  <h1>Hello from RVST</h1>
  <button onclick={() => count++}>
    Clicked {count} times
  </button>
</div>

<style>
  .app {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    gap: 16px;
    font-family: system-ui;
  }
  button {
    padding: 8px 16px;
    border-radius: 6px;
    background: #3b82f6;
    color: white;
    border: none;
    font-size: 14px;
    cursor: pointer;
  }
</style>

Entry point

// src/entry.js
import { mount } from 'svelte';
import App from './App.svelte';

export function rvst_mount(target) {
  return mount(App, { target });
}
export default rvst_mount;

Vite config

// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { rvstPlugin } from '@rvst/vite-plugin';

export default defineConfig({
  plugins: [rvstPlugin(), svelte()],
  build: {
    outDir: 'dist',
    target: 'esnext',
    lib: {
      entry: 'src/entry.js',
      formats: ['es'],
      fileName: 'app',
    },
  },
});

Build and run

rvst build
rvst run

Or with watch mode for development:

rvst dev

How It Works

Svelte Component (.svelte)
        |
        v
  Vite + vite-plugin-rvst
  (compiles to JS bundle)
        |
        v
  Deno Runtime (executes JS)
        |
        v
  DOM Stubs (translate DOM ops to Rust ops)
        |
        v
  rvst-tree (DOM-like tree in Rust)
        |
        v
  lightningcss (CSS parsing + cascade + var() resolution)
        |
        v
  Taffy (CSS Flexbox/Grid layout)
        |
        v
  Vello + wgpu (GPU vector rendering)
        |
        v
  winit (native window)

RVST intercepts Svelte's compiled DOM operations at the lowest level. When Svelte calls createElement, setAttribute, or insertBefore, these become Rust ops that build a tree. CSS is parsed by lightningcss with full cascade, specificity, custom property inheritance, and @media query evaluation. Taffy computes layout. Vello renders to the GPU.

The entire pipeline runs on the main thread with sub-millisecond frame times for typical UIs.

Native APIs

RVST exposes native platform capabilities to Svelte via globalThis.__rvst:

Window Management

<script>
  const rvst = globalThis.__rvst;

  // Custom titlebar (remove OS chrome)
  $effect(() => rvst?.disableDecorations());
</script>

<div onmousedown={() => rvst?.startDragging()}>
  <!-- custom titlebar content -->
  <button onclick={() => rvst?.minimize()}>-</button>
  <button onclick={() => rvst?.maximize()}>+</button>
  <button onclick={() => rvst?.close()}>x</button>
</div>

File System

<script>
  const fs = globalThis.__rvst?.fs;

  async function loadConfig() {
    const text = fs.readText('/path/to/config.json');
    return JSON.parse(text);
  }

  function saveConfig(data) {
    fs.writeText('/path/to/config.json', JSON.stringify(data, null, 2));
  }
</script>

Available APIs

API Description
__rvst.disableDecorations() Remove OS window chrome
__rvst.enableDecorations() Restore OS window chrome
__rvst.startDragging() Begin window drag (call from mousedown)
__rvst.minimize() Minimize window
__rvst.maximize() Maximize/restore window
__rvst.close() Close window
__rvst.fs.readText(path) Read file as string
__rvst.fs.writeText(path, content) Write string to file

Configuration

CLI

rvst create <name> [-t template]   Scaffold a new project
rvst dev                           Build + watch for changes
rvst build                         Build the Svelte bundle
rvst run [file.js] [file.css]      Run the desktop app
rvst snapshot [file.js]            Dump scene graph as JSON
rvst a11y [file.js]                Dump accessibility tree
rvst ascii [file.js]               Semantic tree dump (default)
rvst --ascii=tree:css [file.js]    Tree with CSS classes + properties
rvst --ascii=structure [file.js]   Box-drawing layout map
rvst --ascii=validate [file.js]    Cross-validate tree vs pixels
rvst --filter="role:button"        Filter tree output
rvst analyze [CATEGORY] [file.js]  Run scene analysis
  diagnostics                        Zero-size, offscreen, overlap, no-handler
  layout                             Depth, utilization, whitespace, flex stats
  a11y                               Unlabeled buttons, missing handlers, roles
  contrast                           WCAG 2.1 AA/AAA contrast ratios
  heatmap                            Node density truecolor heatmap
  all                                Run all analyzers
rvst --version                     Show version

Fonts

Place .ttf or .otf files in a fonts/ directory next to your bundle. RVST auto-loads them at startup.

dist/
  app.js
  app.css
  fonts/
    Phosphor.ttf        # icon font
    Inter-Variable.ttf   # custom text font

Use in CSS:

.icon { font-family: "Phosphor"; font-size: 16px; }
.heading { font-family: "Inter"; font-weight: 600; }

CSS Support

RVST's CSS engine (powered by lightningcss) supports:

  • Full cascade with specificity and source order
  • !important override
  • CSS custom properties with ancestor inheritance (var(--theme-bg))
  • @media queries (min-width, max-width, min-height, max-height)
  • @layer (Tailwind v4)
  • Selectors: class, ID, tag, attribute, :not(), :first-child, :last-child, :nth-child
  • Pseudo-classes: :hover, :focus, :active
  • Combinators: descendant, child (>), adjacent (+), sibling (~)
  • calc() with rem, em, px, vw, vh, %
  • linear-gradient() backgrounds
  • transform: translate, rotate, scale, skew
  • text-decoration: underline, line-through, overline
  • Flexbox and CSS Grid (via Taffy)
  • border-radius, box-shadow (multiple), opacity

Templates

rvst create my-app                     # default — counter with scoped CSS
rvst create my-app -t tailwind         # Tailwind v4 + utility classes
rvst create my-app -t dashboard        # custom titlebar, routing, dark/light theme, icons
rvst create my-app -t shadcn           # Tailwind + bits-ui component primitives

Architecture

packages/rvst/
  crates/
    rvst-core/         Protocol layer (NodeId, Op, Rect)
    rvst-tree/         DOM-like tree with event handlers
    rvst-text/         Text shaping (Parley) + font metrics (skrifa)
    rvst-deno/         Deno runtime + DOM stubs for Svelte 5
    rvst-shell/        Layout (Taffy) + rendering (Vello) + windowing (winit)
    rvst-render-wgpu/  GPU rendering backend
  js/
    vite-plugin-rvst/  Vite plugin (redirects Svelte internals to RVST bridge)
    renderer-bridge-js/ DOM operation bridge (Svelte → Deno ops)

Platform Support

Platform Status Notes
macOS (Apple Silicon) Stable Primary development platform. Metal backend.
macOS (Intel) Stable Metal backend.
Linux (X11/Wayland) Supported Vulkan backend. Install vulkan-loader and GPU drivers.
Windows 10/11 Supported DX12 backend (Vulkan fallback).
Headless/CI Supported Software rendering via LLVMpipe/SwiftShader.

Linux Setup

# Ubuntu/Debian
sudo apt install build-essential pkg-config libvulkan-dev libwayland-dev

# Fedora
sudo dnf install vulkan-loader-devel wayland-devel

Windows Setup

No additional dependencies. wgpu uses DX12 natively. Ensure GPU drivers are up to date.

Headless Mode

RVST can render without a window for testing, CI, or server-side rendering:

# Dump scene graph
rvst --snapshot dist/app.js | jq '.nodes | length'

# Dump accessibility tree
rvst --a11y dist/app.js | jq '.[] | select(.role == "button")'

ASCII Scene Introspection

RVST can visualize UI state as text — useful for AI agents, debugging, and CI validation. All examples below are from the same dashboard app:

RVST rendering a full dashboard app with sidebar, stats, and activity feed
Dashboard app — traffic lights, sidebar nav with icons, stat cards, activity feed

Visualization Modes

Structure — box-drawing layout map showing element boundaries and nesting:

rvst --ascii=structure dist/app.js

ASCII structure map of the dashboard

Render — pixel-sampled ASCII art of the actual GPU output:

rvst --ascii=render dist/app.js

ASCII pixel render of the dashboard

Overlay — pixel background with semantic labels overlaid:

rvst --ascii=overlay dist/app.js

ASCII overlay combining pixels and labels

Validate — cross-checks tree against pixels, marks mismatches with !:

rvst --ascii=validate dist/app.js

ASCII validation showing tree vs pixel mismatches

Tree Views

Semantic tree (default) — compact, agent-friendly:

rvst --ascii dist/app.js

Semantic tree view of the dashboard

Tree with CSS — classes and key computed properties:

rvst --ascii=tree:css dist/app.js

Tree view with CSS classes and properties

Tree with layout — computed rects (position + size):

rvst --ascii=tree:layout dist/app.js

Tree view with layout rects

Full tree — role + classes + rects combined:

rvst --ascii=tree:full dist/app.js

Full tree view with roles, CSS, and layout

Filtering

Filter the tree to focus on specific elements:

# Show only buttons
rvst --ascii=tree --filter="role:button" dist/app.js

# Find elements with a CSS class
rvst --ascii=tree:css --filter="class:bg-red" dist/app.js

# Combine filters with +
rvst --ascii=tree --filter="role:button+has:handler" dist/app.js

Scene Analysis

RVST includes built-in analyzers that inspect your UI for layout issues, accessibility gaps, and contrast problems. Each produces a colored terminal report.

Diagnostics

Surfaces layout anomalies automatically detected during rendering — zero-size nodes with content, offscreen elements, sibling overlap >50%, and buttons without event handlers:

rvst analyze diagnostics dist/app.js

Diagnostics report showing zero-size and no-handler warnings

Layout

Quantifies your UI's spatial characteristics — node count, nesting depth, viewport utilization, whitespace ratio, and flex direction distribution:

rvst analyze layout dist/app.js

Layout analysis with depth histogram and utilization stats

In this dashboard: 245 nodes, max depth 7, only 23.5% viewport utilization — most content is in the center, leaving the bottom half empty.

Accessibility

Audits interactive elements for semantic completeness — buttons without accessible names, interactive elements without handlers, role distribution:

rvst analyze a11y dist/app.js

Accessibility audit showing unlabeled buttons and missing handlers

Contrast (WCAG 2.1)

Samples actual rendered pixels behind each text node and computes contrast ratios against WCAG AA (4.5:1) and AAA (7:1) thresholds:

rvst analyze contrast dist/app.js

WCAG contrast analysis with per-text-node ratios

Shows actual foreground/background colors sampled from the GPU render — not CSS values, but what the user sees.

Density Heatmap

Visualizes where UI elements cluster in the viewport as a truecolor terminal heatmap:

rvst analyze heatmap dist/app.js

Density heatmap showing element clustering

Cold (blue) = empty space, hot (red) = many overlapping elements. In this dashboard, the sidebar and stat cards are the densest regions.

Run Everything

rvst analyze all dist/app.js

RenderQuery Test Harness

RVST includes a windowed test harness that opens a real GPU-rendered window and accepts JSON commands via stdin. Built for AI agents, CI pipelines, and interactive debugging.

rvst test launch dist/app.js

RenderQuery test harness — querying state, clicking buttons, detecting contrast regressions

The app opens in a real window. Send JSON commands on stdin, get JSON responses on stdout — one line per command, one line per response. Every interaction automatically diffs the scene and runs lints.

Querying State

# What's rendered right now?
> {"cmd": "snapshot"}
< {"node_count": 245, "viewport_w": 1024, "viewport_h": 768}

# Find all buttons
> {"cmd": "find", "role": "button"}
< {"count": 19, "nodes": [{"id": 152, "name": "Overview", ...}, ...]}

# Get full diagnostic on one node
> {"cmd": "explain", "id": 134}
< {"layout": {...}, "visibility": {...}, "styles": {...}, "clip_chain": [...]}

# Why can't I see this element?
> {"cmd": "why_not_visible", "id": 500}
< {"visible": false, "reasons": ["ClippedByAncestor(134)"]}

Interacting with the UI

Every interaction command automatically snapshots before and after, diffs the changes, and runs lints. You get the full picture in one response:

> {"cmd": "click", "text": "Settings"}
< {
    "ok": true,
    "clicked": "Settings",
    "changes": {"total": 160, "styles": 6, "added": 147, "removed": 7},
    "lints": [
      {"level": "info", "lint": "bulk_change", "message": "160 changes detected"}
    ]
  }

Click by text or position. Scroll, type, navigate with tab:

> {"cmd": "click", "x": 512, "y": 400}
> {"cmd": "scroll", "x": 512, "y": 400, "delta": 200}
> {"cmd": "type", "text": "hello"}
> {"cmd": "navigate", "action": "tab"}

Automatic Lints

After every interaction, the harness checks for common issues and includes warnings in the response. You don't need to ask — problems surface automatically:

Lint Fires when
no_effect Click produced zero changes
contrast_regression Style change reduced text contrast below 3:1
content_lost More nodes removed than added
focus_lost Focused element was removed or hidden
empty_content New nodes have no text content
buttons_no_handlers Buttons without event handlers
scroll_no_effect Scroll didn't change position
bulk_change >50 changes (info, not warning)

Example: toggling a theme produces contrast warnings for elements that don't adapt:

> {"cmd": "click", "text": "Light"}
< {
    "changes": {"total": 70, "styles": 70},
    "lints": [
      {"level": "warning", "lint": "contrast_regression",
       "message": "Node 175 'Overview' now has low contrast 1.3:1 (color:#444 on bg:#313244)"}
    ]
  }

Diffing State Changes

Mark a snapshot, make changes, then diff:

> {"cmd": "snapshot_mark", "label": "before"}
> {"cmd": "click", "text": "Add Todo"}
> {"cmd": "diff", "from": "before"}
< {
    "changes": [
      {"NodeAdded": {"id": 500}},
      {"TextChanged": {"id": 92, "before": "3 todos", "after": "4 todos"}},
      {"StyleChanged": {"id": 88, "property": "background", "before": "#313244", "after": "#89b4fa"}}
    ]
  }

Analysis

Run any of the built-in analyzers on the live rendered app:

> {"cmd": "analyze", "type": "diagnostics"}
> {"cmd": "analyze", "type": "a11y"}
> {"cmd": "suggest_fixes"}
< {"suggestions": [
    {"severity": "error", "message": "Button #88 has no click handler"},
    {"severity": "warning", "message": "Text contrast 2.6:1 fails WCAG AA"}
  ]}

Visualization

Get ASCII representations of the live rendered UI:

> {"cmd": "ascii", "mode": "tree"}
> {"cmd": "ascii", "mode": "structure"}
> {"cmd": "ascii", "mode": "css"}

Performance

Every response includes timing metadata:

< {"node_count": 245, "_queue_ms": 2, "_exec_ms": 15}

_queue_ms shows how long the command waited before the main thread processed it. If this is >100ms, the app is sluggish. If the main thread is completely blocked, the harness detects it and warns:

< {"warning": "app_frozen", "frozen_ms": 3200, "queued_commands": 2}

Frame-level profiling:

> {"cmd": "perf"}
< {"last_layout_ms": 2.3, "last_scene_build_ms": 0.8, "frame_count": 142}

Session Management

# List running test sessions
rvst test list

# Kill a session
rvst test kill rvst-test-12345

Scripting

Pipe a sequence of commands. The wait command pauses between steps without blocking the renderer:

echo '{"cmd":"wait","ms":2000}
{"cmd":"click","text":"Settings"}
{"cmd":"wait","ms":1000}
{"cmd":"click","text":"Light"}
{"cmd":"wait","ms":2000}
{"cmd":"click","text":"Dark"}
{"cmd":"wait","ms":1000}
{"cmd":"close"}' | rvst test launch dist/app.js

Combine with Unix tools:

# Find all buttons without handlers
rvst test launch dist/app.js <<< '{"cmd":"find","role":"button"}' | jq '.nodes[] | select(.has_handlers == false)'

# Check if click actually changed state
rvst test launch dist/app.js <<< '{"cmd":"click","text":"Submit"}' | jq '.changes.total'

Full Command Reference

Category Commands
State snapshot, find, query, explain, computed_styles, accessibility_tree
Interaction click, scroll, hover, type, navigate, focus
Visualization ascii, screenshot, compare_pixels
Analysis analyze, suggest_fixes, stacking_order, compare_layout
Assertions assert_visible, assert_clickable, why_not_visible, hit_test, list_handlers
Diffing snapshot_mark, diff
Performance perf
Streaming watch, watch_stop
Session wait, close

RenderQuery API (Rust)

For programmatic access from Rust tests and tools:

use rvst_shell::HeadlessSession;
use rvst_shell::snapshot::SceneSnapshot;

let mut session = HeadlessSession::new("dist/app.js", 1024, 768);
let snap = session.snapshot();

// Query the scene graph
snap.assert_visible(node_id)?;
snap.assert_clickable(node_id)?;
snap.hit_test_stack(x, y);
snap.why_not_visible(node_id);
snap.accessibility_tree();

// Semantic node handles — stable across re-renders
let btn = snap.nodes.iter().find(|n| n.role == "button").unwrap();
println!("{} {} {:?}", btn.semantic_id, btn.role, btn.name);

// ASCII introspection
use rvst_shell::ascii;
println!("{}", ascii::tree(&snap));
println!("{}", ascii::tree_with_view(&snap, ascii::TreeView::Css));
println!("{}", ascii::structure(&snap, 160, 50));

License

Apache 2.0

Releases

No releases published

Packages

 
 
 

Contributors