@@ -6,9 +6,12 @@ import (
66 "encoding/base64"
77 "fmt"
88 "image"
9+ "io"
10+ "os"
911 "strconv"
1012 "strings"
1113 "testing"
14+ "unicode/utf8"
1215
1316 "github.com/stretchr/testify/assert"
1417)
@@ -36,6 +39,51 @@ func extractFirstKittyImageID(output string) (uint32, error) {
3639 return uint32 (id ), nil
3740}
3841
42+ func captureStdout (t * testing.T , fn func () error ) (string , error ) {
43+ t .Helper ()
44+
45+ oldStdout := os .Stdout
46+ readPipe , writePipe , err := os .Pipe ()
47+ if err != nil {
48+ return "" , fmt .Errorf ("failed to create stdout pipe: %w" , err )
49+ }
50+
51+ os .Stdout = writePipe
52+ var runErr error
53+ var panicValue any
54+ func () {
55+ defer func () {
56+ panicValue = recover ()
57+ }()
58+ runErr = fn ()
59+ }()
60+
61+ os .Stdout = oldStdout
62+ closeErr := writePipe .Close ()
63+ if closeErr != nil {
64+ _ = readPipe .Close ()
65+ if panicValue != nil {
66+ panic (panicValue )
67+ }
68+ return "" , fmt .Errorf ("failed to close write pipe: %w" , closeErr )
69+ }
70+
71+ data , readErr := io .ReadAll (readPipe )
72+ _ = readPipe .Close ()
73+ if readErr != nil {
74+ if panicValue != nil {
75+ panic (panicValue )
76+ }
77+ return "" , fmt .Errorf ("failed to read stdout pipe: %w" , readErr )
78+ }
79+
80+ if panicValue != nil {
81+ panic (panicValue )
82+ }
83+
84+ return string (data ), runErr
85+ }
86+
3987func TestKittyZlibCompression (t * testing.T ) {
4088 img := image .NewRGBA (image .Rect (0 , 0 , 1 , 1 ))
4189 opts := RenderOptions {
@@ -170,7 +218,7 @@ func TestKittyUnicodeHonorsImageNumber(t *testing.T) {
170218 assert .Equal (t , uint32 (42 ), renderer .GetLastImageID (), "last image ID should match caller-provided Unicode image number" )
171219}
172220
173- func TestKittyUnicodeUsesPngTransferWithPlacementCommand (t * testing.T ) {
221+ func TestKittyUnicodeUsesRawTransferWithPlacementCommandByDefault (t * testing.T ) {
174222 img := image .NewRGBA (image .Rect (0 , 0 , 16 , 16 ))
175223 opts := RenderOptions {
176224 KittyOpts : & KittyOptions {
@@ -186,10 +234,51 @@ func TestKittyUnicodeUsesPngTransferWithPlacementCommand(t *testing.T) {
186234 renderer := & KittyRenderer {}
187235 output , err := renderer .Render (img , opts )
188236 assert .NoError (t , err )
189- assert .Contains (t , output , "f=100, t=d,i=42" , "Unicode path should transmit image data using PNG transfer " )
237+ assert .Contains (t , output , "f=32,s=16,v=16, t=d,i=42" , "Unicode path should transmit image data using raw RGBA by default " )
190238 assert .Contains (t , output , "a=p,U=1,i=42" , "Unicode path should emit explicit virtual placement command" )
191239}
192240
241+ func TestKittyUnicodeUsesPngTransferWhenRequested (t * testing.T ) {
242+ img := image .NewRGBA (image .Rect (0 , 0 , 16 , 16 ))
243+ opts := RenderOptions {
244+ KittyOpts : & KittyOptions {
245+ UseUnicode : true ,
246+ ImageNum : 42 ,
247+ PNG : true ,
248+ },
249+ features : & TerminalFeatures {
250+ FontWidth : 8 ,
251+ FontHeight : 16 ,
252+ },
253+ }
254+
255+ renderer := & KittyRenderer {}
256+ output , err := renderer .Render (img , opts )
257+ assert .NoError (t , err )
258+ assert .Contains (t , output , "f=100,t=d,i=42" , "Unicode path should transmit PNG when explicitly requested" )
259+ assert .Contains (t , output , "a=p,U=1,i=42" , "Unicode path should emit explicit virtual placement command" )
260+ }
261+
262+ func TestKittyVirtualWithoutUnicodeDoesNotEmitPlaceholders (t * testing.T ) {
263+ img := image .NewRGBA (image .Rect (0 , 0 , 16 , 16 ))
264+ opts := RenderOptions {
265+ Virtual : true ,
266+ KittyOpts : & KittyOptions {
267+ UseUnicode : false ,
268+ },
269+ features : & TerminalFeatures {
270+ FontWidth : 8 ,
271+ FontHeight : 16 ,
272+ },
273+ }
274+
275+ renderer := & KittyRenderer {}
276+ output , err := renderer .Render (img , opts )
277+ assert .NoError (t , err )
278+ assert .Contains (t , output , "U=1" , "virtual transfer should still create a virtual placement" )
279+ assert .NotContains (t , output , PLACEHOLDER_CHAR , "non-unicode virtual transfer should not append placeholder text" )
280+ }
281+
193282func TestProcessImageUnicodeHonorsExplicitResize (t * testing.T ) {
194283 img := image .NewRGBA (image .Rect (0 , 0 , 20 , 20 ))
195284 opts := RenderOptions {
@@ -278,6 +367,15 @@ func TestRenderPlaceholderAreaWithImageIDUsesTruecolorForLowIDs(t *testing.T) {
278367 assert .NotContains (t , rendered , "\x1b [38;5;1m" , "palette mode should not be used for ID encoding" )
279368}
280369
370+ func TestRenderPlaceholderAreaWithImageIDUsesInheritedPlaceholders (t * testing.T ) {
371+ area := CreatePlaceholderArea (1 , 1 , 3 )
372+ rendered := RenderPlaceholderAreaWithImageID (area , 1 )
373+
374+ assert .Contains (t , rendered , CreatePlaceholder (0 , 0 , 0 )+ PLACEHOLDER_CHAR + PLACEHOLDER_CHAR )
375+ assert .NotContains (t , rendered , CreatePlaceholder (0 , 1 , 0 ))
376+ assert .NotContains (t , rendered , CreatePlaceholder (0 , 2 , 0 ))
377+ }
378+
281379func TestRenderAnchoredPlaceholderAreaPositionsEveryRow (t * testing.T ) {
282380 rendered := renderAnchoredPlaceholderArea (1 , 5 , 5 , 2 , 3 )
283381 assert .Contains (t , rendered , "\x1b [6;6H" )
@@ -287,7 +385,38 @@ func TestRenderAnchoredPlaceholderAreaPositionsEveryRow(t *testing.T) {
287385 assert .Equal (t , 3 , strings .Count (rendered , "\x1b [39m" ))
288386}
289387
388+ func TestRenderAnchoredPlaceholderAreaUsesInheritedPlaceholders (t * testing.T ) {
389+ rendered := renderAnchoredPlaceholderArea (1 , 0 , 0 , 3 , 1 )
390+
391+ assert .Contains (t , rendered , CreatePlaceholder (0 , 0 , 0 )+ PLACEHOLDER_CHAR + PLACEHOLDER_CHAR )
392+ assert .NotContains (t , rendered , CreatePlaceholder (0 , 1 , 0 ))
393+ assert .NotContains (t , rendered , CreatePlaceholder (0 , 2 , 0 ))
394+ }
395+
290396func TestRenderAnchoredPlaceholderAreaEmptyWhenInvalidDimensions (t * testing.T ) {
291397 assert .Equal (t , "" , renderAnchoredPlaceholderArea (1 , 0 , 0 , 0 , 3 ))
292398 assert .Equal (t , "" , renderAnchoredPlaceholderArea (1 , 0 , 0 , 3 , 0 ))
293399}
400+
401+ func TestPlaceImageWithSizeDoesNotWrapPlaceholderTextInTmuxPassthrough (t * testing.T ) {
402+ ForceTmux (true )
403+ defer ForceTmux (false )
404+
405+ renderer := & KittyRenderer {}
406+ output , err := captureStdout (t , func () error {
407+ return renderer .PlaceImageWithSize ("42" , 1 , 2 , 0 , 2 , 1 )
408+ })
409+ assert .NoError (t , err )
410+ assert .Contains (t , output , "\x1b [3;2H" , "place command should move cursor to requested absolute coordinates" )
411+ assert .NotContains (t , output , "\x1b Ptmux;\x1b " , "placeholder text should remain in-band and not be tmux passthrough wrapped" )
412+ }
413+
414+ func TestCreatePlaceholderIncludesExtraDiacriticFor24BitIDs (t * testing.T ) {
415+ placeholder := CreatePlaceholder (1 , 2 , 0 )
416+ assert .Equal (t , 4 , utf8 .RuneCountInString (placeholder ), "24-bit IDs should still include the extra-id diacritic" )
417+ }
418+
419+ func TestCreatePlaceholderIncludesExtraDiacriticFor32BitIDs (t * testing.T ) {
420+ placeholder := CreatePlaceholder (1 , 2 , 1 )
421+ assert .Equal (t , 4 , utf8 .RuneCountInString (placeholder ), "32-bit IDs should include the high-byte diacritic" )
422+ }
0 commit comments