Skip to content

Commit

Permalink
feat: enable / disable kitty keyboard protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
maaslalani committed Nov 19, 2023
1 parent a0e4c27 commit 72eeddf
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 1 deletion.
32 changes: 32 additions & 0 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type Output struct {
fgColor Color
bgSync *sync.Once
bgColor Color
kkpFlags byte
kkpSync *sync.Once
}

// Environ is an interface for getting environment variables.
Expand Down Expand Up @@ -205,3 +207,33 @@ func (o Output) Write(p []byte) (int, error) {
func (o Output) WriteString(s string) (int, error) {
return o.Write([]byte(s))
}

// KittyKeyboardProtocolSupport returns which progressive enhancements the
// terminal supports for the kitty keyboard protocol.
//
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
//
// 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.
func (o Output) KittyKeyboardProtocolSupport() byte {
f := func() {
if !o.isTTY() {
return
}

o.kkpFlags = o.kittyKeyboardProtocolSupport()
}

if o.cache {
o.kkpSync.Do(f)
} else {
f()
}

return o.kkpFlags
}
15 changes: 15 additions & 0 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ const (
StartBracketedPasteSeq = "200~"
EndBracketedPasteSeq = "201~"

// Kitty Keyboard Protocol.
// https://sw.kovidgoyal.net/kitty/keyboard-protocol
EnableKittyKeyboardProtocol = ">1u"
DisableKittyKeyboardProtocol = "<u"

// Session.
SetWindowTitleSeq = "2;%s" + string(BEL)
SetForegroundColorSeq = "10;%s" + string(BEL)
Expand Down Expand Up @@ -301,6 +306,16 @@ func (o Output) DisableBracketedPaste() {
fmt.Fprintf(o.w, CSI+DisableBracketedPasteSeq)
}

// EnableKittyKeyboardProtocol enables the kitty keyboard protocol.
func (o Output) EnableKittyKeyboardProtocol() {
fmt.Fprintf(o.w, CSI+EnableKittyKeyboardProtocol)
}

// DisableKittyKeyboardProtocol disables the kitty keyboard protocol.
func (o Output) DisableKittyKeyboardProtocol() {
fmt.Fprintf(o.w, CSI+DisableKittyKeyboardProtocol)
}

// Legacy functions.

// Reset the terminal to its default style, removing any active styles.
Expand Down
131 changes: 130 additions & 1 deletion termenv_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

const (
// timeout for OSC queries
// timeout for OSC queries.
OSCTimeout = 5 * time.Second
)

Expand Down Expand Up @@ -113,6 +113,79 @@ func (o Output) backgroundColor() Color {
return ANSIColor(0)
}

func (o Output) kittyKeyboardProtocolSupport() byte {
// 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 0
}

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

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

t, err := unix.IoctlGetTermios(fd, tcgetattr)
if err != nil {
return 0
}
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 0
}
}

// 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 {
fd := o.TTY().Fd()
tv := unix.NsecToTimeval(int64(timeout))
Expand Down Expand Up @@ -157,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
5 changes: 5 additions & 0 deletions termenv_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func (o Output) backgroundColor() Color {
return ANSIColor(0)
}

func (o Output) kittyKeyboardProtocolSupport() byte {
// default byte
return 0b00000
}

// EnableWindowsANSIConsole enables virtual terminal processing on Windows
// platforms. This allows the use of ANSI escape sequences in Windows console
// applications. Ensure this gets called before anything gets rendered with
Expand Down

0 comments on commit 72eeddf

Please sign in to comment.