Skip to content

Commit

Permalink
feat: implement detection of kitty keyboard protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
maaslalani committed Nov 19, 2023
1 parent 356fb16 commit 68c77de
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 6 deletions.
10 changes: 5 additions & 5 deletions output.go
Expand Up @@ -215,11 +215,11 @@ func (o Output) WriteString(s string) (int, error) {
//
// The byte returned represents the bitset of supported flags.
//
// 0b1 (01) — Disambiguate escape codes
// 0b10 (02) — Report event types
// 0b100 (04) — Report alternate keys
// 0b1000 (08) — Report all keys as escape codes
// 0b10000 (16) — Report associated text
// 0b1 (01) — Disambiguate escape codes
// 0b10 (02) — Report event types
// 0b100 (04) — Report alternate keys
// 0b1000 (08) — Report all keys as escape codes
// 0b10000 (16) — Report associated text

Check failure on line 222 in output.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)

Check failure on line 222 in output.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)
//

Check failure on line 223 in output.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)

Check failure on line 223 in output.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)
func (o Output) KittyKeyboardProtocolSupport() byte {
f := func() {
Expand Down
127 changes: 126 additions & 1 deletion termenv_unix.go
Expand Up @@ -114,7 +114,76 @@ func (o Output) backgroundColor() Color {
}

func (o Output) kittyKeyboardProtocolSupport() byte {
return 0b11111
// screen/tmux can't support OSC, because they can be connected to multiple
// terminals concurrently.
term := o.environ.Getenv("TERM")
if strings.HasPrefix(term, "screen") || strings.HasPrefix(term, "tmux") {
return 0b00000

Check failure on line 121 in termenv_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 0b00000, in <return> detected (gomnd)
}

tty := o.TTY()
if tty == nil {
return 0b00000

Check failure on line 126 in termenv_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 0b00000, in <return> detected (gomnd)
}

if !o.unsafe {
fd := int(tty.Fd())
// if in background, we can't control the terminal
if !isForeground(fd) {
return 0b00000

Check failure on line 133 in termenv_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 0b00000, in <return> detected (gomnd)
}

t, err := unix.IoctlGetTermios(fd, tcgetattr)
if err != nil {
return 0b00000

Check failure on line 138 in termenv_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 0b00000, in <return> detected (gomnd)
}
defer unix.IoctlSetTermios(fd, tcsetattr, t) //nolint:errcheck

noecho := *t
noecho.Lflag = noecho.Lflag &^ unix.ECHO
noecho.Lflag = noecho.Lflag &^ unix.ICANON
if err := unix.IoctlSetTermios(fd, tcsetattr, &noecho); err != nil {
return 0b00000

Check failure on line 146 in termenv_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 0b00000, in <return> detected (gomnd)
}
}

// first, send CSI query to see whether this terminal supports the
// kitty keyboard protocol
fmt.Fprintf(tty, CSI+"?u")

// then, query primary device data, should be supported by all terminals
// if we receive a response for the primary device data befor the kitty keyboard
// protocol response, this terminal does not support kitty keyboard protocol.
fmt.Fprintf(tty, CSI+"c")

response, isAttrs, err := o.readNextResponseKittyKeyboardProtocol()

// we queried for the kitty keyboard protocol current progressive enhancements
// but received the primary device attributes response, therefore this terminal
// does not support the kitty keyboard protocol.
if err != nil || isAttrs {
return 0
}

// read the primary attrs response and ignore it.
_, _, err = o.readNextResponseKittyKeyboardProtocol()
if err != nil {
return 0
}

// we receive a valid response to the kitty keyboard protocol query, this
// terminal supports the protocol.
//
// parse the response and return the flags supported.
//
// 0 1 2 3 4
// \x1b [ ? 1 u
//
if len(response) <= 3 {

Check failure on line 182 in termenv_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 3, in <condition> detected (gomnd)
return 0
}

return response[3]
}

func (o *Output) waitForData(timeout time.Duration) error {
Expand Down Expand Up @@ -161,6 +230,62 @@ func (o *Output) readNextByte() (byte, error) {
return b[0], nil
}

// readNextResponseKittyKeyboardProtocol reads either a CSI response to the current
// progressive enhancement status or primary device attributes response.
// - CSI response: "\x1b]?31u"
// - primary device attributes response: "\x1b]?64;1;2;7;8;9;15;18;21;44;45;46c"
func (o *Output) readNextResponseKittyKeyboardProtocol() (response string, isAttrs bool, err error) {
start, err := o.readNextByte()
if err != nil {
return "", false, ErrStatusReport
}

// first byte must be ESC
for start != ESC {
start, err = o.readNextByte()
if err != nil {
return "", false, ErrStatusReport
}
}

response += string(start)

// next byte is [
tpe, err := o.readNextByte()
if err != nil {
return "", false, ErrStatusReport
}
response += string(tpe)

if tpe != '[' {
return "", false, ErrStatusReport
}

for {
b, err := o.readNextByte()
if err != nil {
return "", false, ErrStatusReport
}
response += string(b)

switch b {
case 'u':
// kitty keyboard protocol response
return response, false, nil
case 'c':
// primary device attributes response
return response, true, nil
}

// both responses have less than 38 bytes, so if we read more, that's an error
if len(response) > 38 {

Check failure on line 281 in termenv_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 38, in <condition> detected (gomnd)
break
}
}

return response, isAttrs, nil
}

// readNextResponse reads either an OSC response or a cursor position response:
// - OSC response: "\x1b]11;rgb:1111/1111/1111\x1b\\"
// - cursor position response: "\x1b[42;1R"
Expand Down

0 comments on commit 68c77de

Please sign in to comment.