Selkie.git | docs/api/ | Selkie.md
NAME
====
Selkie - High-level TUI framework built on Notcurses
SYNOPSIS
========
```raku
use Selkie;
my $app = Selkie::App.new;
my $root = Selkie::Layout::VBox.new(sizing => Sizing.flex);
$app.add-screen('main', $root);
$app.switch-screen('main');
my $log = Selkie::Widget::TextStream.new(sizing => Sizing.flex);
$root.add($log);
$log.append('Hello, Selkie!');
$app.on-key('ctrl+q', -> $ { $app.quit });
$app.run;
```
DESCRIPTION
===========
`Selkie` is the umbrella module — `use Selkie` imports everything in one go, so you can refer to any class by its full name. Most apps work fine with this; finer-grained imports are available if you prefer to be explicit about what's in scope.
The framework itself is organised into several subnamespaces:
* **Core** — [Selkie::App](Selkie--App.md), [Selkie::Widget](Selkie--Widget.md), [Selkie::Container](Selkie--Container.md), [Selkie::Store](Selkie--Store.md), [Selkie::ScreenManager](Selkie--ScreenManager.md)
* **Support types** — [Selkie::Sizing](Selkie--Sizing.md), [Selkie::Style](Selkie--Style.md), [Selkie::Alpha](Selkie--Alpha.md), [Selkie::Gradient](Selkie--Gradient.md), [Selkie::Tween](Selkie--Tween.md), [Selkie::Theme](Selkie--Theme.md), [Selkie::Event](Selkie--Event.md), [Selkie::BorderStyle](Selkie--BorderStyle.md), [Selkie::Align](Selkie--Align.md)
* **Layouts** — [Selkie::Layout::VBox](Selkie--Layout--VBox.md), [Selkie::Layout::HBox](Selkie--Layout--HBox.md), [Selkie::Layout::Split](Selkie--Layout--Split.md)
* **Display widgets** — [Selkie::Widget::Text](Selkie--Widget--Text.md), [Selkie::Widget::RichText](Selkie--Widget--RichText.md), [Selkie::Widget::TextStream](Selkie--Widget--TextStream.md), [Selkie::Widget::Image](Selkie--Widget--Image.md), [Selkie::Widget::ProgressBar](Selkie--Widget--ProgressBar.md), [Selkie::Widget::Spinner](Selkie--Widget--Spinner.md), [Selkie::Widget::PasswordStrength](Selkie--Widget--PasswordStrength.md), [Selkie::Widget::GradientFill](Selkie--Widget--GradientFill.md)
* **Input widgets** — [Selkie::Widget::TextInput](Selkie--Widget--TextInput.md), [Selkie::Widget::MultiLineInput](Selkie--Widget--MultiLineInput.md), [Selkie::Widget::Button](Selkie--Widget--Button.md), [Selkie::Widget::Checkbox](Selkie--Widget--Checkbox.md), [Selkie::Widget::RadioGroup](Selkie--Widget--RadioGroup.md), [Selkie::Widget::Select](Selkie--Widget--Select.md)
* **List / table widgets** — [Selkie::Widget::ListView](Selkie--Widget--ListView.md), [Selkie::Widget::CardList](Selkie--Widget--CardList.md), [Selkie::Widget::ScrollView](Selkie--Widget--ScrollView.md), [Selkie::Widget::Table](Selkie--Widget--Table.md)
* **Chrome widgets** — [Selkie::Widget::Border](Selkie--Widget--Border.md), [Selkie::Widget::Modal](Selkie--Widget--Modal.md), [Selkie::Widget::ConfirmModal](Selkie--Widget--ConfirmModal.md), [Selkie::Widget::FileBrowser](Selkie--Widget--FileBrowser.md), [Selkie::Widget::Toast](Selkie--Widget--Toast.md), [Selkie::Widget::HelpOverlay](Selkie--Widget--HelpOverlay.md)
* **Navigation** — [Selkie::Widget::TabBar](Selkie--Widget--TabBar.md), [Selkie::Widget::CommandPalette](Selkie--Widget--CommandPalette.md)
* **Chart widgets (opt-in)** — [Selkie::Widget::Sparkline](Selkie--Widget--Sparkline.md), [Selkie::Widget::Plot](Selkie--Widget--Plot.md), [Selkie::Widget::BarChart](Selkie--Widget--BarChart.md), [Selkie::Widget::Histogram](Selkie--Widget--Histogram.md), [Selkie::Widget::Heatmap](Selkie--Widget--Heatmap.md), [Selkie::Widget::ScatterPlot](Selkie--Widget--ScatterPlot.md), [Selkie::Widget::LineChart](Selkie--Widget--LineChart.md), [Selkie::Widget::Axis](Selkie--Widget--Axis.md), [Selkie::Widget::Legend](Selkie--Widget--Legend.md), plus the [Selkie::Plot::Palette](Selkie--Plot--Palette.md), [Selkie::Plot::Scaler](Selkie--Plot--Scaler.md), [Selkie::Plot::Ticks](Selkie--Plot--Ticks.md) primitives
Start with [Selkie::App](Selkie--App.md) for the big picture, [Selkie::Widget](Selkie--Widget.md) if you want to write your own widgets, and [Selkie::Store](Selkie--Store.md) for the reactive state model. Every module has runnable examples in its Pod.
The chart widgets and their plot primitives are not pulled in by `use Selkie` (they have heavier dependencies and aren't needed by most apps) — import them explicitly when you reach for them, e.g. `use Selkie::Widget::LineChart;`.
MOUSE SUPPORT
=============
Selkie has first-class mouse support across every prebuilt widget. You don't need to opt in — [Selkie::App](Selkie--App.md) enables button + drag events on construction (via `notcurses_mice_enable`), and the dispatcher routes presses, drags, releases, and the scroll wheel through the same `handle-event` path as keystrokes.
Dispatch model
--------------
* **Keyboard** goes to the focused widget, then ancestors, then global keybinds (unchanged).
* **Mouse** goes to the deepest widget under the cursor, then ancestors, then global keybinds.
* A primary press on a focusable widget gives it focus before the event is delivered, so mouse-driven activation matches keyboard-driven activation.
* **Drag capture**: a press on widget X routes subsequent drag and release events for that button to X, regardless of where the cursor is. This is what makes scrollbar drags and text-selection drags feel right when the cursor leaves the widget.
* **Modal isolation**: clicks outside the active modal are dropped by default. Modals can opt in to dismiss-on-click-outside by passing `:dismiss-on-click-outside` to their constructor — [Selkie::Widget::HelpOverlay](Selkie--Widget--HelpOverlay.md) uses this.
Per-widget API
--------------
Same registration ergonomics as [Selkie::Widget](Selkie--Widget.md)'s `on-key`:
```raku
# Click anywhere on the widget — primary button by default.
$widget.on-click: -> $ev {
say "clicked at row {$widget.local-row($ev)}, col {$widget.local-col($ev)}";
};
# Right-click via :button(3); :button(0) catches any button.
$widget.on-click: -> $ev { open-context-menu }, button => 3;
# Scroll wheel.
$widget.on-scroll: -> $ev {
given $ev.id {
when NCKEY_SCROLL_UP { ... }
when NCKEY_SCROLL_DOWN { ... }
}
};
# Drag (press + motion-with-button-held). Capture keeps these flowing
# even when the cursor leaves the widget's bounds.
$widget.on-drag: -> $ev {
say "drag now at row {$ev.y - $widget.abs-y}";
};
# Low-level escape hatches — fire on every press / release.
$widget.on-mouse-down: -> $ev { ... };
$widget.on-mouse-up: -> $ev { ... };
```
`self.local-row($ev)` and `self.local-col($ev)` translate the event's absolute screen coordinates into widget-local cells (returning `-1` if the event is out of bounds). `contains-point(y, x)` exposes the same hit-test the framework uses internally.
Multi-click
-----------
Press events are annotated with their multiplicity in `$ev.click-count`: 1 for a single click, 2 for a double-click, 3 for a triple-click. The framework counts a press as a continuation of the previous click when it lands on the same cell with the same button within 300 ms. [Selkie::Widget::TextInput](Selkie--Widget--TextInput.md) uses double / triple click to select word / all; [Selkie::Widget::FileBrowser](Selkie--Widget--FileBrowser.md) uses double-click to descend into directories; [Selkie::Widget::ListView](Selkie--Widget--ListView.md) uses double-click to fire `on-activate`.
Duplicate-press suppression
---------------------------
Terminal mouse drivers occasionally deliver two press events for one physical click. The dispatcher drops a press that repeats the previous one — same button, same cell — inside a suppression window, before click-to-focus, click counting, or delivery happen, so a driver double-fire can neither activate a control twice nor masquerade as a double-click. Widgets that expose a `debounce-ms` attribute (Button, Checkbox, Select — default 120 ms, `0` disables) choose their own window; every other widget gets a 40 ms floor — far below the ~80 ms of the fastest human double-click, so multi-click gestures are unaffected. Wheel events are exempt: they ride the button encoding but are not clicks, and flick-scrolling must never be throttled.
Built-in widget behaviours
--------------------------
* **Button**, **Checkbox** — primary click activates / toggles.
* **TabBar** — primary click activates the tab under the cursor.
* **RadioGroup**, **ListView**, **Table** — single-click selects the row; scroll wheel moves the cursor. Table also clicks the column header to cycle sort. ListView and Table fire `on-activate` on double-click.
* **Select** — click toggles the dropdown; scroll wheel scrolls the open dropdown; click on a dropdown row commits.
* **TextInput**, **MultiLineInput** — click positions the caret; drag selects; double-click selects the word; triple-click selects the line / buffer; Ctrl+A selects all; Ctrl+C / Ctrl+X emit on `on-copy` / `on-cut` (the framework doesn't own the system clipboard — wire OSC 52 / notcurses paste-buffer in your app).
* **CardList** — click selects the card under the cursor; scroll wheel moves between cards.
* **ScrollView**, **TextStream** — scroll wheel scrolls; drag on the scrollbar column drags the thumb.
* **ConfirmModal**, **CommandPalette**, **FileBrowser** — clicks fall through to the embedded Button / ListView / TextInput, which handle them with their built-in behaviour.
* **HelpOverlay** — click outside dismisses; click the Close button to close.
Display-only widgets (Text, RichText, Image, Border, ProgressBar, Spinner, Toast, Legend, all charts) don't react to mouse — Border passes through to its content.
AUTHOR
======
Matt Doughty <matt@apogee.guru>
COPYRIGHT AND LICENSE
=====================
Copyright 2026 Matt Doughty
This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.