package tui import ( "fmt" "strings" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "dirmd/internal/config" ) type State int const ( StateForm State = iota StateGenerating StateDone StateError ) type genResultMsg struct { result GenerateResult } type spinnerState struct { frame int } func (s *spinnerState) View() string { frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} return frames[s.frame%len(frames)] } type Model struct { state State form FormModel result GenerateResult spinner spinnerState isDark bool width int height int command string } func New() Model { return Model{ state: StateForm, form: NewForm(), isDark: true, } } func (m Model) Init() tea.Cmd { return textinput.Blink } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.BackgroundColorMsg: m.isDark = msg.IsDark() m.form.SetDark(m.isDark) return m, nil case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height m.form.SetSize(msg.Width, msg.Height) return m, nil case tea.KeyPressMsg: if isQuit(msg) && m.state != StateGenerating { return m, tea.Quit } switch m.state { case StateForm: if isSubmit(msg) && m.form.focusIndex == fieldGenerateBtn { cfg, err := m.form.ToConfig() if err != nil { m.form.SetError(err.Error()) return m, nil } m.command = buildCommand(cfg) m.state = StateGenerating return m, func() tea.Msg { return genResultMsg{result: Generate(cfg)} } } if isSubmit(msg) && m.form.focusIndex == fieldCancelBtn { return m, tea.Quit } var cmd tea.Cmd m.form, cmd = m.form.Update(msg) return m, cmd case StateDone, StateError: return m, tea.Quit } case genResultMsg: m.result = msg.result if m.result.Error != nil { m.state = StateError } else { m.state = StateDone } return m, nil } if m.state == StateForm { var cmd tea.Cmd m.form, cmd = m.form.Update(msg) return m, cmd } return m, nil } func buildCommand(cfg *config.Config) string { var parts []string parts = append(parts, "dirmd") parts = append(parts, "-o", cfg.OutputPath) if cfg.Force { parts = append(parts, "-f") } if cfg.VerticalSlices { parts = append(parts, "--vertical-slices") } if cfg.SkipFrontend { parts = append(parts, "--skip-frontend") } if cfg.FrontendOnly { parts = append(parts, "--frontend-only") } if cfg.OmitTest { parts = append(parts, "--omit-test") } if cfg.OmitMd { parts = append(parts, "--omit-md") } if cfg.ProtonDrive { parts = append(parts, "--proton-drive") if cfg.DrivePath != "" { parts = append(parts, "--drive-path", cfg.DrivePath) } } if cfg.MaxDepth != 20 { parts = append(parts, "--max-depth", fmt.Sprintf("%d", cfg.MaxDepth)) } if cfg.MaxSize != 512*1024 { parts = append(parts, "--max-size", fmt.Sprintf("%d", cfg.MaxSize)) } extraIgnores := extraIgnores(cfg) for _, ig := range extraIgnores { parts = append(parts, "--ignore", ig) } parts = append(parts, cfg.InputPath) return strings.Join(parts, " ") } func extraIgnores(cfg *config.Config) []string { defaultSet := make(map[string]bool) for _, d := range config.DefaultIgnores { defaultSet[d] = true } var extra []string for _, ig := range cfg.Ignores { if !defaultSet[ig] { extra = append(extra, ig) } } return extra } func (m Model) View() tea.View { var content string switch m.state { case StateForm: content = m.form.View() case StateGenerating: content = m.renderGenerating() case StateDone: content = m.renderDone() case StateError: content = m.renderError() } v := tea.NewView(content) v.AltScreen = true return v } func (m Model) renderGenerating() string { var b strings.Builder b.WriteString(titleStyle.Render("dirmd")) b.WriteString("\n\n") b.WriteString(fmt.Sprintf(" %s Generating...\n", m.spinner.View())) b.WriteString("\n") b.WriteString(footerStyle.Render("Please wait.")) content := appBorder.Render(b.String()) if m.width > 0 && m.height > 0 { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content) } return content } func (m Model) renderDone() string { var b strings.Builder b.WriteString(titleStyle.Render("dirmd")) b.WriteString("\n\n") b.WriteString(successStyle.Render("✓ Done")) b.WriteString("\n\n") for _, f := range m.result.Files { b.WriteString(fmt.Sprintf(" %s\n", f)) } if m.result.Uploaded { b.WriteString(successStyle.Render("\n Uploaded to Proton Drive")) } b.WriteString("\n\n") b.WriteString(footerStyle.Render("Press any key to exit.")) content := appBorder.Render(b.String()) if m.width > 0 && m.height > 0 { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content) } return content } func (m Model) renderError() string { var b strings.Builder b.WriteString(titleStyle.Render("dirmd")) b.WriteString("\n\n") b.WriteString(errorStyle.Render("✗ Error")) b.WriteString("\n\n") b.WriteString(fmt.Sprintf(" %s\n", m.result.Error.Error())) b.WriteString("\n") b.WriteString(footerStyle.Render("Press any key to exit.")) content := appBorder.Render(b.String()) if m.width > 0 && m.height > 0 { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content) } return content }