Files
gKeyMapper/README.md

451 lines
20 KiB
Markdown
Raw Normal View History

2026-10-16 22:11:52 +02:00
# gKeyMapper
2026-10-18 18:26:06 +02:00
![C++17](https://img.shields.io/badge/C%2B%2B-17-00599C?logo=cplusplus&logoColor=white)
![GTK 4](https://img.shields.io/badge/GTK-4-7FE719?logo=gtk&logoColor=black)
![GLib/GIO](https://img.shields.io/badge/GLib%2FGIO-D--Bus-4A86CF)
![libusb](https://img.shields.io/badge/libusb-1.0-c8a200)
![xkbcommon](https://img.shields.io/badge/xkbcommon-layout--aware-6E4C13)
![i18n](https://img.shields.io/badge/i18n-de%20%7C%20en%20%7C%20es%20%7C%20fr-blueviolet)
![systemd](https://img.shields.io/badge/systemd-user%20service-30D475?logo=systemd&logoColor=white)
![Platform](https://img.shields.io/badge/platform-Linux%20Mint%20%7C%20Ubuntu-FCC624?logo=linux&logoColor=black)
2026-10-16 22:11:52 +02:00
GTK4 application (C++) for remapping the G keys **G1–G9** of a Logitech **G910**
to other keys or commands (Linux Mint / Ubuntu).
2026-10-18 02:57:29 +02:00
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`.
2026-10-16 22:11:52 +02:00
2026-10-18 18:26:06 +02:00
**Only the daemon has to run.** It is the part that reads the G keys and replays
the action, and it runs as a systemd user service. The window is for editing the
mappings and for the status: it may be closed at any time, and starting it with
the session (`Options` → `Start with the session`) is a convenience, not a
requirement.
2026-10-16 22:11:52 +02:00
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
2026-10-17 03:53:51 +02:00
- libusb 1.0 development files (for the daemon)
- libxkbcommon development files (for the daemon's layout handling)
2026-10-18 02:57:29 +02:00
- 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)
2026-10-16 22:11:52 +02:00
### Install dependencies
```bash
sudo apt update
2026-10-18 02:57:29 +02:00
sudo apt install -y build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev libglib2.0-dev
2026-10-16 22:11:52 +02:00
```
2026-10-18 02:57:29 +02:00
Afterwards `pkg-config --modversion gtk4`, `pkg-config --modversion libusb-1.0`
and `pkg-config --modversion gio-2.0` should print versions.
2026-10-16 22:11:52 +02:00
## Setting up the project
Change to the project directory:
```bash
2026-10-18 23:03:47 +02:00
cd gKeyMapper
2026-10-16 22:11:52 +02:00
```
2026-10-18 23:03:47 +02:00
(Use the path where you cloned or copied the repository.)
2026-10-16 22:11:52 +02:00
## Build
```bash
make
```
2026-10-17 03:53:51 +02:00
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
2026-10-18 02:57:29 +02:00
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.
2026-10-16 22:11:52 +02:00
2026-10-18 22:53:39 +02:00
A `.deb` package can be built with
```bash
make deb
```
which produces `gkeymapper_<version>_<arch>.deb`. Installing it runs
`systemctl --user daemon-reload` and `systemctl --user enable --now gkeymapper-daemon`
for the installing user.
2026-10-16 22:11:52 +02:00
## Rebuild translations
```bash
make translations
```
2026-10-18 20:26:22 +02:00
Adding or changing a translation, adding a language, and the checks around them
are described in [po/README.md](po/README.md). `scripts/i18n.sh` does the work and
is wired into the Makefile:
```bash
make pot # regenerate po/gkeymapper.pot from the sources
make i18n-check # pot, catalogs and the language list of the GUI
make i18n-status # per language: translated, fuzzy, untranslated
```
2026-10-16 22:11:52 +02:00
## 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.
2026-10-18 02:57:29 +02:00
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:
2026-10-18 03:50:33 +02:00
start or stop the daemon while the GUI is open and the line updates itself. Two
more states are reported explicitly: `Device: not found` (the daemon is up but
has no keyboard) and `Configuration error` (a reload failed, so the daemon kept
the mappings it had).
The window starts from the saved mappings: it loads
`~/.config/gKeyMapper/mappings.conf` when it opens, so `Save mappings` in a fresh
window rewrites the same content instead of emptying the file.
2026-10-18 02:57:29 +02:00
2026-10-18 16:59:51 +02:00
Only one window is open at a time, and it is the one you just started: a new
start closes the instance that is already running — including one from an older
build — and then shows its own window, so a stale window can never stay next to a
2026-10-18 20:26:22 +02:00
fresh one. The rows of the window it replaced come along, so an edit that was
typed but never saved is still there; an instance from an older build cannot hand
them over, and then the window shows the saved mappings. Saving itself stays
explicit either way: press `Save mappings`.
2026-10-18 16:59:51 +02:00
2026-10-18 18:26:06 +02:00
### Menu, tray icon and minimize to tray
The menu button in the title bar holds `Export configuration...` / `Import
configuration...`, `Options`, `Help` (also `F1`), `About` and `Quit` (`Ctrl+Q`).
* `Help` opens a one-page explanation of the nine rows, the four types, the
capture dialog, the daemon status and the tray.
* `Options` opens a small window with the GUI's own switches and the daemon in
detail: state, version, keyboard, the config file it reads and the last G key
that fired (the footer says the same in one line). The window is not modal, so
it keeps updating while it is open. One of its switches is the language
(`System language`, or a fixed `de`/`en`/`es`/`fr`): it switches the window, the
menus and the tray menu right away — nothing to restart, and rows that were not
saved yet are kept.
2026-10-18 20:26:22 +02:00
* `Start with the session` starts the window minimized in the tray when you log
in: it writes the usual autostart entry
(`~/.config/autostart/gkeymapper.desktop`, starting the binary that is running
with the `--start-hidden` argument) and removes it again when you switch it off;
an entry that the desktop's own startup tool has disabled counts as off, too. The
window then waits in the tray until its `Show`, and only a desktop that hosts that
icon can leave it hidden — one without a tray gets the window a few seconds after
login. Like the window itself this is a convenience: the G keys do not need it,
only the daemon has to run (see `Running the daemon` below).
2026-10-18 18:26:06 +02:00
`Minimize to tray` keeps gKeyMapper reachable from the panel: the window is
hidden instead of closed when you close or minimize it, and the tray icon's menu
shows it again (`Show`) or quits the program (`Close`). Closing still quits when
the option is off — or when no panel hosts the tray icon, because then there
would be no way back to the window. The option is remembered in
`~/.config/gKeyMapper/gui.conf`, a file of its own so `mappings.conf` stays a
pure mapping file. The same file holds the chosen language; with `System
language` (the default) the desktop's locale decides.
The tray icon is a StatusNotifierItem plus a `com.canonical.dbusmenu` menu over
GIO — the same protocol libxapp and libappindicator wrap, only without their GTK3
dependency (a GTK4 process must not load GTK3). It needs no package of its own and
works wherever the protocol is hosted: the Cinnamon panel (via `xapp-sn-watcher`),
KDE, Unity or GNOME with the AppIndicator extension. The icon travels as a pixmap
because the panel is a different process and cannot read the icons compiled into
the binary.
2026-10-17 03:53:51 +02:00
## 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.
2026-10-18 03:50:33 +02:00
- The media keys (mute / volume / play / next / previous / stop) keep working
while it runs. The keyboard delivers them on the vendor interface — as a second
kernel input device (`…/input1`, the "… G910 Keyboard" consumer device) when the
daemon is *not* running — so the daemon picks those reports up and forwards them
unchanged through its virtual keyboard. There is nothing to configure; they
behave exactly as they do without the daemon.
2026-10-17 03:53:51 +02:00
- 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
2026-10-18 03:50:33 +02:00
silently disables every key. The GUI shows it as `Configuration error` in the
footer.
- It does **not** need the keyboard to be present: when it is unplugged (or not
there yet when the session starts) the daemon keeps running, reports
`Device: not found` and looks for it again every 2 s. A re-plug needs no
restart and keeps the GUI connection.
- A G key fires when its report first shows it as pressed: holding it down does
not repeat the action, and two G keys held at once both fire (the report
carries the state of all nine keys, not one key per report).
2026-10-18 16:59:51 +02:00
- `keyhold` is the exception to that: its keys stay pressed for as long as the G
key is held, like a normal key (including the desktop's auto-repeat), so a game
or a push-to-talk sees a key that is down exactly as long as you hold the G key.
The keyboard's release report ends it. If the keyboard is unplugged or the
daemon stops while a key is held, the daemon lets it go, so nothing stays stuck.
Holding only makes sense for a single step: `keyhold:ctrl+w` is held,
`keyhold:ctrl+c,ctrl+v` is replayed as a tap (the log says so).
2026-10-18 02:57:29 +02:00
- 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.
2026-10-17 03:53:51 +02:00
- 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.
2026-10-18 03:50:33 +02:00
It inherits the environment of the user service, i.e. of your desktop session —
`DISPLAY`, `XAUTHORITY` and `PATH` are already there on a normal setup. If a
command cannot reach the display, export them into the user manager:
```bash
systemctl --user import-environment DISPLAY WAYLAND_DISPLAY XAUTHORITY
systemctl --user show-environment | grep DISPLAY
```
2026-10-17 03:53:51 +02:00
Stop it with Ctrl+C or `kill`; it reattaches the kernel driver on the way out.
2026-10-18 02:57:29 +02:00
### 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.
2026-10-16 22:11:52 +02:00
## Install (optional)
```bash
sudo make install
```
2026-10-17 03:53:51 +02:00
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`) |
2026-10-18 02:57:29 +02:00
| `share/dbus-1/services/dev.rager.gkeymapper.Daemon.service` | D-Bus activation: the GUI can start the daemon on demand |
2026-10-17 03:53:51 +02:00
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
```
2026-10-18 02:57:29 +02:00
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.
2026-10-17 03:53:51 +02:00
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.
2026-10-16 22:11:52 +02:00
2026-10-18 22:53:39 +02:00
## After installation
If the daemon is not running (the GUI footer says "Daemon: not reachable"):
```bash
systemctl --user daemon-reload
systemctl --user enable --now gkeymapper-daemon
```
The daemon needs the user's session for `/dev/uinput` and for the `DISPLAY` a
`command` action inherits, so it runs as a **user** service, not a system
service. If the commands above fail, check that your user session has systemd
running (`systemctl --user status`).
2026-10-16 22:11:52 +02:00
## Clean
```bash
make clean
```
2026-10-18 02:57:29 +02:00
Removes the binaries and the generated `locale/` and `build/` directories.
2026-10-16 22:11:52 +02:00
2026-10-17 02:42:09 +02:00
## 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.
2026-10-17 03:53:51 +02:00
- 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`.
2026-10-17 02:42:09 +02:00
- 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 `,`
2026-10-17 03:53:51 +02:00
(e.g. `ctrl+c,ctrl+v` or `shift+f4,super+t`). The modifier order is
`ctrl`, `alt`, `shift`, `super`, `altgr`; parsing accepts any order.
2026-10-17 02:42:09 +02:00
## 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:
2026-10-18 16:59:51 +02:00
G4=keyhold:ctrl+w
2026-10-17 02:42:09 +02:00
```
The same format is used by Export/Import. `key` holds a captured shortcut
2026-10-18 16:59:51 +02:00
sequence and fires it once, `command` a program path or command line,
`disabled` ignores the G key, and `keyhold` keeps a single shortcut pressed for
as long as the G key is held (in the GUI it is the `Key (hold)` entry in the
Type dropdown). 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.
2026-10-17 03:53:51 +02:00
`Save mappings` writes atomically (temporary file + `rename`), so the daemon
never sees a half-written file and the watch reloads exactly once per save.
2026-10-17 02:42:09 +02:00
2026-10-16 22:11:52 +02:00
## Project structure
```
2026-10-18 02:57:29 +02:00
src/main.cpp # app actions + GUI + daemon status footer
2026-10-17 03:53:51 +02:00
src/keyrow.h # KeyRow (one per G key)
src/mappings.{h,cpp} # GUI glue: config file <-> row widgets
2026-10-17 02:57:21 +02:00
src/capturedialog.{h,cpp} # key-capture dialog
2026-10-18 02:57:29 +02:00
src/daemonclient.{h,cpp} # D-Bus client for the daemon (status/reload)
2026-10-18 20:26:22 +02:00
src/singleinstance.{h,cpp} # one GUI only: a new start replaces a running one,
# taking the rows of its window along (unsaved edits)
2026-10-18 18:26:06 +02:00
src/trayicon.{h,cpp} # tray icon: StatusNotifierItem + dbusmenu over GIO
src/settings.{h,cpp} # gui.conf (window preferences, e.g. minimize to tray)
src/help.{h,cpp} # help window
src/optionsdialog.{h,cpp} # options window: switches + detailed daemon status
2026-10-17 03:53:51 +02:00
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
2026-10-18 02:57:29 +02:00
shared/ipc_ids.h # app id + D-Bus names shared by GUI and daemon
2026-10-17 03:53:51 +02:00
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
2026-10-18 16:59:51 +02:00
daemon/action.{h,cpp} # dispatch "key" / "command" / "disabled" / "keyhold"
2026-10-17 03:53:51 +02:00
daemon/config_watch.{h,cpp} # inotify watch on mappings.conf (auto-reload)
2026-10-18 02:57:29 +02:00
daemon/ipc_dbus.{h,cpp} # D-Bus service (status/reload/test/signals)
2026-10-17 03:53:51 +02:00
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)
2026-10-18 02:57:29 +02:00
data/dev.rager.gkeymapper.Daemon.service # D-Bus activation (session bus)
2026-10-17 03:53:51 +02:00
docs/daemon-plan.md # daemon plan (reference: g910-gkey-macro-support)
2026-10-18 20:26:22 +02:00
po/ # Translations (pot/po/mo targets, po/README.md)
scripts/i18n.sh # extract/update/check/status/todo/new for the catalogs
2026-10-16 22:11:52 +02:00
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
2026-10-18 16:59:51 +02:00
- The M1–M3 / MR keys of the G910 and profiles: swap mapping sets on the fly,
which is what the reserved `SetProfile` D-Bus method was meant for. Deliberately
left for later (see `docs/daemon-plan.md` §11).
2026-10-18 02:57:29 +02:00
- Key sequence timing: per-step delays for recorded macros.
2026-10-18 16:59:51 +02:00
- `typeout`: type out a fixed text, like the reference project's action of that
name — not decided yet (see §11).
## Not planned
- Remapping the media keys. The daemon forwards mute / volume / play / next /
previous / stop unchanged, and that is meant to stay that way (see the note
above): they are not G keys, so they get no rows in the GUI and no entry in the
config file.