Docs menuSystem access
On this page7 sections
System access for shells
A shell reaches the system through three primitives: files, processes and a D-Bus client. Desktop features (battery, audio, network, apps) are TypeScript libraries on them, so a shell adds a feature without a change to Domicile.
shell page ─ domicile.files, … ────▶ engine (opaque relay) ─────▶ compositor ├─ files ├─ processes └─ D-Bus (session, system)Page API
const Battery = () => { const { files } = useDomicile(); const [charge, setCharge] = useState<string>(); useEffect(() => { files .readText("/sys/class/power_supply/BAT0/capacity") .then((read) => setCharge(read.unwrapOr(undefined))); }, [files]); return <span>{charge}%</span>;};- Each call resolves a
Result<T, SystemError>. A process's output and a watch's changes areReadableStreams. The SDK's types are the reference. dbus.call()anddbus.match()reach the session and system buses. A body is JSON read against its D-Bus signature;domicile_host::dbus_jsondocuments the mapping.capture.screenshot()takes an interactive Screenshot portal screenshot as if the shell were the app:<PortalDialogs />draws the dialog, and the call resolves withScreenshotKept.Saved(path),ScreenshotKept.Copied()when the user pressed Ctrl+C, or failscanceled.capture.screenRecord()records the screen:<PortalDialogs />draws the screen cast picker, then a toolbar until the user stops the recording. Resolves with the WebM's path, or failscanceledwhen the picker is dismissed or the recording discarded. See Dialogs and portals.capture.preview()draws a window as it last drew itself, or a browser window's page, as a PNGdata:URL. The pickers preview a window that is not on screen with it.windows.setStates()andwindows.requests()set a window's tiled, maximized and fullscreen states and hear clients ask for them; see How a shell works.displays.addVirtual()adds a monitor with no connector, which the desk extends onto, untilremove()or the page goes away. Casting to a TV uses it; see CASTING-TO-TVS.md. Manganese'svirtual-monitor addcommand adds one from a key or a terminal.keyboard.keymaps()streams the config's keymaps and the active one;keyboard.selectKeymap()switches to another (The config file).shell.commands()streams each commanddomicile send-shellsends.bindKeyslistens for you and passes them toonCommand; see KEYBINDINGS.md.session.unlockRefusals()streams why each passphrase did not unlock the desktop (Idle and lock).- Spawned programs are found on the compositor's
PATH. The distribution packages recommendpactl,parecandcurl; the Nix package appends them toPATH, so a user's own copies win. - A reload starts every library from nothing: each reads its state again, so a readout is empty until its first reply.
- A compositor restart keeps the page. A call it had open fails
compositor_gone, except a watch, D-Bus match or other stream: the SDK sends it again for the next compositor. Changes in between are not heard: a watch's or match'sitemsstreamListened.Missed()once the next compositor starts it, and a reader reads again.system-battery,system-bluetooth,system-network, the tray menu and the icon theme do.system-backlightstartsudevadmagain and reads;system-audioresubscribes. See ENGINE-FORK.md.
The browser's cookies
browser.fetch(url, { method, headers, body, sapisidHashOrigin }) sends a
GET or a POST to an https address from the engine's browser process with the
cookies of the profile the ordinary browser windows use. A bar item reads a site the user signed in to
in a browser window, with no sign-in of its own.
const inbox = await domicile.browser.fetch( "https://mail.google.com/mail/?authuser=me%40gmail.com",);// { status: 200, url: "https://mail.google.com/mail/u/0/", body: "<!doctype…" }- Resolves with
status, the finalurlafter redirects, and thebodyas text, whatever the status. Rejects with aNetworkErrorfor a network failure, a refusal, or an answer over 8 MiB. methodis"GET"(the default) or"POST". A GET sends no body, and comes from no origin (Sec-Fetch-Site: none). A POST sendsbodywith the page'sContent-Type, as a same-origin CORS request from the address's own origin: thatOrigin,Sec-Fetch-Site: same-origin,Sec-Fetch-Mode: cors.- Refused: an address that is not https, another method, a GET with a body,
a
CookieorOriginheader, a header the network stack sets itself (Host,Content-Length, …), and every request while the desk is locked, or that it locked during. - A GET follows redirects on https only, and one to another origin drops the page's headers. A POST follows none.
sapisidHashOrigin, such as"https://mail.google.com", adds theAuthorization: SAPISIDHASH …header Google's first-party APIs take. The engine computes it from the SAPISID cookie the request carries, so the cookie never reaches the page. A hashed request follows no redirect.- Only the shell's
domicilehas it; app modules do not. - Engine side: CONTROL-CHANNEL.md.
Libraries
| Package | Source |
|---|---|
@domicile-desktop/system-battery | UPower over D-Bus |
@domicile-desktop/system-backlight | sysfs, udevadm monitor, logind SetBrightness; iio-sensor-proxy's ambient light over D-Bus |
@domicile-desktop/system-audio | pactl and parec |
@domicile-desktop/system-network | NetworkManager, iwd or wpa_supplicant over D-Bus; iwctl to join a new network on iwd |
@domicile-desktop/system-bluetooth | BlueZ over D-Bus |
@domicile-desktop/system-apps | desktop entries, icons and bookmarks |
@domicile-desktop/sdk/file-preview | a launcher's file preview: directory entries, text, audio tags and cover |
@domicile-desktop/sdk/dbusmenu | a tray item's com.canonical.dbusmenu menu, on the item's own bus name |
Each library owns its system's format and the safety rules for it (for
example, system-backlight never sets the level to zero), with tests on
recorded output.
What stays in Domicile
| Stays | Why |
|---|---|
| Windows, input, rendering, Wayland protocols | only the compositor and engine can do them |
| The lock and PAM | must outlive the page (LOCK.md) |
| Notifications, tray, portals | D-Bus servers that own a bus name |
The home file index and searchFiles | speed: only the matches cross to the page |
Launching apps (spawn) | a client outlives a shell reload and runs on the compositor's Wayland display |
Clipboard history (copyClipboardEntry) | the compositor serves the paste, so it outlives the client that copied; full text stays out of the page (CLIPBOARD.md) |
The screen's color temperature (set_color_temperature) | only the engine reaches the display's gamma ramp (Night light) |
Theme (theme, windowsTheme) | clients read it from the portal's Settings; the compositor switches them only after every chrome captures its old frame |
Wire
- Page to compositor:
system_request { id, request }.requeststarts a call (read_file,write_file,create_dir,read_dir,stat,watch,spawn,dbus_call,dbus_match,screenshot,screen_record,preview_window,shell_commands,send_shell,idle_warnings,idle_holds,unlock_refusals,set_window_states,window_requests,virtual_monitor,keymaps,select_keymap,set_color_temperature) or drives one running under the sameid(unwatch,stdin,close_stdin,kill).read_filetakes an optional byte range,offsetandlength. - Compositor to page: one
system_replyper call that starts something (failedincluded), then for a watch or process any number ofsystem_eventand onesystem_end. Bytes are base64. - Types:
domicile_protocol::SystemRequestand its neighbors. The executor isdomicile_host::system, one per chrome connection, on the connection's thread. - The engine relays both as opaque strings: the host's
callSystem(id, request)and itssystemevent, aMessageEventwhosedatais the line. Only the engine writescompositor_gone, for the calls of a compositor that hung up. A new request type changesdomicile-protocol, the compositor and the SDK, not the engine. - Large files for display (
<img>,<video>) usedomicile://home/.
Security
The shell is trusted code: it can already spawn any argv. These rules keep that trust from reaching anything else.
- Binding: only the
domicilethat the engine hands toShellin adomicile://shelldocument reaches the system, through the control channel's own binding.<webview>guests, extensions anddomicile://homenever do. An app module'sAppDomiciledoes too, for files, processes and D-Bus only (APP-MODULES.md). - Content Security Policy:
domicile://shellservesscript-src 'self'. A script injected through a notification body, window title or file name does not run (Packaging). - argv only: no shell string. A shell that wants
sh -cpasses it. - Lock:
crate::lock::refusedcovers every system call. While locked:- a call that starts something fails with
locked, except:- a read, stat, listing or watch of an absolute path under
/syswith no.. - the calls
system-battery,system-backlightandsystem-audiomake, andsystem-bluetooth's watch, matched field by field, so a lock screen shows the battery and Bluetooth and sets the brightness and volume (LOCK.md) shell_commandsandwindow_requests, so a page reloaded while locked hears them after unlockidle_warnings, so a lock screen can dimidle_holds, likeidle_warningsunlock_refusals, which a lock screen needs while lockedset_window_states, likewindows.setBoundskeymapsandselect_keymap, so a lock screen shows and switches the keymap the passphrase is typed onset_color_temperature, so the night light keeps its schedule
- a read, stat, listing or watch of an absolute path under
stdinis dropped;unwatch,close_stdinandkillare allowed- processes and watches started before the lock keep running
- a call that starts something fails with
- Lifetime: a page's processes, watches, matches and virtual monitors end
when its connection closes, so a reload or
domicile load-shellleaks nothing.
Why
- Primitives over a message per feature. GNOME Shell (GIO), AGS/Astal and Quickshell expose the same three.
- D-Bus as a primitive, over
spawn("busctl", …). NetworkManager, BlueZ, UPower, MPRIS and logind are D-Bus services; parsingbusctloutput loses types. - In the compositor, over the engine's browser process. The compositor
enforces the lock, already spawns clients and already runs
zbus, and its Rust tests run without a GPU. - Opaque relay in the engine, over typed IDL members. A member per request would grow the engine with every feature. The SDK parses replies with Zod.