Flesh out multipart switcher
This commit is contained in:
parent
3376f926ed
commit
511fea3944
5 changed files with 146 additions and 70 deletions
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue