aerc/widgets/msgviewer.go

225 lines
5.0 KiB
Go
Raw Normal View History

2019-03-30 19:12:04 +01:00
package widgets
import (
"bytes"
2019-03-31 18:14:37 +02:00
"fmt"
2019-03-30 19:12:04 +01:00
"io"
"os/exec"
2019-03-31 18:14:37 +02:00
"github.com/emersion/go-imap"
2019-03-31 19:36:37 +02:00
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
2019-03-30 19:12:04 +01:00
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
2019-03-31 18:14:37 +02:00
"git.sr.ht/~sircmpwn/aerc2/lib"
2019-03-30 19:12:04 +01:00
"git.sr.ht/~sircmpwn/aerc2/lib/ui"
2019-03-31 18:14:37 +02:00
"git.sr.ht/~sircmpwn/aerc2/worker/types"
2019-03-30 19:12:04 +01:00
)
type MessageViewer struct {
2019-03-31 18:35:51 +02:00
cmd *exec.Cmd
2019-03-31 19:36:37 +02:00
msg *types.MessageInfo
2019-03-31 18:35:51 +02:00
source io.Reader
sink io.WriteCloser
grid *ui.Grid
term *Terminal
2019-03-30 19:12:04 +01:00
}
2019-03-31 18:14:37 +02:00
func formatAddresses(addrs []*imap.Address) string {
val := bytes.Buffer{}
for i, addr := range addrs {
if addr.PersonalName != "" {
val.WriteString(fmt.Sprintf("%s <%s@%s>",
addr.PersonalName, addr.MailboxName, addr.HostName))
} else {
val.WriteString(fmt.Sprintf("%s@%s",
addr.MailboxName, addr.HostName))
}
if i != len(addrs)-1 {
val.WriteString(", ")
}
}
return val.String()
}
2019-03-30 19:12:04 +01:00
2019-03-31 18:14:37 +02:00
func NewMessageViewer(store *lib.MessageStore,
msg *types.MessageInfo) *MessageViewer {
2019-03-30 19:12:04 +01:00
grid := ui.NewGrid().Rows([]ui.GridSpec{
2019-03-31 18:14:37 +02:00
{ui.SIZE_EXACT, 3}, // TODO: Based on number of header rows
2019-03-30 19:12:04 +01:00
{ui.SIZE_WEIGHT, 1},
}).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1},
})
2019-03-31 18:14:37 +02:00
// TODO: let user specify additional headers to show by default
2019-03-30 19:12:04 +01:00
headers := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_EXACT, 1},
{ui.SIZE_EXACT, 1},
{ui.SIZE_EXACT, 1},
}).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1},
{ui.SIZE_WEIGHT, 1},
})
headers.AddChild(
&HeaderView{
Name: "From",
2019-03-31 18:14:37 +02:00
Value: formatAddresses(msg.Envelope.From),
2019-03-30 19:12:04 +01:00
}).At(0, 0)
headers.AddChild(
&HeaderView{
Name: "To",
2019-03-31 18:14:37 +02:00
Value: formatAddresses(msg.Envelope.To),
2019-03-30 19:12:04 +01:00
}).At(0, 1)
headers.AddChild(
&HeaderView{
2019-03-31 18:14:37 +02:00
Name: "Subject",
Value: msg.Envelope.Subject,
2019-03-30 19:12:04 +01:00
}).At(1, 0).Span(1, 2)
2019-03-31 18:14:37 +02:00
headers.AddChild(ui.NewFill(' ')).At(2, 0).Span(1, 2)
body := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1},
}).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1},
{ui.SIZE_EXACT, 20},
})
2019-03-30 19:12:04 +01:00
2019-03-31 18:14:37 +02:00
cmd := exec.Command("less")
2019-03-30 19:12:04 +01:00
pipe, _ := cmd.StdinPipe()
term, _ := NewTerminal(cmd)
2019-03-31 18:14:37 +02:00
// TODO: configure multipart view. I left a spot for it in the grid
body.AddChild(term).At(0, 0).Span(1, 2)
grid.AddChild(headers).At(0, 0)
grid.AddChild(body).At(1, 0)
viewer := &MessageViewer{
2019-03-31 18:35:51 +02:00
cmd: cmd,
2019-03-31 18:14:37 +02:00
grid: grid,
2019-03-31 19:36:37 +02:00
msg: msg,
sink: pipe,
2019-03-31 18:14:37 +02:00
term: term,
}
store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) {
2019-03-31 18:35:51 +02:00
viewer.source = reader
viewer.attemptCopy()
2019-03-31 18:14:37 +02:00
})
2019-03-30 19:12:04 +01:00
2019-03-31 18:35:51 +02:00
term.OnStart = func() {
viewer.attemptCopy()
}
2019-03-31 18:14:37 +02:00
return viewer
2019-03-30 19:12:04 +01:00
}
2019-03-31 18:35:51 +02:00
func (mv *MessageViewer) attemptCopy() {
if mv.source != nil && mv.cmd.Process != nil {
2019-03-31 19:36:37 +02:00
header := make(message.Header)
header.Set("Content-Transfer-Encoding", mv.msg.BodyStructure.Encoding)
header.SetContentType(
mv.msg.BodyStructure.MIMEType, mv.msg.BodyStructure.Params)
header.SetContentDescription(mv.msg.BodyStructure.Description)
2019-03-31 18:35:51 +02:00
go func() {
2019-03-31 19:36:37 +02:00
entity, err := message.New(header, mv.source)
if err != nil {
io.WriteString(mv.sink, err.Error())
return
}
reader := mail.NewReader(entity)
part, err := reader.NextPart()
if err != nil {
io.WriteString(mv.sink, err.Error())
return
}
io.Copy(mv.sink, part.Body)
2019-03-31 18:35:51 +02:00
mv.sink.Close()
}()
}
}
2019-03-30 19:12:04 +01:00
func (mv *MessageViewer) Draw(ctx *ui.Context) {
mv.grid.Draw(ctx)
}
func (mv *MessageViewer) Invalidate() {
mv.grid.Invalidate()
}
func (mv *MessageViewer) OnInvalidate(fn func(d ui.Drawable)) {
mv.grid.OnInvalidate(func(_ ui.Drawable) {
fn(mv)
})
}
func (mv *MessageViewer) Event(event tcell.Event) bool {
return mv.term.Event(event)
}
func (mv *MessageViewer) Focus(focus bool) {
mv.term.Focus(focus)
}
type HeaderView struct {
onInvalidate func(d ui.Drawable)
Name string
Value string
}
func (hv *HeaderView) Draw(ctx *ui.Context) {
2019-03-31 18:14:37 +02:00
name := hv.Name
size := runewidth.StringWidth(name)
lim := ctx.Width() - size - 1
value := runewidth.Truncate(" "+hv.Value, lim, "…")
2019-03-30 21:50:14 +01:00
var (
hstyle tcell.Style
vstyle tcell.Style
)
// TODO: Make this more robust and less dumb
if hv.Name == "PGP" {
2019-03-30 21:50:14 +01:00
vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
hstyle = tcell.StyleDefault.Bold(true)
} else {
2019-03-30 21:50:14 +01:00
vstyle = tcell.StyleDefault
hstyle = tcell.StyleDefault.Bold(true)
}
2019-03-30 21:50:14 +01:00
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
2019-03-31 18:14:37 +02:00
ctx.Printf(0, 0, hstyle, name)
ctx.Printf(size, 0, vstyle, value)
2019-03-30 19:12:04 +01:00
}
func (hv *HeaderView) Invalidate() {
if hv.onInvalidate != nil {
hv.onInvalidate(hv)
}
}
func (hv *HeaderView) OnInvalidate(fn func(d ui.Drawable)) {
hv.onInvalidate = fn
}
type MultipartView struct {
onInvalidate func(d ui.Drawable)
}
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() {
if mpv.onInvalidate != nil {
mpv.onInvalidate(mpv)
}
}
func (mpv *MultipartView) OnInvalidate(fn func(d ui.Drawable)) {
mpv.onInvalidate = fn
}