Commit 12fd7b1
Changed files (9)
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