From f6000e5d6013e50e8b1d5dd77f1c26d66559c66b Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 13 Jan 2025 16:04:29 +0300 Subject: [PATCH] feat(input): support kitty graphics responses This adds support for parsing Kitty Graphics Protocol responses. This is useful for applications that want to use the Kitty terminal's graphics protocol to render images and other graphics. When using the protocol, the terminal responds on querying and/or transmitting images. This parses these responses and emits events for them. --- input/key_test.go | 24 ++++++++++++++++++++++++ input/kitty.go | 9 +++++++++ input/parse.go | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/input/key_test.go b/input/key_test.go index 60144e71..29263e06 100644 --- a/input/key_test.go +++ b/input/key_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/ansi/kitty" ) var sequences = buildKeysTable(FlagTerminfo, "dumb") @@ -99,6 +100,29 @@ func buildBaseSeqTests() []seqTest { func TestParseSequence(t *testing.T) { td := buildBaseSeqTests() td = append(td, + // Kitty Graphics response. + seqTest{ + []byte("\x1b_Ga=t;OK\x1b\\"), + []Event{KittyGraphicsEvent{ + Options: kitty.Options{Action: kitty.Transmit}, + Payload: []byte("OK"), + }}, + }, + seqTest{ + []byte("\x1b_Gi=99,I=13;OK\x1b\\"), + []Event{KittyGraphicsEvent{ + Options: kitty.Options{ID: 99, Number: 13}, + Payload: []byte("OK"), + }}, + }, + seqTest{ + []byte("\x1b_Gi=1337,q=1;EINVAL:your face\x1b\\"), + []Event{KittyGraphicsEvent{ + Options: kitty.Options{ID: 1337, Quite: 1}, + Payload: []byte("EINVAL:your face"), + }}, + }, + // Xterm modifyOtherKeys CSI 27 ; ; ~ seqTest{ []byte("\x1b[27;3;20320~"), diff --git a/input/kitty.go b/input/kitty.go index 13b7c997..efc39327 100644 --- a/input/kitty.go +++ b/input/kitty.go @@ -5,8 +5,17 @@ import ( "unicode/utf8" "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/ansi/kitty" ) +// KittyGraphicsEvent represents a Kitty Graphics response event. +// +// See https://sw.kovidgoyal.net/kitty/graphics-protocol/ +type KittyGraphicsEvent struct { + Options kitty.Options + Payload []byte +} + // KittyEnhancementsEvent represents a Kitty enhancements event. type KittyEnhancementsEvent int diff --git a/input/parse.go b/input/parse.go index 4a0a6062..07c7cfcf 100644 --- a/input/parse.go +++ b/input/parse.go @@ -1,6 +1,7 @@ package input import ( + "bytes" "encoding/base64" "strings" "unicode" @@ -136,6 +137,10 @@ func (p *Parser) parseSequence(buf []byte) (n int, Event Event) { return p.parseOsc(buf) case '_': // Esc-prefixed APC return p.parseApc(buf) + case '^': // Esc-prefixed PM + return p.parseStTerminated(ansi.PM, '^', nil)(buf) + case 'X': // Esc-prefixed SOS + return p.parseStTerminated(ansi.SOS, 'X', nil)(buf) default: n, e := p.parseSequence(buf[1:]) if k, ok := e.(KeyPressEvent); ok { @@ -158,6 +163,10 @@ func (p *Parser) parseSequence(buf []byte) (n int, Event Event) { return p.parseOsc(buf) case ansi.APC: return p.parseApc(buf) + case ansi.PM: + return p.parseStTerminated(ansi.PM, '^', nil)(buf) + case ansi.SOS: + return p.parseStTerminated(ansi.SOS, 'X', nil)(buf) default: if b <= ansi.US || b == ansi.DEL || b == ansi.SP { return 1, p.parseControl(b) @@ -661,7 +670,7 @@ func (p *Parser) parseOsc(b []byte) (int, Event) { } // parseStTerminated parses a control sequence that gets terminated by a ST character. -func (p *Parser) parseStTerminated(intro8, intro7 byte) func([]byte) (int, Event) { +func (p *Parser) parseStTerminated(intro8, intro7 byte, fn func([]byte) Event) func([]byte) (int, Event) { return func(b []byte) (int, Event) { var i int if b[i] == intro8 || b[i] == ansi.ESC { @@ -675,12 +684,15 @@ func (p *Parser) parseStTerminated(intro8, intro7 byte) func([]byte) (int, Event // Most common control sequence is terminated by a ST character // ST is a 7-bit string terminator character is (ESC \) // nolint: revive + start := i for ; i < len(b) && b[i] != ansi.ST && b[i] != ansi.ESC; i++ { } if i >= len(b) { return i, UnknownEvent(b[:i]) } + + end := i // end of the sequence data i++ // Check 7-bit ST (string terminator) character @@ -688,6 +700,13 @@ func (p *Parser) parseStTerminated(intro8, intro7 byte) func([]byte) (int, Event i++ } + // Call the function to parse the sequence and return the result + if fn != nil { + if e := fn(b[start:end]); e != nil { + return i, e + } + } + return i, UnknownEvent(b[:i]) } } @@ -813,7 +832,24 @@ func (p *Parser) parseApc(b []byte) (int, Event) { } // APC sequences are introduced by APC (0x9f) or ESC _ (0x1b 0x5f) - return p.parseStTerminated(ansi.APC, '_')(b) + return p.parseStTerminated(ansi.APC, '_', func(b []byte) Event { + if len(b) == 0 { + return nil + } + + switch b[0] { + case 'G': // Kitty Graphics Protocol + var g KittyGraphicsEvent + parts := bytes.Split(b[1:], []byte{';'}) + g.Options.UnmarshalText(parts[0]) //nolint:errcheck + if len(parts) > 1 { + g.Payload = parts[1] + } + return g + } + + return nil + })(b) } func (p *Parser) parseUtf8(b []byte) (int, Event) {