choice.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"reflect"
  6	"strings"
  7
  8	tea "charm.land/bubbletea/v2"
  9	"charm.land/lipgloss/v2"
 10	"github.com/floatpane/matcha/config"
 11)
 12
 13// Styles defined locally to avoid import issues.
 14var (
 15	docStyle          = lipgloss.NewStyle().Margin(1, 2)
 16	titleStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFDF5")).Background(lipgloss.Color("#25A065")).Padding(0, 1)
 17	logoStyle         = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
 18	listHeader        = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).PaddingBottom(1)
 19	itemStyle         = lipgloss.NewStyle().PaddingLeft(2)
 20	selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42"))
 21)
 22
 23// ASCII logo for the start screen
 24const choiceLogo = `
 25                    __       __
 26   ____ ___  ____ _/ /______/ /_  ____ _
 27  / __ '__ \/ __ '/ __/ ___/ __ \/ __ '/
 28 / / / / / / /_/ / /_/ /__/ / / / /_/ /
 29/_/ /_/ /_/\__,_/\__/\___/_/ /_/\__,_/
 30`
 31
 32type Choice struct {
 33	cursor          int
 34	choices         []string
 35	hasSavedDrafts  bool
 36	UpdateAvailable bool
 37	LatestVersion   string
 38	CurrentVersion  string
 39	width           int
 40	height          int
 41}
 42
 43func NewChoice() Choice {
 44	hasSavedDrafts := config.HasDrafts()
 45	choices := []string{"\ueb1c Inbox", "\ueb1b Compose Email"}
 46	if hasSavedDrafts {
 47		choices = append(choices, "\uec0e Drafts")
 48	}
 49	choices = append(choices, "\uf013 Settings")
 50	return Choice{
 51		choices:         choices,
 52		hasSavedDrafts:  hasSavedDrafts,
 53		UpdateAvailable: false,
 54		LatestVersion:   "",
 55		CurrentVersion:  "",
 56	}
 57
 58}
 59
 60func (m Choice) Init() tea.Cmd {
 61	return nil
 62}
 63
 64func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 65	switch msg := msg.(type) {
 66	case tea.WindowSizeMsg:
 67		m.width = msg.Width
 68		m.height = msg.Height
 69		return m, nil
 70	case tea.KeyPressMsg:
 71		switch msg.String() {
 72		case "up", "k":
 73			if m.cursor > 0 {
 74				m.cursor--
 75			}
 76		case "down", "j":
 77			if m.cursor < len(m.choices)-1 {
 78				m.cursor++
 79			}
 80		case "enter":
 81			selectedChoice := m.choices[m.cursor]
 82			switch selectedChoice {
 83			case "\ueb1c Inbox":
 84				return m, func() tea.Msg { return GoToInboxMsg{} }
 85			case "\ueb1b Compose Email":
 86				return m, func() tea.Msg { return GoToSendMsg{} }
 87			case "\uec0e Drafts":
 88				return m, func() tea.Msg { return GoToDraftsMsg{} }
 89			case "\uf013 Settings":
 90				return m, func() tea.Msg { return GoToSettingsMsg{} }
 91			}
 92
 93		}
 94	}
 95
 96	// Handle update notification from other package without importing its type directly.
 97	// We look for a struct named 'UpdateAvailableMsg' that contains 'Latest' and 'Current' string fields.
 98	rv := reflect.ValueOf(msg)
 99	if rv.IsValid() && rv.Kind() == reflect.Struct && rv.Type().Name() == "UpdateAvailableMsg" {
100		f := rv.FieldByName("Latest")
101		c := rv.FieldByName("Current")
102		updated := false
103		if f.IsValid() && f.Kind() == reflect.String {
104			m.LatestVersion = f.String()
105			updated = true
106		}
107		if c.IsValid() && c.Kind() == reflect.String {
108			m.CurrentVersion = c.String()
109			updated = true
110		}
111		if updated {
112			m.UpdateAvailable = true
113			return m, nil
114		}
115	}
116
117	return m, nil
118}
119
120func (m Choice) View() tea.View {
121	var b strings.Builder
122
123	b.WriteString(logoStyle.Render(choiceLogo))
124	b.WriteString("\n")
125	b.WriteString(listHeader.Render("What would you like to do?"))
126	b.WriteString("\n\n")
127
128	// If we detected an update, show a short message under the header.
129	if m.UpdateAvailable {
130		updateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Padding(0, 1)
131		cur := m.CurrentVersion
132		if cur == "" {
133			cur = "unknown"
134		}
135		msg := fmt.Sprintf("Update available: %s (installed: %s) — run `matcha update` to upgrade", m.LatestVersion, cur)
136		b.WriteString(updateStyle.Render(msg))
137		b.WriteString("\n\n")
138	}
139
140	for i, choice := range m.choices {
141		if m.cursor == i {
142			b.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", choice)))
143		} else {
144			b.WriteString(itemStyle.Render(fmt.Sprintf("  %s", choice)))
145		}
146		b.WriteString("\n")
147	}
148
149	mainContent := b.String()
150	helpView := helpStyle.Render("Use ↑/↓ to navigate, enter to select, and ctrl+c to quit.")
151
152	if m.height > 0 {
153		currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
154		gap := m.height - currentHeight
155		if gap > 0 {
156			mainContent += strings.Repeat("\n", gap)
157		}
158	} else {
159		mainContent += "\n\n"
160	}
161
162	return tea.NewView(docStyle.Render(mainContent + helpView))
163}