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

Proposal: use x/input to handle input events #1014

Open
aymanbagabas opened this issue May 10, 2024 · 0 comments
Open

Proposal: use x/input to handle input events #1014

aymanbagabas opened this issue May 10, 2024 · 0 comments
Labels
proposal Introducing an API change

Comments

@aymanbagabas
Copy link
Member

aymanbagabas commented May 10, 2024

Current Bubble Tea input handling is quite limiting as it doesn't support parsing input events such as cursor position report, fixterms & Kitty keyboard, and other terminal features. This is because we only do map lookups for common key sequences used by most terminals.

This is where the work on charmbracelet/x/input began. This new input package can replace the existing Bubble Tea input handler to enhance and improve parsing input events and support. This means we can support more terminal features since we can now parse input sequences such as the ones found in the Kitty keyboard protocol and others.

Advanced Bubble Tea Input Handling

This gives us high fidelity input events like key up events and formerly unavailable keybindings like shift+enter!

So what’s new?

Well the big news is that, in modern terminals, you are freed from the shackles of old terminal ways and now have access to a bunch of key and mouse commands that weren’t previously available. That’s right the shift+enter dream is now a reality. Bubble Tea game engine? Now you can.

Mouse events also received an upgrade, now you can distingiush between mouse events based on the msg type.

switch msg := msg.(type) {
case tea.KeyDownMsg:
    switch msg.String() {
    case "space":
        // Push-to-talk: mic on
    }
case tea.KeyUpMsg:
    switch msg.String() {
    case "space":
        // Push-to-talk: mic off
    }
case tea.MouseDownMsg:
    // Pen: start drawing
case tea.MouseUpMsg:
    // Pen: stop drawing
case tea.MouseWheelMsg:
    // View: scroll up/down/left/right?
case tea.MouseMotionMsg:
    // Drag-n-drop
}

You can still use tea.KeyMsg since it's just an alias to tea.KeyDownMsg. The same goes for tea.MouseMsg and tea.MouseDownMsg.

Keep in mind that, at the moment, the enhanced keyboard is only supported on Windows and the following terminals…

  • Ghostty
  • Kitty
  • Alacrity
  • iTerm2
  • Foot
  • WezTerm
  • Rio
  • Windows Terminal

Note: on non-Windows platforms, the enhanced keyboard relies on the Kitty Keyboard protocol which all the above (except Windows Terminal) support. In case of an unsupported terminal, the application won't get tea.KeyUpMsgs. You can check ctx.SupportsEnhancedKeyboard() from the Bubble Tea Context proposal to check whether the terminal supports enhanced keyboard.

Common keys are still the same except that the KeyCtrl and KeyShift variants and the key.Alt property are now merged into tea.KeyMod. Here's a some of the common keys available.

	KeyBackspace
	KeyTab
	KeyEnter
	KeyEscape
	KeyUp
	KeyDown
	KeyRight
	KeyLeft
	KeyBegin
	KeyFind
	KeyInsert
	KeySelect
	KeyPgUp
	KeyPgDown
	KeyHome
	KeyEnd
	KeyF1
	KeyF2
	KeyF3
	KeyF4
	KeyF5
	KeyF6
	KeyF7
	KeyF8
	KeyF9
	KeyF10
	...

How to upgrade

If you’re matching strings, there’s nothing to do:

switch msg.String() {
case "ctrl+a":
    // Gotcha!
}

Special keys are now indicated in tea.KeySym:

// Before
switch msg.Type {
case tea.KeyEnter:
    // Enter!
case tea.KeyEsc:
    // Escape!
case tea.KeyBackspace:
    // Back!
}

// After
switch msg.Sym {
case tea.KeyEnter:
    // Enter!
case tea.KeyEsc:
    // Escape!
case tea.KeyBackspace:
    // Back!
}

If you’re going for ultimate safety:

// Before
switch msg.Type {
case tea.KeyCtrlA:
    // Ceçi
case tea.KeyCtrlB:
    // Cela
}

