search.go

  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}