Deamon management

This commit is contained in:
2026-10-17 03:53:51 +02:00
parent 335a4674d1
commit b77b4a3fc6
35 changed files with 2001 additions and 172 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
build/ build/
locale/ locale/
gkeymapper gkeymapper
gkeymapper-daemon
*.o *.o
*.so *.so

110
AGENTS.md
View File

@@ -1,15 +1,19 @@
# AGENTS.md # AGENTS.md
## What this is ## What this is
A pure GTK4 C++ GUI for remapping the G1–G9 keys of a Logitech G910. Today it A pure GTK4 C++ GUI plus a background daemon for remapping the G1–G9 keys of a
only edits and persists mappings: there is **no** hardware G-key detection and Logitech G910. The GUI edits and persists mappings; the daemon reads the G-keys
**no** command execution yet (a native daemon is the planned next step). The and replays the configured action. Hardware G-key detection and action execution
code is split into a few small translation units under `src/` (see below). live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
(common) and `daemon/`.
## Build / run / verify ## Build / run / verify
- `make` — builds the `gkeymapper` binary **and** the `.mo` catalogs. - `make` — builds the `gkeymapper` GUI, the `gkeymapper-daemon` and the `.mo`
Needs `g++` (C++17), `make`, `gettext`, `libgtk-4-dev` (see README for catalogs. Needs `g++` (C++17), `make`, `gettext`, `libgtk-4-dev`,
`apt install` on a fresh machine). `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).
- `make translations` — only runs `msgfmt` on the existing `.po` files. - `make translations` — only runs `msgfmt` on the existing `.po` files.
- `make run` — builds, then runs with `GKEYMAPPER_LOCALEDIR=$PWD/locale` and - `make run` — builds, then runs with `GKEYMAPPER_LOCALEDIR=$PWD/locale` and
`GKEYMAPPER_ASSETS_DIR=$PWD/assets` so the working-tree translations and `GKEYMAPPER_ASSETS_DIR=$PWD/assets` so the working-tree translations and
@@ -30,18 +34,34 @@ code is split into a few small translation units under `src/` (see below).
`po/de.po`. `po/de.po`.
## Code layout and conventions ## Code layout and conventions
- Modules under `src/` (shared API lives in `namespace gkeymapper`): - Modules under `src/` (GUI), `shared/` (used by both binaries) and `daemon/`
`keyrow.h` (`KeyRow`, `kNumKeys`), `mappings.{h,cpp}` (config file I/O), (the background service). Shared API lives in `namespace gkeymapper`.
`capturedialog.{h,cpp}` (key-capture dialog), `main.cpp` (app actions + UI). `shared/mapping` owns the mappings file format, `shared/keys` the canonical
- The Makefile globs sources (`$(wildcard src/*.cpp)`): a new `.cpp` is compiled key-name table (lowercase GDK names -> Linux `KEY_*`).
automatically, and `src/*.h` are prerequisites of the target. - 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.
- 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.
- `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`.
- 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
fallback. A failed reload keeps the previous mappings on purpose.
- `main.cpp` keeps the `rows` vector (one `KeyRow` per G key, widget pointers) - `main.cpp` keeps the `rows` vector (one `KeyRow` per G key, widget pointers)
in an anonymous namespace; mappings functions receive it as a parameter. in an anonymous namespace; `src/mappings.cpp` is GUI glue that moves data
`on_activate` registers the app actions `app.export-config`, between the widgets and the shared `MappingSet`. App id:
`app.import-config`, `app.about`; `build_ui` constructs the window. App id:
`com.example.gkeymapper`. `com.example.gkeymapper`.
- The Type dropdown index is meaningful everywhere: `0=Key`, `1=Command`, - The Type dropdown index is meaningful everywhere: `0=Key`, `1=Command`,
`2=Disabled`. Keep that order — import/export map against it. `2=Disabled`, matching `ActionType` and the `key`/`command`/`disabled` tokens
in the file. Keep that order.
- The capture dialog's `CaptureSession` is heap-allocated and freed only by the - The capture dialog's `CaptureSession` is heap-allocated and freed only by the
window's `"destroy"` handler. Always close the dialog with window's `"destroy"` handler. Always close the dialog with
`gtk_window_destroy` so the session is released. `gtk_window_destroy` so the session is released.
@@ -68,13 +88,69 @@ G3=disabled:
`<type>:<target>`, one line per key. `key` is a captured sequence (lowercase `<type>:<target>`, one line per key. `key` is a captured sequence (lowercase
`gdk_keyval_name` output; modifiers joined with `+`, steps joined with `,`), `gdk_keyval_name` output; modifiers joined with `+`, steps joined with `,`),
`command` is a program/file path, `disabled` is inactive. `command` is a program path or command line, `disabled` is inactive. The
canonical modifier order is `ctrl`, `alt`, `shift`, `super`, `altgr`
(`format_shortcut_step()`; parsing accepts any order).
Key names are **layout independent**: `shared/keymap` resolves a name like `z`
to the key that produces `z` on the active layout (read from
`/etc/default/keyboard`, `XKBLAYOUT`) via xkbcommon — on a German keyboard `z` is
evdev 21, not the US position 44. `shared/keys` maps names to the US positions
and is only the fallback. When you add key names, keep them lowercase GDK keysym
names. `parse_shortcut()` takes an optional resolver for this (the daemon passes
one).
The capture dialog stores the **base (level 0) key**: it uses
`Keymap::base_key_name(keycode)` instead of GDK's shifted keyval, so
`Shift`+ß becomes `shift+ssharp` (not `shift+question`, which would double the
shift). `format_shortcut_step()` — not the dialog — renders a step from a
`ModifierBit` mask, so the order stays in one place.
AltGr (level 3) is recorded as the `altgr` modifier plus the base key
(`AltGr`+7 → `altgr+7`). GTK4 dropped the `GDK_MOD5_MASK` name, so the dialog
tracks `ISO_Level3_Shift` / `Mode_switch` through its own key press/release
(`CaptureSession::altGrHeld`) instead of a state bit. On the replay side
`kModAltGr` maps to `KEY_RIGHTALT`.
## Assets ## Assets
`find_asset()` resolves in order: `$GKEYMAPPER_ASSETS_DIR` → `./assets` → `find_asset()` resolves in order: `$GKEYMAPPER_ASSETS_DIR` → `./assets` →
compiled-in `ASSETS_DIR` (`/usr/local/share/gkeymapper/assets`). The icon compiled-in `ASSETS_DIR` (`/usr/local/share/gkeymapper/assets`). The icon
(`assets/icon.png`) is used in the header bar and the About dialog. (`assets/icon.png`) is used in the header bar and the About dialog.
## Driving the daemon for manual checks
- The process name is truncated by the kernel to 15 chars, so `pkill -x
gkeymapper-daemon` and `pgrep -x gkeymapper-daemon` **never match**. Use
`pkill -x gkeymapper-daem` / `pgrep -x gkeymapper-daem` (or `pkill -f
'./gkeymapper-daemon'`).
- Run it in the background with `setsid nohup ./gkeymapper-daemon [config]
> /tmp/daemon.log 2>&1 < /dev/null &`; it survives across shell calls. It
takes an optional config path as `argv[1]` (default
`~/.config/gKeyMapper/mappings.conf`).
- `SIGHUP`/`SIGUSR1` reload the mappings
(`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.
- `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
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
not work, and `key` mappings are injected into the **focused** window.
- The keyboard is `046d:c32b` (also handle `046d:c335`); on this machine the USB
device and `/dev/uinput` already carry ACLs for the user, so no root is needed.
- Verify decoding by pressing G keys and reading `/tmp/daemon.log` (it logs
`G1 -> key:...` per press). Verify what the virtual keyboard emits at the X
level with `xinput test <keyboard-id>` (`gKeyMapper virtual keyboard` shows up
as both a pointer and a keyboard — use the **keyboard** id). X keycodes are
evdev codes + 8. There is no `xdotool`/`wtype`.
## Driving the GUI for manual checks ## Driving the GUI for manual checks
There is no `xdotool`/`wtype` on this machine, and the capture-dialog buttons There is no `xdotool`/`wtype` on this machine, and the capture-dialog buttons
(`Clear`/`Cancel`/`Done`) expose an empty AT-SPI accessible name — the text is in (`Clear`/`Cancel`/`Done`) expose an empty AT-SPI accessible name — the text is in

View File

@@ -1,6 +1,6 @@
# gKeyMapper - Build # gKeyMapper - Build
# Dependencies: g++, pkg-config, msgfmt, libgtk-4-dev # Dependencies: g++, pkg-config, msgfmt, libgtk-4-dev, libusb-1.0-0-dev, libxkbcommon-dev
# Installation: sudo apt install build-essential gettext libgtk-4-dev # Installation: sudo apt install build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev
CXX ?= g++ CXX ?= g++
CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -Wpedantic CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -Wpedantic
@@ -9,29 +9,48 @@ MSGFMT ?= msgfmt
GTK_CFLAGS := $(shell $(PKG_CONFIG) --cflags gtk4) GTK_CFLAGS := $(shell $(PKG_CONFIG) --cflags gtk4)
GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4) GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4)
USB_CFLAGS := $(shell $(PKG_CONFIG) --cflags libusb-1.0)
USB_LIBS := $(shell $(PKG_CONFIG) --libs libusb-1.0)
XKB_CFLAGS := $(shell $(PKG_CONFIG) --cflags xkbcommon)
XKB_LIBS := $(shell $(PKG_CONFIG) --libs xkbcommon)
PACKAGE := gkeymapper PACKAGE := gkeymapper
TARGET := gkeymapper TARGET := gkeymapper
SRC_DIR := src DAEMON := gkeymapper-daemon
SOURCES := $(wildcard $(SRC_DIR)/*.cpp) SRC_DIR := src
HEADERS := $(wildcard $(SRC_DIR)/*.h) SHARED_DIR := shared
PREFIX ?= /usr/local DAEMON_DIR := daemon
LOCALEDIR := $(PREFIX)/share/locale GUI_SOURCES := $(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(SHARED_DIR)/*.cpp)
GUI_HEADERS := $(wildcard $(SRC_DIR)/*.h) $(wildcard $(SHARED_DIR)/*.h)
DAEMON_SOURCES := $(wildcard $(DAEMON_DIR)/*.cpp) $(wildcard $(SHARED_DIR)/*.cpp)
DAEMON_HEADERS := $(wildcard $(DAEMON_DIR)/*.h) $(wildcard $(SHARED_DIR)/*.h)
PREFIX ?= /usr/local
BINDIR := $(PREFIX)/bin
LOCALEDIR := $(PREFIX)/share/locale
ASSETS_DIR := $(PREFIX)/share/$(PACKAGE)/assets ASSETS_DIR := $(PREFIX)/share/$(PACKAGE)/assets
UDEV_DIR := $(PREFIX)/lib/udev/rules.d
MODULES_DIR := $(PREFIX)/lib/modules-load.d
UNIT_DIR := $(PREFIX)/lib/systemd/user
CXXFLAGS += -DPACKAGE=\"$(PACKAGE)\" -DLOCALEDIR=\"$(LOCALEDIR)\" -DASSETS_DIR=\"$(ASSETS_DIR)\" CXXFLAGS += -DPACKAGE=\"$(PACKAGE)\" -DLOCALEDIR=\"$(LOCALEDIR)\" -DASSETS_DIR=\"$(ASSETS_DIR)\"
CXXFLAGS += -I$(SHARED_DIR)
# Translations # Translations
PO_FILES := $(wildcard po/*.po) PO_FILES := $(wildcard po/*.po)
LANGS := $(patsubst po/%.po,%,$(PO_FILES)) LANGS := $(patsubst po/%.po,%,$(PO_FILES))
MO_FILES := $(patsubst %,locale/%/LC_MESSAGES/$(PACKAGE).mo,$(LANGS)) MO_FILES := $(patsubst %,locale/%/LC_MESSAGES/$(PACKAGE).mo,$(LANGS))
.PHONY: all clean run install translations .PHONY: all clean run install translations daemon
all: $(TARGET) translations all: $(TARGET) translations $(DAEMON)
$(TARGET): $(SOURCES) $(HEADERS) $(TARGET): $(GUI_SOURCES) $(GUI_HEADERS)
$(CXX) $(CXXFLAGS) $(GTK_CFLAGS) -o $@ $(SOURCES) $(GTK_LIBS) $(CXX) $(CXXFLAGS) $(GTK_CFLAGS) $(XKB_CFLAGS) -o $@ $(GUI_SOURCES) $(GTK_LIBS) $(XKB_LIBS)
daemon: $(DAEMON)
$(DAEMON): $(DAEMON_SOURCES) $(DAEMON_HEADERS)
$(CXX) $(CXXFLAGS) $(USB_CFLAGS) $(XKB_CFLAGS) -o $@ $(DAEMON_SOURCES) $(USB_LIBS) $(XKB_LIBS)
translations: $(MO_FILES) translations: $(MO_FILES)
@@ -43,12 +62,13 @@ run: $(TARGET) translations
GKEYMAPPER_LOCALEDIR=$(CURDIR)/locale GKEYMAPPER_ASSETS_DIR=$(CURDIR)/assets ./$(TARGET) GKEYMAPPER_LOCALEDIR=$(CURDIR)/locale GKEYMAPPER_ASSETS_DIR=$(CURDIR)/assets ./$(TARGET)
clean: clean:
rm -f $(TARGET) rm -f $(TARGET) $(DAEMON)
rm -rf locale rm -rf locale
install: $(TARGET) translations install: $(TARGET) $(DAEMON) translations
install -d $(DESTDIR)$(PREFIX)/bin install -d $(DESTDIR)$(BINDIR)
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET) install -m 755 $(TARGET) $(DESTDIR)$(BINDIR)/$(TARGET)
install -m 755 $(DAEMON) $(DESTDIR)$(BINDIR)/$(DAEMON)
install -d $(DESTDIR)$(ASSETS_DIR) install -d $(DESTDIR)$(ASSETS_DIR)
install -m 644 assets/* $(DESTDIR)$(ASSETS_DIR)/ install -m 644 assets/* $(DESTDIR)$(ASSETS_DIR)/
install -d $(DESTDIR)$(PREFIX)/share/icons/hicolor/256x256/apps install -d $(DESTDIR)$(PREFIX)/share/icons/hicolor/256x256/apps
@@ -63,4 +83,20 @@ install: $(TARGET) translations
install -d $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES; \ install -d $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES; \
install -m 644 locale/$$lang/LC_MESSAGES/$(PACKAGE).mo \ install -m 644 locale/$$lang/LC_MESSAGES/$(PACKAGE).mo \
$(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES/$(PACKAGE).mo; \ $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES/$(PACKAGE).mo; \
done done
# udev: access to the vendor interface and /dev/uinput without root.
install -d $(DESTDIR)$(UDEV_DIR)
install -m 644 data/60-gkeymapper.rules $(DESTDIR)$(UDEV_DIR)/60-gkeymapper.rules
# Kernel module needed for the virtual keyboard.
install -d $(DESTDIR)$(MODULES_DIR)
install -m 644 data/gkeymapper-uinput.conf $(DESTDIR)$(MODULES_DIR)/$(PACKAGE).conf
# systemd user unit (@BINDIR@ is substituted with the configured PREFIX).
install -d $(DESTDIR)$(UNIT_DIR)
sed 's|@BINDIR@|$(BINDIR)|g' data/$(DAEMON).service > $(DESTDIR)$(UNIT_DIR)/$(DAEMON).service
chmod 644 $(DESTDIR)$(UNIT_DIR)/$(DAEMON).service
-command -v udevadm >/dev/null && udevadm control --reload-rules 2>/dev/null || true
-systemctl --user daemon-reload 2>/dev/null || true
@echo
@echo "Installed. Enable the daemon in your user session with:"
@echo " systemctl --user daemon-reload"
@echo " systemctl --user enable --now $(DAEMON)"

125
README.md
View File

@@ -3,7 +3,8 @@
GTK4 application (C++) for remapping the G keys **G1–G9** of a Logitech **G910** GTK4 application (C++) for remapping the G keys **G1–G9** of a Logitech **G910**
to other keys or commands (Linux Mint / Ubuntu). to other keys or commands (Linux Mint / Ubuntu).
The current focus is on the **GUI** only (no actual key capture/command execution yet). The GUI is complete. A background daemon (`gkeymapper-daemon`) that reads the
G-keys from the keyboard is in progress — see `docs/daemon-plan.md`.
Documentation and source comments are written in English. Documentation and source comments are written in English.
@@ -14,15 +15,18 @@ Documentation and source comments are written in English.
- pkg-config - pkg-config
- gettext (for translations, `msgfmt`) - gettext (for translations, `msgfmt`)
- GTK 4 development files - GTK 4 development files
- libusb 1.0 development files (for the daemon)
- libxkbcommon development files (for the daemon's layout handling)
### Install dependencies ### Install dependencies
```bash ```bash
sudo apt update sudo apt update
sudo apt install -y build-essential gettext libgtk-4-dev sudo apt install -y build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev
``` ```
Afterwards `pkg-config --modversion gtk4` should print a version (e.g. `4.6.x`). Afterwards `pkg-config --modversion gtk4` and
`pkg-config --modversion libusb-1.0` should print versions.
## Setting up the project ## Setting up the project
@@ -40,7 +44,9 @@ cd /home/trager/Nextcloud/Projekte/C++/gKeyMapper
make make
``` ```
This produces the `gkeymapper` binary and the `.mo` translations under `locale/`. 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`.
## Rebuild translations ## Rebuild translations
@@ -64,14 +70,78 @@ LANG=de_DE.UTF-8 GKEYMAPPER_LOCALEDIR=$PWD/locale ./gkeymapper
Supported languages: German, English, Spanish, French. Supported languages: German, English, Spanish, French.
## 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.
- 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.
## Install (optional) ## Install (optional)
```bash ```bash
sudo make install sudo make install
``` ```
Installs `gkeymapper` to `/usr/local/bin` and the `.mo` files to Installs, under `PREFIX` (default `/usr/local`):
`/usr/local/share/locale`.
| 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`) |
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
```
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 ## Clean
@@ -88,13 +158,18 @@ For rows of type **Key**, the action button opens a capture dialog:
- Press keys one after another; each press becomes one step. - Press keys one after another; each press becomes one step.
- Modifier keys (Ctrl, Alt, Shift, Super) held during a press are recorded - Modifier keys (Ctrl, Alt, Shift, Super) held during a press are recorded
as part of that step. 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 - The live preview shows the sequence. `Clear` resets it, `Done` writes it
into the row, `Cancel` discards it. into the row, `Cancel` discards it.
The sequence uses the same format as The sequence uses the same format as
[g910-gkey-macro-support](https://github.com/JSubelj/g910-gkey-macro-support): [g910-gkey-macro-support](https://github.com/JSubelj/g910-gkey-macro-support):
modifiers are joined with `+`, steps are separated with `,` modifiers are joined with `+`, steps are separated with `,`
(e.g. `ctrl+c,ctrl+v` or `shift+f4,super+t`). (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 ## Configuration format
@@ -108,18 +183,34 @@ G3=disabled:
``` ```
The same format is used by Export/Import. `key` holds a captured shortcut The same format is used by Export/Import. `key` holds a captured shortcut
sequence, `command` a program or file path, `disabled` ignores the G key sequence, `command` a program path or command line, `disabled` ignores the
(similar to the `shortcut` / `run` / `nothing` hotkey types of the G key. These map to the `shortcut` / `run` / `nothing` hotkey types of the
reference project above; a native C++ daemon for G-key detection and reference project; the native C++ daemon (`gkeymapper-daemon`) reads this file
replay is planned next, no Python involved). 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 ## Project structure
``` ```
src/main.cpp # app actions + GUI src/main.cpp # app actions + GUI
src/keyrow.h # KeyRow / kNumKeys src/keyrow.h # KeyRow (one per G key)
src/mappings.{h,cpp} # mappings.conf read/write src/mappings.{h,cpp} # GUI glue: config file <-> row widgets
src/capturedialog.{h,cpp} # key-capture dialog src/capturedialog.{h,cpp} # key-capture dialog
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
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)
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)
docs/daemon-plan.md # daemon plan (reference: g910-gkey-macro-support)
po/ # Translations (pot/po/mo targets) po/ # Translations (pot/po/mo targets)
Makefile # Build via pkg-config + gettext Makefile # Build via pkg-config + gettext
.vscode/tasks.json # VS Code tasks for make .vscode/tasks.json # VS Code tasks for make
@@ -153,7 +244,7 @@ export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig:
## Planned next steps ## Planned next steps
- Real capture of key presses (evdev/uinput or libinput). - Daemon ↔ GUI channel (D-Bus): reload on save and show the daemon status.
- Execution of commands when a G key is pressed. - Support the media keys and the M1–M3 / MR keys of the G910.
- Loading and saving profiles. - udev rule and systemd user service so the daemon can autostart.
- Autostart service / background daemon. - `make install` for the daemon binary, the udev rule and the service file.

68
daemon/action.cpp Normal file
View File

@@ -0,0 +1,68 @@
// gKeyMapper daemon - executes the mapping configured for a G key.
#include "action.h"
#include <cstdio>
#include <vector>
#include <unistd.h>
namespace gkeymapper::daemon {
bool ActionRunner::init() {
keymap_.init();
if (!keyboard_.open()) {
std::fprintf(stderr,
"gkeymapper-daemon: no virtual keyboard, 'key' mappings disabled\n");
return false;
}
return true;
}
void ActionRunner::run(const gkeymapper::Mapping& mapping) {
switch (mapping.type) {
case gkeymapper::ActionType::Key: {
std::vector<gkeymapper::ShortcutStep> steps;
// Resolve key names through the active keyboard layout.
gkeymapper::KeyResolver resolve = [this](const std::string& name) {
return keymap_.key_code(name);
};
if (!gkeymapper::parse_shortcut(mapping.target, steps, resolve)) {
std::fprintf(stderr, "gkeymapper-daemon: invalid shortcut '%s'\n",
mapping.target.c_str());
return;
}
keyboard_.play(steps);
break;
}
case gkeymapper::ActionType::Command:
run_command(mapping.target);
break;
case gkeymapper::ActionType::Disabled:
default:
break;
}
}
void ActionRunner::run_command(const std::string& command) {
if (command.empty()) {
return;
}
pid_t pid = fork();
if (pid < 0) {
std::fprintf(stderr, "gkeymapper-daemon: fork failed\n");
return;
}
if (pid == 0) {
// The field may be a bare program path (which may contain spaces) or a
// command line with arguments. Try the path directly first, then fall
// back to a shell so arguments and quoting also work.
execlp(command.c_str(), command.c_str(), static_cast<char*>(nullptr));
execl("/bin/sh", "sh", "-c", command.c_str(), static_cast<char*>(nullptr));
_exit(127);
}
// Parent: children are reaped by the SIGCHLD handler.
}
} // namespace gkeymapper::daemon

30
daemon/action.h Normal file
View File

@@ -0,0 +1,30 @@
// gKeyMapper daemon - executes the mapping configured for a G key.
#ifndef GKEYMAPPER_DAEMON_ACTION_H
#define GKEYMAPPER_DAEMON_ACTION_H
#include <string>
#include "keymap.h"
#include "mapping.h"
#include "uinput_keyboard.h"
namespace gkeymapper::daemon {
class ActionRunner {
public:
// Opens the virtual keyboard and the keyboard layout. Without the virtual
// keyboard, "key" mappings are skipped but "command" mappings still work.
bool init();
void run(const gkeymapper::Mapping& mapping);
private:
void run_command(const std::string& command);
UinputKeyboard keyboard_;
gkeymapper::Keymap keymap_;
};
} // namespace gkeymapper::daemon
#endif // GKEYMAPPER_DAEMON_ACTION_H

109
daemon/config_watch.cpp Normal file
View File

@@ -0,0 +1,109 @@
// gKeyMapper daemon - watches the mappings file for changes (inotify).
#include "config_watch.h"
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <unistd.h>
namespace gkeymapper::daemon {
namespace {
// IN_CLOSE_WRITE is the useful one (the writer closed the file); IN_MODIFY
// covers writers that hold the file open, IN_MOVED_TO/CREATE the atomic save.
constexpr uint32_t kMask = IN_CLOSE_WRITE | IN_MODIFY | IN_MOVED_TO | IN_CREATE;
std::string directory_of(const std::string& path) {
size_t slash = path.find_last_of('/');
if (slash == std::string::npos) {
return ".";
}
return slash == 0 ? std::string("/") : path.substr(0, slash);
}
std::string basename_of(const std::string& path) {
size_t slash = path.find_last_of('/');
return slash == std::string::npos ? path : path.substr(slash + 1);
}
} // namespace
ConfigWatcher::~ConfigWatcher() {
if (fd_ >= 0) {
::close(fd_);
}
}
bool ConfigWatcher::init(const std::string& path) {
name_ = basename_of(path);
directory_ = directory_of(path);
fd_ = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (fd_ < 0) {
std::fprintf(stderr,
"gkeymapper-daemon: inotify unavailable (%s); "
"use SIGHUP to reload\n",
std::strerror(errno));
return false;
}
int watch = inotify_add_watch(fd_, directory_.c_str(), kMask);
if (watch < 0 && errno == ENOENT) {
// First start before anything was ever saved: create the directory the
// GUI would create and try once more.
::mkdir(directory_.c_str(), 0755);
watch = inotify_add_watch(fd_, directory_.c_str(), kMask);
}
if (watch < 0) {
std::fprintf(stderr,
"gkeymapper-daemon: cannot watch %s (%s); "
"use SIGHUP to reload\n",
directory_.c_str(), std::strerror(errno));
::close(fd_);
fd_ = -1;
return false;
}
std::fprintf(stderr, "gkeymapper-daemon: watching %s for changes\n",
directory_.c_str());
return true;
}
void ConfigWatcher::poll_events() {
if (fd_ < 0) {
return;
}
alignas(inotify_event) char buffer[4096];
for (;;) {
ssize_t length = ::read(fd_, buffer, sizeof(buffer));
if (length <= 0) {
return; // nothing queued (EAGAIN) or a read error
}
for (ssize_t offset = 0; offset < length;) {
const auto* event = reinterpret_cast<const inotify_event*>(buffer + offset);
offset += static_cast<ssize_t>(sizeof(inotify_event)) + event->len;
// Only the configured file matters; the directory may hold others.
if (event->len == 0 || name_ == event->name) {
pending_ = true;
}
}
}
}
bool ConfigWatcher::take_change() {
if (!pending_) {
return false;
}
pending_ = false;
return true;
}
} // namespace gkeymapper::daemon

55
daemon/config_watch.h Normal file
View File

@@ -0,0 +1,55 @@
// gKeyMapper daemon - watches the mappings file for changes (inotify).
#ifndef GKEYMAPPER_DAEMON_CONFIG_WATCH_H
#define GKEYMAPPER_DAEMON_CONFIG_WATCH_H
#include <string>
namespace gkeymapper::daemon {
// Watches mappings.conf so a save in the GUI is picked up without a signal.
//
// The *directory* is watched, not the file: that also catches an atomic save
// (write a temporary file, rename it over the config) and the config appearing
// after it was deleted.
//
// poll_events() collects whatever happened since the last call and take_change()
// reports it once. Because the daemon evaluates this once per loop iteration
// (at least every read timeout, i.e. 500 ms), all events a single save produces
// collapse into one reload, and the file is complete by the time it is read
// (that is what the atomic save in shared/mapping.cpp guarantees).
//
// The daemon still accepts SIGHUP/SIGUSR1 as a fallback when inotify is
// unavailable.
class ConfigWatcher {
public:
ConfigWatcher() = default;
~ConfigWatcher();
ConfigWatcher(const ConfigWatcher&) = delete;
ConfigWatcher& operator=(const ConfigWatcher&) = delete;
// Watches the directory holding `path` (creating it if it does not exist
// yet). Returns false if inotify cannot be set up; the daemon then relies
// on signals only.
bool init(const std::string& path);
bool is_watching() const { return fd_ >= 0; }
// Consumes queued inotify events. Non-blocking and cheap; call it once per
// event-loop iteration.
void poll_events();
// True if the mappings file changed since the last call; clears the mark so
// every change is reported exactly once.
bool take_change();
private:
int fd_ = -1;
std::string directory_;
std::string name_;
bool pending_ = false;
};
} // namespace gkeymapper::daemon
#endif // GKEYMAPPER_DAEMON_CONFIG_WATCH_H

96
daemon/device_tables.cpp Normal file
View File

@@ -0,0 +1,96 @@
// gKeyMapper daemon - per-model raw HID report tables.
#include "device_tables.h"
#include <algorithm>
#include <initializer_list>
namespace gkeymapper::daemon {
namespace {
// The G910 reports are 20 bytes; set the given prefix and zero the rest.
Report report20(std::initializer_list<uint8_t> prefix) {
Report report(20, 0);
std::copy(prefix.begin(), prefix.end(), report.begin());
return report;
}
// G1..G9 press reports, ported from LogitechG910EventInterface.macroKeys.
std::vector<Report> g910GKeys() {
return {
report20({0x11, 0xff, 0x08, 0x00, 0x01}), // G1
report20({0x11, 0xff, 0x08, 0x00, 0x02}), // G2
report20({0x11, 0xff, 0x08, 0x00, 0x04}), // G3
report20({0x11, 0xff, 0x08, 0x00, 0x08}), // G4
report20({0x11, 0xff, 0x08, 0x00, 0x10}), // G5
report20({0x11, 0xff, 0x08, 0x00, 0x20}), // G6
report20({0x11, 0xff, 0x08, 0x00, 0x40}), // G7
report20({0x11, 0xff, 0x08, 0x00, 0x80}), // G8
report20({0x11, 0xff, 0x08, 0x00, 0x00, 0x01}), // G9
};
}
// "disable F-key to G-key binding" command (disableGKeys[0]).
std::vector<Report> g910DisableCommands() {
return {report20({0x11, 0xff, 0x08, 0x2e, 0x01})};
}
DeviceProfile make_g910(const char* name, uint16_t product, bool disableBinding) {
DeviceProfile profile;
profile.name = name;
profile.vendor = 0x046d;
profile.product = product;
profile.configuration = 0;
profile.interfaceNumber = 1;
profile.altSetting = 0;
profile.gKeyReports = g910GKeys();
profile.releaseReport = report20({0x11, 0xff, 0x08, 0x00});
profile.disableBinding = disableBinding;
if (disableBinding) {
profile.disableCommands = g910DisableCommands();
}
return profile;
}
} // namespace
const std::vector<DeviceProfile>& profiles() {
// Orion Spectrum (c335) needs the disable command; the Orion Spark (c32b)
// does not (mirrors disableGKeysInterface == 0 in the reference).
static const std::vector<DeviceProfile> list = {
make_g910("Logitech G910 Orion Spectrum", 0xc335, true),
make_g910("Logitech G910 Orion Spark", 0xc32b, false),
};
return list;
}
const DeviceProfile* find_profile(uint16_t vendor, uint16_t product) {
for (const DeviceProfile& profile : profiles()) {
if (profile.vendor == vendor && profile.product == product) {
return &profile;
}
}
return nullptr;
}
int match_gkey(const DeviceProfile& profile, const uint8_t* data, int length) {
if (length <= 0) {
return -1;
}
for (size_t i = 0; i < profile.gKeyReports.size(); ++i) {
const Report& report = profile.gKeyReports[i];
if (static_cast<int>(report.size()) == length &&
std::equal(report.begin(), report.end(), data)) {
return static_cast<int>(i);
}
}
return -1;
}
bool is_release_report(const DeviceProfile& profile, const uint8_t* data, int length) {
return static_cast<int>(profile.releaseReport.size()) == length &&
std::equal(profile.releaseReport.begin(), profile.releaseReport.end(), data);
}
} // namespace gkeymapper::daemon

47
daemon/device_tables.h Normal file
View File

@@ -0,0 +1,47 @@
// gKeyMapper daemon - per-model raw HID report tables.
//
// Ported from the byte tables of the Python reference project
// (g910_gkeys/data_mappers/supported_devices.py). Keys are matched against the
// exact bytes read from the keyboard's vendor interface.
#ifndef GKEYMAPPER_DAEMON_DEVICE_TABLES_H
#define GKEYMAPPER_DAEMON_DEVICE_TABLES_H
#include <cstdint>
#include <vector>
namespace gkeymapper::daemon {
// A raw HID report from the vendor interface.
using Report = std::vector<uint8_t>;
struct DeviceProfile {
const char* name = "";
uint16_t vendor = 0;
uint16_t product = 0;
int configuration = 0; // 0-based configuration index
int interfaceNumber = 1;
int altSetting = 0;
// gKeyReports[i] is the press report for G(i+1).
std::vector<Report> gKeyReports;
Report releaseReport;
// Some models must be told to stop mapping G-keys to F-keys first.
bool disableBinding = false;
std::vector<Report> disableCommands;
};
const std::vector<DeviceProfile>& profiles();
// Matching profile for vendor/product, or nullptr.
const DeviceProfile* find_profile(uint16_t vendor, uint16_t product);
// Index of the G key (0..N-1) for a report, or -1 if it is not a known press.
int match_gkey(const DeviceProfile& profile, const uint8_t* data, int length);
// True if the report is the "all macro keys released" report.
bool is_release_report(const DeviceProfile& profile, const uint8_t* data, int length);
} // namespace gkeymapper::daemon
#endif // GKEYMAPPER_DAEMON_DEVICE_TABLES_H

180
daemon/g910_device.cpp Normal file
View File

@@ -0,0 +1,180 @@
// gKeyMapper daemon - raw USB HID access to the keyboard's vendor interface.
#include "g910_device.h"
#include <cstdio>
// libusb.h uses zero-length arrays, which -Wpedantic flags.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#include <libusb.h>
#pragma GCC diagnostic pop
namespace gkeymapper::daemon {
GKeyDevice::~GKeyDevice() {
close();
}
bool GKeyDevice::open() {
if (handle_) {
return true;
}
if (libusb_init(&context_) != 0) {
std::fprintf(stderr, "gkeymapper-daemon: libusb_init failed\n");
return false;
}
libusb_device** devices = nullptr;
ssize_t count = libusb_get_device_list(context_, &devices);
if (count < 0) {
std::fprintf(stderr, "gkeymapper-daemon: no USB devices\n");
close();
return false;
}
libusb_device* target = nullptr;
for (ssize_t i = 0; i < count && !target; ++i) {
libusb_device_descriptor desc{};
if (libusb_get_device_descriptor(devices[i], &desc) != 0) {
continue;
}
if (find_profile(desc.idVendor, desc.idProduct) != nullptr) {
target = devices[i];
profile_ = find_profile(desc.idVendor, desc.idProduct);
}
}
if (!target) {
std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n");
libusb_free_device_list(devices, 1);
close();
return false;
}
if (libusb_open(target, &handle_) != 0) {
std::fprintf(stderr, "gkeymapper-daemon: could not open %s (permissions?)\n",
profile_->name);
libusb_free_device_list(devices, 1);
close();
return false;
}
libusb_free_device_list(devices, 1);
if (!find_endpoint()) {
close();
return false;
}
// Detach and claim the vendor interface, like the reference driver.
if (libusb_kernel_driver_active(handle_, profile_->interfaceNumber) == 1) {
if (libusb_detach_kernel_driver(handle_, profile_->interfaceNumber) == 0) {
detached_kernel_driver_ = true;
}
}
if (libusb_claim_interface(handle_, profile_->interfaceNumber) != 0) {
std::fprintf(stderr, "gkeymapper-daemon: could not claim interface %d\n",
profile_->interfaceNumber);
close();
return false;
}
claimed_interface_ = profile_->interfaceNumber;
std::fprintf(stderr, "gkeymapper-daemon: %s found (endpoint 0x%02x)\n",
profile_->name, endpoint_);
return true;
}
bool GKeyDevice::find_endpoint() {
libusb_device* device = libusb_get_device(handle_);
libusb_config_descriptor* config = nullptr;
if (libusb_get_config_descriptor(device, profile_->configuration, &config) != 0) {
return false;
}
bool found = false;
for (int i = 0; i < config->bNumInterfaces && !found; ++i) {
const libusb_interface& iface = config->interface[i];
for (int a = 0; a < iface.num_altsetting && !found; ++a) {
const libusb_interface_descriptor& desc = iface.altsetting[a];
if (desc.bInterfaceNumber == profile_->interfaceNumber &&
desc.bAlternateSetting == profile_->altSetting &&
desc.bNumEndpoints > 0) {
endpoint_ = desc.endpoint[0].bEndpointAddress;
found = true;
}
}
}
libusb_free_config_descriptor(config);
return found;
}
bool GKeyDevice::disable_binding() {
if (!handle_ || !profile_) {
return false;
}
if (!profile_->disableBinding) {
return true;
}
for (const Report& command : profile_->disableCommands) {
int written = libusb_control_transfer(handle_, 0x21, 0x09, 0x0212,
profile_->interfaceNumber,
const_cast<unsigned char*>(command.data()),
static_cast<uint16_t>(command.size()),
1000);
if (written < 0) {
std::fprintf(stderr, "gkeymapper-daemon: disable-binding write failed (%d)\n",
written);
return false;
}
}
// Drain the confirmation reports so they do not show up as key events.
uint8_t scratch[64];
for (int i = 0; i < 6; ++i) {
if (read_report(scratch, sizeof(scratch), 200) <= 0) {
break;
}
}
return true;
}
int GKeyDevice::read_report(uint8_t* buffer, int length, int timeoutMs) {
if (!handle_) {
return -1;
}
int transferred = 0;
int rc = libusb_interrupt_transfer(handle_, endpoint_, buffer, length,
&transferred, timeoutMs);
if (rc == 0) {
return transferred;
}
if (rc == LIBUSB_ERROR_TIMEOUT) {
return 0;
}
return rc; // negative
}
void GKeyDevice::close() {
if (handle_) {
if (claimed_interface_ >= 0) {
libusb_release_interface(handle_, claimed_interface_);
}
if (detached_kernel_driver_) {
libusb_attach_kernel_driver(handle_, profile_->interfaceNumber);
}
libusb_close(handle_);
}
handle_ = nullptr;
if (context_) {
libusb_exit(context_);
context_ = nullptr;
}
claimed_interface_ = -1;
detached_kernel_driver_ = false;
profile_ = nullptr;
}
} // namespace gkeymapper::daemon

50
daemon/g910_device.h Normal file
View File

@@ -0,0 +1,50 @@
// gKeyMapper daemon - raw USB HID access to the keyboard's vendor interface.
#ifndef GKEYMAPPER_DAEMON_GKEY_DEVICE_H
#define GKEYMAPPER_DAEMON_GKEY_DEVICE_H
#include <cstdint>
#include "device_tables.h"
struct libusb_context;
struct libusb_device_handle;
namespace gkeymapper::daemon {
class GKeyDevice {
public:
GKeyDevice() = default;
~GKeyDevice();
GKeyDevice(const GKeyDevice&) = delete;
GKeyDevice& operator=(const GKeyDevice&) = delete;
// Finds a supported keyboard, opens it and detaches the kernel driver from
// the vendor interface. Returns false (and logs) if none is found.
bool open();
void close();
bool is_open() const { return handle_ != nullptr; }
// Sends the "stop mapping G-keys to F-keys" command if the model needs it.
bool disable_binding();
// Reads one interrupt report. Returns the number of bytes (>0), 0 on
// timeout, or a negative value on error.
int read_report(uint8_t* buffer, int length, int timeoutMs);
const DeviceProfile* profile() const { return profile_; }
private:
bool find_endpoint();
libusb_context* context_ = nullptr;
libusb_device_handle* handle_ = nullptr;
const DeviceProfile* profile_ = nullptr;
uint8_t endpoint_ = 0;
int claimed_interface_ = -1;
bool detached_kernel_driver_ = false;
};
} // namespace gkeymapper::daemon
#endif // GKEYMAPPER_DAEMON_GKEY_DEVICE_H

118
daemon/main.cpp Normal file
View File

@@ -0,0 +1,118 @@
// gKeyMapper daemon - remaps G1..G9 on the Logitech G910.
//
// It reads the raw HID reports of the keyboard's vendor interface, looks up the
// configured action and replays it ("key" via uinput, "command" via exec).
//
// Milestones: M0 (shared core), M1 (device bring-up/decoding), M2 (action
// execution) and M3 (config watching + lifecycle) are done; the GUI channel
// (D-Bus) is still open (M4).
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
#include "action.h"
#include "config_watch.h"
#include "device_tables.h"
#include "g910_device.h"
#include "mapping.h"
namespace {
volatile std::sig_atomic_t g_running = 1;
volatile std::sig_atomic_t g_reload = 0;
void handle_stop(int /*signum*/) {
g_running = 0;
}
void handle_reload(int /*signum*/) {
g_reload = 1;
}
} // namespace
int main(int argc, char** argv) {
std::signal(SIGINT, handle_stop);
std::signal(SIGTERM, handle_stop);
std::signal(SIGHUP, handle_reload);
std::signal(SIGUSR1, handle_reload);
std::signal(SIGCHLD, SIG_IGN); // auto-reap launched commands
std::string configPath = gkeymapper::default_config_path();
if (argc > 1) {
configPath = argv[1];
}
gkeymapper::MappingSet mappings = gkeymapper::parse_mappings("");
// Reads the config; on failure the previous mappings stay in place, so a
// transient read error never silently disables every key.
auto reload = [&]() {
if (gkeymapper::load_mappings_file(configPath, mappings)) {
std::fprintf(stderr, "gkeymapper-daemon: loaded %s\n", configPath.c_str());
} else {
std::fprintf(stderr, "gkeymapper-daemon: cannot read %s\n", configPath.c_str());
}
};
reload();
// The GUI is the writer; a save is picked up through inotify, SIGHUP/SIGUSR1
// stay as the fallback for scripts and for systems without inotify.
gkeymapper::daemon::ConfigWatcher watcher;
watcher.init(configPath);
gkeymapper::daemon::ActionRunner actions;
actions.init();
gkeymapper::daemon::GKeyDevice device;
if (!device.open()) {
return 1;
}
device.disable_binding();
const gkeymapper::daemon::DeviceProfile* profile = device.profile();
uint8_t buffer[64];
while (g_running) {
// The loop wakes up at least every read timeout, which also bounds how
// long a config change takes to be applied (well under a second).
watcher.poll_events();
if (g_reload || watcher.take_change()) {
g_reload = 0;
reload();
}
int n = device.read_report(buffer, sizeof(buffer), 500);
if (n == 0) {
continue; // no report within the timeout
}
if (n < 0) {
std::fprintf(stderr, "gkeymapper-daemon: read error (%d), reopening\n", n);
device.close();
if (!device.open()) {
return 1;
}
device.disable_binding();
profile = device.profile();
continue;
}
int index = gkeymapper::daemon::match_gkey(*profile, buffer, n);
if (index >= 0 && index < static_cast<int>(mappings.keys.size())) {
const gkeymapper::Mapping& mapping = mappings.keys[index];
std::printf("G%d -> %s:%s\n", index + 1,
gkeymapper::action_type_token(mapping.type),
mapping.target.c_str());
std::fflush(stdout);
actions.run(mapping);
} else if (gkeymapper::daemon::is_release_report(*profile, buffer, n)) {
// Key released; nothing to do.
}
// Media keys and LED acknowledgements are ignored.
}
device.close();
return 0;
}

