Repository: charmbracelet/bubbletea
Pull request #1111 (2024-08-22 21:32:16)

(v2) Use KeyMsg/MouseMsg interfaces
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)


Patch:
diff --git a/driver_windows.go b/driver_windows.go
index fb50a88..70029d7 100644
--- a/driver_windows.go
+++ b/driver_windows.go
@@ -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 {
@@ -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))
 		}
 	}
 
@@ -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
 		}
 
@@ -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)
 		}
diff --git a/key.go b/key.go
index 3367941..a2d1a10 100644
--- a/key.go
+++ b/key.go
@@ -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
 
-	KeyUp
+	KeyUp rune = KeyExtended + iota + 1
 	KeyDown
 	KeyRight
 	KeyLeft
 	KeyBegin
 	KeyFind
 	KeyInsert
+	KeyDelete
 	KeySelect
 	KeyPgUp
 	KeyPgDown
@@ -190,11 +174,65 @@ const (
 	KeyRightMeta
 	KeyIsoLevel3Shift
 	KeyIsoLevel5Shift
+
+	// Special names in C0
+
+	KeyBackspace = rune(ansi.DEL)
+	KeyTab       = rune(ansi.HT)
+	KeyEnter     = rune(ansi.CR)
+	KeyReturn    = KeyEnter
+	KeyEscape    = rune(ansi.ESC)
+	KeyEsc       = KeyEscape
+
+	// Special names in G0
+
+	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) {
@@ -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.
@@ -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.
@@ -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",
diff --git a/key_deprecated.go b/key_deprecated.go
deleted file mode 100644
index 0ef83a9..0000000
--- a/key_deprecated.go
+++ /dev/null
@@ -1,243 +0,0 @@
-package tea
-
-import (
-	"strings"
-)
-
-// KeyMsg contains information about a keypress. KeyMsgs are always sent to
-// the program's update function. There are a couple general patterns you could
-// use to check for keypresses:
-//
-//	// Switch on the string representation of the key (shorter)
-//	switch msg := msg.(type) {
-//	case KeyMsg:
-//	    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:
-//	    switch msg.Type {
-//	    case KeyEnter:
-//	        fmt.Println("you pressed enter!")
-//	    case KeyRunes:
-//	        switch string(msg.Runes) {
-//	        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.
-//
-// TODO(v2): Add a KeyMsg interface that incorporates all the key message
-// types.
-//
-// Deprecated: KeyMsg is deprecated in favor of KeyPressMsg and KeyReleaseMsg.
-type KeyMsg struct {
-	Type  KeyType
-	Runes []rune
-	Alt   bool
-	Paste bool
-}
-
-// String returns a friendly string representation for a key. It's safe (and
-// encouraged) for use in key comparison.
-//
-//	k := Key{Type: KeyEnter}
-//	fmt.Println(k)
-//	// Output: enter
-func (k KeyMsg) String() (str string) {
-	var buf strings.Builder
-	if k.Alt {
-		buf.WriteString("alt+")
-	}
-	if k.Type == KeyRunes {
-		if k.Paste {
-			// Note: bubbles/keys bindings currently do string compares to
-			// recognize shortcuts. Since pasted text should never activate
-			// shortcuts, we need to ensure that the binding code doesn't
-			// match Key events that result from pastes. We achieve this
-			// here by enclosing pastes in '[...]' so that the string
-			// comparison in Matches() fails in that case.
-			buf.WriteByte('[')
-		}
-		buf.WriteString(string(k.Runes))
-		if k.Paste {
-			buf.WriteByte(']')
-		}
-		return buf.String()
-	} else if s, ok := keyNames[k.Type]; ok {
-		buf.WriteString(s)
-		return buf.String()
-	}
-	return ""
-}
-
-// Control key aliases.
-const (
-	KeyNull KeyType = -iota - 10
-	KeyBreak
-
-	KeyCtrlAt // ctrl+@
-	KeyCtrlA
-	KeyCtrlB
-	KeyCtrlC
-	KeyCtrlD
-	KeyCtrlE
-	KeyCtrlF
-	KeyCtrlG
-	KeyCtrlH
-	KeyCtrlI
-	KeyCtrlJ
-	KeyCtrlK
-	KeyCtrlL
-	KeyCtrlM
-	KeyCtrlN
-	KeyCtrlO
-	KeyCtrlP
-	KeyCtrlQ
-	KeyCtrlR
-	KeyCtrlS
-	KeyCtrlT
-	KeyCtrlU
-	KeyCtrlV
-	KeyCtrlW
-	KeyCtrlX
-	KeyCtrlY
-	KeyCtrlZ
-	KeyCtrlOpenBracket  // ctrl+[
-	KeyCtrlBackslash    // ctrl+\
-	KeyCtrlCloseBracket // ctrl+]
-	KeyCtrlCaret        // ctrl+^
-	KeyCtrlUnderscore   // ctrl+_
-	KeyCtrlQuestionMark // ctrl+?
-	KeyCtrlUp
-	KeyCtrlDown
-	KeyCtrlRight
-	KeyCtrlLeft
-	KeyCtrlPgUp
-	KeyCtrlPgDown
-	KeyCtrlHome
-	KeyCtrlEnd
-
-	KeyShiftTab
-	KeyShiftUp
-	KeyShiftDown
-	KeyShiftRight
-	KeyShiftLeft
-	KeyShiftHome
-	KeyShiftEnd
-
-	KeyCtrlShiftUp
-	KeyCtrlShiftDown
-	KeyCtrlShiftLeft
-	KeyCtrlShiftRight
-	KeyCtrlShiftHome
-	KeyCtrlShiftEnd
-
-	// Deprecated: Use KeyEscape instead.
-	KeyEsc = KeyEscape
-)
-
-// Mappings for control keys and other special keys to friendly consts.
-var keyNames = map[KeyType]string{
-	// Control keys.
-	KeyCtrlAt:           "ctrl+@", // also ctrl+` (that's ctrl+backtick)
-	KeyCtrlA:            "ctrl+a",
-	KeyCtrlB:            "ctrl+b",
-	KeyCtrlC:            "ctrl+c",
-	KeyCtrlD:            "ctrl+d",
-	KeyCtrlE:            "ctrl+e",
-	KeyCtrlF:            "ctrl+f",
-	KeyCtrlG:            "ctrl+g",
-	KeyCtrlH:            "ctrl+h",
-	KeyTab:              "tab", // also ctrl+i
-	KeyCtrlJ:            "ctrl+j",
-	KeyCtrlK:            "ctrl+k",
-	KeyCtrlL:            "ctrl+l",
-	KeyEnter:            "enter",
-	KeyCtrlN:            "ctrl+n",
-	KeyCtrlO:            "ctrl+o",
-	KeyCtrlP:            "ctrl+p",
-	KeyCtrlQ:            "ctrl+q",
-	KeyCtrlR:            "ctrl+r",
-	KeyCtrlS:            "ctrl+s",
-	KeyCtrlT:            "ctrl+t",
-	KeyCtrlU:            "ctrl+u",
-	KeyCtrlV:            "ctrl+v",
-	KeyCtrlW:            "ctrl+w",
-	KeyCtrlX:            "ctrl+x",
-	KeyCtrlY:            "ctrl+y",
-	KeyCtrlZ:            "ctrl+z",
-	KeyEscape:           "esc",
-	KeyCtrlOpenBracket:  "ctrl+[",
-	KeyCtrlBackslash:    "ctrl+\\",
-	KeyCtrlCloseBracket: "ctrl+]",
-	KeyCtrlCaret:        "ctrl+^",
-	KeyCtrlUnderscore:   "ctrl+_",
-	KeyBackspace:        "backspace",
-
-	// Other keys.
-	KeyRunes:          "runes",
-	KeyUp:             "up",
-	KeyDown:           "down",
-	KeyRight:          "right",
-	KeySpace:          " ", // for backwards compatibility
-	KeyLeft:           "left",
-	KeyShiftTab:       "shift+tab",
-	KeyHome:           "home",
-	KeyEnd:            "end",
-	KeyCtrlHome:       "ctrl+home",
-	KeyCtrlEnd:        "ctrl+end",
-	KeyShiftHome:      "shift+home",
-	KeyShiftEnd:       "shift+end",
-	KeyCtrlShiftHome:  "ctrl+shift+home",
-	KeyCtrlShiftEnd:   "ctrl+shift+end",
-	KeyPgUp:           "pgup",
-	KeyPgDown:         "pgdown",
-	KeyCtrlPgUp:       "ctrl+pgup",
-	KeyCtrlPgDown:     "ctrl+pgdown",
-	KeyDelete:         "delete",
-	KeyInsert:         "insert",
-	KeyCtrlUp:         "ctrl+up",
-	KeyCtrlDown:       "ctrl+down",
-	KeyCtrlRight:      "ctrl+right",
-	KeyCtrlLeft:       "ctrl+left",
-	KeyShiftUp:        "shift+up",
-	KeyShiftDown:      "shift+down",
-	KeyShiftRight:     "shift+right",
-	KeyShiftLeft:      "shift+left",
-	KeyCtrlShiftUp:    "ctrl+shift+up",
-	KeyCtrlShiftDown:  "ctrl+shift+down",
-	KeyCtrlShiftLeft:  "ctrl+shift+left",
-	KeyCtrlShiftRight: "ctrl+shift+right",
-	KeyF1:             "f1",
-	KeyF2:             "f2",
-	KeyF3:             "f3",
-	KeyF4:             "f4",
-	KeyF5:             "f5",
-	KeyF6:             "f6",
-	KeyF7:             "f7",
-	KeyF8:             "f8",
-	KeyF9:             "f9",
-	KeyF10:            "f10",
-	KeyF11:            "f11",
-	KeyF12:            "f12",
-	KeyF13:            "f13",
-	KeyF14:            "f14",
-	KeyF15:            "f15",
-	KeyF16:            "f16",
-	KeyF17:            "f17",
-	KeyF18:            "f18",
-	KeyF19:            "f19",
-	KeyF20:            "f20",
-}
diff --git a/kitty.go b/kitty.go
index a644888..834ad6d 100644
--- a/kitty.go
+++ b/kitty.go
@@ -49,7 +49,7 @@ func kittyKeyboard() Msg { //nolint:unused
 type _KittyKeyboardMsg int
 
 // Kitty Clipboard Control Sequences
-var kittyKeyMap = map[int]KeyType{
+var kittyKeyMap = map[int]rune{
 	ansi.BS:  KeyBackspace,
 	ansi.HT:  KeyTab,
 	ansi.CR:  KeyEnter,
@@ -229,48 +229,52 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) Msg {
 	if params := csi.Subparams(0); len(params) > 0 {
 		code := params[0]
 		if sym, ok := kittyKeyMap[code]; ok {
-			key.Type = sym
+			key.Code = sym
 		} else {
 			r := rune(code)
 			if !utf8.ValidRune(r) {
 				r = utf8.RuneError
 			}
 
-			key.Type = KeyRunes
-			key.Runes = []rune{r}
+			key.Code = r
+		}
 
-			// alternate key reporting
-			switch len(params) {
-			case 3:
-				// shifted key + base key
-				if b := rune(params[2]); unicode.IsPrint(b) {
-					// XXX: When alternate key reporting is enabled, the protocol
-					// can return 3 things, the unicode codepoint of the key,
-					// the shifted codepoint of the key, and the standard
-					// PC-101 key layout codepoint.
-					// This is useful to create an unambiguous mapping of keys
-					// when using a different language layout.
-					key.baseRune = b
-				}
-				fallthrough
-			case 2:
-				// shifted key
-				if s := rune(params[1]); unicode.IsPrint(s) {
-					// XXX: We swap keys here because we want the shifted key
-					// to be the Rune that is returned by the event.
-					// For example, shift+a should produce "A" not "a".
-					// In such a case, we set AltRune to the original key "a"
-					// and Rune to "A".
-					key.altRune = key.Rune()
-					key.Runes = []rune{s}
-				}
+		// alternate key reporting
+		switch len(params) {
+		case 3:
+			// shifted key + base key
+			if b := rune(params[2]); unicode.IsPrint(b) {
+				// XXX: When alternate key reporting is enabled, the protocol
+				// can return 3 things, the unicode codepoint of the key,
+				// the shifted codepoint of the key, and the standard
+				// PC-101 key layout codepoint.
+				// This is useful to create an unambiguous mapping of keys
+				// when using a different language layout.
+				key.BaseCode = b
+			}
+			fallthrough
+		case 2:
+			// shifted key
+			if s := rune(params[1]); unicode.IsPrint(s) {
+				// XXX: We swap keys here because we want the shifted key
+				// to be the Rune that is returned by the event.
+				// For example, shift+a should produce "A" not "a".
+				// In such a case, we set AltRune to the original key "a"
+				// and Rune to "A".
+				key.ShiftedCode = s
 			}
 		}
 	}
+
 	if params := csi.Subparams(1); len(params) > 0 {
 		mod := params[0]
 		if mod > 1 {
 			key.Mod = fromKittyMod(mod - 1)
+			if key.Mod > ModShift {
+				// XXX: We need to clear the text if we have a modifier key
+				// other than a [ModShift] key.
+				key.Text = ""
+			}
 		}
 		if len(params) > 1 {
 			switch params[1] {
@@ -281,15 +285,27 @@ func parseKittyKeyboard(csi *ansi.CsiSequence) Msg {
 			}
 		}
 	}
+
 	if params := csi.Subparams(2); len(params) > 0 {
-		r := rune(params[0])
-		if unicode.IsPrint(r) {
-			key.altRune = key.Rune()
-			key.Runes = []rune{r}
+		for _, code := range params {
+			if code != 0 {
+				key.Text += string(rune(code))
+			}
+		}
+	}
+
+	noneOrShifted := key.Mod <= ModShift && key.Code != KeyLeftShift && key.Code != KeyRightShift
+	if len(key.Text) == 0 && noneOrShifted {
+		if key.ShiftedCode != 0 && unicode.IsPrint(key.ShiftedCode) {
+			key.Text = string(key.ShiftedCode)
+		} else if unicode.IsPrint(key.Code) {
+			key.Text = string(key.Code)
 		}
 	}
+
 	if isRelease {
 		return KeyReleaseMsg(key)
 	}
+
 	return KeyPressMsg(key)
 }
diff --git a/mouse.go b/mouse.go
index f3460fa..14ce0ad 100644
--- a/mouse.go
+++ b/mouse.go
@@ -1,6 +1,8 @@
 package tea
 
 import (
+	"fmt"
+
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -54,7 +56,33 @@ var mouseButtons = map[MouseButton]string{
 	MouseExtra2:     "button11",
 }
 
-// Mouse represents a mouse message.
+// 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
@@ -91,6 +119,13 @@ func (e MouseClickMsg) String() string {
 	return Mouse(e).String()
 }
 
+// Mouse returns the underlying mouse event. This is a convenience method and
+// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
+// event to [Mouse].
+func (e MouseClickMsg) Mouse() Mouse {
+	return Mouse(e)
+}
+
 // MouseReleaseMsg represents a mouse button release message.
 type MouseReleaseMsg Mouse
 
@@ -99,6 +134,13 @@ func (e MouseReleaseMsg) String() string {
 	return Mouse(e).String()
 }
 
+// Mouse returns the underlying mouse event. This is a convenience method and
+// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
+// event to [Mouse].
+func (e MouseReleaseMsg) Mouse() Mouse {
+	return Mouse(e)
+}
+
 // MouseWheelMsg represents a mouse wheel message event.
 type MouseWheelMsg Mouse
 
@@ -107,6 +149,13 @@ func (e MouseWheelMsg) String() string {
 	return Mouse(e).String()
 }
 
+// Mouse returns the underlying mouse event. This is a convenience method and
+// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
+// event to [Mouse].
+func (e MouseWheelMsg) Mouse() Mouse {
+	return Mouse(e)
+}
+
 // MouseMotionMsg represents a mouse motion message.
 type MouseMotionMsg Mouse
 
@@ -119,6 +168,13 @@ func (e MouseMotionMsg) String() string {
 	return m.String() + "motion"
 }
 
+// Mouse returns the underlying mouse event. This is a convenience method and
+// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
+// event to [Mouse].
+func (e MouseMotionMsg) Mouse() Mouse {
+	return Mouse(e)
+}
+
 // Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
 // look like:
 //
diff --git a/mouse_deprecated.go b/mouse_deprecated.go
deleted file mode 100644
index c0f363e..0000000
--- a/mouse_deprecated.go
+++ /dev/null
@@ -1,165 +0,0 @@
-package tea
-
-// MouseMsg contains information about a mouse event and are sent to a programs
-// update function when mouse activity occurs. Note that the mouse must first
-// be enabled in order for the mouse events to be received.
-//
-// TODO(v2): Add a MouseMsg interface that incorporates all the mouse message
-// types.
-//
-// Deprecated: in favor of MouseClickMsg, MouseReleaseMsg, MouseWheelMsg, and
-// MouseMotionMsg.
-type MouseMsg struct {
-	X      int
-	Y      int
-	Shift  bool
-	Alt    bool
-	Ctrl   bool
-	Action MouseAction
-	Button MouseButton
-	Type   MouseEventType
-}
-
-// MouseEvent represents a mouse event.
-//
-// Deprecated: Use Mouse.
-type MouseEvent = MouseMsg
-
-// IsWheel returns true if the mouse event is a wheel event.
-func (m MouseMsg) IsWheel() bool {
-	return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown ||
-		m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight
-}
-
-// String returns a string representation of a mouse event.
-func (m MouseMsg) String() (s string) {
-	if m.Ctrl {
-		s += "ctrl+"
-	}
-	if m.Alt {
-		s += "alt+"
-	}
-	if m.Shift {
-		s += "shift+"
-	}
-
-	if m.Button == MouseButtonNone { //nolint:nestif
-		if m.Action == MouseActionMotion || m.Action == MouseActionRelease {
-			s += mouseMsgActions[m.Action]
-		} else {
-			s += "unknown"
-		}
-	} else if m.IsWheel() {
-		s += mouseMsgButtons[m.Button]
-	} else {
-		btn := mouseMsgButtons[m.Button]
-		if btn != "" {
-			s += btn
-		}
-		act := mouseMsgActions[m.Action]
-		if act != "" {
-			s += " " + act
-		}
-	}
-
-	return s
-}
-
-// MouseAction represents the action that occurred during a mouse event.
-//
-// Deprecated: Use MouseClickMsg, MouseReleaseMsg, MouseWheelMsg, and
-// MouseMotionMsg.
-type MouseAction int
-
-// Mouse event actions.
-//
-// Deprecated in favor of MouseClickMsg, MouseReleaseMsg, MouseWheelMsg, and
-// MouseMotionMsg.
-const (
-	MouseActionPress MouseAction = iota
-	MouseActionRelease
-	MouseActionMotion
-)
-
-var mouseMsgActions = map[MouseAction]string{
-	MouseActionPress:   "press",
-	MouseActionRelease: "release",
-	MouseActionMotion:  "motion",
-}
-
-// Mouse event buttons
-//
-// This is based on X11 mouse button codes.
-//
-//	1 = left button
-//	2 = middle button (pressing the scroll wheel)
-//	3 = right button
-//	4 = turn scroll wheel up
-//	5 = turn scroll wheel down
-//	6 = push scroll wheel left
-//	7 = push scroll wheel right
-//	8 = 4th button (aka browser backward button)
-//	9 = 5th button (aka browser forward button)
-//	10
-//	11
-//
-// Other buttons are not supported.
-//
-// Deprecated: Use MouseNone, MouseLeft, etc.
-const (
-	MouseButtonNone       = MouseNone
-	MouseButtonLeft       = MouseLeft
-	MouseButtonMiddle     = MouseMiddle
-	MouseButtonRight      = MouseRight
-	MouseButtonWheelUp    = MouseWheelUp
-	MouseButtonWheelDown  = MouseWheelDown
-	MouseButtonWheelLeft  = MouseWheelLeft
-	MouseButtonWheelRight = MouseWheelRight
-	MouseButtonBackward   = MouseBackward
-	MouseButtonForward    = MouseForward
-	MouseButton10         = MouseExtra1
-	MouseButton11         = MouseExtra2
-)
-
-// Deprecated: Use mouseButtons.
-var mouseMsgButtons = map[MouseButton]string{
-	MouseButtonNone:       "none",
-	MouseButtonLeft:       "left",
-	MouseButtonMiddle:     "middle",
-	MouseButtonRight:      "right",
-	MouseButtonWheelUp:    "wheel up",
-	MouseButtonWheelDown:  "wheel down",
-	MouseButtonWheelLeft:  "wheel left",
-	MouseButtonWheelRight: "wheel right",
-	MouseButtonBackward:   "backward",
-	MouseButtonForward:    "forward",
-	MouseButton10:         "button 10",
-	MouseButton11:         "button 11",
-}
-
-// MouseEventType indicates the type of mouse event occurring.
-//
-// Deprecated: Use MouseButton.
-type MouseEventType = MouseButton
-
-// Mouse event types.
-//
-// Deprecated in favor of MouseReleaseMsg and MouseMotionMsg.
-const (
-	MouseUnknown = MouseNone
-
-	MouseRelease MouseEventType = -iota // mouse button release (X10 only)
-	MouseMotion
-)
-
-// toMouseMsg converts a mouse event to a mouse message.
-func toMouseMsg(m Mouse) MouseMsg {
-	return MouseMsg{
-		X:      m.X,
-		Y:      m.Y,
-		Shift:  m.Mod.Contains(ModShift),
-		Alt:    m.Mod.Contains(ModAlt),
-		Ctrl:   m.Mod.Contains(ModCtrl),
-		Button: m.Button,
-	}
-}
diff --git a/parse.go b/parse.go
index 786d5fb..4427c38 100644
--- a/parse.go
+++ b/parse.go
@@ -3,6 +3,7 @@ package tea
 import (
 	"encoding/base64"
 	"strings"
+	"unicode"
 	"unicode/utf8"
 
 	"github.com/charmbracelet/x/ansi"
@@ -108,7 +109,7 @@ func parseSequence(buf []byte) (n int, msg Msg) {
 	case ansi.ESC:
 		if len(buf) == 1 {
 			// Escape key
-			return 1, KeyPressMsg{Type: KeyEscape}
+			return 1, KeyPressMsg{Code: KeyEscape}
 		}
 
 		switch b := buf[1]; b {
@@ -125,13 +126,14 @@ func parseSequence(buf []byte) (n int, msg Msg) {
 		default:
 			n, e := parseSequence(buf[1:])
 			if k, ok := e.(KeyPressMsg); ok {
+				k.Text = ""
 				k.Mod |= ModAlt
 				return n + 1, k
 			}
 
 			// Not a key sequence, nor an alt modified key sequence. In that
 			// case, just report a single escape key.
-			return 1, KeyPressMsg{Type: KeyEscape}
+			return 1, KeyPressMsg{Code: KeyEscape}
 		}
 	case ansi.SS3:
 		return parseSs3(buf)
@@ -150,7 +152,8 @@ func parseSequence(buf []byte) (n int, msg Msg) {
 			// C1 control code
 			// UTF-8 never starts with a C1 control code
 			// Encode these as Ctrl+Alt+<code - 0x40>
-			return 1, KeyPressMsg{Runes: []rune{rune(b) - 0x40}, Mod: ModCtrl | ModAlt}
+			code := rune(b) - 0x40
+			return 1, KeyPressMsg{Code: code, Mod: ModCtrl | ModAlt}
 		}
 		return parseUtf8(buf)
 	}
@@ -159,7 +162,7 @@ func parseSequence(buf []byte) (n int, msg Msg) {
 func parseCsi(b []byte) (int, Msg) {
 	if len(b) == 2 && b[0] == ansi.ESC {
 		// short cut if this is an alt+[ key
-		return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt}
+		return 2, KeyPressMsg{Text: string(rune(b[1])), Mod: ModAlt}
 	}
 
 	var csi ansi.CsiSequence
@@ -280,7 +283,7 @@ func parseCsi(b []byte) (int, Msg) {
 				//
 				// For a non ambiguous cursor position report, use
 				// [ansi.RequestExtendedCursorPosition] (DECXCPR) instead.
-				return i, multiMsg{KeyPressMsg{Type: KeyF3, Mod: KeyMod(csi.Param(1) - 1)}, m}
+				return i, multiMsg{KeyPressMsg{Code: KeyF3, Mod: KeyMod(csi.Param(1) - 1)}, m}
 			}
 
 			return i, m
@@ -296,19 +299,19 @@ func parseCsi(b []byte) (int, Msg) {
 		var k KeyPressMsg
 		switch cmd {
 		case 'a', 'b', 'c', 'd':
-			k = KeyPressMsg{Type: KeyUp + KeyType(cmd-'a'), Mod: ModShift}
+			k = KeyPressMsg{Code: KeyUp + rune(cmd-'a'), Mod: ModShift}
 		case 'A', 'B', 'C', 'D':
-			k = KeyPressMsg{Type: KeyUp + KeyType(cmd-'A')}
+			k = KeyPressMsg{Code: KeyUp + rune(cmd-'A')}
 		case 'E':
-			k = KeyPressMsg{Type: KeyBegin}
+			k = KeyPressMsg{Code: KeyBegin}
 		case 'F':
-			k = KeyPressMsg{Type: KeyEnd}
+			k = KeyPressMsg{Code: KeyEnd}
 		case 'H':
-			k = KeyPressMsg{Type: KeyHome}
+			k = KeyPressMsg{Code: KeyHome}
 		case 'P', 'Q', 'R', 'S':
-			k = KeyPressMsg{Type: KeyF1 + KeyType(cmd-'P')}
+			k = KeyPressMsg{Code: KeyF1 + rune(cmd-'P')}
 		case 'Z':
-			k = KeyPressMsg{Type: KeyTab, Mod: ModShift}
+			k = KeyPressMsg{Code: KeyTab, Mod: ModShift}
 		}
 		if paramsLen > 1 && csi.Param(0) == 1 && csi.Param(1) != -1 {
 			// CSI 1 ; <modifiers> A
@@ -392,38 +395,38 @@ func parseCsi(b []byte) (int, Msg) {
 			switch param {
 			case 1:
 				if flags&_FlagFind != 0 {
-					k = KeyPressMsg{Type: KeyFind}
+					k = KeyPressMsg{Code: KeyFind}
 				} else {
-					k = KeyPressMsg{Type: KeyHome}
+					k = KeyPressMsg{Code: KeyHome}
 				}
 			case 2:
-				k = KeyPressMsg{Type: KeyInsert}
+				k = KeyPressMsg{Code: KeyInsert}
 			case 3:
-				k = KeyPressMsg{Type: KeyDelete}
+				k = KeyPressMsg{Code: KeyDelete}
 			case 4:
 				if flags&_FlagSelect != 0 {
-					k = KeyPressMsg{Type: KeySelect}
+					k = KeyPressMsg{Code: KeySelect}
 				} else {
-					k = KeyPressMsg{Type: KeyEnd}
+					k = KeyPressMsg{Code: KeyEnd}
 				}
 			case 5:
-				k = KeyPressMsg{Type: KeyPgUp}
+				k = KeyPressMsg{Code: KeyPgUp}
 			case 6:
-				k = KeyPressMsg{Type: KeyPgDown}
+				k = KeyPressMsg{Code: KeyPgDown}
 			case 7:
-				k = KeyPressMsg{Type: KeyHome}
+				k = KeyPressMsg{Code: KeyHome}
 			case 8:
-				k = KeyPressMsg{Type: KeyEnd}
+				k = KeyPressMsg{Code: KeyEnd}
 			case 11, 12, 13, 14, 15:
-				k = KeyPressMsg{Type: KeyF1 + KeyType(param-11)}
+				k = KeyPressMsg{Code: KeyF1 + rune(param-11)}
 			case 17, 18, 19, 20, 21:
-				k = KeyPressMsg{Type: KeyF6 + KeyType(param-17)}
+				k = KeyPressMsg{Code: KeyF6 + rune(param-17)}
 			case 23, 24, 25, 26:
-				k = KeyPressMsg{Type: KeyF11 + KeyType(param-23)}
+				k = KeyPressMsg{Code: KeyF11 + rune(param-23)}
 			case 28, 29:
-				k = KeyPressMsg{Type: KeyF15 + KeyType(param-28)}
+				k = KeyPressMsg{Code: KeyF15 + rune(param-28)}
 			case 31, 32, 33, 34:
-				k = KeyPressMsg{Type: KeyF17 + KeyType(param-31)}
+				k = KeyPressMsg{Code: KeyF17 + rune(param-31)}
 			}
 
 			// modifiers
@@ -450,7 +453,7 @@ func parseCsi(b []byte) (int, Msg) {
 func parseSs3(b []byte) (int, Msg) {
 	if len(b) == 2 && b[0] == ansi.ESC {
 		// short cut if this is an alt+O key
-		return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt}
+		return 2, KeyPressMsg{Code: rune(b[1]), Mod: ModAlt}
 	}
 
 	var i int
@@ -482,23 +485,23 @@ func parseSs3(b []byte) (int, Msg) {
 	var k KeyPressMsg
 	switch gl {
 	case 'a', 'b', 'c', 'd':
-		k = KeyPressMsg{Type: KeyUp + KeyType(gl-'a'), Mod: ModCtrl}
+		k = KeyPressMsg{Code: KeyUp + rune(gl-'a'), Mod: ModCtrl}
 	case 'A', 'B', 'C', 'D':
-		k = KeyPressMsg{Type: KeyUp + KeyType(gl-'A')}
+		k = KeyPressMsg{Code: KeyUp + rune(gl-'A')}
 	case 'E':
-		k = KeyPressMsg{Type: KeyBegin}
+		k = KeyPressMsg{Code: KeyBegin}
 	case 'F':
-		k = KeyPressMsg{Type: KeyEnd}
+		k = KeyPressMsg{Code: KeyEnd}
 	case 'H':
-		k = KeyPressMsg{Type: KeyHome}
+		k = KeyPressMsg{Code: KeyHome}
 	case 'P', 'Q', 'R', 'S':
-		k = KeyPressMsg{Type: KeyF1 + KeyType(gl-'P')}
+		k = KeyPressMsg{Code: KeyF1 + rune(gl-'P')}
 	case 'M':
-		k = KeyPressMsg{Type: KeyKpEnter}
+		k = KeyPressMsg{Code: KeyKpEnter}
 	case 'X':
-		k = KeyPressMsg{Type: KeyKpEqual}
+		k = KeyPressMsg{Code: KeyKpEqual}
 	case 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y':
-		k = KeyPressMsg{Type: KeyKpMultiply + KeyType(gl-'j')}
+		k = KeyPressMsg{Code: KeyKpMultiply + rune(gl-'j')}
 	default:
 		return i, UnknownMsg(b[:i])
 	}
@@ -514,7 +517,7 @@ func parseSs3(b []byte) (int, Msg) {
 func parseOsc(b []byte) (int, Msg) {
 	if len(b) == 2 && b[0] == ansi.ESC {
 		// short cut if this is an alt+] key
-		return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt}
+		return 2, KeyPressMsg{Code: rune(b[1]), Mod: ModAlt}
 	}
 
 	var i int
@@ -639,7 +642,7 @@ func parseStTerminated(intro8, intro7 byte) func([]byte) (int, Msg) {
 func parseDcs(b []byte) (int, Msg) {
 	if len(b) == 2 && b[0] == ansi.ESC {
 		// short cut if this is an alt+P key
-		return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt}
+		return 2, KeyPressMsg{Code: rune(b[1]), Mod: ModAlt}
 	}
 
 	var params [16]int
@@ -751,7 +754,7 @@ func parseDcs(b []byte) (int, Msg) {
 func parseApc(b []byte) (int, Msg) {
 	if len(b) == 2 && b[0] == ansi.ESC {
 		// short cut if this is an alt+_ key
-		return 2, KeyPressMsg{Runes: []rune{rune(b[1])}, Mod: ModAlt}
+		return 2, KeyPressMsg{Code: rune(b[1]), Mod: ModAlt}
 	}
 
 	// APC sequences are introduced by APC (0x9f) or ESC _ (0x1b 0x5f)
@@ -769,54 +772,75 @@ func parseUtf8(b []byte) (int, Msg) {
 		return 1, parseControl(c)
 	} else if c > ansi.US && c < ansi.DEL {
 		// ASCII printable characters
-		return 1, KeyPressMsg{Runes: []rune{rune(c)}}
+		code := rune(c)
+		k := KeyPressMsg{Code: code, Text: string(code)}
+		if unicode.IsUpper(code) {
+			// Convert upper case letters to lower case + shift modifier
+			k.Code = unicode.ToLower(code)
+			k.ShiftedCode = code
+			k.Mod |= ModShift
+		}
+
+		return 1, k
 	}
 
-	if r, _ := utf8.DecodeRune(b); r == utf8.RuneError {
+	code, _ := utf8.DecodeRune(b)
+	if code == utf8.RuneError {
 		return 1, UnknownMsg(b[0])
 	}
 
 	cluster, _, _, _ := uniseg.FirstGraphemeCluster(b, -1)
-	return len(cluster), KeyPressMsg{Runes: []rune(string(cluster))}
+	text := string(cluster)
+	for i := range text {
+		if i > 0 {
+			// Use [KeyExtended] for multi-rune graphemes
+			code = KeyExtended
+			break
+		}
+	}
+
+	return len(cluster), KeyPressMsg{Code: code, Text: text}
 }
 
 func parseControl(b byte) Msg {
 	switch b {
 	case ansi.NUL:
 		if flags&_FlagCtrlAt != 0 {
-			return KeyPressMsg{Runes: []rune{'@'}, Mod: ModCtrl}
+			return KeyPressMsg{Code: '@', Mod: ModCtrl}
 		}
-		return KeyPressMsg{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl}
+		return KeyPressMsg{Code: KeySpace, Mod: ModCtrl}
 	case ansi.BS:
-		return KeyPressMsg{Runes: []rune{'h'}, Mod: ModCtrl}
+		return KeyPressMsg{Code: 'h', Mod: ModCtrl}
 	case ansi.HT:
 		if flags&_FlagCtrlI != 0 {
-			return KeyPressMsg{Runes: []rune{'i'}, Mod: ModCtrl}
+			return KeyPressMsg{Code: 'i', Mod: ModCtrl}
 		}
-		return KeyPressMsg{Type: KeyTab}
+		return KeyPressMsg{Code: KeyTab}
 	case ansi.CR:
 		if flags&_FlagCtrlM != 0 {
-			return KeyPressMsg{Runes: []rune{'m'}, Mod: ModCtrl}
+			return KeyPressMsg{Code: 'm', Mod: ModCtrl}
 		}
-		return KeyPressMsg{Type: KeyEnter}
+		return KeyPressMsg{Code: KeyEnter}
 	case ansi.ESC:
 		if flags&_FlagCtrlOpenBracket != 0 {
-			return KeyPressMsg{Runes: []rune{'['}, Mod: ModCtrl}
+			return KeyPressMsg{Code: '[', Mod: ModCtrl}
 		}
-		return KeyPressMsg{Type: KeyEscape}
+		return KeyPressMsg{Code: KeyEscape}
 	case ansi.DEL:
 		if flags&_FlagBackspace != 0 {
-			return KeyPressMsg{Type: KeyDelete}
+			return KeyPressMsg{Code: KeyDelete}
 		}
-		return KeyPressMsg{Type: KeyBackspace}
+		return KeyPressMsg{Code: KeyBackspace}
 	case ansi.SP:
-		return KeyPressMsg{Type: KeySpace, Runes: []rune{' '}}
+		return KeyPressMsg{Code: KeySpace, Text: " "}
 	default:
 		if b >= ansi.SOH && b <= ansi.SUB {
 			// Use lower case letters for control codes
-			return KeyPressMsg{Runes: []rune{rune(b + 0x60)}, Mod: ModCtrl}
+			code := rune(b + 0x60)
+			return KeyPressMsg{Code: code, Mod: ModCtrl}
 		} else if b >= ansi.FS && b <= ansi.US {
-			return KeyPressMsg{Runes: []rune{rune(b + 0x40)}, Mod: ModCtrl}
+			code := rune(b + 0x40)
+			return KeyPressMsg{Code: code, Mod: ModCtrl}
 		}
 		return UnknownMsg(b)
 	}
diff --git a/table.go b/table.go
index 678388c..51afed1 100644
--- a/table.go
+++ b/table.go
@@ -10,39 +10,39 @@ import (
 // events based on the VT100/VT200, XTerm, and Urxvt terminal specs.
 // TODO: Use flags?
 func buildKeysTable(flags int, term string) map[string]Key {
-	nul := Key{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space
+	nul := Key{Code: KeySpace, Mod: ModCtrl} // ctrl+@ or ctrl+space
 	if flags&_FlagCtrlAt != 0 {
-		nul = Key{Runes: []rune{'@'}, Mod: ModCtrl}
+		nul = Key{Code: '@', Mod: ModCtrl}
 	}
 
-	tab := Key{Type: KeyTab} // ctrl+i or tab
+	tab := Key{Code: KeyTab} // ctrl+i or tab
 	if flags&_FlagCtrlI != 0 {
-		tab = Key{Runes: []rune{'i'}, Mod: ModCtrl}
+		tab = Key{Code: 'i', Mod: ModCtrl}
 	}
 
-	enter := Key{Type: KeyEnter} // ctrl+m or enter
+	enter := Key{Code: KeyEnter} // ctrl+m or enter
 	if flags&_FlagCtrlM != 0 {
-		enter = Key{Runes: []rune{'m'}, Mod: ModCtrl}
+		enter = Key{Code: 'm', Mod: ModCtrl}
 	}
 
-	esc := Key{Type: KeyEscape} // ctrl+[ or escape
+	esc := Key{Code: KeyEscape} // ctrl+[ or escape
 	if flags&_FlagCtrlOpenBracket != 0 {
-		esc = Key{Runes: []rune{'['}, Mod: ModCtrl} // ctrl+[ or escape
+		esc = Key{Code: '[', Mod: ModCtrl} // ctrl+[ or escape
 	}
 
-	del := Key{Type: KeyBackspace}
+	del := Key{Code: KeyBackspace}
 	if flags&_FlagBackspace != 0 {
-		del.Type = KeyDelete
+		del.Code = KeyDelete
 	}
 
-	find := Key{Type: KeyHome}
+	find := Key{Code: KeyHome}
 	if flags&_FlagFind != 0 {
-		find.Type = KeyFind
+		find.Code = KeyFind
 	}
 
-	sel := Key{Type: KeyEnd}
+	sel := Key{Code: KeyEnd}
 	if flags&_FlagSelect != 0 {
-		sel.Type = KeySelect
+		sel.Code = KeySelect
 	}
 
 	// The following is a table of key sequences and their corresponding key
@@ -56,155 +56,155 @@ func buildKeysTable(flags int, term string) map[string]Key {
 	table := map[string]Key{
 		// C0 control characters
 		string(byte(ansi.NUL)): nul,
-		string(byte(ansi.SOH)): {Runes: []rune{'a'}, Mod: ModCtrl},
-		string(byte(ansi.STX)): {Runes: []rune{'b'}, Mod: ModCtrl},
-		string(byte(ansi.ETX)): {Runes: []rune{'c'}, Mod: ModCtrl},
-		string(byte(ansi.EOT)): {Runes: []rune{'d'}, Mod: ModCtrl},
-		string(byte(ansi.ENQ)): {Runes: []rune{'e'}, Mod: ModCtrl},
-		string(byte(ansi.ACK)): {Runes: []rune{'f'}, Mod: ModCtrl},
-		string(byte(ansi.BEL)): {Runes: []rune{'g'}, Mod: ModCtrl},
-		string(byte(ansi.BS)):  {Runes: []rune{'h'}, Mod: ModCtrl},
+		string(byte(ansi.SOH)): {Code: 'a', Mod: ModCtrl},
+		string(byte(ansi.STX)): {Code: 'b', Mod: ModCtrl},
+		string(byte(ansi.ETX)): {Code: 'c', Mod: ModCtrl},
+		string(byte(ansi.EOT)): {Code: 'd', Mod: ModCtrl},
+		string(byte(ansi.ENQ)): {Code: 'e', Mod: ModCtrl},
+		string(byte(ansi.ACK)): {Code: 'f', Mod: ModCtrl},
+		string(byte(ansi.BEL)): {Code: 'g', Mod: ModCtrl},
+		string(byte(ansi.BS)):  {Code: 'h', Mod: ModCtrl},
 		string(byte(ansi.HT)):  tab,
-		string(byte(ansi.LF)):  {Runes: []rune{'j'}, Mod: ModCtrl},
-		string(byte(ansi.VT)):  {Runes: []rune{'k'}, Mod: ModCtrl},
-		string(byte(ansi.FF)):  {Runes: []rune{'l'}, Mod: ModCtrl},
+		string(byte(ansi.LF)):  {Code: 'j', Mod: ModCtrl},
+		string(byte(ansi.VT)):  {Code: 'k', Mod: ModCtrl},
+		string(byte(ansi.FF)):  {Code: 'l', Mod: ModCtrl},
 		string(byte(ansi.CR)):  enter,
-		string(byte(ansi.SO)):  {Runes: []rune{'n'}, Mod: ModCtrl},
-		string(byte(ansi.SI)):  {Runes: []rune{'o'}, Mod: ModCtrl},
-		string(byte(ansi.DLE)): {Runes: []rune{'p'}, Mod: ModCtrl},
-		string(byte(ansi.DC1)): {Runes: []rune{'q'}, Mod: ModCtrl},
-		string(byte(ansi.DC2)): {Runes: []rune{'r'}, Mod: ModCtrl},
-		string(byte(ansi.DC3)): {Runes: []rune{'s'}, Mod: ModCtrl},
-		string(byte(ansi.DC4)): {Runes: []rune{'t'}, Mod: ModCtrl},
-		string(byte(ansi.NAK)): {Runes: []rune{'u'}, Mod: ModCtrl},
-		string(byte(ansi.SYN)): {Runes: []rune{'v'}, Mod: ModCtrl},
-		string(byte(ansi.ETB)): {Runes: []rune{'w'}, Mod: ModCtrl},
-		string(byte(ansi.CAN)): {Runes: []rune{'x'}, Mod: ModCtrl},
-		string(byte(ansi.EM)):  {Runes: []rune{'y'}, Mod: ModCtrl},
-		string(byte(ansi.SUB)): {Runes: []rune{'z'}, Mod: ModCtrl},
+		string(byte(ansi.SO)):  {Code: 'n', Mod: ModCtrl},
+		string(byte(ansi.SI)):  {Code: 'o', Mod: ModCtrl},
+		string(byte(ansi.DLE)): {Code: 'p', Mod: ModCtrl},
+		string(byte(ansi.DC1)): {Code: 'q', Mod: ModCtrl},
+		string(byte(ansi.DC2)): {Code: 'r', Mod: ModCtrl},
+		string(byte(ansi.DC3)): {Code: 's', Mod: ModCtrl},
+		string(byte(ansi.DC4)): {Code: 't', Mod: ModCtrl},
+		string(byte(ansi.NAK)): {Code: 'u', Mod: ModCtrl},
+		string(byte(ansi.SYN)): {Code: 'v', Mod: ModCtrl},
+		string(byte(ansi.ETB)): {Code: 'w', Mod: ModCtrl},
+		string(byte(ansi.CAN)): {Code: 'x', Mod: ModCtrl},
+		string(byte(ansi.EM)):  {Code: 'y', Mod: ModCtrl},
+		string(byte(ansi.SUB)): {Code: 'z', Mod: ModCtrl},
 		string(byte(ansi.ESC)): esc,
-		string(byte(ansi.FS)):  {Runes: []rune{'\\'}, Mod: ModCtrl},
-		string(byte(ansi.GS)):  {Runes: []rune{']'}, Mod: ModCtrl},
-		string(byte(ansi.RS)):  {Runes: []rune{'^'}, Mod: ModCtrl},
-		string(byte(ansi.US)):  {Runes: []rune{'_'}, Mod: ModCtrl},
+		string(byte(ansi.FS)):  {Code: '\\', Mod: ModCtrl},
+		string(byte(ansi.GS)):  {Code: ']', Mod: ModCtrl},
+		string(byte(ansi.RS)):  {Code: '^', Mod: ModCtrl},
+		string(byte(ansi.US)):  {Code: '_', Mod: ModCtrl},
 
 		// Special keys in G0
-		string(byte(ansi.SP)):  {Type: KeySpace, Runes: []rune{' '}},
+		string(byte(ansi.SP)):  {Code: KeySpace, Text: " "},
 		string(byte(ansi.DEL)): del,
 
 		// Special keys
 
-		"\x1b[Z": {Type: KeyTab, Mod: ModShift},
+		"\x1b[Z": {Code: KeyTab, Mod: ModShift},
 
 		"\x1b[1~": find,
-		"\x1b[2~": {Type: KeyInsert},
-		"\x1b[3~": {Type: KeyDelete},
+		"\x1b[2~": {Code: KeyInsert},
+		"\x1b[3~": {Code: KeyDelete},
 		"\x1b[4~": sel,
-		"\x1b[5~": {Type: KeyPgUp},
-		"\x1b[6~": {Type: KeyPgDown},
-		"\x1b[7~": {Type: KeyHome},
-		"\x1b[8~": {Type: KeyEnd},
+		"\x1b[5~": {Code: KeyPgUp},
+		"\x1b[6~": {Code: KeyPgDown},
+		"\x1b[7~": {Code: KeyHome},
+		"\x1b[8~": {Code: KeyEnd},
 
 		// Normal mode
-		"\x1b[A": {Type: KeyUp},
-		"\x1b[B": {Type: KeyDown},
-		"\x1b[C": {Type: KeyRight},
-		"\x1b[D": {Type: KeyLeft},
-		"\x1b[E": {Type: KeyBegin},
-		"\x1b[F": {Type: KeyEnd},
-		"\x1b[H": {Type: KeyHome},
-		"\x1b[P": {Type: KeyF1},
-		"\x1b[Q": {Type: KeyF2},
-		"\x1b[R": {Type: KeyF3},
-		"\x1b[S": {Type: KeyF4},
+		"\x1b[A": {Code: KeyUp},
+		"\x1b[B": {Code: KeyDown},
+		"\x1b[C": {Code: KeyRight},
+		"\x1b[D": {Code: KeyLeft},
+		"\x1b[E": {Code: KeyBegin},
+		"\x1b[F": {Code: KeyEnd},
+		"\x1b[H": {Code: KeyHome},
+		"\x1b[P": {Code: KeyF1},
+		"\x1b[Q": {Code: KeyF2},
+		"\x1b[R": {Code: KeyF3},
+		"\x1b[S": {Code: KeyF4},
 
 		// Application Cursor Key Mode (DECCKM)
-		"\x1bOA": {Type: KeyUp},
-		"\x1bOB": {Type: KeyDown},
-		"\x1bOC": {Type: KeyRight},
-		"\x1bOD": {Type: KeyLeft},
-		"\x1bOE": {Type: KeyBegin},
-		"\x1bOF": {Type: KeyEnd},
-		"\x1bOH": {Type: KeyHome},
-		"\x1bOP": {Type: KeyF1},
-		"\x1bOQ": {Type: KeyF2},
-		"\x1bOR": {Type: KeyF3},
-		"\x1bOS": {Type: KeyF4},
+		"\x1bOA": {Code: KeyUp},
+		"\x1bOB": {Code: KeyDown},
+		"\x1bOC": {Code: KeyRight},
+		"\x1bOD": {Code: KeyLeft},
+		"\x1bOE": {Code: KeyBegin},
+		"\x1bOF": {Code: KeyEnd},
+		"\x1bOH": {Code: KeyHome},
+		"\x1bOP": {Code: KeyF1},
+		"\x1bOQ": {Code: KeyF2},
+		"\x1bOR": {Code: KeyF3},
+		"\x1bOS": {Code: KeyF4},
 
 		// Keypad Application Mode (DECKPAM)
 
-		"\x1bOM": {Type: KeyKpEnter},
-		"\x1bOX": {Type: KeyKpEqual},
-		"\x1bOj": {Type: KeyKpMultiply},
-		"\x1bOk": {Type: KeyKpPlus},
-		"\x1bOl": {Type: KeyKpComma},
-		"\x1bOm": {Type: KeyKpMinus},
-		"\x1bOn": {Type: KeyKpDecimal},
-		"\x1bOo": {Type: KeyKpDivide},
-		"\x1bOp": {Type: KeyKp0},
-		"\x1bOq": {Type: KeyKp1},
-		"\x1bOr": {Type: KeyKp2},
-		"\x1bOs": {Type: KeyKp3},
-		"\x1bOt": {Type: KeyKp4},
-		"\x1bOu": {Type: KeyKp5},
-		"\x1bOv": {Type: KeyKp6},
-		"\x1bOw": {Type: KeyKp7},
-		"\x1bOx": {Type: KeyKp8},
-		"\x1bOy": {Type: KeyKp9},
+		"\x1bOM": {Code: KeyKpEnter},
+		"\x1bOX": {Code: KeyKpEqual},
+		"\x1bOj": {Code: KeyKpMultiply},
+		"\x1bOk": {Code: KeyKpPlus},
+		"\x1bOl": {Code: KeyKpComma},
+		"\x1bOm": {Code: KeyKpMinus},
+		"\x1bOn": {Code: KeyKpDecimal},
+		"\x1bOo": {Code: KeyKpDivide},
+		"\x1bOp": {Code: KeyKp0},
+		"\x1bOq": {Code: KeyKp1},
+		"\x1bOr": {Code: KeyKp2},
+		"\x1bOs": {Code: KeyKp3},
+		"\x1bOt": {Code: KeyKp4},
+		"\x1bOu": {Code: KeyKp5},
+		"\x1bOv": {Code: KeyKp6},
+		"\x1bOw": {Code: KeyKp7},
+		"\x1bOx": {Code: KeyKp8},
+		"\x1bOy": {Code: KeyKp9},
 
 		// Function keys
 
-		"\x1b[11~": {Type: KeyF1},
-		"\x1b[12~": {Type: KeyF2},
-		"\x1b[13~": {Type: KeyF3},
-		"\x1b[14~": {Type: KeyF4},
-		"\x1b[15~": {Type: KeyF5},
-		"\x1b[17~": {Type: KeyF6},
-		"\x1b[18~": {Type: KeyF7},
-		"\x1b[19~": {Type: KeyF8},
-		"\x1b[20~": {Type: KeyF9},
-		"\x1b[21~": {Type: KeyF10},
-		"\x1b[23~": {Type: KeyF11},
-		"\x1b[24~": {Type: KeyF12},
-		"\x1b[25~": {Type: KeyF13},
-		"\x1b[26~": {Type: KeyF14},
-		"\x1b[28~": {Type: KeyF15},
-		"\x1b[29~": {Type: KeyF16},
-		"\x1b[31~": {Type: KeyF17},
-		"\x1b[32~": {Type: KeyF18},
-		"\x1b[33~": {Type: KeyF19},
-		"\x1b[34~": {Type: KeyF20},
+		"\x1b[11~": {Code: KeyF1},
+		"\x1b[12~": {Code: KeyF2},
+		"\x1b[13~": {Code: KeyF3},
+		"\x1b[14~": {Code: KeyF4},
+		"\x1b[15~": {Code: KeyF5},
+		"\x1b[17~": {Code: KeyF6},
+		"\x1b[18~": {Code: KeyF7},
+		"\x1b[19~": {Code: KeyF8},
+		"\x1b[20~": {Code: KeyF9},
+		"\x1b[21~": {Code: KeyF10},
+		"\x1b[23~": {Code: KeyF11},
+		"\x1b[24~": {Code: KeyF12},
+		"\x1b[25~": {Code: KeyF13},
+		"\x1b[26~": {Code: KeyF14},
+		"\x1b[28~": {Code: KeyF15},
+		"\x1b[29~": {Code: KeyF16},
+		"\x1b[31~": {Code: KeyF17},
+		"\x1b[32~": {Code: KeyF18},
+		"\x1b[33~": {Code: KeyF19},
+		"\x1b[34~": {Code: KeyF20},
 	}
 
 	// CSI ~ sequence keys
 	csiTildeKeys := map[string]Key{
-		"1": find, "2": {Type: KeyInsert},
-		"3": {Type: KeyDelete}, "4": sel,
-		"5": {Type: KeyPgUp}, "6": {Type: KeyPgDown},
-		"7": {Type: KeyHome}, "8": {Type: KeyEnd},
+		"1": find, "2": {Code: KeyInsert},
+		"3": {Code: KeyDelete}, "4": sel,
+		"5": {Code: KeyPgUp}, "6": {Code: KeyPgDown},
+		"7": {Code: KeyHome}, "8": {Code: KeyEnd},
 		// There are no 9 and 10 keys
-		"11": {Type: KeyF1}, "12": {Type: KeyF2},
-		"13": {Type: KeyF3}, "14": {Type: KeyF4},
-		"15": {Type: KeyF5}, "17": {Type: KeyF6},
-		"18": {Type: KeyF7}, "19": {Type: KeyF8},
-		"20": {Type: KeyF9}, "21": {Type: KeyF10},
-		"23": {Type: KeyF11}, "24": {Type: KeyF12},
-		"25": {Type: KeyF13}, "26": {Type: KeyF14},
-		"28": {Type: KeyF15}, "29": {Type: KeyF16},
-		"31": {Type: KeyF17}, "32": {Type: KeyF18},
-		"33": {Type: KeyF19}, "34": {Type: KeyF20},
+		"11": {Code: KeyF1}, "12": {Code: KeyF2},
+		"13": {Code: KeyF3}, "14": {Code: KeyF4},
+		"15": {Code: KeyF5}, "17": {Code: KeyF6},
+		"18": {Code: KeyF7}, "19": {Code: KeyF8},
+		"20": {Code: KeyF9}, "21": {Code: KeyF10},
+		"23": {Code: KeyF11}, "24": {Code: KeyF12},
+		"25": {Code: KeyF13}, "26": {Code: KeyF14},
+		"28": {Code: KeyF15}, "29": {Code: KeyF16},
+		"31": {Code: KeyF17}, "32": {Code: KeyF18},
+		"33": {Code: KeyF19}, "34": {Code: KeyF20},
 	}
 
 	// URxvt keys
 	// See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes
-	table["\x1b[a"] = Key{Type: KeyUp, Mod: ModShift}
-	table["\x1b[b"] = Key{Type: KeyDown, Mod: ModShift}
-	table["\x1b[c"] = Key{Type: KeyRight, Mod: ModShift}
-	table["\x1b[d"] = Key{Type: KeyLeft, Mod: ModShift}
-	table["\x1bOa"] = Key{Type: KeyUp, Mod: ModCtrl}
-	table["\x1bOb"] = Key{Type: KeyDown, Mod: ModCtrl}
-	table["\x1bOc"] = Key{Type: KeyRight, Mod: ModCtrl}
-	table["\x1bOd"] = Key{Type: KeyLeft, Mod: ModCtrl}
+	table["\x1b[a"] = Key{Code: KeyUp, Mod: ModShift}
+	table["\x1b[b"] = Key{Code: KeyDown, Mod: ModShift}
+	table["\x1b[c"] = Key{Code: KeyRight, Mod: ModShift}
+	table["\x1b[d"] = Key{Code: KeyLeft, Mod: ModShift}
+	table["\x1bOa"] = Key{Code: KeyUp, Mod: ModCtrl}
+	table["\x1bOb"] = Key{Code: KeyDown, Mod: ModCtrl}
+	table["\x1bOc"] = Key{Code: KeyRight, Mod: ModCtrl}
+	table["\x1bOd"] = Key{Code: KeyLeft, Mod: ModCtrl}
 	// TODO: invistigate if shift-ctrl arrow keys collide with DECCKM keys i.e.
 	// "\x1bOA", "\x1bOB", "\x1bOC", "\x1bOD"
 
@@ -232,46 +232,46 @@ func buildKeysTable(flags int, term string) map[string]Key {
 	// different escapes like XTerm, or switch to a better terminal ¯\_(ツ)_/¯
 	//
 	// See https://manpages.ubuntu.com/manpages/trusty/man7/urxvt.7.html#key%20codes
-	table["\x1b[23$"] = Key{Type: KeyF11, Mod: ModShift}
-	table["\x1b[24$"] = Key{Type: KeyF12, Mod: ModShift}
-	table["\x1b[25$"] = Key{Type: KeyF13, Mod: ModShift}
-	table["\x1b[26$"] = Key{Type: KeyF14, Mod: ModShift}
-	table["\x1b[28$"] = Key{Type: KeyF15, Mod: ModShift}
-	table["\x1b[29$"] = Key{Type: KeyF16, Mod: ModShift}
-	table["\x1b[31$"] = Key{Type: KeyF17, Mod: ModShift}
-	table["\x1b[32$"] = Key{Type: KeyF18, Mod: ModShift}
-	table["\x1b[33$"] = Key{Type: KeyF19, Mod: ModShift}
-	table["\x1b[34$"] = Key{Type: KeyF20, Mod: ModShift}
-	table["\x1b[11^"] = Key{Type: KeyF1, Mod: ModCtrl}
-	table["\x1b[12^"] = Key{Type: KeyF2, Mod: ModCtrl}
-	table["\x1b[13^"] = Key{Type: KeyF3, Mod: ModCtrl}
-	table["\x1b[14^"] = Key{Type: KeyF4, Mod: ModCtrl}
-	table["\x1b[15^"] = Key{Type: KeyF5, Mod: ModCtrl}
-	table["\x1b[17^"] = Key{Type: KeyF6, Mod: ModCtrl}
-	table["\x1b[18^"] = Key{Type: KeyF7, Mod: ModCtrl}
-	table["\x1b[19^"] = Key{Type: KeyF8, Mod: ModCtrl}
-	table["\x1b[20^"] = Key{Type: KeyF9, Mod: ModCtrl}
-	table["\x1b[21^"] = Key{Type: KeyF10, Mod: ModCtrl}
-	table["\x1b[23^"] = Key{Type: KeyF11, Mod: ModCtrl}
-	table["\x1b[24^"] = Key{Type: KeyF12, Mod: ModCtrl}
-	table["\x1b[25^"] = Key{Type: KeyF13, Mod: ModCtrl}
-	table["\x1b[26^"] = Key{Type: KeyF14, Mod: ModCtrl}
-	table["\x1b[28^"] = Key{Type: KeyF15, Mod: ModCtrl}
-	table["\x1b[29^"] = Key{Type: KeyF16, Mod: ModCtrl}
-	table["\x1b[31^"] = Key{Type: KeyF17, Mod: ModCtrl}
-	table["\x1b[32^"] = Key{Type: KeyF18, Mod: ModCtrl}
-	table["\x1b[33^"] = Key{Type: KeyF19, Mod: ModCtrl}
-	table["\x1b[34^"] = Key{Type: KeyF20, Mod: ModCtrl}
-	table["\x1b[23@"] = Key{Type: KeyF11, Mod: ModShift | ModCtrl}
-	table["\x1b[24@"] = Key{Type: KeyF12, Mod: ModShift | ModCtrl}
-	table["\x1b[25@"] = Key{Type: KeyF13, Mod: ModShift | ModCtrl}
-	table["\x1b[26@"] = Key{Type: KeyF14, Mod: ModShift | ModCtrl}
-	table["\x1b[28@"] = Key{Type: KeyF15, Mod: ModShift | ModCtrl}
-	table["\x1b[29@"] = Key{Type: KeyF16, Mod: ModShift | ModCtrl}
-	table["\x1b[31@"] = Key{Type: KeyF17, Mod: ModShift | ModCtrl}
-	table["\x1b[32@"] = Key{Type: KeyF18, Mod: ModShift | ModCtrl}
-	table["\x1b[33@"] = Key{Type: KeyF19, Mod: ModShift | ModCtrl}
-	table["\x1b[34@"] = Key{Type: KeyF20, Mod: ModShift | ModCtrl}
+	table["\x1b[23$"] = Key{Code: KeyF11, Mod: ModShift}
+	table["\x1b[24$"] = Key{Code: KeyF12, Mod: ModShift}
+	table["\x1b[25$"] = Key{Code: KeyF13, Mod: ModShift}
+	table["\x1b[26$"] = Key{Code: KeyF14, Mod: ModShift}
+	table["\x1b[28$"] = Key{Code: KeyF15, Mod: ModShift}
+	table["\x1b[29$"] = Key{Code: KeyF16, Mod: ModShift}
+	table["\x1b[31$"] = Key{Code: KeyF17, Mod: ModShift}
+	table["\x1b[32$"] = Key{Code: KeyF18, Mod: ModShift}
+	table["\x1b[33$"] = Key{Code: KeyF19, Mod: ModShift}
+	table["\x1b[34$"] = Key{Code: KeyF20, Mod: ModShift}
+	table["\x1b[11^"] = Key{Code: KeyF1, Mod: ModCtrl}
+	table["\x1b[12^"] = Key{Code: KeyF2, Mod: ModCtrl}
+	table["\x1b[13^"] = Key{Code: KeyF3, Mod: ModCtrl}
+	table["\x1b[14^"] = Key{Code: KeyF4, Mod: ModCtrl}
+	table["\x1b[15^"] = Key{Code: KeyF5, Mod: ModCtrl}
+	table["\x1b[17^"] = Key{Code: KeyF6, Mod: ModCtrl}
+	table["\x1b[18^"] = Key{Code: KeyF7, Mod: ModCtrl}
+	table["\x1b[19^"] = Key{Code: KeyF8, Mod: ModCtrl}
+	table["\x1b[20^"] = Key{Code: KeyF9, Mod: ModCtrl}
+	table["\x1b[21^"] = Key{Code: KeyF10, Mod: ModCtrl}
+	table["\x1b[23^"] = Key{Code: KeyF11, Mod: ModCtrl}
+	table["\x1b[24^"] = Key{Code: KeyF12, Mod: ModCtrl}
+	table["\x1b[25^"] = Key{Code: KeyF13, Mod: ModCtrl}
+	table["\x1b[26^"] = Key{Code: KeyF14, Mod: ModCtrl}
+	table["\x1b[28^"] = Key{Code: KeyF15, Mod: ModCtrl}
+	table["\x1b[29^"] = Key{Code: KeyF16, Mod: ModCtrl}
+	table["\x1b[31^"] = Key{Code: KeyF17, Mod: ModCtrl}
+	table["\x1b[32^"] = Key{Code: KeyF18, Mod: ModCtrl}
+	table["\x1b[33^"] = Key{Code: KeyF19, Mod: ModCtrl}
+	table["\x1b[34^"] = Key{Code: KeyF20, Mod: ModCtrl}
+	table["\x1b[23@"] = Key{Code: KeyF11, Mod: ModShift | ModCtrl}
+	table["\x1b[24@"] = Key{Code: KeyF12, Mod: ModShift | ModCtrl}
+	table["\x1b[25@"] = Key{Code: KeyF13, Mod: ModShift | ModCtrl}
+	table["\x1b[26@"] = Key{Code: KeyF14, Mod: ModShift | ModCtrl}
+	table["\x1b[28@"] = Key{Code: KeyF15, Mod: ModShift | ModCtrl}
+	table["\x1b[29@"] = Key{Code: KeyF16, Mod: ModShift | ModCtrl}
+	table["\x1b[31@"] = Key{Code: KeyF17, Mod: ModShift | ModCtrl}
+	table["\x1b[32@"] = Key{Code: KeyF18, Mod: ModShift | ModCtrl}
+	table["\x1b[33@"] = Key{Code: KeyF19, Mod: ModShift | ModCtrl}
+	table["\x1b[34@"] = Key{Code: KeyF20, Mod: ModShift | ModCtrl}
 
 	// Register Alt + <key> combinations
 	// XXX: this must come after URxvt but before XTerm keys to register URxvt
@@ -280,6 +280,7 @@ func buildKeysTable(flags int, term string) map[string]Key {
 	for seq, key := range table {
 		key := key
 		key.Mod |= ModAlt
+		key.Text = "" // Clear runes
 		tmap["\x1b"+seq] = key
 	}
 	for seq, key := range tmap {
@@ -312,34 +313,34 @@ func buildKeysTable(flags int, term string) map[string]Key {
 		// These are defined in XTerm
 		// Taken from Foot keymap.h and XTerm modifyOtherKeys
 		// https://codeberg.org/dnkl/foot/src/branch/master/keymap.h
-		"M": {Type: KeyKpEnter}, "X": {Type: KeyKpEqual},
-		"j": {Type: KeyKpMultiply}, "k": {Type: KeyKpPlus},
-		"l": {Type: KeyKpComma}, "m": {Type: KeyKpMinus},
-		"n": {Type: KeyKpDecimal}, "o": {Type: KeyKpDivide},
-		"p": {Type: KeyKp0}, "q": {Type: KeyKp1},
-		"r": {Type: KeyKp2}, "s": {Type: KeyKp3},
-		"t": {Type: KeyKp4}, "u": {Type: KeyKp5},
-		"v": {Type: KeyKp6}, "w": {Type: KeyKp7},
-		"x": {Type: KeyKp8}, "y": {Type: KeyKp9},
+		"M": {Code: KeyKpEnter}, "X": {Code: KeyKpEqual},
+		"j": {Code: KeyKpMultiply}, "k": {Code: KeyKpPlus},
+		"l": {Code: KeyKpComma}, "m": {Code: KeyKpMinus},
+		"n": {Code: KeyKpDecimal}, "o": {Code: KeyKpDivide},
+		"p": {Code: KeyKp0}, "q": {Code: KeyKp1},
+		"r": {Code: KeyKp2}, "s": {Code: KeyKp3},
+		"t": {Code: KeyKp4}, "u": {Code: KeyKp5},
+		"v": {Code: KeyKp6}, "w": {Code: KeyKp7},
+		"x": {Code: KeyKp8}, "y": {Code: KeyKp9},
 	}
 
 	// XTerm keys
 	csiFuncKeys := map[string]Key{
-		"A": {Type: KeyUp}, "B": {Type: KeyDown},
-		"C": {Type: KeyRight}, "D": {Type: KeyLeft},
-		"E": {Type: KeyBegin}, "F": {Type: KeyEnd},
-		"H": {Type: KeyHome}, "P": {Type: KeyF1},
-		"Q": {Type: KeyF2}, "R": {Type: KeyF3},
-		"S": {Type: KeyF4},
+		"A": {Code: KeyUp}, "B": {Code: KeyDown},
+		"C": {Code: KeyRight}, "D": {Code: KeyLeft},
+		"E": {Code: KeyBegin}, "F": {Code: KeyEnd},
+		"H": {Code: KeyHome}, "P": {Code: KeyF1},
+		"Q": {Code: KeyF2}, "R": {Code: KeyF3},
+		"S": {Code: KeyF4},
 	}
 
 	// CSI 27 ; <modifier> ; <code> ~ keys defined in XTerm modifyOtherKeys
 	modifyOtherKeys := map[int]Key{
-		ansi.BS:  {Type: KeyBackspace},
-		ansi.HT:  {Type: KeyTab},
-		ansi.CR:  {Type: KeyEnter},
-		ansi.ESC: {Type: KeyEscape},
-		ansi.DEL: {Type: KeyBackspace},
+		ansi.BS:  {Code: KeyBackspace},
+		ansi.HT:  {Code: KeyTab},
+		ansi.CR:  {Code: KeyEnter},
+		ansi.ESC: {Code: KeyEscape},
+		ansi.DEL: {Code: KeyBackspace},
 	}
 
 	for _, m := range modifiers {
diff --git a/terminfo.go b/terminfo.go
index e4be0dd..3f74b2d 100644
--- a/terminfo.go
+++ b/terminfo.go
@@ -56,91 +56,91 @@ func buildTerminfoKeys(flags int, term string) map[string]Key {
 // See https://github.com/mirror/ncurses/blob/master/include/Caps-ncurses
 func defaultTerminfoKeys(flags int) map[string]Key {
 	keys := map[string]Key{
-		"kcuu1": {Type: KeyUp},
-		"kUP":   {Type: KeyUp, Mod: ModShift},
-		"kUP3":  {Type: KeyUp, Mod: ModAlt},
-		"kUP4":  {Type: KeyUp, Mod: ModShift | ModAlt},
-		"kUP5":  {Type: KeyUp, Mod: ModCtrl},
-		"kUP6":  {Type: KeyUp, Mod: ModShift | ModCtrl},
-		"kUP7":  {Type: KeyUp, Mod: ModAlt | ModCtrl},
-		"kUP8":  {Type: KeyUp, Mod: ModShift | ModAlt | ModCtrl},
-		"kcud1": {Type: KeyDown},
-		"kDN":   {Type: KeyDown, Mod: ModShift},
-		"kDN3":  {Type: KeyDown, Mod: ModAlt},
-		"kDN4":  {Type: KeyDown, Mod: ModShift | ModAlt},
-		"kDN5":  {Type: KeyDown, Mod: ModCtrl},
-		"kDN7":  {Type: KeyDown, Mod: ModAlt | ModCtrl},
-		"kDN6":  {Type: KeyDown, Mod: ModShift | ModCtrl},
-		"kDN8":  {Type: KeyDown, Mod: ModShift | ModAlt | ModCtrl},
-		"kcub1": {Type: KeyLeft},
-		"kLFT":  {Type: KeyLeft, Mod: ModShift},
-		"kLFT3": {Type: KeyLeft, Mod: ModAlt},
-		"kLFT4": {Type: KeyLeft, Mod: ModShift | ModAlt},
-		"kLFT5": {Type: KeyLeft, Mod: ModCtrl},
-		"kLFT6": {Type: KeyLeft, Mod: ModShift | ModCtrl},
-		"kLFT7": {Type: KeyLeft, Mod: ModAlt | ModCtrl},
-		"kLFT8": {Type: KeyLeft, Mod: ModShift | ModAlt | ModCtrl},
-		"kcuf1": {Type: KeyRight},
-		"kRIT":  {Type: KeyRight, Mod: ModShift},
-		"kRIT3": {Type: KeyRight, Mod: ModAlt},
-		"kRIT4": {Type: KeyRight, Mod: ModShift | ModAlt},
-		"kRIT5": {Type: KeyRight, Mod: ModCtrl},
-		"kRIT6": {Type: KeyRight, Mod: ModShift | ModCtrl},
-		"kRIT7": {Type: KeyRight, Mod: ModAlt | ModCtrl},
-		"kRIT8": {Type: KeyRight, Mod: ModShift | ModAlt | ModCtrl},
-		"kich1": {Type: KeyInsert},
-		"kIC":   {Type: KeyInsert, Mod: ModShift},
-		"kIC3":  {Type: KeyInsert, Mod: ModAlt},
-		"kIC4":  {Type: KeyInsert, Mod: ModShift | ModAlt},
-		"kIC5":  {Type: KeyInsert, Mod: ModCtrl},
-		"kIC6":  {Type: KeyInsert, Mod: ModShift | ModCtrl},
-		"kIC7":  {Type: KeyInsert, Mod: ModAlt | ModCtrl},
-		"kIC8":  {Type: KeyInsert, Mod: ModShift | ModAlt | ModCtrl},
-		"kdch1": {Type: KeyDelete},
-		"kDC":   {Type: KeyDelete, Mod: ModShift},
-		"kDC3":  {Type: KeyDelete, Mod: ModAlt},
-		"kDC4":  {Type: KeyDelete, Mod: ModShift | ModAlt},
-		"kDC5":  {Type: KeyDelete, Mod: ModCtrl},
-		"kDC6":  {Type: KeyDelete, Mod: ModShift | ModCtrl},
-		"kDC7":  {Type: KeyDelete, Mod: ModAlt | ModCtrl},
-		"kDC8":  {Type: KeyDelete, Mod: ModShift | ModAlt | ModCtrl},
-		"khome": {Type: KeyHome},
-		"kHOM":  {Type: KeyHome, Mod: ModShift},
-		"kHOM3": {Type: KeyHome, Mod: ModAlt},
-		"kHOM4": {Type: KeyHome, Mod: ModShift | ModAlt},
-		"kHOM5": {Type: KeyHome, Mod: ModCtrl},
-		"kHOM6": {Type: KeyHome, Mod: ModShift | ModCtrl},
-		"kHOM7": {Type: KeyHome, Mod: ModAlt | ModCtrl},
-		"kHOM8": {Type: KeyHome, Mod: ModShift | ModAlt | ModCtrl},
-		"kend":  {Type: KeyEnd},
-		"kEND":  {Type: KeyEnd, Mod: ModShift},
-		"kEND3": {Type: KeyEnd, Mod: ModAlt},
-		"kEND4": {Type: KeyEnd, Mod: ModShift | ModAlt},
-		"kEND5": {Type: KeyEnd, Mod: ModCtrl},
-		"kEND6": {Type: KeyEnd, Mod: ModShift | ModCtrl},
-		"kEND7": {Type: KeyEnd, Mod: ModAlt | ModCtrl},
-		"kEND8": {Type: KeyEnd, Mod: ModShift | ModAlt | ModCtrl},
-		"kpp":   {Type: KeyPgUp},
-		"kprv":  {Type: KeyPgUp},
-		"kPRV":  {Type: KeyPgUp, Mod: ModShift},
-		"kPRV3": {Type: KeyPgUp, Mod: ModAlt},
-		"kPRV4": {Type: KeyPgUp, Mod: ModShift | ModAlt},
-		"kPRV5": {Type: KeyPgUp, Mod: ModCtrl},
-		"kPRV6": {Type: KeyPgUp, Mod: ModShift | ModCtrl},
-		"kPRV7": {Type: KeyPgUp, Mod: ModAlt | ModCtrl},
-		"kPRV8": {Type: KeyPgUp, Mod: ModShift | ModAlt | ModCtrl},
-		"knp":   {Type: KeyPgDown},
-		"knxt":  {Type: KeyPgDown},
-		"kNXT":  {Type: KeyPgDown, Mod: ModShift},
-		"kNXT3": {Type: KeyPgDown, Mod: ModAlt},
-		"kNXT4": {Type: KeyPgDown, Mod: ModShift | ModAlt},
-		"kNXT5": {Type: KeyPgDown, Mod: ModCtrl},
-		"kNXT6": {Type: KeyPgDown, Mod: ModShift | ModCtrl},
-		"kNXT7": {Type: KeyPgDown, Mod: ModAlt | ModCtrl},
-		"kNXT8": {Type: KeyPgDown, Mod: ModShift | ModAlt | ModCtrl},
+		"kcuu1": {Code: KeyUp},
+		"kUP":   {Code: KeyUp, Mod: ModShift},
+		"kUP3":  {Code: KeyUp, Mod: ModAlt},
+		"kUP4":  {Code: KeyUp, Mod: ModShift | ModAlt},
+		"kUP5":  {Code: KeyUp, Mod: ModCtrl},
+		"kUP6":  {Code: KeyUp, Mod: ModShift | ModCtrl},
+		"kUP7":  {Code: KeyUp, Mod: ModAlt | ModCtrl},
+		"kUP8":  {Code: KeyUp, Mod: ModShift | ModAlt | ModCtrl},
+		"kcud1": {Code: KeyDown},
+		"kDN":   {Code: KeyDown, Mod: ModShift},
+		"kDN3":  {Code: KeyDown, Mod: ModAlt},
+		"kDN4":  {Code: KeyDown, Mod: ModShift | ModAlt},
+		"kDN5":  {Code: KeyDown, Mod: ModCtrl},
+		"kDN7":  {Code: KeyDown, Mod: ModAlt | ModCtrl},
+		"kDN6":  {Code: KeyDown, Mod: ModShift | ModCtrl},
+		"kDN8":  {Code: KeyDown, Mod: ModShift | ModAlt | ModCtrl},
+		"kcub1": {Code: KeyLeft},
+		"kLFT":  {Code: KeyLeft, Mod: ModShift},
+		"kLFT3": {Code: KeyLeft, Mod: ModAlt},
+		"kLFT4": {Code: KeyLeft, Mod: ModShift | ModAlt},
+		"kLFT5": {Code: KeyLeft, Mod: ModCtrl},
+		"kLFT6": {Code: KeyLeft, Mod: ModShift | ModCtrl},
+		"kLFT7": {Code: KeyLeft, Mod: ModAlt | ModCtrl},
+		"kLFT8": {Code: KeyLeft, Mod: ModShift | ModAlt | ModCtrl},
+		"kcuf1": {Code: KeyRight},
+		"kRIT":  {Code: KeyRight, Mod: ModShift},
+		"kRIT3": {Code: KeyRight, Mod: ModAlt},
+		"kRIT4": {Code: KeyRight, Mod: ModShift | ModAlt},
+		"kRIT5": {Code: KeyRight, Mod: ModCtrl},
+		"kRIT6": {Code: KeyRight, Mod: ModShift | ModCtrl},
+		"kRIT7": {Code: KeyRight, Mod: ModAlt | ModCtrl},
+		"kRIT8": {Code: KeyRight, Mod: ModShift | ModAlt | ModCtrl},
+		"kich1": {Code: KeyInsert},
+		"kIC":   {Code: KeyInsert, Mod: ModShift},
+		"kIC3":  {Code: KeyInsert, Mod: ModAlt},
+		"kIC4":  {Code: KeyInsert, Mod: ModShift | ModAlt},
+		"kIC5":  {Code: KeyInsert, Mod: ModCtrl},
+		"kIC6":  {Code: KeyInsert, Mod: ModShift | ModCtrl},
+		"kIC7":  {Code: KeyInsert, Mod: ModAlt | ModCtrl},
+		"kIC8":  {Code: KeyInsert, Mod: ModShift | ModAlt | ModCtrl},
+		"kdch1": {Code: KeyDelete},
+		"kDC":   {Code: KeyDelete, Mod: ModShift},
+		"kDC3":  {Code: KeyDelete, Mod: ModAlt},
+		"kDC4":  {Code: KeyDelete, Mod: ModShift | ModAlt},
+		"kDC5":  {Code: KeyDelete, Mod: ModCtrl},
+		"kDC6":  {Code: KeyDelete, Mod: ModShift | ModCtrl},
+		"kDC7":  {Code: KeyDelete, Mod: ModAlt | ModCtrl},
+		"kDC8":  {Code: KeyDelete, Mod: ModShift | ModAlt | ModCtrl},
+		"khome": {Code: KeyHome},
+		"kHOM":  {Code: KeyHome, Mod: ModShift},
+		"kHOM3": {Code: KeyHome, Mod: ModAlt},
+		"kHOM4": {Code: KeyHome, Mod: ModShift | ModAlt},
+		"kHOM5": {Code: KeyHome, Mod: ModCtrl},
+		"kHOM6": {Code: KeyHome, Mod: ModShift | ModCtrl},
+		"kHOM7": {Code: KeyHome, Mod: ModAlt | ModCtrl},
+		"kHOM8": {Code: KeyHome, Mod: ModShift | ModAlt | ModCtrl},
+		"kend":  {Code: KeyEnd},
+		"kEND":  {Code: KeyEnd, Mod: ModShift},
+		"kEND3": {Code: KeyEnd, Mod: ModAlt},
+		"kEND4": {Code: KeyEnd, Mod: ModShift | ModAlt},
+		"kEND5": {Code: KeyEnd, Mod: ModCtrl},
+		"kEND6": {Code: KeyEnd, Mod: ModShift | ModCtrl},
+		"kEND7": {Code: KeyEnd, Mod: ModAlt | ModCtrl},
+		"kEND8": {Code: KeyEnd, Mod: ModShift | ModAlt | ModCtrl},
+		"kpp":   {Code: KeyPgUp},
+		"kprv":  {Code: KeyPgUp},
+		"kPRV":  {Code: KeyPgUp, Mod: ModShift},
+		"kPRV3": {Code: KeyPgUp, Mod: ModAlt},
+		"kPRV4": {Code: KeyPgUp, Mod: ModShift | ModAlt},
+		"kPRV5": {Code: KeyPgUp, Mod: ModCtrl},
+		"kPRV6": {Code: KeyPgUp, Mod: ModShift | ModCtrl},
+		"kPRV7": {Code: KeyPgUp, Mod: ModAlt | ModCtrl},
+		"kPRV8": {Code: KeyPgUp, Mod: ModShift | ModAlt | ModCtrl},
+		"knp":   {Code: KeyPgDown},
+		"knxt":  {Code: KeyPgDown},
+		"kNXT":  {Code: KeyPgDown, Mod: ModShift},
+		"kNXT3": {Code: KeyPgDown, Mod: ModAlt},
+		"kNXT4": {Code: KeyPgDown, Mod: ModShift | ModAlt},
+		"kNXT5": {Code: KeyPgDown, Mod: ModCtrl},
+		"kNXT6": {Code: KeyPgDown, Mod: ModShift | ModCtrl},
+		"kNXT7": {Code: KeyPgDown, Mod: ModAlt | ModCtrl},
+		"kNXT8": {Code: KeyPgDown, Mod: ModShift | ModAlt | ModCtrl},
 
-		"kbs":  {Type: KeyBackspace},
-		"kcbt": {Type: KeyTab, Mod: ModShift},
+		"kbs":  {Code: KeyBackspace},
+		"kcbt": {Code: KeyTab, Mod: ModShift},
 
 		// Function keys
 		// This only includes the first 12 function keys. The rest are treated
@@ -152,125 +152,125 @@ func defaultTerminfoKeys(flags int) map[string]Key {
 		// See https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyFunctionKeys
 		// See https://invisible-island.net/xterm/terminfo.html
 
-		"kf1":  {Type: KeyF1},
-		"kf2":  {Type: KeyF2},
-		"kf3":  {Type: KeyF3},
-		"kf4":  {Type: KeyF4},
-		"kf5":  {Type: KeyF5},
-		"kf6":  {Type: KeyF6},
-		"kf7":  {Type: KeyF7},
-		"kf8":  {Type: KeyF8},
-		"kf9":  {Type: KeyF9},
-		"kf10": {Type: KeyF10},
-		"kf11": {Type: KeyF11},
-		"kf12": {Type: KeyF12},
-		"kf13": {Type: KeyF1, Mod: ModShift},
-		"kf14": {Type: KeyF2, Mod: ModShift},
-		"kf15": {Type: KeyF3, Mod: ModShift},
-		"kf16": {Type: KeyF4, Mod: ModShift},
-		"kf17": {Type: KeyF5, Mod: ModShift},
-		"kf18": {Type: KeyF6, Mod: ModShift},
-		"kf19": {Type: KeyF7, Mod: ModShift},
-		"kf20": {Type: KeyF8, Mod: ModShift},
-		"kf21": {Type: KeyF9, Mod: ModShift},
-		"kf22": {Type: KeyF10, Mod: ModShift},
-		"kf23": {Type: KeyF11, Mod: ModShift},
-		"kf24": {Type: KeyF12, Mod: ModShift},
-		"kf25": {Type: KeyF1, Mod: ModCtrl},
-		"kf26": {Type: KeyF2, Mod: ModCtrl},
-		"kf27": {Type: KeyF3, Mod: ModCtrl},
-		"kf28": {Type: KeyF4, Mod: ModCtrl},
-		"kf29": {Type: KeyF5, Mod: ModCtrl},
-		"kf30": {Type: KeyF6, Mod: ModCtrl},
-		"kf31": {Type: KeyF7, Mod: ModCtrl},
-		"kf32": {Type: KeyF8, Mod: ModCtrl},
-		"kf33": {Type: KeyF9, Mod: ModCtrl},
-		"kf34": {Type: KeyF10, Mod: ModCtrl},
-		"kf35": {Type: KeyF11, Mod: ModCtrl},
-		"kf36": {Type: KeyF12, Mod: ModCtrl},
-		"kf37": {Type: KeyF1, Mod: ModShift | ModCtrl},
-		"kf38": {Type: KeyF2, Mod: ModShift | ModCtrl},
-		"kf39": {Type: KeyF3, Mod: ModShift | ModCtrl},
-		"kf40": {Type: KeyF4, Mod: ModShift | ModCtrl},
-		"kf41": {Type: KeyF5, Mod: ModShift | ModCtrl},
-		"kf42": {Type: KeyF6, Mod: ModShift | ModCtrl},
-		"kf43": {Type: KeyF7, Mod: ModShift | ModCtrl},
-		"kf44": {Type: KeyF8, Mod: ModShift | ModCtrl},
-		"kf45": {Type: KeyF9, Mod: ModShift | ModCtrl},
-		"kf46": {Type: KeyF10, Mod: ModShift | ModCtrl},
-		"kf47": {Type: KeyF11, Mod: ModShift | ModCtrl},
-		"kf48": {Type: KeyF12, Mod: ModShift | ModCtrl},
-		"kf49": {Type: KeyF1, Mod: ModAlt},
-		"kf50": {Type: KeyF2, Mod: ModAlt},
-		"kf51": {Type: KeyF3, Mod: ModAlt},
-		"kf52": {Type: KeyF4, Mod: ModAlt},
-		"kf53": {Type: KeyF5, Mod: ModAlt},
-		"kf54": {Type: KeyF6, Mod: ModAlt},
-		"kf55": {Type: KeyF7, Mod: ModAlt},
-		"kf56": {Type: KeyF8, Mod: ModAlt},
-		"kf57": {Type: KeyF9, Mod: ModAlt},
-		"kf58": {Type: KeyF10, Mod: ModAlt},
-		"kf59": {Type: KeyF11, Mod: ModAlt},
-		"kf60": {Type: KeyF12, Mod: ModAlt},
-		"kf61": {Type: KeyF1, Mod: ModShift | ModAlt},
-		"kf62": {Type: KeyF2, Mod: ModShift | ModAlt},
-		"kf63": {Type: KeyF3, Mod: ModShift | ModAlt},
+		"kf1":  {Code: KeyF1},
+		"kf2":  {Code: KeyF2},
+		"kf3":  {Code: KeyF3},
+		"kf4":  {Code: KeyF4},
+		"kf5":  {Code: KeyF5},
+		"kf6":  {Code: KeyF6},
+		"kf7":  {Code: KeyF7},
+		"kf8":  {Code: KeyF8},
+		"kf9":  {Code: KeyF9},
+		"kf10": {Code: KeyF10},
+		"kf11": {Code: KeyF11},
+		"kf12": {Code: KeyF12},
+		"kf13": {Code: KeyF1, Mod: ModShift},
+		"kf14": {Code: KeyF2, Mod: ModShift},
+		"kf15": {Code: KeyF3, Mod: ModShift},
+		"kf16": {Code: KeyF4, Mod: ModShift},
+		"kf17": {Code: KeyF5, Mod: ModShift},
+		"kf18": {Code: KeyF6, Mod: ModShift},
+		"kf19": {Code: KeyF7, Mod: ModShift},
+		"kf20": {Code: KeyF8, Mod: ModShift},
+		"kf21": {Code: KeyF9, Mod: ModShift},
+		"kf22": {Code: KeyF10, Mod: ModShift},
+		"kf23": {Code: KeyF11, Mod: ModShift},
+		"kf24": {Code: KeyF12, Mod: ModShift},
+		"kf25": {Code: KeyF1, Mod: ModCtrl},
+		"kf26": {Code: KeyF2, Mod: ModCtrl},
+		"kf27": {Code: KeyF3, Mod: ModCtrl},
+		"kf28": {Code: KeyF4, Mod: ModCtrl},
+		"kf29": {Code: KeyF5, Mod: ModCtrl},
+		"kf30": {Code: KeyF6, Mod: ModCtrl},
+		"kf31": {Code: KeyF7, Mod: ModCtrl},
+		"kf32": {Code: KeyF8, Mod: ModCtrl},
+		"kf33": {Code: KeyF9, Mod: ModCtrl},
+		"kf34": {Code: KeyF10, Mod: ModCtrl},
+		"kf35": {Code: KeyF11, Mod: ModCtrl},
+		"kf36": {Code: KeyF12, Mod: ModCtrl},
+		"kf37": {Code: KeyF1, Mod: ModShift | ModCtrl},
+		"kf38": {Code: KeyF2, Mod: ModShift | ModCtrl},
+		"kf39": {Code: KeyF3, Mod: ModShift | ModCtrl},
+		"kf40": {Code: KeyF4, Mod: ModShift | ModCtrl},
+		"kf41": {Code: KeyF5, Mod: ModShift | ModCtrl},
+		"kf42": {Code: KeyF6, Mod: ModShift | ModCtrl},
+		"kf43": {Code: KeyF7, Mod: ModShift | ModCtrl},
+		"kf44": {Code: KeyF8, Mod: ModShift | ModCtrl},
+		"kf45": {Code: KeyF9, Mod: ModShift | ModCtrl},
+		"kf46": {Code: KeyF10, Mod: ModShift | ModCtrl},
+		"kf47": {Code: KeyF11, Mod: ModShift | ModCtrl},
+		"kf48": {Code: KeyF12, Mod: ModShift | ModCtrl},
+		"kf49": {Code: KeyF1, Mod: ModAlt},
+		"kf50": {Code: KeyF2, Mod: ModAlt},
+		"kf51": {Code: KeyF3, Mod: ModAlt},
+		"kf52": {Code: KeyF4, Mod: ModAlt},
+		"kf53": {Code: KeyF5, Mod: ModAlt},
+		"kf54": {Code: KeyF6, Mod: ModAlt},
+		"kf55": {Code: KeyF7, Mod: ModAlt},
+		"kf56": {Code: KeyF8, Mod: ModAlt},
+		"kf57": {Code: KeyF9, Mod: ModAlt},
+		"kf58": {Code: KeyF10, Mod: ModAlt},
+		"kf59": {Code: KeyF11, Mod: ModAlt},
+		"kf60": {Code: KeyF12, Mod: ModAlt},
+		"kf61": {Code: KeyF1, Mod: ModShift | ModAlt},
+		"kf62": {Code: KeyF2, Mod: ModShift | ModAlt},
+		"kf63": {Code: KeyF3, Mod: ModShift | ModAlt},
 	}
 
 	// Preserve F keys from F13 to F63 instead of using them for F-keys
 	// modifiers.
 	if flags&_FlagFKeys != 0 {
-		keys["kf13"] = Key{Type: KeyF13}
-		keys["kf14"] = Key{Type: KeyF14}
-		keys["kf15"] = Key{Type: KeyF15}
-		keys["kf16"] = Key{Type: KeyF16}
-		keys["kf17"] = Key{Type: KeyF17}
-		keys["kf18"] = Key{Type: KeyF18}
-		keys["kf19"] = Key{Type: KeyF19}
-		keys["kf20"] = Key{Type: KeyF20}
-		keys["kf21"] = Key{Type: KeyF21}
-		keys["kf22"] = Key{Type: KeyF22}
-		keys["kf23"] = Key{Type: KeyF23}
-		keys["kf24"] = Key{Type: KeyF24}
-		keys["kf25"] = Key{Type: KeyF25}
-		keys["kf26"] = Key{Type: KeyF26}
-		keys["kf27"] = Key{Type: KeyF27}
-		keys["kf28"] = Key{Type: KeyF28}
-		keys["kf29"] = Key{Type: KeyF29}
-		keys["kf30"] = Key{Type: KeyF30}
-		keys["kf31"] = Key{Type: KeyF31}
-		keys["kf32"] = Key{Type: KeyF32}
-		keys["kf33"] = Key{Type: KeyF33}
-		keys["kf34"] = Key{Type: KeyF34}
-		keys["kf35"] = Key{Type: KeyF35}
-		keys["kf36"] = Key{Type: KeyF36}
-		keys["kf37"] = Key{Type: KeyF37}
-		keys["kf38"] = Key{Type: KeyF38}
-		keys["kf39"] = Key{Type: KeyF39}
-		keys["kf40"] = Key{Type: KeyF40}
-		keys["kf41"] = Key{Type: KeyF41}
-		keys["kf42"] = Key{Type: KeyF42}
-		keys["kf43"] = Key{Type: KeyF43}
-		keys["kf44"] = Key{Type: KeyF44}
-		keys["kf45"] = Key{Type: KeyF45}
-		keys["kf46"] = Key{Type: KeyF46}
-		keys["kf47"] = Key{Type: KeyF47}
-		keys["kf48"] = Key{Type: KeyF48}
-		keys["kf49"] = Key{Type: KeyF49}
-		keys["kf50"] = Key{Type: KeyF50}
-		keys["kf51"] = Key{Type: KeyF51}
-		keys["kf52"] = Key{Type: KeyF52}
-		keys["kf53"] = Key{Type: KeyF53}
-		keys["kf54"] = Key{Type: KeyF54}
-		keys["kf55"] = Key{Type: KeyF55}
-		keys["kf56"] = Key{Type: KeyF56}
-		keys["kf57"] = Key{Type: KeyF57}
-		keys["kf58"] = Key{Type: KeyF58}
-		keys["kf59"] = Key{Type: KeyF59}
-		keys["kf60"] = Key{Type: KeyF60}
-		keys["kf61"] = Key{Type: KeyF61}
-		keys["kf62"] = Key{Type: KeyF62}
-		keys["kf63"] = Key{Type: KeyF63}
+		keys["kf13"] = Key{Code: KeyF13}
+		keys["kf14"] = Key{Code: KeyF14}
+		keys["kf15"] = Key{Code: KeyF15}
+		keys["kf16"] = Key{Code: KeyF16}
+		keys["kf17"] = Key{Code: KeyF17}
+		keys["kf18"] = Key{Code: KeyF18}
+		keys["kf19"] = Key{Code: KeyF19}
+		keys["kf20"] = Key{Code: KeyF20}
+		keys["kf21"] = Key{Code: KeyF21}
+		keys["kf22"] = Key{Code: KeyF22}
+		keys["kf23"] = Key{Code: KeyF23}
+		keys["kf24"] = Key{Code: KeyF24}
+		keys["kf25"] = Key{Code: KeyF25}
+		keys["kf26"] = Key{Code: KeyF26}
+		keys["kf27"] = Key{Code: KeyF27}
+		keys["kf28"] = Key{Code: KeyF28}
+		keys["kf29"] = Key{Code: KeyF29}
+		keys["kf30"] = Key{Code: KeyF30}
+		keys["kf31"] = Key{Code: KeyF31}
+		keys["kf32"] = Key{Code: KeyF32}
+		keys["kf33"] = Key{Code: KeyF33}
+		keys["kf34"] = Key{Code: KeyF34}
+		keys["kf35"] = Key{Code: KeyF35}
+		keys["kf36"] = Key{Code: KeyF36}
+		keys["kf37"] = Key{Code: KeyF37}
+		keys["kf38"] = Key{Code: KeyF38}
+		keys["kf39"] = Key{Code: KeyF39}
+		keys["kf40"] = Key{Code: KeyF40}
+		keys["kf41"] = Key{Code: KeyF41}
+		keys["kf42"] = Key{Code: KeyF42}
+		keys["kf43"] = Key{Code: KeyF43}
+		keys["kf44"] = Key{Code: KeyF44}
+		keys["kf45"] = Key{Code: KeyF45}
+		keys["kf46"] = Key{Code: KeyF46}
+		keys["kf47"] = Key{Code: KeyF47}
+		keys["kf48"] = Key{Code: KeyF48}
+		keys["kf49"] = Key{Code: KeyF49}
+		keys["kf50"] = Key{Code: KeyF50}
+		keys["kf51"] = Key{Code: KeyF51}
+		keys["kf52"] = Key{Code: KeyF52}
+		keys["kf53"] = Key{Code: KeyF53}
+		keys["kf54"] = Key{Code: KeyF54}
+		keys["kf55"] = Key{Code: KeyF55}
+		keys["kf56"] = Key{Code: KeyF56}
+		keys["kf57"] = Key{Code: KeyF57}
+		keys["kf58"] = Key{Code: KeyF58}
+		keys["kf59"] = Key{Code: KeyF59}
+		keys["kf60"] = Key{Code: KeyF60}
+		keys["kf61"] = Key{Code: KeyF61}
+		keys["kf62"] = Key{Code: KeyF62}
+		keys["kf63"] = Key{Code: KeyF63}
 	}
 
 	return keys
diff --git a/tty.go b/tty.go
index eb3f305..0dfb9f0 100644
--- a/tty.go
+++ b/tty.go
@@ -131,103 +131,6 @@ func readInputs(ctx context.Context, msgs chan<- Msg, reader *driver) error {
 		for _, msg := range events {
 			incomingMsgs := []Msg{msg}
 
-			// We need to translate new e types to deprecated ones to keep
-			// compatibility.
-			switch e := msg.(type) {
-			case PasteMsg:
-				var k KeyMsg
-				k.Paste = true
-				k.Runes = []rune(e)
-				incomingMsgs = append(incomingMsgs, k)
-			case KeyPressMsg:
-				k := KeyMsg{
-					Alt:   e.Mod.Contains(ModAlt),
-					Runes: e.Runes,
-					Type:  e.Type,
-				}
-
-				// Backwards compatibility for ctrl- and shift- keys
-				switch {
-				case e.Mod.Contains(ModCtrl | ModShift):
-					switch e.Type {
-					case KeyUp, KeyDown, KeyRight, KeyLeft:
-						k.Runes = nil
-						k.Type = KeyCtrlShiftUp - e.Type + KeyUp
-					case KeyHome, KeyEnd:
-						k.Runes = nil
-						k.Type = KeyCtrlShiftHome - e.Type + KeyHome
-					}
-				case e.Mod.Contains(ModCtrl):
-					switch e.Type {
-					case KeyRunes: // KeyRunes
-						switch r := e.Rune(); r {
-						case ' ':
-							k.Runes = nil
-							k.Type = KeyCtrlAt
-						case '[', '\\', ']', '^', '_':
-							k.Runes = nil
-							k.Type = KeyCtrlOpenBracket - KeyType(r) + '['
-						case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
-							'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
-							'u', 'v', 'w', 'x', 'y', 'z':
-							k.Runes = nil
-							k.Type = KeyCtrlA - KeyType(r) + 'a'
-						case '?':
-							k.Runes = nil
-							k.Type = KeyCtrlQuestionMark
-						}
-					case KeyPgUp, KeyPgDown, KeyHome, KeyEnd:
-						k.Runes = nil
-						k.Type = KeyCtrlPgUp - e.Type + KeyPgUp
-					case KeyUp, KeyDown, KeyRight, KeyLeft:
-						k.Runes = nil
-						k.Type = KeyCtrlUp - e.Type + KeyUp
-					}
-				case e.Mod.Contains(ModShift):
-					switch e.Type {
-					case KeyTab:
-						k.Runes = nil
-						k.Type = KeyShiftTab
-					case KeyUp, KeyDown, KeyRight, KeyLeft:
-						k.Runes = nil
-						k.Type = KeyShiftUp - e.Type + KeyUp
-						k.Runes = nil
-					case KeyHome, KeyEnd:
-						k.Runes = nil
-						k.Type = KeyShiftHome - e.Type + KeyHome
-					}
-				}
-
-				switch k.Type {
-				case KeyRunes: // KeyRunes
-					if len(k.Runes) > 0 {
-						incomingMsgs = append(incomingMsgs, k)
-					}
-				default:
-					incomingMsgs = append(incomingMsgs, k)
-				}
-			case MouseClickMsg:
-				m := toMouseMsg(Mouse(e))
-				m.Action = MouseActionPress
-				m.Type = e.Button
-				incomingMsgs = append(incomingMsgs, m)
-			case MouseReleaseMsg:
-				m := toMouseMsg(Mouse(e))
-				m.Action = MouseActionRelease
-				m.Type = MouseRelease
-				incomingMsgs = append(incomingMsgs, m)
-			case MouseWheelMsg:
-				m := toMouseMsg(Mouse(e))
-				m.Action = MouseActionPress
-				m.Type = e.Button
-				incomingMsgs = append(incomingMsgs, m)
-			case MouseMotionMsg:
-				m := toMouseMsg(Mouse(e))
-				m.Action = MouseActionMotion
-				m.Type = MouseMotion
-				incomingMsgs = append(incomingMsgs, m)
-			}
-
 			for _, m := range incomingMsgs {
 				select {
 				case msgs <- m:
diff --git a/win32input.go b/win32input.go
index 7b9f2c0..220a740 100644
--- a/win32input.go
+++ b/win32input.go
@@ -39,30 +39,30 @@ func parseWin32InputKeyEvent(vkc uint16, _ uint16, r rune, keyDown bool, cks uin
 		return nil
 	case _VK_MENU:
 		if cks&_LEFT_ALT_PRESSED != 0 {
-			key.Type = KeyLeftAlt
+			key.Code = KeyLeftAlt
 		} else if cks&_RIGHT_ALT_PRESSED != 0 {
-			key.Type = KeyRightAlt
+			key.Code = KeyRightAlt
 		} else if !keyDown {
 			return nil
 		}
 	case _VK_CONTROL:
 		if cks&_LEFT_CTRL_PRESSED != 0 {
-			key.Type = KeyLeftCtrl
+			key.Code = KeyLeftCtrl
 		} else if cks&_RIGHT_CTRL_PRESSED != 0 {
-			key.Type = KeyRightCtrl
+			key.Code = KeyRightCtrl
 		} else if !keyDown {
 			return nil
 		}
 	case _VK_CAPITAL:
-		key.Type = KeyCapsLock
+		key.Code = KeyCapsLock
 	default:
 		var ok bool
 		key, ok = vkKeyEvent[vkc]
 		if !ok {
 			if isCtrl {
-				key.Runes = []rune{vkCtrlRune(key, r, vkc)}
+				key.Text = string(vkCtrlRune(key, r, vkc))
 			} else {
-				key.Runes = []rune{r}
+				key.Text = string(r)
 			}
 		}
 	}
@@ -87,14 +87,14 @@ func parseWin32InputKeyEvent(vkc uint16, _ uint16, r rune, keyDown bool, cks uin
 	}
 
 	// Use the unshifted key
-	keyRune := key.Rune()
+	keyRune := key.Code
 	if cks&(_SHIFT_PRESSED^_CAPSLOCK_ON) != 0 {
 		if unicode.IsLower(keyRune) {
-			key.altRune = unicode.ToUpper(key.Rune())
+			key.ShiftedCode = unicode.ToUpper(key.Code)
 		}
 	} else {
 		if unicode.IsUpper(keyRune) {
-			key.altRune = unicode.ToLower(keyRune)
+			key.ShiftedCode = unicode.ToLower(keyRune)
 		}
 	}
 
@@ -117,75 +117,75 @@ func parseWin32InputKeyEvent(vkc uint16, _ uint16, r rune, keyDown bool, cks uin
 }
 
 var vkKeyEvent = map[uint16]Key{
-	_VK_RETURN:    {Type: KeyEnter},
-	_VK_BACK:      {Type: KeyBackspace},
-	_VK_TAB:       {Type: KeyTab},
-	_VK_ESCAPE:    {Type: KeyEscape},
-	_VK_SPACE:     {Type: KeySpace, Runes: []rune{' '}},
-	_VK_UP:        {Type: KeyUp},
-	_VK_DOWN:      {Type: KeyDown},
-	_VK_RIGHT:     {Type: KeyRight},
-	_VK_LEFT:      {Type: KeyLeft},
-	_VK_HOME:      {Type: KeyHome},
-	_VK_END:       {Type: KeyEnd},
-	_VK_PRIOR:     {Type: KeyPgUp},
-	_VK_NEXT:      {Type: KeyPgDown},
-	_VK_DELETE:    {Type: KeyDelete},
-	_VK_SELECT:    {Type: KeySelect},
-	_VK_SNAPSHOT:  {Type: KeyPrintScreen},
-	_VK_INSERT:    {Type: KeyInsert},
-	_VK_LWIN:      {Type: KeyLeftSuper},
-	_VK_RWIN:      {Type: KeyRightSuper},
-	_VK_APPS:      {Type: KeyMenu},
-	_VK_NUMPAD0:   {Type: KeyKp0},
-	_VK_NUMPAD1:   {Type: KeyKp1},
-	_VK_NUMPAD2:   {Type: KeyKp2},
-	_VK_NUMPAD3:   {Type: KeyKp3},
-	_VK_NUMPAD4:   {Type: KeyKp4},
-	_VK_NUMPAD5:   {Type: KeyKp5},
-	_VK_NUMPAD6:   {Type: KeyKp6},
-	_VK_NUMPAD7:   {Type: KeyKp7},
-	_VK_NUMPAD8:   {Type: KeyKp8},
-	_VK_NUMPAD9:   {Type: KeyKp9},
-	_VK_MULTIPLY:  {Type: KeyKpMultiply},
-	_VK_ADD:       {Type: KeyKpPlus},
-	_VK_SEPARATOR: {Type: KeyKpComma},
-	_VK_SUBTRACT:  {Type: KeyKpMinus},
-	_VK_DECIMAL:   {Type: KeyKpDecimal},
-	_VK_DIVIDE:    {Type: KeyKpDivide},
-	_VK_F1:        {Type: KeyF1},
-	_VK_F2:        {Type: KeyF2},
-	_VK_F3:        {Type: KeyF3},
-	_VK_F4:        {Type: KeyF4},
-	_VK_F5:        {Type: KeyF5},
-	_VK_F6:        {Type: KeyF6},
-	_VK_F7:        {Type: KeyF7},
-	_VK_F8:        {Type: KeyF8},
-	_VK_F9:        {Type: KeyF9},
-	_VK_F10:       {Type: KeyF10},
-	_VK_F11:       {Type: KeyF11},
-	_VK_F12:       {Type: KeyF12},
-	_VK_F13:       {Type: KeyF13},
-	_VK_F14:       {Type: KeyF14},
-	_VK_F15:       {Type: KeyF15},
-	_VK_F16:       {Type: KeyF16},
-	_VK_F17:       {Type: KeyF17},
-	_VK_F18:       {Type: KeyF18},
-	_VK_F19:       {Type: KeyF19},
-	_VK_F20:       {Type: KeyF20},
-	_VK_F21:       {Type: KeyF21},
-	_VK_F22:       {Type: KeyF22},
-	_VK_F23:       {Type: KeyF23},
-	_VK_F24:       {Type: KeyF24},
-	_VK_NUMLOCK:   {Type: KeyNumLock},
-	_VK_SCROLL:    {Type: KeyScrollLock},
-	_VK_LSHIFT:    {Type: KeyLeftShift},
-	_VK_RSHIFT:    {Type: KeyRightShift},
-	_VK_LCONTROL:  {Type: KeyLeftCtrl},
-	_VK_RCONTROL:  {Type: KeyRightCtrl},
-	_VK_LMENU:     {Type: KeyLeftAlt},
-	_VK_RMENU:     {Type: KeyRightAlt},
-	_VK_OEM_4:     {Runes: []rune{'['}},
+	_VK_RETURN:    {Code: KeyEnter},
+	_VK_BACK:      {Code: KeyBackspace},
+	_VK_TAB:       {Code: KeyTab},
+	_VK_ESCAPE:    {Code: KeyEscape},
+	_VK_SPACE:     {Code: KeySpace, Text: " "},
+	_VK_UP:        {Code: KeyUp},
+	_VK_DOWN:      {Code: KeyDown},
+	_VK_RIGHT:     {Code: KeyRight},
+	_VK_LEFT:      {Code: KeyLeft},
+	_VK_HOME:      {Code: KeyHome},
+	_VK_END:       {Code: KeyEnd},
+	_VK_PRIOR:     {Code: KeyPgUp},
+	_VK_NEXT:      {Code: KeyPgDown},
+	_VK_DELETE:    {Code: KeyDelete},
+	_VK_SELECT:    {Code: KeySelect},
+	_VK_SNAPSHOT:  {Code: KeyPrintScreen},
+	_VK_INSERT:    {Code: KeyInsert},
+	_VK_LWIN:      {Code: KeyLeftSuper},
+	_VK_RWIN:      {Code: KeyRightSuper},
+	_VK_APPS:      {Code: KeyMenu},
+	_VK_NUMPAD0:   {Code: KeyKp0},
+	_VK_NUMPAD1:   {Code: KeyKp1},
+	_VK_NUMPAD2:   {Code: KeyKp2},
+	_VK_NUMPAD3:   {Code: KeyKp3},
+	_VK_NUMPAD4:   {Code: KeyKp4},
+	_VK_NUMPAD5:   {Code: KeyKp5},
+	_VK_NUMPAD6:   {Code: KeyKp6},
+	_VK_NUMPAD7:   {Code: KeyKp7},
+	_VK_NUMPAD8:   {Code: KeyKp8},
+	_VK_NUMPAD9:   {Code: KeyKp9},
+	_VK_MULTIPLY:  {Code: KeyKpMultiply},
+	_VK_ADD:       {Code: KeyKpPlus},
+	_VK_SEPARATOR: {Code: KeyKpComma},
+	_VK_SUBTRACT:  {Code: KeyKpMinus},
+	_VK_DECIMAL:   {Code: KeyKpDecimal},
+	_VK_DIVIDE:    {Code: KeyKpDivide},
+	_VK_F1:        {Code: KeyF1},
+	_VK_F2:        {Code: KeyF2},
+	_VK_F3:        {Code: KeyF3},
+	_VK_F4:        {Code: KeyF4},
+	_VK_F5:        {Code: KeyF5},
+	_VK_F6:        {Code: KeyF6},
+	_VK_F7:        {Code: KeyF7},
+	_VK_F8:        {Code: KeyF8},
+	_VK_F9:        {Code: KeyF9},
+	_VK_F10:       {Code: KeyF10},
+	_VK_F11:       {Code: KeyF11},
+	_VK_F12:       {Code: KeyF12},
+	_VK_F13:       {Code: KeyF13},
+	_VK_F14:       {Code: KeyF14},
+	_VK_F15:       {Code: KeyF15},
+	_VK_F16:       {Code: KeyF16},
+	_VK_F17:       {Code: KeyF17},
+	_VK_F18:       {Code: KeyF18},
+	_VK_F19:       {Code: KeyF19},
+	_VK_F20:       {Code: KeyF20},
+	_VK_F21:       {Code: KeyF21},
+	_VK_F22:       {Code: KeyF22},
+	_VK_F23:       {Code: KeyF23},
+	_VK_F24:       {Code: KeyF24},
+	_VK_NUMLOCK:   {Code: KeyNumLock},
+	_VK_SCROLL:    {Code: KeyScrollLock},
+	_VK_LSHIFT:    {Code: KeyLeftShift},
+	_VK_RSHIFT:    {Code: KeyRightShift},
+	_VK_LCONTROL:  {Code: KeyLeftCtrl},
+	_VK_RCONTROL:  {Code: KeyRightCtrl},
+	_VK_LMENU:     {Code: KeyLeftAlt},
+	_VK_RMENU:     {Code: KeyRightAlt},
+	_VK_OEM_4:     {Text: "["},
 	// TODO: add more keys
 }
 
@@ -257,7 +257,7 @@ func vkCtrlRune(k Key, r rune, kc uint16) rune {
 	}
 
 	// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
-	if len(k.Runes) == 0 &&
+	if len(k.Text) == 0 &&
 		(kc >= 0x30 && kc <= 0x39) ||
 		(kc >= 0x41 && kc <= 0x5a) {
 		return rune(kc)
diff --git a/xterm.go b/xterm.go
index d5110df..298f494 100644
--- a/xterm.go
+++ b/xterm.go
@@ -34,22 +34,24 @@ func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg {
 
 	switch r {
 	case ansi.BS:
-		return KeyPressMsg{Mod: mod, Type: KeyBackspace}
+		return KeyPressMsg{Mod: mod, Code: KeyBackspace}
 	case ansi.HT:
-		return KeyPressMsg{Mod: mod, Type: KeyTab}
+		return KeyPressMsg{Mod: mod, Code: KeyTab}
 	case ansi.CR:
-		return KeyPressMsg{Mod: mod, Type: KeyEnter}
+		return KeyPressMsg{Mod: mod, Code: KeyEnter}
 	case ansi.ESC:
-		return KeyPressMsg{Mod: mod, Type: KeyEscape}
+		return KeyPressMsg{Mod: mod, Code: KeyEscape}
 	case ansi.DEL:
-		return KeyPressMsg{Mod: mod, Type: KeyBackspace}
+		return KeyPressMsg{Mod: mod, Code: KeyBackspace}
 	}
 
 	// CSI 27 ; <modifier> ; <code> ~ keys defined in XTerm modifyOtherKeys
-	return KeyPressMsg{
-		Mod:   mod,
-		Runes: []rune{r},
+	k := KeyPressMsg{Code: r, Mod: mod}
+	if k.Mod <= ModShift {
+		k.Text = string(r)
 	}
+
+	return k
 }
 
 // modifyOtherKeys is an internal message that queries the terminal for its


Test patch:
diff --git a/key_test.go b/key_test.go
index 0b3eae3..a0aea7d 100644
--- a/key_test.go
+++ b/key_test.go
@@ -24,36 +24,22 @@ var sequences = buildKeysTable(_FlagTerminfo, "dumb")
 
 func TestKeyString(t *testing.T) {
 	t.Run("alt+space", func(t *testing.T) {
-		k := KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModAlt}
+		k := KeyPressMsg{Code: KeySpace, Text: " ", Mod: ModAlt}
 		if got := k.String(); got != "alt+space" {
 			t.Fatalf(`expected a "alt+space ", got %q`, got)
 		}
 	})
 
 	t.Run("runes", func(t *testing.T) {
-		k := KeyPressMsg{Runes: []rune{'a'}}
+		k := KeyPressMsg{Code: 'a', Text: "a"}
 		if got := k.String(); got != "a" {
 			t.Fatalf(`expected an "a", got %q`, got)
 		}
 	})
 
 	t.Run("invalid", func(t *testing.T) {
-		k := KeyPressMsg{Type: 99999}
-		if got := k.String(); got != "" {
-			t.Fatalf(`expected a "unknown", got %q`, got)
-		}
-	})
-}
-
-func TestKeyTypeString(t *testing.T) {
-	t.Run("space", func(t *testing.T) {
-		if got := KeySpace.String(); got != "space" {
-			t.Fatalf(`expected a "space", got %q`, got)
-		}
-	})
-
-	t.Run("invalid", func(t *testing.T) {
-		if got := KeyType(99999).String(); got != "" {
+		k := KeyPressMsg{Code: 99999}
+		if got := k.String(); got != "𘚟" {
 			t.Fatalf(`expected a "unknown", got %q`, got)
 		}
 	})
@@ -96,14 +82,14 @@ func buildBaseSeqTests() []seqTest {
 		seqTest{
 			[]byte{' '},
 			[]Msg{
-				KeyPressMsg{Type: KeySpace, Runes: []rune{' '}},
+				KeyPressMsg{Code: KeySpace, Text: " "},
 			},
 		},
 		// An escape character with the alt modifier.
 		seqTest{
 			[]byte{'\x1b', ' '},
 			[]Msg{
-				KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModAlt},
+				KeyPressMsg{Code: KeySpace, Mod: ModAlt},
 			},
 		},
 	)
@@ -116,101 +102,101 @@ func TestParseSequence(t *testing.T) {
 		// Xterm modifyOtherKeys CSI 27 ; <modifier> ; <code> ~
 		seqTest{
 			[]byte("\x1b[27;3;20320~"),
-			[]Msg{KeyPressMsg{Runes: []rune{'你'}, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: '你', Mod: ModAlt}},
 		},
 		seqTest{
 			[]byte("\x1b[27;3;65~"),
-			[]Msg{KeyPressMsg{Runes: []rune{'A'}, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: 'A', Mod: ModAlt}},
 		},
 		seqTest{
 			[]byte("\x1b[27;3;8~"),
-			[]Msg{KeyPressMsg{Type: KeyBackspace, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: KeyBackspace, Mod: ModAlt}},
 		},
 		seqTest{
 			[]byte("\x1b[27;3;27~"),
-			[]Msg{KeyPressMsg{Type: KeyEscape, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: KeyEscape, Mod: ModAlt}},
 		},
 		seqTest{
 			[]byte("\x1b[27;3;127~"),
-			[]Msg{KeyPressMsg{Type: KeyBackspace, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: KeyBackspace, Mod: ModAlt}},
 		},
 
 		// Kitty keyboard / CSI u (fixterms)
 		seqTest{
 			[]byte("\x1b[1B"),
-			[]Msg{KeyPressMsg{Type: KeyDown}},
+			[]Msg{KeyPressMsg{Code: KeyDown}},
 		},
 		seqTest{
 			[]byte("\x1b[1;B"),
-			[]Msg{KeyPressMsg{Type: KeyDown}},
+			[]Msg{KeyPressMsg{Code: KeyDown}},
 		},
 		seqTest{
 			[]byte("\x1b[1;4B"),
-			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyDown}},
+			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Code: KeyDown}},
 		},
 		seqTest{
 			[]byte("\x1b[8~"),
-			[]Msg{KeyPressMsg{Type: KeyEnd}},
+			[]Msg{KeyPressMsg{Code: KeyEnd}},
 		},
 		seqTest{
 			[]byte("\x1b[8;~"),
-			[]Msg{KeyPressMsg{Type: KeyEnd}},
+			[]Msg{KeyPressMsg{Code: KeyEnd}},
 		},
 		seqTest{
 			[]byte("\x1b[8;10~"),
-			[]Msg{KeyPressMsg{Mod: ModShift | ModMeta, Type: KeyEnd}},
+			[]Msg{KeyPressMsg{Mod: ModShift | ModMeta, Code: KeyEnd}},
 		},
 		seqTest{
 			[]byte("\x1b[27;4u"),
-			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyEscape}},
+			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Code: KeyEscape}},
 		},
 		seqTest{
 			[]byte("\x1b[127;4u"),
-			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyBackspace}},
+			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Code: KeyBackspace}},
 		},
 		seqTest{
 			[]byte("\x1b[57358;4u"),
-			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Type: KeyCapsLock}},
+			[]Msg{KeyPressMsg{Mod: ModShift | ModAlt, Code: KeyCapsLock}},
 		},
 		seqTest{
 			[]byte("\x1b[9;2u"),
-			[]Msg{KeyPressMsg{Mod: ModShift, Type: KeyTab}},
+			[]Msg{KeyPressMsg{Mod: ModShift, Code: KeyTab}},
 		},
 		seqTest{
 			[]byte("\x1b[195;u"),
-			[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, Type: KeyRunes}},
+			[]Msg{KeyPressMsg{Text: "Ã", Code: 'Ã'}},
 		},
 		seqTest{
 			[]byte("\x1b[20320;2u"),
-			[]Msg{KeyPressMsg{Runes: []rune{'你'}, Mod: ModShift, Type: KeyRunes}},
+			[]Msg{KeyPressMsg{Text: "你", Mod: ModShift, Code: '你'}},
 		},
 		seqTest{
 			[]byte("\x1b[195;:1u"),
-			[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, Type: KeyRunes}},
+			[]Msg{KeyPressMsg{Text: "Ã", Code: 'Ã'}},
 		},
 		seqTest{
 			[]byte("\x1b[195;2:3u"),
-			[]Msg{KeyReleaseMsg{Runes: []rune{'Ã'}, Mod: ModShift}},
+			[]Msg{KeyReleaseMsg{Code: 'Ã', Text: "Ã", Mod: ModShift}},
 		},
 		seqTest{
 			[]byte("\x1b[195;2:2u"),
-			[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, IsRepeat: true, Mod: ModShift}},
+			[]Msg{KeyPressMsg{Code: 'Ã', Text: "Ã", IsRepeat: true, Mod: ModShift}},
 		},
 		seqTest{
 			[]byte("\x1b[195;2:1u"),
-			[]Msg{KeyPressMsg{Runes: []rune{'Ã'}, Mod: ModShift}},
+			[]Msg{KeyPressMsg{Code: 'Ã', Text: "Ã", Mod: ModShift}},
 		},
 		seqTest{
 			[]byte("\x1b[195;2:3u"),
-			[]Msg{KeyReleaseMsg{Runes: []rune{'Ã'}, Mod: ModShift}},
+			[]Msg{KeyReleaseMsg{Code: 'Ã', Text: "Ã", Mod: ModShift}},
 		},
 		seqTest{
 			[]byte("\x1b[97;2;65u"),
-			[]Msg{KeyPressMsg{Runes: []rune{'A'}, Mod: ModShift, altRune: 'a'}},
+			[]Msg{KeyPressMsg{Code: 'a', Text: "A", Mod: ModShift}},
 		},
 		seqTest{
 			[]byte("\x1b[97;;229u"),
-			[]Msg{KeyPressMsg{Runes: []rune{'å'}, altRune: 'a'}},
+			[]Msg{KeyPressMsg{Code: 'a', Text: "å"}},
 		},
 
 		// focus/blur
@@ -244,72 +230,72 @@ func TestParseSequence(t *testing.T) {
 		seqTest{
 			[]byte{'a'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}},
+				KeyPressMsg{Code: 'a', Text: "a"},
 			},
 		},
 		seqTest{
 			[]byte{'\x1b', 'a'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt},
+				KeyPressMsg{Code: 'a', Mod: ModAlt},
 			},
 		},
 		seqTest{
 			[]byte{'a', 'a', 'a'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}},
-				KeyPressMsg{Runes: []rune{'a'}},
-				KeyPressMsg{Runes: []rune{'a'}},
+				KeyPressMsg{Code: 'a', Text: "a"},
+				KeyPressMsg{Code: 'a', Text: "a"},
+				KeyPressMsg{Code: 'a', Text: "a"},
 			},
 		},
 		// Multi-byte rune.
 		seqTest{
 			[]byte("☃"),
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'☃'}},
+				KeyPressMsg{Code: '☃', Text: "☃"},
 			},
 		},
 		seqTest{
 			[]byte("\x1b☃"),
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'☃'}, Mod: ModAlt},
+				KeyPressMsg{Code: '☃', Mod: ModAlt},
 			},
 		},
-		// Standalone control chacters.
+		// Standalone control characters.
 		seqTest{
 			[]byte{'\x1b'},
 			[]Msg{
-				KeyPressMsg{Type: KeyEscape},
+				KeyPressMsg{Code: KeyEscape},
 			},
 		},
 		seqTest{
 			[]byte{ansi.SOH},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl},
+				KeyPressMsg{Code: 'a', Mod: ModCtrl},
 			},
 		},
 		seqTest{
 			[]byte{'\x1b', ansi.SOH},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl | ModAlt},
+				KeyPressMsg{Code: 'a', Mod: ModCtrl | ModAlt},
 			},
 		},
 		seqTest{
 			[]byte{ansi.NUL},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl},
+				KeyPressMsg{Code: KeySpace, Mod: ModCtrl},
 			},
 		},
 		seqTest{
 			[]byte{'\x1b', ansi.NUL},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{' '}, Type: KeySpace, Mod: ModCtrl | ModAlt},
+				KeyPressMsg{Code: KeySpace, Mod: ModCtrl | ModAlt},
 			},
 		},
 		// C1 control characters.
 		seqTest{
 			[]byte{'\x80'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{0x80 - '@'}, Mod: ModCtrl | ModAlt},
+				KeyPressMsg{Code: rune(0x80 - '@'), Mod: ModCtrl | ModAlt},
 			},
 		},
 	)
@@ -340,7 +326,7 @@ func TestParseSequence(t *testing.T) {
 				buf = buf[width:]
 			}
 			if !reflect.DeepEqual(tc.msgs, events) {
-				t.Errorf("\nexpected event:\n    %#v\ngot:\n    %#v", tc.msgs, events)
+				t.Errorf("\nexpected event for %q:\n    %#v\ngot:\n    %#v", tc.seq, tc.msgs, events)
 			}
 		})
 	}
