Single instance

This commit is contained in:
2026-10-18 16:59:51 +02:00
parent 798708b3e7
commit 74503c27dd
19 changed files with 670 additions and 99 deletions

View File

@@ -2,6 +2,7 @@
#include "action.h"
#include <algorithm>
#include <cstdio>
#include <vector>
@@ -19,17 +20,28 @@ bool ActionRunner::init() {
return true;
}
bool ActionRunner::resolve_shortcut(const std::string& target,
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(target, steps, resolve)) {
std::fprintf(stderr, "gkeymapper-daemon: invalid shortcut '%s'\n", target.c_str());
return false;
}
return true;
}
void ActionRunner::run(const gkeymapper::Mapping& mapping) {
switch (mapping.type) {
case gkeymapper::ActionType::Key: {
case gkeymapper::ActionType::Key:
case gkeymapper::ActionType::KeyHold: {
// A test (TestMapping) and any other caller that cannot wait for a
// key release fires a hold mapping as a tap. A real G-key press goes
// through begin_hold/end_hold instead.
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());
if (!resolve_shortcut(mapping.target, steps)) {
return;
}
keyboard_.play(steps);
@@ -44,6 +56,82 @@ void ActionRunner::run(const gkeymapper::Mapping& mapping) {
}
}
bool ActionRunner::begin_hold(int index, const gkeymapper::Mapping& mapping) {
if (index < 0 || index >= static_cast<int>(held_.size())) {
return false;
}
end_hold(index); // a repeated press must not stack codes
std::vector<gkeymapper::ShortcutStep> steps;
if (!resolve_shortcut(mapping.target, steps)) {
return false;
}
if (steps.size() != 1) {
// Holding only means something for a single step (that is where the
// "same duration as the key press" idea comes from, see README). A
// sequence would have to hold several chords at once, so it is replayed
// as a tap instead - exactly like a "key" mapping.
std::fprintf(stderr,
"gkeymapper-daemon: 'keyhold:%s' is not a single step, replaying it "
"as a tap\n",
mapping.target.c_str());
keyboard_.play(steps);
return false;
}
if (!keyboard_.is_open()) {
return false; // no virtual keyboard: nothing to hold
}
const std::vector<unsigned short> codes = step_key_codes(steps.front());
if (codes.empty()) {
return false;
}
held_[static_cast<size_t>(index)] = codes;
keyboard_.press(codes);
return true;
}
void ActionRunner::end_hold(int index) {
if (index < 0 || index >= static_cast<int>(held_.size())) {
return;
}
std::vector<unsigned short>& codes = held_[static_cast<size_t>(index)];
if (codes.empty()) {
return;
}
// Release in press order; UinputKeyboard::release() turns that into the
// reverse order, so the key goes up first and the modifiers go up last. A
// code another G key also holds stays down until *its* G key is released.
std::vector<unsigned short> up;
for (unsigned short code : codes) {
bool still_held = false;
for (size_t other = 0; other < held_.size(); ++other) {
if (other == static_cast<size_t>(index)) {
continue;
}
if (std::find(held_[other].begin(), held_[other].end(), code) !=
held_[other].end()) {
still_held = true;
break;
}
}
if (!still_held) {
up.push_back(code);
}
}
codes.clear();
if (!up.empty()) {
keyboard_.release(up);
}
}
void ActionRunner::release_held() {
for (size_t i = 0; i < held_.size(); ++i) {
end_hold(static_cast<int>(i));
}
}
void ActionRunner::tap_key(unsigned short code) {
keyboard_.tap(code);
}

View File

@@ -2,7 +2,9 @@
#ifndef GKEYMAPPER_DAEMON_ACTION_H
#define GKEYMAPPER_DAEMON_ACTION_H
#include <array>
#include <string>
#include <vector>
#include "keymap.h"
#include "mapping.h"
@@ -18,6 +20,19 @@ public:
void run(const gkeymapper::Mapping& mapping);
// "keyhold": presses the keys of the mapping and keeps them down until the G
// key is released. Returns whether anything is being held; a sequence, an
// unusable target or a missing virtual keyboard fall back to a tap (false),
// so the caller does not have to special-case them. Releasing index 0..8 is
// done by end_hold(), all of them at once by release_held().
bool begin_hold(int index, const gkeymapper::Mapping& mapping);
void end_hold(int index);
// Let go of every held key. Called on device loss, on shutdown and before a
// new device is claimed: a virtual key that stayed down because its release
// report never arrived would look like a stuck key to the whole session.
void release_held();
// Replays one key press + release. Used by the media-key passthrough, which
// is not a mapping: the keyboard reports mute/volume/play only on the
// interface the daemon holds, so they are forwarded verbatim.
@@ -26,8 +41,15 @@ public:
private:
void run_command(const std::string& command);
// Parses mapping.target through the active layout. Logs and returns false on
// an invalid shortcut.
bool resolve_shortcut(const std::string& target,
std::vector<gkeymapper::ShortcutStep>& steps);
UinputKeyboard keyboard_;
gkeymapper::Keymap keymap_;
// Codes each G key (0 = G1) currently keeps down, empty when not holding.
std::array<std::vector<unsigned short>, gkeymapper::kNumKeys> held_{};
};
} // namespace gkeymapper::daemon

