Skip to content

v2: difficult to match modifier+shifted key #1591

@stevendanna

Description

@stevendanna

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions