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
XF86AudioMuteThe canonical modifier names are:
| Name | Meaning |
|---|---|
MOD | The modifier selected by sweets.general({ mod_key = ... }) |
Ctrl | Control |
Shift | Shift |
Alt | Alt |
Super | Logo, 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
| Action | Argument | Result |
|---|---|---|
spawn | Direct argv table | Launch a program without shell interpretation |
close | None | Ask the focused toplevel to close |
quit | None | Stop Sweets cleanly |
reload | None | Reload and transactionally apply the configuration |
dismiss_error | None | Hide the current config-error bar without clearing its diagnostic |
toggle_shortcuts_inhibit | None | Toggle shortcut inhibition for the focused requesting surface |
toggle_floating | None | Toggle the focused window between tiled and floating |
toggle_fullscreen | None | Toggle the focused window between normal and output fullscreen |
toggle_maximize | None | Toggle the focused window between normal and maximized |
minimize | None | Hide the focused window without removing it from its workspace |
restore_minimized | None | Restore and focus the most recently minimized window on its original workspace |
special_toggle | None | Show or hide the special workspace on the selected output |
special_move | None | Move the focused window into the special workspace without revealing it |
Focus, movement, and workspaces
| Action | Argument | Result |
|---|---|---|
focus_next, focus_prev | None | Cycle focus in the visible workspace |
focus_left, focus_right, focus_up, focus_down | None | Focus spatially in the requested direction |
focus_output_left, focus_output_right, focus_output_up, focus_output_down | None | Select an adjacent output |
move_left, move_right, move_up, move_down | None | Reorder or move the focused window in that direction |
move_output_left, move_output_right, move_output_up, move_output_down | None | Move the focused window to an adjacent output |
workspace | Integer 1 through 10 | Switch to a numbered workspace |
workspace_move | Integer 1 through 10 | Move 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
| Action | Argument | Result |
|---|---|---|
layout | Layout name | Select tile, monocle, centered, grid, columns, deck, or dwindle |
cycle_layout | None | Move forward through the configured layout cycle |
cycle_layout_reverse | None | Move backward through the configured layout cycle |
reset_layout | None | Restore the visible workspace's active layout defaults |
reset_all_layouts | None | Restore active layout defaults on every enabled workspace |
cycle_master_position | None | Rotate the visible workspace's master position forward |
cycle_master_position_reverse | None | Rotate the master position backward |
resize_grow, resize_shrink | None | Grow or shrink the focused tiled window horizontally |
resize_grow_vertical, resize_shrink_vertical | None | Grow 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:
| Binding | Action |
|---|---|
MOD+Return | Spawn foot |
MOD+Q | Close the focused window |
MOD+Shift+Q | Quit Sweets |
MOD+Shift+R | Reload the configuration |
MOD+Shift+E | Dismiss the current config-error bar |
MOD+Escape | Toggle shortcut inhibition |
MOD+Space | Toggle floating |
MOD+F | Toggle fullscreen |
MOD+J, MOD+K | Focus next or previous |
MOD+Shift+H/J/K/L | Move left, down, up, or right |
MOD+equal, MOD+minus | Grow or shrink horizontally |
MOD+Shift+equal, MOD+Shift+minus | Grow or shrink vertically |
The installed Lua configuration adds this desktop workflow:
| Binding | Action |
|---|---|
MOD+Shift+F | Toggle maximize |
MOD+M, MOD+Shift+M | Minimize or restore the most recently minimized window |
MOD+H/L, MOD+Arrow keys | Focus spatially |
MOD+Shift+Arrow keys | Move in a direction |
MOD+Ctrl+H/J/K/L, MOD+Ctrl+Arrow keys | Focus an adjacent output |
MOD+Ctrl+Shift+H/J/K/L, MOD+Ctrl+Shift+Arrow keys | Move to an adjacent output |
MOD+1..9/0 | Switch to workspaces 1 through 10 |
MOD+Shift+1..9/0 | Move the focused window to workspaces 1 through 10 |
MOD+S, MOD+Shift+S | Toggle the special workspace or move a window into it |
MOD+KP_Add, MOD+KP_Subtract | Grow or shrink horizontally |
MOD+Shift+KP_Add, MOD+Shift+KP_Subtract | Grow or shrink vertically |
MOD+I, MOD+Shift+I | Cycle the master position forward or backward |
MOD+W, MOD+Shift+W | Cycle layouts forward or backward |
MOD+U, MOD+Shift+U | Reset 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.
| Binding | Action | Tool |
|---|---|---|
XF86AudioRaiseVolume | raise volume 5% (capped at 100%) | wpctl |
XF86AudioLowerVolume | lower volume 5% | wpctl |
XF86AudioMute | toggle output mute | wpctl |
XF86AudioMicMute | toggle microphone mute | wpctl |
XF86MonBrightnessUp | raise backlight 5% | brightnessctl |
XF86MonBrightnessDown | lower backlight 5% | brightnessctl |
XF86AudioPlay / XF86AudioPause | play / pause | playerctl |
XF86AudioNext / XF86AudioPrev | next / previous track | playerctl |
XF86AudioStop | stop playback | playerctl |
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-configSaving an installed, user, or included configuration file triggers an automatic reload. You can also retry manually:
sweets msg reloadA 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.