Selkie.git | t/ | 04-event.rakutest
use Test;
use lib 'lib';
use Selkie::Event;
use Notcurses::Native::Types;
plan 16;
subtest "Keybind.parse - simple key" => {
plan 2;
my $kb = Keybind.parse('q', -> $ { });
is $kb.id, 'q'.ord, "id is ord of q";
is $kb.modifiers.elems, 0, "no modifiers";
};
subtest "Keybind.parse - ctrl+key" => {
plan 2;
my $kb = Keybind.parse('ctrl+q', -> $ { });
is $kb.id, 'q'.ord, "id is ord of q";
ok Mod-Ctrl ∈ $kb.modifiers, "ctrl modifier present";
};
subtest "Keybind.parse - alt+key" => {
plan 2;
my $kb = Keybind.parse('alt+s', -> $ { });
is $kb.id, 's'.ord, "id is ord of s";
ok Mod-Alt ∈ $kb.modifiers, "alt modifier present";
};
subtest "Keybind.parse - multiple modifiers" => {
plan 3;
my $kb = Keybind.parse('ctrl+shift+x', -> $ { });
is $kb.id, 'x'.ord, "id is ord of x";
ok Mod-Ctrl ∈ $kb.modifiers, "ctrl present";
ok Mod-Shift ∈ $kb.modifiers, "shift present";
};
subtest "Keybind.parse - special keys" => {
plan 6;
is Keybind.parse('enter', -> $ { }).id, NCKEY_ENTER, "enter";
is Keybind.parse('tab', -> $ { }).id, NCKEY_TAB, "tab";
is Keybind.parse('esc', -> $ { }).id, NCKEY_ESC, "esc";
is Keybind.parse('space', -> $ { }).id, NCKEY_SPACE, "space";
is Keybind.parse('backspace', -> $ { }).id, NCKEY_BACKSPACE, "backspace";
is Keybind.parse('delete', -> $ { }).id, NCKEY_DEL, "delete";
};
subtest "Keybind.parse - function keys" => {
plan 3;
is Keybind.parse('f1', -> $ { }).id, NCKEY_F00 + 1, "f1";
is Keybind.parse('f12', -> $ { }).id, NCKEY_F00 + 12, "f12";
is Keybind.parse('ctrl+f5', -> $ { }).id, NCKEY_F00 + 5, "ctrl+f5";
};
subtest "Keybind.parse - navigation keys" => {
plan 4;
is Keybind.parse('up', -> $ { }).id, NCKEY_UP, "up";
is Keybind.parse('down', -> $ { }).id, NCKEY_DOWN, "down";
is Keybind.parse('left', -> $ { }).id, NCKEY_LEFT, "left";
is Keybind.parse('right', -> $ { }).id, NCKEY_RIGHT, "right";
};
subtest "Keybind.parse - ctrl+digit, all ten digits" => {
plan 22;
# Consumers bind perspective/tab hotkeys as ctrl+1..ctrl+9 and wrap
# the tenth onto ctrl+0 (Mindmoor's perspective row). Digits go
# through the generic single-char arm of the parser, so all ten —
# including 0 — must parse and match with an exact Ctrl modifier
# set. Pinned here because at least one consumer comment claimed
# ctrl+0 was unsupported; it never was.
for '0' .. '9' -> $d {
my $kb = Keybind.parse("ctrl+$d", -> $ { });
is $kb.id, $d.ord, "ctrl+$d parses to codepoint of '$d'";
my $ev = Selkie::Event.new(
id => $d.ord,
modifiers => Set(Mod-Ctrl),
input-type => NCTYPE_PRESS,
event-type => KeyEvent,
);
ok $kb.matches($ev), "ctrl+$d keybind matches ctrl+$d event";
}
my $kb0 = Keybind.parse('ctrl+0', -> $ { });
my $plain = Selkie::Event.new(
id => '0'.ord,
modifiers => Set(),
input-type => NCTYPE_PRESS,
event-type => KeyEvent,
);
nok $kb0.matches($plain), "ctrl+0 does not match a plain 0 keystroke";
dies-ok { Keybind.parse('ctrl+10', -> $ { }) },
"two-digit ctrl+10 is rejected (no such key name)";
};
subtest "Keybind.parse - invalid modifier dies" => {
plan 1;
dies-ok { Keybind.parse('foo+q', -> $ { }) }, "unknown modifier dies";
};
subtest "Keybind.parse - invalid key dies" => {
plan 1;
dies-ok { Keybind.parse('f99', -> $ { }) }, "invalid function key dies";
};
subtest "Keybind.matches" => {
plan 3;
my $kb = Keybind.parse('ctrl+q', -> $ { });
my $ev-match = Selkie::Event.new(
id => 'q'.ord,
modifiers => Set(Mod-Ctrl),
input-type => NCTYPE_PRESS,
event-type => KeyEvent,
);
ok $kb.matches($ev-match), "matches ctrl+q event";
my $ev-no-mod = Selkie::Event.new(
id => 'q'.ord,
modifiers => Set(),
input-type => NCTYPE_PRESS,
event-type => KeyEvent,
);
nok $kb.matches($ev-no-mod), "does not match plain q";
my $ev-wrong-mod = Selkie::Event.new(
id => 'q'.ord,
modifiers => Set(Mod-Alt),
input-type => NCTYPE_PRESS,
event-type => KeyEvent,
);
nok $kb.matches($ev-wrong-mod), "does not match alt+q";
};
subtest "Keybind.parse - literal '+' key" => {
plan 9;
# Plain '+' — historically this died because '+'.split('+') yields
# ('','') and the parser saw an empty modifier. Now special-cased.
my $plus = Keybind.parse('+', -> $ { });
is $plus.id, '+'.ord, "plain '+' parses as the '+' codepoint";
is $plus.modifiers.elems, 0, "no modifiers";
# Modifier + '+': trailing '++' peels off as the key.
my $shift-plus = Keybind.parse('shift++', -> $ { });
is $shift-plus.id, '+'.ord, "shift++ id is '+'";
ok Mod-Shift ∈ $shift-plus.modifiers, "shift++ has Mod-Shift";
my $ctrl-plus = Keybind.parse('ctrl++', -> $ { });
is $ctrl-plus.id, '+'.ord, "ctrl++ id is '+'";
ok Mod-Ctrl ∈ $ctrl-plus.modifiers, "ctrl++ has Mod-Ctrl";
my $ctrl-shift-plus = Keybind.parse('ctrl+shift++', -> $ { });
is $ctrl-shift-plus.id, '+'.ord, "ctrl+shift++ id is '+'";
ok Mod-Ctrl ∈ $ctrl-shift-plus.modifiers, "ctrl+shift++ has Mod-Ctrl";
ok Mod-Shift ∈ $ctrl-shift-plus.modifiers, "ctrl+shift++ has Mod-Shift";
};
# --- from-ncinput: modifier normalisation ----------------------------------
#
# Selkie::Event.from-ncinput is the single place raw notcurses input
# becomes a Selkie event, so it is where the three keyboard encodings
# are flattened into one modifier Set. The tests below pin the two
# legacy decodings, because they are invisible in the ncinput
# `modifiers` bitmask and easy to lose in a refactor.
subtest "from-ncinput - modern modifier bits" => {
plan 4;
my $ev = Selkie::Event.from-ncinput(
Ncinput.new(id => NCKEY_ENTER, modifiers => NCKEY_MOD_ALT));
ok Mod-Alt ∈ $ev.modifiers, 'NCKEY_MOD_ALT decoded to Mod-Alt';
is $ev.modifiers.elems, 1, 'and nothing else';
my $both = Selkie::Event.from-ncinput(Ncinput.new(
id => NCKEY_ENTER,
modifiers => NCKEY_MOD_ALT +| NCKEY_MOD_SHIFT,
));
ok Mod-Alt ∈ $both.modifiers, 'Alt in a combo';
ok Mod-Shift ∈ $both.modifiers, 'Shift in a combo';
};
subtest "from-ncinput - legacy ESC-prefix Alt" => {
plan 4;
# Terminals without the kitty protocol / XTMODKEYS send Alt+key as
# ESC then key. notcurses folds the pair into one ncinput but
# records it only in the deprecated `alt` boolean, leaving the
# `modifiers` bitmask at zero (src/lib/automaton.c, escape-root
# fallback). Without this normalisation Alt+Enter would be
# indistinguishable from a bare Enter on every legacy terminal.
my $ev = Selkie::Event.from-ncinput(
Ncinput.new(id => NCKEY_ENTER, alt => 1));
ok Mod-Alt ∈ $ev.modifiers, 'legacy alt boolean decoded to Mod-Alt';
is $ev.modifiers.elems, 1, 'no other modifier fabricated';
is $ev.id, NCKEY_ENTER, 'id preserved';
# Same for a printable key, which is the far more common case.
my $alt-x = Selkie::Event.from-ncinput(
Ncinput.new(id => 'x'.ord, alt => 1));
ok Mod-Alt ∈ $alt-x.modifiers, 'legacy Alt+x decoded';
};
subtest "from-ncinput - no Alt means no Mod-Alt" => {
plan 2;
my $ev = Selkie::Event.from-ncinput(Ncinput.new(id => NCKEY_ENTER));
nok Mod-Alt ∈ $ev.modifiers, 'bare Enter carries no Alt';
is $ev.modifiers.elems, 0, 'no modifiers at all';
};
subtest "from-ncinput - both encodings match the same keybind" => {
plan 3;
# The point of normalising: one 'alt+enter' bind fires whatever
# the terminal speaks.
my $kb = Keybind.parse('alt+enter', -> $ { });
my $modern = Selkie::Event.from-ncinput(
Ncinput.new(id => NCKEY_ENTER, modifiers => NCKEY_MOD_ALT));
my $legacy = Selkie::Event.from-ncinput(
Ncinput.new(id => NCKEY_ENTER, alt => 1));
ok $kb.matches($modern), 'kitty / XTMODKEYS encoding matches';
ok $kb.matches($legacy), 'legacy ESC-prefix encoding matches';
nok $kb.matches(Selkie::Event.from-ncinput(Ncinput.new(id => NCKEY_ENTER))),
'a bare Enter still does not';
};