Metadata-Version: 2.4
Name: poor-mans-macropad
Version: 0.1.0
Summary: Turn every keyboard into a Macropad
Keywords: evdev,keyboard,linux,macropad
Author: Rafael Glikis
Author-email: Rafael Glikis <rafaelglikis@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: dbus-python>=1.3.2
Requires-Dist: evdev>=1.6.1
Requires-Dist: notify2>=0.3.1
Requires-Dist: pyyaml>=6.0.1
Requires-Dist: watchdog>=3.0.0
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/rafaelglikis/macropad
Project-URL: Issues, https://github.com/rafaelglikis/macropad/issues
Project-URL: Repository, https://github.com/rafaelglikis/macropad
Description-Content-Type: text/markdown

# Macropad

[![CI](https://github.com/rafaelglikis/macropad/actions/workflows/ci.yml/badge.svg)](https://github.com/rafaelglikis/macropad/actions/workflows/ci.yml)

Turn every keyboard into a macropad.

## Install

Install the system dependencies required to build the Python DBus bindings:

Debian/Ubuntu:

```bash
sudo apt install libdbus-1-dev libglib2.0-dev
```

Fedora:

```bash
sudo dnf install dbus-devel glib2-devel
```

Install Macropad as an isolated user command with `uv`:

```bash
uv tool install poor-mans-macropad
```

The distribution is named `poor-mans-macropad`; the installed command remains `macropad`. `pipx`
is also supported:

```bash
pipx install poor-mans-macropad
```

Confirm the installed version:

```bash
macropad --version
```

## Usage

Listen using profiles from the default configuration directory:

```bash
macropad listen
macropad --verbose listen
macropad --debug listen
```

Validate profiles without opening or grabbing input devices:

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

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

```bash
macropad doctor
```

Inspect live keyboard worker and device state:

```bash
macropad status
```

Create a first profile through guided device and key detection:

```bash
macropad init
```

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

```bash
macropad monitor
macropad monitor 'Exact Device Name'
```

The module entry point is also available:

```bash
python -m macropad --help
```

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

```yaml
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.

```yaml
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'
```

### 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 five-second idle timeout. |
| `^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.
Active layers use only their own bindings; keys absent from the layer do not fall back to base
bindings.

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

```yaml
device: My Macro Keyboard
version: '1'
bindings:
  KEY_SPACE:
    up: ^layer navigation
  KEY_N:
    up: ^layer navigation once
layers:
  navigation:
    bindings:
      KEY_H: notify-send Macropad Left
      KEY_L: notify-send Macropad Right
      KEY_ESC: ^default_layer
```

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

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

Layers cannot contain nested layers. Persistent layers remain active until an action runs
`^default_layer`. 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:

```yaml
# media.yml
device: My Macro Keyboard
version: '1'
bindings:
  KEY_PLAYPAUSE: playerctl play-pause
```

```yaml
# 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.

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`:

```bash
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:

```bash
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:

```bash
macropad service stop
macropad doctor
macropad service start
```

### Input Permissions

On Debian/Ubuntu, add the current user to the `input` group and then log out and back in:

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

On Fedora, use the same group assignment and start a new login session:

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

Some desktop environments provide device access through per-session ACLs, so group membership is
not required when `macropad doctor` already 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.

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

Pass an exact listed name to stream key activity:

```text
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:

```bash
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:

```text
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:

```bash
macropad 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.

Manage the installed unit through the CLI:

```bash
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.

### Upgrade And Uninstall

Upgrade the tool, refresh the generated service path, and restart it:

```bash
uv tool upgrade poor-mans-macropad
macropad service install
macropad service restart
```

For `pipx`, replace the first command with `pipx upgrade poor-mans-macropad`.

Remove the service before uninstalling the tool:

```bash
macropad service uninstall
uv tool 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. For a `pipx` installation, use
`pipx uninstall poor-mans-macropad` as the final command.

## Development

Install the native dependencies listed above, then create the project environment. Development uses
the same service renderer and lifecycle as released installations:

```bash
make
make install-editable
uv run macropad service install
```

```bash
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`](RELEASING.md) for the Trusted Publishing release process.

## License

Macropad is available under the [MIT License](LICENSE).
