Deamon management

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

68
daemon/action.cpp Normal file
View File

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

30
daemon/action.h Normal file
View File

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

109
daemon/config_watch.cpp Normal file
View File

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

55
daemon/config_watch.h Normal file
View File

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

96
daemon/device_tables.cpp Normal file
View File

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

47
daemon/device_tables.h Normal file
View File

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

180
daemon/g910_device.cpp Normal file
View File

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

50
daemon/g910_device.h Normal file
View File

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

118
daemon/main.cpp Normal file
View File

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

140
daemon/uinput_keyboard.cpp Normal file
View File

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

38
daemon/uinput_keyboard.h Normal file
View File

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