charmbracelet / charmbracelet/bubbletea
v2: difficult to match modifier+shifted key
- Dominant language
- Go
- Stars
- 44.9k
- Forks
- 1.3k
- PR merge metrics
- No merged PRs in 30d
Description
**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
```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.
Contributor guide
Research direction
Start with the keydbg.go reproduction and trace how Bubble Tea requests and configures Kitty keyboard enhancements by default. Reproduce alt-? with the three reported configurations, then verify that the resulting keyboard enhancement controls let modifier-plus-shifted keys be matched reliably.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100