@@ -349,7 +335,7 @@ func TestParseSequence(t *testing.T) {
 func TestReadLongInput(t *testing.T) {
 	expect := make([]Msg, 1000)
 	for i := 0; i < 1000; i++ {
-		expect[i] = KeyPressMsg{Runes: []rune{'a'}}
+		expect[i] = KeyPressMsg{Code: 'a', Text: "a"}
 	}
 	input := strings.Repeat("a", 1000)
 	drv, err := newDriver(strings.NewReader(input), "dumb", 0)
@@ -385,70 +371,70 @@ func TestReadInput(t *testing.T) {
 			"a",
 			[]byte{'a'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}},
+				KeyPressMsg{Code: 'a', Text: "a"},
 			},
 		},
 		{
 			"space",
 			[]byte{' '},
 			[]Msg{
-				KeyPressMsg{Type: KeySpace, Runes: []rune{' '}},
+				KeyPressMsg{Code: KeySpace, Text: " "},
 			},
 		},
 		{
 			"a alt+a",
 			[]byte{'a', '\x1b', 'a'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}},
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt},
+				KeyPressMsg{Code: 'a', Text: "a"},
+				KeyPressMsg{Code: 'a', Mod: ModAlt},
 			},
 		},
 		{
 			"a alt+a a",
 			[]byte{'a', '\x1b', 'a', 'a'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}},
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModAlt},
-				KeyPressMsg{Runes: []rune{'a'}},
+				KeyPressMsg{Code: 'a', Text: "a"},
+				KeyPressMsg{Code: 'a', Mod: ModAlt},
+				KeyPressMsg{Code: 'a', Text: "a"},
 			},
 		},
 		{
 			"ctrl+a",
 			[]byte{byte(ansi.SOH)},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl},
