# Pastebin WM2OHNR2 #!/usr/bin/env raku # use Selkie::UI; # --- Themes ---------------------------------------------------------------- sub dark-theme(--> Selkie::Theme) { Theme( base => Style(:fg(0xEEEEEE), :bg(0x16162E) ), text => Style(:fg(0xEEEEEE) ), text-dim => Style(:fg(0x888899) ), text-highlight => Style(:fg(0xFFFFFF), :bold ), border => Style(:fg(0x444466) ), border-focused => Style(:fg(0x7AA2F7), :bold ), input => Style(:fg(0xEEEEEE), :bg(0x1A1A2E) ), input-focused => Style(:fg(0xFFFFFF), :bg(0x2A2A3E) ), input-placeholder => Style(:fg(0x606080), :italic ), scrollbar-track => Style(:fg(0x333344) ), scrollbar-thumb => Style(:fg(0x7AA2F7) ), divider => Style(:fg(0x444466) ), tab-active => Style(:fg(0xFFFFFF), :bg(0x7AA2F7), :bold), tab-inactive => Style(:fg(0x8888A0), :bg(0x16162E) ), ); } sub light-theme(--> Selkie::Theme) { Theme( base => Style(:fg(0x222222), :bg(0xF5F5F0) ), text => Style(:fg(0x222222) ), text-dim => Style(:fg(0x666666) ), text-highlight => Style(:fg(0x000000), :bold ), border => Style(:fg(0xCCCCCC) ), border-focused => Style(:fg(0x3366CC), :bold ), input => Style(:fg(0x222222), :bg(0xFFFFFF) ), input-focused => Style(:fg(0x000000), :bg(0xEEEEFF) ), input-placeholder => Style(:fg(0x999999), :italic ), scrollbar-track => Style(:fg(0xDDDDDD) ), scrollbar-thumb => Style(:fg(0x3366CC) ), divider => Style(:fg(0xCCCCCC) ), tab-active => Style(:fg(0xFFFFFF), :bg(0x3366CC), :bold), tab-inactive => Style(:fg(0x666688), :bg(0xF5F5F0) ), ); } my $input; my $app = App( { # --- Store handlers ------------------------------------------------------- # # Each message is { speaker => Str, text => Str }. Handler 'app/init', -> $st, %ev { (db => { :messages[ { speaker => 'bot', text => 'Hello! Type something and I will echo it back, transformed.' }, ], :dark, },); } Handler 'chat/send', -> $st, %ev { my $text = (%ev // '').trim; if $text.chars == 0 { (); } else { my @msgs = ($st.get-in('messages') // []).Array; @msgs.push({ speaker => 'you', :$text }); # Synthesize a "bot" reply by reversing the input my $reply = $text.flip; @msgs.push({ speaker => 'bot', text => "Reversed: $reply" }); (:db{ :messages[@msgs] },); } } Handler 'chat/clear', -> $st, %ev { (:db{ :messages[] },); } Handler 'theme/toggle', -> $st, %ev { (:db{ dark => !($st.get-in('dark') // True) },); } # --- Widget tree ---------------------------------------------------------- VBox({ Text( ' Echo Chat — Ctrl+Enter send, Ctrl+L clear, Ctrl+T theme, Ctrl+Q quit' ).size(1).style: :fg(0x7AA2F7), :bold; # Message list (CardList in a Border so focus is visible) Border( "Chat", my $cards = CardList() ) # Input $input = Border( 'Compose', MultiLineInput('Type a message — Ctrl+Enter to send').max-lines(5).size: 1 ).size: 3; # --- Wiring --------------------------------------------------------------- $input.on-submit.tap: -> $text { if $text.chars > 0 { $app.store.dispatch('chat/send', :$text); $input.clear; } }; # --- Subscriptions -------------------------------------------------------- # Build a styled card per message and rebuild the CardList when messages # change. For brevity we destroy and recreate cards on every change — fine # at chat-app scale. A higher-volume app would diff and patch. SubscribeWithCallback 'message-cards', -> $s { $s.get-in('messages') // [] }, -> @msgs { $cards.clear-items; for @msgs -> %m { my $is-bot = %m eq 'bot'; my %name-style = $is-bot ?? |(:fg(0x9ECE6A), :bold) !! |(:fg(0x7AA2F7), :bold); my %body-style = |(:fg(0xEEEEEE)); # Wrap in its own Border so cards visually separate. my $border = Border( RichText.new( Span("{%m}: ").style(%name-style), Span(%m).style: %body-style ).flex; ).flex; $cards.add-item( $rich, # positional: the inner widget :root($border), # the rendered root (the Border) :3height, # 1 text line + 2 border rows :$border, # the Border for focus highlighting ); } $cards.select-last if @msgs; }, $cards, ; # Theme toggle: install the appropriate theme on the root each time `dark` # changes. Children inherit by walking up the parent chain. SubscribeWithCallback 'theme', -> $s { $s.get-in('dark') // True }, -> Bool $dark { $root.set-theme: $dark ?? dark-theme() !! light-theme(); }, $root, ; # --- Global keybinds ------------------------------------------------------ OnKey 'ctrl+q', -> $ { $app.quit }; OnKey 'ctrl+l', -> $ { Dispatch 'chat/clear'; Toast 'Cleared'; }; OnKey 'ctrl+t', -> $ { Dispatch 'theme/toggle'; Toast 'Theme toggled'; }; }).name: "main"; Dispatch 'app/init'; } ).theme: dark-theme(); # --- Go ------------------------------------------------------------------- $app.store.tick; $app.focus($input); $app.run;