main
Raw Download raw file
  1package apt
  2
  3import (
  4	"fmt"
  5	"os/exec"
  6	"strings"
  7	"time"
  8
  9	"github.com/charmbracelet/bubbles/progress"
 10	tea "github.com/charmbracelet/bubbletea"
 11	"github.com/charmbracelet/lipgloss"
 12)
 13
 14type progressModel struct {
 15	progress progress.Model
 16	current  int
 17	total    int
 18	done     bool
 19	spinner  int
 20	packages []string
 21	currentPackage string
 22}
 23
 24var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
 25
 26func (m progressModel) Init() tea.Cmd {
 27	return tea.Tick(time.Millisecond*100, func(time.Time) tea.Msg {
 28		return tickMsg{}
 29	})
 30}
 31
 32func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 33	switch msg := msg.(type) {
 34	case tea.KeyMsg:
 35		return m, tea.Quit
 36	case tickMsg:
 37		if !m.done {
 38			m.spinner = (m.spinner + 1) % len(spinnerFrames)
 39			return m, tea.Tick(time.Millisecond*150, func(time.Time) tea.Msg {
 40				return tickMsg{}
 41			})
 42		}
 43		return m, nil
 44	case progressMsg:
 45		m.current = msg.current
 46		m.total = msg.total
 47		m.currentPackage = msg.currentPackage
 48		if m.current >= m.total {
 49			m.done = true
 50			return m, tea.Quit
 51		}
 52		return m, nil
 53	}
 54	return m, nil
 55}
 56
 57func (m progressModel) View() string {
 58	if m.done {
 59		// Completion style with apt theme (debian red)
 60		checkmark := lipgloss.NewStyle().
 61			Foreground(lipgloss.Color("#d70751")).
 62			Bold(true).
 63			Render("✓")
 64		
 65		title := lipgloss.NewStyle().
 66			Foreground(lipgloss.Color("#ffffff")).
 67			Bold(true).
 68			Render("APT Packages Installed")
 69		
 70		stats := lipgloss.NewStyle().
 71			Foreground(lipgloss.Color("#666666")).
 72			Render(fmt.Sprintf("(%d packages)", len(m.packages)))
 73		
 74		return fmt.Sprintf("\n  %s %s %s\n\n", checkmark, title, stats)
 75	}
 76	
 77	// Progress bar styling with apt-inspired colors (debian red theme)
 78	percentage := float64(m.current) / float64(m.total)
 79	
 80	// Styled spinner 
 81	spinner := lipgloss.NewStyle().
 82		Foreground(lipgloss.Color("#d70751")).
 83		Bold(true).
 84		Render(spinnerFrames[m.spinner])
 85	
 86	// Styled title
 87	title := lipgloss.NewStyle().
 88		Foreground(lipgloss.Color("#ffffff")).
 89		Bold(true).
 90		Render("Installing APT Packages")
 91	
 92	// Progress bar with custom styling
 93	progressBar := m.progress.ViewAs(percentage)
 94	
 95	// Styled stats
 96	stats := lipgloss.NewStyle().
 97		Foreground(lipgloss.Color("#888888")).
 98		Render(fmt.Sprintf("%d packages (%.1f%%)", len(m.packages), percentage*100))
 99	
100	// Current package indicator
101	packageIndicator := ""
102	if m.currentPackage != "" {
103		packageIndicator = lipgloss.NewStyle().
104			Foreground(lipgloss.Color("#a50e4e")).
105			Render(fmt.Sprintf(" • installing %s...", m.currentPackage))
106	}
107	
108	return fmt.Sprintf("\n  %s %s\n  %s\n  %s%s\n", 
109		spinner, title, progressBar, stats, packageIndicator)
110}
111
112type progressMsg struct {
113	current        int
114	total          int
115	currentPackage string
116}
117
118type tickMsg struct{}
119
120// Install installs the specified apt packages with progress display
121func Install(packages ...string) error {
122	if len(packages) == 0 {
123		return nil // Nothing to install
124	}
125
126	// Check if apt is available
127	if _, err := exec.LookPath("apt"); err != nil {
128		return fmt.Errorf("apt not found: %w", err)
129	}
130
131	// Setup enhanced progress bar with apt-inspired colors (debian red theme)
132	p := progress.New(
133		progress.WithScaledGradient("#d70751", "#a50e4e"),
134		progress.WithWidth(52),
135		progress.WithoutPercentage(),
136	)
137	
138	model := progressModel{
139		progress: p,
140		total:    100, // Using percentage-based progress
141		packages: packages,
142	}
143
144	program := tea.NewProgram(model)
145	var installErr error
146	
147	go func() {
148		// Update package lists first (5% of progress)
149		program.Send(progressMsg{
150			current:        0,
151			total:          100,
152			currentPackage: "updating package lists",
153		})
154		
155		updateCmd := exec.Command("sudo", "apt", "update", "-qq")
156		if err := updateCmd.Run(); err != nil {
157			installErr = fmt.Errorf("updating package lists: %w", err)
158			program.Quit()
159			return
160		}
161
162		// Start installation of all packages at once
163		program.Send(progressMsg{
164			current:        5,
165			total:          100,
166			currentPackage: fmt.Sprintf("installing %s", strings.Join(packages, ", ")),
167		})
168
169		// Install all packages in a single command
170		args := append([]string{"apt", "install", "-y", "-qq"}, packages...)
171		installCmd := exec.Command("sudo", args...)
172		
173		// Start the command
174		if err := installCmd.Start(); err != nil {
175			installErr = fmt.Errorf("starting apt install: %w", err)
176			program.Quit()
177			return
178		}
179
180		// Estimate progress based on time (apt installs usually take 30-90 seconds)
181		// We'll simulate progress from 5% to 95% over an estimated time
182		estimatedDuration := time.Duration(len(packages)) * 8 * time.Second // ~8 seconds per package
183		startTime := time.Now()
184		
185		// Progress ticker
186		ticker := time.NewTicker(500 * time.Millisecond)
187		defer ticker.Stop()
188		
189		done := make(chan error, 1)
190		go func() {
191			done <- installCmd.Wait()
192		}()
193
194		for {
195			select {
196			case err := <-done:
197				if err != nil {
198					installErr = fmt.Errorf("installing packages: %w", err)
199					program.Quit()
200					return
201				}
202				// Signal completion
203				program.Send(progressMsg{
204					current: 100,
205					total:   100,
206				})
207				return
208				
209			case <-ticker.C:
210				elapsed := time.Since(startTime)
211				progress := float64(elapsed) / float64(estimatedDuration)
212				if progress > 0.9 {
213					progress = 0.9 // Cap at 90% until actually done
214				}
215				
216				currentPercent := int(5 + (progress * 85)) // 5% to 90%
217				
218				program.Send(progressMsg{
219					current:        currentPercent,
220					total:          100,
221					currentPackage: fmt.Sprintf("installing %s", strings.Join(packages, ", ")),
222				})
223			}
224		}
225	}()
226
227	_, err := program.Run()
228	if err != nil {
229		return fmt.Errorf("running progress: %w", err)
230	}
231	
232	if installErr != nil {
233		return installErr
234	}
235
236	return nil
237}
238
239// InstallList is a convenience function for installing from a slice
240func InstallList(packages []string) error {
241	return Install(packages...)
242}
243
244// CheckInstalled checks if packages are already installed
245func CheckInstalled(packages ...string) ([]string, error) {
246	var missing []string
247	
248	for _, pkg := range packages {
249		cmd := exec.Command("dpkg", "-l", pkg)
250		if err := cmd.Run(); err != nil {
251			missing = append(missing, pkg)
252		}
253	}
254	
255	return missing, nil
256}
257
258// InstallMissing only installs packages that aren't already installed
259func InstallMissing(packages ...string) error {
260	missing, err := CheckInstalled(packages...)
261	if err != nil {
262		return fmt.Errorf("checking installed packages: %w", err)
263	}
264	
265	if len(missing) == 0 {
266		return nil // All packages already installed
267	}
268	
269	return Install(missing...)
270}
271
272// GetPackageList returns a formatted list of packages for display
273func GetPackageList(packages []string) string {
274	return strings.Join(packages, ", ")
275}