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

(v2) fix keyboard enhancements msg, expose types and track requested and active enhancements #1286

Merged
merged 7 commits into from
Jan 16, 2025
5 changes: 4 additions & 1 deletion examples/print-key/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ func (m model) Init() (tea.Model, tea.Cmd) {
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyboardEnhancementsMsg:
return m, tea.Printf("Keyboard enhancements enabled! ReleaseKeys: %v\n", msg.SupportsKeyReleases())
return m, tea.Printf("Keyboard enhancements: Disambiguation: %v, ReleaseKeys: %v, Uniform keys: %v\n",
msg.SupportsKeyDisambiguation(),
msg.SupportsKeyReleases(),
msg.SupportsUniformKeyLayout())
case tea.KeyMsg:
key := msg.Key()
switch msg := msg.(type) {
Expand Down
12 changes: 11 additions & 1 deletion input.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

// translateInputEvent translates an input event into a Bubble Tea Msg.
func translateInputEvent(e input.Event) Msg {
func (p *Program) translateInputEvent(e input.Event) Msg {
switch e := e.(type) {
case input.ClipboardEvent:
switch e.Selection {
Expand Down Expand Up @@ -50,6 +50,16 @@ func translateInputEvent(e input.Event) Msg {
return CapabilityMsg(e)
case input.TerminalVersionEvent:
return TerminalVersionMsg(e)
case input.KittyEnhancementsEvent:
return KeyboardEnhancementsMsg{
kittyFlags: int(e),
modifyOtherKeys: p.activeEnhancements.modifyOtherKeys,
}
case input.ModifyOtherKeysEvent:
return KeyboardEnhancementsMsg{
modifyOtherKeys: int(e),
kittyFlags: p.activeEnhancements.kittyFlags,
}
}
return nil
}
45 changes: 29 additions & 16 deletions keyboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
"github.com/charmbracelet/x/ansi"
)

// keyboardEnhancements is a type that represents a set of keyboard
// KeyboardEnhancements is a type that represents a set of keyboard
// enhancements.
type keyboardEnhancements struct {
type KeyboardEnhancements struct {
// Kitty progressive keyboard enhancements protocol. This can be used to
// enable different keyboard features.
//
// - 0: disable all features
// - 1: [ansi.DisambiguateEscapeCodes] Disambiguate escape codes such as
// - 1: [ansi.KittyDisambiguateEscapeCodes] Disambiguate escape codes such as
// ctrl+i and tab, ctrl+[ and escape, ctrl+space and ctrl+@, etc.
// - 2: [ansi.ReportEventTypes] Report event types such as key presses,
// - 2: [ansi.KittyReportEventTypes] Report event types such as key presses,
// releases, and repeat events.
// - 4: [ansi.ReportAlternateKeys] Report keypresses as though they were
// - 4: [ansi.KittyReportAlternateKeys] Report keypresses as though they were
// on a PC-101 ANSI US keyboard layout regardless of what they layout
// actually is. Also include information about whether or not is enabled,
// - 8: [ansi.ReportAllKeysAsEscapeCodes] Report all key events as escape
// - 8: [ansi.KittyReportAllKeysAsEscapeCodes] Report all key events as escape
// codes. This includes simple printable keys like "a" and other Unicode
// characters.
// - 16: [ansi.ReportAssociatedText] Report associated text with key
// - 16: [ansi.KittyReportAssociatedKeys] Report associated text with key
// events. This encodes multi-rune key events as escape codes instead of
// individual runes.
//
Expand All @@ -38,15 +38,15 @@
modifyOtherKeys int
}

// KeyboardEnhancement is a type that represents a keyboard enhancement.
type KeyboardEnhancement func(k *keyboardEnhancements)
// KeyboardEnhancementOption is a type that represents a keyboard enhancement.
type KeyboardEnhancementOption func(k *KeyboardEnhancements)

// WithKeyReleases enables support for reporting release key events. This is
// useful for terminals that support the Kitty keyboard protocol "Report event
// types" progressive enhancement feature.
//
// Note that not all terminals support this feature.
func WithKeyReleases(k *keyboardEnhancements) {
func WithKeyReleases(k *KeyboardEnhancements) {
k.kittyFlags |= ansi.KittyReportEventTypes
}

Expand All @@ -57,26 +57,39 @@
// progressive enhancement features.
//
// Note that not all terminals support this feature.
func WithUniformKeyLayout(k *keyboardEnhancements) {
func WithUniformKeyLayout(k *KeyboardEnhancements) {
k.kittyFlags |= ansi.KittyReportAlternateKeys | ansi.KittyReportAllKeysAsEscapeCodes
}

// withKeyDisambiguation enables support for disambiguating keyboard escape
// codes. This is useful for terminals that support the Kitty keyboard protocol
// "Disambiguate escape codes" progressive enhancement feature or the XTerm
// modifyOtherKeys mode 1 feature to report ambiguous keys as escape codes.
func withKeyDisambiguation(k *keyboardEnhancements) {
func withKeyDisambiguation(k *KeyboardEnhancements) {
k.kittyFlags |= ansi.KittyDisambiguateEscapeCodes
if k.modifyOtherKeys < 1 {
k.modifyOtherKeys = 1
}
}

type enableKeyboardEnhancementsMsg []KeyboardEnhancement
type enableKeyboardEnhancementsMsg []KeyboardEnhancementOption

// EnableKeyboardEnhancements is a command that enables keyboard enhancements
// RequestKeyboardEnhancements is a command that enables keyboard enhancements
// in the terminal.
func EnableKeyboardEnhancements(enhancements ...KeyboardEnhancement) Cmd {
//
// This command can be used to request specific keyboard enhancements. Use this
// command to request enabling support for key disambiguation. You can also
// request other enhancements by passing additional options. For example:
//
// - [WithKeyReleases] enables support for reporting release key events.
// - [WithUniformKeyLayout] enables support for reporting key events as though
// they were on a PC-101 layout.
//
// If the terminal supports the requested enhancements, it will send a
// [KeyboardEnhancementsMsg] message with the supported enhancements.
//
// Note that not all terminals support all enhancements.
func RequestKeyboardEnhancements(enhancements ...KeyboardEnhancementOption) Cmd {
return func() Msg {
return enableKeyboardEnhancementsMsg(append(enhancements, withKeyDisambiguation))
}
Expand All @@ -92,12 +105,12 @@

// KeyboardEnhancementsMsg is a message that gets sent when the terminal
// supports keyboard enhancements.
type KeyboardEnhancementsMsg keyboardEnhancements
type KeyboardEnhancementsMsg KeyboardEnhancements

// SupportsKeyDisambiguation returns whether the terminal supports reporting
// disambiguous keys as escape codes.
func (k KeyboardEnhancementsMsg) SupportsKeyDisambiguation() bool {
if runtime.GOOS == "windows" {

Check failure on line 113 in keyboard.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (ubuntu-latest)

string `windows` has 7 occurrences, make it a constant (goconst)

Check failure on line 113 in keyboard.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (ubuntu-latest)

string `windows` has 7 occurrences, make it a constant (goconst)

Check failure on line 113 in keyboard.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (macos-latest)

string `windows` has 7 occurrences, make it a constant (goconst)

Check failure on line 113 in keyboard.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (macos-latest)

string `windows` has 7 occurrences, make it a constant (goconst)

Check failure on line 113 in keyboard.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (windows-latest)

string `windows` has 7 occurrences, make it a constant (goconst)

Check failure on line 113 in keyboard.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (windows-latest)

string `windows` has 7 occurrences, make it a constant (goconst)
// We use Windows Console API which supports reporting disambiguous keys.
return true
}
Expand Down
6 changes: 3 additions & 3 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,14 +245,14 @@
//
// This is not supported on all terminals. On Windows, these features are
// enabled by default.
func WithKeyboardEnhancements(enhancements ...KeyboardEnhancement) ProgramOption {
var ke keyboardEnhancements
func WithKeyboardEnhancements(enhancements ...KeyboardEnhancementOption) ProgramOption {
var ke KeyboardEnhancements
for _, e := range append(enhancements, withKeyDisambiguation) {
e(&ke)
}
return func(p *Program) {
p.startupOptions |= withKeyboardEnhancements
p.keyboard = ke
p.requestedEnhancements = ke
}
}

Expand Down Expand Up @@ -283,7 +283,7 @@
}
}

// WithFerociousRenderer tells Bubble Tea to use the new shiny "ferocious"

Check failure on line 286 in options.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (ubuntu-latest)

Comment should end in a period (godot)

Check failure on line 286 in options.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (ubuntu-latest)

Comment should end in a period (godot)

Check failure on line 286 in options.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (macos-latest)

Comment should end in a period (godot)

Check failure on line 286 in options.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (macos-latest)

Comment should end in a period (godot)

Check failure on line 286 in options.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (windows-latest)

Comment should end in a period (godot)

Check failure on line 286 in options.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (windows-latest)

Comment should end in a period (godot)
// renderer. This renderer is experimental and may change or be removed in
// future versions.
//
Expand Down
4 changes: 2 additions & 2 deletions screen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ func TestClearMsg(t *testing.T) {
// Windows supports enhanced keyboard features through the Windows API, not through ANSI sequences.
tests = append(tests, test{
name: "kitty_start_windows",
cmds: []Cmd{DisableKeyboardEnhancements, EnableKeyboardEnhancements(WithKeyReleases)},
cmds: []Cmd{DisableKeyboardEnhancements, RequestKeyboardEnhancements(WithKeyReleases)},
})
} else {
tests = append(tests, test{
name: "kitty_start_other",
cmds: []Cmd{DisableKeyboardEnhancements, EnableKeyboardEnhancements(WithKeyReleases)},
cmds: []Cmd{DisableKeyboardEnhancements, RequestKeyboardEnhancements(WithKeyReleases)},
})
}

Expand Down
106 changes: 73 additions & 33 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,15 @@ type Program struct {
// rendererDone is used to stop the renderer.
rendererDone chan struct{}

keyboard keyboardEnhancements
// stores the requested keyboard enhancements.
requestedEnhancements KeyboardEnhancements
// activeEnhancements stores the active keyboard enhancements read from the
// terminal.
activeEnhancements KeyboardEnhancements

// keyboardc is used to signal that the keyboard enhancements have been
// read from the terminal.
keyboardc chan struct{}

// When a program is suspended, the terminal state is saved and the program
// is paused. This saves the terminal colors state so they can be restored
Expand Down Expand Up @@ -278,6 +286,7 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
initialModel: model,
msgs: make(chan Msg),
rendererDone: make(chan struct{}),
keyboardc: make(chan struct{}),
modes: ansi.Modes{},
}

Expand Down Expand Up @@ -555,12 +564,13 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
p.execute(ansi.RequestCursorColor)

case KeyboardEnhancementsMsg:
if p.keyboard.kittyFlags != msg.kittyFlags {
p.keyboard.kittyFlags |= msg.kittyFlags
}
if p.keyboard.modifyOtherKeys == 0 || msg.modifyOtherKeys > p.keyboard.modifyOtherKeys {
p.keyboard.modifyOtherKeys = msg.modifyOtherKeys
}
p.activeEnhancements.kittyFlags = msg.kittyFlags
p.activeEnhancements.modifyOtherKeys = msg.modifyOtherKeys

go func() {
// Signal that we've read the keyboard enhancements.
p.keyboardc <- struct{}{}
}()

case enableKeyboardEnhancementsMsg:
if runtime.GOOS == "windows" {
Expand All @@ -569,22 +579,21 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
break
}

var ke keyboardEnhancements
var ke KeyboardEnhancements
for _, e := range msg {
e(&ke)
}

p.keyboard.kittyFlags |= ke.kittyFlags
if ke.modifyOtherKeys > p.keyboard.modifyOtherKeys {
p.keyboard.modifyOtherKeys = ke.modifyOtherKeys
p.requestedEnhancements.kittyFlags |= ke.kittyFlags
if ke.modifyOtherKeys > p.requestedEnhancements.modifyOtherKeys {
p.requestedEnhancements.modifyOtherKeys = ke.modifyOtherKeys
}

if p.keyboard.modifyOtherKeys > 0 {
p.execute(ansi.ModifyOtherKeys(p.keyboard.modifyOtherKeys))
}
if p.keyboard.kittyFlags > 0 {
p.execute(ansi.PushKittyKeyboard(p.keyboard.kittyFlags))
}
p.requestKeyboardEnhancements()

// Ensure we send a message so that terminals that don't support the
// requested features can disable them.
go p.sendKeyboardEnhancementsMsg()

case disableKeyboardEnhancementsMsg:
if runtime.GOOS == "windows" {
Expand All @@ -593,13 +602,15 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
break
}

if p.keyboard.modifyOtherKeys > 0 {
if p.activeEnhancements.modifyOtherKeys > 0 {
p.execute(ansi.DisableModifyOtherKeys)
p.keyboard.modifyOtherKeys = 0
p.activeEnhancements.modifyOtherKeys = 0
p.requestedEnhancements.modifyOtherKeys = 0
}
if p.keyboard.kittyFlags > 0 {
if p.activeEnhancements.kittyFlags > 0 {
p.execute(ansi.DisableKittyKeyboard)
p.keyboard.kittyFlags = 0
p.activeEnhancements.kittyFlags = 0
p.requestedEnhancements.kittyFlags = 0
}

case execMsg:
Expand Down Expand Up @@ -828,15 +839,11 @@ func (p *Program) Run() (Model, error) {
if p.startupOptions&withKeyboardEnhancements != 0 && runtime.GOOS != "windows" {
// We use the Windows Console API which supports keyboard
// enhancements.
p.requestKeyboardEnhancements()

if p.keyboard.modifyOtherKeys > 0 {
p.execute(ansi.ModifyOtherKeys(p.keyboard.modifyOtherKeys))
p.execute(ansi.RequestModifyOtherKeys)
}
if p.keyboard.kittyFlags > 0 {
p.execute(ansi.PushKittyKeyboard(p.keyboard.kittyFlags))
p.execute(ansi.RequestKittyKeyboard)
}
// Ensure we send a message so that terminals that don't support the
// requested features can disable them.
go p.sendKeyboardEnhancementsMsg()
}

// Start the renderer.
Expand Down Expand Up @@ -1006,11 +1013,11 @@ func (p *Program) RestoreTerminal() error {
if p.modes.IsSet(ansi.BracketedPasteMode) {
p.execute(ansi.SetBracketedPasteMode)
}
if p.keyboard.modifyOtherKeys != 0 {
p.execute(ansi.ModifyOtherKeys(p.keyboard.modifyOtherKeys))
if p.activeEnhancements.modifyOtherKeys != 0 {
p.execute(ansi.ModifyOtherKeys(p.activeEnhancements.modifyOtherKeys))
}
if p.keyboard.kittyFlags != 0 {
p.execute(ansi.PushKittyKeyboard(p.keyboard.kittyFlags))
if p.activeEnhancements.kittyFlags != 0 {
p.execute(ansi.PushKittyKeyboard(p.activeEnhancements.kittyFlags))
}
if p.modes.IsSet(ansi.FocusEventMode) {
p.execute(ansi.SetFocusEventMode)
Expand Down Expand Up @@ -1121,3 +1128,36 @@ func (p *Program) stopRenderer(kill bool) {

p.renderer.close() //nolint:errcheck
}

// sendKeyboardEnhancementsMsg sends a message with the active keyboard
// enhancements to the program after a short timeout, or immediately if the
// keyboard enhancements have been read from the terminal.
func (p *Program) sendKeyboardEnhancementsMsg() {
if runtime.GOOS == "windows" {
// We use the Windows Console API which supports keyboard enhancements.
p.Send(KeyboardEnhancementsMsg{})
return
}

// Initial keyboard enhancements message. Ensure we send a message so that
// terminals that don't support the requested features can disable them.
const timeout = 100 * time.Millisecond
select {
case <-time.After(timeout):
p.Send(KeyboardEnhancementsMsg{})
case <-p.keyboardc:
}
}

// requestKeyboardEnhancements tries to enable keyboard enhancements and read
// the active keyboard enhancements from the terminal.
func (p *Program) requestKeyboardEnhancements() {
if p.requestedEnhancements.modifyOtherKeys > 0 {
p.execute(ansi.ModifyOtherKeys(p.requestedEnhancements.modifyOtherKeys))
p.execute(ansi.RequestModifyOtherKeys)
}
if p.requestedEnhancements.kittyFlags > 0 {
p.execute(ansi.PushKittyKeyboard(p.requestedEnhancements.kittyFlags))
p.execute(ansi.RequestKittyKeyboard)
}
}
4 changes: 2 additions & 2 deletions testdata/TestClearMsg/kitty_start_other.golden
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[?2004h[>4;1m[>3u[?25l
[?2004h[>4;1m[?4m[>3u[?u[?25l

Msuccess
[?25h[?2004l[>4;0m[>u
[?25h[?2004l
Loading
Loading