Files
dirmd/internal/tui/model.go
T
2026-08-27 19:07:53 +01:00

254 lines
5.3 KiB
Go

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
}