Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c750fe0449 | |||
| 5410ed2ae5 | |||
| f45596a7e3 | |||
| da60aef44e |
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
@@ -44,6 +44,12 @@
|
||||
"type": "shell",
|
||||
"command": "make clean && make",
|
||||
"problemMatcher": "$gcc"
|
||||
},
|
||||
{
|
||||
"label": "make: deb",
|
||||
"type": "shell",
|
||||
"command": "make deb",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
18
AGENTS.md
18
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
|
||||
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +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.
|
||||
10
Makefile
10
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)
|
||||
|
||||
|
||||
17
README.md
17
README.md
@@ -3,7 +3,6 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -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,21 +39,21 @@ 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
|
||||
|
||||
Change to the project directory:
|
||||
|
||||
```bash
|
||||
cd /home/trager/Nextcloud/Projekte/C++/gKeyMapper
|
||||
cd gKeyMapper
|
||||
```
|
||||
|
||||
(Alternatively clone the repository or copy the project folder to your own location.)
|
||||
(Use the path where you cloned or copied the repository.)
|
||||
|
||||
## Build
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <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
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/hidraw.h>
|
||||
|
||||
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
|
||||
// <device>:<config>.<interface>. 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<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);
|
||||
// HID SET_REPORT (feature report). The first byte of the command
|
||||
// is the report ID.
|
||||
int len = static_cast<int>(command.size());
|
||||
// HIDIOCSFEATURE takes a buffer where byte 0 is the report ID.
|
||||
std::vector<uint8_t> 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<int>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,13 +29,58 @@ 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 <gkeymapper@rager.dev>
|
||||
Description: Remap G1-G9 keys of the Logitech G910
|
||||
A GTK4 GUI plus a background daemon for remapping the G1-G9 keys
|
||||
of a Logitech G910 keyboard to keys or commands.
|
||||
EOF
|
||||
|
||||
# preinst: stop any running 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/preinst" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
case "$1" in
|
||||
install|upgrade)
|
||||
# Stop any running daemon (old or new).
|
||||
for pid in $(pgrep -f gkeymapper-daemon 2>/dev/null); do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
sleep 1
|
||||
|
||||
# Re-attach the kernel driver to interface 1 if it was detached by
|
||||
# an older (libusb-based) daemon.
|
||||
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/preinst"
|
||||
|
||||
# postinst: reload udev/systemd and enable the user daemon.
|
||||
cat > "$STAGING/DEBIAN/postinst" <<'EOF'
|
||||
#!/bin/sh
|
||||
@@ -48,6 +93,36 @@ case "$1" in
|
||||
udevadm trigger --subsystem-match=input 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove stale service file from old "make install" (PREFIX=/usr/local)
|
||||
# so the new one from /usr takes precedence.
|
||||
rm -f /usr/local/lib/systemd/user/gkeymapper-daemon.service
|
||||
|
||||
# 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 +170,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
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user