← Otto Developer Guide`org.otto.Settings` — the settings D-Bus contract
The D-Bus interface the compositor serves for reading and writing settings, and the contract a settings client can build against.
Status: implemented on both sides. The compositor serves it (
src/settings/,src/settings_service.rs) andotto-settingsconsumes it (components/otto-settings/src/settings_client.rs). This document is the interface they build against. Behavioural requirements live in specs/settings-app.md; this is the wire.What the compositor serves today: 61 settings, of which 55 are
liveand six arerestart. The ninedock.*ones (size,position,autohide,magnification,genie_scale,genie_span,colorize_icons,colorize_color,colorize_intensity) reconfigure the dock in place. The eleveninput.*touchpad/pointer ones (tap_enabled,tap_drag_enabled,tap_drag_lock_enabled,touchpad_click_method,touchpad_dwt_enabled,touchpad_natural_scroll_enabled,touchpad_left_handed,touchpad_middle_emulation_enabled,scroll_speed,pointer_accel_speed,pointer_accel_profile) reconfigure the connected libinput devices (or, forscroll_speed, take effect on the next scroll event, since Otto reads the live config per event rather than caching it). Fifteen are appearance:theme_scheme,accent_color,rounded_corners,frosting,window_controls_side,show_maximize_button,cursor_theme,cursor_size,icon_theme, and the six wallpaper settingsbackground_color,background_image,background_mode,background_position_x,background_position_yandbackground_zoom. For those the compositor’s own chrome and its window decorations are rebuilt, the cursor and icon caches dropped, the wallpaper frames re-rendered, and the change relayed to other processes through the Settings portal. The seven that need a restart arescreen_scale,font_family,gtk_theme,locales,login.greeter_command,login.greeter_argsandrendering.renderer.topbar.show_app_menu,topbar.show_clockandtopbar.clock_formatare live with nothing for the compositor to do: otto-bar reads them over this interface and followsChangeditself.desktop.widgetis live too: the compositor runs ewwii for it and swaps the window it shows, or stops it fornone(desktop-widget.md). No setting isunsupported. Keyed collections (dock bookmarks, shortcuts, display profiles) have no single identifier and are not in the schema.
Bus name org.otto.Settings, object path /org/otto/Settings, interface
org.otto.Settings. The compositor owns the name.
The portal getters (GetColorScheme, GetIconTheme, GetAccentColor) stay
exactly as they are: xdg-desktop-portal-otto depends on them and must not
be disturbed.
Values
Every setting value crosses the bus as a variant. The variant’s contained type is fixed per setting and given in the schema:
Schema type | D-Bus type | Notes |
|---|---|---|
bool | b | |
int | i | |
double | d | |
string | s | free text (paths, command lines) |
enum | s | one of choices |
color | s | a palette name from choices, or a #RGB/#RRGGBB literal |
string-list | as | ordered, e.g. locales |
Sending a variant whose type does not match the schema is an error; the compositor never coerces.
Identifiers
A stable dotted path matching the configuration structure: dock.size,
input.pointer_accel_speed, power_management.on_lid_close. Top-level keys
have no prefix: accent_color, cursor_size.
Identifiers are a permanent contract. Once shipped, an identifier is never renamed or repurposed. An app built against an older compositor must keep working, and the app is the thing that hardcodes these strings.
Methods
Describe() → aa{sv} schema, one dict per setting
GetAll() → a{sv} current effective value of every setting
Get(id: s) → v one value
GetOverridden() → as ids currently set in the writable file
Set(id: s, value: v) → s status, see below
Reset(id: s) → s status
GetColorScheme() → u 0 none, 1 dark, 2 light
GetIconTheme() → s icon theme name, empty to auto-detect
GetAccentColor() → (ddd) accent as sRGB in 0.0..=1.0
GetSoundTheme() → s sound theme name, empty to auto-detect
GetLocales() → as preferred locales, most preferred first
GetFontSettings() → a{ss} UI font and text rendering, by GNOME key
ListOutputs() → aa{sv} every output, physical and virtual
SetOutputProfile(connector: s, width: u, height: u,
refresh_hz: d, x: i, y: i,
primary: b) → s status, always pending-restart
GetOutputSubpixels() → a{ss} subpixel layout each display profile sets
SetOutputSubpixel(connector: s,
subpixel: s) → s status, always pending-restart
AddVirtualOutput(name: s, width: u, height: u,
refresh_hz: d, interactive: b,
persist: b) → u PipeWire node id
RemoveVirtualOutput(name: s) → ()
ListShortcuts() → a(ss) every shortcut in force, (trigger, action)
ConfigPath() → s the file a change is written to
Portal getters
These three answer in the shapes org.freedesktop.appearance defines, so the
portal backend can pass them through untouched. GetAccentColor resolves the
stored accent name against the current palette before converting. The name
is what Get("accent_color") returns, and it is the value Set takes.
Two the portal asks for by other names
GetSoundTheme goes out on org.gnome.desktop.sound as theme-name, with the
same contract as GetIconTheme: empty means no preference, so the application
decides for itself.
GetLocales has no portal key at all. Otto’s own components take their
language from here rather than from LANG, so changing Preferred languages
moves the whole desktop and not just the compositor. Sandboxed third-party
applications still read the environment, which this does not touch.
GetFontSettings is keyed by the names the portal serves under
org.gnome.desktop.interface: font-name (font_family at body size, in
Pango points), font-antialiasing, font-hinting and font-rgba-order. It
is not a setting: the rendering is fontconfig’s and the subpixel order the
outputs’, so the compositor works it out on its own thread, the same values it
sends Xwayland as XSETTINGS. See
Text rendering.
Outputs
Outputs are deliberately not settings. The schema is a table fixed at compile time, and outputs come and go with the hardware, so there is no honest identifier for “the second display”. They get their own three methods instead.
ListOutputs returns one dictionary per output: name, connector, width,
height, refresh (millihertz), x, y, scale, and virtual. A client
drawing a display arrangement reads it from here rather than inventing one.
SetOutputProfile writes displays.named.<connector>, the same profile the
compositor resolves when it brings that output up, so a resolution, refresh
rate, position or primary choice survives a restart. It is keyed by connector
because that is the handle the config has; see the Open section for why that
identity is not the last word. It applies at the next start, never now, and
says so: a modeset made from under a running session cannot be undone if the
display does not come back, and that is worse than a restart. A zero width or
height leaves the resolution unset and a zero rate leaves the refresh unset, so
moving a display need not name a mode for it. Primary is a choice among
displays, so a client turning it on for one must write it off for the others.
SetOutputSubpixel writes displays.named.<connector>.subpixel: rgb,
bgr, vrgb, vbgr or none, and an empty string removes the key, putting
the display back on automatic (grayscale when fontconfig’s rgba is none;
else the connector’s report, then fontconfig’s rgba, then grayscale). Anything else is refused with OutOfRange. It applies
when the output is next created, at the next start or replug, because the
layout is fixed at output creation and clients learn it only then; the running
configuration is updated too, so a replug this session picks it up.
GetOutputSubpixels answers connector to layout for every named profile that
sets one, so an absent connector is on automatic.
AddVirtualOutput creates a PipeWire-backed output on the running compositor
and answers with the node id to capture from. It takes effect immediately: a
virtual screen you must restart to get is useless for the thing it is mostly
wanted for. persist also writes a [[virtual_outputs]] entry to the writable
config so it returns next session; the entry is written only after the output
actually came up, for the same reason Set persists only after a successful
apply. RemoveVirtualOutput tears one down and drops its config entry, and
refuses physical outputs: unmapping one would black out a real screen.
The configuration file
ConfigPath answers with the file a changed setting is written to. Otto’s
configuration is layered and the writable layer is whichever sits on top: a
local otto_config.toml next to the running binary outranks
~/.config/otto/config.toml. A client cannot work that out for itself. It is
worth showing: everything a settings app changes lands there, and anything it
does not offer is edited by hand.
Describe returns one dictionary per setting. Keys:
| Key | Type | Meaning |
|---|---|---|
id | s | the identifier |
type | s | from the table above |
section | s | config section, e.g. dock; empty for top-level |
label | s | short human-readable name |
description | s | one sentence; may be empty |
default | v | the built-in default |
apply | s | live, restart, or unsupported |
min | v | optional, numeric types only |
max | v | optional, numeric types only |
step | v | optional, numeric types only; granularity to snap to |
choices | as | optional, required for enum; for color, the palette names to offer as swatches |
choice_labels | as | optional; human names for choices, same order |
unavailable_choices | as | optional; entries of choices this build cannot take |
applies_here | b | optional; false when the setting has no effect under the running backend |
confirm | s | optional; password when Set and Reset first ask the user through polkit |
Unknown keys must be ignored by clients, so the schema can grow without breaking them.
A setting with confirm = "password" (the locker, greeter, auto-lock,
locking on suspend, the lid and power button) is changed only after polkit
authorizes org.otto.settings.lock: Otto’s auth panel names the change and the
user types their password. The call waits for the answer, which can take as
long as the user takes, so call it without a short timeout. A refusal, or
polkit not running, is org.freedesktop.DBus.Error.AccessDenied. Setting such
a value to what it already is asks nothing.
step exists because a slider maps pixels to values: without it a drag lands
on whatever float the pixel happened to hit, and a pointer speed the user meant
to leave alone reads -0.01. Where a setting has a range it has a step, and
the step always divides the range so the advertised maximum stays reachable.
Clients should also snap to default when the drag comes within half a step of
it, so a value can land exactly back on the inherited one.
choice_labels exists because the strings in choices are configuration
tokens, and those are part of the permanent contract. clickfinger has to
stay clickfinger on the wire and in the file, however badly it reads in a
menu. When present it has exactly one entry per choice, and a client shows it
in place of the token while continuing to Set the token. When absent, the
tokens are already fit to show.
unavailable_choices lists choices the compositor knows but this session
cannot honour, such as vulkan for rendering.renderer in a build without
the vulkan feature, or in a session that asked for Vulkan and fell back to
OpenGL because it could not start on the primary GPU. They stay in choices
so a client can explain their absence; Set refuses them with Unsupported,
and a menu should leave them out.
applies_here is present only for a setting tied to particular backends.
rendering.renderer reaches the tty (udev) backend alone, so under --winit
or --x11 it is served with applies_here: false. The value can still be
set, for the next login session; a client should present it as not affecting
the session it runs in. A missing key means the setting applies.
apply is what the setting does when set: live takes effect immediately;
restart is persisted but needs a compositor restart, with the running
session left exactly as it was, so nothing that happens to re-read the value
can half-apply it; unsupported cannot be changed on this system or in this
build (a display setting under a windowed backend, say) and Set will reject
it. It
must be truthful: a Set that silently does nothing is worse than one that
refuses.
Get and GetAll answer for a restart setting with the value the next start
will use: after a Set they return the persisted value, not the one the running
session is still on.
Set performs validate → apply → persist → announce, in that order. It
returns a status string:
applied: live now, and persisted.pending-restart: persisted, takes effect on restart.
Any failure is a D-Bus error, not a status, and nothing is persisted.
Reset removes the identifier from the writable config file so its value
falls back to whatever the lower configuration layers provide. This is not
the same as setting it to its default: the writable file is the
highest-priority layer, so a default written into it permanently shadows every
lower layer. Resetting an identifier that is not in the writable file succeeds
and changes nothing. Returns the same status strings as Set.
GetOverridden returns the identifiers currently present in the writable
file, which is what the app needs to show a per-setting revert affordance.
Effective values always come from GetAll.
Signal
Changed(values: a{sv})
Emitted after any effective value changes, from any source: this API, an in-compositor interaction such as dragging the dock handle, or an external edit of a configuration file. Carries only the identifiers that changed.
A client that called Set also receives the signal. It must not assume it is
the only writer, and must not suppress its own echo.
The signal is coalesced: a continuous interaction such as dragging a slider applies live throughout but emits and persists when the interaction settles.
Errors
| Error name | When |
|---|---|
org.otto.Settings.Error.UnknownSetting | no such identifier |
org.otto.Settings.Error.InvalidType | variant type does not match the schema |
org.otto.Settings.Error.OutOfRange | outside min/max, or not in choices |
org.otto.Settings.Error.Unsupported | apply is unsupported on this system, or the value is in unavailable_choices |
org.otto.Settings.Error.ApplyFailed | valid, but the compositor could not apply it; message says why |
ApplyFailed means nothing was persisted and the running state is unchanged.
What a client can rely on
- The schema is authoritative for labels, ranges, defaults and apply
behaviour. A client renders from
Describerather than hardcoding them. GetAllreturns a value for every identifierDescribelists.- Every identifier in
GetOverriddenappears inDescribe. - Values are effective values: whatever the layered configuration resolved to.
Worked example
Describe() → [
{ id: "dock.size", type: "double", section: "dock",
label: "Size", description: "Dock size multiplier",
default: <1.0>, min: <0.5>, max: <2.0>, step: <0.05>, apply: "live" },
{ id: "dock.position", type: "enum", section: "dock",
label: "Position on screen", default: <"bottom">,
choices: ["bottom", "left", "right"],
choice_labels: ["Bottom", "Left", "Right"], apply: "live" },
{ id: "accent_color", type: "color", section: "",
label: "Accent colour", default: <"blue">,
choices: ["blue", "purple", "pink", ...],
choice_labels: ["Blue", "Purple", "Pink", ...], apply: "live" },
{ id: "input.touchpad_click_method", type: "enum", section: "input",
label: "Click method", default: <"clickfinger">,
choices: ["clickfinger", "buttonareas"],
choice_labels: ["Click with fingers", "Click in corners"], apply: "live" },
...
]
Set("dock.size", <1.25>) → "applied"
… Changed({ "dock.size": <1.25> })
Set("login.greeter_command", <"otto-greeter">) → "pending-restart"
Set("dock.size", <9.0>) → error OutOfRange
Set("dock.size", <"big">) → error InvalidType
Reset("dock.size") → "applied"
… Changed({ "dock.size": <1.0> }) // whatever the lower layers give
Open
- Shortcuts are a keyed collection, not a scalar, and do not fit
Set(id, value)cleanly.ListShortcutsreads them, merged across the config layers, with each action described as one line (FocusLeft,Workspace 2,run kitty,open browser). Writing them likely needs its own methods (SetShortcut/ResetShortcut) rather than an identifier per binding. - Per-output display settings need a stable identity for a display, so settings follow the panel rather than the connector. That identity is not yet defined, so display identifiers are not final.