#include #include #include using namespace Keybinds; using namespace Input; static PBind makeResolverBind(std::vector&& keys, BindFlags flags = 0) { auto bind = CBind::make(std::move(keys), flags, [] { return SBindResult{}; }); EXPECT_TRUE(bind.has_value()); return makeShared(std::move(*bind)); } static SResolvedKey resolverKey(const char* name, xkb_keycode_t code) { return { .sym = xkb_keysym_from_name(name, XKB_KEYSYM_CASE_INSENSITIVE), .code = code, }; } TEST(KeybindsMatchResolver, LongerFullChordSuppressesShorterFullChord) { const auto SHORT = makeResolverBind({"SUPER", "K"}); const auto LONG = makeResolverBind({"SUPER", "Q", "K"}); const auto Q = resolverKey("Q", 24); const auto K = resolverKey("K", 45); const std::array HELD = {Q, K}; const SBindEventContext CONTEXT{ .heldKeys = HELD, .trigger = K, .modifiersNow = HL_MODIFIER_META, }; const std::array CANDIDATES = { SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)}, SBindMatchCandidate{LONG, LONG->matches(CONTEXT)}, }; const auto RESOLUTION = resolveChordMatches(CANDIDATES, CONTEXT); ASSERT_EQ(RESOLUTION.immediate.size(), 1); EXPECT_EQ(RESOLUTION.immediate.front(), LONG); EXPECT_TRUE(RESOLUTION.deferred.empty()); } TEST(KeybindsMatchResolver, FullPrefixWaitsForLongerChord) { const auto SHORT = makeResolverBind({"SUPER", "Q"}); const auto LONG = makeResolverBind({"SUPER", "Q", "K"}); const auto Q = resolverKey("Q", 24); const std::array HELD = {Q}; const SBindEventContext CONTEXT{ .heldKeys = HELD, .trigger = Q, .modifiersNow = HL_MODIFIER_META, }; const std::array CANDIDATES = { SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)}, SBindMatchCandidate{LONG, LONG->matches(CONTEXT)}, }; const auto RESOLUTION = resolveChordMatches(CANDIDATES, CONTEXT); EXPECT_TRUE(RESOLUTION.immediate.empty()); ASSERT_EQ(RESOLUTION.deferred.size(), 1); EXPECT_EQ(RESOLUTION.deferred.front(), SHORT); } TEST(KeybindsMatchResolver, NonPrefixSubChordWaitsForLongerChord) { const auto SHORT = makeResolverBind({"SUPER", "Q"}); const auto LONG = makeResolverBind({"SUPER", "K", "Q"}); const auto Q = resolverKey("Q", 24); const std::array HELD = {Q}; const SBindEventContext CONTEXT{ .heldKeys = HELD, .trigger = Q, .modifiersNow = HL_MODIFIER_META, }; const std::array CANDIDATES = { SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)}, SBindMatchCandidate{LONG, LONG->matches(CONTEXT)}, }; const auto RESOLUTION = resolveChordMatches(CANDIDATES, CONTEXT); EXPECT_TRUE(RESOLUTION.immediate.empty()); ASSERT_EQ(RESOLUTION.deferred.size(), 1); EXPECT_EQ(RESOLUTION.deferred.front(), SHORT); } TEST(KeybindsMatchResolver, MiddleSubChordWaitsForLongerChord) { const auto SHORT = makeResolverBind({"SUPER", "D"}); const auto LONG = makeResolverBind({"SUPER", "X", "D", "F"}); const auto X = resolverKey("X", 53); const auto D = resolverKey("D", 40); const std::array HELD = {X, D}; const SBindEventContext CONTEXT{ .heldKeys = HELD, .trigger = D, .modifiersNow = HL_MODIFIER_META, }; const std::array CANDIDATES = { SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)}, SBindMatchCandidate{LONG, LONG->matches(CONTEXT)}, }; const std::array REVERSED_CANDIDATES = { SBindMatchCandidate{LONG, LONG->matches(CONTEXT)}, SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)}, }; for (const auto& candidates : {CANDIDATES, REVERSED_CANDIDATES}) { const auto RESOLUTION = resolveChordMatches(candidates, CONTEXT); EXPECT_TRUE(RESOLUTION.immediate.empty()); ASSERT_EQ(RESOLUTION.deferred.size(), 1); EXPECT_EQ(RESOLUTION.deferred.front(), SHORT); } } TEST(KeybindsMatchResolver, SidedModifierDoesNotIncreaseChordLength) { const auto SHORT = makeResolverBind({"SHIFT_L", "Q"}); const auto LONG = makeResolverBind({"SHIFT", "Q", "K"}); const SResolvedKey SHIFT{ .sym = XKB_KEY_Shift_L, .code = 50, .modifier = HL_MODIFIER_SHIFT, }; const auto Q = resolverKey("Q", 24); const std::array HELD = {SHIFT, Q}; const SBindEventContext CONTEXT{ .heldKeys = HELD, .trigger = Q, .modifiersNow = HL_MODIFIER_SHIFT, }; const std::array CANDIDATES = { SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)}, SBindMatchCandidate{LONG, LONG->matches(CONTEXT)}, }; const auto RESOLUTION = resolveChordMatches(CANDIDATES, CONTEXT); EXPECT_EQ(SHORT->chordSize(), 1); EXPECT_EQ(LONG->chordSize(), 2); EXPECT_TRUE(RESOLUTION.immediate.empty()); ASSERT_EQ(RESOLUTION.deferred.size(), 1); EXPECT_EQ(RESOLUTION.deferred.front(), SHORT); } TEST(KeybindsMatchResolver, SidedModifierSubChordIsSuppressed) { const auto SHORT = makeResolverBind({"SHIFT_L", "K"}); const auto LONG = makeResolverBind({"SHIFT", "Q", "K"}); const SResolvedKey SHIFT{ .sym = XKB_KEY_Shift_L, .code = 50, .modifier = HL_MODIFIER_SHIFT, }; const auto Q = resolverKey("Q", 24); const auto K = resolverKey("K", 45); const std::array HELD = {SHIFT, Q, K}; const SBindEventContext CONTEXT{ .heldKeys = HELD, .trigger = K, .modifiersNow = HL_MODIFIER_SHIFT, }; EXPECT_TRUE(SHORT->isSubChordOf(*LONG, CONTEXT)); } TEST(KeybindsMatchResolver, LongPressPrefixIsDeferred) { const auto SHORT = makeResolverBind({"SUPER", "Q"}, BIND_FLAG_LONG_PRESS); const auto LONG = makeResolverBind({"SUPER", "Q", "K"}); const auto Q = resolverKey("Q", 24); const std::array HELD = {Q}; const SBindEventContext CONTEXT{ .heldKeys = HELD, .trigger = Q, .modifiersNow = HL_MODIFIER_META, }; const std::array CANDIDATES = { SBindMatchCandidate{SHORT, SHORT->matches(CONTEXT)}, SBindMatchCandidate{LONG, LONG->matches(CONTEXT)}, }; const auto RESOLUTION = resolveChordMatches(CANDIDATES, CONTEXT); EXPECT_TRUE(RESOLUTION.immediate.empty()); ASSERT_EQ(RESOLUTION.deferred.size(), 1); EXPECT_EQ(RESOLUTION.deferred.front(), SHORT); } TEST(KeybindsMatchResolver, ReleaseChordArmsOnlyWhenFullyHeld) { const auto BIND = makeResolverBind({"SUPER", "Q", "K"}, BIND_FLAG_RELEASE); const auto Q = resolverKey("Q", 24); const auto K = resolverKey("K", 45); const std::array K_ONLY_HELD = {K}; const SBindEventContext INCOMPLETE{ .heldKeys = K_ONLY_HELD, .trigger = K, .modifiersNow = HL_MODIFIER_META, }; const std::array FULLY_HELD = {Q, K}; const SBindEventContext COMPLETE{ .heldKeys = FULLY_HELD, .trigger = K, .modifiersNow = HL_MODIFIER_META, }; EXPECT_FALSE(BIND->isFullyHeld(INCOMPLETE)); EXPECT_TRUE(BIND->isFullyHeld(COMPLETE)); }