1package tui
2
3import (
4 "fmt"
5 "reflect"
6 "strings"
7
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
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}
40
41func NewChoice() Choice {
42 hasSavedDrafts := config.HasDrafts()
43 choices := []string{"View Inbox", "Compose Email"}
44 if hasSavedDrafts {
45 choices = append(choices, "Drafts")
46 }
47 choices = append(choices, "Settings")
48 return Choice{
49 choices: choices,
50 hasSavedDrafts: hasSavedDrafts,
51 UpdateAvailable: false,
52 LatestVersion: "",
53 CurrentVersion: "",
54 }
55}
56
57func (m Choice) Init() tea.Cmd {
58 return nil
59}
60
61func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
62 switch msg := msg.(type) {
63 case tea.KeyMsg:
64 switch msg.String() {
65 case "up", "k":
66 if m.cursor > 0 {
67 m.cursor--
68 }
69 case "down", "j":
70 if m.cursor < len(m.choices)-1 {
71 m.cursor++
72 }
73 case "enter":
74 selectedChoice := m.choices[m.cursor]
75 switch selectedChoice {
76 case "View Inbox":
77 return m, func() tea.Msg { return GoToInboxMsg{} }
78 case "Compose Email":
79 return m, func() tea.Msg { return GoToSendMsg{} }
80 case "Drafts":
81 return m, func() tea.Msg { return GoToDraftsMsg{} }
82 case "Settings":
83 return m, func() tea.Msg { return GoToSettingsMsg{} }
84 }
85 }
86 }
87
88 // Handle update notification from other package without importing its type directly.
89 // We look for a struct named 'UpdateAvailableMsg' that contains 'Latest' and 'Current' string fields.
90 rv := reflect.ValueOf(msg)
91 if rv.IsValid() && rv.Kind() == reflect.Struct && rv.Type().Name() == "UpdateAvailableMsg" {
92 f := rv.FieldByName("Latest")
93 c := rv.FieldByName("Current")
94 updated := false
95 if f.IsValid() && f.Kind() == reflect.String {
96 m.LatestVersion = f.String()
97 updated = true
98 }
99 if c.IsValid() && c.Kind() == reflect.String {
100 m.CurrentVersion = c.String()
101 updated = true
102 }
103 if updated {
104 m.UpdateAvailable = true
105 return m, nil
106 }
107 }
108
109 return m, nil
110}
111
112func (m Choice) View() string {
113 var b strings.Builder
114
115 b.WriteString(logoStyle.Render(choiceLogo))
116 b.WriteString("\n")
117 b.WriteString(listHeader.Render("What would you like to do?"))
118 b.WriteString("\n\n")
119
120 // If we detected an update, show a short message under the header.
121 if m.UpdateAvailable {
122 updateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Padding(0, 1)
123 cur := m.CurrentVersion
124 if cur == "" {
125 cur = "unknown"
126 }
127 msg := fmt.Sprintf("Update available: %s (installed: %s) — run `matcha update` to upgrade", m.LatestVersion, cur)
128 b.WriteString(updateStyle.Render(msg))
129 b.WriteString("\n\n")
130 }
131
132 for i, choice := range m.choices {
133 if m.cursor == i {
134 b.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", choice)))
135 } else {
136 b.WriteString(itemStyle.Render(fmt.Sprintf(" %s", choice)))
137 }
138 b.WriteString("\n")
139 }
140
141 b.WriteString("\n\n")
142 b.WriteString(helpStyle.Render("Use ↑/↓ to navigate, enter to select, and ctrl+c to quit."))
143
144 return docStyle.Render(b.String())
145}