Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(input): support kitty graphics responses #325

Open
wants to merge 1 commit into
base: ansi/kitty
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading