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		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}