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"}
46 if hasSavedDrafts {
47 choices = append(choices, "\uec0e Drafts")
48 }
49 choices = append(choices, "\uf013 Settings")
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 "\uec0e Drafts":
88 return m, func() tea.Msg { return GoToDraftsMsg{} }
89 case "\uf013 Settings":
90 return m, func() tea.Msg { return GoToSettingsMsg{} }
91 }
92
93 }
94 }
95
96 // Handle update notification from other package without importing its type directly.
97 // We look for a struct named 'UpdateAvailableMsg' that contains 'Latest' and 'Current' string fields.
98 rv := reflect.ValueOf(msg)
99 if rv.IsValid() && rv.Kind() == reflect.Struct && rv.Type().Name() == "UpdateAvailableMsg" {
100 f := rv.FieldByName("Latest")
101 c := rv.FieldByName("Current")
102 updated := false
103 if f.IsValid() && f.Kind() == reflect.String {
104 m.LatestVersion = f.String()
105 updated = true
106 }
107 if c.IsValid() && c.Kind() == reflect.String {
108 m.CurrentVersion = c.String()
109 updated = true
110 }
111 if updated {
112 m.UpdateAvailable = true
113 return m, nil
114 }
115 }
116
117 return m, nil
118}
119
120func (m Choice) View() tea.View {
121 var b strings.Builder
122
123 b.WriteString(logoStyle.Render(choiceLogo))
124 b.WriteString("\n")
125 b.WriteString(listHeader.Render("What would you like to do?"))
126 b.WriteString("\n\n")
127
128 // If we detected an update, show a short message under the header.
129 if m.UpdateAvailable {
130 updateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Padding(0, 1)
131 cur := m.CurrentVersion
132 if cur == "" {
133 cur = "unknown"
134 }
135 msg := fmt.Sprintf("Update available: %s (installed: %s) — run `matcha update` to upgrade", m.LatestVersion, cur)
136 b.WriteString(updateStyle.Render(msg))
137 b.WriteString("\n\n")
138 }
139
140 for i, choice := range m.choices {
141 if m.cursor == i {
142 b.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", choice)))
143 } else {
144 b.WriteString(itemStyle.Render(fmt.Sprintf(" %s", choice)))
145 }
146 b.WriteString("\n")
147 }
148
149 mainContent := b.String()
150 helpView := helpStyle.Render("Use ↑/↓ to navigate, enter to select, and ctrl+c to quit.")
151
152 if m.height > 0 {
153 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
154 gap := m.height - currentHeight
155 if gap > 0 {
156 mainContent += strings.Repeat("\n", gap)
157 }
158 } else {
159 mainContent += "\n\n"
160 }
161
162 return tea.NewView(docStyle.Render(mainContent + helpView))
163}