140
daemon/uinput_keyboard.cpp Normal file
View File

@@ -0,0 +1,140 @@
// gKeyMapper daemon - virtual keyboard for replaying "key" mappings.
#include "uinput_keyboard.h"
#include <cstdio>
#include <fcntl.h>
#include <initializer_list>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/input-event-codes.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include "keys.h"
namespace gkeymapper::daemon {
namespace {
// Key code for a single modifier bit (the "left" variants are enough).
unsigned short modifier_code(unsigned bit) {
switch (bit) {
case gkeymapper::kModCtrl: return KEY_LEFTCTRL;
case gkeymapper::kModAlt: return KEY_LEFTALT;
case gkeymapper::kModShift: return KEY_LEFTSHIFT;
case gkeymapper::kModSuper: return KEY_LEFTMETA;
case gkeymapper::kModAltGr: return KEY_RIGHTALT;
default: return 0;
}
}
} // namespace
UinputKeyboard::~UinputKeyboard() {
close();
}
bool UinputKeyboard::open() {
if (fd_ >= 0) {
return true;
}
fd_ = ::open("/dev/uinput", O_WRONLY | O_NONBLOCK);
if (fd_ < 0) {
std::fprintf(stderr, "gkeymapper-daemon: cannot open /dev/uinput (permissions?)\n");
fd_ = -1;
return false;
}
ioctl(fd_, UI_SET_EVBIT, EV_KEY);
ioctl(fd_, UI_SET_EVBIT, EV_SYN);
for (int code = 0; code <= KEY_MAX; ++code) {
ioctl(fd_, UI_SET_KEYBIT, code);
}
uinput_user_dev device{};
std::snprintf(device.name, UINPUT_MAX_NAME_SIZE, "gKeyMapper virtual keyboard");
device.id.bustype = BUS_USB;
device.id.vendor = 0x046d;
device.id.product = 0xc32b;
device.id.version = 1;
if (::write(fd_, &device, sizeof(device)) < 0) {
std::fprintf(stderr, "gkeymapper-daemon: uinput setup failed\n");
close();
return false;
}
if (ioctl(fd_, UI_DEV_CREATE) < 0) {
std::fprintf(stderr, "gkeymapper-daemon: UI_DEV_CREATE failed\n");
close();
return false;
}
return true;
}
void UinputKeyboard::close() {
if (fd_ >= 0) {
ioctl(fd_, UI_DEV_DESTROY);
::close(fd_);
fd_ = -1;
}
}
void UinputKeyboard::emit(unsigned short type, unsigned short code, int value) {
input_event event{};
event.type = type;
event.code = code;
event.value = value;
// A short write is not recoverable here; drop the event.
ssize_t written = ::write(fd_, &event, sizeof(event));
(void)written;
}
void UinputKeyboard::sync() {
emit(EV_SYN, SYN_REPORT, 0);
}
void UinputKeyboard::key_event(unsigned short code, bool down) {
emit(EV_KEY, code, down ? 1 : 0);
}
void UinputKeyboard::play(const std::vector<gkeymapper::ShortcutStep>& steps) {
if (fd_ < 0) {
return;
}
for (const gkeymapper::ShortcutStep& step : steps) {
std::vector<unsigned short> modifiers;
for (unsigned bit : {gkeymapper::kModCtrl, gkeymapper::kModAlt,
gkeymapper::kModShift, gkeymapper::kModSuper,
gkeymapper::kModAltGr}) {
if ((step.modifiers & bit) != 0) {
unsigned short code = modifier_code(bit);
if (code != 0) {
modifiers.push_back(code);
}
}
}
for (unsigned short code : modifiers) {
key_event(code, true);
}
if (step.linux_key >= 0) {
key_event(static_cast<unsigned short>(step.linux_key), true);
}
sync();
usleep(2000);
if (step.linux_key >= 0) {
key_event(static_cast<unsigned short>(step.linux_key), false);
}
for (auto it = modifiers.rbegin(); it != modifiers.rend(); ++it) {
key_event(*it, false);
}
sync();
usleep(2000);
}
}
} // namespace gkeymapper::daemon

