package widgets import ( "log" "github.com/emersion/go-imap" "github.com/gdamore/tcell" "git.sr.ht/~sircmpwn/aerc2/config" "git.sr.ht/~sircmpwn/aerc2/lib/ui" "git.sr.ht/~sircmpwn/aerc2/worker/types" ) type MessageStore struct { DirInfo types.DirectoryInfo Messages map[uint32]*types.MessageInfo // Ordered list of known UIDs Uids []uint32 // Map of uids we've asked the worker to fetch onUpdate func(store *MessageStore) pendingBodies map[uint32]interface{} pendingHeaders map[uint32]interface{} worker *types.Worker } func NewMessageStore(worker *types.Worker, dirInfo *types.DirectoryInfo) *MessageStore { return &MessageStore{ DirInfo: *dirInfo, pendingBodies: make(map[uint32]interface{}), pendingHeaders: make(map[uint32]interface{}), worker: worker, } } func (store *MessageStore) FetchHeaders(uids []uint32) { // TODO: this could be optimized by pre-allocating toFetch and trimming it // at the end. In practice we expect to get most messages back in one frame. var toFetch imap.SeqSet for _, uid := range uids { if _, ok := store.pendingHeaders[uid]; !ok { toFetch.AddNum(uint32(uid)) store.pendingHeaders[uid] = nil } } if !toFetch.Empty() { store.worker.PostAction(&types.FetchMessageHeaders{ Uids: toFetch, }, nil) } } func (store *MessageStore) Update(msg types.WorkerMessage) { update := false switch msg := msg.(type) { case *types.DirectoryInfo: store.DirInfo = *msg update = true break case *types.DirectoryContents: newMap := make(map[uint32]*types.MessageInfo) for _, uid := range msg.Uids { if msg, ok := store.Messages[uid]; ok { newMap[uid] = msg } else { newMap[uid] = nil } } store.Messages = newMap store.Uids = msg.Uids update = true break case *types.MessageInfo: // TODO: merge message info into existing record, if applicable store.Messages[msg.Uid] = msg if _, ok := store.pendingHeaders[msg.Uid]; msg.Envelope != nil && ok { delete(store.pendingHeaders, msg.Uid) } update = true break } if update && store.onUpdate != nil { store.onUpdate(store) } } func (store *MessageStore) OnUpdate(fn func(store *MessageStore)) { store.onUpdate = fn } type MessageList struct { conf *config.AercConfig logger *log.Logger onInvalidate func(d ui.Drawable) selected int spinner *Spinner store *MessageStore } // TODO: fish in config func NewMessageList(logger *log.Logger) *MessageList { ml := &MessageList{ logger: logger, selected: 0, spinner: NewSpinner(), } ml.spinner.OnInvalidate(func(_ ui.Drawable) { ml.Invalidate() }) // TODO: stop spinner, probably ml.spinner.Start() return ml } func (ml *MessageList) OnInvalidate(onInvalidate func(d ui.Drawable)) { ml.onInvalidate = onInvalidate } func (ml *MessageList) Invalidate() { if ml.onInvalidate != nil { ml.onInvalidate(ml) } } func (ml *MessageList) Draw(ctx *ui.Context) { ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) if ml.store == nil { ml.spinner.Draw(ctx) return } var ( needsHeaders []uint32 row int = 0 ) for i := len(ml.store.Uids) - 1; i >= 0; i-- { uid := ml.store.Uids[i] msg := ml.store.Messages[uid] if row >= ctx.Height() { break } if msg == nil { needsHeaders = append(needsHeaders, uid) ml.spinner.Draw(ctx.Subcontext(0, row, ctx.Width(), 1)) row += 1 continue } style := tcell.StyleDefault if row == ml.selected { style = style.Background(tcell.ColorWhite). Foreground(tcell.ColorBlack) } ctx.Fill(0, row, ctx.Width(), 1, ' ', style) ctx.Printf(0, row, style, "%s", msg.Envelope.Subject) row += 1 } if len(needsHeaders) != 0 { ml.store.FetchHeaders(needsHeaders) ml.spinner.Start() } else { ml.spinner.Stop() } } func (ml *MessageList) SetStore(store *MessageStore) { ml.store = store if store != nil { ml.spinner.Stop() } else { ml.spinner.Start() } ml.Invalidate() } func (ml *MessageList) nextPrev(delta int) { ml.selected += delta if ml.selected < 0 { ml.selected = 0 } if ml.selected >= len(ml.store.Uids) { ml.selected = len(ml.store.Uids) - 1 } // TODO: scrolling ml.Invalidate() } func (ml *MessageList) Next() { ml.nextPrev(1) } func (ml *MessageList) Prev() { ml.nextPrev(-1) }