Skip to content

Commit

Permalink
feat(input): support kitty graphics responses
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aymanbagabas committed Jan 16, 2025
1 parent b4bcd27 commit f6000e5
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 2 deletions.
24 changes: 24 additions & 0 deletions input/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/ansi/kitty"
)

var sequences = buildKeysTable(FlagTerminfo, "dumb")
Expand Down Expand Up @@ -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 ; <modifier> ; <code> ~
seqTest{
[]byte("\x1b[27;3;20320~"),
Expand Down
9 changes: 9 additions & 0 deletions input/kitty.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 38 additions & 2 deletions input/parse.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package input

import (
"bytes"
"encoding/base64"
"strings"
"unicode"
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -675,19 +684,29 @@ 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
if i < len(b) && b[i-1] == ansi.ESC && b[i] == '\\' {
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])
}
}
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit f6000e5

Please sign in to comment.