Deamon management
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user