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 if msg.String() == keyEnter {
56 if o.loading {
57 return nil
58 }
59 if o.done {
60 results := append([]fetcher.Email(nil), o.results...)
61 query := o.query
62 return func() tea.Msg { return ApplySearchResultsMsg{Query: query, Emails: results} }
63 }
64 raw := o.input.Value()
65 if raw == "" {
66 return nil
67 }
68 o.loading, o.done, o.err, o.results = true, false, "", nil
69 query := backend.ParseSearchQuery(raw)
70 return func() tea.Msg { return SearchRequestedMsg{Query: query, Mailbox: mailbox, AccountID: accountID} }
71 }
72 }
73
74 var cmd tea.Cmd
75 o.input, cmd = o.input.Update(msg)
76 return cmd
77}
78
79func (o *SearchOverlay) View() string {
80 boxWidth := o.width - 4
81 if boxWidth < 40 {
82 boxWidth = 40
83 }
84 style := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).
85 BorderForeground(theme.ActiveTheme.Accent).Padding(1, 2).Width(boxWidth)
86 content := "Search mail\n\n" + o.input.View()
87 if o.loading {
88 content += "\n\nSearching..."
89 }
90 if o.err != "" {
91 content += "\n\n" + lipgloss.NewStyle().Foreground(theme.ActiveTheme.Danger).Render(o.err)
92 }
93 if o.done {
94 content += fmt.Sprintf("\n\n%d result(s). Press Enter to apply, Esc to dismiss.\n", len(o.results))
95 content += o.resultsView()
96 }
97 return style.Render(content)
98}
99
100func (o *SearchOverlay) resultsView() string {
101 limit := len(o.results)
102 if limit > 10 {
103 limit = 10
104 }
105 var b strings.Builder
106 for i := 0; i < limit; i++ {
107 email := o.results[i]
108 fmt.Fprintf(&b, "%d. %s - %s\n", i+1, parseSenderName(email.From), email.Subject)
109 }
110 return b.String()
111}