Commit 12fd7b1

bryfry <bryon@fryer.io>
2025-08-09 16:05:14
pre-count file counts tag: v0.0.1
1 parent 10e727f
internal/apt/apt.go
@@ -0,0 +1,275 @@
+package apt
+
+import (
+	"fmt"
+	"os/exec"
+	"strings"
+	"time"
+
+	"github.com/charmbracelet/bubbles/progress"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
+)
+
+type progressModel struct {
+	progress progress.Model
+	current  int
+	total    int
+	done     bool
+	spinner  int
+	packages []string
+	currentPackage string
+}
+
+var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
+
+func (m progressModel) Init() tea.Cmd {
+	return tea.Tick(time.Millisecond*100, func(time.Time) tea.Msg {
+		return tickMsg{}
+	})
+}
+
+func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	case tea.KeyMsg:
+		return m, tea.Quit
+	case tickMsg:
+		if !m.done {
+			m.spinner = (m.spinner + 1) % len(spinnerFrames)
+			return m, tea.Tick(time.Millisecond*150, func(time.Time) tea.Msg {
+				return tickMsg{}
+			})
+		}
+		return m, nil
+	case progressMsg:
+		m.current = msg.current
+		m.total = msg.total
+		m.currentPackage = msg.currentPackage
+		if m.current >= m.total {
+			m.done = true
+			return m, tea.Quit
+		}
+		return m, nil
+	}
+	return m, nil
+}
+
+func (m progressModel) View() string {
+	if m.done {
+		// Completion style with apt theme (debian red)
+		checkmark := lipgloss.NewStyle().
+			Foreground(lipgloss.Color("#d70751")).
+			Bold(true).
+			Render("✓")
+		
+		title := lipgloss.NewStyle().
+			Foreground(lipgloss.Color("#ffffff")).
+			Bold(true).
+			Render("APT Packages Installed")
+		
+		stats := lipgloss.NewStyle().
+			Foreground(lipgloss.Color("#666666")).
+			Render(fmt.Sprintf("(%d packages)", len(m.packages)))
+		
+		return fmt.Sprintf("\n  %s %s %s\n\n", checkmark, title, stats)
+	}
+	
+	// Progress bar styling with apt-inspired colors (debian red theme)
+	percentage := float64(m.current) / float64(m.total)
+	
+	// Styled spinner 
+	spinner := lipgloss.NewStyle().
+		Foreground(lipgloss.Color("#d70751")).
+		Bold(true).
+		Render(spinnerFrames[m.spinner])
+	
+	// Styled title
+	title := lipgloss.NewStyle().
+		Foreground(lipgloss.Color("#ffffff")).
+		Bold(true).
+		Render("Installing APT Packages")
+	
+	// Progress bar with custom styling
+	progressBar := m.progress.ViewAs(percentage)
+	
+	// Styled stats
+	stats := lipgloss.NewStyle().
+		Foreground(lipgloss.Color("#888888")).
+		Render(fmt.Sprintf("%d packages (%.1f%%)", len(m.packages), percentage*100))
+	
+	// Current package indicator
+	packageIndicator := ""
+	if m.currentPackage != "" {
+		packageIndicator = lipgloss.NewStyle().
+			Foreground(lipgloss.Color("#a50e4e")).
+			Render(fmt.Sprintf(" • installing %s...", m.currentPackage))
+	}
+	
+	return fmt.Sprintf("\n  %s %s\n  %s\n  %s%s\n", 
+		spinner, title, progressBar, stats, packageIndicator)
+}
+
+type progressMsg struct {
+	current        int
+	total          int
+	currentPackage string
+}
+
+type tickMsg struct{}
+
+// Install installs the specified apt packages with progress display
+func Install(packages ...string) error {
+	if len(packages) == 0 {
+		return nil // Nothing to install
+	}
+
+	// Check if apt is available
+	if _, err := exec.LookPath("apt"); err != nil {
+		return fmt.Errorf("apt not found: %w", err)
+	}
+
+	// Setup enhanced progress bar with apt-inspired colors (debian red theme)
+	p := progress.New(
+		progress.WithScaledGradient("#d70751", "#a50e4e"),
+		progress.WithWidth(52),
+		progress.WithoutPercentage(),
+	)
+	
+	model := progressModel{
+		progress: p,
+		total:    100, // Using percentage-based progress
+		packages: packages,
+	}
+
+	program := tea.NewProgram(model)
+	var installErr error
+	
+	go func() {
+		// Update package lists first (5% of progress)
+		program.Send(progressMsg{
+			current:        0,
+			total:          100,
+			currentPackage: "updating package lists",
+		})
+		
+		updateCmd := exec.Command("sudo", "apt", "update", "-qq")
+		if err := updateCmd.Run(); err != nil {
+			installErr = fmt.Errorf("updating package lists: %w", err)
+			program.Quit()
+			return
+		}
+
+		// Start installation of all packages at once
+		program.Send(progressMsg{
+			current:        5,
+			total:          100,
+			currentPackage: fmt.Sprintf("installing %s", strings.Join(packages, ", ")),
+		})
+
+		// Install all packages in a single command
+		args := append([]string{"apt", "install", "-y", "-qq"}, packages...)
+		installCmd := exec.Command("sudo", args...)
+		
+		// Start the command
+		if err := installCmd.Start(); err != nil {
+			installErr = fmt.Errorf("starting apt install: %w", err)
+			program.Quit()
+			return
+		}
+
+		// Estimate progress based on time (apt installs usually take 30-90 seconds)
+		// We'll simulate progress from 5% to 95% over an estimated time
+		estimatedDuration := time.Duration(len(packages)) * 8 * time.Second // ~8 seconds per package
+		startTime := time.Now()
+		
+		// Progress ticker
+		ticker := time.NewTicker(500 * time.Millisecond)
+		defer ticker.Stop()
+		
+		done := make(chan error, 1)
+		go func() {
+			done <- installCmd.Wait()
+		}()
+
+		for {
+			select {
+			case err := <-done:
+				if err != nil {
+					installErr = fmt.Errorf("installing packages: %w", err)
+					program.Quit()
+					return
+				}
+				// Signal completion
+				program.Send(progressMsg{
+					current: 100,
+					total:   100,
+				})
+				return
+				
+			case <-ticker.C:
+				elapsed := time.Since(startTime)
+				progress := float64(elapsed) / float64(estimatedDuration)
+				if progress > 0.9 {
+					progress = 0.9 // Cap at 90% until actually done
+				}
+				
+				currentPercent := int(5 + (progress * 85)) // 5% to 90%
+				
+				program.Send(progressMsg{
+					current:        currentPercent,
+					total:          100,
+					currentPackage: fmt.Sprintf("installing %s", strings.Join(packages, ", ")),
+				})
+			}
+		}
+	}()
+
+	_, err := program.Run()
+	if err != nil {
+		return fmt.Errorf("running progress: %w", err)
+	}
+	
+	if installErr != nil {
+		return installErr
+	}
+
+	return nil
+}
+
+// InstallList is a convenience function for installing from a slice
+func InstallList(packages []string) error {
+	return Install(packages...)
+}
+
+// CheckInstalled checks if packages are already installed
+func CheckInstalled(packages ...string) ([]string, error) {
+	var missing []string
+	
+	for _, pkg := range packages {
+		cmd := exec.Command("dpkg", "-l", pkg)
+		if err := cmd.Run(); err != nil {
+			missing = append(missing, pkg)
+		}
+	}
+	
+	return missing, nil
+}
+
+// InstallMissing only installs packages that aren't already installed
+func InstallMissing(packages ...string) error {
+	missing, err := CheckInstalled(packages...)
+	if err != nil {
+		return fmt.Errorf("checking installed packages: %w", err)
+	}
+	
+	if len(missing) == 0 {
+		return nil // All packages already installed
+	}
+	
+	return Install(missing...)
+}
+
+// GetPackageList returns a formatted list of packages for display
+func GetPackageList(packages []string) string {
+	return strings.Join(packages, ", ")
+}
\ No newline at end of file
internal/golang/count.go
@@ -0,0 +1,5 @@
+package golang
+
+// Total files in the golang archive (hardcoded to avoid pre-counting delay)
+// To update: tar -tf internal/golang/go1.24.6.linux-amd64.tar.gz | wc -l
+const golangTotalFiles = 15735
\ No newline at end of file
internal/golang/golang.go
@@ -164,12 +164,6 @@ func Install() error {
 		return fmt.Errorf("cleaning up old install: %w", err)
 	}
 
