fix(graphics): silence anonymous Kitty replies
- commit
- f53f75f19d7f61d09abeaf5d20cb898b25d489e4
- author
- brice · · 2026-08-27 06:09 UTC
- parent
- 03674e0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +22 -2
M protocol/kittygraphics/session.go +8 -2
| @@ -901,7 +901,7 @@ func signedCoordinateInt(n int64, present bool) int64 { | |||
| 901 | 901 | ||
| 902 | 902 | func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte { | |
| 903 | 903 | responses := prior | |
| 904 | - | if c.Quiet != QuietSuccess && c.Quiet != QuietAll { | |
| 904 | + | if responseRequested(c) && c.Quiet != QuietSuccess && c.Quiet != QuietAll { | |
| 905 | 905 | response := makeControlResponse(c, imageID, "OK") | |
| 906 | 906 | if uint64(len(response)) <= s.limits.MaxResponseBytes { | |
| 907 | 907 | responses = append(responses, response) | |
| @@ -910,6 +910,12 @@ func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte | |||
| 910 | 910 | return responses | |
| 911 | 911 | } | |
| 912 | 912 | ||
| 913 | + | // responseRequested matches Kitty's response routing: commands without a | |
| 914 | + | // non-zero image ID or image number are anonymous and never receive replies. | |
| 915 | + | func responseRequested(c Controls) bool { | |
| 916 | + | return c.HasImageID && c.ImageID != 0 || c.HasImageNumber && c.ImageNumber != 0 | |
| 917 | + | } | |
| 918 | + | ||
| 913 | 919 | func makeControlResponse(c Controls, imageID uint64, code string) []byte { | |
| 914 | 920 | if !c.HasImageNumber { | |
| 915 | 921 | return MakeResponse(imageID, code) | |
| @@ -924,7 +930,7 @@ func (s *Session) failure(c Controls, imageID uint64, err error) ([][]byte, *Mut | |||
| 924 | 930 | if err == nil { | |
| 925 | 931 | err = ErrInvalidCommand | |
| 926 | 932 | } | |
| 927 | - | if c.Quiet != QuietAll { | |
| 933 | + | if responseRequested(c) && c.Quiet != QuietAll { | |
| 928 | 934 | code := errorCode(err) | |
| 929 | 935 | response := makeControlResponse(c, imageID, code) | |
| 930 | 936 | if uint64(len(response)) <= s.limits.MaxResponseBytes { | |
| @@ -901,7 +901,7 @@ func signedCoordinateInt(n int64, present bool) int64 { | |||||
| 901 | 901 | ||||
| 902 | func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte { | 902 | func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte { | ||
| 903 | responses := prior | 903 | responses := prior | ||
| 904 | - | if c.Quiet != QuietSuccess && c.Quiet != QuietAll { | 904 | + | if responseRequested(c) && c.Quiet != QuietSuccess && c.Quiet != QuietAll { |
| 905 | response := makeControlResponse(c, imageID, "OK") | 905 | response := makeControlResponse(c, imageID, "OK") | ||
| 906 | if uint64(len(response)) <= s.limits.MaxResponseBytes { | 906 | if uint64(len(response)) <= s.limits.MaxResponseBytes { | ||
| 907 | responses = append(responses, response) | 907 | responses = append(responses, response) | ||
| @@ -910,6 +910,12 @@ func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte | |||||
| 910 | return responses | 910 | return responses | ||
| 911 | } | 911 | } | ||
| 912 | 912 | ||||
| 913 | + | // responseRequested matches Kitty's response routing: commands without a | |||
| 914 | + | // non-zero image ID or image number are anonymous and never receive replies. | |||
| 915 | + | func responseRequested(c Controls) bool { | |||
| 916 | + | return c.HasImageID && c.ImageID != 0 || c.HasImageNumber && c.ImageNumber != 0 | |||
| 917 | + | } | |||
| 918 | + | ||||
| 913 | func makeControlResponse(c Controls, imageID uint64, code string) []byte { | 919 | func makeControlResponse(c Controls, imageID uint64, code string) []byte { | ||
| 914 | if !c.HasImageNumber { | 920 | if !c.HasImageNumber { | ||
| 915 | return MakeResponse(imageID, code) | 921 | return MakeResponse(imageID, code) | ||
| @@ -924,7 +930,7 @@ func (s *Session) failure(c Controls, imageID uint64, err error) ([][]byte, *Mut | |||||
| 924 | if err == nil { | 930 | if err == nil { | ||
| 925 | err = ErrInvalidCommand | 931 | err = ErrInvalidCommand | ||
| 926 | } | 932 | } | ||
| 927 | - | if c.Quiet != QuietAll { | 933 | + | if responseRequested(c) && c.Quiet != QuietAll { |
| 928 | code := errorCode(err) | 934 | code := errorCode(err) | ||
| 929 | response := makeControlResponse(c, imageID, code) | 935 | response := makeControlResponse(c, imageID, code) | ||
| 930 | if uint64(len(response)) <= s.limits.MaxResponseBytes { | 936 | if uint64(len(response)) <= s.limits.MaxResponseBytes { | ||
M screen_graphics_test.go +14 -0
| @@ -43,6 +43,20 @@ func TestScreenKittyGraphicsFragmentedAPCAndResponse(t *testing.T) { | |||
| 43 | 43 | require.Equal(t, uint64(1), snapshot.Usage().Placements) | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | + | func TestScreenKittyGraphicsWithoutImageIDDoesNotRespond(t *testing.T) { | |
| 47 | + | screen := NewScreen(16, 3) | |
| 48 | + | var responses []string | |
| 49 | + | screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) } | |
| 50 | + | payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4}) | |
| 51 | + | ||
| 52 | + | screen.Write([]byte("\x1b_Ga=T,f=32,s=1,v=1,C=1;" + payload + "\x1b\\")) | |
| 53 | + | screen.Write([]byte("\x1b_Ga=T,i=0,f=32,s=1,v=1,C=1;" + payload + "\x1b\\")) | |
| 54 | + | screen.Write([]byte("\x1b_Ga=q,f=24,s=1,v=1;invalid\x1b\\")) | |
| 55 | + | ||
| 56 | + | require.Empty(t, responses) | |
| 57 | + | require.Equal(t, uint64(2), screen.GraphicsSnapshot().Usage().Placements) | |
| 58 | + | } | |
| 59 | + | ||
| 46 | 60 | func TestScreenKittyIcatDetectionResponses(t *testing.T) { | |
| 47 | 61 | screen := NewScreen(80, 24) | |
| 48 | 62 | var responses []string | |
| @@ -43,6 +43,20 @@ func TestScreenKittyGraphicsFragmentedAPCAndResponse(t *testing.T) { | |||||
| 43 | require.Equal(t, uint64(1), snapshot.Usage().Placements) | 43 | require.Equal(t, uint64(1), snapshot.Usage().Placements) | ||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | + | func TestScreenKittyGraphicsWithoutImageIDDoesNotRespond(t *testing.T) { | |||
| 47 | + | screen := NewScreen(16, 3) | |||
| 48 | + | var responses []string | |||
| 49 | + | screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) } | |||
| 50 | + | payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4}) | |||
| 51 | + | ||||
| 52 | + | screen.Write([]byte("\x1b_Ga=T,f=32,s=1,v=1,C=1;" + payload + "\x1b\\")) | |||
| 53 | + | screen.Write([]byte("\x1b_Ga=T,i=0,f=32,s=1,v=1,C=1;" + payload + "\x1b\\")) | |||
| 54 | + | screen.Write([]byte("\x1b_Ga=q,f=24,s=1,v=1;invalid\x1b\\")) | |||
| 55 | + | ||||
| 56 | + | require.Empty(t, responses) | |||
| 57 | + | require.Equal(t, uint64(2), screen.GraphicsSnapshot().Usage().Placements) | |||
| 58 | + | } | |||
| 59 | + | ||||
| 46 | func TestScreenKittyIcatDetectionResponses(t *testing.T) { | 60 | func TestScreenKittyIcatDetectionResponses(t *testing.T) { | ||
| 47 | screen := NewScreen(80, 24) | 61 | screen := NewScreen(80, 24) | ||
| 48 | var responses []string | 62 | var responses []string | ||