Skip to main content

Macropad

CI

Turn every keyboard into a macropad.

Macropad currently supports Linux only. macOS and Windows support are coming soon.

Install

Install the compiler and Python headers required to build the Linux input dependency:

Debian/Ubuntu:

sudo apt install build-essential python3-dev

Fedora:

sudo dnf install gcc python3-devel

The distribution is named poor-mans-macropad; the installed command remains macropad. Install it in an isolated environment with either pipx or uv rather than modifying the system Python.

pipx

Install pipx from the distribution:

sudo apt install pipx

On Fedora, use:

sudo dnf install pipx

Then ensure its command directory is on PATH and install Macropad:

pipx ensurepath
pipx install poor-mans-macropad

To include desktop notifications, install poor-mans-macropad[notifications] instead.

Start a new login shell after the first pipx ensurepath if macropad is not immediately found.

uv

If uv is already installed, its tool interface provides the same isolated installation:

uv tool install poor-mans-macropad

To include desktop notifications, install poor-mans-macropad[notifications] instead.

Optional Desktop Notifications

Desktop notifications are optional. The base installation listens for input, reloads profiles, and executes actions without importing DBus. To install the notification extra, first install its native build dependencies.

Debian/Ubuntu:

sudo apt install pkg-config libdbus-1-dev libglib2.0-dev

Fedora:

sudo dnf install pkgconf-pkg-config dbus-devel glib2-devel

Then use one of these package specifications with the installer chosen above:

pipx install 'poor-mans-macropad[notifications]'
uv tool install 'poor-mans-macropad[notifications]'

The notification dependencies are installed only when the notifications extra is explicitly requested. A base pipx install poor-mans-macropad or uv tool install poor-mans-macropad intentionally omits dbus-python and notify2. The native headers above must be present while installing the extra.

At runtime, notifications also require a graphical session, a working DBus session, and an available desktop notification service. Check all installation and runtime conditions with:

macropad doctor

Without the extra, an attempted notification produces one warning and the process continues without further attempts. Disable notification attempts explicitly in the foreground with:

macropad --no-notifications listen

Confirm the installed version:

macropad --version

Input Permissions

Run macropad doctor first. If it reports input-device permission failures, add the current user to the input group, then log out and back in:

sudo usermod --append --groups input "$USER"

This command applies to Debian, Ubuntu, and Fedora. Some desktop environments already provide access through per-session ACLs, so group membership is unnecessary when macropad doctor reports input access as PASS. Membership in the input group grants access to all input events and can expose every keystroke, including passwords. Prefer a device-specific udev rule when broad input access is not acceptable.

Update

Upgrade Macropad with the installer that owns it:

pipx upgrade poor-mans-macropad

or:

uv tool upgrade poor-mans-macropad

Then refresh the generated service path, restart the service, and confirm the installed version:

macropad service install
macropad service restart
macropad --version

Usage

Listen using profiles from the default configuration directory:

macropad listen
macropad --verbose listen
macropad --debug listen

Validate profiles without opening or grabbing input devices:

macropad validate
macropad validate profile.yml
macropad validate --directory ./profiles

Check input permissions, profile access, and the runtime environment:

macropad doctor

Inspect live keyboard worker and device state:

macropad status

Create a first profile through guided device and key detection:

macropad init

List readable input devices and stream key names without grabbing the keyboard:

macropad monitor
macropad monitor 'Exact Device Name'

Profile Format

Macropad loads .yml files from $XDG_CONFIG_HOME/macropad/profiles/, defaulting to ~/.config/macropad/profiles/ when XDG_CONFIG_HOME is unset or relative. A profile uses the strict version 1 format and names a keyboard exactly as Linux evdev reports it:

When a custom absolute XDG_CONFIG_HOME is configured but its Macropad profile directory does not exist, an existing legacy ~/.config/macropad/profiles/ directory remains active. Create or migrate profiles into the XDG directory to switch; when both directories exist, the XDG directory wins. Macropad never moves profile files automatically.

device: My Macro Keyboard
version: '1'
bindings:
  KEY_A: notify-send Macropad 'A released'
  KEY_B:
    down: playerctl play-pause
    up:
      - notify-send Macropad 'B released'
      - logger 'Macropad B released'