38
daemon/uinput_keyboard.h Normal file
View File

@@ -0,0 +1,38 @@
// gKeyMapper daemon - virtual keyboard for replaying "key" mappings.
#ifndef GKEYMAPPER_DAEMON_UINPUT_KEYBOARD_H
#define GKEYMAPPER_DAEMON_UINPUT_KEYBOARD_H
#include <vector>
#include "mapping.h"
namespace gkeymapper::daemon {
// A virtual keyboard created through /dev/uinput. It is only used to replay
// shortcuts; the daemon never reads from it.
class UinputKeyboard {
public:
UinputKeyboard() = default;
~UinputKeyboard();
UinputKeyboard(const UinputKeyboard&) = delete;
UinputKeyboard& operator=(const UinputKeyboard&) = delete;
bool open();
void close();
bool is_open() const { return fd_ >= 0; }
// Presses and releases every step of the shortcut in order.
void play(const std::vector<gkeymapper::ShortcutStep>& steps);
private:
void emit(unsigned short type, unsigned short code, int value);
void sync();
void key_event(unsigned short code, bool down);
int fd_ = -1;
};
} // namespace gkeymapper::daemon
#endif // GKEYMAPPER_DAEMON_UINPUT_KEYBOARD_H

9
data/60-gkeymapper.rules Normal file
View File

