axctl is a universal IPC daemon and CLI for Wayland compositors. It normalizes window, workspace, monitor, layout, config, and system operations across Hyprland, Niri, and Mango via a JSON-RPC API over a Unix socket.
- Runs a compositor-aware daemon that auto-detects Hyprland, Niri, or Mango
- Exposes a single JSON-RPC interface over
/tmp/axctl-$UID.sock - Provides a CLI for window and workspace management, configuration, and system helpers
- Watches a TOML config file and applies changes live when possible
- Hyprland
- Niri
- Mango
axctl daemondetects the active compositor and starts a JSON-RPC serveraxctl <command>is a client that sends JSON-RPC requests to the daemon- The socket lives at
/tmp/axctl-$UID.sock
curl -L get.axeni.de/axctl | shOn NixOS, the installer uses nix profile add github:Axenide/axctl instead of
writing to /usr/local/bin. On other distros it also adds your user to the
input group (needed for modifier-alone binds on niri — see
Modifier-alone binds).
Requires Go 1.25+.
go build -o axctl .
./axctl --versionnix profile add github:Axenide/axctl
nix build
./result/bin/axctl --version
nix run# Start the daemon (keep it running)
./axctl daemon
# Query state
./axctl window list
# Stream events
./axctl subscribeModifier-alone binds (e.g. Super alone opens the launcher) are emitted as native release binds on Hyprland and MangoWC, and handled by axctl's evdev monitor on niri — the latter needs the
inputgroup, which the installer adds automatically; see Modifier-alone binds.
General form:
axctl <command> <action> [args]Run axctl with no arguments to print the full built-in command list.
axctl window list
axctl window active
axctl window focus <id>
axctl window move <l|r|u|d> [id]
axctl window resize <w> <h> [id]
axctl window fullscreen <0|1> [id]
axctl window toggle-floating [id]axctl workspace list
axctl workspace active
axctl workspace switch <id>
axctl workspace move-to <workspace_id> [window_id]axctl overview toggleToggles the compositor's overview. Only niri implements it
(ToggleOverview IPC action); other compositors return
"feature not supported".
axctl monitor list
axctl monitor focus <id>
axctl monitor set-dpms <monitor_id> <0|1>axctl layout set <name>axctl config get <key>
axctl config set <key> <value>
axctl config batch '{"gaps.inner": 8, "gaps.outer": 12}'
axctl config reloadSupported config keys include:
gaps.inner, gaps.outer, border.width, border.active_color,
border.inactive_color, opacity.active, opacity.inactive, blur.enabled,
blur.size, blur.passes.
axctl system get-cursor-position
axctl system switch-keyboard-layout [next|prev]
axctl system set-keyboard-layouts "us,es" "altgr-intl,"
axctl system idle-wait <ms>
axctl system is-idle <ms>
axctl system get-capabilities
axctl system get-compositorManage backlight (internal panels via brightnessctl) and external monitors
(via ddcutil over DDC/CI). Values are always in the normalized 0..1 range
unless otherwise noted.
axctl brightness list # all devices, with current 0..1
axctl brightness get <monitor> # read one device's brightness
axctl brightness set <monitor> <0..1> # set one device, or omit for all
axctl brightness adjust <monitor> <+/-0..1>
axctl brightness save [monitor] # snapshot to XDG state dir
axctl brightness restore [monitor] # reapply saved valuesDevice names follow the convention backlight-<kernel-dev> for internal
laptop panels (e.g. backlight-intel_backlight, backlight-amdgpu_bl1) and
ddc-<bus> for external displays (e.g. ddc-3 for /dev/i2c-3). Pass
backlight to address every internal panel at once. Saved values persist
across reboots in $XDG_CONFIG_HOME/axctl/brightness.tsv.
Successful set/adjust calls emit an Event.BrightnessChanged
notification on the subscribe channel with {monitor, value}.
Window, workspace, and monitor IDs are compositor-defined. Treat them as strings in scripts because Hyprland can use hexadecimal IDs while Niri uses integers.
The daemon loads TOML from:
~/.config/axctl/config.toml
If the file exists, the daemon will load it on startup and watch it for
changes (including any include files), applying updates live when supported.
Example snippet:
[appearance]
[appearance.gaps]
inner = 5
outer = 10
[appearance.border]
width = 2
active_color = "#ff5555"
inactive_color = "#333333"
[input]
[input.keyboard]
layouts = "us,es"
variants = "altgr-intl,"
[[keybinds]]
modifiers = ["SUPER"]
key = "Return"
dispatcher = "exec"
argument = "kitty"
enabled = trueSee pkg/config/example.toml for the full configuration reference.
The daemon uses these environment variables to detect sockets:
XDG_RUNTIME_DIRfor Wayland socketsWAYLAND_DISPLAYfor Mango fallbackNIRI_SOCKETfor Niri (optional; auto-discovered from$XDG_RUNTIME_DIR/niri*.sockwhen unset)HYPRLAND_INSTANCE_SIGNATUREfor Hyprland
The daemon listens on:
/tmp/axctl-$UID.sock
A bind on the modifier key itself (e.g. Super_L with modifiers [SUPER]
for a Super-alone app launcher) means "fire when the modifier is released
without any other key press in between".
How it is implemented depends on the compositor:
- Hyprland / MangoWC — emitted as a native release bind
(
bindr = SUPER, Super_L, ...). Both compositors suppress the trigger when another key was pressed while the modifier was held, so the bind behaves like a plain config bind: it can be unbound or overridden later in the user's own config, and it keeps working for keyboards connected after the daemon starts. - niri — niri only fires binds on key press and has no release-based
trigger, so such binds are skipped in the generated config and the axctl
daemon implements the behavior itself by observing
/dev/inputevents (read-only; no grab, no uinput), running the bound command when the modifier is released alone.
Niri requirements:
-
The daemon user must be in the
inputgroup to read/dev/input/event*. The installer adds the group automatically; for manual or NixOS installs:sudo usermod -aG input "$USER"On NixOS, add
inputtousers.users.<name>.extraGroupsinstead. Then log out and back in (the change does not apply to running sessions). Verify with:id | tr ',' '\n' | grep -w input. -
Declare the bind normally in the config (key
Super_Lwith modifiers[SUPER]); it is skipped in the generated niri config and handled by the monitor instead. Without the group, the daemon logs a warning and modifier-alone binds stay disabled. -
Keyboards connected after the daemon starts are not monitored; restart the daemon to pick them up.
-
Check the live state with
axctl system keymon-status.
Error: axctl daemon is already running.- Stop the existing daemon or delete a stale
/tmp/axctl-$UID.sockand restart.
- Stop the existing daemon or delete a stale
Error: no supported compositor detected- Ensure your compositor is running and the expected socket variables are set (see Environment and sockets).
Error connecting to daemon- Start the daemon with
axctl daemonand verify the socket exists.
- Start the daemon with
- Super-alone binds do nothing (niri)
- The user must be in the
inputgroup (log back in after adding it). Checkaxctl system keymon-status: it lists the registered binds, the opened/dev/inputdevices, and any per-device errors.
- The user must be in the
- Super-alone binds do nothing (Hyprland / MangoWC)
- These are plain release binds (
bindr); check the generated config andhyprctl binds(or the MangoWC equivalent) for theSuper_Lentry, and make sure no later bind or user config overrides it.
- These are plain release binds (
go test ./...See LICENSE.
