Embedd assets icons
This commit is contained in:
140
AGENTS.md
140
AGENTS.md
@@ -10,16 +10,18 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
|
||||
## Build / run / verify
|
||||
- `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).
|
||||
`libusb-1.0-0-dev`, `libxkbcommon-dev` and `libglib2.0-dev` (see README for
|
||||
`apt install` on a fresh machine). `libglib2.0-dev` also provides
|
||||
`glib-compile-resources`, which generates the icons embedded into the GUI.
|
||||
- `make daemon` — builds only `gkeymapper-daemon` (needs libusb, xkbcommon and
|
||||
gio-2.0, not GTK).
|
||||
- `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.
|
||||
`GKEYMAPPER_ASSETS_DIR=$PWD/assets` so the working-tree translations are used.
|
||||
Plain `./gkeymapper` works too (assets fall back to `./assets`). The window
|
||||
icon itself is compiled into the binary, so it never depends on these.
|
||||
- `make clean` — deletes the binaries and the generated `locale/` and `build/`
|
||||
trees.
|
||||
- No tests and no CI. Verify changes by building and running the app.
|
||||
|
||||
## i18n — easy to get wrong
|
||||
@@ -37,20 +39,24 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
|
||||
- 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.
|
||||
key-name table (lowercase GDK names -> Linux `KEY_*`), `shared/ipc_ids.h` the
|
||||
app id, the version and the D-Bus names both sides use.
|
||||
- The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp`
|
||||
+ 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`) 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.
|
||||
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.
|
||||
- `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`.
|
||||
files from `data/`: the udev rule, the `modules-load.d` fragment, the
|
||||
systemd **user** unit and the D-Bus activation file. Both contain 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
|
||||
@@ -58,7 +64,27 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
|
||||
- `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:
|
||||
`com.example.gkeymapper`.
|
||||
`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
|
||||
service. The daemon owns `dev.rager.gkeymapper.Daemon`, **not** the plain app
|
||||
id — `GtkApplication` owns that itself to detect a second GUI instance, so a
|
||||
daemon on the same name would make every GUI start look like a duplicate and
|
||||
exit. All names live in `shared/ipc_ids.h`.
|
||||
- The GUI never does a blocking D-Bus call: `DaemonClient` uses async calls and
|
||||
signal subscriptions, which arrive on the GTK main loop, and it follows
|
||||
`NameOwnerChanged` so the footer reacts when the daemon starts or stops.
|
||||
- `DaemonService::pump()` dispatches messages once per daemon loop iteration, so
|
||||
a control call is answered within one read timeout (≤500 ms) and outgoing
|
||||
signals are flushed then too. Keep it off the keypress path.
|
||||
- The daemon refuses to start when the name is taken (`DO_NOT_QUEUE` + exit 0
|
||||
with a message): two instances would fight over the device and create two
|
||||
virtual keyboards. That check runs **before** the device is opened.
|
||||
- The daemon status line is a plain `GtkLabel` in the window footer, updated with
|
||||
a single `gtk_label_set_text()`. It is deliberately **not** a `GtkStatusbar`:
|
||||
that needs message pop/push bookkeeping per context id, and GTK4 exposes it
|
||||
over AT-SPI as a generic `filler`, not as a "statusbar" role, so it buys
|
||||
nothing here.
|
||||
- The Type dropdown index is meaningful everywhere: `0=Key`, `1=Command`,
|
||||
`2=Disabled`, matching `ActionType` and the `key`/`command`/`disabled` tokens
|
||||
in the file. Keep that order.
|
||||
@@ -113,9 +139,20 @@ tracks `ISO_Level3_Shift` / `Mode_switch` through its own key press/release
|
||||
`kModAltGr` maps to `KEY_RIGHTALT`.
|
||||
|
||||
## 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.
|
||||
The icons (`assets/icon.png`, `assets/icon.svg`) are compiled into the GUI:
|
||||
`data/icons.gresource.xml` is turned into the generated `build/gresources.c` by
|
||||
`glib-compile-resources` and linked into `gkeymapper`, so the window has its icon
|
||||
with nothing installed. The resource prefix mirrors the app id
|
||||
(`/dev/rager/gkeymapper`) and the `icons/` subtree mirrors the hicolor layout, so
|
||||
`gtk_icon_theme_add_resource_path()` also answers
|
||||
`gtk_window_set_icon_name("gkeymapper")` from the binary. The daemon embeds no
|
||||
resource (it has no UI).
|
||||
|
||||
`make_icon_image()` and the About dialog take the embedded copy first and fall
|
||||
back to `find_asset()`, which resolves `$GKEYMAPPER_ASSETS_DIR` → `./assets` →
|
||||
compiled-in `ASSETS_DIR` (`/usr/local/share/gkeymapper/assets`). Because the
|
||||
resource is built from `assets/` it cannot go stale, and it deliberately wins
|
||||
over any installed file; the file lookup remains only as a safety net.
|
||||
|
||||
## Driving the daemon for manual checks
|
||||
- The process name is truncated by the kernel to 15 chars, so `pkill -x
|
||||
@@ -130,15 +167,44 @@ compiled-in `ASSETS_DIR` (`/usr/local/share/gkeymapper/assets`). The icon
|
||||
(`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.
|
||||
- If the systemd user service is enabled, **stop it first**
|
||||
(`systemctl --user stop gkeymapper-daemon`) before starting the repo build by
|
||||
hand: the daemon refuses to run when the bus name is taken (a second instance
|
||||
would fight over the device). Restore it afterwards with
|
||||
`systemctl --user start gkeymapper-daemon`. Note that the installed unit runs
|
||||
`/usr/local/bin/gkeymapper-daemon`, so *that* binary is the old one until the
|
||||
next `sudo make install`.
|
||||
- D-Bus control channel (the daemon owns `dev.rager.gkeymapper.Daemon`):
|
||||
|
||||
```bash
|
||||
D=dev.rager.gkeymapper.Daemon; O=/dev/rager/gkeymapper/Daemon
|
||||
gdbus call --session --dest $D --object-path $O --method $D.GetStatus
|
||||
gdbus call --session --dest $D --object-path $O --method $D.Reload
|
||||
gdbus call --session --dest $D --object-path $O --method $D.TestMapping 0
|
||||
gdbus monitor --session --dest $D # KeyPressed / ConfigReloaded / ...
|
||||
```
|
||||
|
||||
`TestMapping` runs the same path as a real keypress, so it verifies a mapping
|
||||
without hardware (index is 0-based, `0` = G1). A `TestMapping` for a
|
||||
`disabled:`/empty row fails with `G_n_ has no usable mapping`. `busctl --user`
|
||||
also works.
|
||||
- The config watch is testable without the device: `daemon/config_watch.cpp` has
|
||||
a standalone test shape (create a `ConfigWatcher`, `poll_events()` then
|
||||
`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.
|
||||
- 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
|
||||
`Exec=<repo>/gkeymapper-daemon` (no `SystemdService=`) into
|
||||
`~/.local/share/dbus-1/services/` — the session bus picks it up without a
|
||||
restart — then call a method with no daemon running and watch it appear.
|
||||
Remove that file afterwards, or it shadows the installed one.
|
||||
- `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
|
||||
`/tmp/stage/usr/local/lib/...` and
|
||||
`/tmp/stage/usr/local/share/dbus-1/services/...` to verify the `@BINDIR@`
|
||||
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
|
||||
@@ -158,3 +224,15 @@ 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.
|
||||
|
||||
The footer's daemon line is a plain `GtkLabel`, so its text can be read back over
|
||||
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).
|
||||
- `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