choice.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"reflect"
  6	"regexp"
  7	"strings"
  8
  9	tea "charm.land/bubbletea/v2"
 10	"charm.land/lipgloss/v2"
 11	"github.com/floatpane/matcha/config"
 12	"github.com/floatpane/matcha/theme"
 13)
 14
 15// Styles defined locally to avoid import issues.
 16var (
 17	docStyle          = lipgloss.NewStyle().Margin(1, 2)
 18	titleStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFDF5")).Background(lipgloss.Color("#25A065")).Padding(0, 1)
 19	logoStyle         = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
 20	listHeader        = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).PaddingBottom(1)
 21	itemStyle         = lipgloss.NewStyle().PaddingLeft(2)
 22	selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42"))
 23)
 24
 25// ASCII logo for the start screen
 26const choiceLogo = `
 27                    __       __
 28   ____ ___  ____ _/ /______/ /_  ____ _
 29  / __ '__ \/ __ '/ __/ ___/ __ \/ __ '/
 30 / / / / / / /_/ / /_/ /__/ / / / /_/ /
 31/_/ /_/ /_/\__,_/\__/\___/_/ /_/\__,_/
 32`
 33
 34type Choice struct {
 35	cursor          int
 36	choices         []string
 37	hasSavedDrafts  bool
 38	UpdateAvailable bool
 39	LatestVersion   string
 40	CurrentVersion  string
 41	V1RCAvailable   bool
 42	V1RCVersion     string
 43	width           int
 44	height          int
 45	keybindWarnings []string
 46}
 47
 48func NewChoice() Choice {
 49	hasSavedDrafts := config.HasDrafts()
 50	choices := []string{
 51		"\ueb1c " + t("choice.inbox"),
 52		"\ueb1b " + t("choice.compose"),
 53	}
 54	if hasSavedDrafts {
 55		choices = append(choices, "\uec0e "+t("choice.drafts"))
 56	}
 57	choices = append(choices, "\uf487 "+t("choice.marketplace"))
 58	choices = append(choices, "\uf013 "+t("choice.settings"))
 59	return Choice{
 60		choices:         choices,
 61		hasSavedDrafts:  hasSavedDrafts,
 62		UpdateAvailable: false,
 63		LatestVersion:   "",
 64		CurrentVersion:  "",
 65		V1RCAvailable:   false,
 66		V1RCVersion:     "",
 67		keybindWarnings: config.ValidateKeybinds(config.Keybinds),
 68	}
 69}
 70
 71func (m Choice) Init() tea.Cmd {
 72	return nil
 73}
 74
 75func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 76	switch msg := msg.(type) {
 77	case tea.WindowSizeMsg:
 78		m.width = msg.Width
 79		m.height = msg.Height
 80		return m, nil
 81	case tea.KeyPressMsg:
 82		return m, m.handleKeyPress(msg)
 83	}
 84
 85	if m.handleUpdateAvailableMsg(msg) {
 86		return m, nil
 87	}
 88	if m.handleV1RCAvailableMsg(msg) {
 89		return m, nil
 90	}
 91
 92	return m, nil
 93}
 94
 95func (m *Choice) handleKeyPress(msg tea.KeyPressMsg) tea.Cmd {
 96	kb := config.Keybinds
 97	switch msg.String() {
 98	case "up", kb.Global.NavUp:
 99		m.cursor = (m.cursor - 1 + len(m.choices)) % len(m.choices)
100	case keyDown, kb.Global.NavDown:
101		m.cursor = (m.cursor + 1) % len(m.choices)
102	case keyEnter:
103		return m.navCmd()
104	}
105	return nil
106}
107
108func (m *Choice) navCmd() tea.Cmd {
109	idx := m.cursor
110	if !m.hasSavedDrafts && idx >= 2 {
111		idx++
112	}
113	switch idx {
114	case 0:
115		return func() tea.Msg { return GoToInboxMsg{} }
116	case 1:
117		return func() tea.Msg { return GoToSendMsg{} }
118	case 2:
119		return func() tea.Msg { return GoToDraftsMsg{} }
120	case 3:
121		return func() tea.Msg { return GoToMarketplaceMsg{} }
122	case 4:
123		return func() tea.Msg { return GoToSettingsMsg{} }
124	}
125	return nil
126}
127
128func (m *Choice) handleUpdateAvailableMsg(msg tea.Msg) bool {
129	rv := reflect.ValueOf(msg)
130	if !rv.IsValid() || rv.Kind() != reflect.Struct || rv.Type().Name() != "UpdateAvailableMsg" {
131		return false
132	}
133	updated := false
134	if f := rv.FieldByName("Latest"); f.IsValid() && f.Kind() == reflect.String {
135		m.LatestVersion = f.String()
136		updated = true
137	}
138	if c := rv.FieldByName("Current"); c.IsValid() && c.Kind() == reflect.String {
139		m.CurrentVersion = c.String()
140		updated = true
141	}
142	if updated {
143		m.UpdateAvailable = true
144	}
145	return updated
146}
147
148func (m *Choice) handleV1RCAvailableMsg(msg tea.Msg) bool {
149	rv := reflect.ValueOf(msg)
150	if !rv.IsValid() || rv.Kind() != reflect.Struct || rv.Type().Name() != "V1RCAvailableMsg" {
151		return false
152	}
153	f := rv.FieldByName("Latest")
154	if !f.IsValid() || f.Kind() != reflect.String || !v1RCRegex.MatchString(f.String()) {
155		return false
156	}
157	m.V1RCVersion = f.String()
158	m.V1RCAvailable = true
159	if c := rv.FieldByName("Current"); c.IsValid() && c.Kind() == reflect.String {
160		m.CurrentVersion = c.String()
161	}
162	return true
163}
164
165var (
166	v0Regex   = regexp.MustCompile(`^v?0\.\d+\.\d+$`)
167	v1RCRegex = regexp.MustCompile(`^v?1\.0\.0-rc\d+$`)
168)
169
170func (m Choice) isV0() bool {
171	return v0Regex.MatchString(m.CurrentVersion)
172}
173
174func (m Choice) isV1RCAvailable() bool {
175	return m.V1RCAvailable && m.isV0() && v1RCRegex.MatchString(m.V1RCVersion)
176}
177
178func (m Choice) View() tea.View {
179	var b strings.Builder
180
181	b.WriteString(logoStyle.Render(choiceLogo))
182	b.WriteString("\n")
183
184	if len(m.keybindWarnings) > 0 {
185		warnStyle := lipgloss.NewStyle().Foreground(theme.ActiveTheme.Warning).Padding(0, 1)
186		for _, w := range m.keybindWarnings {
187			b.WriteString(warnStyle.Render("⚠ keybind " + w))
188			b.WriteString("\n")
189		}
190		b.WriteString("\n")
191	}
192
193	b.WriteString(listHeader.Render(t("choice.what_to_do")))
194	b.WriteString("\n\n")
195
196	// If we detected an update, show a short message under the header.
197	if m.UpdateAvailable {
198		updateStyle := lipgloss.NewStyle().Foreground(theme.ActiveTheme.Warning).Padding(0, 1)
199		cur := m.CurrentVersion
200		if cur == "" {
201			cur = t("choice.unknown")
202		}
203		msg := tpl("choice.update_available", map[string]interface{}{
204			"latest":  m.LatestVersion,
205			"current": cur,
206		})
207		b.WriteString(updateStyle.Render(msg))
208		b.WriteString("\n\n")
209	}
210
211	for i, choice := range m.choices {
212		if m.cursor == i {
213			b.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", choice)))
214		} else {
215			b.WriteString(itemStyle.Render(fmt.Sprintf("  %s", choice)))
216		}
217		b.WriteString("\n")
218	}
219
220	mainContent := b.String()
221	helpView := helpStyle.Render(t("choice.help"))
222
223	if m.height > 0 {
224		currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
225		gap := m.height - currentHeight
226		if gap > 0 {
227			mainContent += strings.Repeat("\n", gap)
228		}
229	} else {
230		mainContent += "\n\n"
231	}
232
233	content := mainContent + helpView
234	if m.isV1RCAvailable() {
235		noteStyle := lipgloss.NewStyle().Foreground(theme.ActiveTheme.Warning).Padding(0, 1)
236		content += "\n" + noteStyle.Render(t("choice.upgrade_v1_note"))
237	}
238
239	return tea.NewView(docStyle.Render(content))
240}