Files
gKeyMapper/AGENTS.md

161 lines
9.1 KiB
Markdown
Raw Normal View History

2026-10-17 02:42:09 +02:00
# AGENTS.md
## What this is
2026-10-17 03:53:51 +02:00
A pure GTK4 C++ GUI plus a background daemon for remapping the G1–G9 keys of a
Logitech G910. The GUI edits and persists mappings; the daemon reads the G-keys
and replays the configured action. Hardware G-key detection and action execution
live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
(common) and `daemon/`.
2026-10-17 02:42:09 +02:00
## Build / run / verify
2026-10-17 03:53:51 +02:00
- `make` — builds the `gkeymapper` GUI, the `gkeymapper-daemon` and the `.mo`
catalogs. Needs `g++` (C++17), `make`, `gettext`, `libgtk-4-dev`,
`libusb-1.0-0-dev` and `libxkbcommon-dev` (see README for `apt install` on a
fresh machine).
- `make daemon` — builds only `gkeymapper-daemon` (needs libusb + xkbcommon,
not GTK).
2026-10-17 02:42:09 +02:00
- `make translations` — only runs `msgfmt` on the existing `.po` files.
- `make run` — builds, then runs with `GKEYMAPPER_LOCALEDIR=$PWD/locale` and
`GKEYMAPPER_ASSETS_DIR=$PWD/assets` so the working-tree translations and
assets are used. Plain `./gkeymapper` works too (assets fall back to
`./assets`).
- `make clean` — deletes the binary and the generated `locale/` tree.
- No tests and no CI. Verify changes by building and running the app.
## i18n — easy to get wrong
- English is the source language (the `msgid`) **and** also has an explicit
identity catalog `po/en.po`. Languages: `de`, `en`, `es`, `fr`.
- `make translations` does **not** regenerate the `.pot`. When you add or change
a UI string, edit `po/gkeymapper.pot` and `po/*.po` by hand, then run
`make translations`; validate with `msgfmt --check -o /dev/null po/<lang>.po`.
- Never edit the generated `.mo` files under `locale/` (gitignored). Delete any
`po/*.po~` backups before committing.
- All documentation and source comments are English. German appears only inside
`po/de.po`.
## Code layout and conventions
2026-10-17 03:53:51 +02:00
- Modules under `src/` (GUI), `shared/` (used by both binaries) and `daemon/`
(the background service). Shared API lives in `namespace gkeymapper`.
`shared/mapping` owns the mappings file format, `shared/keys` the canonical
key-name table (lowercase GDK names -> Linux `KEY_*`).
- The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp`)
**and** the daemon (`daemon/*.cpp` + `shared/*.cpp`, needs `libusb-1.0-0-dev`
and `libxkbcommon-dev`). `make daemon` builds only the daemon.
- The daemon is split into: `device_tables` (raw HID report tables per model),
`g910_device` (libusb open/detach/claim/read), `uinput_keyboard` (virtual
keyboard), `action` (dispatch `key`/`command`/`disabled`) and `config_watch`
(inotify auto-reload of the mappings file). `shared/keymap` does the layout
lookups for both sides. The hot path never touches the GUI.
- `make install` (PREFIX default `/usr/local`) also installs the packaging
files from `data/`: the udev rule, the `modules-load.d` fragment and the
systemd **user** unit. The unit contains an `@BINDIR@` placeholder that the
Makefile substitutes with `$(PREFIX)/bin`. Keep it a user service — it needs
the session for `/dev/uinput` and `DISPLAY`.
- Daemon config reload: `config_watch` watches the config **directory** (so an
atomic save via `rename` is caught) and reports a change at most once per loop
iteration, which is the coalescing window. `SIGHUP`/`SIGUSR1` remain as a
fallback. A failed reload keeps the previous mappings on purpose.
2026-10-17 02:57:21 +02:00
- `main.cpp` keeps the `rows` vector (one `KeyRow` per G key, widget pointers)
2026-10-17 03:53:51 +02:00
in an anonymous namespace; `src/mappings.cpp` is GUI glue that moves data
between the widgets and the shared `MappingSet`. App id:
2026-10-17 02:57:21 +02:00
`com.example.gkeymapper`.
2026-10-17 02:42:09 +02:00
- The Type dropdown index is meaningful everywhere: `0=Key`, `1=Command`,
2026-10-17 03:53:51 +02:00
`2=Disabled`, matching `ActionType` and the `key`/`command`/`disabled` tokens
in the file. Keep that order.
2026-10-17 02:42:09 +02:00
- The capture dialog's `CaptureSession` is heap-allocated and freed only by the
window's `"destroy"` handler. Always close the dialog with
`gtk_window_destroy` so the session is released.
- Use GTK4 API only: `gtk_editable_set_text` / `gtk_editable_get_text` (there is
no `gtk_entry_set_text`), `gtk_window_destroy` (no `gtk_dialog_run`),
`G_APPLICATION_FLAGS_NONE`.
- The main window is intentionally **non-resizable** and has **no**
`GtkScrolledWindow` (one collapsed the rows earlier). Keep the fixed widths
(Type dropdown 150 px, target entry 440 px min, action button 180 px) so rows
do not jump when the language changes.
- Because it is pure GTK4 it follows the system theme (Mint-Y ships a
`gtk-4.0` theme). Do **not** add the `suggested-action` CSS class — Mint-Y
tints it green and it clashes with the rest of the layout.
## Config file format
`Save mappings` writes `~/.config/gKeyMapper/mappings.conf`; Export/Import use
the same format:
```
G1=key:ctrl+c,ctrl+v
G2=command:/usr/bin/firefox
G3=disabled:
```
`<type>:<target>`, one line per key. `key` is a captured sequence (lowercase
`gdk_keyval_name` output; modifiers joined with `+`, steps joined with `,`),
2026-10-17 03:53:51 +02:00
`command` is a program path or command line, `disabled` is inactive. The
canonical modifier order is `ctrl`, `alt`, `shift`, `super`, `altgr`
(`format_shortcut_step()`; parsing accepts any order).
Key names are **layout independent**: `shared/keymap` resolves a name like `z`
to the key that produces `z` on the active layout (read from
`/etc/default/keyboard`, `XKBLAYOUT`) via xkbcommon — on a German keyboard `z` is
evdev 21, not the US position 44. `shared/keys` maps names to the US positions
and is only the fallback. When you add key names, keep them lowercase GDK keysym
names. `parse_shortcut()` takes an optional resolver for this (the daemon passes
one).
The capture dialog stores the **base (level 0) key**: it uses
`Keymap::base_key_name(keycode)` instead of GDK's shifted keyval, so
`Shift`+ß becomes `shift+ssharp` (not `shift+question`, which would double the
shift). `format_shortcut_step()` — not the dialog — renders a step from a
`ModifierBit` mask, so the order stays in one place.
AltGr (level 3) is recorded as the `altgr` modifier plus the base key
(`AltGr`+7 → `altgr+7`). GTK4 dropped the `GDK_MOD5_MASK` name, so the dialog
tracks `ISO_Level3_Shift` / `Mode_switch` through its own key press/release
(`CaptureSession::altGrHeld`) instead of a state bit. On the replay side
`kModAltGr` maps to `KEY_RIGHTALT`.
2026-10-17 02:42:09 +02:00
## Assets
`find_asset()` resolves in order: `$GKEYMAPPER_ASSETS_DIR` → `./assets` →
compiled-in `ASSETS_DIR` (`/usr/local/share/gkeymapper/assets`). The icon
(`assets/icon.png`) is used in the header bar and the About dialog.
2026-10-17 03:53:51 +02:00
## Driving the daemon for manual checks
- The process name is truncated by the kernel to 15 chars, so `pkill -x
gkeymapper-daemon` and `pgrep -x gkeymapper-daemon` **never match**. Use
`pkill -x gkeymapper-daem` / `pgrep -x gkeymapper-daem` (or `pkill -f
'./gkeymapper-daemon'`).
- Run it in the background with `setsid nohup ./gkeymapper-daemon [config]
> /tmp/daemon.log 2>&1 < /dev/null &`; it survives across shell calls. It
takes an optional config path as `argv[1]` (default
`~/.config/gKeyMapper/mappings.conf`).
- `SIGHUP`/`SIGUSR1` reload the mappings
(`kill -HUP $(pgrep -x gkeymapper-daem)`). Normally you do not need them: the
config file is watched, so saving in the GUI reloads within about half a
second. Use `SIGHUP` to check the signal path after touching it.
- Test the auto-reload without hardware: write the config through the shared
save path (atomic temp+rename, as the GUI does) and watch `/tmp/daemon.log`
for a second `loaded ...` line. `daemon/config_watch.cpp` also has a
standalone test shape: create a `ConfigWatcher`, `poll_events()` then
`take_change()` per simulated loop iteration.
- `make install DESTDIR=/tmp/stage` stages the packaging files; inspect
`/tmp/stage/usr/local/lib/...` to verify the unit's `ExecStart` substitution.
`systemd-analyze --user verify <unit>` checks the unit. There is no
`udevadm verify` on systemd 249, so the udev rule is checked by eye against
the reference project's rule.
- Starting it detaches the kernel driver from the vendor interface and creates a
`/dev/uinput` keyboard. Warn the user first: while it runs the media keys may
not work, and `key` mappings are injected into the **focused** window.
- The keyboard is `046d:c32b` (also handle `046d:c335`); on this machine the USB
device and `/dev/uinput` already carry ACLs for the user, so no root is needed.
- Verify decoding by pressing G keys and reading `/tmp/daemon.log` (it logs
`G1 -> key:...` per press). Verify what the virtual keyboard emits at the X
level with `xinput test <keyboard-id>` (`gKeyMapper virtual keyboard` shows up
as both a pointer and a keyboard — use the **keyboard** id). X keycodes are
evdev codes + 8. There is no `xdotool`/`wtype`.
2026-10-17 02:42:09 +02:00
## Driving the GUI for manual checks
There is no `xdotool`/`wtype` on this machine, and the capture-dialog buttons
(`Clear`/`Cancel`/`Done`) expose an empty AT-SPI accessible name — the text is in
a child `label` node, so target the button by its child label. `pyatspi`
(`queryAction().doAction(0)`) plus `Xlib`'s `xtest.fake_input` can click buttons
and inject keys. Warn the user before synthesizing keystrokes or stealing window
focus.