Skip to content

Commit

Permalink
feat: add kitty keyboard options and settings (#1083)
Browse files Browse the repository at this point in the history
This adds the necessary options to enable/disable the kitty keyboard
protocol.

Needs: #1080
Fixes: #869
  • Loading branch information
aymanbagabas committed Aug 19, 2024
2 parents 0c1a6a4 + c630d5e commit 8e406ff
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 25 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/charmbracelet/bubbletea
go 1.18

require (
github.com/charmbracelet/x/ansi v0.1.4
github.com/charmbracelet/x/ansi v0.1.5-0.20240814155920-d72bfb0444d7
github.com/charmbracelet/x/term v0.1.1
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.1.5-0.20240813204730-a29dab672bf2 h1:/BRVAHphENSsvuYG+iVP/qgC6Bjjc9zdOmY3pQzg7mE=
github.com/charmbracelet/x/ansi v0.1.5-0.20240813204730-a29dab672bf2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.1.5-0.20240814155920-d72bfb0444d7 h1:wcBKMnW8Fm0Oiw6b7pTXe+5eCntEuKI6s82J41nZcEY=
github.com/charmbracelet/x/ansi v0.1.5-0.20240814155920-d72bfb0444d7/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
Expand Down
78 changes: 78 additions & 0 deletions key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,84 @@ func buildBaseSeqTests() []seqTest {
func TestParseSequence(t *testing.T) {
td := buildBaseSeqTests()
td = append(td,
// Kitty keyboard / CSI u (fixterms)
seqTest{
[]byte("\x1b[1B"),
[]Msg{KeyPressMsg{Type: KeyDown}},
},
seqTest{
[]byte("\x1b[1;B"),
[]Msg{KeyPressMsg{Type: KeyDown}},
},
seqTest{
[]byte("\x1b[1;4B"),
[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyDown}},
},
seqTest{
[]byte("\x1b[8~"),
[]Msg{KeyPressMsg{Type: KeyEnd}},
},
seqTest{
[]byte("\x1b[8;~"),
[]Msg{KeyPressMsg{Type: KeyEnd}},
},
seqTest{
[]byte("\x1b[8;10~"),
[]Msg{KeyPressMsg{Mod: ModShift | ModMeta, Type: KeyEnd}},
},
seqTest{
[]byte("\x1b[27;4u"),
[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyEscape}},
},
seqTest{
[]byte("\x1b[127;4u"),
[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyBackspace}},
},
seqTest{
[]byte("\x1b[57358;4u"),
[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyCapsLock}},
},
seqTest{
[]byte("\x1b[9;2u"),
[]Msg{KeyPressMsg{Mod: ModShift, Type: KeyTab}},
},
seqTest{
[]byte("\x1b[195;u"),
[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, Type: KeyRunes}},
},
seqTest{
[]byte("\x1b[20320;2u"),
[]Msg{KeyPressMsg{Runes: []rune{'你'}, Mod: ModShift, Type: KeyRunes}},
},
seqTest{
[]byte("\x1b[195;:1u"),
[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, Type: KeyRunes}},
},
seqTest{
[]byte("\x1b[195;2:3u"),
[]Msg{KeyReleaseMsg{Runes: []rune{'Ã'}, Mod: ModShift}},
},
seqTest{
[]byte("\x1b[195;2:2u"),
[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, IsRepeat: true, Mod: ModShift}},
},
seqTest{
[]byte("\x1b[195;2:1u"),
[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, Mod: ModShift}},
},
seqTest{
[]byte("\x1b[195;2:3u"),
[]Msg{KeyReleaseMsg{Runes: []rune{'Ã'}, Mod: ModShift}},
},
seqTest{
[]byte("\x1b[97;2;65u"),
[]Msg{KeyPressMsg{Runes: []rune{'A'}, Mod: ModShift, altRune: 'a'}},
},
seqTest{
[]byte("\x1b[97;;229u"),
[]Msg{KeyPressMsg{Runes: []rune{'å'}, altRune: 'a'}},
},

