Releases: charmbracelet/bubbletea
v1.1.2
A tiny tiny release that fixes the tests on Windows, and uses the latest ansi
package definitions.
Changelog
New Features
- 12b04c5: feat(ci): use meta lint workflow (@aymanbagabas)
- 3209d62: feat(ci): use meta lint-sync workflow to sync linter config (@aymanbagabas)
Bug fixes
- 566879a: fix(ci): run lint workflow on all platforms (@aymanbagabas)
- cd1e4d3: fix: exec tests on windows (@aymanbagabas)
Documentation updates
- d928d8d: docs: update contributing guidelines (#1186) (@bashbunni)
- de4788d: docs: update readme badge images (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v2.0.0-alpha.1
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?
- Ghostty
- Kitty
- Alacritty
- iTerm2
- Foot
- WezTerm
- Rio
- Contour Terminal
- Windows Terminal
- tmux (no key release events, however)
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":
// ...
v1.1.1
Don't panic!
Panicking is a part of life…and a part of workin’ in Go. This release addresses two edge cases where a panic()
could tank Bubble Tea and break your terminal:
Panics outside of Bubble Tea
If a panic occurs outside of Bubble Tea you can use Program.Kill
to restore the terminal state before exiting:
func main() {
p := tea.NewProgram(model{})
go func() {
time.Sleep(3 * time.Second)
defer p.Kill()
panic("Urgh")
}()
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
Panics in Cmds
If a panic occurs in a Cmd
Bubble Tea will now automatically restore the terminal to its natural state before exiting.
type model struct{}
// This command will totally panic.
func pancikyCmd() tea.Msg {
panic("Oh no! Jk.")
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "enter":
// Panic time! But everything will be OK.
return m, pancikyCmd
}
}
return m, nil
}
Happy panicking (if that makes any sense).
Changelog
Fixed!
- 0589921: fix: recover from panics within cmds (@aymanbagabas)
- 6e71f52: fix: restore the terminal on kill (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v1.1.0
Let’s Focus
Lose focus much? This release contains support for focus-blur window events.
Usage
All you need to do is to add the program option to your application:
p := tea.NewProgram(model{}, tea.WithReportFocus())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, "Oof:", err)
os.Exit(1)
}
Then later in your Update
function, you can listen for focus-blur messages:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.FocusMsg:
// Focused!
case tea.BlurMsg:
// Not focused :(
}
return m, nil
}
For details, see WithReportFocus.
Tmux
If you're using tmux
, make sure you enable the focus-events
option in your config.
set-option -g focus-events on
Happy focusing (whatever that means)!
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v1.0.1
This release that fixes the way carriage returns are handled with using the WithoutRenderer ProgramOption
and improves the way it works overall by not altering the terminal the way we normally do when starting a Program
. For details see #1120.
- c69bd97: fix: we don't initialize the terminal when using a nilRenderer (#1120) (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v1.0.0
At last: v1.0.0
This is an honorary release denoting that Bubble Tea is now stable. Thank you, open source community, for all your love, support, and great taste in beverage over the past four years.
Stay tuned for v2: we have some great things coming.
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v0.27.1
This is a lil’ workaround for a hang that can occur when starting a program using Lip Gloss. For details see #1107.
Changelog
Bug fixes
- d6458e0: fix: force query the terminal bg before running any programs (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v0.27.0
Suspending, environment hacking, and more
Hi! This release has three nice little features and some bug fixes. Let's take a look:
Suspending and resuming
At last, now you can programmatically suspend and resume programs with the tea.Suspend
command and handle resumes with the tea.ResumeMsg
message:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// Suspend with ctrl+z!
case tea.KeyMsg:
switch msg.String() {
case "ctrl+z":
m.suspended = true
return m, tea.Suspend
}
// Handle resumes
case tea.ResumeMsg:
m.suspended = false
return m, nil
}
// ...
}
There's also a tea.SuspendMsg
that flows through Update
on suspension.
Special thanks to @knz for prototyping the original implementation of this.
Setting the environment
When Bubble Tea is behind Wish you may have needed to pass environment variables from the remote session to the Program
. Now you can with the all new tea.WithEnvironment:
var sess ssh.Session // ssh.Session is a type from the github.com/charmbracelet/ssh package
pty, _, _ := sess.Pty()
environ := append(sess.Environ(), "TERM="+pty.Term)
p := tea.NewProgram(model, tea.WithEnvironment(environ)
Requesting the window dimensions
All the Bubble Tea pros know that you get a tea.WindowSizeMsg
when the Program
starts and when the window resizes. Now you can just query it on demand too with the tea.WindowSize
command.
Changelog
New!
- 7d70838: feat: add a cmd to request window size (#988) (@aymanbagabas)
- ea13ffb: feat: allow to suspend bubbletea programs (#1054) (@caarlos0)
- cae9acd: feat: set the program environment variables (#1063) (@aymanbagabas)
Fixed
- 7c1bfc0: query window-size in a goroutine (#1059) (@aymanbagabas)
- 4497aa9: reset cursor position on renderer exit (#1058) (@aymanbagabas)
- d6a19f0: wrap
ErrProgramKilled
error (@aymanbagabas) - 4a9620e: fix bugs in package-manager example (@AkshayKalose)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v0.26.6
Changelog
Bug fixes
- 60a57ea: fix: nil deref on release terminal (@aymanbagabas)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.
v0.26.5
Fix special keys input handling on Windows using the latest Windows Console Input driver.
Changelog
New Features
- 42a7dd8: feat(ci): use goreleaser for releases (#1023) (@aymanbagabas)
Bug fixes
Other work
- 2d65ed6: chore(examples): removed use of deprecated Copy (@arianizadi)
Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.