Toolkit
toolkit is the library every MixOS egui application is built on. It is
the shell layer of the
egui applications decision
(see AGENTS.md §4.1).
An app keeps its behaviour in a headless engine and its UI state in one
serialisable struct. The toolkit draws the window around that state and
routes every action through one command registry.
The UI-free registry and Fluent Strings now live in
command. Toolkit keeps the egui adapter and its
existing nullary command API, with the moved data types re-exported.
Core and toolkit describe() emit stored chords in the keymaps grammar
(Ctrl+=, ArrowLeft, or short forms), ordered Ctrl, Alt, Shift. Executed
events use the command 0.2.0 v2 body with bounded params and result, and the
legacy wrapper maps authorisation refusals to CommandError::Denied.
It gives each app:
- the resolved design as an egui style, plus the embedded Inter and JetBrains Mono faces and the Lucide icons;
- the client-side title bar, menu bars and menus, and the View › Theme submenu;
- the chrome components (bars, buttons, panels, tabs, fields, toggles, combos, sliders, canvas scrollbars, tooltips, dialogs), drawn to the chrome specification in each chrome scheme;
- the command registry, which generates menus, shortcuts, the palette and the Bus surface, and records every executed command;
- the drive verbs, the UI-level Bus verbs that every app shares;
- Fluent string catalogues, a key-chord grammar, and thumbnail decoding.
The source is
libs/toolkit.
Who uses it
The apps term, ced,
dopus, prefs,
screenshot, backdrop
and busviewer; the editor library; and four of
compd's crates (decor, ui, scene-host,
world). backdrop is the only one that turns on the layer feature.
Starting an app
Call toolkit::install(ctx, &theme) once on the egui context. It installs
the image loaders the icons need and applies the theme's style and fonts.
Theme::load() reads the shared theme.conf.mix from the MixOS etc
directory ($MIXOS_ETC, else $MIXOS/etc, else /etc/mixos for root,
else $XDG_CONFIG_HOME/mixos). It falls back to the embedded design, and
says why on stderr, when the file is missing or unusable.
A minimal window, condensed from tests/snapshot.rs:
use toolkit::command::{Command, Registry, always};
use toolkit::{Icon, Strings, Theme, icons};
#[derive(Default)]
struct Demo { refreshed: u32 }
let mut commands = Registry::new();
commands.add(Command {
id: "bus.refresh", // stable, never reused once published
label: "cmd-refresh", // Fluent key
menu: Some("menu-bus"), // Fluent key of the top-level menu
submenu: None,
group: 0,
shortcut: Some(egui::KeyboardShortcut::new(egui::Modifiers::COMMAND, egui::Key::R)),
icon: Some(Icon::RefreshCw),
enabled: always,
run: |s: &mut Demo| s.refreshed += 1,
});
let theme = Theme::load();
toolkit::install(ctx, &theme);
let strings = Strings::new(include_str!("../i18n/en/demo.ftl"));
let stroke = icons::stroke_width(&theme);
// each frame:
let _ = toolkit::titlebar::show(ui, "Demo", Some(Icon::Server), stroke,
&commands, &state, &strings);
Main pieces
Theme and look
theme:Themeholds a resolved design together with the context it was compiled for (scheme, style, mode, contrast) and its framing (server- or client-side decorations, caption side). Build one withTheme::embedded(),for_context,from_source(a full design document or a selection-only file) orload(). Then adjust it withwith_choice,framedandwith_overlay.session_look: turns the desktop profile's appearance, as settingsd holds it, into aTheme. It is pure, with no Bus and no I/O. An appearance that names a design this build does not know leaves the app's session theme in place.theme_menu: the View › Theme submenu, added withRegistry::theme_menu. It covers the hue schemes (Ocean, Crimson, Stone, Forest, Sunset, Mono), the chrome schemes (Pro, Studio, Classic), modes, styles and framing. Command ids areview.theme.<scheme>,view.mode.*,view.style.*,view.decorations.*andview.captions.*, and each axis can follow the session.style: maps a hue scheme's design onto egui'sStyle. The module docs give the field-by-field mapping table.chrome: the colours and geometry of the title bar and menus. For the chrome schemes (pro,studio,classic) it supplies the whole egui style, taken from the design's chrome family. Components readChrome::styleandChrome::metricsand never ask which scheme they are drawing, so a new style needs only design data. See the chrome family decision.fonts: the embedded faces. Weight is a design token: each typography role names a weight, and the static Inter file of that weight is the one installed. Monospace is JetBrains Mono.icons: Lucide icons, from unmodified upstream SVGs. TheIconenum names them. Stroke width comes from the design metricicon.stroke_width, and colour comes from the widget's text colour at draw time. See the icons and type decision.
Window chrome and components
titlebar: the client-side title bar. From the left it holds the app mark, the registry's menus, the centred title, an optional right-hand group of controls, and the minimize, maximize and close buttons. Captions can be on the left instead. A window opened withtitlebar::viewportis undecorated. Only the free gap between the menus and the buttons drags the window. Calltitlebar::edgeslast in the frame to add the resize zones. Under server-side decorations the row becomes a plain menu bar.menu: menu bars and menus drawn by the toolkit, not by egui'sMenuBar. One highlight is shared by pointer and keyboard, a menu opens on press, and press-drag-release runs an item.Navholds the open menu and its highlights as pure data and is tested without egui.bars,button,panel,tabs,field,toggle,combo,slider,canvas,tooltip,dialog: the chrome components (options, status and tool bars, icon rail, push and icon buttons, dock panel groups, document tabs, value, text and search fields, checkboxes and switches, combo boxes, sliders, canvas scrollbars, tooltip wording, and modal dialogs).markdown: Markdown drawn natively.Document::parse(&str)turns CommonMark with GFM tables, strikethrough and task lists into plain data (pulldown-cmark, parser only), so an app parses once and keeps the document in its state.markdown::show(ui, &document)draws it every frame from the theme: heading sizes stepped from theHeadingstyle, code in the monospace face on the field surface (scrolled sideways, never wrapped), quotes after a rule, lists with read-only task boxes, striped tables, rules and links. Each text run is one selectable label. Links go toContext::open_urlonly for http, https and mailto (markdown::openable), with the address on hover, andshowreturns the link clicked. Images are never fetched (the alt text links to them) and raw HTML shows as text.markdown::looks_like_markdown(&str)is a conservative test for plain text written as Markdown: one fence, table or pair of headings, or two distinct signals among a heading, a list, bold or emphasis, inline code, a link and a non-reply quote, scanned outside quoted lines and the signature. The email reader uses both.
Commands
command:Command<S>is one action, with an id, a label key, a menu and submenu, a group, a shortcut, an icon, anenabledrule and arunhandler over the app's stateS. A handler only changes state. Work that leaves the process, such as a Bus call or a file write, is queued on the state for the app's effect loop.Registry<S>providesadd,add_choice,add_toggle,execute(id, state, origin),describe,menusandmodel,shortcuts,search,shortcut_gridandrebind. The UI, the command line and Bus verbs all dispatch by id.executed: every run is recorded with itsOrigin(menu,shortcut,palette,ui,bus,macro) and given a per-process sequence number. It goes into a boundedJournalthat counts the runs it drops.Registry::take_executedhands the records to the app's Bus connection, which publishes them on<app>.command.executed(decision).keymap: the chord grammar (Ctrl,Alt,Shift, then one key, joined by+).Registry::rebindapplies the person's bindings from the desktop profile.
Drive verbs
drive lets an agent do in the window anything a person can.
drive::install(ctx, app) turns on AccessKit, keeps each pass's widget
tree, and injects queued pointer and key events through egui's input hook,
the same path real input takes. The app registers these verbs under its own
prefix (<app>.ui.tree and so on):
ui.tree, ui.click, ui.pointer, ui.scroll, ui.key, ui.type,
ui.menu, ui.menu.list, ui.menu.open, ui.menu.close, ui.capture,
window, window.state
The app forwards each request, with the prefix removed, to
drive::request from inside a frame. It sends back whatever request, or
drive::logic (called once per frame), returns. It holds later commands
while drive::pending is true, and calls drive::finish before closing.
Reads answer at once. Input verbs run one at a time, one step per pass,
and answer one pass after their last step. At most 32 input requests can
be queued. The driving guide shows them in use.
Strings, thumbnails, layer surfaces
strings:Strings::new(ftl)wraps an app's Fluent catalogue (i18n/en/<app>.ftl). It providesget,with(with arguments) andhas. A missing key renders as the key itself, so a gap shows on screen instead of a blank. The toolkit's own labels, such as window buttons and the Theme menu, are ini18n/en/toolkit.ftl.thumbnail: decodes cached thumbnail PNGs within explicit size limits on aDecoderthread, ready to upload as textures. Making the thumbnails is the job of thumbs.layer(featurelayer): egui programs on wlr-layer-shell surfaces, such as wallpapers, desktops and docks.layer::run(options, program)creates one surface per output (Outputs::Each), or a single surface the compositor places. Each surface has its own egui context and draws through wgpu's GL backend on itswl_surface. It follows fractional scale and takes pointer, scroll and optional keyboard input. It redraws only when something changes, so an idle surface costs nothing.
Features and dependencies
The default build has no features. layer adds smithay-client-toolkit
0.19.2 (the version eframe's winit already links), wayland-backend, wgpu,
egui-wgpu and xkbcommon. Workspace dependencies: assets, config,
design, settings (data types only, no Bus) and thumbs.
Assets and licences
assets/fonts/ holds static Inter (Light, Regular, Medium, SemiBold) and
JetBrains Mono (Light, Regular, Medium) faces with their OFL texts.
assets/icons/ holds the Lucide SVGs with LICENSE-lucide.txt, and
mixos.dev uses the same icons. assets/licenses.conf.mix pins each notice
by SHA-256. This includes the notices for egui's own fallback fonts, which
are in assets/fonts/fallbacks/.
scripts/install_licenses.mix installs them under
<share>/licenses/toolkit (default share /opt/mixos/share). --check
validates only the sources, and --verify checks an installed tree. See
the bundled font licences decision.
Source layout
| Path | What |
|---|---|
src/lib.rs | module list and install |
src/theme.rs, theme_menu.rs, session_look.rs, style.rs, chrome.rs | the look |
src/fonts.rs, icons.rs | embedded faces and Lucide icons |
src/titlebar.rs, menu.rs | window chrome |
src/bars.rs … src/dialog.rs | chrome components |
src/command.rs, executed.rs, keymap.rs | commands, executed records, chords |
src/drive.rs | the drive verbs |
src/strings.rs, thumbnail.rs | Fluent strings, thumbnail decoding |
src/layer.rs, src/layer/ | layer-shell surfaces (feature layer) |
i18n/en/toolkit.ftl | the toolkit's own strings |
assets/, scripts/install_licenses.mix | fonts, icons, licence notices |
Testing
Unit tests sit in the modules; for example menu::Nav is tested as plain
data. The integration tests in tests/ drive real frames through
egui_kittest:
components.rs,menu.rs,titlebar.rs,chrome.rs: component, menu and title-bar behaviour under pointer and keyboard;drive.rs: the drive verbs, requested inside frames the way an app's shell requests them;snapshot.rs,components_snapshot.rs: offscreen wgpu snapshots across schemes and modes, compared withtests/snapshots/*.png. Regenerate them withUPDATE_SNAPSHOTS=1, and look at the images before committing;spec_renders.rs: whenCHROME_RENDER_DIRis set, it renders the chrome scenes for an out-of-tree comparison against the chrome specification's evidence. When it is unset, it does nothing;asset_licenses_test.mix: install, drift and verify checks for the licence installer.
On an authorised build worker at a pushed commit:
cargo test --locked -p toolkit
cargo test --locked -p toolkit --features layer