1package tui
2
3import (
4 "fmt"
5 "strings"
6
7 "charm.land/bubbles/v2/textinput"
8 tea "charm.land/bubbletea/v2"
9 "charm.land/lipgloss/v2"
10 "github.com/floatpane/matcha/backend"
11 "github.com/floatpane/matcha/fetcher"
12 "github.com/floatpane/matcha/theme"
13)
14
15type SearchOverlay struct {
16 input textinput.Model
17 query backend.SearchQuery
18 results []fetcher.Email
19 loading bool
20 done bool
21 err string
22 width int
23}
24
25func NewSearchOverlay(width, height int) *SearchOverlay {
26 ti := textinput.New()
27 ti.Placeholder = "from:alice subject:invoice since:2026-01-01"
28 ti.Prompt = "/ "
29 ti.CharLimit = 256
30 ti.Focus()
31 ti.SetStyles(ThemedTextInputStyles())
32 if width < 44 {
33 width = 44
34 }
35 return &SearchOverlay{input: ti, width: width}
36}
37
38func (o *SearchOverlay) Init() tea.Cmd { return textinput.Blink }
39
40func (o *SearchOverlay) Update(msg tea.Msg, mailbox MailboxKind, accountID string) tea.Cmd {
41 switch msg := msg.(type) {
42 case tea.WindowSizeMsg:
43 o.width = msg.Width
44 return nil
45 case SearchResultsMsg:
46 o.loading, o.done, o.err = false, msg.Err == nil, ""
47 o.query = msg.Query
48 if msg.Err != nil {
49 o.err = msg.Err.Error()
50 return nil
51 }
52 o.results = msg.Emails
53 return nil
54 case tea.KeyPressMsg:
55 switch msg.String() {
56 case "enter":
57 if o.loading {
58 return nil
59 }
60 if o.done {
61 results := append([]fetcher.Email(nil), o.results...)
62 query := o.query
63 return func() tea.Msg { return ApplySearchResultsMsg{Query: query, Emails: results} }
64 }
65 raw := o.input.Value()
66 if raw == "" {
67 return nil
68 }
69 o.loading, o.done, o.err, o.results = true, false, "", nil
70 query := backend.ParseSearchQuery(raw)
71 return func() tea.Msg { return SearchRequestedMsg{Query: query, Mailbox: mailbox, AccountID: accountID} }
72 }
73 }
74
75 var cmd tea.Cmd
76 o.input, cmd = o.input.Update(msg)
77 return cmd
78}
79
80func (o *SearchOverlay) View() string {
81 boxWidth := o.width - 4
82 if boxWidth < 40 {
83 boxWidth = 40
84 }
85 style := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).
86 BorderForeground(theme.ActiveTheme.Accent).Padding(1, 2).Width(boxWidth)
87 content := "Search mail\n\n" + o.input.View()
88 if o.loading {
89 content += "\n\nSearching..."
90 }
91 if o.err != "" {
92 content += "\n\n" + lipgloss.NewStyle().Foreground(theme.ActiveTheme.Danger).Render(o.err)
93 }
94 if o.done {
95 content += fmt.Sprintf("\n\n%d result(s). Press Enter to apply, Esc to dismiss.\n", len(o.results))
96 content += o.resultsView()
97 }
98 return style.Render(content)
99}
100
101func (o *SearchOverlay) resultsView() string {
102 limit := len(o.results)
103 if limit > 10 {
104 limit = 10
105 }
106 var b strings.Builder
107 for i := 0; i < limit; i++ {
108 email := o.results[i]
109 fmt.Fprintf(&b, "%d. %s - %s\n", i+1, parseSenderName(email.From), email.Subject)
110 }
111 return b.String()
112}