// After
if msg.Sym == KeyNone && msg.Mod == tea.Ctrl {
    switch msg.Rune {
    case 'a':
        // Ceçi
    case 'b':
        // Cela
    }
}

If you're matching a modifier key:

// Before
if msg.Alt {
    // alt pressed
}

// After
if msg.Mod.IsAlt() {
    // alt pressed
}

If you're matching multiple modifier keys:

// Before
switch msg.Type {
case tea.KeyCtrlA:
    if msg.Alt {
        // ctrl+alt+a
    }
}

// After
if msg.Rune == 'a' && msg.Mod == tea.Ctrl|tea.Alt {
    // ctrl+alt+a
}

If you’re matching runes:

// Before
switch msg.Type {
case tea.Runes == []rune{'a'}:
    // The letter a
}

// After
switch msg.Type {
case tea.Rune == 'a':
    // The letter a
}

If bracketed paste is enabled, which it is by default, you now need to handle pastes like so:

switch msg {
case tea.PasteMsg:
    // Here’s a big ol’ paste!
}

Matching against mouse events is almost the same. We've dropped msg.Action and msg.Ctrl|Alt|Shift in favor of msg.Mod. The action is now split into different types and the modifiers are combinied into a single field msg.Mod.

A mouse message can have on of the following msg.Buttons:

	MouseNone MouseButton = iota
	MouseLeft
	MouseMiddle
	MouseRight
	MouseWheelUp
	MouseWheelDown
	MouseWheelLeft
	MouseWheelRight
	MouseBackward
	MouseForward
	MouseExtra1
	MouseExtra2

Use the fmt.Stringer interface to easily match against different button combos:

switch msg := msg.(type) {
case tea.MouseMsg:
    switch msg.String() {
    case "left":
        // left press
    case "ctrl+left":
        // ctrl+left press
    case "ctrl+alt+shift+left":
        // ctrl+alt+shift+left press
    }
}

You can now use the same string matchin to match against release, motion, and wheel events.

// Before
switch msg := msg.(type) {
case tea.MouseMsg:
    if msg.Action != tea.MouseRelease {
        break
    }
    switch msg.String() {
    case "left release":
        // left release
    case "ctrl+left release":
        // ctrl+left release
    case "ctrl+alt+shift+left release":
        // ctrl+alt+shift+left release
    }
}

// After
switch msg := msg.(type) {
case tea.MouseUpMsg:
    switch msg.String() {
    case "left":
        // left release
    case "ctrl+left":
        // ctrl+left 
    case "ctrl+alt+shift+left":
        // whoa!
    }
}
// Simpler, eh?

If you're matching against mouse clicks:

// Before
switch msg := msg.(type) {
case tea.MouseMsg:
    xPos, yPos := msg.X, msg.Y
}

// After
switch msg := msg.(type) {
case tea.MouseDownMsg:
    xPos, yPos := msg.X, msg.Y
}

Wanna also capture wheel events?

// Before
switch msg := msg.(type) {
case tea.MouseMsg:
    xPos, yPos := msg.X, msg.Y
    if msg.IsWheel() {
        // scroll down
    }
}

// After
switch msg := msg.(type) {
case tea.MouseDownMsg:
    xPos, yPos := msg.X, msg.Y
case tea.MouseWheelMsg:
    // scroll down
}

Track mouse movements?

// Before
switch msg := msg.(type) {
case tea.MouseMsg:
    if msg.Action == tea.MouseMotion {
        xPos, yPos := msg.X, msg.Y
    }
}

// After
switch msg := msg.(type) {
case tea.MouseMotionMsg:
    xPos, yPos := msg.X, msg.Y
}

If you want to detect modifier keys:

// Before
switch msg := msg.(type) {
case tea.MouseMsg:
    if msg.Shift {
        // shift key
    }
}

// After
switch msg := msg.(type) {
case tea.MouseDownMsg:
    if msg.Mod.IsShift() {
        // shift key
    }
}

You can try this proposal in the beta branch here

@aymanbagabas aymanbagabas added the proposal Introducing an API change label May 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal Introducing an API change
Projects
None yet
Development

No branches or pull requests

1 participant