@@ -2,6 +2,7 @@ package image
22
33import (
44 "bytes"
5+ "errors"
56 "fmt"
67 "hash/fnv"
78 "image"
@@ -13,6 +14,7 @@ import (
1314
1415 tea "charm.land/bubbletea/v2"
1516 "github.com/charmbracelet/crush/internal/uiutil"
17+ uv "github.com/charmbracelet/ultraviolet"
1618 "github.com/charmbracelet/x/ansi"
1719 "github.com/charmbracelet/x/ansi/kitty"
1820 "github.com/charmbracelet/x/mosaic"
@@ -33,6 +35,8 @@ type Capabilities struct {
3335 // SupportsKittyGraphics indicates whether the terminal supports the Kitty
3436 // graphics protocol.
3537 SupportsKittyGraphics bool
38+ // Env is the terminal environment variables.
39+ Env uv.Environ
3640}
3741
3842// CellSize returns the size of a single terminal cell in pixels.
@@ -55,12 +59,15 @@ func CalculateCellSize(pixelWidth, pixelHeight, charWidth, charHeight int) CellS
5559
5660// RequestCapabilities is a [tea.Cmd] that requests the terminal to report
5761// its image related capabilities to the program.
58−func RequestCapabilities() tea.Cmd {
59− return tea.Raw(
60− ansi.WindowOp(14) + // Window size in pixels
61− // ID 31 is just a random ID used to detect Kitty graphics support.
62− ansi.KittyGraphics([]byte("AAAA"), "i=31", "s=1", "v=1", "a=q", "t=d", "f=24"),
63− )
62+func RequestCapabilities(env uv.Environ) tea.Cmd {
63+ winOpReq := ansi.WindowOp(14) // Window size in pixels
64+ // ID 31 is just a random ID used to detect Kitty graphics support.
65+ kittyReq := ansi.KittyGraphics([]byte("AAAA"), "i=31", "s=1", "v=1", "a=q", "t=d", "f=24")
66+ if _, isTmux := env.LookupEnv("TMUX"); isTmux {
67+ kittyReq = ansi.TmuxPassthrough(kittyReq)
68+ }
69+
70+ return tea.Raw(winOpReq + kittyReq)
6471}
6572
6673// TransmittedMsg is a message indicating that an image has been transmitted to
@@ -161,7 +168,7 @@ func HasTransmitted(id string, cols, rows int) bool {
161168
162169// Transmit transmits the image data to the terminal if needed. This is used to
163170// cache the image on the terminal for later rendering.
164−func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows int) tea.Cmd {
171+func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows int, tmux bool) tea.Cmd {
165172 if img == nil {
166173 return nil
167174 }
@@ -188,13 +195,50 @@ func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows i
188195 }
189196
190197 var buf bytes.Buffer
198+ rp, wp := io.Pipe()
199+ go func() {
200+ for {
201+ // Read single Kitty graphic chunks from the pipe and wrap them
202+ // for tmux if needed.
203+ var out bytes.Buffer
204+ seenEsc := false
205+ for {
206+ var p [1]byte
207+ n, err := rp.Read(p[:])
208+ if n > 0 {
209+ out.WriteByte(p[0])
210+ if p[0] == ansi.ESC {
211+ seenEsc = true
212+ } else if seenEsc && p[0] == '\\' {
213+ // End of Kitty graphics sequence
214+ break
215+ } else {
216+ seenEsc = false
217+ }
218+ }
219+ if err != nil {
220+ if !errors.Is(err, io.EOF) {
221+ slog.Error("error reading from pipe", "err", err)
222+ }
223+ return
224+ }
225+ }
226+
227+ seq := out.String()
228+ if tmux {
229+ seq = ansi.TmuxPassthrough(seq)
230+ }
231+
232+ buf.WriteString(seq)
233+ }
234+ }()
235+
191236 img := fitImage(id, img, cs, cols, rows)
192237 bounds := img.Bounds()
193238 imgWidth := bounds.Dx()
194239 imgHeight := bounds.Dy()
195−
196240 imgID := int(key.Hash())
197− if err := kitty.EncodeGraphics(&buf, img, &kitty.Options{
241+ if err := kitty.EncodeGraphics(wp, img, &kitty.Options{
198242 ID: imgID,
199243 Action: kitty.TransmitAndPut,
200244 Transmission: kitty.Direct,
@@ -205,6 +249,7 @@ func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows i
205249 Rows: rows,
206250 VirtualPlacement: true,
207251 Quite: 1,
252+ Chunk: true,
208253 }); err != nil {
209254 slog.Error("failed to encode image for kitty graphics", "err", err)
210255 return uiutil.ReportError(fmt.Errorf("failed to encode image"))