# gKeyMapper GTK4 application (C++) for remapping the G keys **G1–G9** of a Logitech **G910** to other keys or commands (Linux Mint / Ubuntu). The GUI is complete. The background daemon (`gkeymapper-daemon`) that reads the G-keys from the keyboard and replays the actions is implemented as well; the GUI and the daemon talk over a small D-Bus channel (status, reload, test) — see `docs/daemon-plan.md`. Documentation and source comments are written in English. ## Requirements - Linux Mint / Ubuntu - g++ (C++17) - pkg-config - gettext (for translations, `msgfmt`) - GTK 4 development files - libusb 1.0 development files (for the daemon) - libxkbcommon development files (for the daemon's layout handling) - GLib/GIO development files (for the GUI ↔ daemon D-Bus channel; `libgtk-4-dev` already pulls them in, but they are needed for `make daemon` on its own) ### Install dependencies ```bash sudo apt update sudo apt install -y build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev libglib2.0-dev ``` Afterwards `pkg-config --modversion gtk4`, `pkg-config --modversion libusb-1.0` and `pkg-config --modversion gio-2.0` should print versions. ## Setting up the project Change to the project directory: ```bash cd /home/trager/Nextcloud/Projekte/C++/gKeyMapper ``` (Alternatively clone the repository or copy the project folder to your own location.) ## Build ```bash make ``` This produces the `gkeymapper` GUI binary, the `gkeymapper-daemon` binary and the `.mo` translations under `locale/`. The daemon can also be built on its own with `make daemon`. The GUI has its icons (`assets/icon.png`, `assets/icon.svg`) compiled in (`data/icons.gresource.xml` → generated `build/gresources.c`), so the window icon works without an installed `assets/` directory. ## Rebuild translations ```bash make translations ``` ## Run Normal: ```bash ./gkeymapper ``` With explicit language files (useful for testing from the build directory): ```bash LANG=de_DE.UTF-8 GKEYMAPPER_LOCALEDIR=$PWD/locale ./gkeymapper ``` Supported languages: German, English, Spanish, French. The footer of the window shows the daemon state (`Daemon: running · Device: …` plus the last G key that fired, or `Daemon: not reachable`) and follows it live: start or stop the daemon while the GUI is open and the line updates itself. ## Running the daemon The daemon reads G1–G9 from the keyboard and replays the configured action (`key` through a virtual keyboard, `command` by launching the program): ```bash ./gkeymapper-daemon # uses ~/.config/gKeyMapper/mappings.conf ./gkeymapper-daemon /path/to.conf # or an explicit file ``` - It reaches the keyboard through libusb (raw USB HID, vendor interface 1) and creates a virtual keyboard via `/dev/uinput`. Both need access for your user; `make install` ships a udev rule (`TAG+="uaccess"`) for that. On Linux Mint the device already has such an ACL, otherwise install the rule or add your user to the `input`/`uinput` group. - While it runs, the kernel driver is detached from the vendor interface, so the media keys (mute / volume / play) may stop working until it exits — it reattaches the driver on shutdown. - The mappings file is **watched**: save in the GUI and the daemon picks the change up by itself (within about half a second), no restart and no signal. `SIGHUP`/`SIGUSR1` reload it on demand, which is also the fallback on systems without inotify: ```bash kill -HUP $(pgrep -x gkeymapper-daem) ``` Note: the kernel truncates the process name to 15 characters, so `pgrep -x gkeymapper-daemon` does **not** match. - A failed reload keeps the previous mappings, so a transient read error never silently disables every key. - Only one instance may run: a second one prints why it cannot get the bus name and exits, instead of fighting over the keyboard and creating a second virtual keyboard. - Key names are layout-independent: `key:ctrl+z` means the key that produces `z` **on your layout**. The layout is read from `/etc/default/keyboard` (`XKBLAYOUT`); without it, US key positions are used. Because the name is symbolic, the same entry keeps its meaning if the layout changes (as long as the layout has that key); a name the layout does not provide is skipped. - `altgr+...` presses AltGr (level 3), e.g. `key:altgr+7` types `{` on a German layout. - `command:...` is run through the shell, so arguments work (`command:/usr/bin/firefox --new-window`); a plain program path is also fine. Stop it with Ctrl+C or `kill`; it reattaches the kernel driver on the way out. ### Status and control over D-Bus The daemon owns the name `dev.rager.gkeymapper.Daemon` on the session bus. The GUI uses that channel for its footer line ("Daemon: running / not reachable", the device and the last G key that fired) and to apply a save immediately. Nothing on this channel is on the keypress path, so a GUI that is slow or absent never delays an action. ```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 / DeviceChanged ``` - `GetStatus` → `(version, running, device, configPath)`. - `Reload` re-reads the mappings file; `ConfigReloaded(ok)` reports the result. - `TestMapping` fires a mapping without touching the keyboard (useful to check a sequence). The index is **0-based**: `0` = G1 … `8` = G9. It runs the same code path as a real keypress. - `KeyPressed(index, action)` is emitted for every G key, real or tested. If no session bus is available the daemon still works — it just runs without the GUI channel and logs that. ## Install (optional) ```bash sudo make install ``` Installs, under `PREFIX` (default `/usr/local`): | Path | What | |---|---| | `bin/gkeymapper`, `bin/gkeymapper-daemon` | the GUI and the daemon | | `share/locale/*/LC_MESSAGES/gkeymapper.mo` | translations | | `share/gkeymapper/assets/`, `share/applications/`, `share/icons/` | icon, `.desktop` file, icons | | `lib/udev/rules.d/60-gkeymapper.rules` | lets your user reach the keyboard and `/dev/uinput` | | `lib/modules-load.d/gkeymapper.conf` | loads the `uinput` module at boot | | `lib/systemd/user/gkeymapper-daemon.service` | systemd user unit (binary path filled in from `PREFIX`) | | `share/dbus-1/services/dev.rager.gkeymapper.Daemon.service` | D-Bus activation: the GUI can start the daemon on demand | Then enable the daemon for your session — it then starts automatically at login: ```bash systemctl --user daemon-reload systemctl --user enable --now gkeymapper-daemon systemctl --user status gkeymapper-daemon ``` Enabling it is optional: the D-Bus activation file above lets the bus start the daemon as soon as the GUI asks for its status. With both in place the unit is used either way, so the daemon never runs twice. It is a **user** service on purpose: it needs your session for `/dev/uinput` and so that a `command` action inherits `DISPLAY`/`WAYLAND_DISPLAY`. The unit does not run as root. `make install` reloads the udev rules for you; on a fresh machine replug the keyboard (or reboot) once so the ACL applies. ## Clean ```bash make clean ``` Removes the binaries and the generated `locale/` and `build/` directories. ## Capturing key sequences For rows of type **Key**, the action button opens a capture dialog: - Press keys one after another; each press becomes one step. - Modifier keys (Ctrl, Alt, Shift, Super) held during a press are recorded as part of that step. - Each step stores the **base key** (the unshifted key), so `Shift`+ß is recorded as `shift+ssharp` and not as the shifted symbol `question`. - AltGr (level 3) is recorded as the `altgr` modifier plus the base key, so `AltGr`+7 becomes `altgr+7`. - The live preview shows the sequence. `Clear` resets it, `Done` writes it into the row, `Cancel` discards it. The sequence uses the same format as [g910-gkey-macro-support](https://github.com/JSubelj/g910-gkey-macro-support): modifiers are joined with `+`, steps are separated with `,` (e.g. `ctrl+c,ctrl+v` or `shift+f4,super+t`). The modifier order is `ctrl`, `alt`, `shift`, `super`, `altgr`; parsing accepts any order. ## Configuration format `Save mappings` writes `~/.config/gKeyMapper/mappings.conf`: ``` # gKeyMapper mappings (G1..G9) G1=key:ctrl+c,ctrl+v G2=command:/usr/bin/firefox G3=disabled: ``` The same format is used by Export/Import. `key` holds a captured shortcut sequence, `command` a program path or command line, `disabled` ignores the G key. These map to the `shortcut` / `run` / `nothing` hotkey types of the reference project; the native C++ daemon (`gkeymapper-daemon`) reads this file and replays the actions — no Python involved. `Save mappings` writes atomically (temporary file + `rename`), so the daemon never sees a half-written file and the watch reloads exactly once per save. ## Project structure ``` src/main.cpp # app actions + GUI + daemon status footer src/keyrow.h # KeyRow (one per G key) src/mappings.{h,cpp} # GUI glue: config file <-> row widgets src/capturedialog.{h,cpp} # key-capture dialog src/daemonclient.{h,cpp} # D-Bus client for the daemon (status/reload) shared/mapping.{h,cpp} # mappings format + shortcut parsing (GUI & daemon) shared/keys.{h,cpp} # canonical key names -> Linux key codes shared/keymap.{h,cpp} # layout-aware key lookups (xkbcommon), GUI + daemon shared/ipc_ids.h # app id + D-Bus names shared by GUI and daemon daemon/main.cpp # event loop: read G-keys, run the action daemon/device_tables.{h,cpp} # per-model raw HID report tables (G910) daemon/g910_device.{h,cpp} # libusb access to the vendor interface daemon/uinput_keyboard.{h,cpp}# virtual keyboard for "key" mappings daemon/action.{h,cpp} # dispatch "key" / "command" / "disabled" daemon/config_watch.{h,cpp} # inotify watch on mappings.conf (auto-reload) daemon/ipc_dbus.{h,cpp} # D-Bus service (status/reload/test/signals) data/60-gkeymapper.rules # udev rule (uaccess for device + /dev/uinput) data/gkeymapper-uinput.conf # modules-load.d fragment for uinput data/gkeymapper-daemon.service# systemd user unit (installed by make install) data/dev.rager.gkeymapper.Daemon.service # D-Bus activation (session bus) docs/daemon-plan.md # daemon plan (reference: g910-gkey-macro-support) po/ # Translations (pot/po/mo targets) Makefile # Build via pkg-config + gettext .vscode/tasks.json # VS Code tasks for make README.md ``` ## Troubleshooting ### `Package gtk4 was not found` The GTK 4 development files are missing. Install them: ```bash sudo apt install -y libgtk-4-dev ``` Then verify: ```bash pkg-config --modversion gtk4 ``` ### `pkg-config` cannot find `.pc` files (wrong search path) In some environments `PKG_CONFIG_PATH` must be set explicitly. Standard path on Ubuntu/Mint: ```bash export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig:/usr/lib/pkgconfig ``` ## Planned next steps - Support the media keys and the M1–M3 / MR keys of the G910. - Profiles: swap mapping sets on the fly (the G910's M keys), which is what the reserved `SetProfile` D-Bus method was meant for. - Key sequence timing: per-step delays for recorded macros.