Selkie.git | examples/ | typed-counter.raku
#!/usr/bin/env raku
#
# typed-counter.raku — counter.raku, rebuilt on the typed store.
#
# Same app as examples/counter.raku, with the state tree declared as a
# Selkie::Store::Schema class instead of a bare Hash. Demonstrates:
# - Declaring a root schema (including the required `ui` slot)
# - Opting a Store in via :state-class, and handing it to App via :store
# - Handlers using the db effect (validated) and db-update (typed)
# - Typed reads through $store.state in computes and handlers
# - What the schema buys you: uncomment the typo'd handler at the
# bottom to watch a bad key become a loud __effect-error instead of
# a silently ignored state write.
#
# Run with: raku -I lib examples/typed-counter.raku
use Selkie::App;
use Selkie::Store;
use Selkie::Store::Schema;
use Selkie::Layout::VBox;
use Selkie::Layout::HBox;
use Selkie::Widget::Text;
use Selkie::Widget::Button;
use Selkie::Style;
use Selkie::Sizing;
# --- The state tree, as a class -------------------------------------------
# Every root schema declares the framework's `ui` slot; the rest is the
# app's own state. Slots are typed where it helps — a handler that tries
# to write a Str into `count` now fails at dispatch time.
class Counter::Db is Selkie::Store::Schema {
has Selkie::Store::Schema::UI $.ui .= new;
has Int $.count = 0;
}
my $store = Selkie::Store.new(state-class => Counter::Db);
my $app = Selkie::App.new(:$store);
# --- State handlers --------------------------------------------------------
# Same pure (store, payload) -> effects shape as the Hash store. The db
# effect still works (now validated against the slots); db-update is the
# typed alternative — the fn receives the schema instance and returns a
# patched clone.
$store.register-handler('counter/inc', -> $st, %ev {
(db-update => { fn => -> Counter::Db $db { $db.with(count => $db.count + 1) } },);
});
$store.register-handler('counter/dec', -> $st, %ev {
(db-update => { fn => -> Counter::Db $db { $db.with(count => $db.count - 1) } },);
});
$store.register-handler('counter/reset', -> $st, %ev {
(db => { count => 0 },);
});
# --- Widget tree ----------------------------------------------------------
my $root = Selkie::Layout::VBox.new(sizing => Sizing.flex);
$app.add-screen('main', $root);
$app.switch-screen('main');
$root.add: Selkie::Widget::Text.new(
text => ' Selkie Typed Counter — Tab switches focus, Ctrl+Q quits',
sizing => Sizing.fixed(1),
style => Selkie::Style.new(fg => 0x7AA2F7, bold => True),
);
$root.add: Selkie::Widget::Text.new(text => '', sizing => Sizing.flex);
my $count-text = Selkie::Widget::Text.new(
text => '',
sizing => Sizing.fixed(3),
style => Selkie::Style.new(fg => 0xFFFFFF, bold => True),
);
$root.add($count-text);
my $row = Selkie::Layout::HBox.new(sizing => Sizing.fixed(3));
my $dec = Selkie::Widget::Button.new(label => ' - ', sizing => Sizing.flex);
my $inc = Selkie::Widget::Button.new(label => ' + ', sizing => Sizing.flex);
my $reset = Selkie::Widget::Button.new(label => ' reset ', sizing => Sizing.flex);
$row.add($dec);
$row.add($inc);
$row.add($reset);
$root.add($row);
$root.add: Selkie::Widget::Text.new(text => '', sizing => Sizing.flex);
# --- Wire buttons to dispatch ---------------------------------------------
$dec.on-press.tap: -> $ { $store.dispatch('counter/dec') };
$inc.on-press.tap: -> $ { $store.dispatch('counter/inc') };
$reset.on-press.tap: -> $ { $store.dispatch('counter/reset') };
# --- Subscription ---------------------------------------------------------
# The compute reads through the typed accessor — no path strings, and a
# typo here is a compile-adjacent method-not-found, not a silent Nil.
$store.subscribe-with-callback(
'count-display',
-> $s { $s.state.count.Str },
-> $text { $count-text.set-text(" Count: $text") },
$count-text,
);
# No app/init needed: the schema's declared defaults ARE the initial
# state. One synchronous tick primes the display before first render.
$store.tick;
# --- Global keybinds ------------------------------------------------------
$app.on-key('ctrl+q', -> $ { $app.quit });
# --- What the schema catches ----------------------------------------------
# Uncomment to see validation in action: the typo'd key routes through
# __effect-error (naming event + slot) instead of silently growing a
# 'cout' key the UI never reads. Try it, press '+', watch stderr after
# quitting (or register a __effect-error handler that toasts).
#
# $store.register-handler('counter/inc', -> $st, %ev {
# (db => { cout => $st.state.count + 1 },);
# });
# --- Go -------------------------------------------------------------------
$app.focus($inc);
$app.run;