// focus/blur
seqTest{
[]byte{'\x1b', '[', 'I'},
Expand Down
87 changes: 63 additions & 24 deletions kitty.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,71 @@ import (
"github.com/charmbracelet/x/ansi"
)

// KittyKeyboardMsg represents Kitty keyboard progressive enhancement flags message.
type KittyKeyboardMsg int
// setKittyKeyboardFlagsMsg is a message to set Kitty keyboard progressive
// enhancement protocol flags.
type setKittyKeyboardFlagsMsg int

// IsDisambiguateEscapeCodes returns true if the DisambiguateEscapeCodes flag is set.
func (e KittyKeyboardMsg) IsDisambiguateEscapeCodes() bool {
return e&ansi.KittyDisambiguateEscapeCodes != 0
// EnableKittyKeyboard is a command to enable Kitty keyboard progressive
// enhancements.
//
// The flags parameter is a bitmask of the following
//
// 1: Disambiguate escape codes
// 2: Report event types
// 4: Report alternate keys
// 8: Report all keys as escape codes
// 16: Report associated text
//
// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/ for more information.
func EnableKittyKeyboard(flags int) Cmd {
return func() Msg {
return setKittyKeyboardFlagsMsg(flags)
}
}

// DisableKittyKeyboard is a command to disable Kitty keyboard progressive
// enhancements.
func DisableKittyKeyboard() Msg {
return setKittyKeyboardFlagsMsg(0)
}

// IsReportEventTypes returns true if the ReportEventTypes flag is set.
func (e KittyKeyboardMsg) IsReportEventTypes() bool {
return e&ansi.KittyReportEventTypes != 0
// kittyKeyboardMsg is a message that queries the current Kitty keyboard
// progressive enhancement flags.
type kittyKeyboardMsg struct{}

// KittyKeyboard is a command that queries the current Kitty keyboard
// progressive enhancement flags from the terminal.
func KittyKeyboard() Msg {
return kittyKeyboardMsg{}
}

// IsReportAlternateKeys returns true if the ReportAlternateKeys flag is set.
func (e KittyKeyboardMsg) IsReportAlternateKeys() bool {
return e&ansi.KittyReportAlternateKeys != 0
// EnableEnhancedKeyboard is a command to enable enhanced keyboard features.
// This unambiguously reports more key combinations than traditional terminal
// keyboard sequences. This will also enable reporting of release key events.
func EnableEnhancedKeyboard() Msg {
return setKittyKeyboardFlagsMsg(3)

Check failure on line 52 in kitty.go

View workflow job for this annotation

GitHub Actions / lint-soft

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

// IsReportAllKeys returns true if the ReportAllKeys flag is set.
func (e KittyKeyboardMsg) IsReportAllKeys() bool {
return e&ansi.KittyReportAllKeys != 0
// DisableEnhancedKeyboard is a command to disable enhanced keyboard features.
func DisableEnhancedKeyboard() Msg {
return setKittyKeyboardFlagsMsg(0)
}

// IsReportAssociatedKeys returns true if the ReportAssociatedKeys flag is set.
func (e KittyKeyboardMsg) IsReportAssociatedKeys() bool {
return e&ansi.KittyReportAssociatedKeys != 0
// KittyKeyboardMsg represents Kitty keyboard progressive enhancement flags message.
type KittyKeyboardMsg int

// Kitty Keyboard Protocol flags.
const (
KittyDisambiguateEscapeCodes KittyKeyboardMsg = 1 << iota
KittyReportEventTypes
KittyReportAlternateKeys
KittyReportAllKeys
KittyReportAssociatedKeys
)

// Contains reports whether m contains the given flags.
func (m KittyKeyboardMsg) Contains(flags KittyKeyboardMsg) bool {
return m&flags == flags
}

// Kitty Clipboard Control Sequences
Expand Down Expand Up @@ -268,13 +307,13 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) Msg {
}
}
}
// TODO: Associated keys are not support yet.
// if params := csi.Subparams(2); len(params) > 0 {
// r := rune(params[0])
// if unicode.IsPrint(r) {
// key.AltRune = r
// }
// }
if params := csi.Subparams(2); len(params) > 0 {

Check failure on line 310 in kitty.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <argument> detected (gomnd)
r := rune(params[0])
if unicode.IsPrint(r) {
key.altRune = key.Rune()
key.Runes = []rune{r}
}
}
if isRelease {
return KeyReleaseMsg(key)
}
Expand Down
30 changes: 30 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,33 @@ func WithFPS(fps int) ProgramOption {
p.fps = fps
}
}

// WithEnhancedKeyboard enables support for enhanced keyboard features. This
// unambiguously reports more key combinations than traditional terminal
// keyboard sequences. This will also enable reporting of release key events.
//
// This is a syntactic sugar for WithKittyKeyboard(3).
func WithEnhancedKeyboard() ProgramOption {
return WithKittyKeyboard(3)

Check failure on line 244 in options.go

View workflow job for this annotation

GitHub Actions / lint-soft

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

// WithKittyKeyboard enables support for the Kitty keyboard protocol. This
// protocol enables more key combinations and events than the traditional
// ambiguous terminal keyboard sequences.
//
// Use flags to specify which features you want to enable.
//
// 0: Disable all features
// 1: Disambiguate escape codes
// 2: Report event types
// 4: Report alternate keys
// 8: Report all keys as escape codes
// 16: Report associated text
//
// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/ for more information.
func WithKittyKeyboard(flags int) ProgramOption {
return func(p *Program) {
p.kittyFlags = flags
p.startupOptions |= withKittyKeyboard
}
}
5 changes: 5 additions & 0 deletions screen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func TestClearMsg(t *testing.T) {
cmds: []Cmd{DisableBracketedPaste, EnableBracketedPaste},
expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "kitty_start",
cmds: []Cmd{DisableKittyKeyboard, EnableKittyKeyboard(3)},
expected: "\x1b[?25l\x1b[?2004h\x1b[>u\x1b[>3u\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[>0u",
},
}

for _, test := range tests {
Expand Down
21 changes: 21 additions & 0 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const (
// feature is on by default.
withoutCatchPanics
withoutBracketedPaste
withKittyKeyboard
)

// channelHandlers manages the series of channels returned by various processes.
Expand Down Expand Up @@ -173,6 +174,9 @@ type Program struct {
// fps is the frames per second we should set on the renderer, if
// applicable,
fps int

// kittyFlags stores kitty keyboard protocol progressive enhancement flags.
kittyFlags int
}

// Quit is a special command that tells the Bubble Tea program to exit.
Expand Down Expand Up @@ -392,6 +396,17 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
p.renderer.execute(ansi.DisableBracketedPaste)
p.bpActive = false

case KittyKeyboardMsg:
// Store the kitty flags whenever they are queried.
p.kittyFlags = int(msg)

case setKittyKeyboardFlagsMsg:
p.kittyFlags = int(msg)
p.renderer.execute(ansi.PushKittyKeyboard(p.kittyFlags))

case kittyKeyboardMsg:
p.renderer.execute(ansi.RequestKittyKeyboard)

case execMsg:
// NB: this blocks.
p.exec(msg.cmd, msg.fn)
Expand Down Expand Up @@ -556,6 +571,9 @@ func (p *Program) Run() (Model, error) {
p.renderer.execute(ansi.EnableMouseAllMotion)
p.renderer.execute(ansi.EnableMouseSgrExt)
}
if p.startupOptions&withKittyKeyboard != 0 {
p.renderer.execute(ansi.PushKittyKeyboard(p.kittyFlags))
}

// Start the renderer.
p.renderer.start()
Expand Down Expand Up @@ -727,6 +745,9 @@ func (p *Program) RestoreTerminal() error {
p.renderer.hideCursor()
p.renderer.execute(ansi.EnableBracketedPaste)
p.bpActive = true
if p.kittyFlags != 0 {
p.renderer.execute(ansi.PushKittyKeyboard(p.kittyFlags))
}
}

// If the output is a terminal, it may have been resized while another
Expand Down
3 changes: 3 additions & 0 deletions tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ func (p *Program) restoreTerminalState() error {
p.bpActive = false
p.renderer.showCursor()
p.disableMouse()
if p.kittyFlags != 0 {
p.renderer.execute(ansi.DisableKittyKeyboard)
}

if p.renderer.altScreen() {
p.renderer.exitAltScreen()
Expand Down

0 comments on commit 8e406ff

Please sign in to comment.