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