Skip to content

Overlays and system integration

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 name
food → its category
italian → one of its keywords
🍕 → the character itself

Fuzzy 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"

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".

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.

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 = true
refresh_minutes = 5

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"

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 = false

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) or blip (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-change is the stock sound from sound-theme-freedesktop, if you would rather hear what other desktops play.
  • A path to an audio file of your own; ~/ is expanded.
  • none (or off, 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 = true
hide_delay_ms = 1500
margin = 96
volume_sound = "pop"
volume_sound_volume = 0.6

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.

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.

  • Check that the compositor is delivering it: the shell logs input-trigger: "moonswing.power-button" owned on 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 poweroff does 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.

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 = true
max_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)

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.

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.

  • 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 (polkitd on Debian and Ubuntu, polkit on Fedora, Arch and openSUSE).
  • Registration needs a logind session. If echo $XDG_SESSION_ID prints nothing and loginctl does not know about your session, polkit has nothing to register an agent for.

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 = true
max_attempts = 3
[screenshare]
enabled = true
preview_fps = 10
max_fps = 0
  • 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 .portal that tells xdg-desktop-portal the shell implements ScreenCast, and a -portals.conf that prefers it over any other backend claiming the same interface.

    From a source build, make install (or make 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-portal once.

    The snap does all of this for you. Its install hook writes /usr/share/xdg-desktop-portal/portals/moonswing.portal and /usr/share/xdg-desktop-portal/{miracle-wm,mir}-portals.conf; the launcher writes the same pair under ~/.local/share and ~/.config on first run, and restarts xdg-desktop-portal itself. snap remove deletes both sets again. Nothing is manual.

    Two names for the conf because XDG_CURRENT_DESKTOP is miracle-wm:mir, and xdg-desktop-portal tries <desktop>-portals.conf for each name in that list before falling back to the generic portals.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/share is 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.conf already points ScreenCast at another backend it wins, and the shell logs which file and which line to change. Setting enabled = false above 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.

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 = true
  • libeis, libei’s server library — libeis1 on 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-pointer and pointer-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 .portal lists both interfaces, and the -portals.conf routes both to it. An existing *-portals.conf of your own needs one more line under [preferred]:

    org.freedesktop.impl.portal.InputCapture=moonswing

    The 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] enabled off stops the snap registering the portal files at all, including for input capture.

  • 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.