Selkie--App--Internal--Dispatch
NAME
Selkie::App::Internal::Dispatch - internal input dispatch role for Selkie::App
DESCRIPTION
Implementation detail composed by Selkie::App. Use Selkie::App.on-key, Selkie::App.event-supply, and widget handle-event methods from application code.
Input tracing
Setting SELKIE_TERMINAL_DEBUG=1 makes this role log every raw event it pulls off notcurses, and every event a widget claims, to STDERR:
Selkie input at=<timestamp> raw-id=<id> evtype=<n> modifiers=<n> alt=<n> focus=<Widget::Class>
Selkie input handled at=<timestamp> id=<id> by=<Widget::Class>This exists to diagnose input that arrives but never reaches the intended widget ā the class of bug where a keypress is swallowed, misrouted, or lands a keystroke late. It answers "did notcurses deliver it, and who took it?".
The trace deliberately records no effective text, key character, or widget value: only the numeric event identity and the receiving class name. That is what makes it safe to leave enabled while a password field has focus. Do not add decoded text here.
The same variable also enables an unrelated one-shot pixel diagnostic at startup ā see Selkie::App.
method trace-raw-input
method trace-raw-input(
Notcurses::Native::Types::Ncinput $ni
) returns NilSafe, opt-in native input trace. Deliberately records no effective text or widget value: this is usable while a password field has focus.
method collect-escape-burst
method collect-escape-burst(
$nc,
@burst
) returns ListHow many events the fragmented-report guard will look ahead over. Generously above the longest real reply (an XTGETTCAP answer runs to a few dozen characters) and far below a paste, which must keep reaching flush-paste-batch in one piece. How long the guard will hold a bare Escape waiting to see whether an introducer follows it. Deliberately under one hot frame: an Escape keypress closes a modal, and that must stay instant. The cost of this being short is that a terminal reply fragmented exactly after its Escape byte is missed; the cost of it being long is felt by every Escape the user presses. How long the guard will hold an Escape that is already followed by a control-sequence introducer, waiting for the rest of the report to arrive. Matches the quiet-gap the app's start-up drain uses. This window is only ever entered by input that is already a valid partial control sequence and nothing else, so the latency is not something ordinary typing can provoke: the very first character that cannot belong to a report ends the wait immediately and everything collected is dispatched in order. Poll interval while waiting for a fragmented report to complete. Drain the input queue behind an Escape and return the events that survive report-stripping, appending everything read to @burst so the caller can count it. Waits ā briefly, and only while what has arrived so far is still a viable partial report ā for the rest of a fragmented reply. Under load a terminal's answer to a capability query reaches notcurses in pieces, and notcurses replays each piece as its own burst of keypresses; without the wait the guard sees \e[?64;1;2, cannot match it, and types it into whatever has focus a frame before the ;6;9;15;18;21;22c that would have completed it.
method burst-text
method burst-text(
@events
) returns StrThe leading run of single-character events as text, which is what the recogniser works on. Stops at the first event with no character ā a synthesized key, a mouse report, a resize ā because a control sequence cannot span one.
method escape-event
method escape-event(
Selkie::Event $ev
) returns BoolTrue for a bare Escape keypress ā no modifiers, no mouse, the literal 0x1b that notcurses replays as the first byte of an escape sequence it gave up on.
method strip-terminal-reports
method strip-terminal-reports(
@burst
) returns ListDrop every complete terminal report from the front of an Escape-led burst, returning the events that survive. Reports are only ever stripped from the head: once a character that cannot be part of one is reached the rest of the burst is returned untouched, so a reply immediately followed by real typing (which is exactly what the login-screen incident looked like ā \e[?64;1;2;6;9;15;18;21;22c arriving between two of the user's keystrokes) loses the reply and keeps the typing. Events with no character ā synthesized keys, mouse, resize ā end the scan: they cannot be part of a control sequence, and a report cannot span one.
method note-widget-destroyed
method note-widget-destroyed(
Mu $
) returns NilCalled (indirectly, through Selkie::Tree's widget-destroyed observer) whenever any widget in the process is destroyed. Runs on the destroying thread ā possibly the GC finalizer thread ā so it does nothing but raise a flag; !prune-mouse-capture does the work on the render thread.
method capture-routable
method capture-routable(
Mu $w
) returns BoolTrue when a captured widget can still be delivered to: it exists, it has not been torn down, and it is still attached to the surface that owns input. The attachment test is what catches a widget whose screen was swapped out from under a held button; is-destroyed catches the case attachment cannot, since Selkie::Container.remove destroys a child without clearing its parent, leaving a dead widget that still walks up to the root.
method drop-unroutable-captures
method drop-unroutable-captures() returns NilDrop every capture entry that is no longer routable. Cheap and bounded ā the table holds at most one entry per mouse button. The key list is materialised into an Array first: deleting from a Hash while iterating its live .keys Seq is undefined behaviour.
method prune-mouse-capture
method prune-mouse-capture() returns NilPrune the capture table if ā and only if ā a widget has been destroyed since the last check, so the common case costs one atomic read.
method suppress-duplicate-press
method suppress-duplicate-press(
$target,
Int:D $btn where { ... },
Int:D $y,
Int:D $x
) returns BoolTrue when this press should be dropped as a duplicate of the last accepted press: same button, same cell, inside the window. Widgets that expose debounce-ms choose their own window (0 disables); everything else gets the DUPLICATE-PRESS-MS floor, so a driver double-fire can't reach ANY widget twice ā including click-count consumers, where the duplicate would otherwise register as a phantom double-click. The window is keyed on the cell rather than the widget: a duplicate that lands after the first press already changed the layout (committed a dropdown, opened a modal) is still the same physical click and still gets dropped.
method is-wheel-id
method is-wheel-id(
Int $id where { ... }
) returns BoolWheel events ride the button encoding (4 = up, 5 = down) but are not clicks ā see the wheel note in !dispatch-mouse.