Factor UI models out of the worker message package

Before, the information needed to display different parts of the UI was
tightly coupled to the specific messages being sent back and forth to
the backend worker. Separating out a models package allows us to be more
specific about exactly what a backend is able to and required to
provide for the UI.
This commit is contained in:
Ben Burwell 2019-07-07 22:43:56 -04:00 committed by Drew DeVault
parent c79577d376
commit cce7cb4808
12 changed files with 145 additions and 85 deletions

View File

@ -9,11 +9,11 @@ import (
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/models"
) )
func ParseIndexFormat(conf *config.AercConfig, number int, func ParseIndexFormat(conf *config.AercConfig, number int,
msg *types.MessageInfo) (string, []interface{}, error) { msg *models.MessageInfo) (string, []interface{}, error) {
format := conf.Ui.IndexFormat format := conf.Ui.IndexFormat
retval := make([]byte, 0, len(format)) retval := make([]byte, 0, len(format))

View File

@ -6,14 +6,15 @@ import (
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/worker/types"
) )
// Accesses to fields must be guarded by MessageStore.Lock/Unlock // Accesses to fields must be guarded by MessageStore.Lock/Unlock
type MessageStore struct { type MessageStore struct {
Deleted map[uint32]interface{} Deleted map[uint32]interface{}
DirInfo types.DirectoryInfo DirInfo models.DirectoryInfo
Messages map[uint32]*types.MessageInfo Messages map[uint32]*models.MessageInfo
// Ordered list of known UIDs // Ordered list of known UIDs
Uids []uint32 Uids []uint32
@ -33,7 +34,7 @@ type MessageStore struct {
} }
func NewMessageStore(worker *types.Worker, func NewMessageStore(worker *types.Worker,
dirInfo *types.DirectoryInfo) *MessageStore { dirInfo *models.DirectoryInfo) *MessageStore {
return &MessageStore{ return &MessageStore{
Deleted: make(map[uint32]interface{}), Deleted: make(map[uint32]interface{}),
@ -106,11 +107,11 @@ func (store *MessageStore) FetchBodyPart(
if !ok { if !ok {
return return
} }
cb(msg.Reader) cb(msg.Part.Reader)
}) })
} }
func merge(to *types.MessageInfo, from *types.MessageInfo) { func merge(to *models.MessageInfo, from *models.MessageInfo) {
if from.BodyStructure != nil { if from.BodyStructure != nil {
to.BodyStructure = from.BodyStructure to.BodyStructure = from.BodyStructure
} }
@ -131,11 +132,11 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
update := false update := false
switch msg := msg.(type) { switch msg := msg.(type) {
case *types.DirectoryInfo: case *types.DirectoryInfo:
store.DirInfo = *msg store.DirInfo = *msg.Info
store.worker.PostAction(&types.FetchDirectoryContents{}, nil) store.worker.PostAction(&types.FetchDirectoryContents{}, nil)
update = true update = true
case *types.DirectoryContents: case *types.DirectoryContents:
newMap := make(map[uint32]*types.MessageInfo) newMap := make(map[uint32]*models.MessageInfo)
for _, uid := range msg.Uids { for _, uid := range msg.Uids {
if msg, ok := store.Messages[uid]; ok { if msg, ok := store.Messages[uid]; ok {
newMap[uid] = msg newMap[uid] = msg
@ -147,14 +148,14 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
store.Uids = msg.Uids store.Uids = msg.Uids
update = true update = true
case *types.MessageInfo: case *types.MessageInfo:
if existing, ok := store.Messages[msg.Uid]; ok && existing != nil { if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
merge(existing, msg) merge(existing, msg.Info)
} else { } else {
store.Messages[msg.Uid] = msg store.Messages[msg.Info.Uid] = msg.Info
} }
if _, ok := store.pendingHeaders[msg.Uid]; msg.Envelope != nil && ok { if _, ok := store.pendingHeaders[msg.Info.Uid]; msg.Info.Envelope != nil && ok {
delete(store.pendingHeaders, msg.Uid) delete(store.pendingHeaders, msg.Info.Uid)
if cbs, ok := store.headerCallbacks[msg.Uid]; ok { if cbs, ok := store.headerCallbacks[msg.Info.Uid]; ok {
for _, cb := range cbs { for _, cb := range cbs {
cb(msg) cb(msg)
} }
@ -162,11 +163,11 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
} }
update = true update = true
case *types.FullMessage: case *types.FullMessage:
if _, ok := store.pendingBodies[msg.Uid]; ok { if _, ok := store.pendingBodies[msg.Content.Uid]; ok {
delete(store.pendingBodies, msg.Uid) delete(store.pendingBodies, msg.Content.Uid)
if cbs, ok := store.bodyCallbacks[msg.Uid]; ok { if cbs, ok := store.bodyCallbacks[msg.Content.Uid]; ok {
for _, cb := range cbs { for _, cb := range cbs {
cb(msg.Reader) cb(msg.Content.Reader)
} }
} }
} }
@ -283,7 +284,7 @@ func (store *MessageStore) Read(uids []uint32, read bool,
}, cb) }, cb)
} }
func (store *MessageStore) Selected() *types.MessageInfo { func (store *MessageStore) Selected() *models.MessageInfo {
return store.Messages[store.Uids[len(store.Uids)-store.selected-1]] return store.Messages[store.Uids[len(store.Uids)-store.selected-1]]
} }

52
models/models.go Normal file
View File

@ -0,0 +1,52 @@
package models
import (
"io"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/mail"
)
type Directory struct {
Name string
Attributes []string
}
type DirectoryInfo struct {
Name string
Flags []string
ReadOnly bool
// The total number of messages in this mailbox.
Exists int
// The number of messages not seen since the last time the mailbox was opened.
Recent int
// The number of unread messages
Unseen int
}
// A MessageInfo holds information about the structure of a message
type MessageInfo struct {
BodyStructure *imap.BodyStructure
Envelope *imap.Envelope
Flags []string
InternalDate time.Time
RFC822Headers *mail.Header
Size uint32
Uid uint32
}
// A MessageBodyPart can be displayed in the message viewer
type MessageBodyPart struct {
Reader io.Reader
Uid uint32
}
// A FullMessage is the entire message
type FullMessage struct {
Reader io.Reader
Uid uint32
}

View File

@ -9,6 +9,7 @@ import (
"git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
"git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/worker" "git.sr.ht/~sircmpwn/aerc/worker"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/worker/types"
) )
@ -169,7 +170,7 @@ func (acct *AccountView) SelectedAccount() *AccountView {
return acct return acct
} }
func (acct *AccountView) SelectedMessage() *types.MessageInfo { func (acct *AccountView) SelectedMessage() *models.MessageInfo {
return acct.msglist.Selected() return acct.msglist.Selected()
} }
@ -195,11 +196,11 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
acct.dirlist.UpdateList(nil) acct.dirlist.UpdateList(nil)
} }
case *types.DirectoryInfo: case *types.DirectoryInfo:
if store, ok := acct.msgStores[msg.Name]; ok { if store, ok := acct.msgStores[msg.Info.Name]; ok {
store.Update(msg) store.Update(msg)
} else { } else {
store = lib.NewMessageStore(acct.worker, msg) store = lib.NewMessageStore(acct.worker, msg.Info)
acct.msgStores[msg.Name] = store acct.msgStores[msg.Info.Name] = store
store.OnUpdate(func(_ *lib.MessageStore) { store.OnUpdate(func(_ *lib.MessageStore) {
store.OnUpdate(nil) store.OnUpdate(nil)
acct.msglist.SetStore(store) acct.msglist.SetStore(store)

View File

@ -55,7 +55,7 @@ func (dirlist *DirectoryList) UpdateList(done func(dirs []string)) {
switch msg := msg.(type) { switch msg := msg.(type) {
case *types.Directory: case *types.Directory:
dirs = append(dirs, msg.Name) dirs = append(dirs, msg.Dir.Name)
case *types.Done: case *types.Done:
sort.Strings(dirs) sort.Strings(dirs)
dirlist.store.Update(dirs) dirlist.store.Update(dirs)

View File

@ -11,7 +11,7 @@ import (
"git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/models"
) )
type MessageList struct { type MessageList struct {
@ -176,7 +176,7 @@ func (ml *MessageList) Empty() bool {
return store == nil || len(store.Uids) == 0 return store == nil || len(store.Uids) == 0
} }
func (ml *MessageList) Selected() *types.MessageInfo { func (ml *MessageList) Selected() *models.MessageInfo {
store := ml.Store() store := ml.Store()
return store.Messages[store.Uids[len(store.Uids)-ml.store.SelectedIndex()-1]] return store.Messages[store.Uids[len(store.Uids)-ml.store.SelectedIndex()-1]]
} }

View File

@ -20,7 +20,7 @@ import (
"git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/models"
) )
var ansi = regexp.MustCompile("^\x1B\\[[0-?]*[ -/]*[@-~]") var ansi = regexp.MustCompile("^\x1B\\[[0-?]*[ -/]*[@-~]")
@ -31,7 +31,7 @@ type MessageViewer struct {
conf *config.AercConfig conf *config.AercConfig
err error err error
grid *ui.Grid grid *ui.Grid
msg *types.MessageInfo msg *models.MessageInfo
switcher *PartSwitcher switcher *PartSwitcher
store *lib.MessageStore store *lib.MessageStore
} }
@ -44,7 +44,7 @@ type PartSwitcher struct {
} }
func NewMessageViewer(acct *AccountView, conf *config.AercConfig, func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer { store *lib.MessageStore, msg *models.MessageInfo) *MessageViewer {
grid := ui.NewGrid().Rows([]ui.GridSpec{ grid := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_EXACT, 4}, // TODO: Based on number of header rows {ui.SIZE_EXACT, 4}, // TODO: Based on number of header rows
@ -112,7 +112,7 @@ handle_error:
} }
func enumerateParts(conf *config.AercConfig, store *lib.MessageStore, func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
msg *types.MessageInfo, body *imap.BodyStructure, msg *models.MessageInfo, body *imap.BodyStructure,
showHeaders bool, index []int) ([]*PartViewer, error) { showHeaders bool, index []int) ([]*PartViewer, error) {
var parts []*PartViewer var parts []*PartViewer
@ -140,7 +140,7 @@ func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
} }
func createSwitcher(switcher *PartSwitcher, conf *config.AercConfig, func createSwitcher(switcher *PartSwitcher, conf *config.AercConfig,
store *lib.MessageStore, msg *types.MessageInfo, showHeaders bool) error { store *lib.MessageStore, msg *models.MessageInfo, showHeaders bool) error {
var err error var err error
switcher.showHeaders = showHeaders switcher.showHeaders = showHeaders
@ -212,7 +212,7 @@ func (mv *MessageViewer) SelectedAccount() *AccountView {
return mv.acct return mv.acct
} }
func (mv *MessageViewer) SelectedMessage() *types.MessageInfo { func (mv *MessageViewer) SelectedMessage() *models.MessageInfo {
return mv.msg return mv.msg
} }
@ -321,7 +321,7 @@ type PartViewer struct {
fetched bool fetched bool
filter *exec.Cmd filter *exec.Cmd
index []int index []int
msg *types.MessageInfo msg *models.MessageInfo
pager *exec.Cmd pager *exec.Cmd
pagerin io.WriteCloser pagerin io.WriteCloser
part *imap.BodyStructure part *imap.BodyStructure
@ -333,7 +333,7 @@ type PartViewer struct {
} }
func NewPartViewer(conf *config.AercConfig, func NewPartViewer(conf *config.AercConfig,
store *lib.MessageStore, msg *types.MessageInfo, store *lib.MessageStore, msg *models.MessageInfo,
part *imap.BodyStructure, showHeaders bool, part *imap.BodyStructure, showHeaders bool,
index []int) (*PartViewer, error) { index []int) (*PartViewer, error) {

View File

@ -5,7 +5,7 @@ import (
"git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/models"
) )
type PartInfo struct { type PartInfo struct {
@ -19,6 +19,6 @@ type ProvidesMessage interface {
ui.Drawable ui.Drawable
Store() *lib.MessageStore Store() *lib.MessageStore
SelectedAccount() *AccountView SelectedAccount() *AccountView
SelectedMessage() *types.MessageInfo SelectedMessage() *models.MessageInfo
SelectedMessagePart() *PartInfo SelectedMessagePart() *PartInfo
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
"github.com/emersion/go-message/textproto" "github.com/emersion/go-message/textproto"
"git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/worker/types"
) )
@ -82,39 +83,49 @@ func (imapw *IMAPWorker) handleFetchMessages(
header = &mail.Header{message.Header{textprotoHeader}} header = &mail.Header{message.Header{textprotoHeader}}
} }
imapw.worker.PostMessage(&types.MessageInfo{ imapw.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg), Message: types.RespondTo(msg),
BodyStructure: _msg.BodyStructure, Info: &models.MessageInfo{
Envelope: _msg.Envelope, BodyStructure: _msg.BodyStructure,
Flags: _msg.Flags, Envelope: _msg.Envelope,
InternalDate: _msg.InternalDate, Flags: _msg.Flags,
RFC822Headers: header, InternalDate: _msg.InternalDate,
Uid: _msg.Uid, RFC822Headers: header,
Uid: _msg.Uid,
},
}, nil) }, nil)
case *types.FetchFullMessages: case *types.FetchFullMessages:
reader := _msg.GetBody(section) reader := _msg.GetBody(section)
imapw.worker.PostMessage(&types.FullMessage{ imapw.worker.PostMessage(&types.FullMessage{
Message: types.RespondTo(msg), Message: types.RespondTo(msg),
Reader: reader, Content: &models.FullMessage{
Uid: _msg.Uid, Reader: reader,
Uid: _msg.Uid,
},
}, nil) }, nil)
// Update flags (to mark message as read) // Update flags (to mark message as read)
imapw.worker.PostMessage(&types.MessageInfo{ imapw.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg), Message: types.RespondTo(msg),
Flags: _msg.Flags, Info: &models.MessageInfo{
Uid: _msg.Uid, Flags: _msg.Flags,
Uid: _msg.Uid,
},
}, nil) }, nil)
case *types.FetchMessageBodyPart: case *types.FetchMessageBodyPart:
reader := _msg.GetBody(section) reader := _msg.GetBody(section)
imapw.worker.PostMessage(&types.MessageBodyPart{ imapw.worker.PostMessage(&types.MessageBodyPart{
Message: types.RespondTo(msg), Message: types.RespondTo(msg),
Reader: reader, Part: &models.MessageBodyPart{
Uid: _msg.Uid, Reader: reader,
Uid: _msg.Uid,
},
}, nil) }, nil)
// Update flags (to mark message as read) // Update flags (to mark message as read)
imapw.worker.PostMessage(&types.MessageInfo{ imapw.worker.PostMessage(&types.MessageInfo{
Message: types.RespondTo(msg), Message: types.RespondTo(msg),
Flags: _msg.Flags, Info: &models.MessageInfo{
Uid: _msg.Uid, Flags: _msg.Flags,
Uid: _msg.Uid,
},
}, nil) }, nil)
} }
} }

