Describe the bug
Note: I haven't used v2 of this library much yet, so it is very possible I've just missed the obvious solution here.
Bubbletea enables ansi.KittyDisambiguateEscapeCodes by default but doesn't enable ansi.KittyReportAlternateKeys. This can make it hard to match combinations that use both a modifier key and a shifted key.
For example, #1390 describes a problem in which the ? character was hard to match because it was decomposed to shift+/. The solution there was to have been the addition of the String() vs Keystroke() method. But when an additional modifier is in use (such as alt-?), it once against becomes problematic to match (see reproduction below).
To Reproduce
Using the program below, you can see the messages delivered for a key such as alt-?:
With bubbletea's default's:
KeyPress Code=U+002F(/) ShiftedCode=U+0000 BaseCode=U+0000 Mod=3 Text="" String="alt+shift+/" Keystroke="alt+shift+/"
With ReportAlternateKeys:
KeyPress Code=U+002F(/) ShiftedCode=U+003F(?) BaseCode=U+0000 Mod=3 Text="" String="alt+shift+/" Keystroke="alt+shift+/"
With keyboard enhancements disabled:
KeyPress Code=U+003F(?) ShiftedCode=U+0000 BaseCode=U+0000 Mod=2 Text="" String="alt+?" Keystroke="alt+?"
keydbg.go
// keydbg is a minimal bubbletea app that prints the full details of
// every key event it receives.
//
// Usage: go run keydbg.go
package main
import (
"flag"
"fmt"
"os"
"strings"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/x/ansi"
)
var (
noKitty = flag.Bool("nokitty", false, "disable Kitty keyboard protocol on startup")
kittyAltKeys = flag.Bool("kittyaltkeys", false, "enable Kitty alternate keys and associated text reporting")
)
type model struct {
lines []string
kittyUpdateOnce bool
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(imsg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := imsg.(type) {
case tea.KeyboardEnhancementsMsg:
// Wait for bubbletea to send its KeyboardEnhancement before we send ours.
line := fmt.Sprintf("KeyboardEnhancementsMsg: %+v", msg)
m.lines = append(m.lines, line)
if !m.kittyUpdateOnce {
m.kittyUpdateOnce = true
if *noKitty {
_, _ = os.Stdout.WriteString(ansi.PopKittyKeyboard(1))
m.lines = append(m.lines, " -> sent pop to disable Kitty protocol")
} else if *kittyAltKeys {
flags := ansi.KittyDisambiguateEscapeCodes | ansi.KittyReportAlternateKeys | ansi.KittyReportAssociatedKeys
_, _ = os.Stdout.WriteString(ansi.PopKittyKeyboard(1) + ansi.PushKittyKeyboard(flags))
m.lines = append(m.lines, fmt.Sprintf(" -> replaced kitty flags with %d (ReportAlternateKeys|ReportAssociatedKeys)", flags))
}
}
case tea.KeyPressMsg:
k := msg.Key()
line := fmt.Sprintf(
"KeyPress Code=%s ShiftedCode=%s BaseCode=%s Mod=%-6v Text=%-6q String=%-12q Keystroke=%q",
fmtCode(k.Code), fmtCode(k.ShiftedCode), fmtCode(k.BaseCode),
k.Mod, k.Text,
msg.String(), msg.Keystroke(),
)
m.lines = append(m.lines, line)
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
case tea.KeyReleaseMsg:
k := msg.Key()
line := fmt.Sprintf(
"KeyRelease Code=%s ShiftedCode=%s BaseCode=%s Mod=%-6v Text=%-6q String=%-12q Keystroke=%q",
fmtCode(k.Code), fmtCode(k.ShiftedCode), fmtCode(k.BaseCode),
k.Mod, k.Text,
msg.String(), msg.Keystroke(),
)
m.lines = append(m.lines, line)
}
const maxLines = 24
if len(m.lines) > maxLines {
m.lines = m.lines[len(m.lines)-maxLines:]
}
return m, nil
}
func (m model) View() tea.View {
var b strings.Builder
b.WriteString("keydbg — press keys to inspect events, ctrl+c to quit\n")
b.WriteString(strings.Repeat("─", 80))
b.WriteString("\n")
for _, l := range m.lines {
b.WriteString(l)
b.WriteString("\n")
}
return tea.NewView(b.String())
}
func fmtCode(r rune) string {
if r >= 0x20 && r < 0x7f {
return fmt.Sprintf("%-10s", fmt.Sprintf("%U(%s)", r, string(r)))
}
return fmt.Sprintf("%-10s", fmt.Sprintf("%U", r))
}
func main() {
flag.Parse()
m := model{}
p := tea.NewProgram(m, tea.WithoutSignalHandler())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
Expected behavior
One nice solution might be to have more control over the requested keyboard enhancements.
Describe the bug
Note: I haven't used v2 of this library much yet, so it is very possible I've just missed the obvious solution here.
Bubbletea enables
ansi.KittyDisambiguateEscapeCodesby default but doesn't enableansi.KittyReportAlternateKeys. This can make it hard to match combinations that use both a modifier key and a shifted key.For example, #1390 describes a problem in which the
?character was hard to match because it was decomposed toshift+/. The solution there was to have been the addition of the String() vs Keystroke() method. But when an additional modifier is in use (such asalt-?), it once against becomes problematic to match (see reproduction below).To Reproduce
Using the program below, you can see the messages delivered for a key such as
alt-?:With bubbletea's default's:
With ReportAlternateKeys:
With keyboard enhancements disabled:
keydbg.go
Expected behavior
One nice solution might be to have more control over the requested keyboard enhancements.