@@ -34,6 +34,7 @@ type FilePicker struct {
fp filepicker.Model
help help.Model
previewingImage bool // indicates if an image is being previewed
+ isTmux bool
km struct {
Select,
@@ -108,6 +109,7 @@ func (f *FilePicker) SetImageCapabilities(caps *fimage.Capabilities) {
f.imgEnc = fimage.EncodingKitty
}
f.cellSize = caps.CellSize()
+ _, f.isTmux = caps.Env.LookupEnv("TMUX")
}
}
@@ -184,7 +186,7 @@ func (f *FilePicker) HandleMsg(msg tea.Msg) Action {
img, err := loadImage(selFile)
if err == nil {
cmds = append(cmds, tea.Sequence(
- f.imgEnc.Transmit(selFile, img, f.cellSize, f.imgPrevWidth, f.imgPrevHeight),
+ f.imgEnc.Transmit(selFile, img, f.cellSize, f.imgPrevWidth, f.imgPrevHeight, f.isTmux),
func() tea.Msg {
f.previewingImage = true
return nil
@@ -13,6 +13,7 @@ import (
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/crush/internal/uiutil"
+ uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/ansi/kitty"
"github.com/charmbracelet/x/mosaic"
@@ -33,6 +34,8 @@ type Capabilities struct {
// SupportsKittyGraphics indicates whether the terminal supports the Kitty
// graphics protocol.
SupportsKittyGraphics bool
+ // Env is the terminal environment variables.
+ Env uv.Environ
}
// CellSize returns the size of a single terminal cell in pixels.
@@ -55,12 +58,15 @@ func CalculateCellSize(pixelWidth, pixelHeight, charWidth, charHeight int) CellS
// RequestCapabilities is a [tea.Cmd] that requests the terminal to report
// its image related capabilities to the program.
-func RequestCapabilities() tea.Cmd {
- return tea.Raw(
- ansi.WindowOp(14) + // Window size in pixels
- // ID 31 is just a random ID used to detect Kitty graphics support.
- ansi.KittyGraphics([]byte("AAAA"), "i=31", "s=1", "v=1", "a=q", "t=d", "f=24"),
- )
+func RequestCapabilities(env uv.Environ) tea.Cmd {
+ winOpReq := ansi.WindowOp(14) // Window size in pixels
+ // ID 31 is just a random ID used to detect Kitty graphics support.
+ kittyReq := ansi.KittyGraphics([]byte("AAAA"), "i=31", "s=1", "v=1", "a=q", "t=d", "f=24")
+ if _, isTmux := env.LookupEnv("TMUX"); isTmux {
+ kittyReq = ansi.TmuxPassthrough(kittyReq)
+ }
+
+ return tea.Raw(winOpReq + kittyReq)
}
// TransmittedMsg is a message indicating that an image has been transmitted to
@@ -161,7 +167,7 @@ func HasTransmitted(id string, cols, rows int) bool {
// Transmit transmits the image data to the terminal if needed. This is used to
// cache the image on the terminal for later rendering.
-func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows int) tea.Cmd {
+func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows int, tmux bool) tea.Cmd {
if img == nil {
return nil
}
@@ -192,7 +198,6 @@ func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows i
bounds := img.Bounds()
imgWidth := bounds.Dx()
imgHeight := bounds.Dy()
-
imgID := int(key.Hash())
if err := kitty.EncodeGraphics(&buf, img, &kitty.Options{
ID: imgID,
@@ -205,9 +210,19 @@ func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows i
Rows: rows,
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()}
@@ -1,6 +1,7 @@
package model
import (
+ "bytes"
"context"
"errors"
"fmt"
@@ -288,11 +289,6 @@ func (m *UI) Init() tea.Cmd {
var cmds []tea.Cmd
if m.QueryVersion {
cmds = append(cmds, tea.RequestTerminalVersion)
- // XXX: Right now, we're using the same logic to determine image
- // support. Terminals like Apple Terminal and possibly others might
- // bleed characters when querying for Kitty graphics via APC escape
- // sequences.
- cmds = append(cmds, timage.RequestCapabilities())
}
// load the user commands async
cmds = append(cmds, m.loadCustomCommands())
@@ -341,6 +337,12 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if !m.sendProgressBar {
m.sendProgressBar = slices.Contains(msg, "WT_SESSION")
}
+ m.imgCaps.Env = uv.Environ(msg)
+ // XXX: Right now, we're using the same logic to determine image
+ // support. Terminals like Apple Terminal and possibly others might
+ // bleed characters when querying for Kitty graphics via APC escape
+ // sequences.
+ cmds = append(cmds, timage.RequestCapabilities(m.imgCaps.Env))
case loadSessionMsg:
m.state = uiChat
if m.forceCompactMode {
@@ -620,6 +622,11 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// captures the response. Any response means the terminal understands
// the protocol.
m.imgCaps.SupportsKittyGraphics = true
+ if !bytes.HasPrefix(msg.Payload, []byte("OK")) {
+ slog.Warn("unexpected Kitty graphics response",
+ "response", string(msg.Payload),
+ "options", msg.Options)
+ }
default:
if m.dialog.HasDialogs() {
if cmd := m.handleDialogMsg(msg); cmd != nil {