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 if m.cursor > 0 {
81 m.cursor--
82 }
83 case "down", kb.Global.NavDown:
84 if m.cursor < len(m.choices)-1 {
85 m.cursor++
86 }
87 case "enter":
88 // Use cursor index instead of string comparison
89 idx := m.cursor
90 if idx == 0 {
91 // Inbox
92 return m, func() tea.Msg { return GoToInboxMsg{} }
93 } else if idx == 1 {
94 // Compose
95 return m, func() tea.Msg { return GoToSendMsg{} }
96 } else if m.hasSavedDrafts && idx == 2 {
97 // Drafts
98 return m, func() tea.Msg { return GoToDraftsMsg{} }
99 } else if (m.hasSavedDrafts && idx == 3) || (!m.hasSavedDrafts && idx == 2) {
100 // Marketplace
101 return m, func() tea.Msg { return GoToMarketplaceMsg{} }
102 } else if (m.hasSavedDrafts && idx == 4) || (!m.hasSavedDrafts && idx == 3) {
103 // Settings
104 return m, func() tea.Msg { return GoToSettingsMsg{} }
105 }
106
107 }
108 }
109
110 // Handle update notification from other package without importing its type directly.
111 // We look for a struct named 'UpdateAvailableMsg' that contains 'Latest' and 'Current' string fields.
112 rv := reflect.ValueOf(msg)
113 if rv.IsValid() && rv.Kind() == reflect.Struct && rv.Type().Name() == "UpdateAvailableMsg" {
114 f := rv.FieldByName("Latest")
115 c := rv.FieldByName("Current")
116 updated := false
117 if f.IsValid() && f.Kind() == reflect.String {
118 m.LatestVersion = f.String()
119 updated = true
120 }
121 if c.IsValid() && c.Kind() == reflect.String {
122 m.CurrentVersion = c.String()
123 updated = true
124 }
125 if updated {
126 m.UpdateAvailable = true
127 return m, nil
128 }
129 }
130
131 return m, nil
132}
133
134func (m Choice) View() tea.View {
135 var b strings.Builder
136
137 b.WriteString(logoStyle.Render(choiceLogo))
138 b.WriteString("\n")
139
140 if len(m.keybindWarnings) > 0 {
141 warnStyle := lipgloss.NewStyle().Foreground(theme.ActiveTheme.Warning).Padding(0, 1)
142 for _, w := range m.keybindWarnings {
143 b.WriteString(warnStyle.Render("⚠ keybind " + w))
144 b.WriteString("\n")
145 }
146 b.WriteString("\n")
147 }
148
149 b.WriteString(listHeader.Render(t("choice.what_to_do")))
150 b.WriteString("\n\n")
151
152 // If we detected an update, show a short message under the header.
153 if m.UpdateAvailable {
154 updateStyle := lipgloss.NewStyle().Foreground(theme.ActiveTheme.Warning).Padding(0, 1)
155 cur := m.CurrentVersion
156 if cur == "" {
157 cur = t("choice.unknown")
158 }
159 msg := tpl("choice.update_available", map[string]interface{}{
160 "latest": m.LatestVersion,
161 "current": cur,
162 })
163 b.WriteString(updateStyle.Render(msg))
164 b.WriteString("\n\n")
165 }
166
167 for i, choice := range m.choices {
168 if m.cursor == i {
169 b.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", choice)))
170 } else {
171 b.WriteString(itemStyle.Render(fmt.Sprintf(" %s", choice)))
172 }
173 b.WriteString("\n")
174 }
175
176 mainContent := b.String()
177 helpView := helpStyle.Render(t("choice.help"))
178
179 if m.height > 0 {
180 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
181 gap := m.height - currentHeight
182 if gap > 0 {
183 mainContent += strings.Repeat("\n", gap)
184 }
185 } else {
186 mainContent += "\n\n"
187 }
188
189 return tea.NewView(docStyle.Render(mainContent + helpView))
190}