2020-01-10 16:02:04 -05:00
|
|
|
|
package main
|
|
|
|
|
|
2020-10-14 11:51:04 -04:00
|
|
|
|
// An example demonstrating an application with multiple views.
|
2021-01-12 18:13:29 -05:00
|
|
|
|
//
|
|
|
|
|
// Note that this example was produced before the Bubbles progress component
|
|
|
|
|
// was available (github.com/charmbracelet/bubbles/progress) and thus, we're
|
|
|
|
|
// implementing a progress bar from scratch here.
|
2020-10-14 11:51:04 -04:00
|
|
|
|
|
2020-01-10 16:02:04 -05:00
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2020-01-14 13:47:36 -05:00
|
|
|
|
"math"
|
2020-10-02 18:31:19 -04:00
|
|
|
|
"strconv"
|
2020-01-14 13:47:36 -05:00
|
|
|
|
"strings"
|
2020-01-13 17:10:23 -05:00
|
|
|
|
"time"
|
2020-01-14 13:47:36 -05:00
|
|
|
|
|
2020-05-25 19:26:40 -04:00
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
2020-01-14 13:47:36 -05:00
|
|
|
|
"github.com/fogleman/ease"
|
2020-10-02 18:31:19 -04:00
|
|
|
|
"github.com/lucasb-eyer/go-colorful"
|
|
|
|
|
"github.com/muesli/reflow/indent"
|
|
|
|
|
"github.com/muesli/termenv"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
progressBarWidth = 71
|
|
|
|
|
progressFullChar = "█"
|
|
|
|
|
progressEmptyChar = "░"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// General stuff for styling the view
|
|
|
|
|
var (
|
2022-02-05 22:19:46 -05:00
|
|
|
|
term = termenv.EnvColorProfile()
|
2020-10-02 18:31:19 -04:00
|
|
|
|
keyword = makeFgStyle("211")
|
|
|
|
|
subtle = makeFgStyle("241")
|
|
|
|
|
progressEmpty = subtle(progressEmptyChar)
|
|
|
|
|
dot = colorFg(" • ", "236")
|
|
|
|
|
|
|
|
|
|
// Gradient colors we'll use for the progress bar
|
|
|
|
|
ramp = makeRamp("#B14FFF", "#00FFA3", progressBarWidth)
|
2020-01-10 16:02:04 -05:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
2020-10-15 16:30:34 -04:00
|
|
|
|
initialModel := model{0, false, 10, 0, 0, false, false}
|
|
|
|
|
p := tea.NewProgram(initialModel)
|
2020-01-11 10:45:15 -05:00
|
|
|
|
if err := p.Start(); err != nil {
|
|
|
|
|
fmt.Println("could not start program:", err)
|
|
|
|
|
}
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-14 19:08:36 -04:00
|
|
|
|
type (
|
|
|
|
|
tickMsg struct{}
|
|
|
|
|
frameMsg struct{}
|
|
|
|
|
)
|
2020-05-05 14:26:06 -04:00
|
|
|
|
|
2020-06-05 14:12:02 -04:00
|
|
|
|
func tick() tea.Cmd {
|
|
|
|
|
return tea.Tick(time.Second, func(time.Time) tea.Msg {
|
|
|
|
|
return tickMsg{}
|
|
|
|
|
})
|
2020-05-12 17:56:30 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-05 14:12:02 -04:00
|
|
|
|
func frame() tea.Cmd {
|
|
|
|
|
return tea.Tick(time.Second/60, func(time.Time) tea.Msg {
|
|
|
|
|
return frameMsg{}
|
|
|
|
|
})
|
2020-01-25 21:27:43 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-15 16:30:34 -04:00
|
|
|
|
type model struct {
|
|
|
|
|
Choice int
|
|
|
|
|
Chosen bool
|
|
|
|
|
Ticks int
|
|
|
|
|
Frames int
|
|
|
|
|
Progress float64
|
|
|
|
|
Loaded bool
|
|
|
|
|
Quitting bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
|
|
|
return tick()
|
|
|
|
|
}
|
2020-01-14 13:47:36 -05:00
|
|
|
|
|
2020-10-02 18:31:19 -04:00
|
|
|
|
// Main update function.
|
2020-10-15 16:30:34 -04:00
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
// Make sure these keys always quit
|
|
|
|
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
|
|
|
|
k := msg.String()
|
|
|
|
|
if k == "q" || k == "esc" || k == "ctrl+c" {
|
|
|
|
|
m.Quitting = true
|
|
|
|
|
return m, tea.Quit
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-11 20:11:26 -05:00
|
|
|
|
// Hand off the message and model to the appropriate update function for the
|
2020-10-02 18:31:19 -04:00
|
|
|
|
// appropriate view based on the current state.
|
2020-01-14 13:47:36 -05:00
|
|
|
|
if !m.Chosen {
|
|
|
|
|
return updateChoices(msg, m)
|
|
|
|
|
}
|
|
|
|
|
return updateChosen(msg, m)
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-11 20:11:26 -05:00
|
|
|
|
// The main view, which just calls the appropriate sub-view
|
2020-10-15 16:30:34 -04:00
|
|
|
|
func (m model) View() string {
|
|
|
|
|
var s string
|
|
|
|
|
if m.Quitting {
|
|
|
|
|
return "\n See you later!\n\n"
|
|
|
|
|
}
|
|
|
|
|
if !m.Chosen {
|
|
|
|
|
s = choicesView(m)
|
|
|
|
|
} else {
|
|
|
|
|
s = chosenView(m)
|
|
|
|
|
}
|
|
|
|
|
return indent.String("\n"+s+"\n\n", 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sub-update functions
|
|
|
|
|
|
2020-10-02 18:31:19 -04:00
|
|
|
|
// Update loop for the first view where you're choosing a task.
|
2020-07-30 12:32:24 -04:00
|
|
|
|
func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
|
2020-01-10 16:02:04 -05:00
|
|
|
|
switch msg := msg.(type) {
|
2020-01-13 17:10:23 -05:00
|
|
|
|
|
2020-05-25 19:26:40 -04:00
|
|
|
|
case tea.KeyMsg:
|
2020-01-17 20:46:34 -05:00
|
|
|
|
switch msg.String() {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
case "j", "down":
|
2020-01-13 17:10:23 -05:00
|
|
|
|
m.Choice += 1
|
|
|
|
|
if m.Choice > 3 {
|
|
|
|
|
m.Choice = 3
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
2020-10-02 18:31:19 -04:00
|
|
|
|
case "k", "up":
|
2020-01-13 17:10:23 -05:00
|
|
|
|
m.Choice -= 1
|
|
|
|
|
if m.Choice < 0 {
|
|
|
|
|
m.Choice = 0
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
2020-01-14 13:47:36 -05:00
|
|
|
|
case "enter":
|
|
|
|
|
m.Chosen = true
|
2020-06-05 14:12:02 -04:00
|
|
|
|
return m, frame()
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
2020-01-13 17:10:23 -05:00
|
|
|
|
|
2020-01-14 13:47:36 -05:00
|
|
|
|
case tickMsg:
|
2020-01-13 19:07:04 -05:00
|
|
|
|
if m.Ticks == 0 {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
m.Quitting = true
|
2020-05-25 19:26:40 -04:00
|
|
|
|
return m, tea.Quit
|
2020-01-13 19:07:04 -05:00
|
|
|
|
}
|
|
|
|
|
m.Ticks -= 1
|
2020-06-05 14:12:02 -04:00
|
|
|
|
return m, tick()
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-05 14:12:02 -04:00
|
|
|
|
return m, nil
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-30 12:32:24 -04:00
|
|
|
|
// Update loop for the second view after a choice has been made
|
|
|
|
|
func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
switch msg.(type) {
|
2020-01-14 13:47:36 -05:00
|
|
|
|
|
|
|
|
|
case frameMsg:
|
2020-01-14 16:24:01 -05:00
|
|
|
|
if !m.Loaded {
|
|
|
|
|
m.Frames += 1
|
2020-06-05 14:12:02 -04:00
|
|
|
|
m.Progress = ease.OutBounce(float64(m.Frames) / float64(100))
|
2020-01-14 16:24:01 -05:00
|
|
|
|
if m.Progress >= 1 {
|
|
|
|
|
m.Progress = 1
|
|
|
|
|
m.Loaded = true
|
|
|
|
|
m.Ticks = 3
|
2020-06-05 14:12:02 -04:00
|
|
|
|
return m, tick()
|
2020-01-14 16:24:01 -05:00
|
|
|
|
}
|
2020-06-05 14:12:02 -04:00
|
|
|
|
return m, frame()
|
2020-01-14 13:47:36 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 16:24:01 -05:00
|
|
|
|
case tickMsg:
|
|
|
|
|
if m.Loaded {
|
|
|
|
|
if m.Ticks == 0 {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
m.Quitting = true
|
2020-05-25 19:26:40 -04:00
|
|
|
|
return m, tea.Quit
|
2020-01-14 16:24:01 -05:00
|
|
|
|
}
|
|
|
|
|
m.Ticks -= 1
|
2020-06-05 14:12:02 -04:00
|
|
|
|
return m, tick()
|
2020-01-14 16:24:01 -05:00
|
|
|
|
}
|
2020-01-14 13:47:36 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-05 14:12:02 -04:00
|
|
|
|
return m, nil
|
2020-01-14 13:47:36 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-15 16:30:34 -04:00
|
|
|
|
// Sub-views
|
2020-01-13 17:10:23 -05:00
|
|
|
|
|
2020-07-30 12:32:24 -04:00
|
|
|
|
// The first view, where you're choosing a task
|
|
|
|
|
func choicesView(m model) string {
|
2020-01-13 17:10:23 -05:00
|
|
|
|
c := m.Choice
|
2020-01-10 16:02:04 -05:00
|
|
|
|
|
2020-07-30 12:32:24 -04:00
|
|
|
|
tpl := "What to do today?\n\n"
|
|
|
|
|
tpl += "%s\n\n"
|
2020-10-02 18:31:19 -04:00
|
|
|
|
tpl += "Program quits in %s seconds\n\n"
|
|
|
|
|
tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit")
|
2020-07-30 12:32:24 -04:00
|
|
|
|
|
2020-01-10 16:02:04 -05:00
|
|
|
|
choices := fmt.Sprintf(
|
|
|
|
|
"%s\n%s\n%s\n%s",
|
2020-01-13 17:10:23 -05:00
|
|
|
|
checkbox("Plant carrots", c == 0),
|
|
|
|
|
checkbox("Go to the market", c == 1),
|
|
|
|
|
checkbox("Read something", c == 2),
|
|
|
|
|
checkbox("See friends", c == 3),
|
2020-01-10 16:02:04 -05:00
|
|
|
|
)
|
|
|
|
|
|
2020-10-02 18:31:19 -04:00
|
|
|
|
return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Ticks), "79"))
|
2020-01-14 13:47:36 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-30 12:32:24 -04:00
|
|
|
|
// The second view, after a task has been chosen
|
|
|
|
|
func chosenView(m model) string {
|
2020-01-14 13:47:36 -05:00
|
|
|
|
var msg string
|
|
|
|
|
|
|
|
|
|
switch m.Choice {
|
|
|
|
|
case 0:
|
2020-10-02 18:31:19 -04:00
|
|
|
|
msg = fmt.Sprintf("Carrot planting?\n\nCool, we'll need %s and %s...", keyword("libgarden"), keyword("vegeutils"))
|
2020-01-14 13:47:36 -05:00
|
|
|
|
case 1:
|
2020-10-02 18:31:19 -04:00
|
|
|
|
msg = fmt.Sprintf("A trip to the market?\n\nOkay, then we should install %s and %s...", keyword("marketkit"), keyword("libshopping"))
|
2020-01-14 13:47:36 -05:00
|
|
|
|
case 2:
|
2020-10-02 18:31:19 -04:00
|
|
|
|
msg = fmt.Sprintf("Reading time?\n\nOkay, cool, then we’ll need a library. Yes, an %s.", keyword("actual library"))
|
2020-01-14 13:47:36 -05:00
|
|
|
|
default:
|
2020-10-02 18:31:19 -04:00
|
|
|
|
msg = fmt.Sprintf("It’s always good to see friends.\n\nFetching %s and %s...", keyword("social-skills"), keyword("conversationutils"))
|
2020-01-14 13:47:36 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 16:24:01 -05:00
|
|
|
|
label := "Downloading..."
|
|
|
|
|
if m.Loaded {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
label = fmt.Sprintf("Downloaded. Exiting in %s seconds...", colorFg(strconv.Itoa(m.Ticks), "79"))
|
2020-01-14 16:24:01 -05:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-02 18:31:19 -04:00
|
|
|
|
return msg + "\n\n" + label + "\n" + progressbar(80, m.Progress) + "%"
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func checkbox(label string, checked bool) string {
|
|
|
|
|
if checked {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
return colorFg("[x] "+label, "212")
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
2020-10-02 18:31:19 -04:00
|
|
|
|
return fmt.Sprintf("[ ] %s", label)
|
2020-01-10 16:02:04 -05:00
|
|
|
|
}
|
2020-01-14 13:47:36 -05:00
|
|
|
|
|
|
|
|
|
func progressbar(width int, percent float64) string {
|
2020-10-02 18:31:19 -04:00
|
|
|
|
w := float64(progressBarWidth)
|
|
|
|
|
|
2020-01-14 13:47:36 -05:00
|
|
|
|
fullSize := int(math.Round(w * percent))
|
2020-10-02 18:31:19 -04:00
|
|
|
|
var fullCells string
|
|
|
|
|
for i := 0; i < fullSize; i++ {
|
|
|
|
|
fullCells += termenv.String(progressFullChar).Foreground(term.Color(ramp[i])).String()
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-14 13:47:36 -05:00
|
|
|
|
emptySize := int(w) - fullSize
|
2020-10-02 18:31:19 -04:00
|
|
|
|
emptyCells := strings.Repeat(progressEmpty, emptySize)
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s%s %3.0f", fullCells, emptyCells, math.Round(percent*100))
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-15 16:30:34 -04:00
|
|
|
|
// Utils
|
2020-10-02 18:31:19 -04:00
|
|
|
|
|
|
|
|
|
// Color a string's foreground with the given value.
|
|
|
|
|
func colorFg(val, color string) string {
|
|
|
|
|
return termenv.String(val).Foreground(term.Color(color)).String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return a function that will colorize the foreground of a given string.
|
|
|
|
|
func makeFgStyle(color string) func(string) string {
|
|
|
|
|
return termenv.Style{}.Foreground(term.Color(color)).Styled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Color a string's foreground and background with the given value.
|
|
|
|
|
func makeFgBgStyle(fg, bg string) func(string) string {
|
|
|
|
|
return termenv.Style{}.
|
|
|
|
|
Foreground(term.Color(fg)).
|
|
|
|
|
Background(term.Color(bg)).
|
|
|
|
|
Styled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate a blend of colors.
|
|
|
|
|
func makeRamp(colorA, colorB string, steps float64) (s []string) {
|
|
|
|
|
cA, _ := colorful.Hex(colorA)
|
|
|
|
|
cB, _ := colorful.Hex(colorB)
|
|
|
|
|
|
|
|
|
|
for i := 0.0; i < steps; i++ {
|
|
|
|
|
c := cA.BlendLuv(cB, i/steps)
|
|
|
|
|
s = append(s, colorToHex(c))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-11 20:11:26 -05:00
|
|
|
|
// Convert a colorful.Color to a hexadecimal format compatible with termenv.
|
2020-10-02 18:31:19 -04:00
|
|
|
|
func colorToHex(c colorful.Color) string {
|
|
|
|
|
return fmt.Sprintf("#%s%s%s", colorFloatToHex(c.R), colorFloatToHex(c.G), colorFloatToHex(c.B))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function for converting colors to hex. Assumes a value between 0 and
|
|
|
|
|
// 1.
|
2020-10-15 16:30:34 -04:00
|
|
|
|
func colorFloatToHex(f float64) (s string) {
|
|
|
|
|
s = strconv.FormatInt(int64(f*255), 16)
|
|
|
|
|
if len(s) == 1 {
|
|
|
|
|
s = "0" + s
|
2020-10-02 18:31:19 -04:00
|
|
|
|
}
|
2020-10-15 16:30:34 -04:00
|
|
|
|
return
|
2020-01-14 13:47:36 -05:00
|
|
|
|
}
|