Skip to content

Commit

Permalink
feat(input): support kitty keyboard
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Feb 16, 2024
1 parent 61e45fc commit 681ee5c
Show file tree
Hide file tree
Showing 6 changed files with 562 additions and 100 deletions.
41 changes: 39 additions & 2 deletions exp/term/input/ansi/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,48 @@ func (d *driver) parseCsi(i int, p []byte, alt bool) (n int, e input.Event, err
n++
seq += string(p[i])

if seq == "\x1b[M" && i+3 < len(p) {
csi := ansi.CsiSequence(seq)
initial := csi.Initial()
cmd := csi.Command()
switch {
case seq == "\x1b[M" && i+3 < len(p):
// Handle X10 mouse
return n + 3, parseX10MouseEvent(append([]byte(seq), p[i+1:i+3]...)), nil
} else if seq[2] == '<' && (seq[len(seq)-1] == 'm' || seq[len(seq)-1] == 'M') {
case initial == '<' && (cmd == 'm' || cmd == 'M'):
return n, parseSGRMouseEvent([]byte(seq)), nil
case initial == 0 && cmd == 'u':
// Kitty keyboard protocol
params := ansi.Params(csi.Params())
key := input.KeyEvent{}
if len(params) > 0 {
code := int(params[0][0])
if sym, ok := kittyKeyMap[code]; ok {
key.Sym = sym
} else {
key.Rune = rune(code)
// TODO: support alternate keys
}
}
if len(params) > 1 {
mod := int(params[1][0])
if mod > 1 {
key.Mod = fromKittyMod(int(params[1][0] - 1))
}
if len(params[1]) > 1 {
switch int(params[1][1]) {
case 0, 1:
key.Action = input.KeyPress
case 2:
key.Action = input.KeyRepeat
case 3:
key.Action = input.KeyRelease
}
}
}
if len(params) > 2 {
key.Rune = rune(params[2][0])
}
return n, key, nil
}

k, ok := d.table[seq]
Expand Down
145 changes: 121 additions & 24 deletions exp/term/input/ansi/examples/readinput/main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package main

import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"os"

"github.com/charmbracelet/x/exp/term"
"github.com/charmbracelet/x/exp/term/ansi/kitty"
"github.com/charmbracelet/x/exp/term/ansi/sys"
"github.com/charmbracelet/x/exp/term/input"
"github.com/charmbracelet/x/exp/term/input/ansi"
)
Expand All @@ -18,25 +22,24 @@ func main() {
}

defer term.Restore(os.Stdin.Fd(), state)
defer io.WriteString(os.Stdout, "\x1b[>0u") // Disable Kitty keyboard

// r := bufio.NewReader(strings.NewReader("\x00\x1ba\x1b[Z\x1b\x01\x1b[A"))
rd := ansi.NewDriver(bufio.NewReaderSize(os.Stdin, 256), os.Getenv("TERM"), 0)
var in io.Reader = os.Stdin
if !term.IsTerminal(os.Stdin.Fd()) {
bts, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("error reading from stdin: %v\r\n", err)
}

// p, err := d.PeekInput(2)
// if err != nil {
// log.Fatalf("error peeking input: %v\r\n", err)
// }
//
// for _, e := range p {
// log.Printf("event: %s (len: %d)\r\n\r\n", e, len(p))
// }
in = bytes.NewReader(bts)
}
rd := ansi.NewDriver(in, os.Getenv("TERM"), 0)

// go func() {
// time.Sleep(2 * time.Second)
// io.WriteString(os.Stdout, "\x1b[?u\x1b[c\x1b]11;?\x07")
// }()
printHelp()

lastEv := input.Event(nil)
var kittyFlags int
last := input.Event(nil)
OUT:
for {
n, err := rd.ReadInput()
if err != nil {
Expand All @@ -47,20 +50,114 @@ func main() {
log.Fatalf("error reading input: %v\r\n", err)
}

// Gracefully exit on 'qq'
if lastEv != nil {
k, ok1 := n[len(n)-1].(input.KeyEvent)
p, ok2 := lastEv.(input.KeyEvent)
if ok1 && ok2 && k.Rune == 'q' && p.Rune == 'q' {
break
if last != nil {
cur, ok1 := n[len(n)-1].(input.KeyEvent)
prev, ok2 := last.(input.KeyEvent)
if ok1 && ok2 && cur.Sym == 0 && prev.Sym == 0 && cur.Action == 0 && prev.Action == 0 {
switch {
case prev.Rune == 'q' && cur.Rune == 'q':
break OUT
case prev.Rune == 'h' && cur.Rune == 'h':
printHelp()
case prev.Rune == 'k':
switch cur.Rune {
case '0':
kittyFlags = 0
execute(kitty.Disable(kittyFlags))
case '1':
if kittyFlags&kitty.DisambiguateEscapeCodes == 0 {
kittyFlags |= kitty.DisambiguateEscapeCodes
execute(kitty.Enable(kittyFlags))
} else {
kittyFlags &^= kitty.DisambiguateEscapeCodes
execute(kitty.Disable(kittyFlags))
}
case '2':
if kittyFlags&kitty.ReportEventTypes == 0 {
kittyFlags |= kitty.ReportEventTypes
execute(kitty.Enable(kittyFlags))
} else {
kittyFlags &^= kitty.ReportEventTypes
execute(kitty.Disable(kittyFlags))
}
case '3':
if kittyFlags&kitty.ReportAlternateKeys == 0 {
kittyFlags |= kitty.ReportAlternateKeys
execute(kitty.Enable(kittyFlags))
} else {
kittyFlags &^= kitty.ReportAlternateKeys
execute(kitty.Disable(kittyFlags))
}
case '4':
if kittyFlags&kitty.ReportAllKeys == 0 {
kittyFlags |= kitty.ReportAllKeys
execute(kitty.Enable(kittyFlags))
} else {
kittyFlags &^= kitty.ReportAllKeys
execute(kitty.Disable(kittyFlags))
}
case '5':
if kittyFlags&kitty.ReportAssociatedKeys == 0 {
kittyFlags |= kitty.ReportAssociatedKeys
execute(kitty.Enable(kittyFlags))
} else {
kittyFlags &^= kitty.ReportAssociatedKeys
execute(kitty.Disable(kittyFlags))
}
}
case prev.Rune == 'r':
switch cur.Rune {
case 'k':
execute(kitty.Request)
case 'b':
execute(sys.RequestBackgroundColor)
case 'f':
execute(sys.RequestForegroundColor)
case 'c':
execute(sys.RequestCursorColor)
case 'd':
// DA1 (Primary Device Attributes)
execute("\x1b[c")
}
}
}
}

for _, e := range n {
log.Printf("event: %s (len: %d)\r\n\r\n", e, len(n))
log.Printf("event: %s\r\n\r\n", e)
}

// Store last keypress
if len(n) > 0 {
lastEv = n[len(n)-1]
key, ok := n[len(n)-1].(input.KeyEvent)
if ok && key.Action == 0 {
last = key
}
}
}
}

func execute(s string) {
io.WriteString(os.Stdout, s) // nolint: errcheck
}

func printHelp() {
fmt.Fprintf(os.Stdout, "Welcome to input demo!\r\n\r\n")
fmt.Fprintf(os.Stdout, "Press 'qq' to quit.\r\n")
fmt.Fprintf(os.Stdout, "Press 'hh' to print this help again.\r\n")
fmt.Fprintf(os.Stdout, "Press 'k' followed by a number to toggle Kitty keyboard protocol flags.\r\n")
fmt.Fprintf(os.Stdout, " 1: DisambiguateEscapeCodes\r\n")
fmt.Fprintf(os.Stdout, " 2: ReportEventTypes\r\n")
fmt.Fprintf(os.Stdout, " 3: ReportAlternateKeys\r\n")
fmt.Fprintf(os.Stdout, " 4: ReportAllKeys\r\n")
fmt.Fprintf(os.Stdout, " 5: ReportAssociatedKeys\r\n")
fmt.Fprintf(os.Stdout, " 0: Disable all flags\r\n")
fmt.Fprintf(os.Stdout, "\r\n")
fmt.Fprintf(os.Stdout, "Press 'r' followed by a letter to request a terminal capability.\r\n")
fmt.Fprintf(os.Stdout, " k: Kitty keyboard protocol flags\r\n")
fmt.Fprintf(os.Stdout, " b: Background color\r\n")
fmt.Fprintf(os.Stdout, " f: Foreground color\r\n")
fmt.Fprintf(os.Stdout, " c: Cursor color\r\n")
fmt.Fprintf(os.Stdout, " d: Primary Device Attributes\r\n")
fmt.Fprintf(os.Stdout, "\r\n")
}
165 changes: 165 additions & 0 deletions exp/term/input/ansi/kitty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package ansi

import (
"github.com/charmbracelet/x/exp/term/input"
)

// Kitty Clipboard Control Sequences
var kittyKeyMap = map[int]input.KeySym{
9: input.KeyTab,
13: input.KeyEnter,
27: input.KeyEscape,
127: input.KeyBackspace,

57344: input.KeyEscape,
57345: input.KeyEnter,
57346: input.KeyTab,
57347: input.KeyBackspace,
57348: input.KeyInsert,
57349: input.KeyDelete,
57350: input.KeyLeft,
57351: input.KeyRight,
57352: input.KeyUp,
57353: input.KeyDown,
57354: input.KeyPgUp,
57355: input.KeyPgDown,
57356: input.KeyHome,
57357: input.KeyEnd,
57358: input.KeyCapsLock,
57359: input.KeyScrollLock,
57360: input.KeyNumLock,
57361: input.KeyPrintScreen,
57362: input.KeyPause,
57363: input.KeyMenu,
57364: input.KeyF1,
57365: input.KeyF2,
57366: input.KeyF3,
57367: input.KeyF4,
57368: input.KeyF5,
57369: input.KeyF6,
57370: input.KeyF7,
57371: input.KeyF8,
57372: input.KeyF9,
57373: input.KeyF10,
57374: input.KeyF11,
57375: input.KeyF12,
57376: input.KeyF13,
57377: input.KeyF14,
57378: input.KeyF15,
57379: input.KeyF16,
57380: input.KeyF17,
57381: input.KeyF18,
57382: input.KeyF19,
57383: input.KeyF20,
57384: input.KeyF21,
57385: input.KeyF22,
57386: input.KeyF23,
57387: input.KeyF24,
57388: input.KeyF25,
57389: input.KeyF26,
57390: input.KeyF27,
57391: input.KeyF28,
57392: input.KeyF29,
57393: input.KeyF30,
57394: input.KeyF31,
57395: input.KeyF32,
57396: input.KeyF33,
57397: input.KeyF34,
57398: input.KeyF35,
57399: input.KeyKp0,
57400: input.KeyKp1,
57401: input.KeyKp2,
57402: input.KeyKp3,
57403: input.KeyKp4,
57404: input.KeyKp5,
57405: input.KeyKp6,
57406: input.KeyKp7,
57407: input.KeyKp8,
57408: input.KeyKp9,
57409: input.KeyKpPeriod,
57410: input.KeyKpDiv,
57411: input.KeyKpMul,
57412: input.KeyKpMinus,
57413: input.KeyKpPlus,
57414: input.KeyKpEnter,
57415: input.KeyKpEqual,
57416: input.KeyKpSep,
57417: input.KeyKpLeft,
57418: input.KeyKpRight,
57419: input.KeyKpUp,
57420: input.KeyKpDown,
57421: input.KeyKpPgUp,
57422: input.KeyKpPgDown,
57423: input.KeyKpHome,
57424: input.KeyKpEnd,
57425: input.KeyKpInsert,
57426: input.KeyKpDelete,
57427: input.KeyKpBegin,
57428: input.KeyMediaPlay,
57429: input.KeyMediaPause,
57430: input.KeyMediaPlayPause,
57431: input.KeyMediaReverse,
57432: input.KeyMediaStop,
57433: input.KeyMediaFastForward,
57434: input.KeyMediaRewind,
57435: input.KeyMediaNext,
57436: input.KeyMediaPrev,
57437: input.KeyMediaRecord,
57438: input.KeyLowerVol,
57439: input.KeyRaiseVol,
57440: input.KeyMute,
57441: input.KeyLeftShift,
57442: input.KeyLeftCtrl,
57443: input.KeyLeftAlt,
57444: input.KeyLeftSuper,
57445: input.KeyLeftHyper,
57446: input.KeyLeftMeta,
57447: input.KeyRightShift,
57448: input.KeyRightCtrl,
57449: input.KeyRightAlt,
57450: input.KeyRightSuper,
57451: input.KeyRightHyper,
57452: input.KeyRightMeta,
57453: input.KeyIsoLevel3Shift,
57454: input.KeyIsoLevel5Shift,
}

const (
kittyShift = 1 << iota
kittyAlt
kittyCtrl
kittySuper
kittyHyper
kittyMeta
kittyCapsLock
kittyNumLock
)

func fromKittyMod(mod int) input.Mod {
var m input.Mod
if mod&kittyShift != 0 {
m |= input.Shift
}
if mod&kittyAlt != 0 {
m |= input.Alt
}
if mod&kittyCtrl != 0 {
m |= input.Ctrl
}
if mod&kittySuper != 0 {
m |= input.Super
}
if mod&kittyHyper != 0 {
m |= input.Hyper
}
if mod&kittyMeta != 0 {
m |= input.Meta
}
if mod&kittyCapsLock != 0 {
m |= input.CapsLock
}
if mod&kittyNumLock != 0 {
m |= input.NumLock
}
return m
}
Loading

0 comments on commit 681ee5c

Please sign in to comment.