@@ -0,0 +1,9 @@
# gKeyMapper - grant the logged-in user access to the keyboard's vendor
# interface and to /dev/uinput, so the daemon needs no root.
#
# Only models the daemon has a device table for are listed (see
# daemon/device_tables.cpp); add the idProduct of a new model there first.
SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c32b", TAG+="uaccess"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c335", TAG+="uaccess"
KERNEL=="uinput", TAG+="uaccess"

View File

@@ -0,0 +1,24 @@
# gKeyMapper daemon - systemd user unit.
#
# This file is installed by "make install", which fills in the binary directory
# for the configured PREFIX. Enable it per user with:
#
# systemctl --user daemon-reload
# systemctl --user enable --now gkeymapper-daemon
#
# It runs as a user service on purpose: the daemon needs the user's session for
# /dev/uinput and for the DISPLAY/WAYLAND_DISPLAY a "command" action inherits.
[Unit]
Description=gKeyMapper daemon - G1..G9 remapping for the Logitech G910
[Service]
Type=simple
ExecStart=@BINDIR@/gkeymapper-daemon
# Reload the mappings without a restart (the config file is watched as well).
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=5
Restart=on-failure
RestartSec=2
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,2 @@
# gKeyMapper daemon - the daemon creates a virtual keyboard via /dev/uinput.
uinput