View File

@@ -7,6 +7,7 @@
// execution), M3 (config watching + lifecycle), M4 (D-Bus GUI channel) and
// M5 (hot-plug, repeat/chord decoding, error reporting to the GUI) are done.
#include <algorithm>
#include <csignal>
#include <cstdint>
#include <cstdio>
@@ -81,8 +82,10 @@ int main(int argc, char** argv) {
// Runs the mapping of G<index+1> (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 {
// path. `hold` is set for a real press, where a "keyhold" mapping keeps its
// keys down until the G key is released; a test always taps. Returns whether
// there was an action to run.
auto run_mapping = [&](int index, bool test, bool hold) -> bool {
if (index < 0 || index >= static_cast<int>(mappings.keys.size())) {
return false;
}
@@ -94,7 +97,11 @@ int main(int argc, char** argv) {
test ? " (test)" : "");
std::fflush(stdout);
if (usable) {
actions.run(mapping);
if (hold && mapping.type == gkeymapper::ActionType::KeyHold) {
actions.begin_hold(index, mapping);
} else {
actions.run(mapping);
}
}
return usable;
};
@@ -110,7 +117,7 @@ int main(int argc, char** argv) {
return profile != nullptr ? std::string(profile->name) : std::string();
});
service.set_test([&](int index, std::string& action) {
if (!run_mapping(index, true)) {
if (!run_mapping(index, true, false)) {
return false;
}
const gkeymapper::Mapping& mapping = mappings.keys[index];
@@ -194,6 +201,8 @@ int main(int argc, char** argv) {
// Lost the keyboard (unplugged, or the kernel took the interface
// back). Report it and keep looking; the daemon stays up.
std::fprintf(stderr, "gkeymapper-daemon: read error (%d), device lost\n", n);
// A held key would stay down forever without its release report.
actions.release_held();
service.notify_device_changed(false, profile != nullptr ? profile->name : "");
device.close();
profile = nullptr;
@@ -207,19 +216,29 @@ int main(int argc, char** argv) {
}
uint32_t state = 0;
if (gkeymapper::daemon::decode_gkey_state(*profile, buffer, n, state)) {
// Only a key that was not pressed before fires: a held key repeats its
// report, and a chord arrives as one report with several bits set, so
// every key of it fires exactly once.
for (size_t i = 0; i < mappings.keys.size() && i < gkeymapper::kNumKeys; ++i) {
// Only a *change* of a bit acts: a held key repeats its report, and a
// chord arrives as one report with several bits set, so every key of
// it fires exactly once. The falling edge matters for "keyhold",
// which lets its keys go when the G key comes up.
const size_t count =
std::min(mappings.keys.size(), static_cast<size_t>(gkeymapper::kNumKeys));
for (size_t i = 0; i < count; ++i) {
const uint32_t bit = 1u << i;
if ((state & bit) == 0 || (pressed & bit) != 0) {
const bool down = (state & bit) != 0;
const bool was_down = (pressed & bit) != 0;
if (down == was_down) {
continue;
}
if (!down) {
// No-op unless this G key holds a "keyhold" mapping.
actions.end_hold(static_cast<int>(i));
continue;
}
const gkeymapper::Mapping& mapping = mappings.keys[i];
const std::string action =
std::string(gkeymapper::action_type_token(mapping.type)) + ":" +
mapping.target;
run_mapping(static_cast<int>(i), false);
run_mapping(static_cast<int>(i), false, true);
service.notify_key_pressed(static_cast<int>(i), action);
}
pressed = state;
@@ -247,6 +266,9 @@ int main(int argc, char** argv) {
}
}
// Shutting down: a "keyhold" key that is still down belongs to the session,
// not to the daemon, so let it go before the virtual keyboard disappears.
actions.release_held();
service.notify_device_changed(false, profile != nullptr ? profile->name : "");
service.pump();
device.close();

View File

@@ -2,6 +2,7 @@
#include "uinput_keyboard.h"
#include <algorithm>
#include <cstdio>
#include <fcntl.h>
#include <initializer_list>
@@ -16,10 +17,7 @@
namespace gkeymapper::daemon {
namespace {
// Key code for a single modifier bit (the "left" variants are enough).
unsigned short modifier_code(unsigned bit) {
unsigned short modifier_key_code(unsigned bit) {
switch (bit) {
case gkeymapper::kModCtrl: return KEY_LEFTCTRL;
case gkeymapper::kModAlt: return KEY_LEFTALT;
@@ -30,7 +28,29 @@ unsigned short modifier_code(unsigned bit) {
}
}
} // namespace
std::vector<unsigned short> step_key_codes(const gkeymapper::ShortcutStep& step) {
std::vector<unsigned short> codes;
for (unsigned bit : {gkeymapper::kModCtrl, gkeymapper::kModAlt,
gkeymapper::kModShift, gkeymapper::kModSuper,
gkeymapper::kModAltGr}) {
if ((step.modifiers & bit) == 0) {
continue;
}
unsigned short code = modifier_key_code(bit);
if (code != 0) {
codes.push_back(code);
}
}
if (step.linux_key >= 0) {
// "ctrl+leftctrl" and the like would press the same code twice; keep
// the list unique so press()/release() stay balanced.
unsigned short code = static_cast<unsigned short>(step.linux_key);
if (std::find(codes.begin(), codes.end(), code) == codes.end()) {
codes.push_back(code);
}
}
return codes;
}
UinputKeyboard::~UinputKeyboard() {
close();
@@ -110,40 +130,40 @@ void UinputKeyboard::tap(unsigned short code) {
sync();
}
void UinputKeyboard::press(const std::vector<unsigned short>& codes) {
if (fd_ < 0) {
return;
}
for (unsigned short code : codes) {
key_event(code, true);
}
sync();
}
void UinputKeyboard::release(const std::vector<unsigned short>& codes) {
if (fd_ < 0) {
return;
}
// Reverse order: the modifiers go up last.
for (auto it = codes.rbegin(); it != codes.rend(); ++it) {
key_event(*it, false);
}
sync();
}
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);
}
}
const std::vector<unsigned short> codes = step_key_codes(step);
if (codes.empty()) {
continue;
}
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();
press(codes);
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();
release(codes);
usleep(2000);
}
}

View File

@@ -8,6 +8,15 @@
namespace gkeymapper::daemon {
// Key code for a single modifier bit (the "left" variants are enough), 0 when
// the bit is not a modifier.
unsigned short modifier_key_code(unsigned bit);
// Expands one shortcut step into the key codes to press: the modifiers first,
// then the key itself. Used by play() and by the KeyHold action, which has to
// release exactly the codes it pressed.
std::vector<unsigned short> step_key_codes(const gkeymapper::ShortcutStep& step);
// A virtual keyboard created through /dev/uinput. It is only used to replay
// shortcuts; the daemon never reads from it.
class UinputKeyboard {
@@ -30,6 +39,12 @@ public:
// passes them on unchanged instead of turning them into a mapping.
void tap(unsigned short code);
// Presses, respectively releases, all codes in one EV_SYN block (release in
// reverse order). Splitting press and release is what "keyhold" needs: the
// caller keeps the list and releases it when the G key comes up.
void press(const std::vector<unsigned short>& codes);
void release(const std::vector<unsigned short>& codes);
private:
void emit(unsigned short type, unsigned short code, int value);
void sync();