Media keys working again
This commit is contained in:
82
AGENTS.md
82
AGENTS.md
@@ -45,12 +45,25 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
|
||||
+ the generated `build/gresources.c`, see Assets) **and** the daemon
|
||||
(`daemon/*.cpp` + `shared/*.cpp`, needs `libusb-1.0-0-dev`,
|
||||
`libxkbcommon-dev` and `libglib2.0-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`), `config_watch`
|
||||
(inotify auto-reload of the mappings file) and `ipc_dbus` (the GDBus service).
|
||||
`shared/keymap` does the layout lookups for both sides. The hot path never
|
||||
touches the GUI: D-Bus is control plane only and is pumped once per loop.
|
||||
- The daemon is split into: `device_tables` (raw HID report tables per model plus
|
||||
`decode_gkey_state`, the report → pressed-bitmap decoder), `g910_device` (libusb
|
||||
open/detach/claim/read), `uinput_keyboard` (virtual keyboard), `action`
|
||||
(dispatch `key`/`command`/`disabled`), `config_watch` (inotify auto-reload of
|
||||
the mappings file) and `ipc_dbus` (the GDBus service). `shared/keymap` does the
|
||||
layout lookups for both sides. The hot path never touches the GUI: D-Bus is
|
||||
control plane only and is pumped once per loop.
|
||||
- G-key decoding is **state-based**, not per-key matching: the macro report holds
|
||||
a bitmap (byte 4 = G1–G8, low bit of byte 5 = G9), so the daemon fires a key on
|
||||
a 0 → 1 transition. That is what suppresses the repeats of a held key and makes
|
||||
a chord fire every key in it. Media/memory keys and command/LED answers are
|
||||
rejected by report length and header; keep it that way instead of
|
||||
re-introducing exact-report matching.
|
||||
- The daemon runs **without** a keyboard: `open()` failing (or a read error) only
|
||||
logs the absence once, reports `DeviceChanged(false)` and the loop retries every
|
||||
2 s while D-Bus and the config watch keep running. Never make it exit on a
|
||||
missing device — that would drop the bus name and make the GUI look like the
|
||||
daemon died. `GKeyDevice::open(quiet)` exists so those retries stay out of the
|
||||
journal (libusb-level errors always log).
|
||||
- `make install` (PREFIX default `/usr/local`) also installs the packaging
|
||||
files from `data/`: the udev rule, the `modules-load.d` fragment, the
|
||||
systemd **user** unit and the D-Bus activation file. Both contain an
|
||||
@@ -63,7 +76,10 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
|
||||
fallback. A failed reload keeps the previous mappings on purpose.
|
||||
- `main.cpp` keeps the `rows` vector (one `KeyRow` per G key, widget pointers)
|
||||
in an anonymous namespace; `src/mappings.cpp` is GUI glue that moves data
|
||||
between the widgets and the shared `MappingSet`. App id:
|
||||
between the widgets and the shared `MappingSet`. `build_ui()` loads
|
||||
`default_config_path()` into the rows as soon as they exist (a missing file is
|
||||
not an error), so a save from a fresh window rewrites the file instead of
|
||||
emptying it. App id:
|
||||
`dev.rager.gkeymapper` (`ipc::kAppId`); the About dialog uses `ipc::kVersion`.
|
||||
- GUI ↔ daemon is D-Bus on the session bus, GDBus (GIO) on both sides:
|
||||
`src/daemonclient.{h,cpp}` is the client, `daemon/ipc_dbus.{h,cpp}` the
|
||||
@@ -193,6 +209,23 @@ over any installed file; the file lookup remains only as a safety net.
|
||||
`take_change()` per simulated loop iteration), and `ConfigReloaded` on the
|
||||
`gdbus monitor` above reports when a save was applied. A GUI save triggers two
|
||||
reloads on purpose — the file watch and the explicit `Reload()` call.
|
||||
- Decoding and the whole loop are also testable **without** the keyboard, which is
|
||||
the fastest way to check hot-plug and the GUI footer states (plan §9):
|
||||
- compile `daemon/device_tables.cpp` standalone and feed `decode_gkey_state()`
|
||||
synthetic reports: single keys, a chord, a repeated (held) report, the release
|
||||
report, and the families that must be rejected (memory keys `11 ff 09` /
|
||||
`11 ff 0a`, command `11 ff 08 2e` and LED answers); cover `decode_media_key()`
|
||||
with the `02 <bit>` presses, the release, a padded report and the same
|
||||
families, which must *not* decode as media keys (see the bit table above);
|
||||
- build the daemon with a **fake** `GKeyDevice`: keep the header, script
|
||||
`open()`/`read_report()`, link `daemon/*.cpp` **without** `g910_device.cpp`
|
||||
and without libusb. That runs the real loop, so it covers "no keyboard at
|
||||
start", "device lost" and the reopen, emits real D-Bus signals and never
|
||||
claims the real device — nothing touches the keyboard and no key is injected
|
||||
into the focused window. Use a config with `command:` mappings only (a helper
|
||||
script that dumps its environment is also how you check that a `command`
|
||||
action sees `DISPLAY`); with `key:` mappings the fake would inject keys into
|
||||
whatever window is focused.
|
||||
- D-Bus activation is only testable in a real setup: the installed
|
||||
`share/dbus-1/services/dev.rager.gkeymapper.Daemon.service` needs
|
||||
`sudo make install`. For a quick check without root, drop a file with
|
||||
@@ -207,8 +240,23 @@ over any installed file; the file lookup remains only as a safety net.
|
||||
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.
|
||||
`/dev/uinput` keyboard. Warn the user first: `key` mappings are injected into
|
||||
the **focused** window. The media keys are *not* affected: the keyboard reports
|
||||
mute/volume/play only on that very interface (`…/input1`, measured — see below),
|
||||
so the daemon replays those reports unchanged through its virtual keyboard.
|
||||
Deleting that passthrough in `daemon/main.cpp` silently kills the media keys
|
||||
while the daemon runs.
|
||||
- Media keys cannot be observed over the keyboard's usual event node: with the
|
||||
daemon stopped they appear on `event11` (`Phys …/input1`, created when `usbhid`
|
||||
owns the interface), not on `event10` (`…/input0`); *both* nodes advertise the
|
||||
consumer usages in their HID descriptors, so the capability bitmap proves
|
||||
nothing. The authoritative data is the raw report on the `hidraw` node of
|
||||
interface `1.1` (`usb-…-5.4.3/input1`, exists only while `usbhid` owns it):
|
||||
`02 <bit>` per press with bit 0 `01` next, `02` previous, `04` stop, `08`
|
||||
play/pause, `10` volume up, `20` volume down, `40` mute, and `02 00` for the
|
||||
release. Verify the passthrough by reading `/tmp/daemon.log`: a press logs
|
||||
`media -> mute` / `media -> volume up` / … and the desktop reacts as usual.
|
||||
The bit table lives in `g910MediaKeys()` (`daemon/device_tables.cpp`).
|
||||
- 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
|
||||
@@ -229,10 +277,16 @@ The footer's daemon line is a plain `GtkLabel`, so its text can be read back ove
|
||||
AT-SPI: `<app> → frame → panel → label` whose name starts with the translated
|
||||
`Daemon:` string. To pick the right window, filter the application whose tree
|
||||
contains that label — a stale GUI from an earlier build may be running from the
|
||||
same binary name. Two notes for tests:
|
||||
- Clicking `Save mappings` writes `~/.config/gKeyMapper/mappings.conf` for real
|
||||
and the GUI does **not** load that file at startup, so a save from an
|
||||
unconfigured window replaces the user's mappings with empty ones. Back the file
|
||||
up first (or skip the click and call `Reload` over D-Bus instead).
|
||||
same binary name. Three notes for tests:
|
||||
- Clicking `Save mappings` writes `~/.config/gKeyMapper/mappings.conf` for real.
|
||||
Since M5 the window loads that file at startup, so the rows show the saved
|
||||
mappings and a save rewrites the same content; back the file up anyway (or skip
|
||||
the click and call `Reload` over D-Bus instead).
|
||||
- The `GtkEntry` texts are **not** readable over AT-SPI here (their text interface
|
||||
answers with an empty string), so verify what the rows hold with a screenshot:
|
||||
`import -window $(wmctrl -l | grep 'gKeyMapper – Logitech' | awk '{print $1}')
|
||||
/tmp/gui.png`. The footer label works over AT-SPI (its accessible *name* is the
|
||||
text), but a screenshot of the cropped footer is easier and shows what the user
|
||||
sees.
|
||||
- `XTest`-synthesized clicks need the window mapped and focused; the `Save`
|
||||
button is `push button` + name `Mappings speichern` in the German locale.
|
||||
|
||||
Reference in New Issue
Block a user