-	// Count total files first
-	totalFiles, err := countTarFiles(goSrc)
-	if err != nil {
-		return fmt.Errorf("counting files: %w", err)
-	}
-
 	// Setup enhanced progress bar with custom styling
 	p := progress.New(
 		progress.WithScaledGradient("#ffffff", "#00add8"),
@@ -179,7 +173,7 @@ func Install() error {
 	
 	model := progressModel{
 		progress: p,
-		total:    totalFiles,
+		total:    golangTotalFiles,
 	}
 
 	program := tea.NewProgram(model)
@@ -205,7 +199,7 @@ func Install() error {
 			fileCount++
 
 			// Send progress update
-			program.Send(progressMsg{current: fileCount, total: totalFiles})
+			program.Send(progressMsg{current: fileCount, total: golangTotalFiles})
 
 			mode := os.FileMode(header.Mode)
 			target := filepath.Join(installDir, header.Name)
@@ -239,7 +233,7 @@ func Install() error {
 		}
 
 		// Signal completion
-		program.Send(progressMsg{current: totalFiles, total: totalFiles})
+		program.Send(progressMsg{current: golangTotalFiles, total: golangTotalFiles})
 	}()
 
 	_, err = program.Run()
@@ -250,24 +244,3 @@ func Install() error {
 	return nil
 }
 
-func countTarFiles(data []byte) (int, error) {
-	gr, err := gzip.NewReader(bytes.NewReader(data))
-	if err != nil {
-		return 0, err
-	}
-	defer gr.Close()
-	tr := tar.NewReader(gr)
-
-	count := 0
-	for {
-		_, err := tr.Next()
-		if err != nil {
-			if errors.Is(err, io.EOF) {
-				break
-			}
-			return 0, err
-		}
-		count++
-	}
-	return count, nil
-}
internal/nvim/count.go
@@ -0,0 +1,5 @@
+package nvim
+
+// Total files in the nvim archive (hardcoded to avoid pre-counting delay)
+// To update: tar -tf internal/nvim/nvim-linux-x86_64.tar.gz | wc -l
+const nvimTotalFiles = 2141
\ No newline at end of file
internal/nvim/nvim.go
@@ -189,12 +189,6 @@ func Install() error {
 		}
 	}
 
-	// Count total files first
-	totalFiles, err := countTarFiles(nvimSrc)
-	if err != nil {
-		return fmt.Errorf("counting files: %w", err)
-	}
-
 	// Setup enhanced progress bar with vim-inspired colors
 	p := progress.New(
 		progress.WithScaledGradient("#00ff00", "#88ff88"),
@@ -204,7 +198,7 @@ func Install() error {
 	
 	model := progressModel{
 		progress: p,
-		total:    totalFiles + 1, // +1 for git clone step
+		total:    nvimTotalFiles + 1, // +1 for git clone step
 	}
 
 	program := tea.NewProgram(model)
@@ -234,7 +228,7 @@ func Install() error {
 			fileCount++
 
 			// Send progress update
-			program.Send(progressMsg{current: fileCount, total: totalFiles + 1, phase: "extract"})
+			program.Send(progressMsg{current: fileCount, total: nvimTotalFiles + 1, phase: "extract"})
 
 			mode := os.FileMode(header.Mode)
 			target := filepath.Join(installDir, header.Name)
@@ -292,7 +286,7 @@ func Install() error {
 		}
 
 		// Clone kickstart.nvim configuration
-		program.Send(progressMsg{current: totalFiles, total: totalFiles + 1, phase: "git"})
+		program.Send(progressMsg{current: nvimTotalFiles, total: nvimTotalFiles + 1, phase: "git"})
 		
 		configDir := os.Getenv("XDG_CONFIG_HOME")
 		if configDir == "" {
@@ -318,7 +312,7 @@ func Install() error {
 		}
 
 		// Signal completion
-		program.Send(progressMsg{current: totalFiles + 1, total: totalFiles + 1, phase: "complete"})
+		program.Send(progressMsg{current: nvimTotalFiles + 1, total: nvimTotalFiles + 1, phase: "complete"})
 	}()
 
 	_, err = program.Run()
@@ -332,25 +326,3 @@ func Install() error {
 
 	return nil
 }
-
-func countTarFiles(data []byte) (int, error) {
-	gr, err := gzip.NewReader(bytes.NewReader(data))
-	if err != nil {
-		return 0, err
-	}
-	defer gr.Close()
-	tr := tar.NewReader(gr)
-
-	count := 0
-	for {
-		_, err := tr.Next()
-		if err != nil {
-			if errors.Is(err, io.EOF) {
-				break
-			}
-			return 0, err
-		}
-		count++
-	}
-	return count, nil
-}
internal/zig/count.go
@@ -0,0 +1,5 @@
+package zig
+
+// Total files in the zig archive (hardcoded to avoid pre-counting delay)
+// To update: tar -tf internal/zig/zig-linux-x86_64.tar.xz | wc -l
+const zigTotalFiles = 16391
\ No newline at end of file
internal/zig/zig.go
@@ -23,10 +23,6 @@ import (
 //go:embed zig-linux-x86_64.tar.xz
 var zigSrc []byte
 
-// Total files in the zig archive (hardcoded to avoid pre-counting delay)
-// To update: tar -tf internal/zig/zig-linux-x86_64.tar.xz | wc -l
-const zigTotalFiles = 16391
-
 type progressModel struct {
 	progress progress.Model
 	current  int
main.go
@@ -8,6 +8,7 @@ import (
 	"log/slog"
 	"os"
 
+	"github.com/bryfry/bindle/internal/apt"
 	"github.com/bryfry/bindle/internal/bashrcd"
 	"github.com/bryfry/bindle/internal/extractor"
 	"github.com/bryfry/bindle/internal/golang"
@@ -22,7 +23,13 @@ const _homeRoot = "home"
 
 func main() {
 
-	err := golang.Install()
+	err := apt.Install("vim", "tmux", "htop", "curl", "shellcheck", "git", "git-lfs")
+	if err != nil {
+		fmt.Println(err)
+		return // fmt.Errorf("apt install failed: %w", err)
+	}
+
+	err = golang.Install()
 	if err != nil {
 		fmt.Println(err)
 		return // fmt.Errorf("config deploy failed: %w", path, err)
README.md
@@ -8,8 +8,12 @@
 - [x] nvim
 - [x] nvim.kickstart
 - [x] zig
-- [ ] bashrc.d
-- [ ] apt
-  - vim, tmux, htop, curl, shellcheck, git, git-lfs
+- [x] bashrc.d
+- [x] apt
+- [ ] nvim caches and installs
+- [ ] move the progress bars out into a standard package, update extractors to set theme and pass updates
+- [ ] cobra
+- [ ] create release
+- [ ] create specific arch based embed and release
 
 ### Update vendor