Sweets
Configuration

Key Bindings

Bind keys and multi-step sequences to Sweets' typed compositor actions.

sweets.bind(spec, action [, argument]) maps one key combination or a multi-step key sequence to a typed Sweets action.

sweets.bind("MOD+Return", "spawn", { "foot" })
sweets.bind("MOD+Q", "close")
sweets.bind("MOD+2", "workspace", 2)
sweets.bind("MOD+Ctrl+Right", "focus_output_right")
sweets.bind("MOD+B f", "spawn", { "firefox" })

The optional third argument is required only by spawn, workspace, workspace_move, and layout.

Key specifications

A single combination contains zero or more modifiers followed by one XKB keysym. Separate its parts with +:

MOD+Shift+Return
Ctrl+Alt+Delete
XF86AudioMute

The canonical modifier names are:

NameMeaning
MODThe modifier selected by sweets.general({ mod_key = ... })
CtrlControl
ShiftShift
AltAlt
SuperLogo, Windows, or Super key

Modifier names are case-insensitive. Control, Mod1, Logo, Mod4, and Win are also accepted aliases. MOD resolves to Super by default and can be changed to Alt; see General.

Do not combine MOD with the modifier it already represents. For example, MOD+Super+Q is invalid while mod_key = "SUPER".

Key names are case-insensitive XKB keysym names. Common examples include q, Return, Escape, space, equal, KP_Add, and XF86AudioRaiseVolume. q and Q identify the same key; add Shift when the binding requires Shift.

Sweets matches modifiers exactly:

  • An undeclared Ctrl, Shift, Alt, Super, ISO level-3, or ISO level-5 modifier prevents a match.
  • Caps Lock and Num Lock do not affect matching.
  • Latin-letter bindings use the raw Latin symbol when XKB provides one, so they remain stable while switching keyboard layout groups.

A key specification must contain from 1 through 128 bytes. Unknown keysyms, unknown or repeated modifiers, empty components, and duplicate effective modifiers reject the configuration candidate.

Multi-step sequences

Separate sequence steps with whitespace. Each step has its own exact modifiers:

sweets.bind("MOD+B f", "spawn", { "firefox" })
sweets.bind("MOD+B c", "spawn", { "chromium" })
sweets.bind("Ctrl+X Ctrl+S", "reload")

For MOD+B f, press and release MOD+B, then press unmodified f. Keeping MOD held changes the second step and does not match.

A sequence may contain up to eight steps. After a prefix matches, Sweets consumes it and waits one second for the next step. Every accepted step renews the one-second deadline. A complete sequence runs immediately.

If the next key does not match a branch, Sweets cancels the sequence and consumes that key press and release. If the sequence times out first, the next key is handled normally.

The effective binding set must be prefix-free. These two bindings are ambiguous and cannot coexist:

sweets.bind("MOD+B", "close")
sweets.bind("MOD+B f", "spawn", { "firefox" })

Several longer sequences may share the same prefix, as the browser example does. To replace an inherited direct binding with a longer sequence, remove the direct binding first:

sweets.unbind("MOD+Q")
sweets.bind("MOD+Q Q", "close")

Actions

Action names are case-insensitive; the canonical lowercase names below are recommended. Actions listed with no argument reject an extra argument.

Programs and window state

ActionArgumentResult
spawnDirect argv tableLaunch a program without shell interpretation
closeNoneAsk the focused toplevel to close
quitNoneStop Sweets cleanly
reloadNoneReload and transactionally apply the configuration
dismiss_errorNoneHide the current config-error bar without clearing its diagnostic
toggle_shortcuts_inhibitNoneToggle shortcut inhibition for the focused requesting surface
toggle_floatingNoneToggle the focused window between tiled and floating
toggle_fullscreenNoneToggle the focused window between normal and output fullscreen
toggle_maximizeNoneToggle the focused window between normal and maximized
minimizeNoneHide the focused window without removing it from its workspace
restore_minimizedNoneRestore and focus the most recently minimized window on its original workspace
special_toggleNoneShow or hide the special workspace on the selected output
special_moveNoneMove the focused window into the special workspace without revealing it

Focus, movement, and workspaces

ActionArgumentResult
focus_next, focus_prevNoneCycle focus in the visible workspace
focus_left, focus_right, focus_up, focus_downNoneFocus spatially in the requested direction
focus_output_left, focus_output_right, focus_output_up, focus_output_downNoneSelect an adjacent output
move_left, move_right, move_up, move_downNoneReorder or move the focused window in that direction
move_output_left, move_output_right, move_output_up, move_output_downNoneMove the focused window to an adjacent output
workspaceInteger 1 through 10Switch to a numbered workspace
workspace_moveInteger 1 through 10Move the focused window to a numbered workspace

