aboutsummaryrefslogtreecommitdiffstats
path: root/widgets/msglist.go
diff options
context:
space:
mode:
Diffstat (limited to 'widgets/msglist.go')
-rw-r--r--widgets/msglist.go497
1 files changed, 0 insertions, 497 deletions
diff --git a/widgets/msglist.go b/widgets/msglist.go
deleted file mode 100644
index 3187b5d5..00000000
--- a/widgets/msglist.go
+++ /dev/null
@@ -1,497 +0,0 @@
-package widgets
-
-import (
- "bytes"
- "fmt"
- "math"
- "strings"
-
- sortthread "github.com/emersion/go-imap-sortthread"
- "github.com/emersion/go-message/mail"
- "github.com/gdamore/tcell/v2"
-
- "git.sr.ht/~rjarry/aerc/config"
- "git.sr.ht/~rjarry/aerc/lib"
- "git.sr.ht/~rjarry/aerc/lib/state"
- "git.sr.ht/~rjarry/aerc/lib/ui"
- "git.sr.ht/~rjarry/aerc/log"
- "git.sr.ht/~rjarry/aerc/models"
- "git.sr.ht/~rjarry/aerc/worker/types"
-)
-
-type MessageList struct {
- Scrollable
- height int
- width int
- nmsgs int
- spinner *Spinner
- store *lib.MessageStore
- isInitalizing bool
- aerc *Aerc
-}
-
-func NewMessageList(aerc *Aerc, account *AccountView) *MessageList {
- ml := &MessageList{
- spinner: NewSpinner(account.uiConf),
- isInitalizing: true,
- aerc: aerc,
- }
- // TODO: stop spinner, probably
- ml.spinner.Start()
- return ml
-}
-
-func (ml *MessageList) Invalidate() {
- ui.Invalidate()
-}
-
-type messageRowParams struct {
- uid uint32
- needsHeaders bool
- uiConfig *config.UIConfig
- styles []config.StyleObject
- headers *mail.Header
-}
-
-func (ml *MessageList) Draw(ctx *ui.Context) {
- ml.height = ctx.Height()
- ml.width = ctx.Width()
- uiConfig := ml.aerc.SelectedAccountUiConfig()
- ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
- uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT))
-
- acct := ml.aerc.SelectedAccount()
- store := ml.Store()
- if store == nil || acct == nil || len(store.Uids()) == 0 {
- if ml.isInitalizing {
- ml.spinner.Draw(ctx)
- } else {
- ml.spinner.Stop()
- ml.drawEmptyMessage(ctx)
- }
- return
- }
-
- ml.UpdateScroller(ml.height, len(store.Uids()))
- iter := store.UidsIterator()
- for i := 0; iter.Next(); i++ {
- if store.SelectedUid() == iter.Value().(uint32) {
- ml.EnsureScroll(i)
- break
- }
- }
-
- store.UpdateScroll(ml.Scroll(), ml.height)
-
- textWidth := ctx.Width()
- if ml.NeedScrollbar() {
- textWidth -= 1
- }
- if textWidth <= 0 {
- return
- }
-
- var needsHeaders []uint32
-
- data := state.NewDataSetter()
- data.SetAccount(acct.acct)
- data.SetFolder(acct.Directories().SelectedDirectory())
-
- customDraw := func(t *ui.Table, r int, c *ui.Context) bool {
- row := &t.Rows[r]
- params, _ := row.Priv.(messageRowParams)
- if params.needsHeaders {
- needsHeaders = append(needsHeaders, params.uid)
- ml.spinner.Draw(ctx.Subcontext(0, r, c.Width(), 1))
- return true
- }
- return false
- }
-
- getRowStyle := func(t *ui.Table, r int) tcell.Style {
- var style tcell.Style
- row := &t.Rows[r]
- params, _ := row.Priv.(messageRowParams)
- if params.uid == store.SelectedUid() {
- style = params.uiConfig.MsgComposedStyleSelected(
- config.STYLE_MSGLIST_DEFAULT, params.styles,
- params.headers)
- } else {
- style = params.uiConfig.MsgComposedStyle(
- config.STYLE_MSGLIST_DEFAULT, params.styles,
- params.headers)
- }
- return style
- }
-
- table := ui.NewTable(
- ml.height,
- uiConfig.IndexColumns,
- uiConfig.ColumnSeparator,
- customDraw,
- getRowStyle,
- )
-
- showThreads := store.ThreadedView()
- threadView := newThreadView(store)
- iter = store.UidsIterator()
- for i := 0; iter.Next(); i++ {
- if i < ml.Scroll() {
- continue
- }
- uid := iter.Value().(uint32)
- if showThreads {
- threadView.Update(data, uid)
- }
- if addMessage(store, uid, &table, data, uiConfig) {
- break
- }
- }
-
- table.Draw(ctx.Subcontext(0, 0, textWidth, ctx.Height()))
-
- if ml.NeedScrollbar() {
- scrollbarCtx := ctx.Subcontext(textWidth, 0, 1, ctx.Height())
- ml.drawScrollbar(scrollbarCtx)
- }
-
- if len(store.Uids()) == 0 {
- if store.Sorting {
- ml.spinner.Start()
- ml.spinner.Draw(ctx)
- return
- } else {
- ml.drawEmptyMessage(ctx)
- }
- }
-
- if len(needsHeaders) != 0 {
- store.FetchHeaders(needsHeaders, nil)
- ml.spinner.Start()
- } else {
- ml.spinner.Stop()
- }
-}
-
-func addMessage(
- store *lib.MessageStore, uid uint32,
- table *ui.Table, data state.DataSetter,
- uiConfig *config.UIConfig,
-) bool {
- msg := store.Messages[uid]
-
- cells := make([]string, len(table.Columns))
- params := messageRowParams{uid: uid, uiConfig: uiConfig}
-
- if msg == nil || msg.Envelope == nil {
- params.needsHeaders = true
- return table.AddRow(cells, params)
- }
-
- if msg.Flags.Has(models.SeenFlag) {
- params.styles = append(params.styles, config.STYLE_MSGLIST_READ)
- } else {
- params.styles = append(params.styles, config.STYLE_MSGLIST_UNREAD)
- }
- if msg.Flags.Has(models.AnsweredFlag) {
- params.styles = append(params.styles, config.STYLE_MSGLIST_ANSWERED)
- }
- if msg.Flags.Has(models.FlaggedFlag) {
- params.styles = append(params.styles, config.STYLE_MSGLIST_FLAGGED)
- }
- // deleted message
- if _, ok := store.Deleted[msg.Uid]; ok {
- params.styles = append(params.styles, config.STYLE_MSGLIST_DELETED)
- }
- // search result
- if store.IsResult(msg.Uid) {
- params.styles = append(params.styles, config.STYLE_MSGLIST_RESULT)
- }
- // folded thread
- templateData, ok := data.(models.TemplateData)
- if ok {
- if templateData.ThreadFolded() {
- params.styles = append(params.styles, config.STYLE_MSGLIST_THREAD_FOLDED)
- }
- if templateData.ThreadContext() {
- params.styles = append(params.styles, config.STYLE_MSGLIST_THREAD_CONTEXT)
- }
- }
- // marked message
- marked := store.Marker().IsMarked(msg.Uid)
- if marked {
- params.styles = append(params.styles, config.STYLE_MSGLIST_MARKED)
- }
-
- data.SetInfo(msg, len(table.Rows), marked)
-
- for c, col := range table.Columns {
- var buf bytes.Buffer
- err := col.Def.Template.Execute(&buf, data.Data())
- if err != nil {
- log.Errorf("<%s> %s", msg.Envelope.MessageId, err)
- cells[c] = err.Error()
- } else {
- cells[c] = buf.String()
- }
- }
-
- params.headers = msg.RFC822Headers
-
- return table.AddRow(cells, params)
-}
-
-func (ml *MessageList) drawScrollbar(ctx *ui.Context) {
- uiConfig := ml.aerc.SelectedAccountUiConfig()
- gutterStyle := uiConfig.GetStyle(config.STYLE_MSGLIST_GUTTER)
- pillStyle := uiConfig.GetStyle(config.STYLE_MSGLIST_PILL)
-
- // gutter
- ctx.Fill(0, 0, 1, ctx.Height(), ' ', gutterStyle)
-
- // pill
- pillSize := int(math.Ceil(float64(ctx.Height()) * ml.PercentVisible()))
- pillOffset := int(math.Floor(float64(ctx.Height()) * ml.PercentScrolled()))
- ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle)
-}
-
-func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
- if event, ok := event.(*tcell.EventMouse); ok {
- switch event.Buttons() {
- case tcell.Button1:
- if ml.aerc == nil {
- return
- }
- selectedMsg, ok := ml.Clicked(localX, localY)
- if ok {
- ml.Select(selectedMsg)
- acct := ml.aerc.SelectedAccount()
- if acct == nil || acct.Messages().Empty() {
- return
- }
- store := acct.Messages().Store()
- msg := acct.Messages().Selected()
- if msg == nil {
- return
- }
- lib.NewMessageStoreView(msg, acct.UiConfig().AutoMarkRead,
- store, ml.aerc.Crypto, ml.aerc.DecryptKeys,
- func(view lib.MessageView, err error) {
- if err != nil {
- ml.aerc.PushError(err.Error())
- return
- }
- viewer := NewMessageViewer(acct, view)
- ml.aerc.NewTab(viewer, msg.Envelope.Subject)
- })
- }
- case tcell.WheelDown:
- if ml.store != nil {
- ml.store.Next()
- }
- ml.Invalidate()
- case tcell.WheelUp:
- if ml.store != nil {
- ml.store.Prev()
- }
- ml.Invalidate()
- }
- }
-}
-
-func (ml *MessageList) Clicked(x, y int) (int, bool) {
- store := ml.Store()
- if store == nil || ml.nmsgs == 0 || y >= ml.nmsgs {
- return 0, false
- }
- return y + ml.Scroll(), true
-}
-
-func (ml *MessageList) Height() int {
- return ml.height
-}
-
-func (ml *MessageList) Width() int {
- return ml.width
-}
-
-func (ml *MessageList) storeUpdate(store *lib.MessageStore) {
- if ml.Store() != store {
- return
- }
- ml.Invalidate()
-}
-
-func (ml *MessageList) SetStore(store *lib.MessageStore) {
- if ml.Store() != store {
- ml.Scrollable = Scrollable{}
- }
- ml.store = store
- if store != nil {
- ml.spinner.Stop()
- uids := store.Uids()
- ml.nmsgs = len(uids)
- store.OnUpdate(ml.storeUpdate)
- store.OnFilterChange(func(store *lib.MessageStore) {
- if ml.Store() != store {
- return
- }
- ml.nmsgs = len(store.Uids())
- })
- } else {
- ml.spinner.Start()
- }
- ml.Invalidate()
-}
-
-func (ml *MessageList) SetInitDone() {
- ml.isInitalizing = false
-}
-
-func (ml *MessageList) Store() *lib.MessageStore {
- return ml.store
-}
-
-func (ml *MessageList) Empty() bool {
- store := ml.Store()
- return store == nil || len(store.Uids()) == 0
-}
-
-func (ml *MessageList) Selected() *models.MessageInfo {
- return ml.Store().Selected()
-}
-
-func (ml *MessageList) Select(index int) {
- // Note that the msgstore.Select function expects a uid as argument
- // whereas the msglist.Select expects the message number
- store := ml.Store()
- uids := store.Uids()
- if len(uids) == 0 {
- store.Select(lib.MagicUid)
- return
- }
-
- iter := store.UidsIterator()
-
- var uid uint32
- if index < 0 {
- uid = uids[iter.EndIndex()]
- } else {
- uid = uids[iter.StartIndex()]
- for i := 0; iter.Next(); i++ {
- if i >= index {
- uid = iter.Value().(uint32)
- break
- }
- }
- }
- store.Select(uid)
-
- ml.Invalidate()
-}
-
-func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
- uiConfig := ml.aerc.SelectedAccountUiConfig()
- msg := uiConfig.EmptyMessage
- ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
- uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
-}
-
-func countThreads(thread *types.Thread) (ctr int) {
- if thread == nil {
- return
- }
- _ = thread.Walk(func(t *types.Thread, _ int, _ error) error {
- ctr++
- return nil
- })
- return
-}
-
-func threadPrefix(t *types.Thread, reverse bool, point bool) string {
- var arrow string
- if t.Parent != nil {
- switch {
- case t.NextSibling != nil:
- arrow = "├─"
- case reverse:
- arrow = "┌─"
- default:
- arrow = "└─"
- }
- if point {
- arrow += ">"
- }
- }
- var prefix []string
- for n := t; n.Parent != nil; n = n.Parent {
- switch {
- case n.Parent.NextSibling != nil && point:
- prefix = append(prefix, "│ ")
- case n.Parent.NextSibling != nil:
- prefix = append(prefix, "│ ")
- case point:
- prefix = append(prefix, " ")
- default:
- prefix = append(prefix, " ")
- }
- }
- // prefix is now in a reverse order (inside --> outside), so turn it
- for i, j := 0, len(prefix)-1; i < j; i, j = i+1, j-1 {
- prefix[i], prefix[j] = prefix[j], prefix[i]
- }
-
- // we don't want to indent the first child, hence we strip that level
- if len(prefix) > 0 {
- prefix = prefix[1:]
- }
- ps := strings.Join(prefix, "")
- return fmt.Sprintf("%v%v", ps, arrow)
-}
-
-func sameParent(left, right *types.Thread) bool {
- return left.Root() == right.Root()
-}
-
-func isParent(t *types.Thread) bool {
- return t == t.Root()
-}
-
-func threadSubject(store *lib.MessageStore, thread *types.Thread) string {
- msg, found := store.Messages[thread.Uid]
- if !found || msg == nil || msg.Envelope == nil {
- return ""
- }
- subject, _ := sortthread.GetBaseSubject(msg.Envelope.Subject)
- return subject
-}
-
-type threadView struct {
- store *lib.MessageStore
- reverse bool
- prev *types.Thread
- prevSubj string
-}
-
-func newThreadView(store *lib.MessageStore) *threadView {
- return &threadView{
- store: store,
- reverse: store.ReverseThreadOrder(),
- }
-}
-
-func (t *threadView) Update(data state.DataSetter, uid uint32) {
- prefix, same, count, folded, context := "", false, 0, false, false
- thread, err := t.store.Thread(uid)
- if thread != nil && err == nil {
- prefix = threadPrefix(thread, t.reverse, true)
- subject := threadSubject(t.store, thread)
- same = subject == t.prevSubj && sameParent(thread, t.prev) && !isParent(thread)
- t.prev = thread
- t.prevSubj = subject
- count = countThreads(thread)
- folded = thread.FirstChild != nil && thread.FirstChild.Hidden
- context = thread.Context
- }
- data.SetThreading(prefix, same, count, folded, context)
-}