Sweets
Configuration

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
ModifierMeaning
MODWhatever mod_key is set to
CtrlControl
ShiftShift
AltAlt
SuperLogo / 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

ActionArgumentResult
spawnargv tableLaunch a program
closeAsk the focused window to close
force_closeEnd the focused window's application
quitStop Sweets
reloadReload the configuration
dismiss_errorHide the config-error bar
show_bindingsList the active bindings
modemode nameSwitch to a binding mode, or "default" to leave one
toggle_shortcuts_inhibitToggle shortcut inhibition for the focused window
toggle_inputinput selectorTurn a pointer device off and on
keyboard_layoutnext, prev, or 14Switch the active keyboard layout
toggle_floatingTiled or floating
toggle_all_floatingFloat every eligible window on the selected workspace, or tile them when all are floating
toggle_above, toggle_belowTemporarily put the focused window above or below ordinary windows; reload restores its opening rule
toggle_pinKeep the focused floating window on its monitor across every workspace
monitor_poweron, off, or toggleTemporarily change power on the selected monitor
toggle_fullscreenFullscreen
toggle_windowed_fullscreenFullscreen without leaving the slot
toggle_maximizeMaximized
minimizeHide the focused window
restore_minimizedRestore the most recently minimized window
center_windowRecenter the focused floating window, or the focused column on a scrolling workspace
move_floating_left/right/up/downMove the focused floating window one step
resize_floating_grow, resize_floating_shrinkResize a floating window horizontally
resize_floating_grow_vertical, resize_floating_shrink_verticalResize a floating window vertically
special_togglename (optional)Show or hide a special workspace; its options may launch the first window
special_movename (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

ActionArgumentResult
focus_next, focus_prevCycle focus in the workspace
focus_lastReturn to the previously focused window in the workspace
focus_urgentFocus the window that has been urgent the longest
recent_next, recent_previousworkspace, monitor, or all (optional)Walk the held-modifier recent-windows switcher in that scope; the default is all
focus_left/right/up/downFocus in a direction
focus_or_workspace_left/right/up/downFocus in a direction, then fall through to the adjacent workspace
focus_output_left/right/up/downSelect an adjacent monitor
focus_output_next, focus_output_prevSelect the next or previous monitor, wrapping
move_left/right/up/downMove the focused window in a direction
move_output_left/right/up/downMove it to an adjacent monitor
move_output_next, move_output_prevMove it to the next or previous monitor, wrapping
workspace110Switch workspace
workspace_move110Move the focused window to a workspace
workspace_previousReturn this monitor to the workspace it showed before
workspace_relative_next, workspace_relative_prevSwitch to the next or previous numbered workspace
workspace_move_relative_next, workspace_move_relative_prevMove 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

ActionArgumentResult
layoutlayout nameSelect tile, monocle, centered, grid, columns, deck, dwindle, or scrolling
cycle_layout, cycle_layout_reverseStep through the layout cycle
reset_layout, reset_all_layoutsRestore layout proportions and runtime gap overrides
gaps_grow, gaps_shrinkoptional inner or outerAdjust the selected workspace by layout.gaps_step; defaults to inner
toggle_gapsHide or restore the selected workspace's gaps
cycle_master_position, cycle_master_position_reverseRotate the master position, or the scrolling strip's direction
swap_with_masterSwap the focused tiled window with the master slot
rotate_next, rotate_prevRoll the tiled order so the next or previous window becomes master
focus_masterFocus the master slot, or its stack from the master
add_master, remove_masterPut one more or one fewer window in the master area
consume_or_expel_left, consume_or_expel_rightScrolling: stack into the neighbouring column, or leave a column
move_column_left, move_column_rightScrolling: move the focused column
focus_column_first, focus_column_lastScrolling: focus the first or last column
cycle_column_width, cycle_column_width_reverseScrolling: step the focused column through the preset widths
toggle_full_widthScrolling: fill the screen with the focused column, or undo it
expand_columnScrolling: widen the column into the free space on screen
resize_grow, resize_shrinkResize horizontally
resize_grow_vertical, resize_shrink_verticalResize 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:

OptionValuesDefaultEffect
repeatingtrue, falsefalseRuns the binding again while its key is held.
on"press", "release""press"Runs a single-key binding when pressed or tapped and released.
cooldown05000 ms0Smallest gap between two runs.
descriptiontext, up to 128 bytesnoneThe label shown in the bindings list.
spawnargv tablenoneShow 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:

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

The installed configuration adds the full workflow:

BindingAction
MOD+Shift+FToggle maximize
MOD+M, MOD+Shift+MMinimize, restore
MOD+H/L, MOD+ArrowsFocus in a direction
MOD+Shift+ArrowsMove in a direction
MOD+Ctrl+H/J/K/L, MOD+Ctrl+ArrowsFocus an adjacent monitor
MOD+Ctrl+Shift+H/J/K/L, MOD+Ctrl+Shift+ArrowsMove to an adjacent monitor
MOD+1..9/0Switch to workspaces 1–10
MOD+Shift+1..9/0Move the window to workspaces 1–10
MOD+S, MOD+Shift+SToggle the special workspace, move into it
MOD+KP_Add, MOD+KP_SubtractResize horizontally
MOD+Shift+KP_Add, MOD+Shift+KP_SubtractResize vertically
MOD+I, MOD+Shift+ICycle the master position
MOD+W, MOD+Shift+WCycle layouts
MOD+U, MOD+Shift+UReset 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",
})
FieldDefaultEffect
columns214 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.

BindingActionTool
XF86AudioRaiseVolumeVolume +5%, capped at 100%wpctl
XF86AudioLowerVolumeVolume −5%wpctl
XF86AudioMuteToggle output mutewpctl
XF86AudioMicMuteToggle microphone mutewpctl
XF86MonBrightnessUp / DownBacklight ±5%brightnessctl
XF86AudioPlay / XF86AudioPausePlay / pauseplayerctl
XF86AudioNext / XF86AudioPrevNext / previous trackplayerctl
XF86AudioStopStop playbackplayerctl

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 reload

A valid reload replaces every binding at once and cancels a half-typed sequence. An invalid one changes nothing.

On this page