13 KiB
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-devalready pulls them in, but they are needed formake daemonon its own)
Install dependencies
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:
cd /home/trager/Nextcloud/Projekte/C++/gKeyMapper
(Alternatively clone the repository or copy the project folder to your own location.)
Build
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
make translations
Run
Normal:
./gkeymapper
With explicit language files (useful for testing from the build directory):
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. 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.
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):
./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 installships 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 theinput/uinputgroup. -
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. -
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/SIGUSR1reload it on demand, which is also the fallback on systems without inotify:kill -HUP $(pgrep -x gkeymapper-daem)Note: the kernel truncates the process name to 15 characters, so
pgrep -x gkeymapper-daemondoes not match. -
A failed reload keeps the previous mappings, so a transient read error never silently disables every key. The GUI shows it as
Configuration errorin 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 foundand 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).
-
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+zmeans the key that produceszon 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+7types{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. It inherits the environment of the user service, i.e. of your desktop session —DISPLAY,XAUTHORITYandPATHare already there on a normal setup. If a command cannot reach the display, export them into the user manager:systemctl --user import-environment DISPLAY WAYLAND_DISPLAY XAUTHORITY systemctl --user show-environment | grep DISPLAY
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.
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).Reloadre-reads the mappings file;ConfigReloaded(ok)reports the result.TestMappingfires 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)
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:
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
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 asshift+ssharpand not as the shifted symbolquestion. - AltGr (level 3) is recorded as the
altgrmodifier plus the base key, soAltGr+7 becomesaltgr+7. - The live preview shows the sequence.
Clearresets it,Donewrites it into the row,Canceldiscards it.
The sequence uses the same format as
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:
sudo apt install -y libgtk-4-dev
Then verify:
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:
export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig:/usr/lib/pkgconfig
Planned next steps
- Make the media keys remappable (they are forwarded unchanged today, see above) and support the M1–M3 / MR keys of the G910.
typeout: type out a fixed text, like the reference project's action of that name (the daemon would resolve the text through the active layout).- Profiles: swap mapping sets on the fly (the G910's M keys), which is what the
reserved
SetProfileD-Bus method was meant for. - Key sequence timing: per-step delays for recorded macros.