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

feat: add kitty keyboard options and settings #1083

Merged
merged 7 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 @@
}
}
}
// 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 @@
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
Loading