detached f53f75f

fix(graphics): silence anonymous Kitty replies

commit
f53f75f19d7f61d09abeaf5d20cb898b25d489e4
author
brice · · 2026-08-27 06:09 UTC
parent
03674e0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
901901
902902 func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte {
903903 responses := prior
904- if c.Quiet != QuietSuccess && c.Quiet != QuietAll {
904+ if responseRequested(c) && c.Quiet != QuietSuccess && c.Quiet != QuietAll {
905905 response := makeControlResponse(c, imageID, "OK")
906906 if uint64(len(response)) <= s.limits.MaxResponseBytes {
907907 responses = append(responses, response)
@@ -910,6 +910,12 @@ func (s *Session) success(c Controls, imageID uint64, prior ...[]byte) [][]byte
910910 return responses
911911 }
912912
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+
913919 func makeControlResponse(c Controls, imageID uint64, code string) []byte {
914920 if !c.HasImageNumber {
915921 return MakeResponse(imageID, code)
@@ -924,7 +930,7 @@ func (s *Session) failure(c Controls, imageID uint64, err error) ([][]byte, *Mut
924930 if err == nil {
925931 err = ErrInvalidCommand
926932 }
927- if c.Quiet != QuietAll {
933+ if responseRequested(c) && c.Quiet != QuietAll {
928934 code := errorCode(err)
929935 response := makeControlResponse(c, imageID, code)
930936 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 := prior903 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 responses910 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 = ErrInvalidCommand931 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) {
4343 require.Equal(t, uint64(1), snapshot.Usage().Placements)
4444 }
4545
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+
4660 func TestScreenKittyIcatDetectionResponses(t *testing.T) {
4761 screen := NewScreen(80, 24)
4862 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 []string62 var responses []string

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close