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")) 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().Unix()) if err := tea.NewProgram(newModel()).Start(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } }