Overview
Bubble Tea provides robust keyboard input handling through the KeyPressMsg and KeyReleaseMsg message types. Input handling is a core part of The Elm Architecture pattern, where all user interactions flow through the Update method as messages.
Key Messages
KeyPressMsg
The KeyPressMsg is sent when a key is pressed. It wraps a Key struct containing detailed information about the key event:
type KeyPressMsg Key
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyPressMsg :
switch msg . String () {
case "enter" :
fmt . Println ( "you pressed enter!" )
case "a" :
fmt . Println ( "you pressed a!" )
}
}
return m , nil
}
Key Structure
The Key type provides detailed information about keyboard events:
type Key struct {
// Text contains the actual characters received
Text string
// Mod represents modifier keys (Ctrl, Alt, Shift, etc.)
Mod KeyMod
// Code represents the key pressed (KeyTab, KeyEnter, etc.)
Code rune
// ShiftedCode is the shifted key (e.g., 'A' when shift+a is pressed)
ShiftedCode rune
// BaseCode is the key on standard PC-101 layout
BaseCode rune
// IsRepeat indicates if the key is being held down
IsRepeat bool
}
String Matching (Simple)
The most common pattern is to match against the string representation of the key:
examples/simple/main.go:45-54
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyPressMsg :
switch msg . String () {
case "ctrl+c" , "q" :
return m , tea . Quit
case "ctrl+z" :
return m , tea . Suspend
}
}
return m , nil
}
Key Code Matching (Type-Safe)
For more robust matching, use the key code:
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyMsg :
key := msg . Key ()
switch key . Code {
case tea . KeyEnter :
return m , submitForm ()
case tea . KeyEsc :
return m , tea . Quit
default :
if key . Text == "a" && key . Mod == tea . ModCtrl {
return m , selectAll ()
}
}
}
return m , nil
}
Navigation Keys
Handle arrow keys and vim-style navigation:
switch msg . String () {
case "ctrl+c" , "q" :
return m , tea . Quit
// The "up" and "k" keys move the cursor up
case "up" , "k" :
if m . cursor > 0 {
m . cursor --
}
// The "down" and "j" keys move the cursor down
case "down" , "j" :
if m . cursor < len ( m . choices ) - 1 {
m . cursor ++
}
// The "enter" key and space bar toggle selection
case "enter" , " " :
_ , ok := m . selected [ m . cursor ]
if ok {
delete ( m . selected , m . cursor )
} else {
m . selected [ m . cursor ] = struct {}{}
}
}
Special Keys
Bubble Tea supports a comprehensive set of special keys:
Navigation Keys
KeyUp
KeyDown
KeyRight
KeyLeft
KeyHome
KeyEnd
KeyPgUp
KeyPgDown
KeyInsert
KeyDelete
Function Keys
Modifier Keys
KeyLeftShift
KeyLeftAlt
KeyLeftCtrl
KeyLeftSuper
KeyRightShift
KeyRightAlt
KeyRightCtrl
KeyRightSuper
Control Keys
KeyBackspace
KeyTab
KeyEnter
KeyReturn
KeyEscape
KeySpace
Keyboard Enhancements
Modern terminals support enhanced keyboard protocols that provide additional information:
type KeyboardEnhancementsMsg struct {
// Flags is a bitmask of enabled keyboard enhancement features
Flags int
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyboardEnhancementsMsg :
if msg . SupportsEventTypes () {
// Terminal supports press/release/repeat events
}
if msg . SupportsKeyDisambiguation () {
// Terminal can distinguish modifier keys
}
}
return m , nil
}
Key Release Events
Handle key release events with KeyReleaseMsg:
type KeyReleaseMsg Key
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyPressMsg :
// Key was pressed
m . keyPressed = true
case tea . KeyReleaseMsg :
// Key was released
m . keyPressed = false
}
return m , nil
}
Key release events require terminal support for keyboard enhancements (Kitty keyboard protocol or Windows Console API).
Catching All Keys
Use the KeyMsg interface to catch both press and release events:
switch msg := msg .( type ) {
case tea . KeyMsg :
// Catches both KeyPressMsg and KeyReleaseMsg
key := msg . Key ()
fmt . Printf ( "Key event: %s \n " , key . String ())
}
Modifier Detection
Detect modifier keys in combination:
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyPressMsg :
key := msg . Key ()
// Check for Ctrl+C
if key . Text == "c" && key . Mod & tea . ModCtrl != 0 {
return m , tea . Quit
}
// Check for Shift+Tab
if key . Code == tea . KeyTab && key . Mod & tea . ModShift != 0 {
return m , focusPrevious ()
}
}
return m , nil
}
Best Practices
Use String Matching for Simplicity
For most cases, msg.String() provides a clean, readable API: case tea . KeyPressMsg :
switch msg . String () {
case "q" , "ctrl+c" :
return m , tea . Quit
}
Use Key Codes for Robustness
When you need precise control or locale-independence, match on key codes: case tea . KeyPressMsg :
key := msg . Key ()
if key . Code == tea . KeyEnter {
return m , submit ()
}
Provide Multiple Key Bindings
Support both arrow keys and vim-style navigation: case "up" , "k" :
m . cursor --
case "down" , "j" :
m . cursor ++
Ensure users can always exit your application: examples/simple/main.go:48-50
switch msg . String () {
case "ctrl+c" , "q" :
return m , tea . Quit
}
Common Patterns
type model struct {
input string
cursor int
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyPressMsg :
switch msg . String () {
case "backspace" :
if len ( m . input ) > 0 {
m . input = m . input [: len ( m . input ) - 1 ]
}
case "enter" :
return m , submitForm ( m . input )
default :
if msg . Key (). Text != "" {
m . input += msg . Key (). Text
}
}
}
return m , nil
}
List Navigation
case "up" , "k" :
if m . cursor > 0 {
m . cursor --
}
case "down" , "j" :
if m . cursor < len ( m . choices ) - 1 {
m . cursor ++
}
Disable input processing entirely by passing nil to WithInput:
p := tea . NewProgram ( model , tea . WithInput ( nil ))