Single instance
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user