View File

@ -3,6 +3,7 @@ package imap
import ( import (
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/worker/types"
) )
@ -18,9 +19,11 @@ func (imapw *IMAPWorker) handleListDirectories(msg *types.ListDirectories) {
continue continue
} }
imapw.worker.PostMessage(&types.Directory{ imapw.worker.PostMessage(&types.Directory{
Message: types.RespondTo(msg), Message: types.RespondTo(msg),
Name: mbox.Name, Dir: &models.Directory{
Attributes: mbox.Attributes, Name: mbox.Name,
Attributes: mbox.Attributes,
},
}, nil) }, nil)
} }
done <- nil done <- nil

View File

@ -10,6 +10,7 @@ import (
idle "github.com/emersion/go-imap-idle" idle "github.com/emersion/go-imap-idle"
"github.com/emersion/go-imap/client" "github.com/emersion/go-imap/client"
"git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/worker/types"
) )
@ -169,13 +170,15 @@ func (w *IMAPWorker) handleImapUpdate(update client.Update) {
w.selected = *status w.selected = *status
} }
w.worker.PostMessage(&types.DirectoryInfo{ w.worker.PostMessage(&types.DirectoryInfo{
Flags: status.Flags, Info: &models.DirectoryInfo{
Name: status.Name, Flags: status.Flags,
ReadOnly: status.ReadOnly, Name: status.Name,
ReadOnly: status.ReadOnly,
Exists: int(status.Messages), Exists: int(status.Messages),
Recent: int(status.Recent), Recent: int(status.Recent),
Unseen: int(status.Unseen), Unseen: int(status.Unseen),
},
}, nil) }, nil)
case *client.MessageUpdate: case *client.MessageUpdate:
msg := update.Message msg := update.Message
@ -183,11 +186,13 @@ func (w *IMAPWorker) handleImapUpdate(update client.Update) {
msg.Uid = w.seqMap[msg.SeqNum-1] msg.Uid = w.seqMap[msg.SeqNum-1]
} }
w.worker.PostMessage(&types.MessageInfo{ w.worker.PostMessage(&types.MessageInfo{
BodyStructure: msg.BodyStructure, Info: &models.MessageInfo{
Envelope: msg.Envelope, BodyStructure: msg.BodyStructure,
Flags: msg.Flags, Envelope: msg.Envelope,
InternalDate: msg.InternalDate, Flags: msg.Flags,
Uid: msg.Uid, InternalDate: msg.InternalDate,
Uid: msg.Uid,
},
}, nil) }, nil)
case *client.ExpungeUpdate: case *client.ExpungeUpdate:
i := update.SeqNum - 1 i := update.SeqNum - 1

