Skip to content

Releases: charmbracelet/bubbletea

v2.0.0-alpha.2

12 Nov 22:03
v2.0.0-alpha.2
Compare
Choose a tag to compare
v2.0.0-alpha.2 Pre-release
Pre-release

Are you excited about another alpha release?

We know we are! This release is chock full of features and improvements.

If you’re new to Bubble Tea v2 we recommending taking a look at the v2.0.0-alpha.1 release notes as well.

If you're already using v2.0.0-alpha.1 there's nothing new to do for v2.0.0-alpha.2. You can blissfully ignore everything below and gleefully reap the benefits…but don't you want to know what’s new?

Let's dive in!

🚚 Packages

This release was designed to work with Bubble Tea, Bubbles, and Lip Gloss v2 alphas with the same tag, so make sure you catch ’em all:

go get github.com/charmbracelet/bubbletea/v2@v2.0.0-alpha.2
go get github.com/charmbracelet/bubbles/v2@v2.0.0-alpha.2
go get github.com/charmbracelet/lipgloss/v2@v2.0.0-alpha.2

🎩 Better Windows Input Support

We take Windows support seriously, and with this release, we've revamped the
Windows input handling. Now, Bubble Tea works even better on Windows.

Bubble Tea now takes advantage of the Windows Console API to
get the terminal's size, focus events, and advanced keyboard and mouse
handling. This won't interfere with the existing escape sequence handling, so
you can still use the same code across different platforms.

🪄 Program-level Color Profiles and Styles

Bubble Tea now automatically downsamples ANSI, when necessary, for the appropriate output. For example, if you're setting 24-bit (TrueColor) colors in your application, and the user is using Apple Terminal, the colors will be automatically downsampled to the nearest colors in the 256 color spectrum. Or, if output's being directed to a file, colors will be stripped entirely.

Even better, this works with colors and styling generated by anything, not just Lip Gloss.
That means you can flow in ANSI styling from any tool or library and rest assured that colors
will just work with the user's terminal. This is all thanks to our new
magical colorprofile
library.

Detecting the color profile

Need to use the detected color profile in your app? Listen to tea.ColorProfileMsg in Update:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.ColorProfileMsg:
        m.colorProfile = msg.Profile // gottem!
    }
    return m, nil
}

Manually applying a color profile

What's that you say? You want to manually set a color profile for testing? Now you can, on the program level.

import (
    "github.com/charmbraclelet/bubbletea/v2"
    "github.com/charmbraclelet/colorprofile"
)

p := colorprofile.TrueColor // i love colors. lets' use 16,777,216 of 'em
p = colorprofile.ANSI256    // jk, 256 colors are plenty
p = colorprofile.ANSI       // actually let's juse use 16 colors
p = colorprofile.Ascii      // nm, no colors, but keep things like bold, italics, etc.
p = colorprofile.NoTTY      // lol actually strip all ANSI sequences

prog := tea.Program(tea.WithColorProfile(p))

Want to hard detect the color profile in wish? We bet you do.

func main() {
    var s ssh.Session
    pty, _, _ := s.Pty()

    // Get the environment...
    envs := append(s.Environ(), "TERM="+pty.Term)

    // ...and give it to Bubble Tea so it can detect the color profile.
    opt := tea.WithEnvironment(envs)

    p := tea.NewProgram(model,
        tea.WithInput(pty.Slave),
        tea.WithOutput(pty.Slave),
        opt, // wow
    )
}

Going deep

Hungry for more? This is for the more hard core terminal devs, like you.

😮 An Acual Cursor

Another nice feature is the ability to use a real terminal
cursor. If you enable the cursor visibility, you can control the cursor
position and shape in your program.

func (m model) Init() (tea.Model, tea.Cmd) {
    return m, tea.Batch(
        tea.ShowCursor,
        tea.SetCursorStyle(tea.CursorBlock, false),     // non-blinking block cursor
        tea.SetCursorStyle(tea.CursorUnderline, true),  // blinking underline cursor
        tea.SetCursorStyle(tea.CursorBar, true),        // blinking vertical bar cursor
    )
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "q":
            return m, tea.Quit
        }
    }
    // move the cursor to the second row and first column of the program screen.
    return m, tea.SetCursorPosition(0, 1)
}

func (m model) View() string {
    return "Hey there!\n <- I'm a cursor!"
}

You can also request the cursor position by using the tea.RequestCursorPosition command.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "enter":
            return m, tea.RequestCursorPosition
        case "q":
            return m, tea.Quit
        }
    case tea.CursorPositionMsg:
        m.cursorPosition.X, m.cursorPosition.Y = msg.X, msg.Y
    }
    return m, nil
}

We’re still in the process of integrating true cursor support into textarea and textinput in Bubbles, however a true cursor is now at your disposal.

🗝️ Uniform Key Handling

One of the biggest changes coming to Bubble Tea v2 is enhanced keyboard
handling. This includes support for more key events, and complex key
combinations, as well as key releases.

When targeting users on different platforms and keyboard layouts, it's
important to have a consistent and reliable way to handle key events. With this
release, you can now use tea.WithUniformKeyLayout keyboard enhancement to
ensure that your program works as expected across different keyboard layouts.

