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	keybindWarnings []string
 43}
 44
 45func NewChoice() Choice {
 46	hasSavedDrafts := config.HasDrafts()
 47	choices := []string{
 48		"\ueb1c " + t("choice.inbox"),
 49		"\ueb1b " + t("choice.compose"),
 50	}
 51	if hasSavedDrafts {
 52		choices = append(choices, "\uec0e "+t("choice.drafts"))
 53	}
 54	choices = append(choices, "\uf487 "+t("choice.marketplace"))
 55	choices = append(choices, "\uf013 "+t("choice.settings"))
 56	return Choice{
 57		choices:         choices,
 58		hasSavedDrafts:  hasSavedDrafts,
 59		UpdateAvailable: false,
 60		LatestVersion:   "",
 61		CurrentVersion:  "",
 62		keybindWarnings: config.ValidateKeybinds(config.Keybinds),
 63	}
 64}
 65
 66func (m Choice) Init() tea.Cmd {
 67	return nil
 68}
 69
 70func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 71	switch msg := msg.(type) {
 72	case tea.WindowSizeMsg:
 73		m.width = msg.Width
 74		m.height = msg.Height
 75		return m, nil
 76	case tea.KeyPressMsg:
 77		kb := config.Keybinds
 78		switch msg.String() {
 79		case "up", kb.Global.NavUp:
 80			m.cursor = (m.cursor - 1 + len(m.choices)) % len(m.choices)
 81		case keyDown, kb.Global.NavDown:
 82			m.cursor = (m.cursor + 1) % len(m.choices)
 83		case keyEnter:
 84			// Use cursor index instead of string comparison
 85			idx := m.cursor
 86			if idx == 0 { //nolint:gocritic
 87				// Inbox
 88				return m, func() tea.Msg { return GoToInboxMsg{} }
 89			} else if idx == 1 {
 90				// Compose
 91				return m, func() tea.Msg { return GoToSendMsg{} }
 92			} else if m.hasSavedDrafts && idx == 2 {
 93				// Drafts
 94				return m, func() tea.Msg { return GoToDraftsMsg{} }
 95			} else if (m.hasSavedDrafts && idx == 3) || (!m.hasSavedDrafts && idx == 2) {
 96				// Marketplace
 97				return m, func() tea.Msg { return GoToMarketplaceMsg{} }
 98			} else if (m.hasSavedDrafts && idx == 4) || (!m.hasSavedDrafts && idx == 3) {
 99				// Settings
100				return m, func() tea.Msg { return GoToSettingsMsg{} }
101			}
102		}
103	}
104
105	// Handle update notification from other package without importing its type directly.
106	// We look for a struct named 'UpdateAvailableMsg' that contains 'Latest' and 'Current' string fields.
107	rv := reflect.ValueOf(msg)
108	if rv.IsValid() && rv.Kind() == reflect.Struct && rv.Type().Name() == "UpdateAvailableMsg" {
109		f := rv.FieldByName("Latest")
110		c := rv.FieldByName("Current")
111		updated := false
112		if f.IsValid() && f.Kind() == reflect.String {
113			m.LatestVersion = f.String()
114			updated = true
115		}
116		if c.IsValid() && c.Kind() == reflect.String {
117			m.CurrentVersion = c.String()
118			updated = true
119		}
120		if updated {
121			m.UpdateAvailable = true
122			return m, nil
123		}
124	}
125
126	return m, nil
127}
128
129func (m Choice) View() tea.View {
130	var b strings.Builder
131
132	b.WriteString(logoStyle.Render(choiceLogo))
133	b.WriteString("\n")
134
135	if len(m.keybindWarnings) > 0 {
136		warnStyle := lipgloss.NewStyle().Foreground(theme.ActiveTheme.Warning).Padding(0, 1)
137		for _, w := range m.keybindWarnings {
138			b.WriteString(warnStyle.Render("⚠ keybind " + w))
139			b.WriteString("\n")
140		}
141		b.WriteString("\n")
142	}
143
144	b.WriteString(listHeader.Render(t("choice.what_to_do")))
145	b.WriteString("\n\n")
146
147	// If we detected an update, show a short message under the header.
148	if m.UpdateAvailable {
149		updateStyle := lipgloss.NewStyle().Foreground(theme.ActiveTheme.Warning).Padding(0, 1)
150		cur := m.CurrentVersion
151		if cur == "" {
152			cur = t("choice.unknown")
153		}
154		msg := tpl("choice.update_available", map[string]interface{}{
155			"latest":  m.LatestVersion,
156			"current": cur,
157		})
158		b.WriteString(updateStyle.Render(msg))
159		b.WriteString("\n\n")
160	}
161
162	for i, choice := range m.choices {
163		if m.cursor == i {
164			b.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", choice)))
165		} else {
166			b.WriteString(itemStyle.Render(fmt.Sprintf("  %s", choice)))
167		}
168		b.WriteString("\n")
169	}
170
171	mainContent := b.String()
172	helpView := helpStyle.Render(t("choice.help"))
173
174	if m.height > 0 {
175		currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
176		gap := m.height - currentHeight
177		if gap > 0 {
178			mainContent += strings.Repeat("\n", gap)
179		}
180	} else {
181		mainContent += "\n\n"
182	}
183
184	return tea.NewView(docStyle.Render(mainContent + helpView))
185}