package cmd import ( "fmt" "os" "os/exec" "path/filepath" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( headerStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("86")). Bold(true). Padding(0, 0, 1, 0) inputStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("252")). Background(lipgloss.Color("235")). Padding(0, 1) buttonStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("86")). Background(lipgloss.Color("235")). Padding(0, 2). Margin(0, 1) buttonActiveStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("235")). Background(lipgloss.Color("86")). Padding(0, 2). Margin(0, 1) helpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("240")) ) type InitModel struct { step int blogName string siteTitle string authorName string authorRole string authorBio string theme string themeList list.Model inputBuffer string quitting bool finished bool } func (m *InitModel) Init() tea.Cmd { themes := []string{ "modern-light", "modern-dark", "modern-dark-2", "modern-dark-catppuccin", "everforest", "gruvbox", "rose-pine", "terminal-gruvbox", "tufte", } items := make([]list.Item, len(themes)) for i, t := range themes { items[i] = listItem{t} } m.themeList = list.New(items, list.NewDefaultDelegate(), 10, 20) m.themeList.Title = "Select a theme:" m.themeList.SetShowStatusBar(false) m.themeList.SetFilteringEnabled(false) m.themeList.SetWidth(40) return nil } type listItem struct { title string } func (i listItem) Title() string { return i.title } func (i listItem) Description() string { return "" } func (i listItem) FilterValue() string { return i.title } func (m *InitModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.step == 5 { if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "enter" { selected := m.themeList.SelectedItem().(listItem) m.theme = selected.title m.step++ m.finished = true return m, tea.Quit } var cmd tea.Cmd m.themeList, cmd = m.themeList.Update(msg) return m, cmd } switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": m.quitting = true return m, nil case "enter": return m.handleEnter() case "backspace": if len(m.inputBuffer) > 0 { m.inputBuffer = m.inputBuffer[:len(m.inputBuffer)-1] } case "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "_", " ", ".": m.inputBuffer += msg.String() } } return m, nil } func (m *InitModel) handleEnter() (tea.Model, tea.Cmd) { switch m.step { case 0: m.blogName = m.inputBuffer m.inputBuffer = "" m.step++ case 1: m.siteTitle = m.inputBuffer if m.siteTitle == "" { m.siteTitle = m.blogName } m.inputBuffer = "" m.step++ case 2: m.authorName = m.inputBuffer m.inputBuffer = "" m.step++ case 3: m.authorRole = m.inputBuffer m.inputBuffer = "" m.step++ case 4: m.authorBio = m.inputBuffer m.inputBuffer = "" m.step++ case 5: selected := m.themeList.SelectedItem().(listItem) m.theme = selected.title m.step++ m.finished = true return m, tea.Quit } return m, nil } func (m *InitModel) View() string { var s string switch m.step { case 0: s = headerStyle.Render("╭─── Kite Setup") + "\n" + "\n" + "What's the name of your blog?" + "\n" + "(e.g. my-tech-blog, dev-diary)" + "\n\n" + inputStyle.Render(m.inputBuffer+"_") + "\n\n" + helpStyle.Render("type to enter · enter to continue · esc to cancel") case 1: s = headerStyle.Render("╭─── Kite Setup") + "\n" + "\n" + "Site title (for the header):" + "\n\n" + inputStyle.Render(m.inputBuffer+"_") + "\n\n" + helpStyle.Render("type to enter · enter to continue · esc to cancel") case 2: s = headerStyle.Render("╭─── Kite Setup") + "\n" + "\n" + "Your name:" + "\n\n" + inputStyle.Render(m.inputBuffer+"_") + "\n\n" + helpStyle.Render("type to enter · enter to continue · esc to cancel") case 3: s = headerStyle.Render("╭─── Kite Setup") + "\n" + "\n" + "Your role (e.g. Developer, Writer):" + "\n\n" + inputStyle.Render(m.inputBuffer+"_") + "\n\n" + helpStyle.Render("type to enter · enter to continue · esc to cancel") case 4: s = headerStyle.Render("╭─── Kite Setup") + "\n" + "\n" + "Short bio:" + "\n\n" + inputStyle.Render(m.inputBuffer+"_") + "\n\n" + helpStyle.Render("type to enter · enter to continue · esc to cancel") case 5: s = headerStyle.Render("╭─── Kite Setup") + "\n\n" + m.themeList.View() + "\n" + helpStyle.Render("↑↓ to select · enter to confirm") default: s = "Setting up your blog..." } return s } func copyDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } dstPath := filepath.Join(dst, rel) if info.IsDir() { return os.MkdirAll(dstPath, 0o755) } data, err := os.ReadFile(path) if err != nil { return err } return os.WriteFile(dstPath, data, info.Mode()) }) } func downloadThemes() error { themesURL := "https://github.com/HimanshuSardana/kite/archive/refs/heads/main.zip" fmt.Println(" Downloading themes from GitHub...") cmd := exec.Command("curl", "-sL", themesURL, "-o", "kite-main.zip") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to download: %w", err) } cmd = exec.Command("unzip", "-qo", "-d", ".", "kite-main.zip") if err := cmd.Run(); err != nil { os.Remove("kite-main.zip") return fmt.Errorf("failed to extract: %w", err) } if err := os.MkdirAll("themes", 0o755); err != nil { return err } cmd = exec.Command("cp", "-r", "kite-main/themes/.", "themes/") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to copy themes: %w", err) } os.Remove("kite-main.zip") os.RemoveAll("kite-main") fmt.Println(" ✓ Themes downloaded successfully") return nil } func RunInit() error { m := &InitModel{} p := tea.NewProgram(m, tea.WithAltScreen()) if _, err := p.Run(); err != nil { return err } if m.quitting { fmt.Println("\nInit cancelled.") return nil } if !m.finished { fmt.Println("\nInit not complete.") return nil } fmt.Println("\n" + headerStyle.Render("Setting up your blog...")) theme := m.theme if theme == "" { theme = "modern-light" } dirs := []string{"content", "output", "themes"} for _, dir := range dirs { if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("creating %s directory: %w", dir, err) } } if _, err := os.Stat("themes/modern-light/layout.html"); err == nil { fmt.Println(" ✓ Found themes in current directory") } else if entries, err := os.ReadDir("../themes"); err == nil && len(entries) > 0 { fmt.Println(" ✓ Copying themes from parent directory...") for _, e := range entries { if e.IsDir() { src := filepath.Join("../themes", e.Name()) dst := filepath.Join("themes", e.Name()) if err := copyDir(src, dst); err != nil { fmt.Printf(" ⚠ Could not copy theme %s: %v\n", e.Name(), err) } } } fmt.Println(" ✓ Themes copied successfully") } else { fmt.Println(" ⚠ No themes found - downloading from GitHub...") if err := downloadThemes(); err != nil { return fmt.Errorf("downloading themes: %w", err) } } siteTitle := m.siteTitle if siteTitle == "" { siteTitle = m.blogName } configContent := fmt.Sprintf(`siteTitle: "%s" authorName: "%s" authorRole: "%s" authorBio: "%s" defaultTheme: "%s" `, siteTitle, m.authorName, m.authorRole, m.authorBio, theme) if err := os.WriteFile("config.yaml", []byte(configContent), 0o644); err != nil { return fmt.Errorf("writing config: %w", err) } sampleContent := "---\n" + "title: Welcome to Kite\n" + "date: 2026-01-01\n" + "tags: [getting-started]\n" + "---\n\n" + "This is your first post! Write your content in Markdown here.\n\n" + "## Getting Started\n\n" + "- Add more posts to the content/ directory\n" + "- Run `kite build` to generate your site\n" + "- Run `kite serve` to preview locally\n\n" + "Enjoy blogging!\n" if err := os.WriteFile("content/1.md", []byte(sampleContent), 0o644); err != nil { return fmt.Errorf("writing sample content: %w", err) } fmt.Println(" ✓ Created config.yaml") fmt.Println(" ✓ Created content/ directory") fmt.Println(" ✓ Created output/ directory") fmt.Println(" ✓ Created themes/ directory") fmt.Println(" ✓ Created sample post (content/1.md)") fmt.Println("\nRun `kite build` to generate your site!") fmt.Println("Run `kite serve` to preview it locally.") return nil }