///|
/// Message emitter used by a program.
///
/// Calling `emit(msg)` returns a `Cmd` that queues `msg` back into the
/// update loop. This mirrors Rabbita's current TEA API and keeps `Cmd`
/// monomorphic.
#alias(Dispatch, deprecated="Use Emit[Msg] instead.")
pub(all) struct Emit[Msg]((Msg) -> Cmd)
///|
pub fn[Msg] Emit::new(send : (Msg) -> Unit) -> Emit[Msg] {
msg => CmdMessage(() => send(msg))
}
///|
pub fn[Msg] Emit::send(self : Emit[Msg], msg : Msg) -> Unit {
match self(msg) {
CmdMessage(send) => send()
_ => ()
}
}
///|
pub fn[A, B] Emit::map(self : Emit[A], map : (B) -> A) -> Emit[B] {
msg => self(map(msg))
}
///|
enum Cmd {
NoCmd
CmdMessage(() -> Unit)
CmdBatch(Array[Cmd])
CmdSequence(Array[Cmd])
CmdDelay(Int, Cmd)
CmdTask(async () -> Cmd)
CmdSuspend(async () -> Cmd)
CmdExecProcess(String, (Int) -> Cmd)
CmdTerminal(TerminalCommand)
}
///|
pub let none : Cmd = NoCmd
///|
pub fn Cmd::none() -> Cmd {
NoCmd
}
///|
#deprecated("Use emit(msg) instead.")
pub fn[Msg] Cmd::message(emit : Emit[Msg], msg : Msg) -> Cmd {
emit(msg)
}
///|
pub fn Cmd::batch(cmds : Array[Cmd]) -> Cmd {
let flat : Array[Cmd] = []
for cmd in cmds {
match cmd {
NoCmd => ()
CmdBatch(nested) =>
for child in nested {
if !(child is NoCmd) {
flat.push(child)
}
}
_ => flat.push(cmd)
}
}
if flat.is_empty() {
NoCmd
} else {
CmdBatch(flat)
}
}
///|
pub fn batch(cmds : Array[Cmd]) -> Cmd {
Cmd::batch(cmds)
}
///|
pub fn Cmd::sequence(cmds : Array[Cmd]) -> Cmd {
let flat : Array[Cmd] = []
for cmd in cmds {
match cmd {
NoCmd => ()
CmdSequence(nested) =>
for child in nested {
if !(child is NoCmd) {
flat.push(child)
}
}
_ => flat.push(cmd)
}
}
if flat.is_empty() {
NoCmd
} else {
CmdSequence(flat)
}
}
///|
pub fn Cmd::delay(milliseconds : Int, cmd : Cmd) -> Cmd {
if cmd is NoCmd {
NoCmd
} else {
CmdDelay(milliseconds, cmd)
}
}
///|
pub fn delay(cmd : Cmd, milliseconds : Int) -> Cmd {
Cmd::delay(milliseconds, cmd)
}
///|
pub fn Cmd::tick(milliseconds : Int, cmd : Cmd) -> Cmd {
Cmd::delay(milliseconds, cmd)
}
///|
pub fn Cmd::every(milliseconds : Int, cmd : Cmd) -> Cmd {
Cmd::delay(milliseconds, cmd)
}
///|
pub fn Cmd::effect(task : async () -> Unit noraise) -> Cmd {
CmdTask(() => {
task()
none
})
}
///|
pub fn effect(task : async () -> Unit noraise) -> Cmd {
Cmd::effect(task)
}
///|
pub fn[A] Cmd::perform(
to_cmd : (A) -> Cmd,
task : async () -> A noraise,
) -> Cmd {
CmdTask(() => to_cmd(task()))
}
///|
pub fn[A] perform(to_cmd : (A) -> Cmd, task : async () -> A noraise) -> Cmd {
Cmd::perform(to_cmd, task)
}
///|
pub fn[A, E : Error] Cmd::attempt(
to_cmd : (Result[A, E]) -> Cmd,
task : async () -> A raise E,
) -> Cmd {
CmdTask(() => {
let next = try task() catch {
err => to_cmd(Err(err))
} noraise {
value => to_cmd(Ok(value))
}
next
})
}
///|
pub fn[A, E : Error] attempt(
to_cmd : (Result[A, E]) -> Cmd,
task : async () -> A raise E,
) -> Cmd {
Cmd::attempt(to_cmd, task)
}
///|
pub fn[A] Cmd::exec(to_cmd : (A) -> Cmd, task : async () -> A noraise) -> Cmd {
CmdSuspend(() => to_cmd(task()))
}
///|
pub fn Cmd::suspend(task : async () -> Cmd) -> Cmd {
CmdSuspend(task)
}
///|
pub fn Cmd::exec_process(command : String, done : (Int) -> Cmd) -> Cmd {
CmdExecProcess(command, done)
}
///|
pub fn Cmd::terminal(command : TerminalCommand) -> Cmd {
CmdTerminal(command)
}
///|
pub fn Cmd::write(value : String) -> Cmd {
CmdTerminal(Print(value))
}
///|
pub fn Cmd::log(value : String) -> Cmd {
CmdTerminal(PrintLine(value))
}
///|
pub fn Cmd::err_write(value : String) -> Cmd {
CmdTerminal(PrintErr(value))
}
///|
pub fn Cmd::err_log(value : String) -> Cmd {
CmdTerminal(PrintErrLine(value))
}
///|
pub fn Cmd::quit() -> Cmd {
CmdTerminal(QuitProgram)
}
///|
pub fn Cmd::repaint() -> Cmd {
CmdTerminal(Repaint)
}
///|
pub fn Cmd::clear_screen() -> Cmd {
CmdTerminal(ClearScreen)
}
///|
pub fn Cmd::enter_alternate_screen() -> Cmd {
CmdTerminal(EnterAlternateScreen)
}
///|
pub fn Cmd::leave_alternate_screen() -> Cmd {
CmdTerminal(LeaveAlternateScreen)
}
///|
pub fn Cmd::hide_cursor() -> Cmd {
CmdTerminal(HideCursor)
}
///|
pub fn Cmd::show_cursor() -> Cmd {
CmdTerminal(ShowCursor)
}
///|
pub fn Cmd::enable_mouse(mode : MouseMode) -> Cmd {
CmdTerminal(EnableMouse(mode))
}
///|
pub fn Cmd::disable_mouse() -> Cmd {
CmdTerminal(DisableMouse)
}
///|
pub fn Cmd::enable_bracketed_paste() -> Cmd {
CmdTerminal(EnableBracketedPaste)
}
///|
pub fn Cmd::disable_bracketed_paste() -> Cmd {
CmdTerminal(DisableBracketedPaste)
}
///|
pub fn Cmd::enable_focus_events() -> Cmd {
CmdTerminal(EnableFocusEvents)
}
///|
pub fn Cmd::disable_focus_events() -> Cmd {
CmdTerminal(DisableFocusEvents)
}
///|
pub fn Cmd::set_window_title(title : String) -> Cmd {
CmdTerminal(SetWindowTitle(title))
}
///|
pub async fn Cmd::run(self : Cmd) -> Unit {
self.run_with_terminal(_ => ())
}
///|
pub async fn Cmd::run_with_terminal(
self : Cmd,
terminal : (TerminalCommand) -> Unit,
) -> Unit {
match self {
NoCmd => ()
CmdMessage(send) => send()
CmdBatch(cmds) =>
@async.with_task_group(group => {
for cmd in cmds {
group.spawn_bg(() => cmd.run_with_terminal(terminal))
}
})
CmdSequence(cmds) =>
for cmd in cmds {
cmd.run_with_terminal(terminal)
}
CmdDelay(milliseconds, cmd) => {
@async.sleep(milliseconds)
cmd.run_with_terminal(terminal)
}
CmdTask(task) => task().run_with_terminal(terminal)
CmdSuspend(task) => task().run_with_terminal(terminal)
CmdExecProcess(_, done) => done(0).run_with_terminal(terminal)
CmdTerminal(command) => terminal(command)
}
}
///|
pub fn Cmd::terminal_commands(self : Cmd) -> Array[TerminalCommand] {
let out : Array[TerminalCommand] = []
collect_terminal_commands(self, out)
out
}
///|
pub fn Cmd::is_none(self : Cmd) -> Bool {
match self {
NoCmd => true
CmdBatch(cmds) | CmdSequence(cmds) => cmds.all(cmd => cmd.is_none())
_ => false
}
}
///|
fn collect_terminal_commands(cmd : Cmd, out : Array[TerminalCommand]) -> Unit {
match cmd {
NoCmd
| CmdMessage(_)
| CmdDelay(_)
| CmdTask(_)
| CmdSuspend(_)
| CmdExecProcess(_) => ()
CmdTerminal(command) => out.push(command)
CmdBatch(cmds) | CmdSequence(cmds) =>
for child in cmds {
collect_terminal_commands(child, out)
}
}
}
///|
enum Sub[Msg] {
NoSub
SubBatch(Array[Sub[Msg]])
EventSub((Event) -> Msg)
Keyboard((Event) -> Msg)
KeySub((Key) -> Msg)
MouseSub((Event) -> Msg)
MouseEventSub((Mouse) -> Msg)
ResizeSub((Size) -> Msg)
PasteSub((String) -> Msg)
FocusSub((Event) -> Msg)
FocusChangesSub((Bool) -> Msg)
TickSub(Int, Msg)
}
///|
pub fn[Msg] Sub::none() -> Sub[Msg] {
NoSub
}
///|
pub fn[Msg] Sub::batch(subs : Array[Sub[Msg]]) -> Sub[Msg] {
let flat : Array[Sub[Msg]] = []
for sub in subs {
match sub {
NoSub => ()
SubBatch(nested) =>
for child in nested {
if !(child is NoSub) {
flat.push(child)
}
}
_ => flat.push(sub)
}
}
if flat.is_empty() {
NoSub
} else {
SubBatch(flat)
}
}
///|
pub fn[Msg] Sub::event(map : (Event) -> Msg) -> Sub[Msg] {
EventSub(map)
}
///|
pub fn[Msg] Sub::keyboard(map : (Event) -> Msg) -> Sub[Msg] {
Keyboard(map)
}
///|
pub fn[Msg] Sub::keys(map : (Key) -> Msg) -> Sub[Msg] {
KeySub(map)
}
///|
pub fn[Msg] Sub::mouse(map : (Event) -> Msg) -> Sub[Msg] {
MouseSub(map)
}
///|
pub fn[Msg] Sub::mouse_events(map : (Mouse) -> Msg) -> Sub[Msg] {
MouseEventSub(map)
}
///|
pub fn[Msg] Sub::resize(map : (Size) -> Msg) -> Sub[Msg] {
ResizeSub(map)
}
///|
pub fn[Msg] Sub::paste(map : (String) -> Msg) -> Sub[Msg] {
PasteSub(map)
}
///|
pub fn[Msg] Sub::focus(map : (Event) -> Msg) -> Sub[Msg] {
FocusSub(map)
}
///|
pub fn[Msg] Sub::focus_changes(map : (Bool) -> Msg) -> Sub[Msg] {
FocusChangesSub(map)
}
///|
pub fn[Msg] Sub::tick(milliseconds : Int, msg : Msg) -> Sub[Msg] {
TickSub(milliseconds, msg)
}
///|
pub fn[Msg] Sub::map_event(self : Sub[Msg], event : Event) -> Array[Msg] {
let out : Array[Msg] = []
collect_subscription(self, event, out)
out
}
///|
fn[Msg] collect_subscription(
sub : Sub[Msg],
event : Event,
out : Array[Msg],
) -> Unit {
match sub {
NoSub => ()
SubBatch(subs) =>
for child in subs {
collect_subscription(child, event, out)
}
EventSub(map) => out.push(map(event))
Keyboard(map) => if event is Key(_) { out.push(map(event)) }
KeySub(map) =>
match event {
Key(key) => out.push(map(key))
_ => ()
}
MouseSub(map) => if event is Mouse(_) { out.push(map(event)) }
MouseEventSub(map) =>
match event {
Mouse(mouse) => out.push(map(mouse))
_ => ()
}
ResizeSub(map) =>
match event {
Resize(size) => out.push(map(size))
_ => ()
}
PasteSub(map) =>
match event {
Paste(value) => out.push(map(value))
_ => ()
}
FocusSub(map) =>
if event is FocusGained || event is FocusLost {
out.push(map(event))
}
FocusChangesSub(map) =>
match event {
FocusGained => out.push(map(true))
FocusLost => out.push(map(false))
_ => ()
}
TickSub(_, msg) => if event is Tick(_) { out.push(msg) }
}
}
///|
pub(all) struct HeadlessOptions {
size : Size
render : Bool
max_steps : Int
} derive(Eq, Debug)
///|
pub fn HeadlessOptions::default() -> HeadlessOptions {
{ size: { width: 80, height: 24 }, render: true, max_steps: 1000 }
}
///|
pub fn HeadlessOptions::size(
self : HeadlessOptions,
size : Size,
) -> HeadlessOptions {
{ ..self, size, }
}
///|
pub fn HeadlessOptions::render(
self : HeadlessOptions,
enabled : Bool,
) -> HeadlessOptions {
{ ..self, render: enabled }
}
///|
pub fn HeadlessOptions::max_steps(
self : HeadlessOptions,
value : Int,
) -> HeadlessOptions {
{ ..self, max_steps: Int::max(1, value) }
}
///|
pub(all) struct ProgramRunResult[Model] {
model : Model
terminal_commands : Array[TerminalCommand]
frames : Array[Frame]
quit : Bool
steps : Int
limit_reached : Bool
}
///|
pub struct Program[Model, Msg] {
mut model : Model
init_fn : (Emit[Msg], Model) -> Cmd
update_fn : (Emit[Msg], Msg, Model) -> (Cmd, Model)
view_fn : (Model) -> Node
subscriptions_fn : (Model) -> Sub[Msg]
mut dirty : Bool
}
///|
pub type Cell[Model, Msg] = Program[Model, Msg]
///|
pub fn[Model, Msg] cell(
model~ : Model,
init? : (Emit[Msg], Model) -> Cmd = (_, _) => none,
update~ : (Emit[Msg], Msg, Model) -> (Cmd, Model),
view~ : (Model) -> Node,
subscriptions? : (Model) -> Sub[Msg] = _ => NoSub,
) -> Cell[Model, Msg] {
{
model,
init_fn: init,
update_fn: update,
view_fn: view,
subscriptions_fn: subscriptions,
dirty: true,
}
}
///|
pub fn[Model, Msg] simple_cell(
model~ : Model,
init? : (Emit[Msg], Model) -> Cmd = (_, _) => none,
update~ : (Msg, Model) -> (Cmd, Model),
view~ : (Model) -> Node,
subscriptions? : (Model) -> Sub[Msg] = _ => NoSub,
) -> Cell[Model, Msg] {
cell(
model~,
init~,
update=(emit, msg, model) => {
ignore(emit)
update(msg, model)
},
view~,
subscriptions~,
)
}
///|
pub fn[Model, Msg] cell_with_emit(
model~ : Model,
init? : (Emit[Msg], Model) -> Cmd = (_, _) => none,
update~ : (Emit[Msg], Msg, Model) -> (Cmd, Model),
view~ : (Model) -> Node,
subscriptions? : (Model) -> Sub[Msg] = _ => NoSub,
) -> (Emit[Msg], Cell[Model, Msg]) {
let inbox : Array[Msg] = []
let emit = Emit::new(msg => inbox.push(msg))
let program = cell(model~, init~, update~, view~, subscriptions~)
ignore(inbox)
(emit, program)
}
///|
#deprecated("Use cell_with_emit instead.")
pub fn[Model, Msg] cell_with_dispatch(
model~ : Model,
init? : (Emit[Msg], Model) -> Cmd = (_, _) => none,
update~ : (Emit[Msg], Msg, Model) -> (Cmd, Model),
view~ : (Model) -> Node,
subscriptions? : (Model) -> Sub[Msg] = _ => NoSub,
) -> (Emit[Msg], Cell[Model, Msg]) {
cell_with_emit(model~, init~, update~, view~, subscriptions~)
}
///|
pub fn[Model, Msg] Program::model(self : Program[Model, Msg]) -> Model {
self.model
}
///|
pub fn[Model, Msg] Program::is_dirty(self : Program[Model, Msg]) -> Bool {
self.dirty
}
///|
pub fn[Model, Msg] Program::mark_clean(self : Program[Model, Msg]) -> Unit {
self.dirty = false
}
///|
pub fn[Model, Msg] Program::mark_dirty(self : Program[Model, Msg]) -> Unit {
self.dirty = true
}
///|
pub fn[Model, Msg] Program::view(self : Program[Model, Msg]) -> Node {
(self.view_fn)(self.model)
}
///|
pub fn[Model, Msg] Program::init(
self : Program[Model, Msg],
emit : Emit[Msg],
) -> Cmd {
(self.init_fn)(emit, self.model)
}
///|
pub fn[Model, Msg] Program::subscriptions(
self : Program[Model, Msg],
) -> Sub[Msg] {
(self.subscriptions_fn)(self.model)
}
///|
pub fn[Model, Msg] Program::step(
self : Program[Model, Msg],
emit : Emit[Msg],
msg : Msg,
) -> Cmd {
let (cmd, next) = (self.update_fn)(emit, msg, self.model)
self.model = next
self.dirty = true
cmd
}
///|
pub fn[Model, Msg] Program::handle_event(
self : Program[Model, Msg],
emit : Emit[Msg],
event : Event,
) -> Array[Cmd] {
let cmds : Array[Cmd] = []
for msg in self.subscriptions().map_event(event) {
cmds.push(self.step(emit, msg))
}
cmds
}
///|
pub async fn[Model, Msg] Program::run_headless(
self : Program[Model, Msg],
events? : Array[Event] = [],
options? : HeadlessOptions = HeadlessOptions::default(),
) -> ProgramRunResult[Model] {
let queue = @aqueue.Queue::new(kind=Unbounded)
let terminal_commands : Array[TerminalCommand] = []
let frames : Array[Frame] = []
let emit = Emit::new(msg => ignore(queue.try_put(msg) catch { _ => false }))
let mut quit = false
let mut steps = 0
let mut limit_reached = false
for cmd in self.handle_event(emit, Resize(options.size)) {
if run_headless_cmd(cmd, terminal_commands) {
quit = true
}
let drained = drain_headless_messages(
self, emit, queue, terminal_commands, frames, options, steps,
)
if drained.quit {
quit = true
}
steps = drained.steps
limit_reached = limit_reached || drained.limit_reached
}
if !quit {
if run_headless_cmd(self.init(emit), terminal_commands) {
quit = true
}
let drained = drain_headless_messages(
self, emit, queue, terminal_commands, frames, options, steps,
)
if drained.quit {
quit = true
}
steps = drained.steps
limit_reached = limit_reached || drained.limit_reached
}
if options.render && self.is_dirty() {
frames.push(Frame::from_node(self.view(), options.size))
self.mark_clean()
}
for event in events {
if quit || limit_reached {
break
}
for cmd in self.handle_event(emit, event) {
if run_headless_cmd(cmd, terminal_commands) {
quit = true
}
let drained = drain_headless_messages(
self, emit, queue, terminal_commands, frames, options, steps,
)
if drained.quit {
quit = true
}
steps = drained.steps
limit_reached = limit_reached || drained.limit_reached
}
if options.render && self.is_dirty() {
frames.push(Frame::from_node(self.view(), options.size))
self.mark_clean()
}
}
{ model: self.model(), terminal_commands, frames, quit, steps, limit_reached }
}
///|
priv struct HeadlessDrain {
quit : Bool
steps : Int
limit_reached : Bool
}
///|
async fn run_headless_cmd(
cmd : Cmd,
terminal_commands : Array[TerminalCommand],
) -> Bool {
let mut quit = false
cmd.run_with_terminal(command => {
terminal_commands.push(command)
if command is QuitProgram {
quit = true
}
})
quit
}
///|
async fn[Model, Msg] drain_headless_messages(
program : Program[Model, Msg],
emit : Emit[Msg],
queue : @aqueue.Queue[Msg],
terminal_commands : Array[TerminalCommand],
frames : Array[Frame],
options : HeadlessOptions,
steps : Int,
) -> HeadlessDrain {
let mut quit = false
let mut next_steps = steps
let mut limit_reached = false
while true {
if next_steps >= options.max_steps {
limit_reached = true
break
}
match (queue.try_get() catch { _ => None }) {
Some(msg) => {
next_steps += 1
let cmd = program.step(emit, msg)
if run_headless_cmd(cmd, terminal_commands) {
quit = true
}
if options.render && program.is_dirty() {
frames.push(Frame::from_node(program.view(), options.size))
program.mark_clean()
}
if quit {
break
}
}
None => break
}
}
{ quit, steps: next_steps, limit_reached }
}