2026-10-16 22:11:52 +02:00
|
|
|
|
# gKeyMapper
|
|
|
|
|
|
|
2026-10-18 18:26:06 +02:00
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|
|
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
|
|
|
|
|
|
cd /home/trager/Nextcloud/Projekte/C++/gKeyMapper
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
(Alternatively clone the repository or copy the project folder to your own location.)
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
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
|
|
|
|
|
|
fresh one. Row edits that were not saved are lost with the replaced window,
|
|
|
|
|
|
because saving is always explicit (`Save mappings`).
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
* `Start with the session` opens the window when you log in. It writes the usual
|
|
|
|
|
|
autostart entry (`~/.config/autostart/gkeymapper.desktop`, starting the binary
|
|
|
|
|
|
that is running) and removes it again when you switch it off; an entry that the
|
|
|
|
|
|
desktop's own startup tool has disabled counts as off, too. 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).
|
|
|
|
|
|
|
|
|
|
|
|
`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
|
|
|
|
|
|
|
|
|
|
## 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 16:59:51 +02:00
|
|
|
|
src/singleinstance.{h,cpp} # one GUI only: a new start closes a running instance
|
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-16 22:11:52 +02:00
|
|
|
|
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
|
|
|
|
|
|
|
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.
|