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)
12
13// Styles defined locally to avoid import issues.
14var (
15 docStyle = lipgloss.NewStyle().Margin(1, 2)
16 titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFDF5")).Background(lipgloss.Color("#25A065")).Padding(0, 1)
17 logoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
18 listHeader = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).PaddingBottom(1)
19 itemStyle = lipgloss.NewStyle().PaddingLeft(2)
20 selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42"))
21)
22
23// ASCII logo for the start screen
24const choiceLogo = `
25 __ __
26 ____ ___ ____ _/ /______/ /_ ____ _
27 / __ '__ \/ __ '/ __/ ___/ __ \/ __ '/
28 / / / / / / /_/ / /_/ /__/ / / / /_/ /
29/_/ /_/ /_/\__,_/\__/\___/_/ /_/\__,_/
30`
31
32type Choice struct {
33 cursor int
34 choices []string
35 hasSavedDrafts bool
36 UpdateAvailable bool
37 LatestVersion string
38 CurrentVersion string
39 width int
40 height int
41}
42
43func NewChoice() Choice {
44 hasSavedDrafts := config.HasDrafts()
45 choices := []string{"\ueb1c Inbox", "\ueb1b Compose Email", "\uf1d8 Sent"}
46 if hasSavedDrafts {
47 choices = append(choices, "\uec0e Drafts")
48 }
49 choices = append(choices, "\uf013 Settings", "\uea81 Trash & Archive")
50 return Choice{
51 choices: choices,
52 hasSavedDrafts: hasSavedDrafts,
53 UpdateAvailable: false,
54 LatestVersion: "",
55 CurrentVersion: "",
56 }
57
58}
59
60func (m Choice) Init() tea.Cmd {
61 return nil
62}
63
64func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
65 switch msg := msg.(type) {
66 case tea.WindowSizeMsg:
67 m.width = msg.Width
68 m.height = msg.Height
69 return m, nil
70 case tea.KeyPressMsg:
71 switch msg.String() {
72 case "up", "k":
73 if m.cursor > 0 {
74 m.cursor--
75 }
76 case "down", "j":
77 if m.cursor < len(m.choices)-1 {
78 m.cursor++
79 }
80 case "enter":
81 selectedChoice := m.choices[m.cursor]
82 switch selectedChoice {
83 case "\ueb1c Inbox":
84 return m, func() tea.Msg { return GoToInboxMsg{} }
85 case "\ueb1b Compose Email":
86 return m, func() tea.Msg { return GoToSendMsg{} }
87 case "\uf1d8 Sent":
88 return m, func() tea.Msg { return GoToSentInboxMsg{} }
89 case "\uec0e Drafts":
90 return m, func() tea.Msg { return GoToDraftsMsg{} }
91 case "\uf013 Settings":
92 return m, func() tea.Msg { return GoToSettingsMsg{} }
93 case "\uea81 Trash & Archive":
94 return m, func() tea.Msg { return GoToTrashArchiveMsg{} }
95 }
96
97 }
98 }
99
100 // Handle update notification from other package without importing its type directly.
101 // We look for a struct named 'UpdateAvailableMsg' that contains 'Latest' and 'Current' string fields.
102 rv := reflect.ValueOf(msg)
103 if rv.IsValid() && rv.Kind() == reflect.Struct && rv.Type().Name() == "UpdateAvailableMsg" {
104 f := rv.FieldByName("Latest")
105 c := rv.FieldByName("Current")
106 updated := false
107 if f.IsValid() && f.Kind() == reflect.String {
108 m.LatestVersion = f.String()
109 updated = true
110 }
111 if c.IsValid() && c.Kind() == reflect.String {
112 m.CurrentVersion = c.String()
113 updated = true
114 }
115 if updated {
116 m.UpdateAvailable = true
117 return m, nil
118 }
119 }
120
121 return m, nil
122}
123
124func (m Choice) View() tea.View {
125 var b strings.Builder
126
127 b.WriteString(logoStyle.Render(choiceLogo))
128 b.WriteString("\n")
129 b.WriteString(listHeader.Render("What would you like to do?"))
130 b.WriteString("\n\n")
131
132 // If we detected an update, show a short message under the header.
133 if m.UpdateAvailable {
134 updateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Padding(0, 1)
135 cur := m.CurrentVersion
136 if cur == "" {
137 cur = "unknown"
138 }
139 msg := fmt.Sprintf("Update available: %s (installed: %s) — run `matcha update` to upgrade", m.LatestVersion, cur)
140 b.WriteString(updateStyle.Render(msg))
141 b.WriteString("\n\n")
142 }
143
144 for i, choice := range m.choices {
145 if m.cursor == i {
146 b.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", choice)))
147 } else {
148 b.WriteString(itemStyle.Render(fmt.Sprintf(" %s", choice)))
149 }
150 b.WriteString("\n")
151 }
152
153 mainContent := b.String()
154 helpView := helpStyle.Render("Use ↑/↓ to navigate, enter to select, and ctrl+c to quit.")
155
156 if m.height > 0 {
157 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
158 gap := m.height - currentHeight
159 if gap > 0 {
160 mainContent += strings.Repeat("\n", gap)
161 }
162 } else {
163 mainContent += "\n\n"
164 }
165
166 return tea.NewView(docStyle.Render(mainContent + helpView))
167}