main
Raw Download raw file
  1package golang
  2
  3import (
  4	_ "embed"
  5	"text/template"
  6	"time"
  7
  8	"archive/tar"
  9	"bytes"
 10	"compress/gzip"
 11	"errors"
 12	"fmt"
 13	"io"
 14	"os"
 15	"path/filepath"
 16
 17	"github.com/charmbracelet/bubbles/progress"
 18	tea "github.com/charmbracelet/bubbletea"
 19	"github.com/charmbracelet/lipgloss"
 20)
 21
 22//go:embed go1.*.linux-amd64.tar.gz
 23var goSrc []byte
 24
 25type progressModel struct {
 26	progress progress.Model
 27	current  int
 28	total    int
 29	done     bool
 30	spinner  int
 31}
 32
 33var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
 34
 35func (m progressModel) Init() tea.Cmd {
 36	return tea.Tick(time.Millisecond*100, func(time.Time) tea.Msg {
 37		return tickMsg{}
 38	})
 39}
 40
 41func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 42	switch msg := msg.(type) {
 43	case tea.KeyMsg:
 44		return m, tea.Quit
 45	case tickMsg:
 46		if !m.done {
 47			m.spinner = (m.spinner + 1) % len(spinnerFrames)
 48			return m, tea.Tick(time.Millisecond*100, func(time.Time) tea.Msg {
 49				return tickMsg{}
 50			})
 51		}
 52		return m, nil
 53	case progressMsg:
 54		m.current = msg.current
 55		m.total = msg.total
 56		if m.current >= m.total {
 57			m.done = true
 58			return m, tea.Quit
 59		}
 60		return m, nil
 61	}
 62	return m, nil
 63}
 64
 65func (m progressModel) View() string {
 66	if m.done {
 67		// Completion style
 68		checkmark := lipgloss.NewStyle().
 69			Foreground(lipgloss.Color("#00add8")).
 70			Bold(true).
 71			Render("✓")
 72		
 73		title := lipgloss.NewStyle().
 74			Foreground(lipgloss.Color("#ffffff")).
 75			Bold(true).
 76			Render("Go Installation Complete")
 77		
 78		stats := lipgloss.NewStyle().
 79			Foreground(lipgloss.Color("#666666")).
 80			Render(fmt.Sprintf("(%d files extracted)", m.total))
 81		
 82		return fmt.Sprintf("\n  %s %s %s\n\n", checkmark, title, stats)
 83	}
 84	
 85	// Progress bar styling
 86	percentage := float64(m.current) / float64(m.total)
 87	
 88	// Styled spinner
 89	spinner := lipgloss.NewStyle().
 90		Foreground(lipgloss.Color("#00add8")).
 91		Bold(true).
 92		Render(spinnerFrames[m.spinner])
 93	
 94	// Styled title
 95	title := lipgloss.NewStyle().
 96		Foreground(lipgloss.Color("#ffffff")).
 97		Bold(true).
 98		Render("Installing Go")
 99	
100	// Progress bar with custom styling
101	progressBar := m.progress.ViewAs(percentage)
102	
103	// Styled stats
104	stats := lipgloss.NewStyle().
105		Foreground(lipgloss.Color("#888888")).
106		Render(fmt.Sprintf("%d/%d files (%.1f%%)", m.current, m.total, percentage*100))
107	
108	// File rate calculation (rough estimate)
109	rate := ""
110	if m.current > 100 {
111		rate = lipgloss.NewStyle().
112			Foreground(lipgloss.Color("#00add8")).
113			Render(" • extracting...")
114	}
115	
116	return fmt.Sprintf("\n  %s %s\n  %s\n  %s%s\n", 
117		spinner, title, progressBar, stats, rate)
118}
119
120type progressMsg struct {
121	current int
122	total   int
123}
124
125type tickMsg struct{}
126
127func bashrcd() error {
128	// TODO: installDir
129	homeDir := os.Getenv("HOME")
130	installDir := filepath.Join(homeDir, ".local")
131	rcPath := filepath.Join(homeDir, ".bashrc.d", "go.sh")
132
133	const tmpl = `#!/bin/bash
134export PATH=$PATH:{{ .InstallDir }}/go/bin
135`
136
137	t, err := template.New("pathRC").Parse(tmpl)
138	if err != nil {
139		return err
140	}
141	f, err := os.Create(rcPath)
142	if err != nil {
143		return err
144	}
145	err = t.Execute(f, struct{ InstallDir string }{InstallDir: installDir})
146	if err != nil {
147		return err
148	}
149	return nil
150}
151
152func Install() error {
153	homeDir := os.Getenv("HOME")
154	installDir := filepath.Join(homeDir, ".local")
155
156	err := bashrcd()
157	if err != nil {
158		return fmt.Errorf("installing bashrc.d env helper: %w", err)
159	}
160
161	goDir := filepath.Join(installDir, "go")
162	err = os.RemoveAll(goDir)
163	if err != nil {
164		return fmt.Errorf("cleaning up old install: %w", err)
165	}
166
167	// Setup enhanced progress bar with custom styling
168	p := progress.New(
169		progress.WithScaledGradient("#ffffff", "#00add8"),
170		progress.WithWidth(50),
171		progress.WithoutPercentage(),
172	)
173	
174	model := progressModel{
175		progress: p,
176		total:    golangTotalFiles,
177	}
178
179	program := tea.NewProgram(model)
180	go func() {
181		gr, err := gzip.NewReader(bytes.NewReader(goSrc))
182		if err != nil {
183			program.Quit()
184			return
185		}
186		defer gr.Close()
187		tr := tar.NewReader(gr)
188
189		fileCount := 0
190		for {
191			header, err := tr.Next()
192			if err != nil {
193				if errors.Is(err, io.EOF) {
194					break
195				}
196				program.Quit()
197				return
198			}
199			fileCount++
200
201			// Send progress update
202			program.Send(progressMsg{current: fileCount, total: golangTotalFiles})
203
204			mode := os.FileMode(header.Mode)
205			target := filepath.Join(installDir, header.Name)
206			switch header.Typeflag {
207			case tar.TypeDir:
208				err = os.MkdirAll(target, mode)
209				if err != nil {
210					program.Quit()
211					return
212				}
213
214			case tar.TypeReg:
215				flag := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
216				f, err := os.OpenFile(target, flag, mode)
217				if err != nil {
218					program.Quit()
219					return
220				}
221				_, err = io.Copy(f, tr)
222				if err != nil {
223					f.Close()
224					program.Quit()
225					return
226				}
227				err = f.Close()
228				if err != nil {
229					program.Quit()
230					return
231				}
232			}
233		}
234
235		// Signal completion
236		program.Send(progressMsg{current: golangTotalFiles, total: golangTotalFiles})
237	}()
238
239	_, err = program.Run()
240	if err != nil {
241		return fmt.Errorf("running progress: %w", err)
242	}
243
244	return nil
245}
246