From 147c8df2ee59863f2b687479e6d17cabc0be7ebd Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 13 Jan 2020 17:10:23 -0500 Subject: [PATCH] Implement subscriptions, and update example accordingly --- example/main.go | 58 ++++++++++++++++++++++++++++++++----------------- tea.go | 53 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/example/main.go b/example/main.go index c0d0cfb..806079f 100644 --- a/example/main.go +++ b/example/main.go @@ -3,12 +3,26 @@ package main import ( "fmt" "tea" + "time" ) -type Model int +type Model struct { + Choice int + Ticks int +} + +type TickMsg struct{} + +const tpl = `What to do today? + +%s + +Elapsed: %d seconds. + +(press j/k or up/down to select, q or esc to quit)` func main() { - p := tea.NewProgram(0, update, view) + p := tea.NewProgram(Model{0, 0}, update, view, []tea.Sub{tick}) if err := p.Start(); err != nil { fmt.Println("could not start program:", err) } @@ -18,53 +32,57 @@ func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) { m, _ := model.(Model) switch msg := msg.(type) { + case tea.KeyPressMsg: switch msg { - case "j": fallthrough case "down": - m += 1 - if m > 3 { - m = 3 + m.Choice += 1 + if m.Choice > 3 { + m.Choice = 3 } - case "k": fallthrough case "up": - m -= 1 - if m < 0 { - m = 0 + m.Choice -= 1 + if m.Choice < 0 { + m.Choice = 0 } - case "q": fallthrough case "esc": fallthrough case "ctrl+c": return m, tea.Quit - } + + case TickMsg: + m.Ticks += 1 } return m, nil } +// Subscription +func tick(_ tea.Model) tea.Msg { + time.Sleep(time.Second * 1) + return TickMsg{} +} + func view(model tea.Model) string { m, _ := model.(Model) + c := m.Choice choices := fmt.Sprintf( "%s\n%s\n%s\n%s", - checkbox("Plant carrots", m == 0), - checkbox("Go to the market", m == 1), - checkbox("Read something", m == 2), - checkbox("See friends", m == 3), + checkbox("Plant carrots", c == 0), + checkbox("Go to the market", c == 1), + checkbox("Read something", c == 2), + checkbox("See friends", c == 3), ) - return fmt.Sprintf( - "What to do today?\n\n%s\n\n(press j/k or up/down to select, q or esc to quit)", - choices, - ) + return fmt.Sprintf(tpl, choices, m.Ticks) } func checkbox(label string, checked bool) string { diff --git a/tea.go b/tea.go index 9bbc53f..c2befea 100644 --- a/tea.go +++ b/tea.go @@ -20,6 +20,10 @@ type Model interface{} // Cmd is an IO operation. If it's nil it's considered a no-op. type Cmd func() Msg +// Sub is an event subscription. If it's nil it's considered a no-op. But why +// would you subscribe to nil? +type Sub func(Model) Msg + // Update is called when a message is received. It may update the model and/or // send a command. type Update func(Msg, Model) (Model, Cmd) @@ -29,10 +33,11 @@ type View func(Model) string // Program is a terminal user interface type Program struct { - model Model - update Update - view View - rw io.ReadWriter + model Model + update Update + view View + subscriptions []Sub + rw io.ReadWriter } // Quit command @@ -44,12 +49,12 @@ func Quit() Msg { type quitMsg struct{} // NewProgram creates a new Program -func NewProgram(model Model, update Update, view View) *Program { +func NewProgram(model Model, update Update, view View, subs []Sub) *Program { return &Program{ - model: model, - update: update, - view: view, - // TODO: subscriptions + model: model, + update: update, + view: view, + subscriptions: subs, } } @@ -81,8 +86,9 @@ func (p *Program) Start() error { p.render(model, true) // Subscribe to user input - // TODO: move to program struct to allow for subscriptions to other things, - // too, like timers, frames, download/upload progress and so on. + // TODO: should we move this to the end-user program level or just keep this + // here, since it blocks nicely and user input will probably be something + // users typically need? go func() { for { select { @@ -109,6 +115,28 @@ func (p *Program) Start() error { } }() + // Subscriptions. Subscribe to user input. + // TODO: should the blocking `for` be here, or in the end-user portion + // of the program? + go func() { + select { + case <-done: + return + default: + if len(p.subscriptions) > 0 { + for _, sub := range p.subscriptions { + if sub != nil { + go func() { + for { + msgs <- sub(p.model) + } + }() + } + } + } + } + }() + // Handle updates and draw for { select { @@ -163,6 +191,7 @@ func clearLines(n int) { } } -func clearScreen() { +// ClearScreen clears the visible portion of the terminal +func ClearScreen() { fmt.Printf(esc + "2J" + esc + "3J" + esc + "1;1H") }