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 Nil

Safe, 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 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

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

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

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 note-widget-destroyed

method note-widget-destroyed(
    Mu $
) returns Nil

Called (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 Bool

True 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 Nil

Drop 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 Nil

Prune 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 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

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.

Selkie v0.16.0

High-level TUI framework built on Notcurses

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Notcurses::Native:ver<0.6.5+>:auth<zef:apogee>

Test Dependencies

Provides

  • Selkie
  • Selkie::Align
  • Selkie::Alpha
  • Selkie::App
  • Selkie::App::Internal::Animation
  • Selkie::App::Internal::Dispatch
  • Selkie::App::Internal::ErrorLog
  • Selkie::App::Internal::ErrorLogPlatform
  • Selkie::App::Internal::FocusTree
  • Selkie::App::Internal::HitTest
  • Selkie::App::Internal::IdleBudget
  • Selkie::App::Internal::OverlayTree
  • Selkie::App::Internal::PosixFD
  • Selkie::App::Internal::RenderLoop
  • Selkie::App::Internal::ScreenModalLifecycle
  • Selkie::App::Internal::Terminal
  • Selkie::App::Internal::TerminalPlatform
  • Selkie::App::Internal::TerminalReport
  • Selkie::App::Internal::TerminalSequences
  • Selkie::BorderStyle
  • Selkie::Container
  • Selkie::EffectiveBounds
  • Selkie::Event
  • Selkie::Gradient
  • Selkie::Layout::Allocate
  • Selkie::Layout::HBox
  • Selkie::Layout::Split
  • Selkie::Layout::VBox
  • Selkie::Plot::Palette
  • Selkie::Plot::Scaler
  • Selkie::Plot::Ticks
  • Selkie::ScreenManager
  • Selkie::Sizing
  • Selkie::Store
  • Selkie::Store::Schema
  • Selkie::Store::Schema::UI
  • Selkie::Style
  • Selkie::Test::Focus
  • Selkie::Test::Keys
  • Selkie::Test::Snapshot
  • Selkie::Test::Snapshot::Harness
  • Selkie::Test::SnapshotPlatform
  • Selkie::Test::Store
  • Selkie::Test::Supply
  • Selkie::Test::Tree
  • Selkie::Theme
  • Selkie::Trace
  • Selkie::Tree
  • Selkie::Tween
  • Selkie::Widget
  • Selkie::Widget::Axis
  • Selkie::Widget::BarChart
  • Selkie::Widget::Border
  • Selkie::Widget::Button
  • Selkie::Widget::CardList
  • Selkie::Widget::Checkbox
  • Selkie::Widget::CommandPalette
  • Selkie::Widget::ConfirmModal
  • Selkie::Widget::FileBrowser
  • Selkie::Widget::FocusableByDefault
  • Selkie::Widget::GradientFill
  • Selkie::Widget::Heatmap
  • Selkie::Widget::HelpOverlay
  • Selkie::Widget::Histogram
  • Selkie::Widget::Image
  • Selkie::Widget::Legend
  • Selkie::Widget::LineChart
  • Selkie::Widget::ListView
  • Selkie::Widget::Modal
  • Selkie::Widget::MultiLineInput
  • Selkie::Widget::PasswordStrength
  • Selkie::Widget::Plot
  • Selkie::Widget::ProgressBar
  • Selkie::Widget::RadioGroup
  • Selkie::Widget::RichText
  • Selkie::Widget::RichText::Span
  • Selkie::Widget::ScatterPlot
  • Selkie::Widget::ScrollView
  • Selkie::Widget::Select
  • Selkie::Widget::Sparkline
  • Selkie::Widget::Spinner
  • Selkie::Widget::TabBar
  • Selkie::Widget::Table
  • Selkie::Widget::Text
  • Selkie::Widget::TextInput
  • Selkie::Widget::TextInput::HighlightSpan
  • Selkie::Widget::TextStream
  • Selkie::Widget::Toast
  • Selkie::Widget::ViewportedCardList
  • X::Selkie::WidgetCycle

Documentation

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.