Unknown top-level fields, key names, event names, layer fields, and duplicate YAML keys are errors. device must be a non-empty string. version may be the string '1' or integer 1; omitted versions default to 1. A complete merged device configuration must contain at least one base action.

Use any key name exported by Linux evdev, such as KEY_A, KEY_UP, or KEY_PLAYPAUSE. A command string is shorthand for an up action. An event can run one command string or a non-empty list of commands, which are submitted in the listed order as independent detached processes.

Events

Event Behavior
down Runs on key press. A binding containing only down also repeats on kernel key-repeat events.
up Runs when the key is released.
hold Resolves when evdev reports that the key was held; this depends on the keyboard repeat behavior.
double_tap Runs after two press/release pairs occur within the multi-tap resolution window.
triple_tap Runs after three press/release pairs occur within the multi-tap resolution window.

Bindings with multiple event choices or requiring hold or multi-tap resolution wait for a 200 ms quiet period. Simple bindings containing one immediate event run without that delay.

device: My Macro Keyboard
version: '1'
bindings:
  KEY_C:
    up: notify-send Macropad 'Single tap'
    double_tap: notify-send Macropad 'Double tap'
    triple_tap: notify-send Macropad 'Triple tap'
  KEY_D:
    up: notify-send Macropad 'Released normally'
    hold: notify-send Macropad 'Held and released'

Timing

Timing configuration is optional. Profiles without it retain the existing 200 ms multi-tap window and five-second one-shot timeout. Configure either or both fields at the top level:

device: My Macro Keyboard
version: '1'
timing:
  multi_tap_ms: 300
  one_shot_timeout_ms: 8000
bindings:
  KEY_C:
    up: notify-send Macropad 'Single tap'
    double_tap: notify-send Macropad 'Double tap'
  KEY_N:
    up: ^layer navigation once
layers:
  navigation:
    bindings:
      KEY_H: notify-send Macropad Left
Field Range Default Behavior
multi_tap_ms 1–5,000 ms 200 ms Quiet period after the latest event before a binding that needs hold or multi-tap resolution is resolved.
one_shot_timeout_ms 1–3,600,000 ms 5,000 ms Idle time a one-shot layer waits for its next key before deactivating.

Increasing multi_tap_ms gives slower taps more time to form double- or triple-tap sequences, but also delays single-tap and hold resolution for bindings that contain those event choices. Hold detection itself remains based on evdev kernel-repeat events, so this setting does not turn hold into a duration-based action. One-shot timing is cancelled once a key claims the layer; that key completes normally and then deactivates the layer.

Layers

Layer commands begin with ^ and are handled by Macropad instead of the shell:

Command Behavior
^layer <name> Activates a persistent layer.
^layer <name> once Activates a layer for the next key, with a configurable idle timeout.
^layer <name> momentary Activates a layer while its activation key is held, then restores the prior layer.
^layer <name> toggle Toggles a layer on or off. Its activation key always remains an escape.
^default_layer Deactivates the current layer.

Layer references are checked after all fragments for a device are merged. This permits one fragment to activate a layer defined in another fragment while still rejecting genuinely missing layers. By default, a key absent from the active layer uses its base binding. A layer binding overrides the base binding for that key. This changes the earlier version 1 behavior, which ignored missing layer keys; add fallback: none to preserve that behavior for a layer. A fallback binding consumes a one-shot layer just like a binding declared directly in that layer.

Top-level layers collect all alternate bindings in one section:

device: My Macro Keyboard
version: '1'
bindings:
  KEY_SPACE:
    up: ^layer navigation
  KEY_N:
    up: ^layer navigation once
  KEY_M:
    down: ^layer navigation momentary
  KEY_T: ^layer navigation toggle
layers:
  navigation:
    fallback: base
    bindings:
      KEY_H: notify-send Macropad Left
      KEY_L: notify-send Macropad Right
      KEY_ESC: ^default_layer

Momentary commands are valid only in a binding whose sole event is down; multiple commands may still be listed under that event. Releasing the activation key restores the layer that was active before it was held. A later persistent, one-shot, or toggle transition supersedes that restoration. Momentary press/release transitions are omitted from desktop notifications to avoid noise. Other layer modes retain activation and deactivation notifications.

Layer metadata can be declared without top-level bindings when the bindings live inline:

layers:
  navigation:
    fallback: none

An inline layer defines the alternate action beside a key's base action. It produces the same layer configuration as the top-level form:

device: My Macro Keyboard
version: '1'
bindings:
  KEY_SPACE:
    up: ^layer navigation
  KEY_H:
    up: notify-send Macropad Base
    layers:
      navigation: notify-send Macropad Left

An inline command string is shorthand for up, matching base and top-level layer bindings. Use an explicit event mapping when an inline layer binding runs multiple commands.

Layers cannot contain nested layers. Persistent layers remain active until replaced or an action runs ^default_layer. Toggle layers deactivate when their activation key is pressed again, including when fallback: none would otherwise block that key. One-shot layers deactivate after the claimed key is released or the idle timeout passes.

Profile Fragments

Multiple files with the same exact device name are merged before a worker starts. This allows base bindings and layers to be organized into separate files:

# media.yml
device: My Macro Keyboard
version: '1'
bindings:
  KEY_PLAYPAUSE: playerctl play-pause
# volume.yml
device: My Macro Keyboard
version: '1'
bindings:
  KEY_VOLUMEUP: playerctl volume 0.05+
  KEY_VOLUMEDOWN: playerctl volume 0.05-

Bindings for different keys or events combine. Defining different commands for the same device, layer, key, and event is a conflict and validation fails with the later file and field path. Timing fields can also be split across fragments. Repeating the same value is allowed; different values for the same timing field are a conflict attributed to the later fragment.

When default profiles or --watch directories are used, Macropad recognizes .yml files that are created, modified, deleted, or moved. Move events inspect both source and destination paths so editors that atomically replace profiles through temporary files are supported. A burst of changes is coalesced until the directory has been quiet for one second, then the complete candidate set is loaded and merged once. An invalid edit is reported while the last valid workers continue running.

Action Execution And Security

Profile actions are trusted shell code and must never be loaded from an untrusted source. Each command runs with shell=True, starts from /, inherits the Macropad process environment, receives no stdin, and normally has stdout and stderr discarded. Actions are detached and continue across profile reloads or worker shutdown.

Each keyboard worker tracks at most eight concurrent actions. Additional actions are dropped rather than queued until an earlier action exits. The systemd user service includes ~/bin, ~/.local/bin, and standard system command directories in its deterministic PATH. Use absolute paths or a service override for commands installed in other interactive-shell or tool-manager directories.

Runtime And Action Debugging

Global logging options precede the subcommand and work with both macropad and python -m macropad:

macropad --verbose listen
python -m macropad --debug listen
macropad --action-debug listen

Normal mode shows warnings and errors, including failed action starts, nonzero exits, worker failures, and actions dropped at the concurrency limit. --verbose adds lifecycle summaries, resolved action submissions, process starts, successful exits, layer transitions, and profile reloads. --debug also shows unbound input, event resolution, active binding selection, and the action execution environment.

Action diagnostics identify the resolved device, KEY_* name, event, base or named layer, command, process ID, exit status, and concurrency count where applicable. Debug environment output includes the / working directory, action PATH, display names, whether a DBus session address is set, and whether output is discarded or inherited. It intentionally does not dump the complete environment or the DBus address.

--action-debug implies debug logging and changes only action stdout and stderr: they inherit the Macropad process streams instead of /dev/null. In a foreground listener they appear in that terminal; when configured on the systemd service command they go to the journal. This output can contain credentials or other sensitive application data, so enable it only while diagnosing a trusted action. Stdin remains /dev/null, actions remain detached, and the eight-action limit is unchanged.

The generated service uses --verbose so lifecycle and action summaries remain available through macropad service logs. To capture action output in the journal, add --action-debug before listen in the service's effective ExecStart, reload the unit, and restart the service. Remove it after debugging.

Validation

macropad validate uses the same loading, strict schema validation, fragment grouping, merge conflict detection, and merged semantic checks as startup. It does not initialize notifications, start workers, execute actions, or open input devices.

With no arguments, it validates the default profile directory without creating it. Explicit files and directories can be combined:

macropad validate base.yml --directory ./profile-fragments

A valid complete set prints a blank-separated PASS section for each file, followed by the profile-file and device counts, and exits with status 0. Missing or invalid files get their own FAIL sections with multiline source and field-path diagnostics. Files that pass standalone schema validation show PARSED instead of PASS when merged validation cannot complete. Unresolved layers and conflicts are attributed to the relevant file; device-wide errors use a Profile set section.

