main
Raw Download raw file
  1package nvim
  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	"strings"
 17
 18	"github.com/charmbracelet/bubbles/progress"
 19	tea "github.com/charmbracelet/bubbletea"
 20	"github.com/charmbracelet/lipgloss"
 21	"github.com/go-git/go-git/v5"
 22)
 23
 24//go:embed nvim-linux-x86_64.tar.gz
 25var nvimSrc []byte
 26
 27type progressModel struct {
 28	progress progress.Model
 29	current  int
 30	total    int
 31	done     bool
 32	phase    string
 33	spinner  int
 34}
 35
 36var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
 37
 38func (m progressModel) Init() tea.Cmd {
 39	return tea.Tick(time.Millisecond*120, func(time.Time) tea.Msg {
 40		return tickMsg{}
 41	})
 42}
 43
 44func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 45	switch msg := msg.(type) {
 46	case tea.KeyMsg:
 47		return m, tea.Quit
 48	case tickMsg:
 49		if !m.done {
 50			m.spinner = (m.spinner + 1) % len(spinnerFrames)
 51			return m, tea.Tick(time.Millisecond*120, func(time.Time) tea.Msg {
 52				return tickMsg{}
 53			})
 54		}
 55		return m, nil
 56	case progressMsg:
 57		m.current = msg.current
 58		m.total = msg.total
 59		m.phase = msg.phase
 60		if m.current >= m.total && m.phase == "complete" {
 61			m.done = true
 62			return m, tea.Quit
 63		}
 64		return m, nil
 65	}
 66	return m, nil
 67}
 68
 69func (m progressModel) View() string {
 70	if m.done {
 71		// Completion style with vim colors
 72		checkmark := lipgloss.NewStyle().
 73			Foreground(lipgloss.Color("#00ff00")).
 74			Bold(true).
 75			Render("✓")
 76		
 77		title := lipgloss.NewStyle().
 78			Foreground(lipgloss.Color("#ffffff")).
 79			Bold(true).
 80			Render("Nvim + Kickstart Installation Complete")
 81		
 82		stats := lipgloss.NewStyle().
 83			Foreground(lipgloss.Color("#666666")).
 84			Render(fmt.Sprintf("(%d files + kickstart.nvim config)", m.total))
 85		
 86		return fmt.Sprintf("\n  %s %s %s\n\n", checkmark, title, stats)
 87	}
 88	
 89	// Progress bar styling with vim-inspired colors
 90	percentage := float64(m.current) / float64(m.total)
 91	
 92	// Styled spinner
 93	spinnerColor := "#00ff00" // Green like vim
 94	if m.phase == "git" {
 95		spinnerColor = "#ff6600" // Orange for git phase
 96	}
 97	
 98	spinner := lipgloss.NewStyle().
 99		Foreground(lipgloss.Color(spinnerColor)).
100		Bold(true).
101		Render(spinnerFrames[m.spinner])
102	
103	// Styled title
104	status := "Installing Nvim"
105	titleColor := "#ffffff"
106	if m.phase == "git" {
107		status = "Cloning kickstart.nvim"
108		titleColor = "#ff9900"
109	}
110	
111	title := lipgloss.NewStyle().
112		Foreground(lipgloss.Color(titleColor)).
113		Bold(true).
114		Render(status)
115	
116	// Progress bar with custom styling
117	progressBar := m.progress.ViewAs(percentage)
118	
119	// Styled stats
120	stats := lipgloss.NewStyle().
121		Foreground(lipgloss.Color("#888888")).
122		Render(fmt.Sprintf("%d/%d files (%.1f%%)", m.current, m.total, percentage*100))
123	
124	// Phase indicator
125	phaseIndicator := ""
126	if m.phase == "git" {
127		phaseIndicator = lipgloss.NewStyle().
128			Foreground(lipgloss.Color("#ffaa00")).
129			Render(" • configuring editor...")
130	} else if m.current > 50 {
131		phaseIndicator = lipgloss.NewStyle().
132			Foreground(lipgloss.Color("#00aa00")).
133			Render(" • extracting runtime...")
134	}
135	
136	return fmt.Sprintf("\n  %s %s\n  %s\n  %s%s\n", 
137		spinner, title, progressBar, stats, phaseIndicator)
138}
139
140type progressMsg struct {
141	current int
142	total   int
143	phase   string
144}
145
146type tickMsg struct{}
147
148func bashrcd() error {
149	homeDir := os.Getenv("HOME")
150	installDir := filepath.Join(homeDir, ".local")
151	rcPath := filepath.Join(homeDir, ".bashrc.d", "nvim.sh")
152
153	const tmpl = `#!/bin/bash
154export PATH=$PATH:{{ .InstallDir }}/bin
155alias vim=nvim
156`
157
158	t, err := template.New("pathRC").Parse(tmpl)
159	if err != nil {
160		return err
161	}
162	f, err := os.Create(rcPath)
163	if err != nil {
164		return err
165	}
166	err = t.Execute(f, struct{ InstallDir string }{InstallDir: installDir})
167	if err != nil {
168		return err
169	}
170	return nil
171}
172
173func Install() error {
174	homeDir := os.Getenv("HOME")
175	installDir := filepath.Join(homeDir, ".local")
176
177	err := bashrcd()
178	if err != nil {
179		return fmt.Errorf("installing bashrc.d env helper: %w", err)
180	}
181
182	nvimShareDir := filepath.Join(installDir, "share/nvim")
183	nvimLibDir := filepath.Join(installDir, "lib/nvim")
184	nvimBin := filepath.Join(installDir, "bin/nvim")
185	for _, path := range []string{nvimShareDir, nvimLibDir, nvimBin} {
186		err = os.RemoveAll(path)
187		if err != nil {
188			return fmt.Errorf("cleaning up old install path=%q: %w", path, err)
189		}
190	}
191
192	// Setup enhanced progress bar with vim-inspired colors
193	p := progress.New(
194		progress.WithScaledGradient("#00ff00", "#88ff88"),
195		progress.WithWidth(45),
196		progress.WithoutPercentage(),
197	)
198	
199	model := progressModel{
200		progress: p,
201		total:    nvimTotalFiles + 1, // +1 for git clone step
202	}
203
204	program := tea.NewProgram(model)
205	var installErr error
206	
207	go func() {
208		gr, err := gzip.NewReader(bytes.NewReader(nvimSrc))
209		if err != nil {
210			installErr = fmt.Errorf("setting up gzip reader: %w", err)
211			program.Quit()
212			return
213		}
214		defer gr.Close()
215		tr := tar.NewReader(gr)
216
217		fileCount := 0
218		for {
219			header, err := tr.Next()
220			if err != nil {
221				if errors.Is(err, io.EOF) {
222					break
223				}
224				installErr = err
225				program.Quit()
226				return
227			}
228			fileCount++
229
230			// Send progress update
231			program.Send(progressMsg{current: fileCount, total: nvimTotalFiles + 1, phase: "extract"})
232
233			mode := os.FileMode(header.Mode)
234			target := filepath.Join(installDir, header.Name)
235
236			// Strip the first component (nvim-linux-x86_64/)
237			parts := strings.Split(header.Name, "/")
238			if len(parts) > 0 {
239				parts = parts[1:]
240				if len(parts) == 0 {
241					continue
242				}
243				target = filepath.Join(append([]string{installDir}, parts...)...)
244			}
245
246			switch header.Typeflag {
247			case tar.TypeDir:
248				err = os.MkdirAll(target, mode)
249				if err != nil {
250					installErr = err
251					program.Quit()
252					return
253				}
254
255			case tar.TypeReg:
256				// Ensure parent directory exists
257				dir := filepath.Dir(target)
258				err = os.MkdirAll(dir, 0755)
259				if err != nil {
260					installErr = err
261					program.Quit()
262					return
263				}
264
265				flag := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
266				f, err := os.OpenFile(target, flag, mode)
267				if err != nil {
268					installErr = err
269					program.Quit()
270					return
271				}
272				_, err = io.Copy(f, tr)
273				if err != nil {
274					f.Close()
275					installErr = err
276					program.Quit()
277					return
278				}
279				err = f.Close()
280				if err != nil {
281					installErr = err
282					program.Quit()
283					return
284				}
285			}
286		}
287
288		// Clone kickstart.nvim configuration
289		program.Send(progressMsg{current: nvimTotalFiles, total: nvimTotalFiles + 1, phase: "git"})
290		
291		configDir := os.Getenv("XDG_CONFIG_HOME")
292		if configDir == "" {
293			configDir = filepath.Join(homeDir, ".config")
294		}
295		nvimConfigDir := filepath.Join(configDir, "nvim")
296		
297		// Remove existing nvim config if it exists
298		err = os.RemoveAll(nvimConfigDir)
299		if err != nil {
300			installErr = fmt.Errorf("cleaning up existing nvim config: %w", err)
301			program.Quit()
302			return
303		}
304
305		_, err = git.PlainClone(nvimConfigDir, false, &git.CloneOptions{
306			URL: "https://github.com/nvim-lua/kickstart.nvim.git",
307		})
308		if err != nil {
309			installErr = fmt.Errorf("cloning kickstart.nvim: %w", err)
310			program.Quit()
311			return
312		}
313
314		// Signal completion
315		program.Send(progressMsg{current: nvimTotalFiles + 1, total: nvimTotalFiles + 1, phase: "complete"})
316	}()
317
318	_, err = program.Run()
319	if err != nil {
320		return fmt.Errorf("running progress: %w", err)
321	}
322	
323	if installErr != nil {
324		return installErr
325	}
326
327	return nil
328}