fix(ui): filepicker: simplify tmux kitty image encoding

Ayman Bagabas created

Change summary

go.mod                     |  4 +-
go.sum                     |  8 +++---
internal/ui/image/image.go | 52 ++++++++-------------------------------
3 files changed, 17 insertions(+), 47 deletions(-)

Detailed changes

go.mod 🔗

@@ -23,7 +23,7 @@ require (
 	github.com/charmbracelet/colorprofile v0.4.1
 	github.com/charmbracelet/fang v0.4.4
 	github.com/charmbracelet/ultraviolet v0.0.0-20251212194010-b927aa605560
-	github.com/charmbracelet/x/ansi v0.11.3
+	github.com/charmbracelet/x/ansi v0.11.4
 	github.com/charmbracelet/x/editor v0.2.0
 	github.com/charmbracelet/x/etag v0.2.0
 	github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
@@ -104,7 +104,7 @@ require (
 	github.com/charmbracelet/x/json v0.2.0 // indirect
 	github.com/charmbracelet/x/termios v0.1.1 // indirect
 	github.com/charmbracelet/x/windows v0.2.2 // indirect
-	github.com/clipperhouse/displaywidth v0.6.2 // indirect
+	github.com/clipperhouse/displaywidth v0.7.0 // indirect
 	github.com/clipperhouse/stringish v0.1.1 // indirect
 	github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect

go.sum 🔗

@@ -104,8 +104,8 @@ github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1Yk
 github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
 github.com/charmbracelet/ultraviolet v0.0.0-20251212194010-b927aa605560 h1:j3PW2hypGoPKBy3ooKzW0TFxaxhyHK3NbkLLn4KeRFc=
 github.com/charmbracelet/ultraviolet v0.0.0-20251212194010-b927aa605560/go.mod h1:VWATWLRwYP06VYCEur7FsNR2B1xAo7Y+xl1PTbd1ePc=
-github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
-github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
+github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
+github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
 github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIRg8gGWwk=
 github.com/charmbracelet/x/editor v0.2.0/go.mod h1:p3oQ28TSL3YPd+GKJ1fHWcp+7bVGpedHpXmo0D6t1dY=
 github.com/charmbracelet/x/etag v0.2.0 h1:Euj1VkheoHfTYA9y+TCwkeXF/hN8Fb9l4LqZl79pt04=
@@ -130,8 +130,8 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
 github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
 github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
-github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
-github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
+github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
+github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
 github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
 github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
 github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=

internal/ui/image/image.go 🔗

@@ -2,7 +2,6 @@ package image
 
 import (
 	"bytes"
-	"errors"
 	"fmt"
 	"hash/fnv"
 	"image"
@@ -195,50 +194,12 @@ func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows i
 		}
 
 		var buf bytes.Buffer
-		rp, wp := io.Pipe()
-		go func() {
-			for {
-				// Read single Kitty graphic chunks from the pipe and wrap them
-				// for tmux if needed.
-				var out bytes.Buffer
-				seenEsc := false
-				for {
-					var p [1]byte
-					n, err := rp.Read(p[:])
-					if n > 0 {
-						out.WriteByte(p[0])
-						if p[0] == ansi.ESC {
-							seenEsc = true
-						} else if seenEsc && p[0] == '\\' {
-							// End of Kitty graphics sequence
-							break
-						} else {
-							seenEsc = false
-						}
-					}
-					if err != nil {
-						if !errors.Is(err, io.EOF) {
-							slog.Error("error reading from pipe", "err", err)
-						}
-						return
-					}
-				}
-
-				seq := out.String()
-				if tmux {
-					seq = ansi.TmuxPassthrough(seq)
-				}
-
-				buf.WriteString(seq)
-			}
-		}()
-
 		img := fitImage(id, img, cs, cols, rows)
 		bounds := img.Bounds()
 		imgWidth := bounds.Dx()
 		imgHeight := bounds.Dy()
 		imgID := int(key.Hash())
-		if err := kitty.EncodeGraphics(wp, img, &kitty.Options{
+		if err := kitty.EncodeGraphics(&buf, img, &kitty.Options{
 			ID:               imgID,
 			Action:           kitty.TransmitAndPut,
 			Transmission:     kitty.Direct,
@@ -250,9 +211,18 @@ func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows i
 			VirtualPlacement: true,
 			Quite:            1,
 			Chunk:            true,
+			ChunkFormatter: func(chunk string) string {
+				if tmux {
+					return ansi.TmuxPassthrough(chunk)
+				}
+				return chunk
+			},
 		}); err != nil {
 			slog.Error("failed to encode image for kitty graphics", "err", err)
-			return uiutil.ReportError(fmt.Errorf("failed to encode image"))
+			return uiutil.InfoMsg{
+				Type: uiutil.InfoTypeError,
+				Msg:  "failed to encode image",
+			}
 		}
 
 		return tea.RawMsg{Msg: buf.String()}