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}