Key Bindings
Bind keys and key sequences to Sweets actions
sweets.bind(spec, action [, argument]) maps a key combination or a multi-step
sequence to an 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" })Only spawn, workspace, workspace_move, layout, and mode take a third
argument.
Mouse buttons and scroll directions use
sweets.bind_mouse and sweets.bind_scroll,
which take the same actions and options.
Key specifications
Zero or more modifiers and one XKB keysym, joined with +:
MOD+Shift+Return
Ctrl+Alt+Delete
XF86AudioMute| Modifier | Meaning |
|---|---|
MOD | Whatever mod_key is set to |
Ctrl | Control |
Shift | Shift |
Alt | Alt |
Super | Logo / Windows key |
Names are case-insensitive, and Control, Mod1, Logo, Mod4, and Win
also work. Do not combine MOD with the modifier it stands for —
MOD+Super+Q is invalid while mod_key = "SUPER".
Key names are XKB keysyms such as q, Return, Escape, space, equal,
KP_Add, or XF86AudioRaiseVolume. q and Q are the same key; add Shift
when you mean Shift.
Modifiers must match exactly, so an extra held modifier prevents a match. Caps Lock and Num Lock are ignored, and Latin-letter bindings stay put when you switch keyboard layouts.
Multi-step sequences
Separate steps with a space. Each step has its own 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 MOD+B, release, then press f alone — still holding
MOD does not match.
A sequence may have up to eight steps, and each step gives you one second to press the next. A key that matches no branch cancels the sequence and is swallowed; a timeout instead lets the next key through normally.
Bindings must be prefix-free, so these cannot coexist:
sweets.bind("MOD+B", "close")
sweets.bind("MOD+B f", "spawn", { "firefox" })Remove the shorter one first:
sweets.unbind("MOD+Q")
sweets.bind("MOD+Q Q", "close")Actions
Programs and window state
| Action | Argument | Result |
|---|---|---|
spawn | argv table | Launch a program |
close | — | Ask the focused window to close |
force_close | — | End the focused window's application |
quit | — | Stop Sweets |
reload | — | Reload the configuration |
dismiss_error | — | Hide the config-error bar |
show_bindings | — | List the active bindings |
mode | mode name | Switch to a binding mode, or "default" to leave one |
toggle_shortcuts_inhibit | — | Toggle shortcut inhibition for the focused window |
toggle_input | input selector | Turn a pointer device off and on |
keyboard_layout | next, prev, or 1–4 | Switch the active keyboard layout |
toggle_floating | — | Tiled or floating |
toggle_all_floating | — | Float every eligible window on the selected workspace, or tile them when all are floating |
toggle_above, toggle_below | — | Temporarily put the focused window above or below ordinary windows; reload restores its opening rule |
toggle_pin | — | Keep the focused floating window on its monitor across every workspace |
monitor_power | on, off, or toggle | Temporarily change power on the selected monitor |
toggle_fullscreen | — | Fullscreen |
toggle_windowed_fullscreen | — | Fullscreen without leaving the slot |
toggle_maximize | — | Maximized |
minimize | — | Hide the focused window |
restore_minimized | — | Restore the most recently minimized window |
center_window | — | Recenter the focused floating window, or the focused column on a scrolling workspace |
move_floating_left/right/up/down | — | Move the focused floating window one step |
resize_floating_grow, resize_floating_shrink | — | Resize a floating window horizontally |
resize_floating_grow_vertical, resize_floating_shrink_vertical | — | Resize a floating window vertically |
special_toggle | name (optional) | Show or hide a special workspace; its options may launch the first window |
special_move | name (optional) | Move the focused window into a special workspace |
toggle_all_floating leaves minimized, pinned, maximized, and fullscreen
windows alone. It is a batch toggle, not a saved snapshot of the old mixture:
when any eligible window is tiled, all eligible windows float; when all are
floating, the next press tiles all of them through their remembered slots.
toggle_above and toggle_below affect drawing and pointer hit testing
together. Their override is temporary; every valid configuration reload clears
it and restores the stacking class retained from the window's opening rules.
Focus, movement, and workspaces
| Action | Argument | Result |
|---|---|---|
focus_next, focus_prev | — | Cycle focus in the workspace |
focus_last | — | Return to the previously focused window in the workspace |
focus_urgent | — | Focus the window that has been urgent the longest |
recent_next, recent_previous | workspace, monitor, or all (optional) | Walk the held-modifier recent-windows switcher in that scope; the default is all |
focus_left/right/up/down | — | Focus in a direction |
focus_or_workspace_left/right/up/down | — | Focus in a direction, then fall through to the adjacent workspace |
focus_output_left/right/up/down | — | Select an adjacent monitor |
focus_output_next, focus_output_prev | — | Select the next or previous monitor, wrapping |
move_left/right/up/down | — | Move the focused window in a direction |
move_output_left/right/up/down | — | Move it to an adjacent monitor |
move_output_next, move_output_prev | — | Move it to the next or previous monitor, wrapping |
workspace | 1–10 | Switch workspace |
workspace_move | 1–10 | Move the focused window to a workspace |
workspace_previous | — | Return this monitor to the workspace it showed before |
workspace_relative_next, workspace_relative_prev | — | Switch to the next or previous numbered workspace |
workspace_move_relative_next, workspace_move_relative_prev | — | Move the focused window to the relative workspace |
Directional focus and movement look for a local target first; whether they
continue onto the next monitor is set by focus_cross_monitor and
move_cross_monitor. The *_output_* actions always cross.
focus_last is a toggle: it records the window you left, so pressing it again
returns. It skips minimized and closed windows. focus_prev is a different
action — it walks layout order, not recency.
recent_next and recent_previous are the held-modifier switcher. Their
optional scope is workspace, monitor, or all; omission preserves the
all-workspace, all-monitor behavior. See Recent Windows.
focus_urgent reaches any monitor. It switches workspace and restores a
minimized window if it has to, then obeys pointer.follow_focus.
The *_output_next/*_output_prev actions cycle monitors in connector-name
order and wrap at either end. They are the ones to bind on three or more
monitors, where a direction runs out at the edge.
workspace_previous uses the same per-monitor memory as
workspace_back_and_forth, but works whether or not that option is on: the
option redirects a repeated number key, this is a key of its own. It does
nothing on a monitor you have not switched away from yet.
Relative workspace actions use general.workspace_wrap and
general.workspace_skip_empty; relative moves also use
general.workspace_move_follow. They still pass the resolved number through
the ordinary workspace path, so a visible or pinned target selects its monitor.
The focus_or_workspace_* family preserves ordinary directional focus,
including monitor and off-axis fallbacks, and changes workspace only when that
search has no target.
Layout and resizing
| Action | Argument | Result |
|---|---|---|
layout | layout name | Select tile, monocle, centered, grid, columns, deck, dwindle, or scrolling |
cycle_layout, cycle_layout_reverse | — | Step through the layout cycle |
reset_layout, reset_all_layouts | — | Restore layout proportions and runtime gap overrides |
gaps_grow, gaps_shrink | optional inner or outer | Adjust the selected workspace by layout.gaps_step; defaults to inner |
toggle_gaps | — | Hide or restore the selected workspace's gaps |
cycle_master_position, cycle_master_position_reverse | — | Rotate the master position, or the scrolling strip's direction |
swap_with_master | — | Swap the focused tiled window with the master slot |
rotate_next, rotate_prev | — | Roll the tiled order so the next or previous window becomes master |
focus_master | — | Focus the master slot, or its stack from the master |
add_master, remove_master | — | Put one more or one fewer window in the master area |
consume_or_expel_left, consume_or_expel_right | — | Scrolling: stack into the neighbouring column, or leave a column |
move_column_left, move_column_right | — | Scrolling: move the focused column |
focus_column_first, focus_column_last | — | Scrolling: focus the first or last column |
cycle_column_width, cycle_column_width_reverse | — | Scrolling: step the focused column through the preset widths |
toggle_full_width | — | Scrolling: fill the screen with the focused column, or undo it |
expand_column | — | Scrolling: widen the column into the free space on screen |
resize_grow, resize_shrink | — | Resize horizontally |
resize_grow_vertical, resize_shrink_vertical | — | Resize vertically |
swap_with_master promotes the focused tiled window to the master slot. A
window that is already master swaps with the stack member you focused most
recently, so the same key sends it back. With several masters, a stack window
swaps with the master you focused most recently. Floating windows are unaffected.
rotate_next and rotate_prev roll the whole stack by one instead of swapping
two windows, and leave focus where it is. A minimized window keeps its place in
the order. On a scrolling workspace they roll whole columns along the strip.
focus_master jumps to the master slot from anywhere in the stack. From the
master it focuses the stack member you used most recently, so the same key
sends you back. On a scrolling workspace use focus_column_first instead.
center_window moves a floating window and keeps its size. On a scrolling
workspace it centers the focused column on screen instead. On a tiled window of
any other layout it does nothing.
The move_floating_* and resize_floating_* actions travel one
layout.floating_step. They act only on floating windows; resize_grow and
its variants act only on tiled ones.
See Layout for how each layout resizes.
Spawning programs
spawn takes an argument array — the executable, then one string per argument:
sweets.bind("MOD+E", "spawn", { "foot", "nvim", "/tmp/notes.txt" })
sweets.bind("MOD+D", "spawn", { "fuzzel" })There is no shell, so pipes, redirection, variables, wildcards, quoting, and
~ are not interpreted. Use up to 32 strings, 256 bytes each and 4096 bytes
total.
Programs inherit the session environment and WAYLAND_DISPLAY. Their standard
error goes to the session log, so a failed launch is visible there and never
affects the compositor. Add variables with
sweets.env.
Running a binding while locked
The lock screen ignores your bindings. Add allow_when_locked to let one
through:
sweets.bind("XF86AudioMute", "spawn", {
"wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle",
}, { allow_when_locked = true })The options table goes after the action's argument, or in its place when the action takes none. It defaults to off.
Only a single-key spawn may use it. Any other action, or a multi-step
sequence, rejects the configuration. Keys that have not opted in still reach
the lock screen, so password entry is unaffected.
A program launched this way runs with your session's full access while the screen is locked. Bind only what you would be comfortable letting someone at your keyboard run.
Binding options
The options table also takes these keys:
| Option | Values | Default | Effect |
|---|---|---|---|
repeating | true, false | false | Runs the binding again while its key is held. |
on | "press", "release" | "press" | Runs a single-key binding when pressed or tapped and released. |
cooldown | 0–5000 ms | 0 | Smallest gap between two runs. |
description | text, up to 128 bytes | none | The label shown in the bindings list. |
spawn | argv table | none | Show an empty special_toggle workspace and launch its first window. |
sweets.bind("XF86AudioRaiseVolume", "spawn", {
"wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%+",
}, { repeating = true })
sweets.bind("MOD+Ctrl+r", "cycle_column_width", { cooldown = 120 })
sweets.bind("MOD+d", "spawn", { "fuzzel" }, { description = "Launcher" })
sweets.bind("Super_L", "spawn", { "fuzzel" }, { on = "release" })A held binding repeats at your
keyboard repeat_delay and repeat_rate.
It stops when you release the key or press another one. repeat_rate = 0
turns it off. Only a single key can repeat; a key sequence rejects the option.
Write repeating, not repeat. repeat is a reserved word in Lua, so
{ repeat = true } is a syntax error.
A press during the cooldown does nothing and does not reach the application. The shipped configuration repeats the volume and brightness keys.
on = "release" fires only if you release that key without pressing another
key, clicking, or scrolling in between. Both the press and release still reach
the focused app. It accepts one key and cannot be combined with repeating.
Replacing and removing bindings
Binding the same key again in your configuration replaces it:
sweets.bind("MOD+Return", "spawn", { "alacritty" })Remove one with sweets.unbind:
sweets.unbind("MOD+Space")sweets.unbind also removes a mouse or scroll binding — it works out which
kind you named:
sweets.unbind("MOD+BTN_LEFT")An unbind removes that exact sequence only, not longer ones sharing its steps.
Unbinding something that is not bound is an error, as is binding the same key
twice in one layer. Replacing a binding also replaces all of its options, so
rebinding a key without allow_when_locked turns it off.
Up to 256 bindings are kept. Any duplicate, ambiguous prefix, unknown action, or wrong argument rejects the whole configuration and keeps the previous bindings.
Default bindings
Sweets always starts from a small built-in map, which works even with no configuration at all:
| 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 config-error bar |
MOD+Escape | Toggle shortcut inhibition |
MOD+Shift+slash | List the active bindings |
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, right |
MOD+equal, MOD+minus | Resize horizontally |
MOD+Shift+equal, MOD+Shift+minus | Resize vertically |
The installed configuration adds the full workflow:
| Binding | Action |
|---|---|
MOD+Shift+F | Toggle maximize |
MOD+M, MOD+Shift+M | Minimize, restore |
MOD+H/L, MOD+Arrows | Focus in a direction |
MOD+Shift+Arrows | Move in a direction |
MOD+Ctrl+H/J/K/L, MOD+Ctrl+Arrows | Focus an adjacent monitor |
MOD+Ctrl+Shift+H/J/K/L, MOD+Ctrl+Shift+Arrows | Move to an adjacent monitor |
MOD+1..9/0 | Switch to workspaces 1–10 |
MOD+Shift+1..9/0 | Move the window to workspaces 1–10 |
MOD+S, MOD+Shift+S | Toggle the special workspace, move into it |
MOD+KP_Add, MOD+KP_Subtract | Resize horizontally |
MOD+Shift+KP_Add, MOD+Shift+KP_Subtract | Resize vertically |
MOD+I, MOD+Shift+I | Cycle the master position |
MOD+W, MOD+Shift+W | Cycle layouts |
MOD+U, MOD+Shift+U | Reset this layout, or all of them |
Bindings your configuration does not mention stay active. Changing mod_key
moves every MOD binding; ones written with an explicit Super do not move.
Listing your bindings
MOD+Shift+slash opens a list of every binding in the active set on the
focused monitor. The next key press, click, or scroll closes it, so pressing the
same binding again closes it too. The press still reaches its binding or window.
Bindings with a description come first and show it. The rest show their
action. Inside a binding mode, the list
shows that mode's bindings. MOD+Shift+slash is built in, so it works in every
mode. If you open the list with another key, bind that key inside each mode as
well.
sweets.bind("MOD+Shift+slash", "show_bindings")
sweets.bindings_overlay({
columns = 2,
background = "#1C1F26F2",
text = "#E6E9F0",
accent = "#7AA2F7",
font = "Monospace 10",
})| Field | Default | Effect |
|---|---|---|
columns | 2 | 1–4 columns |
background | "#1C1F26F2" | Panel color, #RRGGBB or #RRGGBBAA |
text | "#E6E9F0" | Label color |
accent | "#7AA2F7" | Title and key color |
font | "Monospace 10" | Pango font description |
The list shows as many bindings as fit on one screen. When some are left out,
its last line says how many. sweets msg binds prints every binding in a
terminal.
Media keys
The installed configuration binds the XF86 keys. They carry no modifier, so
they work straight from a laptop keyboard, but each needs its tool installed.
| Binding | Action | Tool |
|---|---|---|
XF86AudioRaiseVolume | Volume +5%, capped at 100% | wpctl |
XF86AudioLowerVolume | Volume −5% | wpctl |
XF86AudioMute | Toggle output mute | wpctl |
XF86AudioMicMute | Toggle microphone mute | wpctl |
XF86MonBrightnessUp / Down | Backlight ±5% | brightnessctl |
XF86AudioPlay / XF86AudioPause | Play / pause | playerctl |
XF86AudioNext / XF86AudioPrev | Next / previous track | playerctl |
XF86AudioStop | Stop playback | playerctl |
XF86TouchpadToggle is bound too, to
turn the touchpad off and on.
All the spawning keys set
allow_when_locked, so they keep working on
the lock screen. Rebind any key without that option to turn that off.
The installed file also carries commented recipes for the keyboard backlight, the lock key, the radio key and the power key, which not every laptop has.
The power key is the one that needs more than a binding: logind powers the
machine off on its own. Set
general.power_key = "compositor" to
take the key away from it.
Some laptops report brightness keys from two devices at once, so one press
runs the binding twice and the backlight jumps by double the step. Run
libinput debug-events and press the key to see whether yours does.
Volume uses wpctl from PipeWire and WirePlumber. On PulseAudio, rebind to
the pactl equivalents shown in the installed sweets.lua. Sweets does not
install these tools.
Windowed fullscreen
toggle_windowed_fullscreen tells the focused window it is fullscreen without
moving it. The window hides its own chrome and keeps its slot, its border and
its gaps.
sweets.bind("MOD+Ctrl+Shift+F", "toggle_windowed_fullscreen")Use it to share a browser presentation, or a video, while keeping your notes on screen beside it.
A window that is already fullscreen returns to its slot and stays chromeless.
Pressing toggle_fullscreen, or the window asking for fullscreen itself, takes
over and clears the windowed state.
Not every application reacts to the fullscreen state, so on some windows this appears to do nothing. XWayland windows are refused outright. Either way the compositor logs what it decided at debug level.
Force close
force_close ends an application that will not close. It sends SIGTERM, then
SIGKILL three seconds later if the application is still connected.
sweets.bind("MOD+Ctrl+Q", "force_close")It ends the whole application, so its other windows close too. XWayland windows are refused, because they all share one Xwayland process.
A window that ignores close for five seconds is marked unresponsive. Its
border takes border.unresponsive,
and sweets msg windows reports unresponsive: true. The mark clears as soon
as the application responds again.
Shortcut inhibition
Games, virtual machines, and remote-desktop clients can ask to receive shortcuts themselves. While such a window has focus, your bindings go to it instead.
toggle_shortcuts_inhibit stays with the compositor, and the built-in
MOD+Escape is the escape hatch. If you remove it, bind that action to
something else.
A sequence cannot start while inhibition is active unless one of its branches
reaches toggle_shortcuts_inhibit. Virtual-terminal switching always stays
with the compositor, and no binding runs while the session is locked.
Reload
sweets --check-config
sweets msg reloadA valid reload replaces every binding at once and cancels a half-typed sequence. An invalid one changes nothing.