+				KeyPressMsg{Code: 'a', Mod: ModCtrl},
 			},
 		},
 		{
 			"ctrl+a ctrl+b",
 			[]byte{byte(ansi.SOH), byte(ansi.STX)},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl},
-				KeyPressMsg{Runes: []rune{'b'}, Mod: ModCtrl},
+				KeyPressMsg{Code: 'a', Mod: ModCtrl},
+				KeyPressMsg{Code: 'b', Mod: ModCtrl},
 			},
 		},
 		{
 			"alt+a",
 			[]byte{byte(0x1b), 'a'},
 			[]Msg{
-				KeyPressMsg{Mod: ModAlt, Runes: []rune{'a'}},
+				KeyPressMsg{Code: 'a', Mod: ModAlt},
 			},
 		},
 		{
 			"a b c d",
 			[]byte{'a', 'b', 'c', 'd'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}},
-				KeyPressMsg{Runes: []rune{'b'}},
-				KeyPressMsg{Runes: []rune{'c'}},
-				KeyPressMsg{Runes: []rune{'d'}},
+				KeyPressMsg{Code: 'a', Text: "a"},
+				KeyPressMsg{Code: 'b', Text: "b"},
+				KeyPressMsg{Code: 'c', Text: "c"},
+				KeyPressMsg{Code: 'd', Text: "d"},
 			},
 		},
 		{
 			"up",
 			[]byte("\x1b[A"),
 			[]Msg{
-				KeyPressMsg{Type: KeyUp},
+				KeyPressMsg{Code: KeyUp},
 			},
 		},
 		{
@@ -473,33 +459,33 @@ func TestReadInput(t *testing.T) {
 			"shift+tab",
 			[]byte{'\x1b', '[', 'Z'},
 			[]Msg{
-				KeyPressMsg{Type: KeyTab, Mod: ModShift},
+				KeyPressMsg{Code: KeyTab, Mod: ModShift},
 			},
 		},
 		{
 			"enter",
 			[]byte{'\r'},
-			[]Msg{KeyPressMsg{Type: KeyEnter}},
+			[]Msg{KeyPressMsg{Code: KeyEnter}},
 		},
 		{
 			"alt+enter",
 			[]byte{'\x1b', '\r'},
 			[]Msg{
-				KeyPressMsg{Type: KeyEnter, Mod: ModAlt},
+				KeyPressMsg{Code: KeyEnter, Mod: ModAlt},
 			},
 		},
 		{
 			"insert",
 			[]byte{'\x1b', '[', '2', '~'},
 			[]Msg{
-				KeyPressMsg{Type: KeyInsert},
+				KeyPressMsg{Code: KeyInsert},
 			},
 		},
 		{
 			"ctrl+alt+a",
 			[]byte{'\x1b', byte(ansi.SOH)},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}, Mod: ModCtrl | ModAlt},
