diff --git a/metadata/input.xml b/metadata/input.xml
index 8914955..493df8f 100644
--- a/metadata/input.xml
+++ b/metadata/input.xml
@@ -19,6 +19,21 @@
<_long>Cancels modifier actions (like `Super` for the `expo` plugin) when held for the specified timeout (in milliseconds). Setting the value to **0** never cancels.
400
+
+
+ <_short>Double and triple press interval
+ <_long>How long a key with a `<double>` or `<triple>` binding waits for its next press (in milliseconds). A single press of such a key fires its plain binding this much later.
+ 300
+ 100
+ 1000
+
+
+ <_short>Long press delay
+ <_long>How long a key with a `<long>` binding must be held to fire it (in milliseconds).
+ 500
+ 200
+ 5000
+
<_short>Key repeat delay
diff --git a/src/api/wayfire/bindings-repository.hpp b/src/api/wayfire/bindings-repository.hpp
index 080d516..0ee10d2 100644
--- a/src/api/wayfire/bindings-repository.hpp
+++ b/src/api/wayfire/bindings-repository.hpp
@@ -60,6 +60,20 @@ class bindings_repository_t
*/
bool handle_key(const wf::keybinding_t& pressed, uint32_t mod_binding_key);
+ /**
+ * Pyr press kinds: the press words (wf::KEYBOARD_PRESS_* bits) that key and activator bindings use with
+ * exactly these modifiers and this key. 0 means the key fires on press as always.
+ */
+ uint32_t press_kinds(const wf::keybinding_t& base) const;
+
+ /**
+ * Pyr press kinds: trigger the bindings of this key pressed this way (`press` is a wf::KEYBOARD_PRESS_* bit,
+ * or 0 for a plain press). Key callbacks receive `base`, without the press word.
+ *
+ * @return true if any of the matching registered bindings consume the event.
+ */
+ bool handle_key_press(const wf::keybinding_t& base, uint32_t press);
+
/** Handle an axis event. */
bool handle_axis(uint32_t modifiers, wlr_pointer_axis_event *ev);
diff --git a/src/core/seat/bindings-repository.cpp b/src/core/seat/bindings-repository.cpp
index 9cefeb6..19bceb6 100644
--- a/src/core/seat/bindings-repository.cpp
+++ b/src/core/seat/bindings-repository.cpp
@@ -46,46 +46,48 @@ void wf::bindings_repository_t::add_activator(
}
}
-bool wf::bindings_repository_t::handle_key(const wf::keybinding_t& pressed,
- uint32_t mod_binding_key)
+// Key and activator bindings matching `match`; key callbacks receive `report` (the same key, without a press
+// word when Pyr's press kinds fire it).
+static bool trigger_key(wf::bindings_repository_t::impl& priv, const wf::keybinding_t& match,
+ const wf::keybinding_t& report, uint32_t mod_binding_key)
{
- if (!priv->enabled)
+ if (!priv.enabled)
{
return false;
}
std::vector> callbacks;
- for (auto& binding : this->priv->keys)
+ for (auto& binding : priv.keys)
{
- if (binding->activated_by->get_value() == pressed)
+ if (binding->activated_by->get_value() == match)
{
/* We must be careful because the callback might be erased,
* so force copy the callback into the lambda */
auto callback = binding->callback;
- callbacks.emplace_back([pressed, callback] ()
+ callbacks.emplace_back([report, callback] ()
{
- return (*callback)(pressed);
+ return (*callback)(report);
});
}
}
- for (auto& binding : this->priv->activators)
+ for (auto& binding : priv.activators)
{
- if (binding->activated_by->get_value().has_match(pressed))
+ if (binding->activated_by->get_value().has_match(match))
{
/* We must be careful because the callback might be erased,
* so force copy the callback into the lambda */
auto callback = binding->callback;
- callbacks.emplace_back([pressed, callback, mod_binding_key] ()
+ callbacks.emplace_back([report, callback, mod_binding_key] ()
{
wf::activator_data_t ev = {
- .source = activator_source_t::KEYBINDING,
- .activation_data = pressed.get_key()
+ .source = wf::activator_source_t::KEYBINDING,
+ .activation_data = report.get_key()
};
if (mod_binding_key)
{
- ev.source = activator_source_t::MODIFIERBINDING;
+ ev.source = wf::activator_source_t::MODIFIERBINDING;
ev.activation_data = mod_binding_key;
}
@@ -103,6 +105,55 @@ bool wf::bindings_repository_t::handle_key(const wf::keybinding_t& pressed,
return handled;
}
+bool wf::bindings_repository_t::handle_key(const wf::keybinding_t& pressed,
+ uint32_t mod_binding_key)
+{
+ return trigger_key(*priv, pressed, pressed, mod_binding_key);
+}
+
+uint32_t wf::bindings_repository_t::press_kinds(const wf::keybinding_t& base) const
+{
+ if (!priv->enabled || (base.get_key() == 0))
+ {
+ return 0;
+ }
+
+ static const uint32_t words[] = {
+ wf::KEYBOARD_PRESS_DOUBLE, wf::KEYBOARD_PRESS_TRIPLE, wf::KEYBOARD_PRESS_LONG
+ };
+ uint32_t found = 0;
+ for (auto& binding : priv->keys)
+ {
+ const auto value = binding->activated_by->get_value();
+ if ((value.get_key() == base.get_key()) && (value.get_modifiers() & wf::KEYBOARD_PRESS_MASK) &&
+ ((value.get_modifiers() & ~(uint32_t)wf::KEYBOARD_PRESS_MASK) == base.get_modifiers()))
+ {
+ found |= value.get_modifiers() & wf::KEYBOARD_PRESS_MASK;
+ }
+ }
+
+ for (auto& binding : priv->activators)
+ {
+ const auto value = binding->activated_by->get_value();
+ for (auto word : words)
+ {
+ if (!(found & word) &&
+ value.has_match(wf::keybinding_t{base.get_modifiers() | word, base.get_key()}))
+ {
+ found |= word;
+ }
+ }
+ }
+
+ return found;
+}
+
+bool wf::bindings_repository_t::handle_key_press(const wf::keybinding_t& base, uint32_t press)
+{
+ const wf::keybinding_t match{base.get_modifiers() | (press & wf::KEYBOARD_PRESS_MASK), base.get_key()};
+ return trigger_key(*priv, match, base, 0);
+}
+
bool wf::bindings_repository_t::handle_axis(uint32_t modifiers,
wlr_pointer_axis_event *ev)
{
diff --git a/src/core/seat/keyboard.cpp b/src/core/seat/keyboard.cpp
index 6fdf8cd..bff733c 100644
--- a/src/core/seat/keyboard.cpp
+++ b/src/core/seat/keyboard.cpp
@@ -1,4 +1,6 @@
#include
+#include
+#include
#include
#include
#include
@@ -15,11 +17,61 @@
#include "wayfire/signal-definitions.hpp"
#include
+static int64_t press_kinds_now()
+{
+ using namespace std::chrono;
+ return duration_cast(steady_clock::now().time_since_epoch()).count();
+}
+
+// Fire what the press-kind machine decided, then arm its timer for the next deadline.
+static void press_kinds_fire(std::vector& fire)
+{
+ for (auto& f : fire)
+ {
+ for (int i = 0; i < f.times; i++)
+ {
+ wf::get_core().bindings->handle_key_press(wf::keybinding_t{f.mods, f.key}, f.press);
+ }
+ }
+
+ fire.clear();
+ auto& seat = wf::get_core_impl().seat;
+ seat->priv->press_timer.disconnect();
+ const int64_t when = seat->priv->press_machine.deadline();
+ if (when >= 0)
+ {
+ const int64_t delay = std::clamp(when - press_kinds_now(), 1, 60000);
+ seat->priv->press_timer.set_timeout(delay, [] ()
+ {
+ std::vector due;
+ wf::get_core_impl().seat->priv->press_machine.timeout(press_kinds_now(), due);
+ press_kinds_fire(due);
+ });
+ }
+}
+
+void wf::press_kinds_cancel(bool forget)
+{
+ auto& seat = wf::get_core_impl().seat;
+
+ if (forget)
+ {
+ seat->priv->press_machine.forget();
+ } else
+ {
+ seat->priv->press_machine.cancel();
+ }
+
+ seat->priv->press_timer.disconnect();
+}
+
void wf::keyboard_t::setup_listeners()
{
on_config_reload = [=] (auto)
{
reload_input_options();
+ // Bindings may have changed: a pending press sequence is dropped.
+ wf::press_kinds_cancel(false);
};
wf::get_core().connect(&on_config_reload);
@@ -395,6 +447,26 @@ bool wf::keyboard_t::handle_keyboard_key(uint32_t key, uint32_t state)
mod_binding_key = 0;
}
+ // Pyr press kinds: a key with , or bound for these modifiers is decided by the
+ // press-kind machine (press-kinds.hpp) and never reaches the app; any other key goes on as always,
+ // after a pending sequence it interrupts has fired.
+ if (!mod)
+ {
+ auto& machine = seat->priv->press_machine;
+ machine.interval_ms = std::clamp(int(wf::option_wrapper_t("input/multi_press_interval")), 100,
+ 1000);
+ machine.long_ms = std::clamp(int(wf::option_wrapper_t("input/long_press_delay")), 200, 5000);
+ const uint32_t mods = get_modifiers();
+ std::vector fire;
+ const bool ours = machine.press(mods, key,
+ wf::get_core().bindings->press_kinds(wf::keybinding_t{mods, key}), press_kinds_now(), fire);
+ press_kinds_fire(fire);
+ if (ours)
+ {
+ return true;
+ }
+ }
+
handled_in_plugin |= wf::get_core().bindings->handle_key(
wf::keybinding_t{get_modifiers(), key}, mod_binding_key);
} else
@@ -414,6 +486,14 @@ bool wf::keyboard_t::handle_keyboard_key(uint32_t key, uint32_t state)
}
mod_binding_key = 0;
+
+ // The release of a press the press-kind machine took is swallowed too.
+ if (!mod)
+ {
+ std::vector fire;
+ handled_in_plugin |= seat->priv->press_machine.release(key, press_kinds_now(), fire);
+ press_kinds_fire(fire);
+ }
}
return handled_in_plugin;
diff --git a/src/core/seat/keyboard.hpp b/src/core/seat/keyboard.hpp
index 32b1e7f..80b302b 100644
--- a/src/core/seat/keyboard.hpp
+++ b/src/core/seat/keyboard.hpp
@@ -15,6 +15,12 @@ enum locked_mods_t
KB_MOD_CAPS_LOCK = 1 << 1,
};
+/**
+ * Pyr press kinds: drop a pending press sequence (bindings reloaded), and with `forget` also the record of
+ * swallowed keys (a keyboard was removed, so their releases never come).
+ */
+void press_kinds_cancel(bool forget);
+
/**
* Represents a logical keyboard.
*/
diff --git a/src/core/seat/press-kinds.hpp b/src/core/seat/press-kinds.hpp
new file mode 100644
index 0000000..89dbf2a
--- /dev/null
+++ b/src/core/seat/press-kinds.hpp
@@ -0,0 +1,220 @@
+#pragma once
+// Pyr press kinds: key bindings with the wf-config press words , and .
+//
+// Only a key that has a press word bound for exactly its modifiers comes here; every other key fires on press as
+// always. Such a key is owned by the compositor: its presses and releases never reach the app.
+//
+// held long enough ( bound, first press) -> fire while still held; the release does nothing
+// released, a higher count bound -> wait the multi-press interval for the next press
+// released, nothing higher bound / interval over -> decide: 1 tap = the plain binding, 2 = ,
+// 3 = ; a count with no binding of its own fires
+// the plain binding once per tap
+// another non-modifier key goes down -> decide now (a held key counts as a tap), then that key
+// is handled normally
+// the same key down again while held -> a repeat (or another keyboard): ignored
+// modifiers -> never handled here; they may change between taps, and
+// the decision fires with the modifiers of the first press
+// cancel (bindings reloaded, a keyboard removed) -> drop the sequence without firing
+//
+// No Wayland here: time is passed in and the caller fires what comes out, so this is unit-tested on its own.
+#include
+#include
+#include
+#include
+
+namespace wf
+{
+namespace press_kinds
+{
+// Fire the bindings of `mods` + `key` with this press bit (0: the plain binding) `times` times.
+struct fire_t
+{
+ uint32_t mods = 0;
+ uint32_t key = 0;
+ uint32_t press = 0;
+ int times = 1;
+
+ bool operator ==(const fire_t& o) const
+ {
+ return mods == o.mods && key == o.key && press == o.press && times == o.times;
+ }
+};
+
+class machine_t
+{
+ public:
+ int interval_ms = 300;
+ int long_ms = 500;
+
+ /**
+ * A non-modifier key went down with these modifiers. `kinds` holds the press words bound for exactly these
+ * modifiers and this key (wf::KEYBOARD_PRESS_* bits). Returns true when the press is handled here (the caller
+ * swallows it). `out` receives what to fire now: a pending sequence that this key settled.
+ */
+ bool press(uint32_t mods, uint32_t key, uint32_t kinds, int64_t now, std::vector& out)
+ {
+ if (on && held && (key == this->key))
+ {
+ return true; // a repeat of the held key, or the same key on another keyboard
+ }
+
+ const bool next_tap = on && !held && (key == this->key) && (mods == this->mods);
+ if (on && !next_tap)
+ {
+ decide(out);
+ }
+
+ if (next_tap)
+ {
+ count++;
+ held = true;
+ until = -1; // a long press counts only on the first press
+ swallowed.insert(key);
+ return true;
+ }
+
+ if (!(kinds & wf::KEYBOARD_PRESS_MASK))
+ {
+ // Not ours. A press that goes to the app supersedes any stale record of this key.
+ swallowed.erase(key);
+ return false;
+ }
+
+ on = true;
+ held = true;
+ long_fired = false;
+ this->mods = mods;
+ this->key = key;
+ this->kinds = kinds;
+ count = 1;
+ until = (kinds & wf::KEYBOARD_PRESS_LONG) ? now + long_ms : -1;
+ swallowed.insert(key);
+ return true;
+ }
+
+ /** A key came up. Returns true when its press was handled here (the caller swallows the release too). */
+ bool release(uint32_t key, int64_t now, std::vector& out)
+ {
+ auto found = swallowed.find(key);
+ if (found == swallowed.end())
+ {
+ return false;
+ }
+
+ swallowed.erase(found);
+ if (on && held && (key == this->key))
+ {
+ held = false;
+ if (long_fired)
+ {
+ reset();
+ } else if (count < most())
+ {
+ until = now + interval_ms;
+ } else
+ {
+ decide(out);
+ }
+ }
+
+ return true;
+ }
+
+ /** Call when now >= deadline(). */
+ void timeout(int64_t now, std::vector& out)
+ {
+ if (!on || (until < 0) || (now < until))
+ {
+ return;
+ }
+
+ until = -1;
+ if (held)
+ {
+ // Only the long-press timer runs while the key is down.
+ if ((kinds & wf::KEYBOARD_PRESS_LONG) && (count == 1) && !long_fired)
+ {
+ out.push_back({mods, key, (uint32_t)wf::KEYBOARD_PRESS_LONG, 1});
+ long_fired = true;
+ }
+
+ return;
+ }
+
+ decide(out);
+ }
+
+ /** When to call timeout(), or -1. */
+ int64_t deadline() const
+ {
+ return on ? until : -1;
+ }
+
+ /** Drop the sequence without firing; releases of swallowed keys are still swallowed. */
+ void cancel()
+ {
+ reset();
+ }
+
+ /** Forget everything, swallowed keys too (a keyboard went away, so its releases never come). */
+ void forget()
+ {
+ reset();
+ swallowed.clear();
+ }
+
+ bool active() const
+ {
+ return on;
+ }
+
+ private:
+ bool on = false, held = false, long_fired = false;
+ uint32_t mods = 0;
+ uint32_t key = 0;
+ uint32_t kinds = 0;
+ int count = 0;
+ int64_t until = -1;
+ std::multiset swallowed;
+
+ // The highest press count bound for this key: decided at once when reached.
+ int most() const
+ {
+ return (kinds & wf::KEYBOARD_PRESS_TRIPLE) ? 3 : (kinds & wf::KEYBOARD_PRESS_DOUBLE) ? 2 : 1;
+ }
+
+ void reset()
+ {
+ on = false;
+ held = false;
+ long_fired = false;
+ count = 0;
+ until = -1;
+ }
+
+ void decide(std::vector& out)
+ {
+ if (!on)
+ {
+ return;
+ }
+
+ if (!long_fired)
+ {
+ if ((count >= 3) && (kinds & wf::KEYBOARD_PRESS_TRIPLE))
+ {
+ out.push_back({mods, key, (uint32_t)wf::KEYBOARD_PRESS_TRIPLE, 1});
+ } else if ((count == 2) && (kinds & wf::KEYBOARD_PRESS_DOUBLE))
+ {
+ out.push_back({mods, key, (uint32_t)wf::KEYBOARD_PRESS_DOUBLE, 1});
+ } else
+ {
+ out.push_back({mods, key, 0, count});
+ }
+ }
+
+ reset();
+ }
+};
+}
+}
diff --git a/src/core/seat/seat-impl.hpp b/src/core/seat/seat-impl.hpp
index 7739695..6480980 100644
--- a/src/core/seat/seat-impl.hpp
+++ b/src/core/seat/seat-impl.hpp
@@ -13,6 +13,7 @@
#include "wayfire/signal-provider.hpp"
#include "wayfire/toplevel-view.hpp"
#include "wayfire/util.hpp"
+#include "press-kinds.hpp"
namespace wf
{
@@ -77,6 +78,10 @@ struct seat_t::impl
wf::scene::node_ptr keyboard_focus;
// Keys sent to the current keyboard focus
std::multiset pressed_keys;
+
+ // Pyr press kinds (keyboard.cpp): one sequence per seat, so presses and releases match across keyboards.
+ wf::press_kinds::machine_t press_machine;
+ wf::wl_timer press_timer;
void transfer_grab(wf::scene::node_ptr new_focus);
/**
diff --git a/src/core/seat/seat.cpp b/src/core/seat/seat.cpp
index 6bf1092..ee98ea5 100644
--- a/src/core/seat/seat.cpp
+++ b/src/core/seat/seat.cpp
@@ -389,6 +389,8 @@ wf::seat_t::seat_t(wl_display *display, std::string name) : seat(wlr_seat_create
});
priv->keyboards.erase(it, priv->keyboards.end());
+ // A key held on it was maybe being decided as a press kind; its release never comes now.
+ wf::press_kinds_cancel(true);
if (current_kbd_destroyed && priv->keyboards.size())
{
diff --git a/test/misc/meson.build b/test/misc/meson.build
index c83f99a..0de92da 100644
--- a/test/misc/meson.build
+++ b/test/misc/meson.build
@@ -18,3 +18,10 @@ object_signal = executable(
dependencies: libwayfire,
install: false)
test('Object and signal test', object_signal)
+
+press_kinds = executable(
+ 'press-kinds-test',
+ 'press-kinds-test.cpp',
+ dependencies: [doctest, wfconfig],
+ install: false)
+test('Press kinds test', press_kinds)
diff --git a/test/misc/press-kinds-test.cpp b/test/misc/press-kinds-test.cpp
new file mode 100644
index 0000000..56890df
--- /dev/null
+++ b/test/misc/press-kinds-test.cpp
@@ -0,0 +1,211 @@
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include
+#include
+#include "../../src/core/seat/press-kinds.hpp"
+
+using wf::press_kinds::fire_t;
+using wf::press_kinds::machine_t;
+
+namespace
+{
+constexpr uint32_t SUPER = wf::KEYBOARD_MODIFIER_LOGO;
+constexpr uint32_t DOUBLE = wf::KEYBOARD_PRESS_DOUBLE;
+constexpr uint32_t TRIPLE = wf::KEYBOARD_PRESS_TRIPLE;
+constexpr uint32_t LONG = wf::KEYBOARD_PRESS_LONG;
+
+struct rig
+{
+ machine_t m;
+ std::vector out;
+ int64_t now = 1000;
+
+ bool press(uint32_t key, uint32_t kinds, uint32_t mods = SUPER)
+ {
+ return m.press(mods, key, kinds, now, out);
+ }
+
+ bool release(uint32_t key)
+ {
+ return m.release(key, now, out);
+ }
+
+ // Advance the clock, firing timeouts on the way like the compositor's timer.
+ void wait(int ms)
+ {
+ const int64_t end = now + ms;
+ while (m.deadline() >= 0 && m.deadline() <= end)
+ {
+ now = m.deadline();
+ m.timeout(now, out);
+ }
+
+ now = end;
+ }
+
+ std::vector take()
+ {
+ auto r = out;
+ out.clear();
+ return r;
+ }
+};
+}
+
+TEST_CASE("keys without press words are not handled")
+{
+ rig r;
+ CHECK(!r.press(KEY_V, 0));
+ CHECK(!r.release(KEY_V));
+ CHECK(r.take().empty());
+}
+
+TEST_CASE("a single tap fires the plain binding after the interval")
+{
+ rig r;
+ CHECK(r.press(KEY_V, DOUBLE));
+ CHECK(r.release(KEY_V));
+ r.wait(299);
+ CHECK(r.take().empty());
+ r.wait(1);
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, 0, 1}});
+ CHECK(!r.m.active());
+}
+
+TEST_CASE("double and triple")
+{
+ rig r;
+ r.press(KEY_V, DOUBLE);
+ r.release(KEY_V);
+ r.wait(100);
+ CHECK(r.press(KEY_V, DOUBLE));
+ CHECK(r.release(KEY_V)); // the most bound: decided at once
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, DOUBLE, 1}});
+
+ for (int i = 0; i < 3; i++)
+ {
+ r.press(KEY_V, DOUBLE | TRIPLE);
+ r.release(KEY_V);
+ r.wait(50);
+ }
+
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, TRIPLE, 1}});
+
+ // Two taps when and are bound: once the interval is over.
+ r.press(KEY_V, DOUBLE | TRIPLE);
+ r.release(KEY_V);
+ r.press(KEY_V, DOUBLE | TRIPLE);
+ r.release(KEY_V);
+ r.wait(300);
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, DOUBLE, 1}});
+}
+
+TEST_CASE("a count with no binding of its own fires the plain binding once per tap")
+{
+ rig r;
+ r.press(KEY_V, TRIPLE);
+ r.release(KEY_V);
+ r.press(KEY_V, TRIPLE);
+ r.release(KEY_V);
+ r.wait(300);
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, 0, 2}});
+}
+
+TEST_CASE("long press")
+{
+ rig r;
+ r.press(KEY_K, LONG);
+ r.wait(499);
+ CHECK(r.take().empty());
+ r.wait(1);
+ CHECK(r.take() == std::vector{{SUPER, KEY_K, LONG, 1}});
+ CHECK(r.release(KEY_K));
+ CHECK(r.take().empty());
+ CHECK(!r.m.active());
+
+ // Released early: with only bound, the plain binding fires at release.
+ r.press(KEY_K, LONG);
+ r.wait(200);
+ r.release(KEY_K);
+ CHECK(r.take() == std::vector{{SUPER, KEY_K, 0, 1}});
+}
+
+TEST_CASE("a long press counts only on the first press")
+{
+ rig r;
+ r.press(KEY_K, LONG | DOUBLE);
+ r.release(KEY_K);
+ r.press(KEY_K, LONG | DOUBLE);
+ r.wait(2000); // held a long time on the second press
+ CHECK(r.take().empty());
+ r.release(KEY_K);
+ CHECK(r.take() == std::vector{{SUPER, KEY_K, DOUBLE, 1}});
+}
+
+TEST_CASE("repeats of the held key are ignored")
+{
+ rig r;
+ r.press(KEY_K, LONG);
+ r.wait(100);
+ CHECK(r.press(KEY_K, LONG)); // repeat: still ours, no new tap, no timer reset
+ r.wait(400);
+ CHECK(r.take() == std::vector{{SUPER, KEY_K, LONG, 1}});
+ r.release(KEY_K);
+}
+
+TEST_CASE("another key settles a pending tap first")
+{
+ rig r;
+ r.press(KEY_V, DOUBLE);
+ r.release(KEY_V);
+ CHECK(!r.press(KEY_C, 0)); // Super+C: the pending Super+V fires first, then C goes on as usual
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, 0, 1}});
+
+ // While the first key is still held: it counts as a tap; its release is still swallowed.
+ r.press(KEY_V, DOUBLE);
+ CHECK(r.press(KEY_X, DOUBLE)); // a key with press words starts its own sequence
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, 0, 1}});
+ CHECK(r.release(KEY_V));
+ CHECK(r.release(KEY_X));
+ r.wait(300);
+ CHECK(r.take() == std::vector{{SUPER, KEY_X, 0, 1}});
+}
+
+TEST_CASE("modifiers of the first press are kept; other modifiers are another key")
+{
+ rig r;
+ r.press(KEY_V, DOUBLE, SUPER);
+ r.release(KEY_V);
+ // Super came up and went down again between the taps: the same key with the same modifiers.
+ r.press(KEY_V, DOUBLE, SUPER);
+ r.release(KEY_V);
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, DOUBLE, 1}});
+
+ r.press(KEY_V, DOUBLE, SUPER);
+ r.release(KEY_V);
+ CHECK(!r.press(KEY_V, 0, 0)); // plain V: settles Super+V, then goes to the app
+ CHECK(r.take() == std::vector{{SUPER, KEY_V, 0, 1}});
+}
+
+TEST_CASE("cancel drops the sequence but keeps swallowing releases")
+{
+ rig r;
+ r.press(KEY_V, DOUBLE);
+ r.m.cancel();
+ CHECK(r.m.deadline() == -1);
+ CHECK(r.release(KEY_V)); // its press never reached the app, so neither does the release
+ r.wait(1000);
+ CHECK(r.take().empty());
+
+ r.press(KEY_V, DOUBLE);
+ r.m.forget(); // a keyboard went away
+ CHECK(!r.release(KEY_V));
+}
+
+TEST_CASE("a stale record is cleared by the next press that goes to the app")
+{
+ rig r;
+ r.press(KEY_V, DOUBLE);
+ r.m.cancel(); // its release never came
+ CHECK(!r.press(KEY_V, 0)); // bindings changed: now a plain key
+ CHECK(!r.release(KEY_V)); // reaches the app, as its press did
+}