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