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.

### method collect-escape-burst

```raku
method collect-escape-burst(
    $nc,
    @burst
) returns List
```

How 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

```raku
method burst-text(
    @events
) returns Str
```

The 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

```raku
method escape-event(
    Selkie::Event $ev
) returns Bool
```

True 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

```raku
method strip-terminal-reports(
    @burst
) returns List
```

Drop 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 suppress-duplicate-press

```raku
method suppress-duplicate-press(
    $target,
    Int:D $btn where { ... },
    Int:D $y,
    Int:D $x
) returns Bool
```

True 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

```raku
method is-wheel-id(
    Int $id where { ... }
) returns Bool
```

Wheel events ride the button encoding (4 = up, 5 = down) but are not clicks — see the wheel note in `!dispatch-mouse`.