View File

@ -5,9 +5,9 @@ import (
"time" "time"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/emersion/go-message/mail"
"git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/models"
) )
type WorkerMessage interface { type WorkerMessage interface {
@ -139,17 +139,12 @@ type AppendMessage struct {
type Directory struct { type Directory struct {
Message Message
Attributes []string Dir *models.Directory
Name string
} }
type DirectoryInfo struct { type DirectoryInfo struct {
Message Message
Flags []string Info *models.DirectoryInfo
Name string
ReadOnly bool
Exists, Recent, Unseen int
} }
type DirectoryContents struct { type DirectoryContents struct {
@ -164,25 +159,17 @@ type SearchResults struct {
type MessageInfo struct { type MessageInfo struct {
Message Message
BodyStructure *imap.BodyStructure Info *models.MessageInfo
Envelope *imap.Envelope
Flags []string
InternalDate time.Time
RFC822Headers *mail.Header
Size uint32
Uid uint32
} }
type FullMessage struct { type FullMessage struct {
Message Message
Reader io.Reader Content *models.FullMessage
Uid uint32
} }
type MessageBodyPart struct { type MessageBodyPart struct {
Message Message
Reader io.Reader Part *models.MessageBodyPart
Uid uint32
} }
type MessagesDeleted struct { type MessagesDeleted struct {