Deamon management
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
build/
|
build/
|
||||||
locale/
|
locale/
|
||||||
gkeymapper
|
gkeymapper
|
||||||
|
gkeymapper-daemon
|
||||||
*.o
|
*.o
|
||||||
*.so
|
*.so
|
||||||
|
|||||||
110
AGENTS.md
110
AGENTS.md
@@ -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
|
||||||
|
|||||||
72
Makefile
72
Makefile
@@ -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
125
README.md
@@ -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
68
daemon/action.cpp
Normal 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
30
daemon/action.h
Normal 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
109
daemon/config_watch.cpp
Normal 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
55
daemon/config_watch.h
Normal 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
96
daemon/device_tables.cpp
Normal 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
47
daemon/device_tables.h
Normal 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
180
daemon/g910_device.cpp
Normal 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
50
daemon/g910_device.h
Normal 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
118
daemon/main.cpp
Normal 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
140
daemon/uinput_keyboard.cpp
Normal 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
38
daemon/uinput_keyboard.h
Normal 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
9
data/60-gkeymapper.rules
Normal 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"
|
||||||
24
data/gkeymapper-daemon.service
Normal file
24
data/gkeymapper-daemon.service
Normal 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
|
||||||
2
data/gkeymapper-uinput.conf
Normal file
2
data/gkeymapper-uinput.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# gKeyMapper daemon - the daemon creates a virtual keyboard via /dev/uinput.
|
||||||
|
uinput
|
||||||
@@ -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).
|
||||||
|
|||||||
9
po/de.po
9
po/de.po
@@ -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..."
|
||||||
|
|||||||
8
po/en.po
8
po/en.po
@@ -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..."
|
||||||
|
|||||||
10
po/es.po
10
po/es.po
@@ -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..."
|
||||||
|
|||||||
6
po/fr.po
6
po/fr.po
@@ -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..."
|
||||||
|
|||||||
@@ -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
134
shared/keymap.cpp
Normal 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
51
shared/keymap.h
Normal 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
110
shared/keys.cpp
Normal 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
41
shared/keys.h
Normal 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
193
shared/mapping.cpp
Normal 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
68
shared/mapping.h
Normal 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
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user