Docs menuDisplays, theme, tray, notifications
On this page5 sections
Desktop events in a shell
What a shell receives about the desktop around its windows: displays, theme, tray icons, notifications and application dialogs. Part of How a shell works.
Displays
The shell is one page covering the bounding box of every display, in logical pixels. Each display is a rectangle in it.
import { useDisplays } from "@domicile-desktop/react/displays";// DomicileDisplay: { name, x, y, width, height, scale, modeWidth, modeHeight, transform }const Bars = () => (useDisplays() ?? []).map((display) => ( // undefined until described <Bar display={display} key={display.name} /> ));- On a tty the engine shows the page on every monitor, rotated and scaled per the config. Pointer events are always in page pixels. The shell does nothing for rotation or scale.
modeWidth×modeHeight(the panel's unrotated scanout pixels) andtransformare informational. A 4K panel rotated 90° at scale 1.2 has a 3840×2160 mode and an 1800×3200 box.- Config: The config file,
transform.
Theme
theme.mode in the config is "dark" or "light".
- The shell reads
domicile.theme.current;theme.on("change")reports changes. - The compositor also publishes it to the settings portal
(
org.freedesktop.appearancecolor-scheme, followed by GTK4, Qt6, Electron and Firefox) and applies it to browser windows. theme.accent_color,contrastandreduced_motiongo to the settings portal and to the shell, asdomicile.appearance'saccentColor,highContrastandreducedMotion, with achangeevent.applyAppearancein@domicile-desktop/component-library/appearanceapplies them to the preset: theaccenttoken, the_contrastHighcondition and shortened animations.theme.icon_themegoes to the settings portal, and draws the tray's and notifications' icons.readIconThemeandwatchIconThemein@domicile-desktop/sdk/icon-themeread it from there,watchIconThemeagain on eachSettingChanged.prefers-color-schemein the shell's page follows it.domicile.theme.set("light")changes it until the desktop exits. It does not write the config. A later config edit overrides it.
Windows change theme after the shell has captured its old frame, so a shell can animate the switch:
theme.on("change")fires.- The shell captures its old frame (for example, starts a view transition)
and calls
domicile.theme.captured(theme). With no animation, it calls it at once. - Once every shell has called it, the compositor repaints the windows and
sets
theme.windowsand fireswindowschange. - If no shell calls it, windows switch after about a second.
In a view transition, call theme.captured once transition.ready resolves,
and pause the animation until windowschange. Calling it from the
update callback is too early:
- The engine runs the update callback before it has drawn the capture.
- Until
ready, the screen shows the live page, and an<app>in it turns at once.
ThemeProvider in @domicile-desktop/component-library runs the view
transition. Implement its ThemeSource.turnWindows to call
domicile.theme.captured(theme) and resolve on windowschange (see
manganese's theme/host-theme.ts).
System tray
Apps' tray icons (StatusNotifierItem, hosted by the compositor on the session
bus) are domicile.tray.items, the full list; tray.on("change") reports
changes.
const Tray = () => { const { tray } = useDomicile(); // { id, title, icon, bus, menu, itemIsMenu } const items = useWatch(tray, "change", () => tray.items) ?? []; return items.map(({ id, icon, title }) => ( <button key={id} onClick={() => tray.activate(id, "primary")} // or "secondary", "context" > <img alt={title} src={icon} /> </button> ));};- The app decides what a click does.
iconis adata:URL or"".menuis the item'scom.canonical.dbusmenuobject path onbus, or"". Read it withwatchMenufrom@domicile-desktop/sdk/dbusmenu(SYSTEM-TRAY.md).itemIsMenuis the item'sItemIsMenu: open its menu on the primary button instead of sending"primary".- Manganese's:
tray/Tray.tsx.
Notifications
App notifications, and sites' Web Notifications, are
domicile.notifications.list: every uncleared notification, oldest first.
notifications.on("change") reports changes.
const Notifications = () => { const { notifications } = useDomicile(); // { id, appName, summary, body, icon, urgency, actions, clickable, timeoutMs, time } const list = useWatch(notifications, "change", () => notifications.list) ?? []; return list.map(({ id, summary }) => ( <div key={id}> {/* a click (if clickable), or an action's key */} <button onClick={() => notifications.invoke(id, "default")}>{summary}</button> {/* clear; the app is told */} <button onClick={() => notifications.dismiss([id])}>×</button> </div> ));};// notifications.reply(id, "On my way"): the text of an "inline-reply" action's field- The shell decides which are new. The first list is history.
- The shell decides how long a toast stays up. A notification stays in the list until cleared.
- An action with key
"inline-reply"(INLINE_REPLY_NOTIFICATION_ACTION) is a reply, not a plain action: pressing it never sends the key. Itslabellabels the reply. - Design: NOTIFICATIONS.md.
Manganese's:
notifications/.
Application dialogs
Applications ask for dialogs through xdg-desktop-portal, and the compositor
is its backend. The shell draws each one. See
Dialogs and portals.
A shell must answer each request. One that ignores them leaves the app waiting for an answer: a file picker that never opens, a call that never shares. Either draw them or refuse them:
import { PortalDialogs } from "@domicile-desktop/component-library/PortalDialogs";import { useDomicile } from "@domicile-desktop/react/domicile-provider";// Put it anywhere in your shell.const Dialogs = () => <PortalDialogs domicile={useDomicile()} />;The full props, for a shell with monitors and keybindings of its own:
import { PortalDialogs } from "@domicile-desktop/component-library/PortalDialogs";<PortalDialogs domicile={domicile} screen={focusedScreen} screenOf={(appId) => screenShowing(appId)} shellChords={ownedChords(keybindings)}/>;PortalDialogsdraws every kind it knows and refuses the rest:Access(a yes/no question),Account(share the user's name and picture),AppChooser(an app list for "Open with" andOpenURI),FileChooser(the component library'sFilePicker),RemoteDesktop(which devices and the clipboard an application may control),InputCapture,GlobalShortcuts(review the chords an app asks for),Wallpaper(a picture to preview),DynamicLauncher(an install confirm with an editable name),Usb(a device grant),ScreenCast(a picker of the windows, browser windows or screens to share, each previewed on the desk, or a region to draw; passshownWindowshere too),Print(a printer and its options, or "No printers are set up."),Screenshot(pick the desk, a monitor, a window or a dragged area of a frozen frame, and save it or copy it with Ctrl+C; passshownWindowsso a window includes your frame around it) andPickColor(pick a pixel of it, with a magnifier). AnswerAccess,Account,WallpaperandUsbwithPortalAnswer.Access()to allow, andDynamicLauncherwithPortalAnswer.DynamicLauncher(name). PassclosedBrowsersfor browser windows you keep running but show as closed, such as a background inbox's; the window pickers leave them out.- The app list, the file picker, the account picture and the wallpaper
preview read the system through
domicile.files(desktop entries,mimeapps.list, directories, pictures). - A dialog goes on the screen
screenOfnames for the window that asked, or onscreenwhen the application named no window. - An
Inhibitrequest is an application holding off logout, user switching or suspend (body.what), listed until it lets go. It asks nothing, so a shell only shows it ("Editor is preventing logout"). The compositor refuses answers to it. - It also shows each running screen cast, remote desktop or input capture
session, with a Stop button. Pass
omitScreenCastswhen the shell shows screen casts itself, as manganese'sSharingbar item does. - Dialogs and the indicator name an application by its desktop entry, with
its icon, else by its app id; no app id is "An application".
useAppsin@domicile-desktop/component-library/useAppsdoes the same for a shell's own UI, andsourceNamein./source-namenames a screen cast's sources. PortalDialogsalso fires the chords apps hold.shellChordsare the shell's own, which a review flags as taken.- Without React,
domicile.portalshas the requests, parsed (requests), and sends answers (answer).capturingandstopCapturingdo the same for running sessions. The shell's own screen recording also takespauseRecording,resumeRecordinganddiscardRecording; see Dialogs and portals.fireGlobalShortcutsin@domicile-desktop/sdk/global-shortcutsfires the chords. - A shell must answer every request. One left unanswered keeps its application waiting until it gives up.
- The compositor refuses answers while the desktop is locked.
portals.wallpaperis the pictures applications set for the background and the lock screen, as paths;usePortalWallpaperin@domicile-desktop/component-library/usePortalWallpaperis the hook.usePictureUrlin@domicile-desktop/component-library/usePictureUrlturns a path into a URL for an<img>.