+				KeyPressMsg{Code: 'a', Mod: ModCtrl | ModAlt},
 			},
 		},
 		{
@@ -511,52 +497,52 @@ func TestReadInput(t *testing.T) {
 		{
 			"up",
 			[]byte{'\x1b', 'O', 'A'},
-			[]Msg{KeyPressMsg{Type: KeyUp}},
+			[]Msg{KeyPressMsg{Code: KeyUp}},
 		},
 		{
 			"down",
 			[]byte{'\x1b', 'O', 'B'},
-			[]Msg{KeyPressMsg{Type: KeyDown}},
+			[]Msg{KeyPressMsg{Code: KeyDown}},
 		},
 		{
 			"right",
 			[]byte{'\x1b', 'O', 'C'},
-			[]Msg{KeyPressMsg{Type: KeyRight}},
+			[]Msg{KeyPressMsg{Code: KeyRight}},
 		},
 		{
 			"left",
 			[]byte{'\x1b', 'O', 'D'},
-			[]Msg{KeyPressMsg{Type: KeyLeft}},
+			[]Msg{KeyPressMsg{Code: KeyLeft}},
 		},
 		{
 			"alt+enter",
 			[]byte{'\x1b', '\x0d'},
-			[]Msg{KeyPressMsg{Type: KeyEnter, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: KeyEnter, Mod: ModAlt}},
 		},
 		{
 			"alt+backspace",
 			[]byte{'\x1b', '\x7f'},
-			[]Msg{KeyPressMsg{Type: KeyBackspace, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: KeyBackspace, Mod: ModAlt}},
 		},
 		{
 			"ctrl+space",
 			[]byte{'\x00'},
-			[]Msg{KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModCtrl}},
+			[]Msg{KeyPressMsg{Code: KeySpace, Mod: ModCtrl}},
 		},
 		{
 			"ctrl+alt+space",
 			[]byte{'\x1b', '\x00'},
-			[]Msg{KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModCtrl | ModAlt}},
+			[]Msg{KeyPressMsg{Code: KeySpace, Mod: ModCtrl | ModAlt}},
 		},
 		{
 			"esc",
 			[]byte{'\x1b'},
-			[]Msg{KeyPressMsg{Type: KeyEscape}},
+			[]Msg{KeyPressMsg{Code: KeyEscape}},
 		},
 		{
 			"alt+esc",
 			[]byte{'\x1b', '\x1b'},
-			[]Msg{KeyPressMsg{Type: KeyEscape, Mod: ModAlt}},
+			[]Msg{KeyPressMsg{Code: KeyEscape, Mod: ModAlt}},
 		},
 		{
 			"a b o",
@@ -570,7 +556,7 @@ func TestReadInput(t *testing.T) {
 				PasteStartMsg{},
 				PasteMsg("a b"),
 				PasteEndMsg{},
-				KeyPressMsg{Runes: []rune{'o'}},
+				KeyPressMsg{Code: 'o', Text: "o"},
 			},
 		},
 		{
@@ -597,10 +583,10 @@ func TestReadInput(t *testing.T) {
 			"a ?0xfe?   b",
 			[]byte{'a', '\xfe', ' ', 'b'},
 			[]Msg{
-				KeyPressMsg{Runes: []rune{'a'}},
+				KeyPressMsg{Code: 'a', Text: "a"},
 				UnknownMsg(rune(0xfe)),
-				KeyPressMsg{Type: KeySpace, Runes: []rune{' '}},
-				KeyPressMsg{Runes: []rune{'b'}},
+				KeyPressMsg{Code: KeySpace, Text: " "},
+				KeyPressMsg{Code: 'b', Text: "b"},
 			},
 		},
 	}
