From 5410ed2ae5f55819c63ee5c45bfa066953fb5b67 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 27 Oct 2026 18:57:14 +0100 Subject: [PATCH] 0.1.2 --- AGENTS.md | 18 ++- LICENSE.md | 42 +++---- Makefile | 10 +- README.md | 13 +-- daemon/g910_device.cpp | 246 +++++++++++++++++++++-------------------- daemon/g910_device.h | 25 ++--- scripts/i18n.sh | 0 scripts/make-deb.sh | 80 +++++++++++++- shared/ipc_ids.h | 2 +- 9 files changed, 258 insertions(+), 178 deletions(-) mode change 100644 => 100755 scripts/i18n.sh mode change 100644 => 100755 scripts/make-deb.sh diff --git a/AGENTS.md b/AGENTS.md index 72fd084..8c508ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,10 +10,10 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/` ## Build / run / verify - `make` — builds the `gkeymapper` GUI, the `gkeymapper-daemon` and the `.mo` catalogs. Needs `g++` (C++17), `make`, `gettext`, `libgtk-4-dev`, - `libusb-1.0-0-dev`, `libxkbcommon-dev` and `libglib2.0-dev` (see README for + `libxkbcommon-dev` and `libglib2.0-dev` (see README for `apt install` on a fresh machine). `libglib2.0-dev` also provides `glib-compile-resources`, which generates the icons embedded into the GUI. -- `make daemon` — builds only `gkeymapper-daemon` (needs libusb, xkbcommon and +- `make daemon` — builds only `gkeymapper-daemon` (needs xkbcommon and gio-2.0, not GTK). - `make translations` — only runs `msgfmt` on the existing `.po` files. - `make run` — builds, then runs with `GKEYMAPPER_LOCALEDIR=$PWD/locale` and @@ -66,11 +66,9 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/` `optionsdialog` and `popovers` (the popup input grab, see below). - The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp` + the generated `build/gresources.c`, see Assets) **and** the daemon - (`daemon/*.cpp` + `shared/*.cpp`, needs `libusb-1.0-0-dev`, - `libxkbcommon-dev` and `libglib2.0-dev`). `make daemon` builds only the daemon. + (`daemon/*.cpp` + `shared/*.cpp`, needs `libxkbcommon-dev` and `libglib2.0-dev`). `make daemon` builds only the daemon. - The daemon is split into: `device_tables` (raw HID report tables per model plus - `decode_gkey_state`, the report → pressed-bitmap decoder), `g910_device` (libusb - open/detach/claim/read), `uinput_keyboard` (virtual keyboard), `action` + `decode_gkey_state`, the report → pressed-bitmap decoder), `g910_device` (hidraw open/read/close), `uinput_keyboard` (virtual keyboard), `action` (dispatch `key`/`command`/`disabled`/`keyhold`), `config_watch` (inotify auto-reload of the mappings file) and `ipc_dbus` (the GDBus service). `shared/keymap` does the layout lookups for both sides. The hot path never touches the GUI: D-Bus is @@ -100,7 +98,7 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/` 2 s while D-Bus and the config watch keep running. Never make it exit on a missing device — that would drop the bus name and make the GUI look like the daemon died. `GKeyDevice::open(quiet)` exists so those retries stay out of the - journal (libusb-level errors always log). + journal (hidraw-level errors always log). - `make install` (PREFIX default `/usr/local`) also installs the packaging files from `data/`: the udev rule, the `modules-load.d` fragment, the systemd **user** unit and the D-Bus activation file. Both contain an @@ -389,9 +387,9 @@ over any installed file; the file lookup remains only as a safety net. families, which must *not* decode as media keys (see the bit table above); - build the daemon with a **fake** `GKeyDevice`: keep the header, script `open()`/`read_report()`, link `daemon/*.cpp` **without** `g910_device.cpp` - and without libusb. That runs the real loop, so it covers "no keyboard at + and without g910_device.cpp. That runs the real loop, so it covers "no keyboard at start", "device lost" and the reopen, emits real D-Bus signals and never - claims the real device — nothing touches the keyboard and no key is injected + opens the real device — nothing touches the keyboard and no key is injected into the focused window. Use a config with `command:` mappings only (a helper script that dumps its environment is also how you check that a `command` action sees `DISPLAY`); with `key:` mappings the fake would inject keys into @@ -401,7 +399,7 @@ over any installed file; the file lookup remains only as a safety net. the fake `g910_device.h` in an include directory. `#include "g910_device.h"` searches the *including file's* directory first, so compiling the real `daemon/main.cpp` would always pick up the real header (its inline - `is_open()` needs libusb). + `is_open()` needs hidraw). - **Give the fake its own session bus** (`dbus-daemon --session --address= unix:path=/tmp/…/fake_bus --fork --print-pid=1`, then `DBUS_SESSION_BUS_ADDRESS=unix:path=…` for the fake). Otherwise it refuses diff --git a/LICENSE.md b/LICENSE.md index 2e3d3a4..ad0e526 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2026 Thomas Rager - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2026 Thomas Rager + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 3254fe6..2d9f6e3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # gKeyMapper - Build # Dependencies: g++, pkg-config, gettext (msgfmt, for the translations), -# libgtk-4-dev, libusb-1.0-0-dev, libxkbcommon-dev -# Installation: sudo apt install build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev +# libgtk-4-dev, libxkbcommon-dev +# Installation: sudo apt install build-essential gettext libgtk-4-dev libxkbcommon-dev CXX ?= g++ CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -Wpedantic @@ -11,8 +11,6 @@ GLIB_COMPILE_RESOURCES ?= glib-compile-resources GTK_CFLAGS := $(shell $(PKG_CONFIG) --cflags 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) GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-2.0) @@ -32,7 +30,7 @@ DAEMON_HEADERS := $(wildcard $(DAEMON_DIR)/*.h) $(wildcard $(SHARED_DIR)/*.h) GUI_RES := build/gresources.c GUI_RES_SRC := data/icons.gresource.xml PREFIX ?= /usr/local -DEB_VERSION ?= 0.1.1 +DEB_VERSION ?= 0.1.2 DEB_ARCH := $(shell dpkg --print-architecture 2>/dev/null || echo amd64) BINDIR := $(PREFIX)/bin LOCALEDIR := $(PREFIX)/share/locale @@ -70,7 +68,7 @@ $(TARGET): $(GUI_SOURCES) $(GUI_HEADERS) $(GUI_RES) daemon: $(DAEMON) $(DAEMON): $(DAEMON_SOURCES) $(DAEMON_HEADERS) - $(CXX) $(CXXFLAGS) $(USB_CFLAGS) $(XKB_CFLAGS) $(GIO_CFLAGS) -o $@ $(DAEMON_SOURCES) $(USB_LIBS) $(XKB_LIBS) $(GIO_LIBS) + $(CXX) $(CXXFLAGS) $(XKB_CFLAGS) $(GIO_CFLAGS) -o $@ $(DAEMON_SOURCES) $(XKB_LIBS) $(GIO_LIBS) translations: $(MO_FILES) diff --git a/README.md b/README.md index 386ec9a..de74f92 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ ![C++17](https://img.shields.io/badge/C%2B%2B-17-00599C?logo=cplusplus&logoColor=white) ![GTK 4](https://img.shields.io/badge/GTK-4-7FE719?logo=gtk&logoColor=black) ![GLib/GIO](https://img.shields.io/badge/GLib%2FGIO-D--Bus-4A86CF) -![libusb](https://img.shields.io/badge/libusb-1.0-c8a200) ![xkbcommon](https://img.shields.io/badge/xkbcommon-layout--aware-6E4C13) ![i18n](https://img.shields.io/badge/i18n-de%20%7C%20en%20%7C%20es%20%7C%20fr-blueviolet) ![systemd](https://img.shields.io/badge/systemd-user%20service-30D475?logo=systemd&logoColor=white) @@ -32,7 +31,6 @@ Documentation and source comments are written in English. - pkg-config - gettext (for translations, `msgfmt`) - GTK 4 development files -- libusb 1.0 development files (for the daemon) - libxkbcommon development files (for the daemon's layout handling) - GLib/GIO development files (for the GUI ↔ daemon D-Bus channel; `libgtk-4-dev` already pulls them in, but they are needed for `make daemon` on its own) @@ -41,11 +39,11 @@ Documentation and source comments are written in English. ```bash sudo apt update -sudo apt install -y build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev libglib2.0-dev +sudo apt install -y build-essential gettext libgtk-4-dev libxkbcommon-dev libglib2.0-dev ``` -Afterwards `pkg-config --modversion gtk4`, `pkg-config --modversion libusb-1.0` -and `pkg-config --modversion gio-2.0` should print versions. +Afterwards `pkg-config --modversion gtk4` and `pkg-config --modversion xkbcommon` +should print versions. ## Setting up the project @@ -181,7 +179,8 @@ The daemon reads G1–G9 from the keyboard and replays the configured action ./gkeymapper-daemon /path/to.conf # or an explicit file ``` -- It reaches the keyboard through libusb (raw USB HID, vendor interface 1) and +- It reads the keyboard's vendor interface through the kernel's hidraw + interface (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 @@ -391,7 +390,7 @@ shared/keymap.{h,cpp} # layout-aware key lookups (xkbcommon), GUI + daemon shared/ipc_ids.h # app id + D-Bus names shared by GUI and daemon daemon/main.cpp # event loop: read G-keys, run the action daemon/device_tables.{h,cpp} # per-model raw HID report tables (G910) -daemon/g910_device.{h,cpp} # libusb access to the vendor interface +daemon/g910_device.{h,cpp} # hidraw access to the vendor interface daemon/uinput_keyboard.{h,cpp}# virtual keyboard for "key" mappings daemon/action.{h,cpp} # dispatch "key" / "command" / "disabled" / "keyhold" daemon/config_watch.{h,cpp} # inotify watch on mappings.conf (auto-reload) diff --git a/daemon/g910_device.cpp b/daemon/g910_device.cpp index ca0223d..17afcba 100644 --- a/daemon/g910_device.cpp +++ b/daemon/g910_device.cpp @@ -1,14 +1,21 @@ -// gKeyMapper daemon - raw USB HID access to the keyboard's vendor interface. +// gKeyMapper daemon - raw HID access to the keyboard's vendor interface. +// +// Uses the kernel's hidraw interface instead of libusb. hidraw provides +// non-exclusive access: the kernel driver (usbhid) stays attached, and other +// tools (OpenRGB, etc.) can open the same device at the same time. This +// avoids the exclusive claim that blocked OpenRGB from detecting the keyboard. #include "g910_device.h" #include - -// libusb.h uses zero-length arrays, which -Wpedantic flags. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" -#include -#pragma GCC diagnostic pop +#include +#include +#include +#include +#include +#include +#include +#include namespace gkeymapper::daemon { @@ -16,108 +23,106 @@ GKeyDevice::~GKeyDevice() { close(); } +// Scan /sys/class/hidraw for a node whose HID_ID matches vendor:product and +// whose sysfs path points at interface 1 (the vendor-specific one). Returns +// the minor number (>= 0) or -1. +int GKeyDevice::find_hidraw_node(uint16_t vendor, uint16_t product) { + char id_str[64]; + std::snprintf(id_str, sizeof(id_str), "0003:%08X:%08X", (unsigned)vendor, (unsigned)product); + + for (int i = 0; i < 64; ++i) { + char path[512]; + std::snprintf(path, sizeof(path), "/sys/class/hidraw/hidraw%d/device/uevent", i); + FILE* f = std::fopen(path, "r"); + if (!f) { + continue; + } + char buf[2048]; + size_t n = std::fread(buf, 1, sizeof(buf) - 1, f); + std::fclose(f); + buf[n] = '\0'; + + // Check HID_ID matches our vendor:product. + if (std::strstr(buf, id_str) == nullptr) { + continue; + } + + // Check the sysfs path contains ":1." (interface 1, the vendor one). + // The readlink gives us the full device path. + char linkpath[512]; + std::snprintf(linkpath, sizeof(linkpath), "/sys/class/hidraw/hidraw%d", i); + char resolved[1024]; + ssize_t rlen = readlink(linkpath, resolved, sizeof(resolved) - 1); + if (rlen < 0) { + continue; + } + resolved[rlen] = '\0'; + + // The resolved path contains the interface number in the form + // :.. Interface 1 (vendor-specific) has + // config 1, so the path contains ":1.1" (altsetting 1) or ":1.0". + // We check the uevent's HID_PHYS instead, which contains "input1" + // for interface 1 and "input0" for interface 0. + if (std::strstr(buf, "input1") == nullptr) { + continue; + } + + return i; + } + return -1; +} + bool GKeyDevice::open(bool quiet) { - if (handle_) { + if (fd_ >= 0) { return true; } - if (libusb_init(&context_) != 0) { - std::fprintf(stderr, "gkeymapper-daemon: libusb_init failed\n"); - return false; - } + // Find a supported profile first (by vendor/product). + // We iterate all profiles and try to find a matching hidraw node. + const auto& profs = profiles(); + const DeviceProfile* matched = nullptr; + int node = -1; - 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); + for (const DeviceProfile& p : profs) { + node = find_hidraw_node(p.vendor, p.product); + if (node >= 0) { + matched = &p; + break; } } - if (!target) { + if (!matched || node < 0) { if (!quiet) { std::fprintf(stderr, "gkeymapper-daemon: no supported keyboard found\n"); } - libusb_free_device_list(devices, 1); - close(); return false; } + profile_ = matched; - if (libusb_open(target, &handle_) != 0) { + char devpath[64]; + std::snprintf(devpath, sizeof(devpath), "/dev/hidraw%d", node); + + fd_ = ::open(devpath, O_RDWR | O_NONBLOCK); + if (fd_ < 0) { if (!quiet) { - std::fprintf(stderr, "gkeymapper-daemon: could not open %s (permissions?)\n", - profile_->name); + std::fprintf(stderr, "gkeymapper-daemon: could not open %s (%s)\n", + devpath, std::strerror(errno)); } - libusb_free_device_list(devices, 1); - close(); - return false; - } - libusb_free_device_list(devices, 1); - - if (!find_endpoint()) { - close(); + profile_ = nullptr; 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) { - if (!quiet) { - std::fprintf(stderr, "gkeymapper-daemon: could not claim interface %d\n", - profile_->interfaceNumber); - } - close(); - return false; - } - claimed_interface_ = profile_->interfaceNumber; + // Disable the default G-key binding (G-keys mapped to F-keys) if needed. + // Done after open so disable_binding() can use the fd. + // (disable_binding is called separately by main.cpp) - std::fprintf(stderr, "gkeymapper-daemon: %s found (endpoint 0x%02x)\n", - profile_->name, endpoint_); + std::fprintf(stderr, "gkeymapper-daemon: %s found (hidraw%d)\n", + profile_->name, node); 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_) { + if (fd_ < 0 || !profile_) { return false; } if (!profile_->disableBinding) { @@ -125,19 +130,20 @@ bool GKeyDevice::disable_binding() { } for (const Report& command : profile_->disableCommands) { - int written = libusb_control_transfer(handle_, 0x21, 0x09, 0x0212, - profile_->interfaceNumber, - const_cast(command.data()), - static_cast(command.size()), - 1000); - if (written < 0) { - std::fprintf(stderr, "gkeymapper-daemon: disable-binding write failed (%d)\n", - written); + // HID SET_REPORT (feature report). The first byte of the command + // is the report ID. + int len = static_cast(command.size()); + // HIDIOCSFEATURE takes a buffer where byte 0 is the report ID. + std::vector buf(command.begin(), command.end()); + int rc = ioctl(fd_, HIDIOCSFEATURE(len), buf.data()); + if (rc < 0) { + std::fprintf(stderr, "gkeymapper-daemon: disable-binding ioctl failed (%s)\n", + std::strerror(errno)); return false; } } - // Drain the confirmation reports so they do not show up as key events. + // Drain confirmation reports. uint8_t scratch[64]; for (int i = 0; i < 6; ++i) { if (read_report(scratch, sizeof(scratch), 200) <= 0) { @@ -148,38 +154,44 @@ bool GKeyDevice::disable_binding() { } int GKeyDevice::read_report(uint8_t* buffer, int length, int timeoutMs) { - if (!handle_) { + if (fd_ < 0) { return -1; } - int transferred = 0; - int rc = libusb_interrupt_transfer(handle_, endpoint_, buffer, length, - &transferred, timeoutMs); - if (rc == 0) { - return transferred; + + struct pollfd pfd; + pfd.fd = fd_; + pfd.events = POLLIN; + pfd.revents = 0; + + int rc = poll(&pfd, 1, timeoutMs); + if (rc < 0) { + if (errno == EINTR) { + return 0; + } + return -1; } - if (rc == LIBUSB_ERROR_TIMEOUT) { + if (rc == 0) { + return 0; // timeout + } + if (!(pfd.revents & POLLIN)) { return 0; } - return rc; // negative + + ssize_t n = ::read(fd_, buffer, length); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return -1; + } + return static_cast(n); } 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_); + if (fd_ >= 0) { + ::close(fd_); } - handle_ = nullptr; - if (context_) { - libusb_exit(context_); - context_ = nullptr; - } - claimed_interface_ = -1; - detached_kernel_driver_ = false; + fd_ = -1; profile_ = nullptr; } diff --git a/daemon/g910_device.h b/daemon/g910_device.h index 02cbae8..ead877b 100644 --- a/daemon/g910_device.h +++ b/daemon/g910_device.h @@ -1,4 +1,8 @@ -// gKeyMapper daemon - raw USB HID access to the keyboard's vendor interface. +// gKeyMapper daemon - raw HID access to the keyboard's vendor interface. +// +// Uses the kernel's hidraw interface instead of libusb: hidraw provides +// non-exclusive access, so other tools (OpenRGB, etc.) can talk to the same +// device at the same time. The kernel driver (usbhid) stays attached. #ifndef GKEYMAPPER_DAEMON_GKEY_DEVICE_H #define GKEYMAPPER_DAEMON_GKEY_DEVICE_H @@ -6,9 +10,6 @@ #include "device_tables.h" -struct libusb_context; -struct libusb_device_handle; - namespace gkeymapper::daemon { class GKeyDevice { @@ -19,15 +20,15 @@ public: 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. + // Finds a supported keyboard and opens its hidraw node. Returns false (and + // logs) if none is found. // // `quiet` silences the "cannot use the keyboard" lines only, which the // hot-plug retry uses so an unplugged keyboard does not flood the journal - // with one line every retry; libusb-level failures always log. + // with one line every retry. bool open(bool quiet = false); void close(); - bool is_open() const { return handle_ != nullptr; } + bool is_open() const { return fd_ >= 0; } // Sends the "stop mapping G-keys to F-keys" command if the model needs it. bool disable_binding(); @@ -39,14 +40,10 @@ public: const DeviceProfile* profile() const { return profile_; } private: - bool find_endpoint(); + int find_hidraw_node(uint16_t vendor, uint16_t product); - libusb_context* context_ = nullptr; - libusb_device_handle* handle_ = nullptr; + int fd_ = -1; const DeviceProfile* profile_ = nullptr; - uint8_t endpoint_ = 0; - int claimed_interface_ = -1; - bool detached_kernel_driver_ = false; }; } // namespace gkeymapper::daemon diff --git a/scripts/i18n.sh b/scripts/i18n.sh old mode 100644 new mode 100755 diff --git a/scripts/make-deb.sh b/scripts/make-deb.sh old mode 100644 new mode 100755 index 066c59b..a800f13 --- a/scripts/make-deb.sh +++ b/scripts/make-deb.sh @@ -3,7 +3,7 @@ # Usage: scripts/make-deb.sh [version] [arch] set -e -VERSION="${1:-0.1.1}" +VERSION="${1:-0.1.2}" ARCH="${2:-$(dpkg --print-architecture 2>/dev/null || echo amd64)}" PACKAGE="gkeymapper" STAGING="build/deb" @@ -29,7 +29,7 @@ Section: utils Installed-Size: $INSTALLED_SIZE Priority: optional Architecture: $ARCH -Depends: libc6, libgtk-4-1, libusb-1.0-0, libxkbcommon0, libglib2.0-0, systemd, udev +Depends: libc6, libgtk-4-1, libxkbcommon0, libglib2.0-0, systemd, udev Maintainer: gKeyMapper Description: Remap G1-G9 keys of the Logitech G910 A GTK4 GUI plus a background daemon for remapping the G1-G9 keys @@ -48,6 +48,32 @@ case "$1" in udevadm trigger --subsystem-match=input 2>/dev/null || true fi + # Re-attach the kernel driver to interface 1 if it was detached by an + # older (libusb-based) daemon. Without this the new (hidraw-based) + # daemon cannot find /dev/hidraw for interface 1. + for _dev in /sys/bus/usb/devices/*; do + _vendor=$(cat "$_dev/idVendor" 2>/dev/null) || continue + _product=$(cat "$_dev/idProduct" 2>/dev/null) || continue + case "$_vendor" in + 046d) + case "$_product" in + c32b|c335) + for _iface in "$_dev"/*:*; do + case "$_iface" in + *:1.*) + _name=$(basename "$_iface") + if [ -w /sys/bus/usb/drivers/usbhid/bind ]; then + echo "$_name" > /sys/bus/usb/drivers/usbhid/bind 2>/dev/null || true + fi + ;; + esac + done + ;; + esac + ;; + esac + done + # Helper: run a command as a given user with the D-Bus session bus # available. "systemctl --user" needs DBUS_SESSION_BUS_ADDRESS, which # is not set in a plain "su -" login shell. @@ -95,6 +121,56 @@ EOF chmod 755 "$STAGING/DEBIAN/postinst" +# prerm: stop the daemon and re-attach the kernel driver before upgrading. +# The old (libusb-based) daemon detached the kernel driver from interface 1; +# if it was not stopped gracefully, the driver stays detached and the new +# (hidraw-based) daemon cannot find the device. +cat > "$STAGING/DEBIAN/prerm" <<'EOF' +#!/bin/sh +set -e +case "$1" in + remove|upgrade|deconfigure) + # Stop the daemon if running. + if [ -n "$SUDO_USER" ]; then + runuser -u "$SUDO_USER" -- systemctl --user stop gkeymapper-daemon >/dev/null 2>&1 || true + runuser -u "$SUDO_USER" -- systemctl --user daemon-reload >/dev/null 2>&1 || true + else + for _user in $(loginctl list-users --no-legend 2>/dev/null | awk '{print $2}'); do + runuser -u "$_user" -- systemctl --user stop gkeymapper-daemon >/dev/null 2>&1 || true + done + fi + + # Re-attach the kernel driver to interface 1 if it was detached by + # an older (libusb-based) daemon. Without this, the new daemon + # (hidraw-based) cannot find /dev/hidraw for interface 1. + for _dev in /sys/bus/usb/devices/*; do + _vendor=$(cat "$_dev/idVendor" 2>/dev/null) || continue + _product=$(cat "$_dev/idProduct" 2>/dev/null) || continue + case "$_vendor" in + 046d) + case "$_product" in + c32b|c335) + for _iface in "$_dev"/*:*; do + case "$_iface" in + *:1.*) + _name=$(basename "$_iface") + if [ -w /sys/bus/usb/drivers/usbhid/bind ]; then + echo "$_name" > /sys/bus/usb/drivers/usbhid/bind 2>/dev/null || true + fi + ;; + esac + done + ;; + esac + ;; + esac + done + ;; +esac +exit 0 +EOF +chmod 755 "$STAGING/DEBIAN/prerm" + # postrm: disable the user daemon on removal. cat > "$STAGING/DEBIAN/postrm" <<'EOF' #!/bin/sh diff --git a/shared/ipc_ids.h b/shared/ipc_ids.h index cb8387d..11d989f 100644 --- a/shared/ipc_ids.h +++ b/shared/ipc_ids.h @@ -13,7 +13,7 @@ namespace gkeymapper::ipc { inline constexpr const char* kAppId = "dev.rager.gkeymapper"; // Reported by GetStatus and shown in the About dialog. -inline constexpr const char* kVersion = "0.1.1"; +inline constexpr const char* kVersion = "0.1.2"; // The daemon owns this name; the GUI talks to it via the object below. //