Skip to content
AxenidePublic

About

A Wayland multi-compositor IPC.

Resources

Stars

14 stars

Watchers

1 watching

Forks

Repository files navigation

axctl banner

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.

What it does

  • 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

Supported compositors

  • Hyprland
  • Niri
  • Mango

Architecture overview

  • axctl daemon detects the active compositor and starts a JSON-RPC server
  • axctl <command> is a client that sends JSON-RPC requests to the daemon
  • The socket lives at /tmp/axctl-$UID.sock

Install

One-line install

curl -L get.axeni.de/axctl | sh

On 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).

Build from source

Requires Go 1.25+.

go build -o axctl .
./axctl --version

Nix

nix profile add github:Axenide/axctl

nix build
./result/bin/axctl --version

nix run

Quick start

# Start the daemon (keep it running)
./axctl daemon

# Query state
./axctl window list

# Stream events
./axctl subscribe

Modifier-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 input group, which the installer adds automatically; see Modifier-alone binds.

Usage guide

General form:

axctl <command> <action> [args]

Run axctl with no arguments to print the full built-in command list.

Window

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]

Workspace

axctl workspace list
axctl workspace active
axctl workspace switch <id>
axctl workspace move-to <workspace_id> [window_id]

Overview

axctl overview toggle

Toggles the compositor's overview. Niri and Mango implement it; Hyprland returns "feature not supported".

Monitor

axctl monitor list
axctl monitor focus <id>
axctl monitor set-dpms <monitor_id> <0|1>

Layout

axctl layout set <name>

Config

axctl config get <key>
axctl config set <key> <value>
axctl config batch '{"gaps.inner": 8, "gaps.outer": 12}'
axctl config reload

Supported 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.

System

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-compositor

Brightness

Manage 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 values

Device 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}.

Notes on IDs

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.

Configuration

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 = true

See pkg/config/example.toml for the full configuration reference.

Environment and sockets

The daemon uses these environment variables to detect sockets:

  • XDG_RUNTIME_DIR for Wayland sockets
  • NIRI_SOCKET for Niri (optional; auto-discovered from $XDG_RUNTIME_DIR/niri*.sock when unset)
  • HYPRLAND_INSTANCE_SIGNATURE for Hyprland
  • MANGO_INSTANCE_SIGNATURE for Mango's native IPC socket (normally an absolute path such as $XDG_RUNTIME_DIR/mango-<pid>.sock). When unset, axctl detects XDG_CURRENT_DESKTOP=mango and discovers a live Mango socket.

The daemon listens on:

/tmp/axctl-$UID.sock

Modifier-alone binds

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/input events (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 input group to read /dev/input/event*. The installer adds the group automatically; for manual or NixOS installs:

    sudo usermod -aG input "$USER"

    On NixOS, add input to users.users.<name>.extraGroups instead. 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_L with 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.

Troubleshooting

  • Error: axctl daemon is already running.
    • Stop the existing daemon or delete a stale /tmp/axctl-$UID.sock and restart.
  • 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 daemon and verify the socket exists.
  • Super-alone binds do nothing (niri)
    • The user must be in the input group (log back in after adding it). Check axctl system keymon-status: it lists the registered binds, the opened /dev/input devices, and any per-device errors.
  • Super-alone binds do nothing (Hyprland / MangoWC)
    • These are plain release binds (bindr); check the generated config and hyprctl binds (or the MangoWC equivalent) for the Super_L entry, and make sure no later bind or user config overrides it.

Mango IPC notes

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.conf

Generated 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=1

They 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.

Development

go test ./...

License

See LICENSE.

About

A Wayland multi-compositor IPC.

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages