aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorTim Culverhouse <tim@timculverhouse.com>2024-02-12 06:26:14 -0600
committerRobin Jarry <robin@jarry.cc>2024-02-12 13:47:29 +0100
commit4e26faf498b84b90ecd1af3bd6c6e1f2f3e44a6e (patch)
tree8e04d288683b220080cbae64e420e4e839c6c66d /app
parent0ef4717b2a0fad1ee0a6426efa77325affaf566b (diff)
downloadaerc-4e26faf498b84b90ecd1af3bd6c6e1f2f3e44a6e.tar.gz
msgviewer: implement inline image viewing
Implement inline image viewing for jpeg, png, bmp, tiff, and webp formats. When a user has no configured image filter and the image is supported and the terminal has either sixel or kitty image protocol support, the image will be displayed in the message viewer. Always clear the screen before each draw. This call is necessary in vaxis to allow for images to be cleared properly between renders. There is no performance impact: the call only resets each cell to a blank cell, and aerc will redraw each one already. Changelog-added: Inline image previews when no filter is defined for `image/*` and the terminal supports it. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
Diffstat (limited to 'app')
-rw-r--r--app/msgviewer.go101
1 files changed, 99 insertions, 2 deletions
diff --git a/app/msgviewer.go b/app/msgviewer.go
index e45a6d3e..8d2e380a 100644
--- a/app/msgviewer.go
+++ b/app/msgviewer.go
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
+ "image"
"io"
"os"
"os/exec"
@@ -24,8 +25,28 @@ import (
"git.sr.ht/~rjarry/aerc/log"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/go-opt"
+ "git.sr.ht/~rockorager/vaxis"
+ "git.sr.ht/~rockorager/vaxis/widgets/align"
+
+ // Image support
+ _ "image/jpeg"
+ _ "image/png"
+
+ _ "golang.org/x/image/bmp"
+ _ "golang.org/x/image/tiff"
+ _ "golang.org/x/image/webp"
)
+// All imported image types need to be explicitly stated here. We want to check
+// if we _can_ display something before we download it
+var supportedImageTypes = []string{
+ "image/jpeg",
+ "image/png",
+ "image/bmp",
+ "image/tiff",
+ "image/webp",
+}
+
var _ ProvidesMessages = (*MessageViewer)(nil)
type MessageViewer struct {
@@ -405,6 +426,11 @@ type PartViewer struct {
noFilter *ui.Grid
uiConfig *config.UIConfig
copying int32
+ inlineImg bool
+ image image.Image
+ graphic vaxis.Image
+ width int
+ height int
links []string
}
@@ -535,7 +561,27 @@ func NewPartViewer(
func (pv *PartViewer) SetSource(reader io.Reader) {
pv.source = reader
- pv.attemptCopy()
+ switch pv.inlineImg {
+ case true:
+ pv.decodeImage()
+ default:
+ pv.attemptCopy()
+ }
+}
+
+func (pv *PartViewer) decodeImage() {
+ atomic.StoreInt32(&pv.copying, copying)
+ go func() {
+ defer log.PanicHandler()
+ defer pv.Invalidate()
+ defer atomic.StoreInt32(&pv.copying, 0)
+ img, _, err := image.Decode(pv.source)
+ if err != nil {
+ log.Errorf("error decoding image: %v", err)
+ return
+ }
+ pv.image = img
+ }()
}
func (pv *PartViewer) attemptCopy() {
@@ -711,7 +757,13 @@ func (pv *PartViewer) Invalidate() {
func (pv *PartViewer) Draw(ctx *ui.Context) {
style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT)
- if pv.filter == nil {
+ switch {
+ case pv.filter == nil && canInline(pv.part.FullMIMEType()) && pv.err == nil:
+ pv.inlineImg = true
+ case pv.filter == nil:
+ // No filter, can't inline, and/or we attempted to inline an image
+ // and resulted in an error (maybe because of a bad encoding or
+ // the terminal doesn't support any graphics protocol).
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
pv.noFilter.Draw(ctx)
return
@@ -728,12 +780,48 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
if pv.term != nil {
pv.term.Draw(ctx)
}
+ if pv.image != nil && (pv.resized(ctx) || pv.graphic == nil) {
+ // This path should only occur on resizes or the first pass
+ // after the image is downloaded and could be slow due to
+ // encoding the image to either sixel or uploading via the kitty
+ // protocol. Generally it's pretty fast since we will only ever
+ // be downsizing images
+ vx := ctx.Window().Vx
+ if pv.graphic == nil {
+ var err error
+ pv.graphic, err = vx.NewImage(pv.image)
+ if err != nil {
+ log.Errorf("Couldn't create image: %v", err)
+ return
+ }
+ }
+ pv.graphic.Resize(pv.width, pv.height)
+ }
+ if pv.graphic != nil {
+ w, h := pv.graphic.CellSize()
+ win := align.Center(ctx.Window(), w, h)
+ pv.graphic.Draw(win)
+ }
}
func (pv *PartViewer) Cleanup() {
if pv.term != nil {
pv.term.Close()
}
+ if pv.graphic != nil {
+ pv.graphic.Destroy()
+ }
+}
+
+func (pv *PartViewer) resized(ctx *ui.Context) bool {
+ w := ctx.Width()
+ h := ctx.Height()
+ if pv.width != w || pv.height != h {
+ pv.width = w
+ pv.height = h
+ return true
+ }
+ return false
}
func (pv *PartViewer) Event(event tcell.Event) bool {
@@ -779,3 +867,12 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
func (hv *HeaderView) Invalidate() {
ui.Invalidate()
}
+
+func canInline(mime string) bool {
+ for _, ext := range supportedImageTypes {
+ if mime == ext {
+ return true
+ }
+ }
+ return false
+}