View File

@@ -4,6 +4,33 @@ Status: draft / proposal
Scope: a native C/C++ background service that detects G1–G9 (and later M1–M3/MR Scope: a native C/C++ background service that detects G1–G9 (and later M1–M3/MR
and media keys) on the Logitech G910 and replays the mappings stored by the GUI. and media keys) on the Logitech G910 and replays the mappings stored by the GUI.
## Implementation status
- **M0 done** — `shared/mapping.{h,cpp}` (file format + shortcut parsing) and
`shared/keys.{h,cpp}` (canonical names → Linux codes); the GUI uses them.
Round-trip and shortcut parsing are covered by a small standalone test.
- **M1 done** — `daemon/` builds (`gkeymapper-daemon`): device discovery,
kernel-driver detach, F-key binding disable, 64-byte interrupt reads and G1–G9
decoding. Verified on hardware: all nine keys decode correctly (press and
release), in order.
- **M2 done** — action execution: `key` shortcuts are replayed through a
`/dev/uinput` virtual keyboard, `command` runs via fork/exec (with a shell
fallback so arguments work), `disabled` does nothing. Verified on hardware.
- **M3 done** — config watching and lifecycle: `daemon/config_watch` watches the
config *directory* with inotify, so a save in the GUI reloads within about
half a second; `SIGHUP`/`SIGUSR1` stay as the fallback. `Save mappings` now
writes atomically (temp file + `rename`), so the daemon never reads a
half-written file. `make install` ships the udev rule, the `modules-load.d`
fragment and the systemd **user** unit (`data/`).
- **Layout handling (added during M2)** — key names are resolved through the
active layout (`XKBLAYOUT` from `/etc/default/keyboard`, via xkbcommon). This
matters because the US-position code `KEY_Z` (44) is `y` on a German keyboard;
`z` is now correctly resolved to evdev 21.
- **AltGr (added after M2)** — level-3 combinations are representable as
`altgr+7`; the base (level 0) key is stored plus the `altgr` modifier.
- **Next: M4 (D-Bus GUI channel)** — status, explicit `Reload`, `TestMapping`
and signals. The reload side of it is already covered by the M3 file watch.
## 1. Goals and non-goals ## 1. Goals and non-goals
Goals Goals
@@ -243,24 +270,16 @@ Start-up orders:
``` ```
src/ # GUI (existing) src/ # GUI (existing)
daemon/ daemon/ # as implemented (see the list in §6 above)
main.cpp # event loop, wiring
device_libusb.{h,cpp} # discovery, detach/attach, read, disable-binding
decoder.{h,cpp} # report bytes -> action id (compile-time tables)
uinput_keyboard.{h,cpp}# virtual keyboard, emit / combo / press-release stack
actions.{h,cpp} # key / command / disabled execution, child reaping
config.{h,cpp} # mappings.conf parse + watch
ipc_dbus.{h,cpp} # GDBus service
generated/devices.inc # ported from supported_devices.py
shared/ shared/
mapping.{h,cpp} # parse/format mappings.conf (used by GUI and daemon) mapping.{h,cpp} # parse/format mappings.conf (used by GUI and daemon)
keys.{h,cpp} # canonical names <-> gdk keyval <-> linux KEY_* keys.{h,cpp} # canonical names <-> gdk keyval <-> linux KEY_*
ipc_ids.h # D-Bus name/path/interface constants keymap.{h,cpp} # layout-aware name <-> input code (xkbcommon)
data/ data/
60-gkeymapper.rules 60-gkeymapper.rules # udev: uaccess for the device and /dev/uinput
gkeymapper-daemon.service gkeymapper-uinput.conf # modules-load.d fragment (uinput)
uinput.conf # modules-load.d gkeymapper-daemon.service # systemd user unit (@BINDIR@ filled in on install)
po/ # daemon strings, reuse the existing catalogs po/ # shared catalogs (GUI strings)
``` ```
Build: the Makefile gets a second target `gkeymapper-daemon` and install rules Build: the Makefile gets a second target `gkeymapper-daemon` and install rules
@@ -289,8 +308,9 @@ for the unit, udev rule and modules-load fragment. New build dependency:
F-key binding, logs `G1..G9` presses. No output yet. F-key binding, logs `G1..G9` presses. No output yet.
- **M2 – Action layer.** uinput keyboard; `key` (shortcut, incl. release) and - **M2 – Action layer.** uinput keyboard; `key` (shortcut, incl. release) and
`command` work end-to-end. `disabled` no-op. `command` work end-to-end. `disabled` no-op.
- **M3 – Config & lifecycle.** config parse/reload (inotify, SIGUSR1), signals, - **M3 – Config & lifecycle (done).** config reload via inotify (`config_watch`,
systemd --user unit + udev rule + modules-load. directory watch + atomic save) and `SIGHUP`/`SIGUSR1`; systemd --user unit +
udev rule + modules-load, installed by `make install`.
- **M4 – IPC.** GDBus service (`Reload`, `GetStatus`, `TestMapping`, signals); - **M4 – IPC.** GDBus service (`Reload`, `GetStatus`, `TestMapping`, signals);
GUI status line + Reload-on-save + optional Test button. GUI status line + Reload-on-save + optional Test button.
- **M5 – Polish.** Packaging/install rules, docs, translations, hardening - **M5 – Polish.** Packaging/install rules, docs, translations, hardening
@@ -333,3 +353,33 @@ for the unit, udev rule and modules-load fragment. New build dependency:
dependency). Recommendation: D-Bus, with signals as a fallback. dependency). Recommendation: D-Bus, with signals as a fallback.
4. Scope of v1: keep `key`/`command`/`disabled` only, or also port 4. Scope of v1: keep `key`/`command`/`disabled` only, or also port
`typeout`/`media`? `typeout`/`media`?
### Resolved
- **Event loop (as implemented, deviates from §4.2).** The daemon is
single-threaded with a blocking `libusb_interrupt_transfer` and a 500 ms
timeout; the config watch is drained once per iteration. That is ~2 idle
wakeups/s at 0 % CPU (the "simple variant" of §4.2) rather than the preferred
async libusb + GMainLoop design. Switching to libusb's async API with its poll
fds in a `poll()` loop stays open if idle wakeups ever become a concern — the
config watch would fold into the same set.
- **Layout handling (done in M2).** Key names in the file are layout
independent; the daemon resolves them through the active layout (`XKBLAYOUT`
from `/etc/default/keyboard`, via xkbcommon) and only falls back to the US
positions from `shared/keys`. This closes the vocabulary-drift risk for non-US
layouts — a German `z` is evdev 21, not the US position 44.
- **Reload without restart (done in M2).** `SIGHUP` re-reads the config file
(the full D-Bus `Reload` comes with M4).
- **Config watching (done in M3).** The daemon watches the config **directory**
with inotify rather than the file, so an atomic save (temp file + `rename`) and
a recreated file are caught too. The loop iteration (≤ 500 ms) is the
coalescing window, so one save yields exactly one reload; `SIGHUP`/`SIGUSR1`
stay as the fallback and there is no extra fd in the loop. `Save mappings` in
the GUI now writes atomically, so the daemon can never read a partial file. A
failed reload keeps the previous mappings instead of disabling every key.
- **Lifecycle (done in M3).** A systemd **user** unit (not a system service): the
daemon needs the session for `/dev/uinput` and for the `DISPLAY` a `command`
action inherits. It is `Type=simple` for now; it becomes `Type=dbus` with a
`BusName` once M4 adds the D-Bus service. The udev rule and the
`modules-load.d` fragment live in `data/` and are installed by `make install`
(under `PREFIX/lib/...`, which udev and systemd both search).