For example, on a multi-layout QWERTY keyboard, ctrl+a should always
trigger a ctrl+a event, regardless of the layout language. Similarly,
shift+h should send a shift+h event with the letter H as its
printable value. But what happens when you press shift+h in a
different QWERTY layout?

Let's take the PC-101 QWERTY Arabic layout as an example. In this layout,
shift+h corresponds to the Arabic letter أ. If you're building a
game or an application that relies on key events where you don't really care
about the actual printable value of the key press, you'd want to ensure that
shift+h always sends a shift+h event despite the layout language.

This will also respect the keyboard layout and send the correct printable
value for the key press. For example, on a US QWERTY keyboard, ctrl+a
corresponds to the first letter on the second row of the keyboard. On a French
AZERTY keyboard, the same key combinations correspond to ctrl+q.

To achieve this, you can use the tea.WithUniformKeyLayout option and let Bubble
Tea handle the rest.

p := tea.NewProgram(model, tea.WithEnhancedKeyboard(
        tea.WithKeyReleases, // Do we want to listen to key releases?
        tea.WithUniformKeyLayout,
    ))

// Later in your program's Update function.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "ctrl+a":
            // Handle ctrl+a event regardless of the keyboard layout.
        case "shift+h":
            // Handle shift+h event regardless of the keyboard layout and get the correct printable value.
        }
    }
    return m, nil
}

💬 Requesting Terminal Properties

With this release, we made it explicit how to request terminal properties. Use
the tea.Request... commands to get the terminal's size, background color,
foreground color, etc.

// Before
func (m model) Init() (tea.Model, tea.Cmd) {
    return m, tea.BackgroundColor
}

// After
func (m model) Init() (tea.Model, tea.Cmd) {
    return m, tea.RequestBackgroundColor
}

The following Cmds are now at your disposal:

  • tea.RequestBackgroundColor (sends a tea.BackgroundColorMsg)
  • tea.RequestForegroundColor (sends a tea.ForgroundColorMsg)
  • tea.RequestCursorColor (sends a tea.CursorColorMsg)
  • tea.RequestWindowSize (sends a tea.WindowSizeMsg)
  • tea.RequestCursorPosition (sends a tea.CursorPostionMsg)

🍇 Grapheme Clustering

Grapheme what? In short, grapheme clustering is a way to handle Unicode text
segmentation and boundaries. It's useful when you want to determine the terminal
cell width a string or a grapheme cluster occupies. The algorithm is defined
in the Unicode Text Segmentation UAX #29.

For example, '🧑‍🌾' is a single grapheme cluster, but it's made up of 3
UTF-8 codepoints. Terminals that support grapheme clustering will treat this as
a 2 cell wide character, while those that don't might treat it as 2, 4, 5, or
even 6 cells wide. Our friend Mitchell Hashimoto has a great blog post on this
topic: Grapheme Clusters and Terminal Emulators.

We've added grapheme clustering and mode 2027 support in the previous alpha.1
release, and it was on by default. We've noticed that some terminals, like
Apple Terminal, don't play well with this feature. Specifically, with the
DECRQM control sequence. Not cool.

Now we're making the feature opt-in and off by default. You can still enable it by
using the tea.WithGraphemeClustering option.

p := tea.NewProgram(model, tea.WithGraphemeClustering())

// Or in your program's Init function.
func (m model) In...
Read more

v1.2.0

06 Nov 16:25
f4d1e0e
Compare
Choose a tag to compare

It’s performance boost time

Sometimes you have to take matters into your own hands. That’s exactly what @LeperGnome did when he wanted faster rendering. This release features adjustments to the rendering algorithm for faster repaints. We encourage you to upgrade and give it a go!

Note

Renderer changes are no laughing matter. We’ve tested the new renderer extensively, however if you notice any bugs let us know. Rendering accuracy is among our top priorities.

Changelog

New Contributors

Full Changelog: v1.1.2...v1.2.0


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

v1.1.2

24 Oct 18:37
v1.1.2
Compare
Choose a tag to compare

This and that

A tiny tiny release that fixes the tests on Windows, and uses the latest ansi package definitions.

Changelog

New Features

Bug fixes

Documentation updates


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

v2.0.0-alpha.1

18 Sep 18:39
v2.0.0-alpha.1
3274e41
Compare
Choose a tag to compare
v2.0.0-alpha.1 Pre-release
Pre-release

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":
            // ...
Read more

v1.1.1

11 Sep 17:06
v1.1.1
Compare
Choose a tag to compare

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!


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

v1.1.0

30 Aug 13:23
v1.1.0
e58efab
Compare
Choose a tag to compare

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)!


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

v1.0.1

29 Aug 17:48
v1.0.1
c69bd97
Compare
Choose a tag to compare

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.


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

v1.0.0

28 Aug 15:06
v1.0.0
105d88a
Compare
Choose a tag to compare

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.


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

v0.27.1

22 Aug 17:19
v0.27.1
17443d8
Compare
Choose a tag to compare

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


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.

v0.27.0

16 Aug 16:21
v0.27.0
d6a19f0
Compare
Choose a tag to compare

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
	}

	// ...
}

Example

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!

Fixed


The Charm logo

Thoughts? Questions? We love hearing from you. Feel free to reach out on Twitter, The Fediverse, or on Discord.