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. Niri and Mango implement it; Hyprland returns "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.rounding,
border.active_color, border.inactive_color, opacity.active,
opacity.inactive, blur.enabled, blur.size, blur.passes, shadow.enabled,
shadow.size, shadow.color, animations.enabled.
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 and
Mango use numeric window IDs. Mango workspace IDs include the monitor, for
example DP-1:3, because each monitor has its own tag 3. Numeric workspace
arguments such as 3 continue to target the currently selected monitor. Mango
window metadata includes every assigned tag because a window can belong to
multiple tags.
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 socketsNIRI_SOCKETfor Niri (optional; auto-discovered from$XDG_RUNTIME_DIR/niri*.sockwhen unset)HYPRLAND_INSTANCE_SIGNATUREfor HyprlandMANGO_INSTANCE_SIGNATUREfor Mango's native IPC socket (normally an absolute path such as$XDG_RUNTIME_DIR/mango-<pid>.sock). When unset, axctl detectsXDG_CURRENT_DESKTOP=mangoand discovers a live Mango socket.
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 (
Mango uses one Unix-socket connection per query or dispatch and one connection
per event watch. Axctl follows that protocol directly; it does not invoke
mmsg as a subprocess. Workspaces are Mango tags, including the special tag 0.
Named special workspaces and silent moves to another monitor have no equivalent
in Mango and return "feature not supported". The monitor's active_tags: [0]
is Mango's native overview marker; the special tag's client visibility is
reported separately from the active normal tagset.
Mango can switch keyboard layouts to the next layout or to a numeric index;
the IPC does not expose a reliable previous-layout action.
Mango stores layouts per tag. Switch to a workspace and use axctl layout set
to change just that workspace's active layout; workspace list exposes each
tag's normalized layout name in its metadata. Ambxst persists those selections
and generates a default tag_rule followed by monitor-specific tag overrides.
For standalone configs, Mango's native tag_rule entries can also set a
different layout for each tag.
The Mango keybind generator maps layoutmsg colresize +conf to Mango's native
next-proportion preset action. Relative colresize ±N bindings call
axctl window layout-prop scroller_proportion ±N through the daemon, so Mango
must be able to find axctl in PATH. Mango's native preset dispatcher has no
previous-preset action. In a horizontal scroller, resizewin on the X axis
changes a window's width proportion, while the Y axis adjusts stack proportion.
The generated Mango config uses the current .conf option names. Include the
generated file from your Mango root config, for example:
source=~/.config/axctl/mango/axctl.generated.confGenerated keybind changes are applied by reloading Mango's root config. Mango's IPC has no runtime unbind operation; a keybind that also exists in the root config may therefore conflict with the generated bind.
From a Mango session, run the optional live checks with:
AXCTL_MANGO_LIVE=1 go test ./pkg/ipc/mango -run '^TestMangoLive' -count=1They query the active session, validate a temporary generated config with
mango -p, toggle overview twice when its state is unambiguous, and change the
layout on an empty tag temporarily to verify that another tag's layout is
preserved before restoring the original workspace state.
go test ./...See LICENSE.
