forked from Mirrors/bubbletea
224 lines
4.3 KiB
Go
224 lines
4.3 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"strings"
|
||
"time"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/fogleman/ease"
|
||
)
|
||
|
||
func main() {
|
||
p := tea.NewProgram(
|
||
initialize,
|
||
update,
|
||
view,
|
||
)
|
||
if err := p.Start(); err != nil {
|
||
fmt.Println("could not start program:", err)
|
||
}
|
||
}
|
||
|
||
// MESSAGES
|
||
|
||
type tickMsg struct{}
|
||
type frameMsg struct{}
|
||
|
||
// MODEL
|
||
|
||
type model struct {
|
||
Choice int
|
||
Chosen bool
|
||
Ticks int
|
||
Frames int
|
||
Progress float64
|
||
Loaded bool
|
||
}
|
||
|
||
func initialize() (tea.Model, tea.Cmd) {
|
||
return model{0, false, 10, 0, 0, false}, tick()
|
||
}
|
||
|
||
// CMDS
|
||
|
||
func tick() tea.Cmd {
|
||
return tea.Tick(time.Second, func(time.Time) tea.Msg {
|
||
return tickMsg{}
|
||
})
|
||
}
|
||
|
||
func frame() tea.Cmd {
|
||
return tea.Tick(time.Second/60, func(time.Time) tea.Msg {
|
||
return frameMsg{}
|
||
})
|
||
}
|
||
|
||
// UPDATE
|
||
|
||
// Main update function. This just hands off the message and model to the
|
||
// approprate update loop based on the current state.
|
||
func update(msg tea.Msg, mdl tea.Model) (tea.Model, tea.Cmd) {
|
||
m, _ := mdl.(model)
|
||
|
||
if !m.Chosen {
|
||
return updateChoices(msg, m)
|
||
}
|
||
return updateChosen(msg, m)
|
||
}
|
||
|
||
// Update loop for the first view where you're choosing a task
|
||
func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
|
||
case tea.KeyMsg:
|
||
switch msg.String() {
|
||
case "j":
|
||
fallthrough
|
||
case "down":
|
||
m.Choice += 1
|
||
if m.Choice > 3 {
|
||
m.Choice = 3
|
||
}
|
||
case "k":
|
||
fallthrough
|
||
case "up":
|
||
m.Choice -= 1
|
||
if m.Choice < 0 {
|
||
m.Choice = 0
|
||
}
|
||
case "enter":
|
||
m.Chosen = true
|
||
return m, frame()
|
||
case "q":
|
||
fallthrough
|
||
case "esc":
|
||
fallthrough
|
||
case "ctrl+c":
|
||
return m, tea.Quit
|
||
}
|
||
|
||
case tickMsg:
|
||
if m.Ticks == 0 {
|
||
return m, tea.Quit
|
||
}
|
||
m.Ticks -= 1
|
||
return m, tick()
|
||
}
|
||
|
||
return m, nil
|
||
}
|
||
|
||
// Update loop for the second view after a choice has been made
|
||
func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
|
||
case tea.KeyMsg:
|
||
switch msg.String() {
|
||
case "q":
|
||
fallthrough
|
||
case "esc":
|
||
fallthrough
|
||
case "ctrl+c":
|
||
return m, tea.Quit
|
||
}
|
||
|
||
case frameMsg:
|
||
if !m.Loaded {
|
||
m.Frames += 1
|
||
m.Progress = ease.OutBounce(float64(m.Frames) / float64(100))
|
||
if m.Progress >= 1 {
|
||
m.Progress = 1
|
||
m.Loaded = true
|
||
m.Ticks = 3
|
||
return m, tick()
|
||
}
|
||
return m, frame()
|
||
}
|
||
|
||
case tickMsg:
|
||
if m.Loaded {
|
||
if m.Ticks == 0 {
|
||
return m, tea.Quit
|
||
}
|
||
m.Ticks -= 1
|
||
return m, tick()
|
||
}
|
||
}
|
||
|
||
return m, nil
|
||
}
|
||
|
||
// VIEW
|
||
|
||
// The main view, which just calls the approprate sub-view
|
||
func view(mdl tea.Model) string {
|
||
m, _ := mdl.(model)
|
||
if !m.Chosen {
|
||
return choicesView(m) + "\n"
|
||
}
|
||
return chosenView(m) + "\n"
|
||
}
|
||
|
||
// The first view, where you're choosing a task
|
||
func choicesView(m model) string {
|
||
c := m.Choice
|
||
|
||
tpl := "What to do today?\n\n"
|
||
tpl += "%s\n\n"
|
||
tpl += "Program quits in %d seconds\n\n"
|
||
tpl += "(press j/k or up/down to select, enter to choose, and q or esc to quit)`"
|
||
|
||
choices := fmt.Sprintf(
|
||
"%s\n%s\n%s\n%s",
|
||
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(tpl, choices, m.Ticks)
|
||
}
|
||
|
||
// The second view, after a task has been chosen
|
||
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, an actual library."
|
||
default:
|
||
msg = "It’s always good to see friends.\n\nFetching social-skills and conversationutils..."
|
||
}
|
||
|
||
label := "Downloading..."
|
||
if m.Loaded {
|
||
label = fmt.Sprintf("Downloaded. Exiting in %d...", m.Ticks)
|
||
}
|
||
|
||
return msg + "\n\n " + label + "\n" + progressbar(80, m.Progress) + "%"
|
||
}
|
||
|
||
func checkbox(label string, checked bool) string {
|
||
check := " "
|
||
if checked {
|
||
check = "x"
|
||
}
|
||
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))
|
||
}
|