github charmbracelet/bubbletea v2.0.0-alpha.1

latest release: v1.1.2
pre-releaseone month ago

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

Bug fixes

Documentation updates

Other work

Feedback

Have thoughts on Bubble Tea v2 Alpha? We’d love to hear about it. Let us know on…


Part of Charm.

The Charm logo

Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة

Don't miss a new bubbletea release

NewReleases is sending notifications on new releases.