diff --git a/examples/package-manager/main.go b/examples/package-manager/main.go new file mode 100644 index 0000000000..0d16f9a0a1 --- /dev/null +++ b/examples/package-manager/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "fmt" + "math/rand" + "os" + "strings" + "time" + + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type model struct { + packages []string + index int + width int + height int + spinner spinner.Model + progress progress.Model + done bool +} + +var ( + currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) + subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239")) + doneStyle = lipgloss.NewStyle().Margin(1, 2) + checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") +) + +func newModel() model { + p := progress.New( + progress.WithDefaultGradient(), + progress.WithWidth(40), + progress.WithoutPercentage(), + ) + s := spinner.New() + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + return model{ + packages: getPackages(), + spinner: s, + progress: p, + } +} + +func (m model) Init() tea.Cmd { + return tea.Batch(downloadAndInstall(m.packages[m.index]), m.spinner.Tick) +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width, m.height = msg.Width, msg.Height + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc", "q": + return m, tea.Quit + } + case installedPkgMsg: + if m.index >= len(m.packages)-1 { + // Everything's been installed. We're done! + m.done = true + return m, tea.Quit + } + + // Update progress bar + progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages)-1)) + + m.index++ + return m, tea.Batch( + progressCmd, + tea.Printf("%s %s", checkMark, m.packages[m.index]), // print success message above our program + downloadAndInstall(m.packages[m.index]), // download the next package + ) + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + case progress.FrameMsg: + newModel, cmd := m.progress.Update(msg) + if newModel, ok := newModel.(progress.Model); ok { + m.progress = newModel + } + return m, cmd + } + return m, nil +} + +func (m model) View() string { + n := len(m.packages) + w := lipgloss.Width(fmt.Sprintf("%d", n)) + + if m.done { + return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n)) + } + + pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n-1) + + spin := m.spinner.View() + " " + prog := m.progress.View() + cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount)) + + pkgName := currentPkgNameStyle.Render(m.packages[m.index]) + info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName) + + cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount)) + gap := strings.Repeat(" ", cellsRemaining) + + return spin + info + gap + prog + pkgCount +} + +type installedPkgMsg string + +func downloadAndInstall(pkg string) tea.Cmd { + // This is where you'd do i/o stuff to download and install packages. In + // our case we're just pausing for a moment to simulate the process. + d := time.Millisecond * time.Duration(rand.Intn(500)) + return tea.Tick(d, func(t time.Time) tea.Msg { + return installedPkgMsg(pkg) + }) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func main() { + rand.Seed(time.Now().UnixMicro()) + + if err := tea.NewProgram(newModel()).Start(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/examples/package-manager/packages.go b/examples/package-manager/packages.go new file mode 100644 index 0000000000..7ed8478a3c --- /dev/null +++ b/examples/package-manager/packages.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "math/rand" +) + +var packages = []string{ + "vegeutils", + "libgardening", + "currykit", + "spicerack", + "fullenglish", + "eggy", + "bad-kitty", + "chai", + "hojicha", + "libtacos", + "babys-monads", + "libpurring", + "currywurst-devel", + "xmodmeow", + "licorice-utils", + "cashew-apple", + "rock-lobster", + "standmixer", + "coffee-CUPS", + "libesszet", + "zeichenorientierte-benutzerschnittstellen", + "schnurrkit", + "old-socks-devel", + "jalapeño", + "molasses-utils", + "xkohlrabi", + "party-gherkin", + "snow-peas", + "libyuzu", +} + +func getPackages() []string { + pkgs := packages + copy(pkgs, packages) + + rand.Shuffle(len(pkgs), func(i, j int) { + pkgs[i], pkgs[j] = pkgs[j], pkgs[i] + }) + + for k := range pkgs { + pkgs[k] += fmt.Sprintf("-%d.%d.%d", rand.Intn(10), rand.Intn(10), rand.Intn(10)) + } + return pkgs +} diff --git a/standard_renderer.go b/standard_renderer.go index f0ece47689..f88dd2bc14 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -505,7 +505,7 @@ func Println(args ...interface{}) Cmd { // its own line. // // If the altscreen is active no output will be printed. -func Printf(format string, args ...interface{}) Cmd { +func Printf(template string, args ...interface{}) Cmd { return func() Msg { return printLineMessage{ messageBody: fmt.Sprintf(template, args...),