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