Directional focus and movement first look for a local target. Their optional cross-monitor fallback is controlled by focus_cross_monitor and move_cross_monitor. The explicit focus_output_* and move_output_* actions are independent of those fallback settings.

workspace_move follows the moved window only when general.workspace_move_follow is enabled. Workspace actions above the active workspace_count remain valid bindings but do nothing until that workspace number is enabled. See Workspaces.

Layout and resizing

ActionArgumentResult
layoutLayout nameSelect tile, monocle, centered, grid, columns, deck, or dwindle
cycle_layoutNoneMove forward through the configured layout cycle
cycle_layout_reverseNoneMove backward through the configured layout cycle
reset_layoutNoneRestore the visible workspace's active layout defaults
reset_all_layoutsNoneRestore active layout defaults on every enabled workspace
cycle_master_positionNoneRotate the visible workspace's master position forward
cycle_master_position_reverseNoneRotate the master position backward
resize_grow, resize_shrinkNoneGrow or shrink the focused tiled window horizontally
resize_grow_vertical, resize_shrink_verticalNoneGrow or shrink the focused tiled window vertically

Layout cycling uses sweets.layout({ cycle = ... }). Resize behavior depends on the active layout and has a compatibility rule for rotated single-axis layouts. See Layout for the complete behavior.

Spawning programs

spawn requires a direct argv table. The first string is the executable and every remaining string is one literal argument:

sweets.bind("MOD+E", "spawn", { "foot", "nvim", "/tmp/notes.txt" })
sweets.bind("MOD+D", "spawn", { "fuzzel" })

Sweets does not invoke a shell. Pipes, redirection, variable expansion, wildcards, quoting, and ~ expansion are not interpreted.

The argv table must contain 1 through 32 consecutive strings. Each string can use up to 256 UTF-8 bytes, all strings together can use up to 4096 bytes, and the executable cannot be empty.

Spawned programs receive the configured Sweets session environment and its authoritative WAYLAND_DISPLAY. Standard error remains connected to the Sweets session log, so launch failures are diagnosable there. A missing executable does not stop the compositor.

Use sweets.env when future binding spawns need an environment variable; see Environment.

Replacing and removing bindings

Every configuration candidate begins with a small emergency binding map. The installed sweets.lua adds the normal desktop bindings, then your user configuration can replace or remove either set.

Binding the same canonical sequence once in your user layer replaces the active action:

sweets.bind("MOD+Return", "spawn", { "alacritty" })

Remove an active binding with sweets.unbind:

sweets.unbind("MOD+Space")

An unbind affects only that exact sequence. It does not remove longer sequences sharing the same steps.

A canonical sequence may be mentioned once in the installed layer and once in the user layer; the user declaration wins. Mentioning it twice within one layer, including through files loaded by sweets.include, is invalid. An attempt to unbind an inactive sequence is also invalid.

At most 256 binding declarations and 256 active bindings are retained. Any duplicate, ambiguous prefix, unknown action, wrong argument, inactive unbind, or limit violation rejects the complete candidate. Sweets keeps the previous valid binding set.

Default bindings

MOD uses general.mod_key; its built-in and installed default is Super. The Rust emergency map is always the starting point:

BindingAction
MOD+ReturnSpawn foot
MOD+QClose the focused window
MOD+Shift+QQuit Sweets
MOD+Shift+RReload the configuration
MOD+Shift+EDismiss the current config-error bar
MOD+EscapeToggle shortcut inhibition
MOD+SpaceToggle floating
MOD+FToggle fullscreen
MOD+J, MOD+KFocus next or previous
MOD+Shift+H/J/K/LMove left, down, up, or right
MOD+equal, MOD+minusGrow or shrink horizontally
MOD+Shift+equal, MOD+Shift+minusGrow or shrink vertically

The installed Lua configuration adds this desktop workflow:

