Docs menuThe config file
On this page14 sections
The config file
One JSON file (or a module that builds it) describes a desktop: displays,
keyboard, idle, lock, lockdown, theme and extensions. The compositor reads it at startup
and reloads it on every change. The schema is the domicile-config crate's.
For what a shell does with these settings, see How a shell works. The Settings app edits the file (SETTINGS.md).
Where it is
domicile --config ./desk.json ./my-desktop/dist/shell.jsnames it. The flag can go before or after the shell.- Without the flag,
domicilelooks for$XDG_CONFIG_HOME/domicile/domicile.{ts,tsx,js,mjs,json}(or~/.config/domicile/ifXDG_CONFIG_HOMEis unset). Two matches is an error. - With no config file, the compositor uses its defaults: one output that follows the engine's window (suits a nested dev run).
--configwith no value, or given twice, is an error.domicileprints which config it used before starting:
config: /home/you/.config/domicile/domicile.json, found where a config livesconfig: ./desk.json, because --config names itconfig: none -- no domicile.{ts,tsx,js,mjs,json} in /home/you/.config/domicile -- so the compositor's defaultsdomicile-compositor itself reads nothing from the environment. It takes every
value on its command line. The lookup above is the domicile binary's.
A config module
A .ts, .tsx, .js or .mjs config is evaluated to JSON.
- The
Shellexport is the shell to run when none is given. - In a JSON config, name the shell with
"shell", relative to the config file:{ "shell": "@domicile-desktop/manganese" }. - An
optionsexport is the shell's declared options (How a shell works), not a section. - Every other export is a top-level section of the schema. Keybindings are
shell props, so a
keybindingsormodesexport is an unknown section and is rejected.
// ~/.config/domicile/domicile.tsximport { runManganese } from "@domicile-desktop/manganese";import type { InputConfig } from "@domicile-desktop/sdk/config";export const input: InputConfig = { keyboard: { xkb_variant: "dvp" } };export const Shell = runManganese();- Styling custom manganese bar items: CUSTOM-BAR-ITEMS.md.
@domicile-desktop/*and React resolve to the running Domicile's copies, whateverpackage.jsonsays. Building it: Packaging.- How it is evaluated: THE-DOMICILE-BINARY.md.
Types and schema
@domicile-desktop/sdk/confighas a type for each section (InputConfig,OutputConfig, …) andConfigfor the whole file.@domicile-desktop/sdk/config.schema.jsonis the JSON Schema. A JSON config names it for editors, and the compositor ignores the key:{ "$schema": "https://domicile-desktop.com/config.schema.json" }. That copy is built from the commit these docs are, so it matches them. The SDK's copy matches the SDK version you installed.- Both are generated from
domicile-config's Rust types; see the SDK's Generated types.
Home-manager
programs.domicile has an option for every schema field:
{ imports = [domicile.homeManagerModules.default]; programs.domicile = { enable = true; shell = "${domicile.packages.${system}.manganese}/shell.js"; settings.output.profiles = [{ name = "desk"; displays = [ {display = "DEL DELL U3219Q 2ZLS413"; scale = 1.2; transform = "rotate-270";} ]; }]; };}- It writes the config to the standard path and puts
domicileonPATHwith your shell set. - It installs no login session. That is the NixOS module's job (RUNNING-A-DESKTOP.md).
settingsis freeform, so keys newer than the module pass through.- The build runs
domicile check-configon the file, so a key the compositor refuses failshome-manager switchinstead of the desk. Run it yourself on a hand-written config:domicile check-config ~/.config/domicile/domicile.json. configDirectorybuilds a config module athome-manager switch:programs.domicile.configDirectory = ./domicile;- The directory holds one
domicile.{ts,tsx,js,mjs}, itspackage.jsonandbun.lock, and abun.nix. Writebun.nixwithbunx bun2nix -o bun.nixafter each change tobun.lock. - Every package the config imports must be in
bun.lock. The build has no network, so it cannot add one. - Exports are written over
settings;settingsfills in the rest. Shellis bundled into the store and becomesshell.
- The directory holds one
webAppsinstalls sites as apps. Each gets a desktop entry that runsdomicile open --app <url>(WEB-APPS.md):programs.domicile.webApps.Music = { url = "https://music.example/"; icon = "multimedia-audio-player"; # an icon theme name or a file};
Reloading
An edit applies to the running desktop, and windows stay open:
| Section | Effect of an edit |
|---|---|
input.keyboard | New layout; moves the keys a shell bound (Keybindings). The desktop stays on the active keymap if keymaps still names it, else goes to the first |
input.pointers | Tablets, touchscreens and absolute mice rebound |
output.max_scale | Re-advertised to clients |
output.displays, output.profiles | Displays rearranged |
idle.blank_after_seconds, idle.warn_before_seconds | Idle timer restarts. Dark screens relight |
theme.mode | Shell and windows repaint |
theme.accent_color, contrast, reduced_motion | Shell and windows that read the settings portal follow |
theme.cursor_theme, cursor_size | The pointer on a tty is redrawn; windows that read the settings portal follow |
theme.font_name, monospace_font_name | Windows that read the settings portal follow |
theme.force_dark | Those sites are drawn dark from their next load |
theme.icon_theme | Shell and windows that read the settings portal follow; tray icons redrawn and later notifications drawn in it; manganese on the next menu or launcher opening, and title bars when a new app id appears |
files.omit | Launcher file index rebuilt |
extensions | Extensions installed or removed (EXTENSIONS.md) |
lockdown | Applications told through the Lockdown portal |
lock | The next passphrase is checked through the new PAM service. The desktop never unlocks from a reload (LOCK.md) |
- A config module is watched with its directory, except
node_modulesand dot directories. An edit re-evaluates it and, if it is the shell, rebuilds and reloads the shell. A reload during an engine restart waits for the engine. A failure is logged, posted as a notification, and leaves the desktop as it was. startupis read only at startup. Rerunning it would duplicate its commands.- A file that fails to parse is rejected and logged. The last good config stays.
- A keyboard xkb cannot compile, any of its keymaps included, is rejected and logged. The current layout stays.
- A
lock.pam_servicethe machine does not declare is taken, and logged: the desktop does not lock until the file is added (LOCK.md).
Other sections
shell_options: values for the options the shell declares, by key. The compositor passes them to the shell without reading them. See How a shell works.startup.commands: commands run once when the compositor starts. Each is an argv:[["emacs", "--daemon"], ["sh", "-c", "…"]].files: what the launcher's file index leaves out. See LAUNCHER.md.output.max_scale: the highest scale advertised for the output that follows Domicile's window. Applies only whenoutput.displaysis empty. Default2;1turns scaling off.
Keyboard
input.keyboard takes sway's xkb_* fields: xkb_layout, xkb_variant,
xkb_options and the rest. The default is plain us, no variant, no options.
A shell for users of other layouts must set them here; nothing else does.
To switch between layouts, list them in keymaps instead:
{ "input": { "keyboard": { "keymaps": [ { "name": "dvp", "xkb_variant": "dvp", "xkb_options": ["caps:escape"] }, { "name": "us" } ] } }}- Each entry is a whole keymap with the same
xkb_*fields and defaults. It takes nothing from the fields besidekeymaps, which must stay unset. nameis what the shell shows and selects. Each is unique, and none isnext. A keyboard withoutkeymapsis one keymap, named for its variant, or its layout when it has none.- The desktop starts on the first. A switch reaches every window, the browser and the shell's keybindings, and lasts until the desktop restarts.
- A shell lists them, each with xkb's name for its layouts (such as
English (programmer Dvorak)), withdomicile.keyboard.keymaps()and switches withkeyboard.selectKeymap()(System access). Both work while locked.
Tablets and touchscreens
On a tty, input.pointers binds each tablet, touchscreen or absolute mouse
(such as QEMU's USB tablet) to the display it covers. An unbound touchscreen
covers no display. An unbound absolute mouse covers the display the pointer is
on.
{ "input": { "pointers": [ { "device": "usb-0000:00:04.0-1/input0", "display": "Virtual-1" }, { "device": "usb-0000:00:04.0-2/input0", "display": "Virtual-2" } ] }}deviceis the evdev name orphys, matched exactly. A name binds every device that has it, so tell identical tablets apart byphys.udevadm info /dev/input/eventNprints both.displaynames a display as a profile does (Naming a display).- Details: DISPLAYS.md.
Idle
idle.blank_after_seconds is how long the desktop goes without input before
the screens turn off. Any key, click, scroll or pointer motion turns them back
on. idle.lock_after_seconds is how long before it locks.
- Unset means the screens never blank.
0is an error. idle.warn_before_secondswarns the shell that long before blanking, so it can dim or count down (Idle and lock). Unset means no warning. It must be at least1and less thanblank_after_seconds.idle.lock_after_secondsruns on its own clock from the same input, so it may be shorter or longer thanblank_after_seconds. Unset means no lock on idle; the desktop still locks when the shell asks and before the machine sleeps (Locking).0is an error.- Apps can block blanking and locking (for example, during a video) with
zwp_idle_inhibit_manager_v1. This involves only the app and the compositor. - Shell side: Idle and lock. Implementation: IDLE.md.
Lock
The desktop always has a lock, with or without this section. The user's password unlocks it, checked by PAM. There is no other way to unlock it.
lock.pam_service: the PAM service that checks it. Unset isdomicile, which the NixOS module and the distribution packages declare. Empty, or a name that is not a file name in/etc/pam.d, is an error.- Without
/etc/pam.d/<service>, the desktop does not lock, since nothing could unlock it. LOCK.md says which file to add.
Shell side: Idle and lock. Implementation: LOCK.md.
Lockdown
{ "lockdown": { "disable_camera": true, "disable_printing": true } }Switches that ask applications not to do something. The compositor reports
them through the org.freedesktop.impl.portal.Lockdown portal; applications
that read it enforce them. Every switch defaults to false.
disable_printing,disable_save_to_disk,disable_application_handlersdisable_location,disable_camera,disable_microphone,disable_sound_output
An application that ignores the portal is not stopped.
Theme
{ "theme": { "mode": "light" } }theme.mode is "dark" or "light". There is no "system" value, because
this config is the system setting. "system" is rejected.
The shell's toggle and domicile theme change it until the desktop restarts
(RUNNING-A-DESKTOP.md).
Applications read the rest through the settings portal
(org.freedesktop.appearance), and the shell receives them too:
| Key | Values | Default |
|---|---|---|
theme.accent_color | "#rrggbb" | unset: each application's own |
theme.contrast | "normal", "high" | "normal" |
theme.reduced_motion | true, false | false |
theme.icon_theme | an icon theme's directory name, such as "Papirus-Dark" | unset: hicolor only |
theme.cursor_theme | a cursor theme's directory name, such as "Bibata-Modern-Ice", or one per mode: { "dark": "Bibata-Modern-Ice", "light": "Bibata-Modern-Classic" } | unset: each application's own |
theme.cursor_size | logical pixels, at least 1 | unset: each application's own |
theme.font_name | a Pango font description, such as "Inter 11" | unset: each application's own |
theme.monospace_font_name | a Pango font description, such as "Iosevka 12" | unset: each application's own |
icon_theme:
- Served as
org.gnome.desktop.interface'sicon-theme. Unset leaves it to the next portal backend; a reload that unsets it signalshicolor. - Install the theme so it is under a data directory's
icons, such as~/.local/share/icons. - Shells read it with
watchIconThemefrom@domicile-desktop/sdk/icon-themeand look icons up with@domicile-desktop/system-apps/app-icons. - Portal dialogs draw applications in it:
describeAppsreads it each time a dialog's applications change.
cursor_theme, cursor_size, font_name, monospace_font_name:
- A per-mode
cursor_themeswitches withmode, including on the shell's toggle. - Served as
org.gnome.desktop.interface'scursor-theme,cursor-size,font-nameandmonospace-font-name. Unset leaves each to the next portal backend; a reload that unsets one signals the toolkit default:default,24,Sans 10,Monospace 10.
The engine draws every pointer on a tty, including a window's, from
cursor_theme at cursor_size:
- Either key alone fills in the other's toolkit default. Neither draws Chromium's own arrows.
- The theme is looked up in
XCURSOR_PATH, else in$XDG_DATA_HOME/icons,~/.icons,iconsunder each ofXDG_DATA_DIRS, and/usr/share/pixmaps, in that order. Inherited themes are followed. - A cursor the theme lacks is Chromium's own.
- The engine logs
the desk's pointer theme is "<theme>" at <size> px, thenloaded <n> cursors from "<theme>", orno cursor theme "<theme>" on the XCursor path. - A cursor larger than the GPU's cursor plane, often 64 or 256 pixels, is cut off.
- Nested, the host draws the pointer from its own theme.
In manganese:
accent_colorreplaces theaccenttoken in both themes.contrast: "high"draws text at the palette's ends and darkens borders and muted text.reduced_motionruns every animation and transition once at the shortest duration: wallpaper crossfades, the theme wipe, window animations.icon_themedraws tray menu, launcher and title bar icons. Symbolic icons take the text's color. It is followed once per desktop; a theme that cannot be read is logged and drawn ashicolor.
Shell side: Displays, theme, tray, notifications.
Sites drawn dark
{ "theme": { "force_dark": ["mail.google.com"] } }For sites with no dark theme of their own, such as Gmail:
- While
modeis"dark", the browser draws these sites with Chromium's automatic dark mode. A switch to"light"draws them as they are. - Each entry is a host name. It covers its subdomains. A URL, path or port is rejected.
- Default:
[].
Displays
output has two ways to describe displays:
output.displayslists the desktop's displays outright: name, size, position and scale. Use it for nested runs, where there are no monitors to detect.output.profilesarranges the monitors that are plugged in. The first profile whose displays are all connected applies. Matching reruns on every hotplug and reload.
{ "output": { "profiles": [ { "name": "desk", "displays": [ { "display": "drm-1", "enabled": false }, { "display": "DEL DELL U3219Q 2ZLS413", "mode": [3840, 2160], "position": [0, 0], "scale": 1.2, "transform": "rotate-270" } ] }, { "name": "laptop-only", "displays": [{ "display": "drm-1", "scale": 1.5 }] } ] }}Each profile entry sets enabled, position, a fractional scale, a
transform, and optionally mode. An entry without position goes right of
the positioned ones, in profile order.
Naming a display
Any of these match:
- the
wl_outputname: on a tty,drm-<id>, or the connector (HDMI-A-1) for a monitor with no EDID make, model or serial - the EDID description
"<MAKE> <MODEL> <SERIAL>", e.g.DEL DELL U3219Q 2ZLS413 - the description with the maker's full name from hwdata's
pnp.ids, e.g.Dell Inc. DELL U3219Q 2ZLS413, as kanshi and sway use
A monitor with no EDID make, model or serial has an empty description.
mode
mode is a check. Domicile does not modeset: the engine lights every
connector at its native mode. A profile's mode (a size, no refresh rate)
states the mode its positions assume. If the monitor comes up at a different
mode, the profile does not apply, the current layout stays, and the log names
both modes. Omit mode to skip the check.
transform
transform uses wl_output values, which count counterclockwise, as in
kanshi and sway. rotate-90 is for a monitor turned a quarter clockwise;
rotate-270 the other way.
More: DISPLAYS.md and RUNNING-A-DESKTOP.md.
Extensions
See Extensions.