Skip to content

Architecture

Umbra pins small web widgets to the Windows desktop. The C# host knows nothing about any specific widget; widgets are folders with a widget.json. It targets .NET 10 (WinForms + WebView2) on Windows 10 1809+ / Windows 11.

widgets/<id>/ ──► WebHost (WebView2 env, virtual hosts, sandbox)
│
settings.json ──► App ─┼─► WidgetWindow ×N (borderless, pinned under apps, JS bridge)
├─► SettingsWindow (HTML panel + message protocol)
├─► Tray / EditPill / ToastService / UpdateService
│
umbractl ──┐ │
MCP agent ─┼─► named pipe (current user) ─► Control.Execute ─► App
scripts ──┘
  • Host (src/Umbra): owns settings, creates one borderless window per enabled widget, draws the tray menu and Settings, and serves the control pipe.
  • Widgets (widgets/<id>/, installed to %APPDATA%\Umbra\widgets): a widget.json plus a web page. They talk to the host through a small JS bridge.
  • Control protocol: one JSON line per request over a per-user pipe. Control.cs is the single place that defines what automation can do.
  • CLI and MCP (src/Umbra.Cli): umbractl.exe maps commands onto the protocol and runs the MCP stdio server.
Path Role
src/Umbra/Program.cs Entry: single instance, WebView2 check, starts App, serves the control pipe
src/Umbra/App.cs Owns settings, widget windows, tray menu, Settings window. OnUi() marshals work to the UI thread
src/Umbra/WidgetWindow.cs One borderless desktop-pinned window per widget; JS bridge (fetchJson, drag, rendered…); host-drawn right-click menu; CaptureAsync
src/Umbra/DesktopPin.cs, Native.cs Win32: owned by Progman, kept at HWND_BOTTOM, no activation, DWM corners
src/Umbra/WebHost.cs WebView2 environment, virtual hosts umbra.local / community.umbra.local, sandbox
src/Umbra/SettingsWindow.cs + settings/ Settings panel (HTML) and its message protocol
src/Umbra/EditPill.cs The host-drawn “Editing widgets · Snap · Done” pill per monitor
src/Umbra/Tray.cs Tray tooltip, the “update ready” badge, the --restart flag
src/Umbra/CrashRecovery.cs Policy for renderer and browser-process failures
src/Umbra/Community.cs Catalog fetch, install and remove, path and type validation
src/Umbra/Themes.cs Theme model, token allow-list and validation, generated :root CSS
src/Umbra/WidgetManifest.cs, Settings.cs widget.json model and discovery; settings.json model
src/Umbra/WidgetStore.cs, WidgetTrash.cs umbra.store data and the 7-day trash
src/Umbra/WidgetCommands.cs, CommandService.cs, CommandRunner.cs, CommandApprovals.cs permissions.commands
src/Umbra/ToastService.cs, Notifier.cs umbra.notify
src/Umbra/Updates.cs, src/Umbra.Updater/ Update check, download and the update window
src/Umbra/Protocol.cs, Control.cs Control protocol and its executor
src/Umbra.Cli/ umbractl.exe: Program.cs, CommandLine.cs, McpServer.cs, Scaffold.cs
shared/umbra.js, umbra.css Widget SDK and design tokens served to every widget
widgets/<id>/, themes/<id>/ Catalog widgets and themes; the index.json files are generated by node scripts/catalog.mjs
schemas/ widget.schema.json, theme.schema.json
installer/Umbra.iss, build.ps1 Per-user Inno Setup installer; the version comes from Directory.Build.props
tests/Umbra.Tests/ Unit tests for host and CLI

Nothing is built into the app: even sp500 is installed from Browse. The generic mechanism for built-in widgets exists but none ship today.

%APPDATA%\Umbra (or <AppDir>\data when portable): settings.json, widgets\, data\<id>.json, themes\, trash\, approvals.json and umbra.log. UMBRA_DATA_DIR overrides it, and tests use a temporary directory.

WinForms and WebView2 are single-threaded: App.OnUi() marshals work from the pipe server and background tasks onto the UI thread. Control commands run on the pipe’s thread and call into App through that.

Desktop pinning · WebView2 host · Security model · Crash recovery · Dev setup