From 511fea39449b6db551151da86d914bf7ab3b3713 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Mon, 20 May 2019 16:42:44 -0400 Subject: [PATCH] Flesh out multipart switcher --- commands/account/reply.go | 4 +- lib/msgstore.go | 2 +- widgets/msgviewer.go | 206 ++++++++++++++++++++++++++------------ worker/imap/fetch.go | 2 +- worker/types/messages.go | 2 +- 5 files changed, 146 insertions(+), 70 deletions(-) diff --git a/commands/account/reply.go b/commands/account/reply.go index f55d448..cd07ec9 100644 --- a/commands/account/reply.go +++ b/commands/account/reply.go @@ -123,8 +123,8 @@ func Reply(aerc *widgets.Aerc, args []string) error { } if quote { - // TODO: something more intelligent than fetching the 0th part - store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) { + // TODO: something more intelligent than fetching the 1st part + store.FetchBodyPart(msg.Uid, []int{1}, func(reader io.Reader) { header := message.Header{} header.SetText( "Content-Transfer-Encoding", msg.BodyStructure.Encoding) diff --git a/lib/msgstore.go b/lib/msgstore.go index 827d7cb..6ab7fc2 100644 --- a/lib/msgstore.go +++ b/lib/msgstore.go @@ -90,7 +90,7 @@ func (store *MessageStore) FetchFull(uids []uint32, cb func(io.Reader)) { } func (store *MessageStore) FetchBodyPart( - uid uint32, part int, cb func(io.Reader)) { + uid uint32, part []int, cb func(io.Reader)) { store.worker.PostAction(&types.FetchMessageBodyPart{ Uid: uid, diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index 60e1d23..ab79b5e 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -23,10 +23,17 @@ import ( ) type MessageViewer struct { + ui.Invalidatable conf *config.AercConfig err error - msg *types.MessageInfo grid *ui.Grid + msg *types.MessageInfo + switcher *PartSwitcher + store *lib.MessageStore +} + +type PartSwitcher struct { + ui.Invalidatable parts []*PartViewer selected int } @@ -48,8 +55,8 @@ func formatAddresses(addrs []*imap.Address) string { return val.String() } -func NewMessageViewer(conf *config.AercConfig, store *lib.MessageStore, - msg *types.MessageInfo) *MessageViewer { +func NewMessageViewer(conf *config.AercConfig, + store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer { grid := ui.NewGrid().Rows([]ui.GridSpec{ {ui.SIZE_EXACT, 3}, // TODO: Based on number of header rows @@ -91,28 +98,51 @@ func NewMessageViewer(conf *config.AercConfig, store *lib.MessageStore, {ui.SIZE_EXACT, 20}, }) - for i, part := range msg.BodyStructure.Parts { - fmt.Println(i, part.MIMEType, part.MIMESubType) - } + var ( + err error + mv *MessageViewer + ) - // TODO: add multipart switcher and configure additional parts - pv, err := NewPartViewer(conf, msg, 0) - if err != nil { - goto handle_error + switcher := &PartSwitcher{} + if len(msg.BodyStructure.Parts) == 0 { + pv, err := NewPartViewer(conf, store, msg, msg.BodyStructure, []int{1}) + if err != nil { + goto handle_error + } + switcher.parts = []*PartViewer{pv} + pv.OnInvalidate(func(_ ui.Drawable) { + switcher.Invalidate() + }) + } else { + switcher.parts, err = enumerateParts(conf, store, + msg, msg.BodyStructure, []int{}) + if err != nil { + goto handle_error + } + for i, pv := range switcher.parts { + pv.OnInvalidate(func(_ ui.Drawable) { + switcher.Invalidate() + }) + // TODO: switch to user's preferred mimetype, if configured + if switcher.selected == 0 && pv.part.MIMEType != "multipart" { + switcher.selected = i + } + } } - body.AddChild(pv).At(0, 0).Span(1, 2) grid.AddChild(headers).At(0, 0) grid.AddChild(body).At(1, 0) - store.FetchBodyPart(msg.Uid, 0, pv.SetSource) - - return &MessageViewer{ - grid: grid, - msg: msg, - parts: []*PartViewer{pv}, + mv = &MessageViewer{ + grid: grid, + msg: msg, + store: store, + switcher: switcher, } + body.AddChild(mv.switcher).At(0, 0).Span(1, 2) + return mv + handle_error: return &MessageViewer{ err: err, @@ -121,6 +151,34 @@ handle_error: } } +func enumerateParts(conf *config.AercConfig, store *lib.MessageStore, + msg *types.MessageInfo, body *imap.BodyStructure, + index []int) ([]*PartViewer, error) { + + var parts []*PartViewer + for i, part := range body.Parts { + curindex := append(index, i+1) + if part.MIMEType == "multipart" { + // Multipart meta-parts are faked + pv := &PartViewer{part: part} + parts = append(parts, pv) + subParts, err := enumerateParts( + conf, store, msg, part, curindex) + if err != nil { + return nil, err + } + parts = append(parts, subParts...) + continue + } + pv, err := NewPartViewer(conf, store, msg, part, curindex) + if err != nil { + return nil, err + } + parts = append(parts, pv) + } + return parts, nil +} + func (mv *MessageViewer) Draw(ctx *ui.Context) { if mv.err != nil { ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) @@ -140,44 +198,69 @@ func (mv *MessageViewer) OnInvalidate(fn func(d ui.Drawable)) { }) } -func (mv *MessageViewer) Event(event tcell.Event) bool { - // What is encapsulation even - if mv.parts[mv.selected].term != nil { - return mv.parts[mv.selected].term.Event(event) +func (ps *PartSwitcher) Invalidate() { + ps.DoInvalidate(ps) +} + +func (ps *PartSwitcher) Focus(focus bool) { + if ps.parts[ps.selected].term != nil { + ps.parts[ps.selected].term.Focus(focus) + } +} + +func (ps *PartSwitcher) Event(event tcell.Event) bool { + if ps.parts[ps.selected].term != nil { + return ps.parts[ps.selected].term.Event(event) } return false } -func (mv *MessageViewer) Focus(focus bool) { - if mv.parts[mv.selected].term != nil { - mv.parts[mv.selected].term.Focus(focus) +func (ps *PartSwitcher) Draw(ctx *ui.Context) { + height := len(ps.parts) + if height == 1 { + ps.parts[ps.selected].Draw(ctx) + return } + // TODO: cap height and add scrolling for messages with many parts + y := ctx.Height() - height + for i, part := range ps.parts { + style := tcell.StyleDefault.Reverse(ps.selected == i) + ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style) + ctx.Printf(len(part.index)*2, y+i, style, "%s/%s", + strings.ToLower(part.part.MIMEType), + strings.ToLower(part.part.MIMESubType)) + } + ps.parts[ps.selected].Draw(ctx.Subcontext( + 0, 0, ctx.Width(), ctx.Height()-height)) +} + +func (mv *MessageViewer) Event(event tcell.Event) bool { + return mv.switcher.Event(event) +} + +func (mv *MessageViewer) Focus(focus bool) { + mv.switcher.Focus(focus) } type PartViewer struct { + ui.Invalidatable err error + fetched bool filter *exec.Cmd - index string + index []int msg *types.MessageInfo pager *exec.Cmd pagerin io.WriteCloser part *imap.BodyStructure sink io.WriteCloser source io.Reader + store *lib.MessageStore term *Terminal } func NewPartViewer(conf *config.AercConfig, - msg *types.MessageInfo, index int) (*PartViewer, error) { - var ( - part *imap.BodyStructure - ) - // TODO: Find IMAP index, which may differ - if len(msg.BodyStructure.Parts) != 0 { - part = msg.BodyStructure.Parts[index] - } else { - part = msg.BodyStructure - } + store *lib.MessageStore, msg *types.MessageInfo, + part *imap.BodyStructure, index []int) (*PartViewer, error) { var ( filter *exec.Cmd @@ -228,26 +311,30 @@ func NewPartViewer(conf *config.AercConfig, if pagerin, _ = pager.StdinPipe(); err != nil { return nil, err } - } else { - if pipe, err = pager.StdinPipe(); err != nil { + if term, err = NewTerminal(pager); err != nil { return nil, err } } - if term, err = NewTerminal(pager); err != nil { - return nil, err - } pv := &PartViewer{ filter: filter, + index: index, // TODO: Nested multipart does indicies differently + msg: msg, pager: pager, pagerin: pagerin, part: part, sink: pipe, + store: store, term: term, } - term.OnStart = func() { - pv.attemptCopy() + if term != nil { + term.OnStart = func() { + pv.attemptCopy() + } + term.OnInvalidate(func(_ ui.Drawable) { + pv.Invalidate() + }) } return pv, nil @@ -297,17 +384,22 @@ func (pv *PartViewer) attemptCopy() { } } -func (pv *PartViewer) OnInvalidate(fn func(ui.Drawable)) { - pv.term.OnInvalidate(func(_ ui.Drawable) { - fn(pv) - }) -} - func (pv *PartViewer) Invalidate() { - pv.term.Invalidate() + pv.DoInvalidate(pv) } func (pv *PartViewer) Draw(ctx *ui.Context) { + if pv.filter == nil { + // TODO: Let them download it directly or something + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Printf(0, 0, tcell.StyleDefault, + "No filter configured for this mimetype") + return + } + if !pv.fetched { + pv.store.FetchBodyPart(pv.msg.Uid, pv.index, pv.SetSource) + pv.fetched = true + } if pv.err != nil { ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error()) @@ -347,19 +439,3 @@ func (hv *HeaderView) Draw(ctx *ui.Context) { func (hv *HeaderView) Invalidate() { hv.DoInvalidate(hv) } - -type MultipartView struct { - ui.Invalidatable -} - -func (mpv *MultipartView) Draw(ctx *ui.Context) { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) - ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true)) - ctx.Printf(0, 0, tcell.StyleDefault.Reverse(true), "text/plain") - ctx.Printf(0, 1, tcell.StyleDefault, "text/html") - ctx.Printf(0, 2, tcell.StyleDefault, "application/pgp-si…") -} - -func (mpv *MultipartView) Invalidate() { - mpv.DoInvalidate(mpv) -} diff --git a/worker/imap/fetch.go b/worker/imap/fetch.go index e3d8fa3..c5e7cd6 100644 --- a/worker/imap/fetch.go +++ b/worker/imap/fetch.go @@ -26,7 +26,7 @@ func (imapw *IMAPWorker) handleFetchMessageBodyPart( imapw.worker.Logger.Printf("Fetching message part") section := &imap.BodySectionName{} - section.Path = []int{msg.Part + 1} + section.Path = msg.Part items := []imap.FetchItem{section.FetchItem()} uids := imap.SeqSet{} uids.AddNum(msg.Uid) diff --git a/worker/types/messages.go b/worker/types/messages.go index 555e3ce..0169e05 100644 --- a/worker/types/messages.go +++ b/worker/types/messages.go @@ -94,7 +94,7 @@ type FetchFullMessages struct { type FetchMessageBodyPart struct { Message Uid uint32 - Part int + Part []int } type DeleteMessages struct {