Diagnostics

Run macropad doctor before starting the service to check input-device enumeration, open and exclusive-grab access, the default profile directory, optional desktop notifications, executable and service paths, current and service action PATH values, and graphical-session variables. Each blocking FAIL includes remediation. Expected grabs held by an active Macropad service are reported as nonblocking WARN results; optional notification, service, and session-environment gaps are INFO results.

The input check briefly grabs and immediately releases each readable event device, closing every handle even when a check fails. When the service is active, stop it and rerun doctor only when you need to distinguish Macropad's expected grabs from another input grabber:

macropad service stop
macropad doctor
macropad service start

Device Monitoring

Run macropad monitor without arguments to list every readable input device. Devices sharing an exact evdev name are grouped together and every matching /dev/input/event* path is shown; paths that cannot be opened include their permission or device error. Device names matching a profile in the default profile directory are marked [configured].

Pass an exact listed name to stream key activity:

CONNECTED    /dev/input/event12
down         KEY_A                    /dev/input/event12
repeat       KEY_A                    /dev/input/event12
up           KEY_A                    /dev/input/event12

Monitoring never exclusively grabs a device, so normal keyboard input continues. It opens every current path with the selected name, reports disconnects, reacquires matching paths after reconnect, and exits cleanly with Ctrl+C or SIGTERM. An active Macropad worker already holds its configured device exclusively; if monitor connects but shows no events, run macropad service stop before monitoring and start the service again afterward.

Guided Initialization

Run macropad init to create one working release binding without manually discovering evdev names or writing YAML. The command first runs the blocking input-device checks from macropad doctor, then asks you to disconnect the target keyboard. Detection starts only after you confirm that it is disconnected: reconnect it and press the key you want to configure. By default, each device or key capture times out after 60 seconds; use --timeout SECONDS to choose another positive timeout.

The command asks for a trusted shell command and writes a new fragment under the resolved default profile directory. Generated macros run when the captured key is released. The directory follows the same XDG and legacy fallback rules as listen and validate.

If the detected device already has profiles, init lists every existing fragment and asks whether to add a new one. It never edits those files. If the captured key already has an up action, release the key and capture another one or cancel. Other events on that key can safely merge with the generated release binding.

Filenames are derived from the device and key, for example macro_keyboard_key_a.yml. Existing filenames are preserved and a numeric suffix is selected. The new fragment is conflict-checked against the complete existing profile set before creation, opened exclusively, and validated with the normal complete on-disk validation workflow before success is reported. Cancellation, timeout, write failure, or validation failure removes the new file without touching existing fragments.

An active Macropad service may already own a configured keyboard and prevent non-grabbing key capture. If initialization times out for an existing device, stop the service and retry:

macropad service stop
macropad init

On success, init reports the exact path and refreshes the systemd service state. Active state alone cannot prove which arguments a customized service uses, so automatic loading is confirmed only as a condition of the standard listen --watch command and default profile directory. If the service is inactive, start in the foreground with macropad listen or use the service command shown by init.

Runtime Status

macropad status reports whether each configured keyboard worker is actually waiting, opening event paths, listening after a successful exclusive grab, backing off after failure, reporting an input error, or shutting down. It includes the parent and worker PIDs, profile fragment paths, currently grabbed event paths, failure reason, and retry delay where available:

Macropad parent PID: 1234

Macro Keyboard
  state: listening
  pid: 1235
  profiles: /home/user/.config/macropad/profiles/macro.yml
  paths: /dev/input/event12

Media Pad
  state: backing_off (retry in 4.0s)
  profiles: /home/user/.config/macropad/profiles/media.yml
  error: process 1236 exited

An invalid watched profile candidate is shown separately while the last valid workers keep running. Permission and grab errors include the affected event path when known. listening is reported only after the worker successfully grabs at least one matching path; a visible device before child startup does not count as listening.

The command connects to a read-only Unix socket owned by the listening parent under $XDG_RUNTIME_DIR/macropad/, with a user-specific directory under the system temporary directory as a fallback. The directory and socket are user-only, stale sockets are removed safely, and no remote control operations are exposed. If no parent is reachable, the command reports whether the systemd service is active and points to the appropriate next command.

macropad status and macropad service status answer different questions: the former reports live Macropad worker/device readiness, while the latter remains a direct proxy for systemd unit state and logs.