BindingAction
MOD+Shift+FToggle maximize
MOD+M, MOD+Shift+MMinimize or restore the most recently minimized window
MOD+H/L, MOD+Arrow keysFocus spatially
MOD+Shift+Arrow keysMove in a direction
MOD+Ctrl+H/J/K/L, MOD+Ctrl+Arrow keysFocus an adjacent output
MOD+Ctrl+Shift+H/J/K/L, MOD+Ctrl+Shift+Arrow keysMove to an adjacent output
MOD+1..9/0Switch to workspaces 1 through 10
MOD+Shift+1..9/0Move the focused window to workspaces 1 through 10
MOD+S, MOD+Shift+SToggle the special workspace or move a window into it
MOD+KP_Add, MOD+KP_SubtractGrow or shrink horizontally
MOD+Shift+KP_Add, MOD+Shift+KP_SubtractGrow or shrink vertically
MOD+I, MOD+Shift+ICycle the master position forward or backward
MOD+W, MOD+Shift+WCycle layouts forward or backward
MOD+U, MOD+Shift+UReset the current layout or every enabled workspace

Bindings not mentioned by your user configuration remain active. Changing general.mod_key moves every MOD binding, including the emergency map; bindings written with an explicit modifier such as Super do not move.

Shortcut inhibition

Games, virtual machines, remote-session tools, and other clients can request keyboard-shortcut inhibition. While the requesting surface has focus, ordinary Sweets bindings are forwarded to that client.

toggle_shortcuts_inhibit remains compositor-owned and can disable or restore the focused surface's inhibitor. The emergency MOD+Escape binding provides a default escape hatch. If you remove it, keep another binding for the same action.

An ordinary sequence cannot begin while inhibition is active. A sequence prefix remains available only when one of its branches reaches toggle_shortcuts_inhibit; choosing another branch cancels and consumes that key. Native virtual-terminal switching also remains compositor-owned and is not replaced by Lua bindings.

Configured bindings are not handled while the session is locked. The lock client receives keyboard input, while native virtual-terminal switching remains reserved.

Media-key examples

The installed configuration does not bind multimedia keys. They are ordinary XKB keysyms and can launch your preferred helpers:

sweets.bind("XF86AudioRaiseVolume", "spawn",
    { "wpctl", "set-volume", "-l", "1.0", "@DEFAULT_AUDIO_SINK@", "5%+" })
sweets.bind("XF86AudioLowerVolume", "spawn",
    { "wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%-" })
sweets.bind("XF86AudioMute", "spawn",
    { "wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle" })
sweets.bind("XF86AudioMicMute", "spawn",
    { "wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", "toggle" })

-- PulseAudio equivalents if you do not run PipeWire:
--   pactl set-sink-volume @DEFAULT_SINK@ +5%
--   pactl set-sink-volume @DEFAULT_SINK@ -5%
--   pactl set-sink-mute @DEFAULT_SINK@ toggle
--   pactl set-source-mute @DEFAULT_SOURCE@ toggle

-- Backlight brightness: step 5%.
sweets.bind("XF86MonBrightnessUp", "spawn", { "brightnessctl", "set", "5%+" })
sweets.bind("XF86MonBrightnessDown", "spawn", { "brightnessctl", "set", "5%-" })

-- Media playback control.
sweets.bind("XF86AudioPlay", "spawn", { "playerctl", "play-pause" })
sweets.bind("XF86AudioPause", "spawn", { "playerctl", "play-pause" })
sweets.bind("XF86AudioNext", "spawn", { "playerctl", "next" })
sweets.bind("XF86AudioPrev", "spawn", { "playerctl", "previous" })
sweets.bind("XF86AudioStop", "spawn", { "playerctl", "stop" })

Media keys

The packaged config binds the XF86 multimedia keys to helper commands via spawn. They have no modifier, so they work straight from a laptop's keys. Each needs the matching tool installed.

BindingActionTool
XF86AudioRaiseVolumeraise volume 5% (capped at 100%)wpctl
XF86AudioLowerVolumelower volume 5%wpctl
XF86AudioMutetoggle output mutewpctl
XF86AudioMicMutetoggle microphone mutewpctl
XF86MonBrightnessUpraise backlight 5%brightnessctl
XF86MonBrightnessDownlower backlight 5%brightnessctl
XF86AudioPlay / XF86AudioPauseplay / pauseplayerctl
XF86AudioNext / XF86AudioPrevnext / previous trackplayerctl
XF86AudioStopstop playbackplayerctl

The volume binds use wpctl (PipeWire/WirePlumber). On PulseAudio, rebind them to the pactl equivalents shown in the packaged sweets.lua.

These examples require wpctl and brightnessctl; Sweets does not install the helper programs.

Validate and reload

Check the complete installed-plus-user binding set without starting Sweets:

sweets --check-config

Saving an installed, user, or included configuration file triggers an automatic reload. You can also retry manually:

sweets msg reload

A valid reload atomically replaces the complete binding matcher and cancels any pending multi-step sequence. An invalid reload leaves the active bindings unchanged and reports the error.

On this page