main
Raw Download raw file
  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}