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) {