Overlays and system integration
Emoji Picker
Section titled “Emoji Picker”Ctrl+Shift+E opens a centred picker over whatever is on screen. Type to search, move with the arrow keys, and press Enter to copy the highlighted emoji and close. Escape — or a click on the backdrop — leaves without copying.
The search is fuzzy and runs across every dimension an emoji has, so all four of these find 🍕:
pizza → its namefood → its categoryitalian → one of its keywords🍕 → the character itselfFuzzy means the letters need only appear in order: gfws finds “grinning face with sweat”, thmbs finds “thumbs up”. Spaces are ordinary characters here — the copy key is Enter, not Space — so a query may be several words, and those words need only appear in order too: face joy finds “face with tears of joy”.
A literal match always beats a scattered one, and the dimensions are weighted: the name first, then the keywords, then the category. So typing cat puts the cat face above the emoji merely filed under a category.
Copying goes through wl-copy, from the wl-clipboard package. Wayland has no clipboard a client can write to without a seat, and the shell’s surfaces are layer-shell ones, so there is no in-process route; if wl-copy is missing the picker says so in a notification rather than failing quietly.
Rebind or disable it under [shortcuts]:
[shortcuts]open_emoji = "super+period"Calendar
Section titled “Calendar”The [calendar] section configures the Calendar tab, reached by clicking the clock. The tab is a month grid beside the current local time. On its own it fetches nothing over the network. Events appear only once an account is signed in under Settings › Accounts: a Google account (see Google Account), a CalDAV server (see CalDAV calendars below), or any number of both. Settings › Shell › Calendar holds this section and, per signed-in account, which of its calendars are read.
| Key | Type | Default | Description |
|---|---|---|---|
week_start |
string | "sunday" |
First column of the month grid and the week view: "sunday" or "monday". |
Events on the calendar
Section titled “Events on the calendar”With a Google account or a CalDAV calendar signed in and chosen, events are drawn on the calendar itself, every account’s together. Each day of the month holds a bar per event, as many as the cell has room for, then +N more, which opens that day. An all-day or multi-day event is a filled banner in its calendar’s colour; a timed one is a dot, its start time and its title.
Month, Week and Day above the grid switch the view. Week and Day are a time grid: each timed event is a box as tall as the meeting is long, events that overlap share the column, all-day events sit in a strip across the top, and a line marks the current time. They open scrolled to the working day. In Week, clicking a day’s name opens that day. The chevrons step by a month, a week or a day.
Clicking any event opens its details: when, which calendar (and account), where, and the description. The buttons under it go where the event links: Join for a Google Meet, Zoom or Teams call, Open in Google Calendar (for a Google event), and one per attachment or link in the description.
If a calendar cannot be read, its name and the server’s reason appear above the grid with Retry. The other calendars’ events stay on screen.
CalDAV calendars
Section titled “CalDAV calendars”The CalDAV servers signed in under Settings › Accounts (see Task list sync, under the todo board, for signing in) can show their calendars of events here, as many servers as you like. Settings › Shell › Calendar lists each server’s calendars under its own heading, with a switch each; none is shown until it is switched on. The calendars are looked for again each time that page opens, so one made on the server since appears there.
A recurring event is expanded by the server, into its occurrences in the month on screen. For a server that does not, the shell reads the common rules itself (daily, weekly on given days, monthly on a day of the month, yearly, with an interval, a count or an end date, and the dates excepted); an event with a rule it cannot read shows its first occurrence only. Times written in a named time zone are converted with the time zone database the world clocks use.
| Key | Type | Default | Description |
|---|---|---|---|
calendars |
array of strings | [] |
The calendars to show, by their address on the server, as Settings › Shell › Calendar writes them. One list serves every account, since an address belongs to one server. |
show_in_calendar |
bool | true |
Draw the chosen calendars’ events on the Calendar tab, and offer its Week and Day views. |
refresh_minutes |
int | 5 |
How often events are read again while the Calendar tab is open (1–60). Nothing is fetched while it is closed. |
[caldav]calendars = ["https://dav.example.com/dav/calendars/me/personal/"]show_in_calendar = truerefresh_minutes = 5World Clocks
Section titled “World Clocks”The column on the right of the tab shows the local time as an analog dial and a digital readout, and under it one row per [[calendar.world_clocks]] entry. The + button on that column writes these entries for you and each row’s ✕ removes it, so there is nothing to configure in the Settings tab — the keys are documented because the file is yours to edit.
| Key | Type | Default | Description |
|---|---|---|---|
zone |
string | An IANA time zone name, e.g. Europe/London. Required — an entry without one is ignored. |
|
label |
string | derived | Renames the row. Omit it to use the zone’s city (Europe/London → London). |
Rows are drawn in the order they appear in the file. Offsets follow daylight saving automatically, from the IANA database bundled with the shell, and a row whose calendar date differs from yours is badged +1d or -1d. A zone this build’s database does not know renders as “Unknown time zone” — with its remove button intact — rather than being silently deleted.
[calendar]week_start = "monday"
[[calendar.world_clocks]]zone = "Europe/London"
[[calendar.world_clocks]]zone = "Asia/Tokyo"label = "HQ"Notifications
Section titled “Notifications”The [notifications] section holds the state of the notification bell’s silence switch. The shell writes this key itself — right-click the bell, or use the switch at the top of the notification panel — so it is documented because the file is yours to edit, not because you have to.
| Key | Type | Default | Description |
|---|---|---|---|
silenced |
boolean | false |
Whether notifications are silenced. |
Silencing is about interruption, never about delivery. The shell keeps acting as the notification daemon and every notification still arrives and stacks up in the panel; what stops is the shell asking for your attention — the bell no longer shakes, the chime does not play, and the floating card that plants itself in the corner of every monitor does not appear. The bell wears a crossed-out glyph while the switch is on, so the state is never invisible.
The sound itself is configured in [modules.notifications], not here: this key is a decision about the machine, and that one is an option of the module that plays it.
It is a file key rather than state that dies with the process on purpose: a shell that quietly started interrupting you again after a restart would be the one failure a “do not disturb” switch may not have.
[notifications]silenced = falseOn-Screen Indicator
Section titled “On-Screen Indicator”The [osd] section configures the indicator that appears when the volume, microphone volume, or screen brightness changes, and the sound a volume change makes. The indicator is an icon for what changed plus a bar for its current level, floating above the bottom edge of every monitor. It fades out once the changes stop.
The shell only watches these values; it does not bind the keys. Whatever already applies the change (your compositor’s media-key bindings, or the shell’s own volume slider) keeps doing so, and the indicator follows. Brightness is read from /sys/class/backlight, so machines without a panel backlight simply never see the sun indicator.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Whether to show the indicator at all. |
hide_delay_ms |
integer | 1500 |
How long the indicator stays up after the last change. Clamped to a minimum of 100. |
margin |
integer | 96 |
Distance from the bottom edge of the screen, in pixels. |
volume_sound |
string | "pop" |
What plays when the output volume changes or is unmuted. See below. |
volume_sound_volume |
float | 0.6 |
How loud that sound is, from 0 to 1. Clamped to that range. |
A volume change also makes a small sound, played through the output that just changed — so you hear the new level before any music does. A mute is silent; the unmute that follows is not. A volume key held down ticks at most about eight times a second rather than buzzing. The sound works with enabled = false too: switching the card off leaves it playing.
volume_sound takes the same spellings as every other sound key:
- A shipped sound —
pop(a short, soft tone; the default),tick(a dry click) orblip(a brighter tone that carries over music). They are synthesised by the shell itself, so they are always there and are free software under the shell’s own licence. - A sound-theme name — looked up in the
sounds/directories of your XDG data dirs.audio-volume-changeis the stock sound fromsound-theme-freedesktop, if you would rather hear what other desktops play. - A path to an audio file of your own;
~/is expanded. none(oroff,silent) for no sound.
A name that finds nothing is not treated as silence: Settings › Shell › Volume Sound says what is wrong, and its play button previews the sound. Both keys are live, but if the indicator and the sound were both off when the shell started, nothing is watching the volume until it is restarted.
[osd]enabled = truehide_delay_ms = 1500margin = 96volume_sound = "pop"volume_sound_volume = 0.6Power Button
Section titled “Power Button”The [power] section decides what pressing the machine’s own power button does. Out of the box the shell intercepts it and shows the power menu — a dialog offering Lock, Log Out, Sleep, Restart and Shut Down — instead of the machine powering off where it stands.
[power]key_action = "menu"inhibit_logind = true| Key | Type | Default | Description |
|---|---|---|---|
key_action |
string | "menu" |
What a press does |
inhibit_logind |
boolean | true |
Hold systemd-logind’s handle-power-key lock while the shell handles the key |
key_action takes one of:
| Value | What happens |
|---|---|
"menu" |
Show the power menu. Escape, or a click outside it, cancels; Enter answers with Shut Down |
"shutdown" |
Power off, with no confirmation |
"reboot" |
Restart, with no confirmation |
"suspend" |
Suspend to RAM |
"lock" |
Lock the session |
"logout" |
End the session |
"none" |
Nothing — the key is left to systemd-logind, which is what handles it on a machine with no shell running |
"off", "restart", "sleep" and "ignore" are accepted as spellings of shutdown, reboot, suspend and none. A value the shell does not recognise falls back to "menu" and logs why, rather than silently leaving you with a power button that does nothing.
Both keys are live: changing them in Settings → Shell → Power Button takes effect on the next press. The binding — [shortcuts] power_button — is not, because global shortcuts are registered once at start-up.
key_action is about the physical key alone. The panel’s power icon and the open_power_menu shortcut (Super+Shift+E) always open the power menu, whatever this is set to — including "none", which leaves the key to logind without leaving you with no way to shut down from the shell.
Why inhibit_logind exists
Section titled “Why inhibit_logind exists”systemd-logind watches the power button directly, and HandlePowerKey in logind.conf is poweroff on a stock system. It does not care that a compositor also delivered the key to the shell — so a shell that only listened would draw its power menu onto a machine that was already going down.
While the shell is handling the key it therefore takes logind’s handle-power-key inhibitor lock, in block mode, which stops logind acting on presses for as long as the shell holds it. The lock is released the moment the shell exits — including if it crashes — so it can never leave a machine that will not power off.
Two things it deliberately does not claim: handle-power-key-long-press (holding the button down keeps doing whatever logind is configured to do, which is the “get me out of here” gesture), and the key on a compositor that never delivered it. The shell takes the lock only once the compositor has confirmed the shortcut registration, so on a compositor without Mir’s ext-input-trigger protocols — or where another client already owns the key — the button keeps working exactly as it did before.
Turn inhibit_logind off if your logind.conf already says HandlePowerKey=ignore; otherwise, with it off, logind’s shutdown and the shell’s dialog race.
If the button does nothing
Section titled “If the button does nothing”-
Check that the compositor is delivering it: the shell logs
input-trigger: "moonswing.power-button" ownedon start-up when it has the key, and says so when another client owns it instead. -
Some keyboards’ power keys emit a keysym this table’s
poweroffdoes not match. Bind the physical key instead, which is layout- and keysym-independent:[shortcuts]power_button = "code:116" # evdev KEY_POWER -
A laptop lid or a “sleep” key is a different key entirely (
sleep,code:142); this section is only about the power button.
Authentication Prompts
Section titled “Authentication Prompts”The [polkit] section decides whether the shell answers polkit’s authentication requests — the “Authentication required” dialog you get when something asks for administrator rights.
[polkit]enabled = truemax_attempts = 3| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Register as this session’s polkit authentication agent |
max_attempts |
integer | 3 |
How many times the password may be refused before the dialog gives up (clamped to 1–5) |
What this is for
Section titled “What this is for”polkit never asks anybody anything by itself. When an application requests a privileged operation — changing the system keyboard layout, mounting a disk, installing an update — polkitd looks for an authentication agent registered for your session and asks it to get the password. With no agent registered there is nothing to ask, so every one of those requests is refused immediately, which usually surfaces as a setting that silently refuses to change.
Most desktops ship one (polkit-gnome, mate-polkit, lxpolkit). miracle-wm does not, which is why the shell is one.
The prompt is a dialog in the middle of the screen, in your theme: what is being asked, which account is answering, and a field for the password. Escape, a click outside it, or Cancel all refuse the request — dismissing is denying, not deferring.
Turning it off
Section titled “Turning it off”The shell yields automatically if it finds an agent already registered when it starts: that session already has working prompts, and two agents fighting over the registration is how one of them silently stops prompting. enabled = false is for the other ordering — an agent that starts after the shell, which would otherwise find the registration taken.
With enabled = false and no other agent running, nothing on the desktop can ask for administrator rights.
If prompts do not appear
Section titled “If prompts do not appear”- The shell logs
polkit: registered as the authentication agent for session <id>on start-up, and says so when another agent has the registration instead. - The password is checked by
polkit-agent-helper-1, a small setuid program that ships with polkit itself. If it is missing the dialog says so rather than failing silently; install polkit (polkitdon Debian and Ubuntu,polkiton Fedora, Arch and openSUSE). - Registration needs a logind session. If
echo $XDG_SESSION_IDprints nothing andloginctldoes not know about your session, polkit has nothing to register an agent for.
Screen Sharing
Section titled “Screen Sharing”The [screenshare] section configures the shell’s xdg-desktop-portal ScreenCast backend. With it installed, an application asking to share your screen (a browser calling getDisplayMedia, a video call, OBS) raises the shell’s own picker: a full-screen overlay showing a live preview of every monitor and every open window. Nothing is shared until you choose a source and press Share — dismissing the picker with Escape or a click outside denies the request.
Both monitors and individual windows can be shared. Frames reach the application as a PipeWire video stream.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Whether to claim the ScreenCast backend at all. Set false to leave screen sharing to another backend such as xdg-desktop-portal-wlr. |
preview_fps |
integer | 10 |
Frame rate for the picker’s live previews. Clamped to 1–60. Deliberately low: the picker captures every monitor and every window at once. |
max_fps |
integer | 0 |
Cap on the shared stream’s frame rate. 0 follows the monitor’s refresh rate. |
[polkit]enabled = truemax_attempts = 3
[screenshare]enabled = truepreview_fps = 10max_fps = 0Requirements
Section titled “Requirements”-
A compositor with
ext-image-copy-capture-v1— miracle-wm built against MirAL 5.6 or newer. Without it the feature disables itself and logs why. -
PipeWire 1.0 or newer, which every current desktop already runs.
-
The portal files installed. Two of them: a
.portalthat tells xdg-desktop-portal the shell implements ScreenCast, and a-portals.confthat prefers it over any other backend claiming the same interface.From a source build,
make install(ormake install-portal) writes:~/.local/share/xdg-desktop-portal/portals/moonswing.portal~/.config/xdg-desktop-portal/mir-portals.conf(only written if absent, so an existing preference is never overwritten)
then run
systemctl --user restart xdg-desktop-portalonce.The snap does all of this for you. Its install hook writes
/usr/share/xdg-desktop-portal/portals/moonswing.portaland/usr/share/xdg-desktop-portal/{miracle-wm,mir}-portals.conf; the launcher writes the same pair under~/.local/shareand~/.configon first run, and restarts xdg-desktop-portal itself.snap removedeletes both sets again. Nothing is manual.Two names for the conf because
XDG_CURRENT_DESKTOPismiracle-wm:mir, and xdg-desktop-portal tries<desktop>-portals.conffor each name in that list before falling back to the genericportals.conf. Two locations because of precedence:portals.conf(5)searches$XDG_CONFIG_HOME, then$XDG_CONFIG_DIRS, then/etc, then$XDG_DATA_HOME, then$XDG_DATA_DIRS./usr/shareis the last of those, which makes the system-wide copy a machine default that never out-ranks a choice you made; only the per-user copy is high enough to beat an existing~/.config/xdg-desktop-portal/portals.conf.Nothing you wrote is ever edited. If a
~/.config/xdg-desktop-portal/*-portals.confalready points ScreenCast at another backend it wins, and the shell logs which file and which line to change. Settingenabled = falseabove also stops the snap registering the backend at all, so opting out does not leave a dangling preference behind.
There is deliberately no D-Bus activation file: the shell owns the backend name from session start, so screen sharing cannot start the shell. If the shell is not running, screen sharing is simply unavailable.
Input Capture
Section titled “Input Capture”The [input_capture] section configures the shell’s xdg-desktop-portal InputCapture backend — what lets a keyboard-and-mouse sharing application such as DeskFlow or Input Leap use this computer as the one whose keyboard and mouse are shared. The application asks once; the shell shows a prompt naming it, and nothing is captured unless you press Allow. Escape, a click outside, or Deny refuses.
Once allowed, the application draws its barriers — the screen edges its other computers sit beyond. Push the pointer through one and the cursor stays put, hidden, while every key, click, scroll and movement goes to the application (over a libei socket) instead of to anything on this computer, until the application hands them back.
Super+Shift+Escape always takes the keyboard and mouse back, and stops that application capturing again until it asks anew — so an application that hangs or misbehaves while holding your input cannot keep it.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Whether to serve InputCapture at all. Set false to leave it to another backend, or to nobody. |
[input_capture]enabled = trueRequirements
Section titled “Requirements”-
libeis, libei’s server library —
libeis1on Debian and on Ubuntu 24.04 and later. The snap carries its own. Without it the feature disables itself and logs why. -
A compositor with
wlr-layer-shell,relative-pointerandpointer-constraints. sway and the other wlroots compositors have all three; on any compositor missing one, the shell logs which. -
The portal files, the same ones as screen sharing: the shell’s
.portallists both interfaces, and the-portals.confroutes both to it. An existing*-portals.confof your own needs one more line under[preferred]:org.freedesktop.impl.portal.InputCapture=moonswingThe snap adds that line itself to a conf it wrote before the shell served InputCapture; one you wrote is left for you to change. Turning
[screenshare] enabledoff stops the snap registering the portal files at all, including for input capture.
How it behaves
Section titled “How it behaves”- The last pixel of an armed edge belongs to the shell. The shell can only see the pointer reach an edge through a surface of its own, so while an application has barriers armed, a one-pixel strip along that edge takes clicks that would have landed on whatever was under it. Nothing is there while no application is armed.
- A barrier must be on the outside of your screens. The edge where two monitors meet is refused, so moving between your own monitors is never captured.
- Plugging or unplugging a monitor ends a capture and asks the application to draw its barriers again.
- A lock screen ends a capture, so a password is never typed into the other computer.