Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number Litr A small, native web browser for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Architecture

Architecture

Litr is a single Rust binary. GPUI (Zed's UI framework, drawing with Metal) draws the interface. WebKit's WKWebView, reached through the objc2 bindings, renders pages.

NSWindow (transparent, full-size content view)
└── content view
    ├── NSVisualEffectView      glass blur, when enabled
    ├── GPUI's Metal view        sidebar, toolbar, panels
    ├── container NSView         rounded clip, one per attached tab
    │   └── LitrWebView          WKWebView subclass
    └── NSImageView              icon in the web app and About panels

Source layout

File Role
main.rs Startup, key bindings, menus. Switches to --compile-blocklist and web app mode.
browser.rs The Browser view: tabs, rooms, memory policy, sidebar, toolbar, panels, persistence, schedules, web app creation, responsive view.
webview.rs The bridge to WebKit: LitrWebView, WebView, NativeHost, NativeImage, WebEvent, and scroll sync's isolated-world message handler.
text_input.rs Single-line text field (from GPUI's input example), used by the address bar and panels.
address.rs Address bar input to URL or search. Search engines.
storage.rs SQLite: history, tabs, rooms, schedules, web apps, developer sites, settings.
history_view.rs The history panel.
palette.rs The command palette window and fuzzy matching.
dev.rs Developer tabs: local origins, server checks, project folder detection (lsof), FSEvents watching.
downloads.rs WKDownload handling.
schedule.rs Scheduled tab types and timing.
webapp.rs Detecting app mode, building web app bundles, icons.
adblock.rs Content blocker compilation and lookup, the YouTube script.
privacy.rs Privacy report: blocked hosts per page and per day, estimates. Fed by the _webView:contentRuleListWithIdentifier:performedAction:forURL: navigation delegate SPI.
assistant.rs The page assistant: runs helpers/litr-ai (Swift: FoundationModels and Translation) per request, streams its answer.
tidy.rs Tab tidying: cleaning the model's groups, grouping by site, duplicates.
links.rs Default browser (NSWorkspace) and link rules for links from other apps, delivered through GPUI's on_open_urls.
share.rs Handoff (NSUserActivity), receiving handed-off pages (a method added to GPUI's app delegate), Markdown links.
importer.rs Reading other browsers' bookmarks, history and Arc's sidebar (Chromium JSON and SQLite, Safari plist and SQLite, Firefox places).
vault.rs Password vault: keychain internet passwords (Security framework), Touch ID (LocalAuthentication), Chromium password decryption (CommonCrypto), CSV import. The page side is resources/passwords.js, in the isolated client world.
reader.rs Reader mode: Readability scripts, the reader page template.
update.rs Update feed, download, signature check, bundle swap, relaunch.
memory.rs Per-process memory (proc_pid_rusage), the memory limit's choice of tab.
memory_pressure.rs libdispatch memory pressure source.
time.rs Local time helpers (localtime_r).
actions.rs GPUI actions.

GPUI and WebKit in one window

GPUI can't host native views, so web views are siblings of GPUI's Metal view in the window's content view, and always sit above GPUI content. The consequences:

  • Layout: GPUI lays out an empty area where the page belongs. A canvas element's prepaint callback reports that area's bounds, and WebView::set_frame converts them from GPUI's top-left coordinates to AppKit's and moves the web view. Frames are only set when the bounds change.
  • Overlays: nothing GPUI draws can appear over a page. Litr avoids needing to:
    • The find bar is a row above the page, not over it.
    • History, settings, schedules and the web app panel replace the page. The web view is detached while they are open.
    • The command palette is a separate borderless GPUI window, attached to the main window as a native child window so it moves with it.
  • Keyboard focus: AppKit's first responder is either GPUI's view or a web view. When Litr focuses a GPUI element (address bar, panel), it makes GPUI's view first responder first (NativeHost::focus_gpui). When the user clicks into a page, LitrWebView overrides becomeFirstResponder and reports it, so the address bar can give up focus.
  • Shortcuts go through the menu bar, so they work whichever view has focus. Edit menu items use MenuItem::os_action, which maps to the native copy:/paste: selectors, so they reach the web view through the responder chain.
  • Rounded corners: WKWebView manages its own layers, so each web view sits in a layer-backed container view that clips to rounded corners.

WebKit events

LitrWebView is a WKWebView subclass (define_class!) and is its own navigation delegate, UI delegate and KVO observer. It observes title, URL, loading, estimatedProgress, canGoBack, canGoForward, themeColor and underPageBackgroundColor.

Everything becomes a WebEvent on an async_channel that the browser drains on GPUI's main-thread executor: page changes, focus, new windows, close requests, crashed content processes, find results, downloads, icon lookups and the ad block list. Litr never polls.

New windows (webView:createWebViewWithConfiguration:…) return a new LitrWebView built with the configuration WebKit passes in. That keeps window.opener working for sign-in pop-ups.

Memory

  • Lazy web views. A Tab holds Option<WebView>, so new and restored tabs have none until shown.
  • One attached view. Only the active tab's web view is in the window. Out-of-window pages drop their tile backing stores, and with WKInactiveSchedulingPolicy::Suspend (macOS 14+) they are suspended.
  • Live limit. At most max_live_tabs web views exist. The least recently used background tabs are unloaded first.
  • Memory limit. Every 5 seconds each loaded tab's web content process (_webProcessIdentifier, SPI, checked with respondsToSelector:) is measured with proc_pid_rusage (physical footprint, as in Activity Monitor). Over the limit, the heaviest background tab that isn't busy with media is unloaded, one per sample. "Busy" means _isPlayingAudio (SPI) or a camera or microphone capture state; busy tabs are exempt from every kind of automatic unloading.
  • Snapshots instead of blank reloads. Leaving a tab calls takeSnapshotWithConfiguration: while the view is still in the window; the image is redrawn at 1x and stored as JPEG (quality 0.6, about 100 KB) in the tab and the session (tabs.snapshot, schema v4). The old view stays attached under the new one until the snapshot arrives (at most 600 ms), so nothing flickers. Reloading an unloaded tab puts the JPEG over the page in an NSImageView and fades it out once the page has loaded (at most 4 s).
  • Idle unloading and memory pressure. A timer unloads idle tabs, and a libdispatch memory pressure source unloads all background tabs.
  • Assistant. The on-device model and translation run in litr-ai, a Swift helper started per request and killed when the request is replaced or the pane closes. The model's memory belongs to the system's model service, not to Litr.
  • Forget tabs. A forget tab holds its own WKWebViewConfiguration with a nonPersistentDataStore, sharing the preferences and user content controller (ad blocking, scripts) with the shared configuration. The tab keeps the configuration while unloaded, so its cookies survive until it closes. Views WebKit creates for its pop-ups inherit it.
  • Rooms. Only the current room's tabs are in Browser::tabs, so every index-based tab operation works on one room. The other rooms' tabs wait in Room::tabs, unloaded apart from tabs that play sound or are in a call; the memory sampler counts those and unloads each once it goes quiet. The active tab of the room being left takes its snapshot first and is unloaded when the snapshot arrives.
  • Unloading keeps the interaction state. It saves WebKit's interactionState (back/forward list and scroll position, NSData of 1–2 KB), which is restored on reload.
  • Two drawables. GPUI's CAMetalLayer is limited to two window-sized drawables instead of three (about 15 MB saved per window at 1200×800 on Retina, 60 MB fullscreen on 5K).
  • No image decoders. GPUI's img() would add about 1.1 MB of decoders, so the one image Litr shows (the web app icon preview) is a native NSImageView.

Persistence

storage.rs uses the system libsqlite3 (the rusqlite crate without bundled) in WAL mode with a small cache. The schema version is in PRAGMA user_version:

Table Contents
history url (key), title, host, last_visit, visit_count
tabs position, url, title, theme_color (packed RGBA), zoom, state (interaction state blob), pinned_url, active (per room), snapshot (JPEG), room
blocked day (local day number), host, count; key (day, host); 30 days kept
bookmarks id, url, title, folder (path; empty for Litr's own), added; unique (url, folder)
link_rules id, site, target (app:<bundle id>, room:<id> or forget)
rooms id (key), name, color (packed RGB), position, apps (web app bundle ids, one per line)
schedules url, title, action, daily, at (Unix time or minute of day), last_fired_day
web_apps bundle_id (key), name, url, path
dev_sites origin (key), folder, watch
settings key, value

Tab changes are debounced by a second. They are also saved when the window closes and when the app quits.

Ad blocking

scripts/update_blocklist.py converts EasyList and EasyPrivacy into WebKit content blocker JSON and compresses it (resources/blocklist.json.zlib). The file is embedded with include_bytes!.

WKContentRuleListStore compilation of about 117,000 rules peaks at around 400 MB, so it runs in a child process, litr --compile-blocklist. The child inflates the rules with miniz_oxide, compiles them into a store at ~/Library/Application Support/Litr/ContentRules, and exits. The store is keyed by an FNV-1a hash of the embedded rules, so each rule set compiles once for Litr and all its web apps. Stale lists are removed.

At startup the browser looks up the compiled list and delays the first tab until the lookup answers, which takes milliseconds. If the list isn't there it starts the child, browses meanwhile, and reloads live tabs once it's ready.

The rule list and resources/youtube.js (a document-start user script, in all frames) are added to the shared configuration's WKUserContentController. Popups inherit the controller. Turning blocking off removes both.

Web apps

A web app is a copy of the running binary in ~/Applications/<Name>.app with:

  • CFBundleIdentifier set to no.gets.litr.webapp.<slug>,
  • LitrWebAppURL and LitrWebAppVersion in its Info.plist.

At startup webapp::detect() reads NSBundle.mainBundle and switches to app mode:

  • a separate database directory,
  • the site as a pinned first tab,
  • no sidebar or toolbar,
  • the app's name in the menus.

WebKit keys cookies and storage by bundle identifier, which gives each web app its own data.

Icons: the page's best icon URL is found with JavaScript and fetched on a background thread. It is composed onto a squircle with AppKit drawing (NSBitmapImageRep, NSBezierPath, NSImage), then converted with sips -z and iconutil.

Signing: bundles are signed ad hoc. Litr's LSFileQuarantineEnabled quarantines everything Litr and its child processes write, so the quarantine flag is cleared after codesign. Otherwise Gatekeeper would translocate and block the app.

Updates: main-mode Litr refreshes the binaries of the web apps it recorded when LitrWebAppVersion differs from its own version.

Updates

update.rs follows eterm's updater, using system facilities instead of crates:

Step How
Fetch the feed NSData dataWithContentsOfURL:
Read it NSJSONSerialization
Check the zip's hash CommonCrypto's CC_SHA256
Unpack ditto -x -k
Check the signature codesign --verify --deep --strict -R "=<requirement>"

The requirement is Apple's Developer ID designated requirement narrowed to team 7G383N3VY7 and identifier no.gets.litr. After the checks, the quarantine flag (set because Litr quarantines what it writes) is cleared.

The running bundle is renamed aside (.Litr.app.previous), not overwritten, because macOS kills a process whose signed executable changes under it. The new bundle is moved in, and the old one is restored if that fails. Relaunch runs open -n on the bundle and quits.

Checks run 30 seconds after launch and then daily, in main mode only. The status (update::Status) drives the strip under the toolbar, the About panel and the settings row.

Dependencies

Crate Why
gpui (pinned =0.2.2, runtime_shaders) UI. Pinned because GPUI follows Zed and changes without notice. Runtime shaders mean building doesn't need Xcode's metal compiler.
objc2, objc2-foundation, objc2-app-kit, objc2-web-kit, block2 AppKit and WebKit bindings, Objective-C blocks.
raw-window-handle GPUI's native view.
async-channel WebKit events to the UI.
rusqlite (system SQLite) Storage.
miniz_oxide Inflating the embedded rules.