5685a17674
Many Drawable implementations have their own Invalidate and OnInvalidate functions, with an unexported onInvalidate field. However OnInvalidate and Invalidate are usually not called in the same goroutine. This results in a race on this field, e.g.: Read at 0x00c000094748 by goroutine 7: git.sr.ht/~sircmpwn/aerc2/widgets.NewDirectoryList.func1() /home/simon/src/aerc2/widgets/dirlist.go:85 +0x56 git.sr.ht/~sircmpwn/aerc2/widgets.(*Spinner).Start.func1() /home/simon/src/aerc2/widgets/spinner.go:93 +0x1bb Previous write at 0x00c000094748 by main goroutine: [failed to restore the stack] Goroutine 7 (running) created at: git.sr.ht/~sircmpwn/aerc2/widgets.(*Spinner).Start() /home/simon/src/aerc2/widgets/spinner.go:46 +0x8f git.sr.ht/~sircmpwn/aerc2/widgets.NewDirectoryList() /home/simon/src/aerc2/widgets/dirlist.go:37 +0x286 git.sr.ht/~sircmpwn/aerc2/widgets.NewAccountView() /home/simon/src/aerc2/widgets/account.go:50 +0x5ca git.sr.ht/~sircmpwn/aerc2/widgets.NewAerc() /home/simon/src/aerc2/widgets/aerc.go:60 +0x800 main.main() /home/simon/src/aerc2/aerc.go:65 +0x33e To fix this, introduce a new type, Invalidatable, which protects the field. Unfortunately the Drawable must be passed to the callback function in Invalidate, so we still need to re-implement this in each Invalidatable user.
300 lines
6.6 KiB
Go
300 lines
6.6 KiB
Go
package widgets
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/danwakefield/fnmatch"
|
|
"github.com/emersion/go-imap"
|
|
"github.com/emersion/go-message"
|
|
"github.com/emersion/go-message/mail"
|
|
"github.com/gdamore/tcell"
|
|
"github.com/google/shlex"
|
|
"github.com/mattn/go-runewidth"
|
|
|
|
"git.sr.ht/~sircmpwn/aerc2/config"
|
|
"git.sr.ht/~sircmpwn/aerc2/lib"
|
|
"git.sr.ht/~sircmpwn/aerc2/lib/ui"
|
|
"git.sr.ht/~sircmpwn/aerc2/worker/types"
|
|
)
|
|
|
|
type MessageViewer struct {
|
|
conf *config.AercConfig
|
|
err error
|
|
filter *exec.Cmd
|
|
msg *types.MessageInfo
|
|
pager *exec.Cmd
|
|
source io.Reader
|
|
pagerin io.WriteCloser
|
|
sink io.WriteCloser
|
|
grid *ui.Grid
|
|
term *Terminal
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
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
|
|
{ui.SIZE_WEIGHT, 1},
|
|
}).Columns([]ui.GridSpec{
|
|
{ui.SIZE_WEIGHT, 1},
|
|
})
|
|
|
|
// TODO: let user specify additional headers to show by default
|
|
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",
|
|
Value: formatAddresses(msg.Envelope.From),
|
|
}).At(0, 0)
|
|
headers.AddChild(
|
|
&HeaderView{
|
|
Name: "To",
|
|
Value: formatAddresses(msg.Envelope.To),
|
|
}).At(0, 1)
|
|
headers.AddChild(
|
|
&HeaderView{
|
|
Name: "Subject",
|
|
Value: msg.Envelope.Subject,
|
|
}).At(1, 0).Span(1, 2)
|
|
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},
|
|
})
|
|
|
|
var (
|
|
filter *exec.Cmd
|
|
pager *exec.Cmd
|
|
pipe io.WriteCloser
|
|
pagerin io.WriteCloser
|
|
term *Terminal
|
|
viewer *MessageViewer
|
|
)
|
|
cmd, err := shlex.Split(conf.Viewer.Pager)
|
|
if err != nil {
|
|
goto handle_error
|
|
}
|
|
pager = exec.Command(cmd[0], cmd[1:]...)
|
|
|
|
for _, f := range conf.Filters {
|
|
mime := strings.ToLower(msg.BodyStructure.MIMEType) + "/" + strings.ToLower(msg.BodyStructure.MIMESubType)
|
|
switch f.FilterType {
|
|
case config.FILTER_MIMETYPE:
|
|
if fnmatch.Match(f.Filter, mime, 0) {
|
|
filter = exec.Command("sh", "-c", f.Command)
|
|
}
|
|
case config.FILTER_HEADER:
|
|
var header string
|
|
switch f.Header {
|
|
case "subject":
|
|
header = msg.Envelope.Subject
|
|
case "from":
|
|
header = formatAddresses(msg.Envelope.From)
|
|
case "to":
|
|
header = formatAddresses(msg.Envelope.To)
|
|
case "cc":
|
|
header = formatAddresses(msg.Envelope.Cc)
|
|
}
|
|
if f.Regex.Match([]byte(header)) {
|
|
filter = exec.Command("sh", "-c", f.Command)
|
|
}
|
|
}
|
|
if filter != nil {
|
|
break
|
|
}
|
|
}
|
|
if filter != nil {
|
|
pipe, _ = filter.StdinPipe()
|
|
pagerin, _ = pager.StdinPipe()
|
|
} else {
|
|
pipe, _ = pager.StdinPipe()
|
|
}
|
|
|
|
term, _ = NewTerminal(pager)
|
|
// 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{
|
|
filter: filter,
|
|
grid: grid,
|
|
msg: msg,
|
|
pager: pager,
|
|
pagerin: pagerin,
|
|
sink: pipe,
|
|
term: term,
|
|
}
|
|
|
|
store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) {
|
|
viewer.source = reader
|
|
viewer.attemptCopy()
|
|
})
|
|
|
|
term.OnStart = func() {
|
|
viewer.attemptCopy()
|
|
}
|
|
|
|
return viewer
|
|
|
|
handle_error:
|
|
viewer = &MessageViewer{
|
|
err: err,
|
|
grid: grid,
|
|
msg: msg,
|
|
}
|
|
return viewer
|
|
}
|
|
|
|
func (mv *MessageViewer) attemptCopy() {
|
|
if mv.source != nil && mv.pager.Process != nil {
|
|
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)
|
|
if mv.filter != nil {
|
|
stdout, _ := mv.filter.StdoutPipe()
|
|
mv.filter.Start()
|
|
go func() {
|
|
_, err := io.Copy(mv.pagerin, stdout)
|
|
if err != nil {
|
|
mv.err = err
|
|
mv.Invalidate()
|
|
}
|
|
mv.pagerin.Close()
|
|
stdout.Close()
|
|
}()
|
|
}
|
|
go func() {
|
|
entity, err := message.New(header, mv.source)
|
|
if err != nil {
|
|
mv.err = err
|
|
mv.Invalidate()
|
|
return
|
|
}
|
|
reader := mail.NewReader(entity)
|
|
part, err := reader.NextPart()
|
|
if err != nil {
|
|
mv.err = err
|
|
mv.Invalidate()
|
|
return
|
|
}
|
|
io.Copy(mv.sink, part.Body)
|
|
mv.sink.Close()
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
|
if mv.err != nil {
|
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
|
ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error())
|
|
return
|
|
}
|
|
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 {
|
|
if mv.term != nil {
|
|
return mv.term.Event(event)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (mv *MessageViewer) Focus(focus bool) {
|
|
if mv.term != nil {
|
|
mv.term.Focus(focus)
|
|
}
|
|
}
|
|
|
|
type HeaderView struct {
|
|
ui.Invalidatable
|
|
Name string
|
|
Value string
|
|
}
|
|
|
|
func (hv *HeaderView) Draw(ctx *ui.Context) {
|
|
name := hv.Name
|
|
size := runewidth.StringWidth(name)
|
|
lim := ctx.Width() - size - 1
|
|
value := runewidth.Truncate(" "+hv.Value, lim, "…")
|
|
var (
|
|
hstyle tcell.Style
|
|
vstyle tcell.Style
|
|
)
|
|
// TODO: Make this more robust and less dumb
|
|
if hv.Name == "PGP" {
|
|
vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
|
|
hstyle = tcell.StyleDefault.Bold(true)
|
|
} else {
|
|
vstyle = tcell.StyleDefault
|
|
hstyle = tcell.StyleDefault.Bold(true)
|
|
}
|
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
|
|
ctx.Printf(0, 0, hstyle, name)
|
|
ctx.Printf(size, 0, vstyle, value)
|
|
}
|
|
|
|
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)
|
|
}
|