// 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), M3 (config watching + lifecycle) and M4 (D-Bus GUI channel) are // done. #include #include #include #include #include #include "action.h" #include "config_watch.h" #include "device_tables.h" #include "g910_device.h" #include "ipc_dbus.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 load = [&]() -> bool { const bool ok = gkeymapper::load_mappings_file(configPath, mappings); std::fprintf(stderr, "gkeymapper-daemon: %s %s\n", ok ? "loaded" : "cannot read", configPath.c_str()); return ok; }; load(); // 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(); // Declared before the IPC service so its status callback can report the // device. It is opened only after the name check below: a second instance // must not detach the kernel driver from the keyboard the running one uses. gkeymapper::daemon::GKeyDevice device; const gkeymapper::daemon::DeviceProfile* profile = nullptr; // Runs the mapping of G (0-based). A real keypress and the D-Bus // TestMapping call both go through here, so a test fires exactly the same // path. Returns whether there was an action to run. auto run_mapping = [&](int index, bool test) -> bool { if (index < 0 || index >= static_cast(mappings.keys.size())) { return false; } const gkeymapper::Mapping& mapping = mappings.keys[index]; const bool usable = mapping.type != gkeymapper::ActionType::Disabled && !mapping.target.empty(); std::printf("G%d -> %s:%s%s\n", index + 1, gkeymapper::action_type_token(mapping.type), mapping.target.c_str(), test ? " (test)" : ""); std::fflush(stdout); if (usable) { actions.run(mapping); } return usable; }; gkeymapper::daemon::DaemonService service; auto reload = [&]() -> bool { const bool ok = load(); service.notify_config_reloaded(ok); return ok; }; service.set_reload(reload); service.set_device([&]() { return profile != nullptr ? std::string(profile->name) : std::string(); }); service.set_test([&](int index, std::string& action) { if (!run_mapping(index, true)) { return false; } const gkeymapper::Mapping& mapping = mappings.keys[index]; action = std::string(gkeymapper::action_type_token(mapping.type)) + ":" + mapping.target; return true; }); switch (service.start(configPath)) { case gkeymapper::daemon::StartResult::AlreadyRunning: // Two instances would fight over the device and create a second // virtual keyboard, so the newcomer gives way. std::fprintf(stderr, "gkeymapper-daemon: another instance is already running; " "exiting\n"); return 0; case gkeymapper::daemon::StartResult::NoBus: case gkeymapper::daemon::StartResult::Ok: break; } if (!device.open()) { return 1; } device.disable_binding(); profile = device.profile(); service.notify_device_changed(true, profile != nullptr ? profile->name : ""); uint8_t buffer[64]; while (g_running) { // Control plane first (bounded by the read timeout below), then the // keyboard. The loop wakes up at least every read timeout, which also // bounds how long a config change takes to be applied. service.pump(); 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); service.notify_device_changed(false, profile != nullptr ? profile->name : ""); device.close(); if (!device.open()) { return 1; } device.disable_binding(); profile = device.profile(); service.notify_device_changed(true, profile != nullptr ? profile->name : ""); continue; } int index = gkeymapper::daemon::match_gkey(*profile, buffer, n); if (index >= 0 && index < static_cast(mappings.keys.size())) { run_mapping(index, false); const gkeymapper::Mapping& mapping = mappings.keys[index]; service.notify_key_pressed( index, std::string(gkeymapper::action_type_token(mapping.type)) + ":" + mapping.target); } else if (gkeymapper::daemon::is_release_report(*profile, buffer, n)) { // Key released; nothing to do. } // Media keys and LED acknowledgements are ignored. } service.notify_device_changed(false, profile != nullptr ? profile->name : ""); service.pump(); device.close(); return 0; }