View File

@@ -40,11 +40,12 @@ msgid "Done"
msgstr "Fertig" msgstr "Fertig"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded " "Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"too). Click Done to apply or Cancel to discard." "recorded too). Click Done to apply or Cancel to discard."
msgstr "" msgstr ""
"Tasten der Reihe nach drücken (Modifikatoren wie Strg, Alt, Shift und Super " "Tasten der Reihe nach drücken (Modifikatoren wie Strg, Alt, Shift, Super und "
"werden mit aufgezeichnet). Mit Fertig übernehmen oder mit Abbrechen verwerfen." "AltGr werden mit aufgezeichnet). Mit Fertig übernehmen oder mit Abbrechen "
"verwerfen."
msgid "Choose file..." msgid "Choose file..."
msgstr "Datei auswählen..." msgstr "Datei auswählen..."

View File

@@ -41,11 +41,11 @@ msgid "Done"
msgstr "Done" msgstr "Done"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded " "Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"too). Click Done to apply or Cancel to discard." "recorded too). Click Done to apply or Cancel to discard."
msgstr "" msgstr ""
"Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded " "Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"too). Click Done to apply or Cancel to discard." "recorded too). Click Done to apply or Cancel to discard."
msgid "Choose file..." msgid "Choose file..."
msgstr "Choose file..." msgstr "Choose file..."

View File

@@ -38,12 +38,12 @@ msgid "Done"
msgstr "Aceptar" msgstr "Aceptar"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded " "Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"too). Click Done to apply or Cancel to discard." "recorded too). Click Done to apply or Cancel to discard."
msgstr "" msgstr ""
"Pulsa las teclas en orden (los modificadores como Ctrl, Alt, Mayús y Super " "Pulsa las teclas en orden (los modificadores como Ctrl, Alt, Mayús, Super y "
"también se registran). Haz clic en Aceptar para aplicar o en Cancelar para " "AltGr también se registran). Haz clic en Aceptar para aplicar o en Cancelar "
"descartar." "para descartar."
msgid "Choose file..." msgid "Choose file..."
msgstr "Elegir archivo..." msgstr "Elegir archivo..."

View File

@@ -38,11 +38,11 @@ msgid "Done"
msgstr "Terminer" msgstr "Terminer"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded " "Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"too). Click Done to apply or Cancel to discard." "recorded too). Click Done to apply or Cancel to discard."
msgstr "" msgstr ""
"Appuyez sur les touches dans l'ordre (les modificateurs comme Ctrl, Alt, " "Appuyez sur les touches dans l'ordre (les modificateurs comme Ctrl, Alt, "
"Maj et Super sont également enregistrés). Cliquez sur Terminer pour " "Maj, Super et AltGr sont également enregistrés). Cliquez sur Terminer pour "
"appliquer ou sur Annuler pour abandonner." "appliquer ou sur Annuler pour abandonner."
msgid "Choose file..." msgid "Choose file..."

View File

@@ -51,8 +51,8 @@ msgstr ""
#: src/main.cpp:213 #: src/main.cpp:213
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded " "Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"too). Click Done to apply or Cancel to discard." "recorded too). Click Done to apply or Cancel to discard."
msgstr "" msgstr ""
#: src/main.cpp:263 #: src/main.cpp:263

134
shared/keymap.cpp Normal file
View File

@@ -0,0 +1,134 @@
// gKeyMapper - keyboard layout lookups (xkbcommon).
#include "keymap.h"
#include <cctype>
#include <cstdio>
#include <fstream>
#include <xkbcommon/xkbcommon.h>
#include "keys.h"
namespace gkeymapper {
namespace {
// xkbcommon uses X11 keycodes, which are the evdev codes offset by 8.
constexpr int kXkbKeycodeOffset = 8;
// Reads XKBLAYOUT from /etc/default/keyboard, e.g. "de".
std::string read_layout_name() {
std::ifstream in("/etc/default/keyboard");
std::string line;
while (std::getline(in, line)) {
const std::string key = "XKBLAYOUT=";
auto pos = line.find(key);
if (pos == std::string::npos) {
continue;
}
std::string value = line.substr(pos + key.size());
if (!value.empty() && value.front() == '"') {
value.erase(0, 1);
}
if (!value.empty() && value.back() == '"') {
value.pop_back();
}
return value;
}
return std::string();
}
} // namespace
Keymap::~Keymap() {
if (state_) {
xkb_state_unref(state_);
}
if (keymap_) {
xkb_keymap_unref(keymap_);
}
}
bool Keymap::init() {
layoutName_ = read_layout_name();
if (layoutName_.empty()) {
std::fprintf(stderr, "gkeymapper: no XKBLAYOUT, using US key positions\n");
return false;
}
xkb_context* context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if (!context) {
return false;
}
xkb_rule_names names{};
names.rules = "evdev";
names.model = "pc105";
names.layout = layoutName_.c_str();
names.variant = "";
names.options = "";
keymap_ = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
xkb_context_unref(context);
if (!keymap_) {
std::fprintf(stderr, "gkeymapper: unknown layout '%s', using US key positions\n",
layoutName_.c_str());
return false;
}
state_ = xkb_state_new(keymap_);
if (!state_) {
return false;
}
return true;
}
int Keymap::key_code(const std::string& name) const {
if (!state_) {
return linux_key_code(name);
}
xkb_keysym_t keysym =
xkb_keysym_from_name(name.c_str(), XKB_KEYSYM_CASE_INSENSITIVE);
if (keysym != XKB_KEY_NoSymbol) {
for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap_);
code <= xkb_keymap_max_keycode(keymap_); ++code) {
if (xkb_state_key_get_one_sym(state_, code) == keysym) {
int evdev = static_cast<int>(code) - kXkbKeycodeOffset;
if (evdev > 0) {
return evdev;
}
}
}
}
// Not present on this layout (e.g. "semicolon" on a German keyboard) or
// unknown: fall back to the US position.
return linux_key_code(name);
}
std::string Keymap::base_key_name(unsigned int keycode) const {
if (!state_) {
return std::string();
}
xkb_keysym_t keysym = xkb_state_key_get_one_sym(state_, keycode);
if (keysym == XKB_KEY_NoSymbol) {
return std::string();
}
char buffer[64];
int written = xkb_keysym_get_name(keysym, buffer, sizeof(buffer));
if (written <= 0) {
return std::string();
}
std::string name(buffer);
for (char& c : name) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return name;
}
} // namespace gkeymapper

51
shared/keymap.h Normal file
View File

@@ -0,0 +1,51 @@
// gKeyMapper - keyboard layout lookups (xkbcommon), shared by GUI and daemon.
//
// The active layout is read from /etc/default/keyboard (XKBLAYOUT). Without it,
// names fall back to the layout-independent US key positions in shared/keys.
#ifndef GKEYMAPPER_KEYMAP_H
#define GKEYMAPPER_KEYMAP_H
#include <string>
struct xkb_keymap;
struct xkb_state;
namespace gkeymapper {
// Two-way lookup on the active keyboard layout:
// - daemon: canonical key name -> Linux input (evdev) code
// - GUI: pressed X keycode -> canonical base key name (level 0)
class Keymap {
public:
Keymap() = default;
~Keymap();
Keymap(const Keymap&) = delete;
Keymap& operator=(const Keymap&) = delete;
// Builds the keymap from /etc/default/keyboard. Returns false if it cannot
// be built; the lookups then use the US fallback.
bool init();
bool is_ready() const { return state_ != nullptr; }
const std::string& layout_name() const { return layoutName_; }
// Canonical key name -> Linux input (evdev) code, e.g. "z" -> 21 on a German
// layout. Falls back to linux_key_code() for names that are not part of the
// layout.
int key_code(const std::string& name) const;
// X keycode (same value GDK reports) -> level-0 keysym name, lowercased
// ("ssharp", "a", "f4"). Empty if it cannot be resolved. Used by the capture
// dialog so "shift+ssharf" is recorded instead of the shifted symbol.
std::string base_key_name(unsigned int keycode) const;
private:
xkb_keymap* keymap_ = nullptr;
xkb_state* state_ = nullptr;
std::string layoutName_;
};
} // namespace gkeymapper
#endif // GKEYMAPPER_KEYMAP_H

110
shared/keys.cpp Normal file
View File

@@ -0,0 +1,110 @@
// gKeyMapper - canonical key name tables.
#include "keys.h"
#include <linux/input-event-codes.h>
namespace gkeymapper {
namespace {
const KeyDef kKeys[] = {
// Modifiers (also needed as key codes when a shortcut is replayed).
{"ctrl", KEY_LEFTCTRL},
{"control", KEY_LEFTCTRL},
{"alt", KEY_LEFTALT},
{"shift", KEY_LEFTSHIFT},
{"super", KEY_LEFTMETA},
{"meta", KEY_LEFTMETA},
{"altgr", KEY_RIGHTALT},
{"iso_level3_shift", KEY_RIGHTALT},
// Letters.
{"a", KEY_A}, {"b", KEY_B}, {"c", KEY_C}, {"d", KEY_D},
{"e", KEY_E}, {"f", KEY_F}, {"g", KEY_G}, {"h", KEY_H},
{"i", KEY_I}, {"j", KEY_J}, {"k", KEY_K}, {"l", KEY_L},
{"m", KEY_M}, {"n", KEY_N}, {"o", KEY_O}, {"p", KEY_P},
{"q", KEY_Q}, {"r", KEY_R}, {"s", KEY_S}, {"t", KEY_T},
{"u", KEY_U}, {"v", KEY_V}, {"w", KEY_W}, {"x", KEY_X},
{"y", KEY_Y}, {"z", KEY_Z},
// Digits.
{"0", KEY_0}, {"1", KEY_1}, {"2", KEY_2}, {"3", KEY_3}, {"4", KEY_4},
{"5", KEY_5}, {"6", KEY_6}, {"7", KEY_7}, {"8", KEY_8}, {"9", KEY_9},
// Function keys.
{"f1", KEY_F1}, {"f2", KEY_F2}, {"f3", KEY_F3}, {"f4", KEY_F4},
{"f5", KEY_F5}, {"f6", KEY_F6}, {"f7", KEY_F7}, {"f8", KEY_F8},
{"f9", KEY_F9}, {"f10", KEY_F10}, {"f11", KEY_F11}, {"f12", KEY_F12},
// Editing / navigation.
{"escape", KEY_ESC},
{"tab", KEY_TAB},
{"space", KEY_SPACE},
{"return", KEY_ENTER},
{"backspace", KEY_BACKSPACE},
{"delete", KEY_DELETE},
{"insert", KEY_INSERT},
{"home", KEY_HOME},
{"end", KEY_END},
{"page_up", KEY_PAGEUP},
{"page_down", KEY_PAGEDOWN},
{"up", KEY_UP},
{"down", KEY_DOWN},
{"left", KEY_LEFT},
{"right", KEY_RIGHT},
{"caps_lock", KEY_CAPSLOCK},
// Punctuation.
{"minus", KEY_MINUS},
{"equal", KEY_EQUAL},
{"bracketleft", KEY_LEFTBRACE},
{"bracketright", KEY_RIGHTBRACE},
{"semicolon", KEY_SEMICOLON},
{"apostrophe", KEY_APOSTROPHE},
{"grave", KEY_GRAVE},
{"backslash", KEY_BACKSLASH},
{"comma", KEY_COMMA},
{"period", KEY_DOT},
{"slash", KEY_SLASH},
};
} // namespace
const KeyDef* key_definitions() {
return kKeys;
}
int key_definition_count() {
return static_cast<int>(sizeof(kKeys) / sizeof(kKeys[0]));
}
int linux_key_code(const std::string& name) {
for (const KeyDef& def : kKeys) {
if (name == def.name) {
return def.linux_code;
}
}
return -1;
}
unsigned modifier_bit(const std::string& name) {
if (name == "ctrl" || name == "control") {
return kModCtrl;
}
if (name == "alt") {
return kModAlt;
}
if (name == "shift") {
return kModShift;
}
if (name == "super" || name == "meta") {
return kModSuper;
}
if (name == "altgr" || name == "iso_level3_shift") {
return kModAltGr;
}
return kModNone;
}
} // namespace gkeymapper