diff --git a/parse_test.go b/parse_test.go
index 6e8e0b6..e74e530 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -9,14 +9,14 @@ import (
 func TestParseSequence_Events(t *testing.T) {
 	input := []byte("\x1b\x1b[Ztest\x00\x1b]10;rgb:1234/1234/1234\x07\x1b[27;2;27~\x1b[?1049;2$y")
 	want := []Msg{
-		KeyPressMsg{Type: KeyTab, Mod: ModShift | ModAlt},
-		KeyPressMsg{Type: KeyRunes, Runes: []rune{'t'}},
-		KeyPressMsg{Type: KeyRunes, Runes: []rune{'e'}},
-		KeyPressMsg{Type: KeyRunes, Runes: []rune{'s'}},
-		KeyPressMsg{Type: KeyRunes, Runes: []rune{'t'}},
-		KeyPressMsg{Type: KeySpace, Runes: []rune{' '}, Mod: ModCtrl},
+		KeyPressMsg{Code: KeyTab, Mod: ModShift | ModAlt},
+		KeyPressMsg{Code: 't', Text: "t"},
+		KeyPressMsg{Code: 'e', Text: "e"},
+		KeyPressMsg{Code: 's', Text: "s"},
+		KeyPressMsg{Code: 't', Text: "t"},
+		KeyPressMsg{Code: KeySpace, Mod: ModCtrl},
 		ForegroundColorMsg{color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}},
-		KeyPressMsg{Type: KeyEscape, Mod: ModShift},
+		KeyPressMsg{Code: KeyEscape, Mod: ModShift},
 		ReportModeMsg{Mode: 1049, Value: 2},
 	}
 	for i := 0; len(input) != 0; i++ {
@@ -25,7 +25,7 @@ func TestParseSequence_Events(t *testing.T) {
 		}
 		n, got := parseSequence(input)
 		if !reflect.DeepEqual(got, want[i]) {
-			t.Errorf("got %v (%T), want %v (%T)", got, got, want[i], want[i])
+			t.Errorf("got %#v (%T), want %#v (%T)", got, got, want[i], want[i])
 		}
 		input = input[n:]
 	}
diff --git a/tea_test.go b/tea_test.go
index 359465b..4c78e7e 100644
--- a/tea_test.go
+++ b/tea_test.go
@@ -30,7 +30,7 @@ func (m *testModel) Update(msg Msg) (Model, Cmd) {
 			m.counter.Store(i.(int) + 1)
 		}
 
-	case KeyMsg:
+	case KeyPressMsg:
 		return m, Quit
 	}