charmbracelet / charmbracelet/bubbletea

v2: difficult to match modifier+shifted key

Open
#1,591 2 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.