Service

Install and start the systemd user service:

macropad service install

To persistently disable notification attempts in the generated service, install it with:

macropad --no-notifications service install

The command writes $XDG_CONFIG_HOME/systemd/user/macropad.service when XDG_CONFIG_HOME is absolute, defaulting to ~/.config/systemd/user/macropad.service, then reloads systemd and enables and starts the unit. The generated unit invokes the macropad executable from the active tool environment and does not depend on a source checkout. Reinstalling updates a recognized generated unit. Macropad refuses to replace an unrelated unit at that path unless macropad service install --force is used.

The generated unit starts as part of the standard systemd graphical session, after the desktop has published variables such as DISPLAY, WAYLAND_DISPLAY, and XAUTHORITY. It stops with that session and remains desktop-environment independent.

By default, profile actions use a deterministic PATH containing standard system and user command directories. To capture the current shell's PATH in the generated unit instead, run:

macropad service install --action-path "$PATH"

The value is persisted until the next service installation. Every entry must be non-empty and absolute; duplicate entries are removed. Review the value first because profile actions resolve commands through these directories with your user permissions.

Manage the installed unit through the CLI:

macropad service status
macropad service logs
macropad service restart
macropad service stop
macropad service disable

service logs follows the systemd journal until interrupted. These commands preserve the output and exit status from systemctl --user or journalctl --user. Use macropad listen instead when running Macropad directly in the foreground.

Uninstall

Remove the service before uninstalling the tool:

macropad service uninstall
uv tool uninstall poor-mans-macropad

For a pipx installation, replace the final command with:

pipx uninstall poor-mans-macropad

service uninstall stops and disables the unit, removes it only when its generated marker is present, and reloads systemd. It does not remove profiles.

Coming Up Next

  • Native Keyboard Shortcuts And Chords: Emit key presses, shortcuts, and sequences without depending on external tools such as ydotool or xdotool.
  • Tray Icon: Inspect service status, reload profiles, and access common controls from the desktop.
  • Context-Aware Bindings: Change bindings based on the current application, window title, or other context.
  • Windows And macOS Compatibility: Bring Macropad profiles and workflows to additional desktop platforms.

Changelog

See the changelog for released and upcoming user-facing changes.

Development

Install the base and optional-notification native dependencies listed above before running the full integration suite, then create the project environment. Development uses the same service renderer and lifecycle as released installations:

make
make install-editable
uv run macropad service install
make test
make test-init
make test-wheel
make test-service
make test-systemd
make lint
make build

Run make format to apply the Ruff lint and formatting policy.

CI runs the unit suite on Python 3.10 through 3.14, verifies linting, formatting, the lockfile, wheel installation, packaged assets, service lifecycle, systemd unit, and distribution build. See RELEASING.md for the Trusted Publishing release process.

License

Macropad is available under the MIT License.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

poor_mans_macropad-0.3.0.tar.gz (51.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

poor_mans_macropad-0.3.0-py3-none-any.whl (62.8 kB view details)

Uploaded Python 3

File details

Details for the file poor_mans_macropad-0.3.0.tar.gz.

File metadata

  • Download URL: poor_mans_macropad-0.3.0.tar.gz
  • Upload date:
  • Size: 51.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for poor_mans_macropad-0.3.0.tar.gz
Algorithm Hash digest
SHA256 1d698e64c7a65bd9429c58432c1826083e96116f7e03a21f4f535ccd14a5e592
MD5 fc378e98830197761861129ad68632ab
BLAKE2b-256 85f5d6924dfef349a11ef4fc569374d7de5f5bfbe7ac60ce98e33e3894b1c6cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for poor_mans_macropad-0.3.0.tar.gz:

Publisher: release.yml on rafaelglikis/macropad

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file poor_mans_macropad-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for poor_mans_macropad-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 283bd9df3e3d25c9e5a3ad19f623e7654cc04c85b37519c3bb2965fbbfd5cbd9
MD5 5b2339626b4935de5ee058db80d052e6
BLAKE2b-256 45cd2ebfd9533e6422cf0a15b36fc911d98f2eb8aa0e42b031605e67aaec3c74

See more details on using hashes here.

Provenance

The following attestation bundles were made for poor_mans_macropad-0.3.0-py3-none-any.whl:

Publisher: release.yml on rafaelglikis/macropad

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page