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}