IPC
Control Sweets at runtime over its Unix socket with the sweetctl CLI.
Sweets exposes a small control socket for querying state and dispatching
commands. The bundled sweetctl CLI is the easiest way to use it.
Socket
The compositor listens on a Unix socket:
$XDG_RUNTIME_DIR/sweets-<WAYLAND_DISPLAY>.sockIt exports SWEETS_SOCKET to spawned processes. sweetctl finds the socket via
$SWEETS_SOCKET, falling back to deriving the path from $WAYLAND_DISPLAY.
Trust model
IPC is an owner-only control interface, not a multi-user authorization system. Any process running as the compositor's effective user can issue every command, including spawning programs, closing windows, and stopping Sweets.
Before publishing IPC, Sweets requires XDG_RUNTIME_DIR to be an absolute, real
directory that is owned by its effective user, writable and searchable by that
owner, and inaccessible to group and other. The session launcher enforces the
same contract. If validation fails, no IPC socket is created.
The socket is set to mode 0600, independent of the process umask. Every
accepted connection must report the same effective UID through kernel peer
credentials — SO_PEERCRED on Linux, getpeereid on supported BSDs. A
credential failure, mismatched UID, or unsupported platform is denied.
An inherited SWEETS_SOCKET is cleared and replaced only after the secure
listener is ready.
sweetctl applies the runtime-directory checks when deriving a socket from
WAYLAND_DISPLAY. Setting SWEETS_SOCKET explicitly is an intentional request
to connect to that exact path.
Protocol
IPC is a bounded, line-framed stream protocol. A client sends one command
terminated by a newline. The command may be at most 4096 bytes and may not
contain NUL bytes. sweetctl adds the newline automatically.
Sweets waits for the complete frame before dispatching. EOF before the newline, an oversized frame, and malformed subscriptions are rejected without executing anything.
For a normal request the server replies and closes the connection, so EOF is the
response boundary. Responses begin with OK or ERR <message>, with query data
on following lines. A one-shot response is limited to 4 MiB. sweetctl prints
the response and exits non-zero on ERR.
subscribe is the exception. After the initial OK, the connection stays open
and streams event lines until the client disconnects. A subscriber
more than 1 MiB behind is disconnected.
Limits: at most 64 simultaneous connections. Incomplete requests and one-shot clients that do not consume their response are closed after 5 seconds. Established subscriptions are exempt from the idle timeout.
The server documents itself, so the running compositor is always the authoritative reference:
sweetctl help # every command, with a one-line summary
sweetctl help output # just the forms of one command
sweetctl -j help # the command list as JSONcommands is an alias for help. An unknown name replies ERR.
Commands
| Command | Reply |
|---|---|
help / commands | every command with a summary; help <command> details one |
version | compositor version |
config | active general/layout settings, one key value per line |
config-sources | files the running config was built from |
config-error | last config failure, if any |
outputs | one line per output: name, mode, position, scale, active workspace |
output <name> | detailed geometry for one output |
output <name> scale <factor> | set the output scale (0.1–16) |
output <name> mode <w> <h> [hz] | set the output mode; refresh 0.001–1000 Hz |
output <name> position <x> <y> | set the layout position |
output <name> transform <t> | set the transform |
output <name> power <on|off> | power the output on or off |
output all power <on|off> | power all connected outputs |
output <name> enabled <true|false> | raw alias for output power state |
output <name> adaptive_sync <on|off> | enable or disable VRR when supported |
workspaces | active and non-empty workspaces with counts, layout, master position |
views | one line per window (see views) |
inputs | one line per input device |
input <name> | detailed info for one matching device |
input <name> <setting> <value> | change one libinput setting temporarily |
workspace <1-10> | switch the current output to that workspace |
focus next / focus prev | focus the next / previous window |
close | close the focused window |
minimize / unminimize | minimize the focused window / restore the last one |
reload | manually reload the Lua config (fail-safe) |
action <name> [arg] | run any window-manager action |
subscribe <event>... | stream events |
quit | exit the compositor |
Numeric arguments must be complete finite values. nan, infinity, trailing
characters, overflow, and underflow are rejected rather than approximated.
Examples
sweetctl version
sweetctl config
sweetctl outputs
sweetctl output DP-1 scale 1.5
sweetctl output DP-1 mode 2560 1440 144
sweetctl output DP-1 transform 90
sweetctl output all power on
sweetctl inputs
sweetctl input ELAN accel_speed -0.25
sweetctl views
sweetctl workspace 2
sweetctl reload
sweetctl action toggle_floating
sweetctl action layout grid
sweetctl action spawn foot
sweetctl subscribe focus view workspace layoutSetting scale, mode, position, transform, power, enabled, or
adaptive_sync stages the in-memory rule, tests the output, and publishes it
only after the backend accepts. A rejected request replies ERR and changes
nothing. Changes are runtime-only — reload reverts to the file.
output all power ... affects connected outputs only, applying each
independently. If only some accept, it replies ERR, reports per-connector
status, and keeps the successful changes. Powering an output off keeps its
windows assigned; they reappear when it powers on.
Sweets also reloads sweets.lua automatically when it or any
included file changes. The IPC command
is a fallback for scripts.
JSON output
Add -j (or --json) before the command to emit a single JSON value instead
of line-oriented text:
sweetctl -j outputs
sweetctl -j views | jq '.[] | select(.focused) | .title'
sweetctl -j workspaces | jq '.workspaces[] | select(.active) | .index'
sweetctl -j config | jq -r .mod_keyThe flag applies to every read query: version, outputs, output <name>,
workspaces, views, config, config-sources, config-error, inputs, and
input <name>.
Arrays (outputs, views, inputs) come back as JSON arrays; everything else
is an object. Strings are escaped, so titles and device names are always valid
JSON.
Behavior in -j mode:
- Success prints only the JSON to stdout, exit
0. TheOKline is stripped, so it pipes straight intojq. - Errors print to stderr and exit non-zero, so a failed query never feeds malformed data into a parser.
- Mutating commands accept
-jtoo. On success they print nothing and exit0; on failure the message goes to stderr. Use the exit code.
-j is for one-shot queries. subscribe still streams plain event <name>
lines — re-query in JSON when an event fires.
Example shapes:
// sweetctl -j outputs
[{"name":"DP-1","width":2560,"height":1440,"refresh":144.000,"x":0,"y":0,"scale":1.00,"workspace":1}]
// sweetctl -j views
[{"output":"DP-1","workspace":1,"app_id":"foot","title":"~","floating":false,
"minimized":false,"fullscreen":false,"maximized":false,"focused":true,"icon":"foot"}]
// sweetctl -j workspaces
{"workspaces":[{"output":"DP-1","index":1,"windows":2,"layout":"tile",
"master_position":"left","active":true}],"special":null}
// sweetctl -j config-error (when the config is valid)
{"error":false}Nullable fields are explicit. app_id, icon, and a missing title report
null, and special is null when the scratchpad is empty.
Query output
views
One line per window: output, workspace, app-id, any of
floating/minimized/fullscreen/maximized/focused, an optional
icon="<name>" when an xdg-toplevel-icon override is set, and the quoted title.
output <name>
Reports the live geometry of a single output:
OK
output DP-1
enabled true
position 0,0
size 2560x1440
refresh 144.000
scale 1.00
transform normal
usable 0,32 2560x1408
active_workspace 1
master 50.0config
Reports the live values after any reloads:
OK
mod_key SUPER
background #12121C
gap_inner 8
gap_outer 8
smart_gaps true
master 50
new_window master
border_width 2
border_focused #C27AFF
border_unfocused #333338
pointer_follow_focus true
pointer_follow_focus_restore true
pointer_follow_workspace true
pointer_follow_new_window true
pointer_hide_when_following falseconfig-sources
Lists every Lua file the running config was built from, in load order. This is exactly the set the reload watcher tracks, so it confirms whether an include took effect:
OK
count 4
source /usr/share/sweets/sweets.lua
source /home/ns/.config/sweets/sweets.lua
source /home/ns/.config/sweets/parts/binds.lua
source /home/ns/.config/sweets/parts/rules.luaPaths are absolute and canonical. count 0 means no config file was found and
the compositor is on built-in defaults.
config-error
Reports the most recent config failure, so a status bar can show it without scraping the log. This is the same error the on-screen bar displays.
When the config is valid:
OK
error falseAfter a failure, error true is followed by the failing file, the 1-based line
(omitted when unknown), and the message:
OK
error true
file /home/ns/.config/sweets/sweets.lua
line 9
message /home/ns/.config/sweets/sweets.lua:9: '}' expected (to close '{' at line 6) near '='The error clears as soon as a later edit reloads cleanly.
Status-bar recipe: a config-health module
Pairing config-error with config-sources gives a bar an "is my config OK"
indicator. This Waybar custom module
shows a warning on failure and lists the loaded files in the tooltip:
#!/bin/sh
# config-error -> health; config-sources -> tooltip file list
err=$(sweetctl config-error)
if printf '%s' "$err" | grep -q '^error true'; then
msg=$(printf '%s' "$err" | sed -n 's/^message //p')
jq -cn --arg t "config ⚠" --arg tip "$msg" \
'{text:$t, class:"error", tooltip:$tip}'
else
files=$(sweetctl config-sources | sed -n 's/^source //p')
count=$(printf '%s\n' "$files" | grep -c .)
tip=$(printf 'config OK — %s files\n%s' "$count" "$files")
jq -cn --arg t "config ✔" --arg tip "$tip" \
'{text:$t, class:"ok", tooltip:$tip}'
fi"custom/config": {
"exec": "~/.config/sweets/scripts/config-health.sh",
"return-type": "json",
"interval": 5,
"on-click": "sweetctl reload"
}The file set only changes on a reload, so a short interval is plenty. There is
no event to subscribe to for config changes.
inputs
Reports connected input devices. Physical libinput pointers include the same
touchpad / mouse type used by sweets.input(selector, { ... }). Touch,
tablet, tablet-pad, and switch devices are listed too:
OK
input keyboard libinput=true name="AT Translated Set 2 keyboard"
input pointer type=touchpad libinput=true name="ELAN1200:00 04F3:30BA Touchpad"
input pointer type=mouse libinput=true name="Logitech USB Optical Mouse"
input touch libinput=true name="ELAN Touchscreen"
input tablet libinput=true name="Wacom Pen and multitouch sensor Pen stylus"
input tablet_pad libinput=true name="Wacom ExpressKey Remote Pad"
input switch libinput=true name="Lid Switch"Use a unique part of the quoted name with sweets.input("name:TEXT", { ... }),
or with input <name> to inspect one device:
OK
kind pointer
type touchpad
name "ELAN1200:00 04F3:30BA Touchpad"
libinput true
sysname event4
id bus=0x0018 vendor=0x04f3 product=0x30ba
tap_fingers 3
tap supported=true current=true default=false
tap_drag supported=true current=true default=true
tap_drag_lock supported=true current=false default=false
natural_scroll supported=true current=true default=false
left_handed supported=true current=false default=false
middle_emulation supported=false
disable_while_typing supported=true current=true default=true
accel supported=true current=-0.250 default=0.000
accel_profiles flat adaptive
accel_profile current=adaptive default=adaptiveIf the substring matches more than one device, Sweets replies ERR and asks
for a more specific substring.
Live input tuning
Use the same unique substring to change one supported setting immediately:
sweetctl input ELAN tap true
sweetctl input ELAN natural_scroll true
sweetctl input ELAN accel_speed -0.25
sweetctl input "Logitech USB Optical Mouse" accel_profile flatBooleans accept true/false or on/off. Supported settings are tap,
tap_drag, tap_drag_lock, natural_scroll, left_handed,
middle_emulation, disable_while_typing, accel_speed (-1.0–1.0), and
accel_profile (adaptive or flat). Sweets rejects settings a device cannot
support.
Live changes are temporary. A reload, reconnect, or restart resets the device
and reapplies sweets.input(...). Find the comfortable value here, then put
it in Lua.
Actions
action <name> [arg] runs a window-manager command by name. The names and
arguments match sweets.bind, so anything
reachable from a keybind is reachable from a script without binding a key.
sweetctl action focus_right
sweetctl action toggle_fullscreen
sweetctl action workspace 3
sweetctl action cycle_layout prev
sweetctl action master_position right
sweetctl action spawn foot -e htopThe reply is OK, or ERR <message> when the name is unknown or an argument is
invalid. Compositor state is left untouched on error.
| Action | Argument | Effect |
|---|---|---|
spawn | command line | launch a detached process (spaces allowed) |
close | — | close the focused window |
focus_next / focus_prev | — | cycle focus in stacking order |
focus_left / focus_right / focus_up / focus_down | — | focus by direction |
move_left / move_right / move_up / move_down | — | move by direction |
toggle_floating | — | float or tile the focused window |
toggle_fullscreen / toggle_maximize | — | toggle window display mode |
minimize / unminimize | — | minimize / restore onto the current workspace |
workspace | 1–10 | switch the current output to that workspace |
workspace_move | 1–10 | move the focused window to that workspace |
special_toggle / special_move | — | show or hide the scratchpad / send a window to it |
resize_grow / resize_shrink | — | resize the master split horizontally |
resize_grow_vertical / resize_shrink_vertical | — | resize vertically |
layout | layout name | set the active workspace's layout |
cycle_layout | next (default) / prev | step through the layout cycle |
master_position | left / right / top / bottom | place the master area |
cycle_master_position | — | rotate the master area |
grid_balance | — | even out the grid layout |
reload / quit / dismiss_error | — | reload / exit / dismiss the error bar |
Tile-only actions no-op when the active workspace is not that layout, matching keybind behavior.
action targets the focused window and current output, exactly like a keybind
press. There is no window selector — focus a window first to act on it.
Events
subscribe turns a connection into a live event stream, so bars redraw on
change instead of polling:
sweetctl subscribe focus view workspace layout
# or, for everything:
sweetctl subscribe allThe server replies OK once, then writes one line per event while the
connection stays open.
| Event | Line |
|---|---|
focus | event focus output=<name> ws=<N> app_id="<id>" title="<title>" (or event focus none) |
view | event view — the window set changed; re-query views |
workspace | event workspace output=<name> index=<N> |
layout | event layout output=<name> index=<N> layout=<name> |
title | event title output=<name> ws=<N> app_id="<id>" title="<title>" |
app_id and title are double-quoted and escaped like the
--json strings, so one event is always one line. app_id is
"" when the client set none. The output/ws pair is present whenever the
window is on a workspace.
title is split out from view on purpose. Titles change constantly, so a bar
tracking only the window set can subscribe to view and avoid being woken on
every title tick.
view is a bare notification — "the set changed, re-read it" — because no
single window identifies the change. Everything else carries enough to redraw
without a round trip:
# Print the focused window's title as it changes, no re-query needed
sweetctl subscribe focus title | while read -r _ _ rest; do
echo "$rest" # e.g. output=DP-1 ws=2 app_id="foot" title="~"
doneFor fuller state, re-query on the matching event:
# Re-render a workspace/window indicator on every relevant change
sweetctl subscribe view workspace layout | while read -r _; do
sweetctl -j workspaces
sweetctl -j views
doneThat is all a bar needs for a stack indicator like [2/5] on a
tabbed group: workspaces reports
windows=N, and views marks the focused window and its order.
A subscriber that stops reading is dropped once its backlog grows too large, so a stuck bar can never make the compositor buffer without bound. Reconnect.
title events fire for native Wayland (xdg-shell) windows. XWayland windows
do not yet push title changes; re-query views on the next focus or view
event.