Skip to content

Commit

Permalink
(v2) Use KeyMsg/MouseMsg interfaces (#1111)
Browse files Browse the repository at this point in the history
This replaces KeyMsg and MouseMsg with interfaces that catch their
respective types. For example, both KeyPressMsg and KeyReleaseMsg
implement KeyMsg. Same goes for MouseClickMsg, MouseReleaseMsg, etc,
they all implement MouseMsg. This makes it possible to switch on the
interface type to catch all events of the same input message type.

New API:

```go

// KeyMsg represents a key event. This can be either a key press or a key
// release event.
type KeyMsg interface {
	fmt.Stringer

	// Key returns the underlying key event.
	Key() Key
}


// Key represents a Key press or release event. It contains information about
// the Key pressed, like the runes, the type of Key, and the modifiers pressed.
// There are a couple general patterns you could use to check for key presses
// or releases:
//
//	// Switch on the string representation of the key (shorter)
//	switch msg := msg.(type) {
//	case KeyPressMsg:
//	    switch msg.String() {
//	    case "enter":
//	        fmt.Println("you pressed enter!")
//	    case "a":
//	        fmt.Println("you pressed a!")
//	    }
//	}
//
//	// Switch on the key type (more foolproof)
//	switch msg := msg.(type) {
//	case KeyMsg:
//	    // catch both KeyPressMsg and KeyReleaseMsg
//	    switch key := msg.Key(); key.Code {
//	    case KeyEnter:
//	        fmt.Println("you pressed enter!")
//	    default:
//	        switch key.Text {
//	        case "a":
//	            fmt.Println("you pressed a!")
//	        }
//	    }
//	}
//
// Note that [Key.Text] will be empty for special keys like [KeyEnter],
// [KeyTab], and for keys that don't represent printable characters like key
// combos with modifier keys. In other words, [Key.Text] is populated only for
// keys that represent printable characters shifted or unshifted (like 'a',
// 'A', '1', '!', etc.).
type Key struct {
	// Text contains the actual characters received. This usually the same as
	// [Key.Code]. When [Key.Text] is non-empty, it indicates that the key
	// pressed represents printable character(s).
	Text string

	// Mod represents modifier keys, like [ModCtrl], [ModAlt], and so on.
	Mod KeyMod

	// Code represents the key pressed. This is usually a special key like
	// [KeyTab], [KeyEnter], [KeyF1], or a printable character like 'a'.
	Code rune

	// ShiftedCode is the actual, shifted key pressed by the user. For example,
	// if the user presses shift+a, or caps lock is on, [Key.ShiftedCode] will
	// be 'A' and [Key.Code] will be 'a'.
	//
	// In the case of non-latin keyboards, like Arabic, [Key.ShiftedCode] is the
	// unshifted key on the keyboard.
	//
	// This is only available with the Kitty Keyboard Protocol or the Windows
	// Console API.
	ShiftedCode rune

	// BaseCode is the key pressed according to the standard PC-101 key layout.
	// On international keyboards, this is the key that would be pressed if the
	// keyboard was set to US PC-101 layout.
	//
	// For example, if the user presses 'q' on a French AZERTY keyboard,
	// [Key.BaseCode] will be 'q'.
	//
	// This is only available with the Kitty Keyboard Protocol or the Windows
	// Console API.
	BaseCode rune

	// IsRepeat indicates whether the key is being held down and sending events
	// repeatedly.
	//
	// This is only available with the Kitty Keyboard Protocol or the Windows
	// Console API.
	IsRepeat bool
}


// MouseMsg represents a mouse message. This is a generic mouse message that
// can represent any kind of mouse event.
type MouseMsg interface {
	fmt.Stringer

	// Mouse returns the underlying mouse event.
	Mouse() Mouse
}

// Mouse represents a Mouse message. Use [MouseMsg] to represent all mouse
// messages.
//
// The X and Y coordinates are zero-based, with (0,0) being the upper left
// corner of the terminal.
//
//	// Catch all mouse events
//	switch msg := msg.(type) {
//	case MouseMsg:
//	    m := msg.Mouse()
//	    fmt.Println("Mouse event:", m.X, m.Y, m)
//	}
//
//	// Only catch mouse click events
//	switch msg := msg.(type) {
//	case MouseClickMsg:
//	    fmt.Println("Mouse click event:", msg.X, msg.Y, msg)
//	}
type Mouse struct {
	X, Y   int
	Button MouseButton
	Mod    KeyMod
}

```

TODO: update examples (in a separate PR)
  • Loading branch information
aymanbagabas authored Aug 30, 2024
2 parents 537c80b + 2f6637b commit 8a75439
Show file tree
Hide file tree
Showing 15 changed files with 894 additions and 1,327 deletions.
8 changes: 4 additions & 4 deletions driver_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ loop:
for i, e := range events {
switch e := e.(type) {
case KeyPressMsg:
switch e.Rune() {
switch e.Code {
case ansi.ESC, ansi.CSI, ansi.OSC, ansi.DCS, ansi.APC:
// start of a sequence
if start == -1 {
Expand All @@ -85,7 +85,7 @@ loop:
for i := start; i <= end; i++ {
switch e := events[i].(type) {
case KeyPressMsg:
seq = append(seq, byte(e.Rune()))
seq = append(seq, byte(e.Code))
}
}

Expand Down Expand Up @@ -127,7 +127,7 @@ func parseConInputEvent(event xwindows.InputRecord, buttonState *uint32, windowS
// (e.g. function keys, arrows, etc.)
// Otherwise, try to translate it to a rune based on the active keyboard
// layout.
if len(key.Runes) == 0 {
if len(key.Text) == 0 {
return event
}

Expand Down Expand Up @@ -163,7 +163,7 @@ func parseConInputEvent(event xwindows.InputRecord, buttonState *uint32, windowS
return event
}

key.baseRune = runes[0]
key.BaseCode = runes[0]
if kevent.KeyDown {
return KeyPressMsg(key)
}
Expand Down
277 changes: 132 additions & 145 deletions key.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,32 @@
package tea

// KeyType indicates whether the key is a special key or runes. Special
// keys are things like KeyEnter, KeyBackspace, and so on. Runes keys are just
// regular characters like 'a', '你', 'ض', '🦄', and so on.
//
// k := Key{Type: KeyRunes, Runes: []rune{'A'}, Mod: ModShift}
// if k.Type == KeyRunes {
//
// fmt.Println(k.Runes)
// // Output: A
//
// fmt.Println(k.String())
// // Output: shift+a
//
// }
type KeyType int

// Special key symbols.
const (
// KeyRunes indicates that the key represents rune(s), like 'a', 'b', 'c',
// and so on.
KeyRunes KeyType = iota

// Special names in C0
import (
"fmt"
"strings"
"unicode"

KeyBackspace
KeyTab
KeyEnter
KeyEscape
"github.com/charmbracelet/x/ansi"
)

// Special names in G0
const (
// KeyExtended is a special key code used to signify that a key event
// contains multiple runes.
KeyExtended = unicode.MaxRune + 1
)

KeySpace
KeyDelete
// Special key symbols.
const (

// Special keys

Check failure on line 20 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)

KeyUp
KeyUp rune = KeyExtended + iota + 1
KeyDown
KeyRight
KeyLeft
KeyBegin
KeyFind
KeyInsert
KeyDelete
KeySelect
KeyPgUp
KeyPgDown
Expand Down Expand Up @@ -190,11 +174,65 @@ const (
KeyRightMeta
KeyIsoLevel3Shift
KeyIsoLevel5Shift

// Special names in C0

Check failure on line 178 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)

Check failure on line 178 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)

KeyBackspace = rune(ansi.DEL)
KeyTab = rune(ansi.HT)
KeyEnter = rune(ansi.CR)
KeyReturn = KeyEnter
KeyEscape = rune(ansi.ESC)
KeyEsc = KeyEscape

// Special names in G0

Check failure on line 187 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)

Check failure on line 187 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)

KeySpace = rune(ansi.SP)
)

// Key contains information about a key or release. Keys are always sent to the
// program's update function. There are a couple general patterns you could use
// to check for key presses or releases:
// KeyPressMsg represents a key press message.
type KeyPressMsg Key

// String implements [fmt.Stringer] and is quite useful for matching key
// events. For details, on what this returns see [Key.String].
func (k KeyPressMsg) String() string {
return Key(k).String()
}

// Key returns the underlying key event. This is a syntactic sugar for casting
// the key event to a [Key].
func (k KeyPressMsg) Key() Key {
return Key(k)
}

// KeyReleaseMsg represents a key release message.
type KeyReleaseMsg Key

// String implements [fmt.Stringer] and is quite useful for matching key
// events. For details, on what this returns see [Key.String].
func (k KeyReleaseMsg) String() string {
return Key(k).String()
}

// Key returns the underlying key event. This is a convenience method and
// syntactic sugar to satisfy the [KeyMsg] interface, and cast the key event to
// [Key].
func (k KeyReleaseMsg) Key() Key {
return Key(k)
}

// KeyMsg represents a key event. This can be either a key press or a key
// release event.
type KeyMsg interface {
fmt.Stringer

// Key returns the underlying key event.
Key() Key
}

// Key represents a Key press or release event. It contains information about
// the Key pressed, like the runes, the type of Key, and the modifiers pressed.
// There are a couple general patterns you could use to check for key presses
// or releases:
//
// // Switch on the string representation of the key (shorter)
// switch msg := msg.(type) {
Expand All @@ -209,55 +247,58 @@ const (
//
// // Switch on the key type (more foolproof)
// switch msg := msg.(type) {
// case KeyReleaseMsg:
// switch msg.Sym {
// case KeyMsg:
// // catch both KeyPressMsg and KeyReleaseMsg
// switch key := msg.Key(); key.Code {
// case KeyEnter:
// fmt.Println("you pressed enter!")
// case KeyRunes:
// switch string(msg.Runes) {
// default:
// switch key.Text {
// case "a":
// fmt.Println("you pressed a!")
// }
// }
// }
//
// Note that Key.Runes will always contain at least one character, so you can
// always safely call Key.Runes[0]. In most cases Key.Runes will only contain
// one character, though certain input method editors (most notably Chinese
// IMEs) can input multiple runes at once.
// Note that [Key.Text] will be empty for special keys like [KeyEnter],
// [KeyTab], and for keys that don't represent printable characters like key
// combos with modifier keys. In other words, [Key.Text] is populated only for
// keys that represent printable characters shifted or unshifted (like 'a',
// 'A', '1', '!', etc.).
type Key struct {
// Runes contains the actual characters received. This usually has a length
// of 1. Use [Rune()] to get the first key rune received. If the user
// presses shift+a, the Runes will be `[]rune{'A'}`.
Runes []rune
// Text contains the actual characters received. This usually the same as
// [Key.Code]. When [Key.Text] is non-empty, it indicates that the key
// pressed represents printable character(s).
Text string

// Type is a special key, like enter, tab, backspace, and so on.
Type KeyType
// Mod represents modifier keys, like [ModCtrl], [ModAlt], and so on.
Mod KeyMod

// altRune is the actual, unshifted key pressed by the user. For example,
// if the user presses shift+a, or caps lock is on, the altRune will be
// 'a'.
// Code represents the key pressed. This is usually a special key like
// [KeyTab], [KeyEnter], [KeyF1], or a printable character like 'a'.
Code rune

// ShiftedCode is the actual, shifted key pressed by the user. For example,
// if the user presses shift+a, or caps lock is on, [Key.ShiftedCode] will
// be 'A' and [Key.Code] will be 'a'.
//
// In the case of non-latin keyboards, like Arabic, altRune is the
// In the case of non-latin keyboards, like Arabic, [Key.ShiftedCode] is the
// unshifted key on the keyboard.
//
// This is only available with the Kitty Keyboard Protocol or the Windows
// Console API.
altRune rune
ShiftedCode rune

// baseRune is the key pressed according to the standard PC-101 key layout.
// On internaltional keyboards, this is the key that would be pressed if
// the keyboard was set to US layout.
// BaseCode is the key pressed according to the standard PC-101 key layout.
// On international keyboards, this is the key that would be pressed if the
// keyboard was set to US PC-101 layout.
//
// For example, if the user presses 'q' on a French AZERTY keyboard, the
// baseRune will be 'q'.
// For example, if the user presses 'q' on a French AZERTY keyboard,
// [Key.BaseCode] will be 'q'.
//
// This is only available with the Kitty Keyboard Protocol or the Windows
// Console API.
baseRune rune

// Mod is a modifier key, like ctrl, alt, and so on.
Mod KeyMod
BaseCode rune

// IsRepeat indicates whether the key is being held down and sending events
// repeatedly.
Expand All @@ -267,46 +308,7 @@ type Key struct {
IsRepeat bool
}

// KeyPressMsg represents a key press message.
type KeyPressMsg Key

// String implements fmt.Stringer and is quite useful for matching key
// events. For details, on what this returns see [Key.String].
func (k KeyPressMsg) String() string {
return Key(k).String()
}

// Rune returns the first rune in the Runes field. If the Runes field is empty,
// it returns 0.
func (k KeyPressMsg) Rune() rune {
return Key(k).Rune()
}

// KeyReleaseMsg represents a key release message.
type KeyReleaseMsg Key

// String implements fmt.Stringer and is quite useful for matching complex key
// events. For details, on what this returns see [Key.String].
func (k KeyReleaseMsg) String() string {
return Key(k).String()
}

// Rune returns the first rune in the Runes field. If the Runes field is empty,
// it returns 0.
func (k KeyReleaseMsg) Rune() rune {
return Key(k).Rune()
}

// Rune returns the first rune in the Runes field. If the Runes field is empty,
// it returns 0.
func (k Key) Rune() rune {
if len(k.Runes) == 0 {
return 0
}
return k.Runes[0]
}

// String implements fmt.Stringer and is used to convert a key to a string.
// String implements [fmt.Stringer] and is used to convert a key to a string.
// While less type safe than looking at the individual fields, it will usually
// be more convenient and readable to use this method when matching against
// keys.
Expand All @@ -322,63 +324,48 @@ func (k Key) Rune() rune {
// For example, you'll always see "ctrl+shift+alt+a" and never
// "shift+ctrl+alt+a".
func (k Key) String() string {
var s string
if k.Mod.Contains(ModCtrl) && k.Type != KeyLeftCtrl && k.Type != KeyRightCtrl {
s += "ctrl+"
var sb strings.Builder
if k.Mod.Contains(ModCtrl) && k.Code != KeyLeftCtrl && k.Code != KeyRightCtrl {
sb.WriteString("ctrl+")
}
if k.Mod.Contains(ModAlt) && k.Type != KeyLeftAlt && k.Type != KeyRightAlt {
s += "alt+"
if k.Mod.Contains(ModAlt) && k.Code != KeyLeftAlt && k.Code != KeyRightAlt {
sb.WriteString("alt+")
}
if k.Mod.Contains(ModShift) && k.Type != KeyLeftShift && k.Type != KeyRightShift {
s += "shift+"
if k.Mod.Contains(ModShift) && k.Code != KeyLeftShift && k.Code != KeyRightShift {
sb.WriteString("shift+")
}
if k.Mod.Contains(ModMeta) && k.Type != KeyLeftMeta && k.Type != KeyRightMeta {
s += "meta+"
if k.Mod.Contains(ModMeta) && k.Code != KeyLeftMeta && k.Code != KeyRightMeta {
sb.WriteString("meta+")
}
if k.Mod.Contains(ModHyper) && k.Type != KeyLeftHyper && k.Type != KeyRightHyper {
s += "hyper+"
if k.Mod.Contains(ModHyper) && k.Code != KeyLeftHyper && k.Code != KeyRightHyper {
sb.WriteString("hyper+")
}
if k.Mod.Contains(ModSuper) && k.Type != KeyLeftSuper && k.Type != KeyRightSuper {
s += "super+"
if k.Mod.Contains(ModSuper) && k.Code != KeyLeftSuper && k.Code != KeyRightSuper {
sb.WriteString("super+")
}

runeStr := func(r rune) string {
// Space is the only invisible printable character.
if r == ' ' {
return "space"
if kt, ok := keyTypeString[k.Code]; ok {
sb.WriteString(kt)
} else {
code := k.Code
if k.BaseCode != 0 {
// If a [Key.BaseCode] is present, use it to represent a key using the standard
// PC-101 key layout.
code = k.BaseCode
}
return string(r)
}
if k.baseRune != 0 {
// If a baseRune is present, use it to represent a key using the standard
// PC-101 key layout.
s += runeStr(k.baseRune)
} else if k.altRune != 0 {
// Otherwise, use the AltRune aka the non-shifted one if present.
s += runeStr(k.altRune)
} else if len(k.Runes) > 0 {
// Else, just print the rune.
if len(k.Runes) > 1 {
s += string(k.Runes)

if code == ' ' {
// Space is the only invisible printable character.
sb.WriteString("space")
} else {
s += runeStr(k.Rune())
sb.WriteRune(code)
}
} else {
s += k.Type.String()
}
return s
}

// String returns the string representation of the key type.
func (k KeyType) String() string {
if s, ok := keyTypeString[k]; ok {
return s
}
return ""
return sb.String()
}

var keyTypeString = map[KeyType]string{
KeyRunes: "runes",
var keyTypeString = map[rune]string{
KeyEnter: "enter",
KeyTab: "tab",
KeyBackspace: "backspace",
Expand Down
Loading

0 comments on commit 8a75439

Please sign in to comment.