41
shared/keys.h Normal file
View File

@@ -0,0 +1,41 @@
// gKeyMapper - canonical key names shared by GUI and daemon.
//
// Canonical names are the lowercase GDK keysym names the GUI already writes
// (e.g. "f4", "escape", "page_up", "ctrl"). The daemon maps the same names to
// Linux input codes, so both halves of the project agree on the spelling.
#ifndef GKEYMAPPER_KEYS_H
#define GKEYMAPPER_KEYS_H
#include <string>
namespace gkeymapper {
// Modifier bits used by the shortcut grammar ("ctrl+c", "altgr+7").
enum ModifierBit : unsigned {
kModNone = 0,
kModCtrl = 1u << 0,
kModAlt = 1u << 1,
kModShift = 1u << 2,
kModSuper = 1u << 3,
kModAltGr = 1u << 4,
};
struct KeyDef {
const char* name; // canonical, lowercase; may also be a modifier name
int linux_code; // KEY_* from <linux/input-event-codes.h>
};
// All supported keys (regular keys and modifiers), no particular order.
const KeyDef* key_definitions();
int key_definition_count();
// Linux KEY_* code for a canonical name, or -1 if unknown.
int linux_key_code(const std::string& name);
// Modifier bit for "ctrl"/"alt"/"shift"/"super" ("meta" counts as super),
// 0 for everything else.
unsigned modifier_bit(const std::string& name);
} // namespace gkeymapper
#endif // GKEYMAPPER_KEYS_H

193
shared/mapping.cpp Normal file
View File

@@ -0,0 +1,193 @@
// gKeyMapper - mappings file parsing/formatting.
#include "mapping.h"
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include "keys.h"
namespace gkeymapper {
const char* action_type_token(ActionType type) {
switch (type) {
case ActionType::Key:
return "key";
case ActionType::Command:
return "command";
case ActionType::Disabled:
default:
return "disabled";
}
}
ActionType action_type_from_token(const std::string& token) {
if (token == "key") {
return ActionType::Key;
}
if (token == "command") {
return ActionType::Command;
}
return ActionType::Disabled;
}
std::string format_mappings(const MappingSet& set) {
std::string out = "# gKeyMapper mappings (G1..G9)\n";
for (int i = 0; i < kNumKeys; ++i) {
Mapping mapping;
if (i < static_cast<int>(set.keys.size())) {
mapping = set.keys[i];
}
out += "G";
out += std::to_string(i + 1);
out += "=";
out += action_type_token(mapping.type);
out += ":";
out += mapping.target;
out += "\n";
}
return out;
}
MappingSet parse_mappings(const std::string& text) {
MappingSet set;
set.keys.assign(kNumKeys, Mapping{});
std::istringstream in(text);
std::string line;
while (std::getline(in, line)) {
if (line.empty() || line[0] == '#') {
continue;
}
auto eq = line.find('=');
if (eq == std::string::npos) {
continue;
}
std::string key = line.substr(0, eq);
if (key.size() < 2 || key[0] != 'G') {
continue;
}
int n = 0;
try {
n = std::stoi(key.substr(1)) - 1;
} catch (...) {
continue;
}
if (n < 0 || n >= kNumKeys) {
continue;
}
std::string rest = line.substr(eq + 1);
auto col = rest.find(':');
std::string typeToken = (col == std::string::npos) ? rest : rest.substr(0, col);
std::string target = (col == std::string::npos) ? std::string() : rest.substr(col + 1);
set.keys[n].type = action_type_from_token(typeToken);
set.keys[n].target = target;
}
return set;
}
bool load_mappings_file(const std::string& path, MappingSet& out) {
std::ifstream in(path);
if (!in) {
return false;
}
std::stringstream buffer;
buffer << in.rdbuf();
out = parse_mappings(buffer.str());
return true;
}
bool save_mappings_file(const std::string& path, const MappingSet& set) {
// Write a temporary file first and rename it into place: a reader (the
// daemon) either sees the old or the new file, never a half-written one.
const std::string tempPath = path + ".tmp";
{
std::ofstream out(tempPath);
if (!out) {
return false;
}
out << format_mappings(set);
out.flush();
if (!out) {
return false;
}
}
return std::rename(tempPath.c_str(), path.c_str()) == 0;
}
std::string default_config_path() {
const char* home = std::getenv("HOME");
return std::string(home ? home : "") + "/.config/gKeyMapper/mappings.conf";
}
std::string format_shortcut_step(unsigned modifiers, const std::string& key_name) {
std::string step;
if ((modifiers & kModCtrl) != 0) {
step += "ctrl+";
}
if ((modifiers & kModAlt) != 0) {
step += "alt+";
}
if ((modifiers & kModShift) != 0) {
step += "shift+";
}
if ((modifiers & kModSuper) != 0) {
step += "super+";
}
if ((modifiers & kModAltGr) != 0) {
step += "altgr+";
}
step += key_name;
return step;
}
bool parse_shortcut(const std::string& text, std::vector<ShortcutStep>& out,
const KeyResolver& resolve) {
out.clear();
if (text.empty()) {
return false;
}
std::istringstream combos(text);
std::string combo;
while (std::getline(combos, combo, ',')) {
if (combo.empty()) {
return false;
}
ShortcutStep step;
bool haveKey = false;
std::istringstream parts(combo);
std::string part;
while (std::getline(parts, part, '+')) {
if (part.empty()) {
return false;
}
unsigned mod = modifier_bit(part);
if (mod != kModNone) {
step.modifiers |= mod;
continue;
}
int code = resolve ? resolve(part) : linux_key_code(part);
if (code < 0) {
return false; // unknown key name
}
step.key_name = part;
step.linux_key = code;
haveKey = true;
}
if (!haveKey) {
return false; // combo without an actual key
}
out.push_back(step);
}
return !out.empty();
}
} // namespace gkeymapper

68
shared/mapping.h Normal file
View File

@@ -0,0 +1,68 @@
// gKeyMapper - the mappings file format, shared by GUI and daemon.
//
// File format (one line per G key):
// # gKeyMapper mappings (G1..G9)
// G1=key:ctrl+c,ctrl+v
// G2=command:/usr/bin/firefox
// G3=disabled:
#ifndef GKEYMAPPER_MAPPING_H
#define GKEYMAPPER_MAPPING_H
#include <functional>
#include <string>
#include <vector>
namespace gkeymapper {
constexpr int kNumKeys = 9; // G1..G9
enum class ActionType { Disabled, Key, Command };
struct Mapping {
ActionType type = ActionType::Disabled;
std::string target;
};
// One entry per G key; index 0 -> G1.
struct MappingSet {
std::vector<Mapping> keys;
};
const char* action_type_token(ActionType type);
ActionType action_type_from_token(const std::string& token);
// Serialize to / parse from the text representation. parse_mappings always
// returns exactly kNumKeys entries (missing ones default to Disabled).
std::string format_mappings(const MappingSet& set);
MappingSet parse_mappings(const std::string& text);
bool load_mappings_file(const std::string& path, MappingSet& out);
bool save_mappings_file(const std::string& path, const MappingSet& set);
// Default config file: ~/.config/gKeyMapper/mappings.conf.
std::string default_config_path();
// One step of a "ctrl+c" shortcut, ready to be replayed via uinput.
struct ShortcutStep {
unsigned modifiers = 0; // ModifierBit mask
std::string key_name; // canonical key name (layout independent)
int linux_key = -1; // resolved input code, -1 if unresolved
};
// Resolves a canonical key name to an input code for the keyboard layout in
// use. The default (empty) resolver uses linux_key_code(), i.e. the
// layout-independent US key positions.
using KeyResolver = std::function<int(const std::string& name)>;
// Parse "ctrl+c,ctrl+v" into individual steps. Returns false if the text is
// empty or contains an unknown key name.
bool parse_shortcut(const std::string& text, std::vector<ShortcutStep>& out,
const KeyResolver& resolve = KeyResolver());
// Formats one step ("ctrl+shift+t") from a ModifierBit mask and a base key
// name. The modifier order is fixed: ctrl, alt, shift, super, altgr.
std::string format_shortcut_step(unsigned modifiers, const std::string& key_name);
} // namespace gkeymapper
#endif // GKEYMAPPER_MAPPING_H

View File

