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

Implement Enable/Disable/Detect Kitty Keyboard Protocol #159

Open
wants to merge 2 commits into
base: master
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion ansicolors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package termenv

// ANSI color codes
// ANSI color codes.
const (
ANSIBlack ANSIColor = iota
ANSIRed
Expand Down
2 changes: 1 addition & 1 deletion color.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
ErrInvalidColor = errors.New("invalid color")
)

// Foreground and Background sequence codes
// Foreground and Background sequence codes.
const (
Foreground = "38"
Background = "48"
Expand Down Expand Up @@ -83,9 +83,9 @@
}

if col < 8 {
return fmt.Sprintf("%d", bgMod(col)+30)

Check failure on line 86 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 30, in <argument> detected (gomnd)
}
return fmt.Sprintf("%d", bgMod(col-8)+90)

Check failure on line 88 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 90, in <argument> detected (gomnd)
}

// Sequence returns the ANSI Sequence for the color.
Expand All @@ -108,7 +108,7 @@
if bg {
prefix = Background
}
return fmt.Sprintf("%s;2;%d;%d;%d", prefix, uint8(f.R*255), uint8(f.G*255), uint8(f.B*255))

Check failure on line 111 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 255, in <argument> detected (gomnd)
}

func xTermColor(s string) (RGBColor, error) {
Expand Down Expand Up @@ -166,13 +166,13 @@
if v < 115 {
return 1
}
return int((v - 35) / 40)

Check failure on line 169 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 40, in <argument> detected (gomnd)
}

// Calculate the nearest 0-based color index at 16..231
r := v2ci(c.R * 255.0) // 0..5 each

Check failure on line 173 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 255.0, in <argument> detected (gomnd)
g := v2ci(c.G * 255.0)

Check failure on line 174 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 255.0, in <argument> detected (gomnd)
b := v2ci(c.B * 255.0)

Check failure on line 175 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 255.0, in <argument> detected (gomnd)
ci := 36*r + 6*g + b /* 0..215 */

// Calculate the represented colors back from the index
Expand All @@ -198,7 +198,7 @@
grayDist := c.DistanceHSLuv(g2)

if colorDist <= grayDist {
return ANSI256Color(16 + ci)

Check failure on line 201 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 16, in <argument> detected (gomnd)
}
return ANSI256Color(232 + grayIdx)

Check failure on line 203 in color.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 232, in <argument> detected (gomnd)
}
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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth introducing constants for these return values.

f := func() {
if !o.isTTY() {
return
}

o.kkpFlags = o.kittyKeyboardProtocolSupport()
}

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

return o.kkpFlags
}
8 changes: 4 additions & 4 deletions profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (
type Profile int

const (
// TrueColor, 24-bit color profile
// TrueColor, 24-bit color profile.
TrueColor = Profile(iota)
// ANSI256, 8-bit color profile
// ANSI256, 8-bit color profile.
ANSI256
// ANSI, 4-bit color profile
// ANSI, 4-bit color profile.
ANSI
// Ascii, uncolored profile
// Ascii, uncolored profile.
Ascii //nolint:revive
)

Expand Down
15 changes: 15 additions & 0 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
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 @@ -113,7 +118,7 @@

// ClearScreen clears the visible portion of the terminal.
func (o Output) ClearScreen() {
fmt.Fprintf(o.w, CSI+EraseDisplaySeq, 2)

Check failure on line 121 in screen.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 2, in <argument> detected (gomnd)
o.MoveCursor(1, 1)
}

Expand Down Expand Up @@ -301,6 +306,16 @@
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
10 changes: 5 additions & 5 deletions termenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ var (
)

const (
// Escape character
// Escape character.
ESC = '\x1b'
// Bell
// Bell.
BEL = '\a'
// Control Sequence Introducer
// Control Sequence Introducer.
CSI = string(ESC) + "["
// Operating System Command
// Operating System Command.
OSC = string(ESC) + "]"
// String Terminator
// String Terminator.
ST = string(ESC) + `\`
)

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

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

Expand Down Expand Up @@ -113,6 +113,79 @@
return ANSIColor(0)
}

func (o Output) kittyKeyboardProtocolSupport() byte {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to combine both this and termStatusReport into one routine that gets executed when the output is being initialized. Right now, both kittyKeyboardProtocolSupport & termStatusReport will set the tty termios and read the envs before reading the response. This results in multiple unnecessary calls to GetTermios/SetTermios and Getenv.

Instead, we could define an initialize function that queries the terminal once. However, I believe this might break things since we query the terminal during the lifetime of WithColorCache https://github.com/muesli/termenv/pull/159/files#diff-35d16970a063a59d666ecdce46f6ac3363f36d17a3e672d8ff69f9ef2056d4cbR114

cc/ @muesli

// 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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume other terminals simply ignore these queries? We should test this with the common set of terminals we usually test against.


// 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 @@
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