From b6c879230947deb14e90945d8d5254579caf7e09 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Tue, 14 Jan 2020 13:47:36 -0500 Subject: [PATCH] Multi-view example with progress bar --- example/main.go | 124 ++++++++++++++++++++++++++++++++++++++++-------- go.mod | 1 + go.sum | 2 + key.go | 6 ++- 4 files changed, 112 insertions(+), 21 deletions(-) diff --git a/example/main.go b/example/main.go index 22dda74..b887ebc 100644 --- a/example/main.go +++ b/example/main.go @@ -2,35 +2,63 @@ package main import ( "fmt" + "math" + "strings" "tea" "time" + + "github.com/fogleman/ease" ) // Model contains the data for our application. type Model struct { - Choice int - Ticks int + Choice int + Chosen bool + Ticks int + Frames int + Progress float64 } -// TickMsg signals that the timer has ticked -type TickMsg struct{} +type tickMsg struct{} + +type frameMsg struct{} func main() { p := tea.NewProgram( - Model{0, 10}, + Model{0, false, 10, 0, 0}, update, view, - []tea.Sub{tick}, + []tea.Sub{tick, frame}, ) if err := p.Start(); err != nil { fmt.Println("could not start program:", err) } } -// Update. Triggered when new messages arrive. +// SUBSCRIPTIONS + +func tick(model tea.Model) tea.Msg { + time.Sleep(time.Second) + return tickMsg{} +} + +func frame(model tea.Model) tea.Msg { + time.Sleep(time.Second / 16) + return frameMsg{} +} + +// UPDATES + func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) { m, _ := model.(Model) + if !m.Chosen { + return updateChoices(msg, m) + } + return updateChosen(msg, m) +} + +func updateChoices(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: @@ -49,6 +77,9 @@ func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) { if m.Choice < 0 { m.Choice = 0 } + case "enter": + m.Chosen = true + return m, nil case "q": fallthrough case "esc": @@ -57,7 +88,7 @@ func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) { return m, tea.Quit } - case TickMsg: + case tickMsg: if m.Ticks == 0 { return m, tea.Quit } @@ -67,24 +98,51 @@ func update(msg tea.Msg, model tea.Model) (tea.Model, tea.Cmd) { return m, nil } -// Subscription -func tick(_ tea.Model) tea.Msg { - time.Sleep(time.Second) - return TickMsg{} +func updateChosen(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + case tea.KeyPressMsg: + switch msg { + case "q": + fallthrough + case "esc": + fallthrough + case "ctrl+c": + return m, tea.Quit + } + + case frameMsg: + m.Frames += 1 + m.Progress = ease.OutBounce(float64(m.Frames) / float64(120)) + if m.Progress > 1 { + m.Progress = 1 + } + return m, nil + + } + + return m, nil } -// View template -const tpl = `What to do today? +// VIEWS + +func view(model tea.Model) string { + m, _ := model.(Model) + if !m.Chosen { + return choicesView(m) + } + return chosenView(m) +} + +const choicesTpl = `What to do today? %s Program quits in %d seconds. -(press j/k or up/down to select, q or esc to quit)` +(press j/k or up/down to select, enter to choose, and q or esc to quit)` -// View function. Called after an Update(). -func view(model tea.Model) string { - m, _ := model.(Model) +func choicesView(m Model) string { c := m.Choice choices := fmt.Sprintf( @@ -95,10 +153,26 @@ func view(model tea.Model) string { checkbox("See friends", c == 3), ) - return fmt.Sprintf(tpl, choices, m.Ticks) + return fmt.Sprintf(choicesTpl, choices, m.Ticks) +} + +func chosenView(m Model) string { + var msg string + + switch m.Choice { + case 0: + msg = "Carrot planting?\n\nCool, we'll need libgarden and vegeutils..." + case 1: + msg = "A trip to the market?\n\nOkay, then we should install marketkit and libshopping..." + case 2: + msg = "Reading time?\n\nOkay, cool, then we’ll need a library. Yes, a literal library..." + default: + msg = "It’s always good to see friends.\n\nFetching social-skills and conversationutils..." + } + + return "\n" + msg + "\n\n\n\n\n Downloading...\n" + progressbar(80, m.Progress) + "%" } -// Checkbox widget func checkbox(label string, checked bool) string { check := " " if checked { @@ -106,3 +180,13 @@ func checkbox(label string, checked bool) string { } return fmt.Sprintf("[%s] %s", check, label) } + +func progressbar(width int, percent float64) string { + metaChars := 7 + w := float64(width - metaChars) + fullSize := int(math.Round(w * percent)) + emptySize := int(w) - fullSize + fullCells := strings.Repeat("#", fullSize) + emptyCells := strings.Repeat(".", emptySize) + return fmt.Sprintf("|%s%s| %3.0f", fullCells, emptyCells, math.Round(percent*100)) +} diff --git a/go.mod b/go.mod index fca1f83..10ab6e6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module tea go 1.13 require ( + github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 github.com/tj/go-terminput v1.0.0 ) diff --git a/go.sum b/go.sum index 97b96b1..8d87886 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA= +github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0= diff --git a/key.go b/key.go index 9e35bf4..6e687d4 100644 --- a/key.go +++ b/key.go @@ -11,13 +11,17 @@ type KeyPressMsg string // Control keys const ( - keyETX = 3 // ctrl+c + keyETX = 3 // break, ctrl+c + keyLF = 9 // line-feed, \n + keyCR = 13 // carriage return, \r keyESC = 27 keyUS = 31 ) var controlKeyNames = map[int]string{ keyETX: "ctrl+c", + keyLF: "enter", + keyCR: "enter", keyESC: "esc", keyUS: "us", }