main
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