@@ -10,6 +10,9 @@
#include <glib/gi18n.h> #include <glib/gi18n.h>
#include "keymap.h"
#include "keys.h"
namespace gkeymapper { namespace gkeymapper {
namespace { namespace {
@@ -18,9 +21,14 @@ struct CaptureSession {
KeyRow* row = nullptr; KeyRow* row = nullptr;
GtkWidget* displayLabel = nullptr; GtkWidget* displayLabel = nullptr;
std::vector<std::string> steps; std::vector<std::string> steps;
std::set<guint> heldKeys; // non-modifier keyvals currently held (ignores auto-repeat) std::set<guint> heldKeys; // keycodes currently held (ignores auto-repeat)
bool altGrHeld = false; // ISO_Level3_Shift / Mode_switch is down
}; };
bool is_altgr_keyval(guint keyval) {
return keyval == GDK_KEY_ISO_Level3_Shift || keyval == GDK_KEY_Mode_switch;
}
bool is_modifier_keyval(guint keyval) { bool is_modifier_keyval(guint keyval) {
switch (keyval) { switch (keyval) {
case GDK_KEY_Control_L: case GDK_KEY_Control_L:
@@ -34,34 +42,60 @@ bool is_modifier_keyval(guint keyval) {
case GDK_KEY_Meta_L: case GDK_KEY_Meta_L:
case GDK_KEY_Meta_R: case GDK_KEY_Meta_R:
case GDK_KEY_ISO_Level3_Shift: // AltGr case GDK_KEY_ISO_Level3_Shift: // AltGr
case GDK_KEY_Mode_switch: // AltGr on some layouts
return true; return true;
default: default:
return false; return false;
} }
} }
// One keymap for the whole app; building it takes a moment, so it is created
// lazily on the first captured key.
const Keymap& capture_keymap() {
static Keymap keymap;
static bool initialized = false;
if (!initialized) {
keymap.init();
initialized = true;
}
return keymap;
}
// Format one step like the reference project: modifiers joined with '+', // Format one step like the reference project: modifiers joined with '+',
// e.g. "ctrl+shift+t". Multiple steps are joined with ',' ("ctrl+c,ctrl+v"). // e.g. "ctrl+shift+t". Multiple steps are joined with ',' ("ctrl+c,ctrl+v").
std::string format_capture_step(GdkModifierType state, guint keyval) { //
std::string step; // The key is stored as its *base* (level 0) name, so Shift+s-sharp is recorded
// as "shift+ssharp" rather than the shifted symbol "question" (GDK's keyval is
// already shifted, so using it would double the shift on replay). AltGr
// (level 3) becomes the "altgr" modifier plus the base key, e.g. "altgr+7".
std::string format_capture_step(GdkModifierType state, guint keycode, guint keyval,
bool altgr) {
unsigned modifiers = 0;
if (state & GDK_CONTROL_MASK) { if (state & GDK_CONTROL_MASK) {
step += "ctrl+"; modifiers |= kModCtrl;
} }
if (state & GDK_ALT_MASK) { if (state & GDK_ALT_MASK) {
step += "alt+"; modifiers |= kModAlt;
} }
if (state & GDK_SHIFT_MASK) { if (state & GDK_SHIFT_MASK) {
step += "shift+"; modifiers |= kModShift;
} }
if (state & (GDK_SUPER_MASK | GDK_META_MASK)) { if (state & (GDK_SUPER_MASK | GDK_META_MASK)) {
step += "super+"; modifiers |= kModSuper;
} }
const char* name = gdk_keyval_name(keyval); // GTK4 no longer exposes a Mod5 mask, so AltGr is tracked via its key.
std::string key = name ? name : "?"; if (altgr) {
std::transform(key.begin(), key.end(), key.begin(), modifiers |= kModAltGr;
[](unsigned char c) { return std::tolower(c); }); }
step += key;
return step; std::string key = capture_keymap().base_key_name(keycode);
if (key.empty()) {
const char* name = gdk_keyval_name(keyval);
key = name ? name : "?";
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return std::tolower(c); });
}
return format_shortcut_step(modifiers, key);
} }
std::string join_capture_steps(const std::vector<std::string>& steps) { std::string join_capture_steps(const std::vector<std::string>& steps) {
@@ -84,7 +118,7 @@ void update_capture_display(CaptureSession* session) {
} }
gboolean on_capture_key_pressed(GtkEventControllerKey* controller, gboolean on_capture_key_pressed(GtkEventControllerKey* controller,
guint keyval, guint /*keycode*/, guint keyval, guint keycode,
GdkModifierType state, gpointer userData) { GdkModifierType state, gpointer userData) {
auto* session = static_cast<CaptureSession*>(userData); auto* session = static_cast<CaptureSession*>(userData);
@@ -96,27 +130,36 @@ gboolean on_capture_key_pressed(GtkEventControllerKey* controller,
return TRUE; return TRUE;
} }
if (is_altgr_keyval(keyval)) {
// GTK4 dropped the Mod5 mask, so AltGr is tracked through its own key.
session->altGrHeld = true;
return TRUE;
}
if (is_modifier_keyval(keyval)) { if (is_modifier_keyval(keyval)) {
return TRUE; // modifiers are recorded as part of the next real key return TRUE; // modifiers are recorded as part of the next real key
} }
if (session->heldKeys.count(keyval)) { if (session->heldKeys.count(keycode)) {
return TRUE; // ignore auto-repeat while the key is held down return TRUE; // ignore auto-repeat while the key is held down
} }
session->heldKeys.insert(keyval); session->heldKeys.insert(keycode);
constexpr GdkModifierType kModMask = static_cast<GdkModifierType>( constexpr GdkModifierType kModMask = static_cast<GdkModifierType>(
GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_ALT_MASK | GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_ALT_MASK |
GDK_SUPER_MASK | GDK_META_MASK); GDK_SUPER_MASK | GDK_META_MASK);
session->steps.push_back(format_capture_step( session->steps.push_back(format_capture_step(
static_cast<GdkModifierType>(state & kModMask), keyval)); static_cast<GdkModifierType>(state & kModMask), keycode, keyval,
session->altGrHeld));
update_capture_display(session); update_capture_display(session);
return TRUE; return TRUE;
} }
gboolean on_capture_key_released(GtkEventControllerKey* /*controller*/, gboolean on_capture_key_released(GtkEventControllerKey* /*controller*/,
guint keyval, guint /*keycode*/, guint keyval, guint keycode,
GdkModifierType /*state*/, gpointer userData) { GdkModifierType /*state*/, gpointer userData) {
auto* session = static_cast<CaptureSession*>(userData); auto* session = static_cast<CaptureSession*>(userData);
session->heldKeys.erase(keyval); if (is_altgr_keyval(keyval)) {
session->altGrHeld = false;
}
session->heldKeys.erase(keycode);
return TRUE; return TRUE;
} }
@@ -174,7 +217,7 @@ void open_key_capture_dialog(KeyRow* row, GtkWindow* parent) {
gtk_label_set_xalign(GTK_LABEL(title), 0.0f); gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
gtk_box_append(GTK_BOX(outer), title); gtk_box_append(GTK_BOX(outer), title);
GtkWidget* info = gtk_label_new(_("Press keys in order (modifiers like Ctrl, Alt, Shift and Super are recorded too). Click Done to apply or Cancel to discard.")); GtkWidget* info = gtk_label_new(_("Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are recorded too). Click Done to apply or Cancel to discard."));
gtk_label_set_wrap(GTK_LABEL(info), TRUE); gtk_label_set_wrap(GTK_LABEL(info), TRUE);
gtk_label_set_xalign(GTK_LABEL(info), 0.0f); gtk_label_set_xalign(GTK_LABEL(info), 0.0f);
gtk_widget_add_css_class(info, "dim-label"); gtk_widget_add_css_class(info, "dim-label");

View File

@@ -4,9 +4,9 @@
#include <gtk/gtk.h> #include <gtk/gtk.h>
namespace gkeymapper { #include "mapping.h" // kNumKeys, ActionType
constexpr int kNumKeys = 9; namespace gkeymapper {
// Widgets belonging to a single G key row. // Widgets belonging to a single G key row.
struct KeyRow { struct KeyRow {

View File

@@ -11,6 +11,7 @@
#include "capturedialog.h" #include "capturedialog.h"
#include "keyrow.h" #include "keyrow.h"
#include "mapping.h"
#include "mappings.h" #include "mappings.h"
#ifndef PACKAGE #ifndef PACKAGE

View File

@@ -1,84 +1,55 @@
// gKeyMapper - mappings configuration file I/O. // gKeyMapper - GUI glue between the mappings file and the row widgets.
#include "mappings.h" #include "mappings.h"
#include <fstream>
#include <string>
#include <glib/gi18n.h> #include <glib/gi18n.h>
#include "mapping.h"
namespace gkeymapper { namespace gkeymapper {
std::string default_config_path() {
const char* home = g_get_home_dir();
return std::string(home ? home : "") + "/.config/gKeyMapper/mappings.conf";
}
bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows) { bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
std::ofstream out(path); MappingSet set;
if (!out) { set.keys.assign(kNumKeys, Mapping{});
return false;
}
out << "# gKeyMapper mappings (G1..G9)\n"; for (const KeyRow& row : rows) {
for (const auto& row : rows) { if (row.index < 0 || row.index >= kNumKeys) {
const char* type = "disabled"; continue;
}
Mapping mapping;
switch (gtk_drop_down_get_selected(GTK_DROP_DOWN(row.typeDrop))) { switch (gtk_drop_down_get_selected(GTK_DROP_DOWN(row.typeDrop))) {
case 0: type = "key"; break; case 0: mapping.type = ActionType::Key; break;
case 1: type = "command"; break; case 1: mapping.type = ActionType::Command; break;
default: type = "disabled"; break; default: mapping.type = ActionType::Disabled; break;
} }
const char* target = gtk_editable_get_text(GTK_EDITABLE(row.targetEntry)); const char* target = gtk_editable_get_text(GTK_EDITABLE(row.targetEntry));
out << "G" << (row.index + 1) << "=" << type << ":" << (target ? target : "") << "\n"; mapping.target = target ? target : "";
set.keys[row.index] = mapping;
} }
return true;
return save_mappings_file(path, set);
} }
void read_mappings(const std::string& path, const std::vector<KeyRow>& rows) { void read_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
std::ifstream in(path); MappingSet set;
if (!in) { if (!load_mappings_file(path, set)) {
g_printerr(_("Could not open %s.\n"), path.c_str()); g_printerr(_("Could not open %s.\n"), path.c_str());
return; return;
} }
std::string line; for (const KeyRow& row : rows) {
while (std::getline(in, line)) { if (row.index < 0 || row.index >= static_cast<int>(set.keys.size())) {
if (line.empty() || line[0] == '#') {
continue; continue;
} }
auto eq = line.find('=');
if (eq == std::string::npos) {
continue;
}
std::string key = line.substr(0, eq);
if (key.size() < 2 || key[0] != 'G') {
continue;
}
int n = 0;
try {
n = std::stoi(key.substr(1)) - 1;
} catch (...) {
continue;
}
if (n < 0 || n >= static_cast<int>(rows.size())) {
continue;
}
std::string rest = line.substr(eq + 1);
auto col = rest.find(':');
std::string typeToken = (col == std::string::npos) ? rest : rest.substr(0, col);
std::string target = (col == std::string::npos) ? std::string() : rest.substr(col + 1);
guint idx = 2; guint idx = 2;
if (typeToken == "key") { switch (set.keys[row.index].type) {
idx = 0; case ActionType::Key: idx = 0; break;
} else if (typeToken == "command") { case ActionType::Command: idx = 1; break;
idx = 1; default: idx = 2; break;
} else if (typeToken == "disabled") {
idx = 2;
} }
gtk_drop_down_set_selected(GTK_DROP_DOWN(rows[n].typeDrop), idx); gtk_drop_down_set_selected(GTK_DROP_DOWN(row.typeDrop), idx);
gtk_editable_set_text(GTK_EDITABLE(rows[n].targetEntry), target.c_str()); gtk_editable_set_text(GTK_EDITABLE(row.targetEntry),
set.keys[row.index].target.c_str());
} }
} }

View File

@@ -1,4 +1,4 @@
// gKeyMapper - reading and writing the mappings configuration file. // gKeyMapper - GUI glue between the mappings file and the row widgets.
#ifndef GKEYMAPPER_MAPPINGS_H #ifndef GKEYMAPPER_MAPPINGS_H
#define GKEYMAPPER_MAPPINGS_H #define GKEYMAPPER_MAPPINGS_H
@@ -9,14 +9,10 @@
namespace gkeymapper { namespace gkeymapper {
// Default config file: ~/.config/gKeyMapper/mappings.conf. // Collects the current widget state and writes it to path.
std::string default_config_path();
// Writes all rows as "G1=type:target" lines. Returns false on I/O error.
bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows); bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows);
// Loads mappings from path into the rows' widgets. Missing/invalid lines are // Loads path into the rows' widgets (type dropdown + target entry).
// skipped; a missing file is reported and otherwise ignored.
void read_mappings(const std::string& path, const std::vector<KeyRow>& rows); void read_mappings(const std::string& path, const std::vector<KeyRow>& rows);
} // namespace gkeymapper } // namespace gkeymapper