Who’s ready for Bubble Tea v2 Alpha?
We’re so excited for you to try Bubble Tea v2! Keep in mind that this is an alpha release and things may change.
Note
We don't take API changes lightly and strive to make the upgrade process as simple as possible. We believe the changes bring necessary improvements as well as pave the way for the future. If something feels way off, let us know.
Here are the the big things to look out for in v2:
Key handling is way better now
We added support for way, way, way better key handling in newer terminals. For example, now you can map to things like shift+enter and super+space, as long as the terminal supports progressive keyboard enhancement. You can also detect key releases too (we're looking at you, terminal game developers).
Init looks more like Update
We changed Init()
’s signature to match Update()
to make programs easier to follow and to make swapping models easier.
Upgrading
Upgrading to Bubble Tea v2 is easy. Just update your imports and follow the
instructions below.
go get github.com/charmbracelet/bubbletea/v2@v2.0.0-alpha.1
# If you're using Bubbles you'll also want to update that.
# Note that Huh isn't supported in v2 yet.
go get github.com/charmbracelet/bubbles/v2@v2.0.0-alpha.1
Init()
signature
Change your Model
's Init()
signature to return a tea.Model
and a tea.Cmd
:
// Before:
func (m Model) Init() Cmd
// do your thing
return cmd
}
// After:
func (m Model) Init() (Model, Cmd)
// oooh, I can return a new model now
return m, cmd
Why the change?
Now you can use Init
to initialize the Model
with some state. By following this pattern Bubble Tea programs become easier to follow. Of course, you can still initialize the model before passing it to Program
if you want, too.
It also becomes more natural to switch models in Update
, which is a very useful
way to manage state. Consider the following:
func (m ListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case SwitchToEditorMsg:
// The user wants to edit an item. Switch to the editor model.
return EditorModel{}.Init()
}
// ...
}
While the change to Init()
may seem big, in practice we've found that it doesn't take much to update existing programs.
Keyboard enhancements (optional)
With Bubble Tea v2, you can get more out of your terminal. Progressive keyboard enhancements, allow you to use more key combinations. Just use the tea.WithKeyboardEnhancements
option when creating a new program to get all the keys, in supported terminals only.
You can enable enhanced keyboard support by passing the tea.WithKeyboardEnhancements
option to tea.NewProgram
or by using the tea.EnableKeyboardEnhancements
command.
p := tea.NewProgram(model, tea.WithKeyboardEnhancements())
// Or in your `Init` function:
func (m Model) Init() (tea.Model, tea.Cmd) {
return m, tea.EnableKeyboardEnhancements()
}
By default, release events aren't included, but you can opt-into them with the tea.WithKeyReleases
flag:
tea.WithKeyboardEnhancements(tea.WithKeyReleases) // ProgramOption
tea.EnableKeyboardEnhancements(tea.WithKeyReleases) // Cmd
You can detect if a terminal supports keyboard enhancements by listening for tea.KeyboardEnhancementsMsg
after enabling progressive enhancements.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyboardEnhancementsMsg:
// More keys, please!
}
}
Note
This feature is enabled by default on Windows due to the fact that we use the Windows Console API to support other Windows features.
Which terminals support progressive enhancement?
Key messages
Key messages are now split into tea.KeyPressMsg
and tea.KeyReleaseMsg
. Use
tea.KeyMsg
to match against both.
// Before:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// I'm a key press message
}
}
// After:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// I'm a key press or release message
switch key := msg.(type) {
case tea.KeyPressMsg:
// I'm a key press message
return m, tea.Printf("You pressed %s", key)
case tea.KeyReleaseMsg:
// I'm a key release message ;)
return m, tea.Printf("Key released: %s", key)
}
}
}
We no longer have key.Type
and key.Runes
fields. These have been replaced
with key.Code
and key.Text
respectively. A key code is just a rune
that
represents the key message. It can be a special key like tea.KeyEnter
,
tea.KeyTab
, tea.KeyEscape
, or a printable rune.
// Before:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter:
// Enter key
case tea.KeyRune:
// A printable rune
switch msg.Runes[0] {
case 'a':
// The letter 'a'
}
}
}
}
// After:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Code {
case tea.KeyEnter:
// Enter key
case 'a':
// The letter 'a'
}
}
}
The new key.Text
field signifies a printable key event. If the key event has
a non-empty Text
field, it means the key event is a printable key event. In
that case, key.Code
is always going to be the first rune of key.Text
.
// Before:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Type == tea.KeyRune {
// A printable rune
switch string(msg.Runes) {
case "😃":
// Smiley face
}
}
}
}
// After:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// A printable rune
switch msg.Text {
case "😃":
// Smiley face
}
}
}
Instead of matching against msg.Type == tea.KeyCtrl...
keys, key modifiers
are now part of the key event itself as key.Mod
. Shifted keys now have their
own key code in key.ShiftedCode
. Typing shift+b will produce
key.Code == 'b'
, key.ShiftedCode == 'B'
, key.Text == "B"
, and key.Mod == tea.ModShift
.
// Before:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC:
// ctrl+c
case tea.KeyCtrlA:
// ctrl+a
default: // 🙄
// can i catch all ctrl+<key> combinations?
if msg.Alt {
// but i want to catch ctrl+alt+<key> combinations too 🤔
return m, tea.Printf("idk what to do with '%s'", msg)
}
switch msg.Runes[0] {
case 'B': // shift+a
// ugh, i forgot caps lock was on
return m, tea.Printf("You typed '%s'", msg.Runes)
}
}
}
}
// After:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Mod {
case tea.ModCtrl: // We're only interested in ctrl+<key>
switch msg.Code {
case 'c':
// ctrl+c
case 'a':
// ctrl+a
default:
return m, tea.Printf("That's an interesting key combo! %s", msg)
}
default:
if msg.Mod.Contains(tea.ModCtrl|tea.ModAlt) {
return m, tea.Printf("I'm a '%s' 😎!", msg)
}
if len(msg.Text) > 0 {
switch msg.String() {
case "shift+b":
// It doesn't matter if caps lock is on or off, we got your back!
return m, tea.Printf("You typed '%s'", msg.Text) // "B"
}
}
}
}
}
The easiest way to match against key events is to use msg.String()
:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "space":
// Space bar returns "space" now :D
return m, tea.Println("You pressed the space bar!")
case "ctrl+c":
// Copy to the clipboard.
return m, tea.SetClipboard("Howdy")
case "ctrl+v":
// Read the clipboard (not supported by all terminals).
return m, tea.ReadClipboard()
case "alt+enter":
// Fullscreen mode, anyone?
case "shift+x":
// Just an upper case 'x'
return m, tea.Println("You typed %q", msg.Text) // "X"
case "shift+enter":
// Awesome, right?
case "ctrl+alt+super+enter":
// Yes, you can do that now!
}
}
}
Oh, and we finally changed space bar to return "space"
instead of " "
. In
this case, key.Code == ' '
and key.Text == " "
.
Paste messages
Bracketed-paste has its own message type now. Use tea.PasteMsg
to match
against paste events.
// Before:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Paste {
// That's weird, I'm a paste message
m.text += string(msg.Runes)
}
}
}
// After:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// I'm a key message
case tea.PasteMsg:
// Here comes a paste!
m.text += string(msg)
}
}
Mouse messages
We've also improved the mouse API, which is a breaking change. Use
tea.MouseMsg
to match against different types of mouse events. Mouse messages
are split into tea.MouseClickMsg
, tea.MouseReleaseMsg
, tea.MouseWheelMsg
,
and tea.MouseMotionMsg
.
// Before:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
switch msg.Button {
case tea.MouseButtonWheelUp:
// Whee!
case tea.MouseButtonLeft:
// Clickety click
}
}
}
// After:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
mouse := msg.Mouse()
switch msg := msg.(type) {
case tea.MouseClickMsg:
switch msg.Button {
case tea.MouseLeft:
// Clickety click
}
case tea.MouseReleaseMsg:
// Release the mouse!
case tea.MouseWheelMsg:
// Scroll, scroll, scrollllll
case tea.MouseMotionMsg:
// Where did the mouse go?
}
return m, tea.Printf("%T: (X: %d, Y: %d) %s", msg, mouse.X, mouse.Y, mouse)
}
}
Other new things
Native clipboard support
Bubble Tea now supports native clipboard operations, also known as OSC52.
This means you can even copy and paste over SSH!
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
// Copy to the clipboard.
return m, tea.SetClipboard("Howdy")
case "ctrl+v":
// Read the clipboard (not supported by all terminals).
return m, tea.ReadClipboard()
}
case tea.ClipboardMsg:
// Here's the system clipboard contents.
case tea.PrimaryClipboardMsg:
// Only on X11 and Wayland ;)
}
}
X11 and Wayland users can also use tea.SetPrimaryClipboard
to set the primary
clipboard. Note that this is a very niche sort of thing and may or may not
work on macOS, Windows, and other platforms without the notion of more than one
clipboard.
Bracketed-paste messages
Bubble Tea v1 supports bracketed-paste mode by default. It sends paste events
as a tea.KeyMsg
with msg.Paste
flag set to true
, which to be honest, was
pretty confusing.
In Bubble Tea v2, paste events are sent as their own tea.PasteMsg
message. If
you don't care about the content and want to listen to paste start/end events,
you can use tea.PasteStartMsg
and tea.PasteEndMsg
.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.PasteStartMsg:
// The user started pasting.
case tea.PasteEndMsg:
// The user stopped pasting.
case tea.PasteMsg:
// Here comes a paste!
m.text += string(msg)
}
}
Detecting terminal colors
You can now read and set the terminal's foreground, background, and cursor
colors. This is useful for creating advanced terminal applications.
To change the terminal's colors, use tea.SetForegroundColor
, tea.SetBackgroundColor
,
and tea.SetCursorColor
.
// Read the terminal's colors when the program starts.
func (m Model) Init() (tea.Model, tea.Cmd) {
return m, tea.Batch(
tea.ForegroundColor,
tea.BackgroundColor,
tea.CursorColor,
)
}
// Change the terminal's colors when the user presses enter and reset on exit.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.BackgroundColorMsg:
return m, tea.Printf("Background color: %s\n", msg)
case tea.ForegroundColorMsg:
return m, tea.Printf("Foreground color: %s\n", msg)
case tea.CursorColorMsg:
return m, tea.Printf("Cursor color: %s\n", msg)
case tea.KeyPressMsg:
switch msg.String() {
case "enter":
return m, tea.Batch(
tea.SetForegroundColor(ansi.Red),
tea.SetBackgroundColor(ansi.Green),
tea.SetCursorColor(ansi.Blue),
)
case "esc":
return m, tea.Quit
}
}
return m, nil
}
Mode 2027 (grapheme clustering)
Most terminals use wcwidth
to calculate the width of characters displayed on
the screen. This becomes a problem when you have characters that are composed
of multiple codepoints, like emojis or characters with diacritics. Mode 2027
tries to solve this issue by informing the terminal that it should use grapheme
clusters instead of codepoints to calculate the width of characters.
In Bubble Tea v2, this feature is enabled by default. It's a no-op on terminals
that don't support it. You can disable it by passing the
WithoutGraphemeClustering
option.
Terminal version and name
Don't know what terminal you're running in? $TERM
is too vague? Bubble Tea
now has a tea.TerminalVersion
command that queries the terminal for its name
and version using XTVERSION control sequence.
Note
This feature is not supported by all terminals.
func (m Model) Init() (tea.Model, tea.Cmd) {
return m, tea.TerminalVersion
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.TerminalVersionMsg:
fmt.Printf("Terminal: %s\n", string(msg))
}
}
Terminfo and Termcap capabilities
Sometimes you need to know what capabilities the terminal has. Bubble Tea now
has a tea.RequestCapability
command that queries the terminal for a specific
terminfo/termcap capability.
Note
This feature is not supported by all terminals.
func (m Model) Init() (tea.Model, tea.Cmd) {
return m, tea.RequestCapability("Ms") // Ms is the terminfo capability for clipboard support
}
Changelog
New Features
- 7cb34be: (v2) feat: combine keyboard enhancements into a nicer API (#1152) (@aymanbagabas)
- 5cf8c29: feat(example): add example to query the terminal (@aymanbagabas)
- cdc2503: feat(example): add example to set-terminal-color's (@aymanbagabas)
- e16559d: feat(examples): add print-key example (@aymanbagabas)
- eacd930: feat(examples): add request-capability example (@aymanbagabas)
- 3e87bf8: feat: add Enable/Disable focus report commands (@caarlos0)
- fa03b59: feat: add custom renderer option (@aymanbagabas)
- 8e406ff: feat: add kitty keyboard options and settings (#1083) (@aymanbagabas)
- 0126caf: feat: add kitty keyboard options and settings (@aymanbagabas)
- b2e983a: feat: add mode 2027 grapheme clustering stubs (#1105) (@aymanbagabas)
- c385aba: feat: add renderer Resize and InsertAbove (@aymanbagabas)
- ed7dc3d: feat: add support for win32-input-mode (@aymanbagabas)
- 53d72b4: feat: add the ability to change the renderer output (@aymanbagabas)
- 76fc19d: feat: detect key input grapheme runes (@aymanbagabas)
- 70853cb: feat: drop erikgeiser/coninput in favor of x/windows (@aymanbagabas)
- 05f8399: feat: dry setting and retrieving terminal modes (@aymanbagabas)
- 05e8ecb: feat: enable focus reporting (#1081) (@aymanbagabas)
- c629efb: feat: enable focus reporting (@caarlos0)
- 033071b: feat: export kitty/xterm/win32 input commands and messages (@aymanbagabas)
- 0825f61: feat: export nil renderer (@aymanbagabas)
- 1f5a28f: feat: expose the renderer interface (#1094) (@aymanbagabas)
- 6f7f9e6: feat: expose the renderer interface (@aymanbagabas)
- ea54605: feat: introduce ExecuteSequence to send the terminal arbitrary sequence (@aymanbagabas)
- 03df14c: feat: kitty: support associated text keyboard enhancement (@aymanbagabas)
- 485e52e: feat: query and set terminal background, foreground, and cursor colors (#1085) (@aymanbagabas)
- 44d4973: feat: query and set terminal background, foreground, and cursor colors (@aymanbagabas)
- ad68fc1: feat: query modify other keys (@aymanbagabas)
- c657bba: feat: query the terminal version string and primary device attrs (#1088) (@aymanbagabas)
- 26bc2cb: feat: query the terminal version string and primary device attrs (@aymanbagabas)
- 73773e8: feat: recognize nested sequence msgs (@aymanbagabas)
- 6062461: feat: support setting and querying the terminal clipboard using OSC52 (#1086) (@aymanbagabas)
- 04f843d: feat: support setting and querying the terminal clipboard using OSC52 (@aymanbagabas)
- 828ff70: feat: support xterm modifyOtherKeys keyboard protocol (#1084) (@aymanbagabas)
- 1678d85: feat: use bubbles/v2 and bubbletea/v2 in examples and tutorials (@aymanbagabas)
- 3faa9b3: feat: use kitty report alternate keys with enhanced keyboard (@aymanbagabas)
- 0fe006d: feat: use the new renderer interface (@aymanbagabas)
Bug fixes
- 448eb82: fix(ci): skip CI for examples/tutorials running go1.18 (@aymanbagabas)
- 5df8f28: fix(examples): keys shouldn't be routed to viewport in chat example (@meowgorithm)
- 356c649: fix(examples): update testdata (@aymanbagabas)
- 9d02251: fix(key): use the key text value when keycode is multirune (@aymanbagabas)
- 0c8967d: fix(kitty): only use printables (@aymanbagabas)
- 3f5fb9a: fix(lint): reorder key struct to fix fieldalignment (@aymanbagabas)
- 508be2e: fix: force query the terminal bg before running any programs (@aymanbagabas)
- 7df684a: fix: handle Kitty keyboard protocol extensions to legacy sequences (@aymanbagabas)
- 8635fb3: fix: hide cursor if needed after restore (@aymanbagabas)
- d1827e4: fix: ignore invalid XTGETTCAP responses (@aymanbagabas)
- 1bd66e6: fix: ignore nil terminal colors (@aymanbagabas)
- a6725ba: fix: implement RequestCapability and unexport and remove internal messages and commands (@aymanbagabas)
- a26ecc5: fix: implement String() method for MouseButton type (@aymanbagabas)
- 0dd6210: fix: initialize the terminal before the renderer (@aymanbagabas)
- 9660d7d: fix: keep track of terminal capabilities and gracefully turn them off (@aymanbagabas)
- 4d2072b: fix: kitty: request protocol flags and rename flag methods (@aymanbagabas)
- bdb3237: fix: kitty: request protocol flags and rename flag methods (@aymanbagabas)
- c0f9975: fix: lint errors (@aymanbagabas)
- fa69e03: fix: lint issues (#1109) (@aymanbagabas)
- 4e118e9: fix: lint issues (@aymanbagabas)
- 7567352: fix: mouse sequence enable/disable order (@aymanbagabas)
- 45222df: fix: only reset terminal colors if they're changed (@aymanbagabas)
- 695fbf3: fix: only shutdown the program once (@aymanbagabas)
- 88a5cd2: fix: parse invalid utf8 sequences (@aymanbagabas)
- cb37f88: fix: rename events to messages (@aymanbagabas)
- 614aa93: fix: renderer: don't return a Cmd on update (@aymanbagabas)
- 516b7cd: fix: renderer: nilRenderer doesn't need to be exported (@aymanbagabas)
- 3650670: fix: reset terminal colors on exit using osc 110/111/112 (@aymanbagabas)
- ceaed4d: fix: restore terminal colors (@aymanbagabas)
- c9f2a56: fix: screen test (@aymanbagabas)
- fe88dd5: fix: show the cursor on exit (@aymanbagabas)
- 544a715: fix: simplify instantiating a new standard renderer (@aymanbagabas)
- c630d5e: fix: simplify kitty keyboard msg flags (@aymanbagabas)
- c2c195c: fix: special case modified f3 key and cursor pos report (@aymanbagabas)
- 4fa1f06: fix: terminal colors tests (@aymanbagabas)
- 62e46fe: fix: unexport kitty, modifyOtherKeys, and windowsInputMode options (@aymanbagabas)
- 4967f6b: fix: unexport standardRenderer (@aymanbagabas)
- f2bdd36: fix: use KeyRunes to indicate text input (@aymanbagabas)
- e206b36: fix: use explicit names for kitty keyboard option (@aymanbagabas)
- 9636413: fix: use safeWriter to guard writing to output (@aymanbagabas)
- 2f6637b: fix: windows driver build (@aymanbagabas)
- da83499: fix: windows: correctly parse upper/lower key (@aymanbagabas)
Documentation updates
- fe54df7: docs(examples): add help view to table example (@meowgorithm)
- 3274e41: docs(tutorials): upgrade tuts to v2 (#1155) (@meowgorithm)
- 2f14548: docs: add godoc examples (@aymanbagabas)
- 210358d: docs: add v2 todo code reminders (@aymanbagabas)
- de4788d: docs: update readme badge images (@aymanbagabas)
Other work
- 85c5adc: (v2) Export different input mode commands and messages (#1119) (@aymanbagabas)
- 8a75439: (v2) Use KeyMsg/MouseMsg interfaces (#1111) (@aymanbagabas)
- 3ef72f2: Fix Windows API & Add support for win32-input-mode (#1087) (@aymanbagabas)
- ad68c42: feat!: make Init return the model (#1112) (@aymanbagabas)
- e8903bb: feat!: use KeyExtended to signify more than one rune (@aymanbagabas)
- 14cb6b5: feat!: v2: update module path to github.com/charmbracelet/bubbletea/v2 (@aymanbagabas)
- 6e060ca: refactor!: remove backwards compatibility (@aymanbagabas)
- 7b87642: refactor!: use key/mouse msg interfaces (@aymanbagabas)
- 3075646: refactor: bracketed-paste active state (@aymanbagabas)
- ffe0133: refactor: change kitty keyboard flag name for clarity (@aymanbagabas)
- ae0c273: refactor: check the initial size during Run (@aymanbagabas)
- 7a49b33: refactor: define renderer execute to write queries to the terminal (@aymanbagabas)
- 16f706a: refactor: expose key codes and define key/mouse interfaces (@aymanbagabas)
- 4247aac: refactor: flatten csi sequence parsing (@aymanbagabas)
- 0c1a6a4: refactor: reimplement the event loop with a sequence parser (#1080) (@aymanbagabas)
- 13ffcad: refactor: reimplement the event loop with a sequence parser (@aymanbagabas)
- 370d248: refactor: remove kitty protocol flags (@aymanbagabas)
- 3981b80: refactor: remove tea.ExecuteSequence (@aymanbagabas)
- eb2eee4: refactor: rename KeySym to KeyType and make KeyRunes the default (@aymanbagabas)
- ec5b362: refactor: rename query kitty keyboard command (@aymanbagabas)
- e0865cf: refactor: simplify key modifier matching (@aymanbagabas)
- 1f88b9e: refactor: simplify renderer interface (@aymanbagabas)
- 39ea34c: refactor: unexport kitty keyboard settings and options (@aymanbagabas)
- 5f7700e: refactor: unexport modify other keys (@aymanbagabas)
- c47c2b9: refactor: unexport win32 input mode (@aymanbagabas)
- f31a5f3: refactor: we don't care about renderer render errors (@aymanbagabas)
Feedback
Have thoughts on Bubble Tea v2 Alpha? We’d love to hear about it. Let us know on…
Part of Charm.
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة