Flesh out multipart switcher

This commit is contained in:
Drew DeVault 2019-05-20 16:42:44 -04:00
parent 3376f926ed
commit 511fea3944
5 changed files with 146 additions and 70 deletions

View file

@ -123,8 +123,8 @@ func Reply(aerc *widgets.Aerc, args []string) error {
} }
if quote { if quote {
// TODO: something more intelligent than fetching the 0th part // TODO: something more intelligent than fetching the 1st part
store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) { store.FetchBodyPart(msg.Uid, []int{1}, func(reader io.Reader) {
header := message.Header{} header := message.Header{}
header.SetText( header.SetText(
"Content-Transfer-Encoding", msg.BodyStructure.Encoding) "Content-Transfer-Encoding", msg.BodyStructure.Encoding)

View file

@ -90,7 +90,7 @@ func (store *MessageStore) FetchFull(uids []uint32, cb func(io.Reader)) {
} }
func (store *MessageStore) FetchBodyPart( 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{ store.worker.PostAction(&types.FetchMessageBodyPart{
Uid: uid, Uid: uid,

View file

@ -23,10 +23,17 @@ import (
) )
type MessageViewer struct { type MessageViewer struct {
ui.Invalidatable
conf *config.AercConfig conf *config.AercConfig
err error err error
msg *types.MessageInfo
grid *ui.Grid grid *ui.Grid
msg *types.MessageInfo
switcher *PartSwitcher
store *lib.MessageStore
}
type PartSwitcher struct {
ui.Invalidatable
parts []*PartViewer parts []*PartViewer
selected int selected int
} }
@ -48,8 +55,8 @@ func formatAddresses(addrs []*imap.Address) string {
return val.String() return val.String()
} }
func NewMessageViewer(conf *config.AercConfig, store *lib.MessageStore, func NewMessageViewer(conf *config.AercConfig,
msg *types.MessageInfo) *MessageViewer { store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer {
grid := ui.NewGrid().Rows([]ui.GridSpec{ grid := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_EXACT, 3}, // TODO: Based on number of header rows {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}, {ui.SIZE_EXACT, 20},
}) })
for i, part := range msg.BodyStructure.Parts { var (
fmt.Println(i, part.MIMEType, part.MIMESubType) err error
} mv *MessageViewer
)
// TODO: add multipart switcher and configure additional parts switcher := &PartSwitcher{}
pv, err := NewPartViewer(conf, msg, 0) if len(msg.BodyStructure.Parts) == 0 {
pv, err := NewPartViewer(conf, store, msg, msg.BodyStructure, []int{1})
if err != nil { if err != nil {
goto handle_error goto handle_error
} }
body.AddChild(pv).At(0, 0).Span(1, 2) 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
}
}
}
grid.AddChild(headers).At(0, 0) grid.AddChild(headers).At(0, 0)
grid.AddChild(body).At(1, 0) grid.AddChild(body).At(1, 0)
store.FetchBodyPart(msg.Uid, 0, pv.SetSource) mv = &MessageViewer{
return &MessageViewer{
grid: grid, grid: grid,
msg: msg, msg: msg,
parts: []*PartViewer{pv}, store: store,
switcher: switcher,
} }
body.AddChild(mv.switcher).At(0, 0).Span(1, 2)
return mv
handle_error: handle_error:
return &MessageViewer{ return &MessageViewer{
err: err, 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) { func (mv *MessageViewer) Draw(ctx *ui.Context) {
if mv.err != nil { if mv.err != nil {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) 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 { func (ps *PartSwitcher) Invalidate() {
// What is encapsulation even ps.DoInvalidate(ps)
if mv.parts[mv.selected].term != nil { }
return mv.parts[mv.selected].term.Event(event)
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 return false
} }
func (mv *MessageViewer) Focus(focus bool) { func (ps *PartSwitcher) Draw(ctx *ui.Context) {
if mv.parts[mv.selected].term != nil { height := len(ps.parts)
mv.parts[mv.selected].term.Focus(focus) 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 { type PartViewer struct {
ui.Invalidatable
err error err error
fetched bool
filter *exec.Cmd filter *exec.Cmd
index string index []int
msg *types.MessageInfo msg *types.MessageInfo
pager *exec.Cmd pager *exec.Cmd
pagerin io.WriteCloser pagerin io.WriteCloser
part *imap.BodyStructure part *imap.BodyStructure
sink io.WriteCloser sink io.WriteCloser
source io.Reader source io.Reader
store *lib.MessageStore
term *Terminal term *Terminal
} }
func NewPartViewer(conf *config.AercConfig, func NewPartViewer(conf *config.AercConfig,
msg *types.MessageInfo, index int) (*PartViewer, error) { store *lib.MessageStore, msg *types.MessageInfo,
var ( part *imap.BodyStructure, index []int) (*PartViewer, error) {
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
}
var ( var (
filter *exec.Cmd filter *exec.Cmd
@ -228,27 +311,31 @@ func NewPartViewer(conf *config.AercConfig,
if pagerin, _ = pager.StdinPipe(); err != nil { if pagerin, _ = pager.StdinPipe(); err != nil {
return nil, err return nil, err
} }
} else {
if pipe, err = pager.StdinPipe(); err != nil {
return nil, err
}
}
if term, err = NewTerminal(pager); err != nil { if term, err = NewTerminal(pager); err != nil {
return nil, err return nil, err
} }
}
pv := &PartViewer{ pv := &PartViewer{
filter: filter, filter: filter,
index: index, // TODO: Nested multipart does indicies differently
msg: msg,
pager: pager, pager: pager,
pagerin: pagerin, pagerin: pagerin,
part: part, part: part,
sink: pipe, sink: pipe,
store: store,
term: term, term: term,
} }
if term != nil {
term.OnStart = func() { term.OnStart = func() {
pv.attemptCopy() pv.attemptCopy()
} }
term.OnInvalidate(func(_ ui.Drawable) {
pv.Invalidate()
})
}
return pv, nil 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() { func (pv *PartViewer) Invalidate() {
pv.term.Invalidate() pv.DoInvalidate(pv)
} }
func (pv *PartViewer) Draw(ctx *ui.Context) { 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 { if pv.err != nil {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error()) 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() { func (hv *HeaderView) Invalidate() {
hv.DoInvalidate(hv) 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)
}

View file

@ -26,7 +26,7 @@ func (imapw *IMAPWorker) handleFetchMessageBodyPart(
imapw.worker.Logger.Printf("Fetching message part") imapw.worker.Logger.Printf("Fetching message part")
section := &imap.BodySectionName{} section := &imap.BodySectionName{}
section.Path = []int{msg.Part + 1} section.Path = msg.Part
items := []imap.FetchItem{section.FetchItem()} items := []imap.FetchItem{section.FetchItem()}
uids := imap.SeqSet{} uids := imap.SeqSet{}
uids.AddNum(msg.Uid) uids.AddNum(msg.Uid)

View file

@ -94,7 +94,7 @@ type FetchFullMessages struct {
type FetchMessageBodyPart struct { type FetchMessageBodyPart struct {
Message Message
Uid uint32 Uid uint32
Part int Part []int
} }
type DeleteMessages struct { type DeleteMessages struct {