Code Monkey home page Code Monkey logo

bubbletea's Introduction

Bubble Tea

Bubble Tea Title Treatment
Latest Release GoDoc Build Status phorm.ai

The fun, functional and stateful way to build terminal apps. A Go framework based on The Elm Architecture. Bubble Tea is well-suited for simple and complex terminal applications, either inline, full-window, or a mix of both.

Bubble Tea Example

Bubble Tea is in use in production and includes a number of features and performance optimizations we’ve added along the way. Among those is a standard framerate-based renderer, a renderer for high-performance scrollable regions which works alongside the main renderer, and mouse support.

To get started, see the tutorial below, the examples, the docs, the video tutorials and some common resources.

By the way

Be sure to check out Bubbles, a library of common UI components for Bubble Tea.

Bubbles Badge   Text Input Example from Bubbles


Tutorial

Bubble Tea is based on the functional design paradigms of The Elm Architecture, which happens to work nicely with Go. It's a delightful way to build applications.

This tutorial assumes you have a working knowledge of Go.

By the way, the non-annotated source code for this program is available on GitHub.

Enough! Let's get to it.

For this tutorial, we're making a shopping list.

To start we'll define our package and import some libraries. Our only external import will be the Bubble Tea library, which we'll call tea for short.

package main

import (
    "fmt"
    "os"

    tea "github.com/charmbracelet/bubbletea"
)

Bubble Tea programs are comprised of a model that describes the application state and three simple methods on that model:

  • Init, a function that returns an initial command for the application to run.
  • Update, a function that handles incoming events and updates the model accordingly.
  • View, a function that renders the UI based on the data in the model.

The Model

So let's start by defining our model which will store our application's state. It can be any type, but a struct usually makes the most sense.

type model struct {
    choices  []string           // items on the to-do list
    cursor   int                // which to-do list item our cursor is pointing at
    selected map[int]struct{}   // which to-do items are selected
}

Initialization

Next, we’ll define our application’s initial state. In this case, we’re defining a function to return our initial model, however, we could just as easily define the initial model as a variable elsewhere, too.

func initialModel() model {
	return model{
		// Our to-do list is a grocery list
		choices:  []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},

		// A map which indicates which choices are selected. We're using
		// the  map like a mathematical set. The keys refer to the indexes
		// of the `choices` slice, above.
		selected: make(map[int]struct{}),
	}
}

Next, we define the Init method. Init can return a Cmd that could perform some initial I/O. For now, we don't need to do any I/O, so for the command, we'll just return nil, which translates to "no command."

func (m model) Init() tea.Cmd {
    // Just return `nil`, which means "no I/O right now, please."
    return nil
}

The Update Method

Next up is the update method. The update function is called when ”things happen.” Its job is to look at what has happened and return an updated model in response. It can also return a Cmd to make more things happen, but for now don't worry about that part.

In our case, when a user presses the down arrow, Update’s job is to notice that the down arrow was pressed and move the cursor accordingly (or not).

The “something happened” comes in the form of a Msg, which can be any type. Messages are the result of some I/O that took place, such as a keypress, timer tick, or a response from a server.

We usually figure out which type of Msg we received with a type switch, but you could also use a type assertion.

For now, we'll just deal with tea.KeyMsg messages, which are automatically sent to the update function when keys are pressed.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {

    // Is it a key press?
    case tea.KeyMsg:

        // Cool, what was the actual key pressed?
        switch msg.String() {

        // These keys should exit the program.
        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 the spacebar (a literal space) toggle
        // the selected state for the item that the cursor is pointing at.
        case "enter", " ":
            _, ok := m.selected[m.cursor]
            if ok {
                delete(m.selected, m.cursor)
            } else {
                m.selected[m.cursor] = struct{}{}
            }
        }
    }

    // Return the updated model to the Bubble Tea runtime for processing.
    // Note that we're not returning a command.
    return m, nil
}

You may have noticed that ctrl+c and q above return a tea.Quit command with the model. That’s a special command which instructs the Bubble Tea runtime to quit, exiting the program.

The View Method

At last, it’s time to render our UI. Of all the methods, the view is the simplest. We look at the model in its current state and use it to return a string. That string is our UI!

Because the view describes the entire UI of your application, you don’t have to worry about redrawing logic and stuff like that. Bubble Tea takes care of it for you.

func (m model) View() string {
    // The header
    s := "What should we buy at the market?\n\n"

    // Iterate over our choices
    for i, choice := range m.choices {

        // Is the cursor pointing at this choice?
        cursor := " " // no cursor
        if m.cursor == i {
            cursor = ">" // cursor!
        }

        // Is this choice selected?
        checked := " " // not selected
        if _, ok := m.selected[i]; ok {
            checked = "x" // selected!
        }

        // Render the row
        s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
    }

    // The footer
    s += "\nPress q to quit.\n"

    // Send the UI for rendering
    return s
}

All Together Now

The last step is to simply run our program. We pass our initial model to tea.NewProgram and let it rip:

func main() {
    p := tea.NewProgram(initialModel())
    if _, err := p.Run(); err != nil {
        fmt.Printf("Alas, there's been an error: %v", err)
        os.Exit(1)
    }
}

What’s Next?

This tutorial covers the basics of building an interactive terminal UI, but in the real world you'll also need to perform I/O. To learn about that have a look at the Command Tutorial. It's pretty simple.

There are also several Bubble Tea examples available and, of course, there are Go Docs.

Debugging

Debugging with Delve

Since Bubble Tea apps assume control of stdin and stdout, you’ll need to run delve in headless mode and then connect to it:

# Start the debugger
$ dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
API server listening at: 127.0.0.1:43000

# Connect to it from another terminal
$ dlv connect 127.0.0.1:43000

If you do not explicitly supply the --listen flag, the port used will vary per run, so passing this in makes the debugger easier to use from a script or your IDE of choice.

Additionally, we pass in --api-version=2 because delve defaults to version 1 for backwards compatibility reasons. However, delve recommends using version 2 for all new development and some clients may no longer work with version 1. For more information, see the Delve documentation.

Logging Stuff

You can’t really log to stdout with Bubble Tea because your TUI is busy occupying that! You can, however, log to a file by including something like the following prior to starting your Bubble Tea program:

if len(os.Getenv("DEBUG")) > 0 {
	f, err := tea.LogToFile("debug.log", "debug")
	if err != nil {
		fmt.Println("fatal:", err)
		os.Exit(1)
	}
	defer f.Close()
}

To see what’s being logged in real time, run tail -f debug.log while you run your program in another window.

Libraries we use with Bubble Tea

  • Bubbles: Common Bubble Tea components such as text inputs, viewports, spinners and so on
  • Lip Gloss: Style, format and layout tools for terminal applications
  • Harmonica: A spring animation library for smooth, natural motion
  • BubbleZone: Easy mouse event tracking for Bubble Tea components
  • Termenv: Advanced ANSI styling for terminal applications
  • Reflow: Advanced ANSI-aware methods for working with text

Bubble Tea in the Wild

For some Bubble Tea programs in production, see:

  • ASCII Movie: a Star Wars ASCII art movie player
  • AT CLI: execute AT Commands via serial port connections
  • Aztify: bring Microsoft Azure resources under Terraform
  • brows: a GitHub release browser
  • Canard: an RSS client
  • charm: the official Charm user account manager
  • chatgpt-cli: a CLI for ChatGPT
  • chatgpt-tui: a TUI for ChatGPT with SQLite sessions
  • ChatGPTUI: a TUI for ChatGPT
  • chezmoi: securely manage your dotfiles across multiple machines
  • chip-8: a CHIP-8 interpreter
  • chtop: monitor your ClickHouse node without leaving the terminal
  • circumflex: read Hacker News in the terminal
  • clidle: a Wordle clone
  • cLive: automate terminal operations and view them live in a browser
  • container-canary: a container validator
  • countdown: a multi-event countdown timer
  • CRT: a simple terminal emulator for running Bubble Tea in a dedicated window, with optional shaders
  • cueitup: inspect messages in an AWS SQS queue in a simple and deliberate manner
  • Daytona: an development environment manager
  • dns53: dynamic DNS with Amazon Route53; expose your EC2 quickly, securely and privately
  • eks-node-viewer: a tool for visualizing dynamic node usage within an EKS cluster
  • End Of Eden: a "Slay the Spire"-like, roguelike deck-builder game
  • enola: find social media accounts by username across social networks
  • flapioca: Flappy Bird on the CLI!
  • fm: a terminal-based file manager
  • fork-cleaner: clean up old and inactive forks in your GitHub account
  • fractals-cli: a multiplatform terminal fractal explorer
  • fztea: a Flipper Zero TUI
  • gama: manage GitHub Actions from the terminal
  • gambit: chess in the terminal
  • gembro: a mouse-driven Gemini browser
  • gh-b: a GitHub CLI extension for managing branches
  • gh-dash: a GitHub CLI extension for PRs and issues
  • gitflow-toolkit: a GitFlow submission tool
  • Glow: a markdown reader, browser, and online markdown stash
  • go-sweep: Minesweeper in the terminal
  • gocovsh: explore Go coverage reports from the CLI
  • got: a simple translator and text-to-speech app built on simplytranslate's APIs
  • gum: interactivity and styling for shells and shell scripts
  • hiSHtory: your shell history in context: synced, and queryable
  • httpit: a rapid http(s) benchmark tool
  • Huh?: an interactive prompt and form toolkit
  • IDNT: a batch software uninstaller
  • json-log-viewer: an interactive JSON log viewer
  • kboard: a typing game
  • kplay: inspect messages in a Kafka topic
  • laboon: a Docker-desktop-style container manager
  • mc: the official MinIO client
  • mergestat: run SQL queries on git repositories
  • meteor: a highly customizable conventional commit message tool
  • mods: AI on the CLI, built for pipelines
  • nachrichten: access up-to-date news in German provided by the Tagesschau
  • Neon Modem Overdrive: a BBS-style TUI client for Discourse, Lemmy, Lobste.rs and Hacker News
  • nom: an RSS reader and manager
  • Noted: a note viewer and manager
  • outtasync: identify CloudFormation stacks that are out of sync with their template files
  • pathos: a PATH environment variable editor
  • Plandex: a terminal-based AI coding engine for complex tasks
  • portal: secure transfers between computers
  • prs: stay up to date with your PRs
  • puffin: a TUI for hledger to manage your finances
  • punchout: takes the suck out of logging time on JIRA
  • redis-viewer: a Redis database browser
  • redis_tui: a Redis database browser
  • schemas: lets you inspect postgres schemas in the terminal
  • scrabbler: an automatic draw tool for your duplicate Scrabble games
  • sku: Sudoku on the CLI
  • Slides: a markdown-based presentation tool
  • SlurmCommander: a Slurm workload manager
  • Soft Serve: a command-line-first Git server that runs a TUI over SSH
  • solitaire-tui: Klondike Solitaire for the terminal
  • StormForge Optimize Controller: a tool for experimenting with application configurations in Kubernetes
  • Storydb: an improved bash/zsh-style ctrl+r command history finder
  • STTG: a teletext client for SVT, Sweden’s national public television station
  • sttr: a general-purpose text transformer
  • tasktimer: a dead-simple task timer
  • termdbms: a keyboard and mouse driven database browser
  • tgpt: conversational AI for the CLI; no API keys necessary
  • ticker: a terminal stock viewer and stock position tracker
  • trainer: a Go concurrency coding interview simulator with learning materials
  • tran: securely transfer stuff between computers (based on portal)
  • Trufflehog: find leaked credentials
  • Typer: a typing test
  • typioca: a typing test
  • tz: a scheduling aid for people in multiple time zones
  • ugm: a unix user and group browser
  • walk: a terminal navigator
  • wander: a HashiCorp Nomad terminal client
  • WG Commander: a TUI for a simple WireGuard VPN setup
  • wishlist: an SSH directory

Feedback

We'd love to hear your thoughts on this project. Feel free to drop us a note!

Acknowledgments

Bubble Tea is based on the paradigms of The Elm Architecture by Evan Czaplicki et alia and the excellent go-tea by TJ Holowaychuk. It’s inspired by the many great Zeichenorientierte Benutzerschnittstellen of days past.

License

MIT


Part of Charm.

The Charm logo

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

bubbletea's People

Contributors

adjective-object avatar ajeetdsouza avatar aymanbagabas avatar bashbunni avatar caarlos0 avatar dependabot[bot] avatar dhth avatar geodimm avatar inkel avatar irevenko avatar kiyonlin avatar knz avatar lusingander avatar maaslalani avatar meowgorithm avatar michelefiladelfia avatar mrusme avatar muesli avatar nderjung avatar pheon-dev avatar rubysolo avatar siddhantac avatar stefanlogue avatar superpaintman avatar taigrr avatar tearingitup786 avatar timmattison avatar tjovicic avatar tklauser avatar twpayne avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bubbletea's Issues

Swapping out current model dynamically

👋 Not sure if I'm just approaching this incorrectly. I'm building a UI with an index that has a list of items, and selecting the item drills down into the details for the item.

I created a top level model as recommended in #13, but because all my child models are dynamic, I was generating them and sending up to the parent via a channel. Here's a simplified, contrived example:

package main

import (
	"fmt"

	tea "github.com/charmbracelet/bubbletea"
)

type item struct {
	details string
}

func (i item) String() string {
	return i.details
}

/* Parent model *****************************************************/
type model struct {
	current tea.Model
	ch      chan tea.Model
}

func (m *model) Watch() {
	for {
		select {
		case mm := <-m.ch:
			m.current = mm
		}
	}
}

func (m model) Init() tea.Cmd {
	return tea.EnterAltScreen
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	return m.current.Update(msg)
}

func (m model) View() string {
	return m.current.View()
}

/* First child model ************************************************/
type listModel struct {
	cursor int
	msgs   []item
	ch     chan tea.Model
}

func (l listModel) Init() tea.Cmd {
	return tea.EnterAltScreen
}

func (l listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "q":
			return l, tea.Quit
		case "k":
			if l.cursor > 0 {
				l.cursor--
			}
		case "j":
			if l.cursor < len(l.msgs)-1 {
				l.cursor++
			}
		case "enter", " ":
			l.ch <- itemModel{item: l.msgs[l.cursor], ch: l.ch}
		}
	}
	return l, nil
}

func (l listModel) View() string {
	str := "List view\n"
	for i, msg := range l.msgs {
		cursor := " "
		if l.cursor == i {
			cursor = ">"
		}
		str += fmt.Sprintf("%s %s\n", cursor, msg)
	}
	str += "\nPress q to quit\n"
	return str
}

func newListModel(ch chan tea.Model) listModel {
	return listModel{
		ch: ch,
		msgs: []item{
			{"one"},
			{"two"},
			{"three"},
		},
	}
}

/* Drilldown child model ********************************************/

type itemModel struct {
	item item
	ch   chan tea.Model
}

func (i itemModel) Init() tea.Cmd {
	return tea.EnterAltScreen
}

func (i itemModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "q":
			return i, tea.Quit
		case "b":
			i.ch <- newListModel(i.ch)
		}
	}
	return i, nil
}

func (i itemModel) View() string {
	return fmt.Sprintf("Item view\n %s\n q to quit, b to go back\n", i.item)
}

/* main *************************************************************/

func main() {
	ch := make(chan tea.Model, 1)
	m := model{
		current: newListModel(ch),
		ch:      ch,
	}

	go m.Watch()

	p := tea.NewProgram(m)
	p.Start()
}

The models never seem to update. I've tried manually kicking off Init() and Update() after setting in Watch(), but no luck.

Is there some lifecycle event I'm missing? Am I just approaching this in a dumb way?

Questions about viewport

  • Maybe im not going about this the correct way but im working on a file manager just for fun here https://github.com/knipferrc/fm. Trying to figure out how to implement the viewport and its somewhat working but I have a couple issues.
  1. ) Whenever I check if m.ready inside the view, it just sits in a loading state infinitely. Maybe im not initializing something correctly?
case tea.WindowSizeMsg:
		if !m.ready {
			m.screenwidth = msg.Width
			m.screenheight = msg.Height
			m.viewport = viewport.Model{
				Width:  msg.Width,
				Height: msg.Height - 1,
			}
			m.viewport.YPosition = 0
			m.ready = true
		} else {
			m.screenwidth = msg.Width
			m.screenheight = msg.Height
			m.viewport.Width = msg.Width
			m.viewport.Height = msg.Height - 1
		}
  1. ) Whenever I call viewport.LineUp or LineDown it seems to take no effect on the viewport? Attempting to get it so that you can scroll through your directly tree when the height goes outside the terminal

Any help is much appreciated

Mouse scroll doesn't work in foot terminal

In the foot terminal mouse scroll events aren't triggered at all (though other mouse events trigger okay). Mouse scrolling works in other terminals such as alacritty so I don't think the issue is with my code, and other programs such as htop have working mouse scrolling support in foot making me think the issue is on bubbletea's side

Question about Design of `Cmd` in `Update(Msg) (Model, Cmd)`

In the original Elm Architecture, Update() just sends updated model to View(). Just like the figure below:

drawing

Whereas in Bubble Tea, Update() returns an newly updated model and also a command to execute asynchronously, which returns another message and henceforth invokes Update() again.

So can you please explain why Bubble Tea should be designed so, and what's the impact if the Update() is just Update(Msg) Model without Cmd in return (command would be executed in Update() directly sync-or-asynchronously then)?

Why not use interfaces?

Just found this lib and something that seems a bit off to me is the API with regards to actually implementing your "program".

func NewProgram(init Init, update Update, view View) *Program

The setup function firstly takes a number of custom types, now there's nothing wrong with these however when using an editor with intellisense like vscode, the actual meaning of those types is obfuscated and means you have to open the docs or Ctrl+click a few times to know what you're trying to satisfy.

Second, the update and view function also takes our model as a parameter, only it's not really our model now, it's a blank interface so every time I want my data I need to cast it back into the real model, check that it wasn't passed something else and then return a copy of it? I would like to know the reasoning behind this as it seems like a bit of a runabout way of doing things.

As an alternative I would propose that NewProgram takes an interface that satisfies the library:

type Model interface {
	Init() tea.Cmd
	Update(tea.Msg) tea.Cmd
	View() string
}

func NewProgram(from Model) *Program {...}

A program could then be initialised like so:

type myModel struct {
	content  string
	ready    bool
	viewport viewport.Model
}

func (m *myModel) Init() tea.Cmd {...} // Could possibly omit this in favour of a smaller interface?
func (m *myModel) Update(tea.Msg) tea.Cmd {...}
func (m *myModel) View() string {...}

myInstance := myModel{
	content: "hello",
}

p := tea.NewProgram(myInstance)
if err := p.Start(); err != nil {
	fmt.Println("could not run program:", err)
	os.Exit(1)
}

Again I have not looked to deeply at the library sources to see anything jump out as to why this would not be possible, I just think this would allow a slightly cleaner API for users and allow for more fancy use cases such as type composition defined views.

Filter search not working in list-simple example

After running the example list-simple/main.go I noticed that the search function is not getting triggered.

I see the FilterValue() is also defined just like in other list examples.

Not sure if this is the default behavior or some issue.

Edit

l.SetFilteringEnabled(true) setting this to true enables the search but now the issue is it shows nothing in the search like so:

issue-bubble

Arrow keys no longer work on Windows after upgrading to version 0.16

I can reproduce this on all of the examples in this repo that require the arrow keys or mouse. After poking around a bit, I was able to fix this by changing this line: newMode &^= windows.ENABLE_VIRTUAL_TERMINAL_INPUT here: https://github.com/charmbracelet/bubbletea/blob/master/cancelreader_windows.go#L209 to newMode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT. I'm a complete novice when it comes to terminal inputs so I don't know what the consequence of this change is, but figured I'd throw it out there in case this is a valid fix.

Navigating between Models

In Elm you can use an Application in order to switch between models/views (application).

For more complicated applications how would one do that in Bubbletea? In the views example the views effectively share a Model, but for more complex applications that's not feasible.

Unicode support

Hello i tried to input unicode but it is not recognized by bubbletea, Thank you.
go version go1.17 windows/amd64

image

image
image

type tuiModel struct {
	p string
}
func (m *tuiModel) Init() tui.Cmd {
	return func() tui.Msg {
		return nil
	}
}
func (m *tuiModel) Update(msg tui.Msg) (tui.Model, tui.Cmd) {
	switch msg := msg.(type) {
	case tui.KeyMsg:
		return m.handleKeyboardInput(msg)
	}
	return m, nil
}
func (m *tuiModel) View() string {
	return m.p
}
func (m *tuiModel) handleKeyboardInput(msg tui.KeyMsg) (tui.Model, tui.Cmd) {
	switch msg.String() {
	case "ctrl+c":
		return m, tui.Quit
	}
	m.p = m.p + fmt.Sprintf("\n%s (%+#v)", msg, msg)
	return m, nil
}

Multiple choices

Hi,

I'm looking in the doc but don't fully understand how to create multiple model and view in the same file.
like:
create your own drink:

~ choose two flavors:
[] orange
[] strawberrie
[] watermelon
[] apple

~ select adds:
[] candies
[] mixed colors
[] strange glass shape

Input is still read after program quits

If multiple tea.Programs are run in succession, each of them appears to set up a system signal handler (for catching SIGINT etc.), but does not clean it up after the program has quit without being interrupted this way. If you run n programs in succession that each want to catch "ctrl+c", for the n-th such program, you'll need to press Ctrl-C (n-1-m) times, where m is the number of previous programs that were interrupted. Of course, the interrupt-catching functionality should work independently of what has happened before.

WithOutput should receive an io.Writer

Currently, WithOutput receives an *os.File which makes it harder to write tests.

I do not see any reason for this not to be changed, as the output field in Program is already an io.Writer.

PS: I'm loving this library! ❤️

examples/spinners: Spinner not spinning

The spinners demo spinner stops spinning when changing the spinner. Should Update return spinner.Tick to get the new spinner spinning when changing the spinner?

diff --git a/examples/spinners/main.go b/examples/spinners/main.go
index e3488bd..165b453 100644
--- a/examples/spinners/main.go
+++ b/examples/spinners/main.go
@@ -57,14 +57,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
                                m.index = len(spinners) - 1
                        }
                        m.resetSpinner()
-                       return m, nil
+                       return m, spinner.Tick
                case "l", "right":
                        m.index++
                        if m.index >= len(spinners) {
                                m.index = 0
                        }
                        m.resetSpinner()
-                       return m, nil
+                       return m, spinner.Tick
                case "ctrl+c", "q":
                        return m, tea.Quit
                default:

Bit literals in mouse.go break build on pre-1.12

02a0509 broke builds on pre-1.12 environments.

/gocode/src/github.com/charmbracelet/bubbletea/mouse.go:70:16: syntax error: unexpected b0000_0100, expecting semicolon or newline or )

Any chance you could control this using build tags?

Mouse clickable buttons

Do you have an examples of making certain text a clickable button with keyboard or mouse? For example the dialogBox in the lipgloss example? How would you make those buttons clickable?

Thank you!

Allow both native text selection and mouse wheel scrolling

I tried using either WithMouseCellMotion or WithMouseAllMotion and they both seem to allow for scrolling a viewport with the mouse wheel but disable text selection/highlighting completely. Tried passing different flags to muesli's termenv to facilitate this, to no avail.

Would it be possible to allow both scrolling with mouse and text selection?

Alternate screen doesn't clear the terminal when running in GNU Screen

Steps to reproduce

  1. Run GNU Screen (I'm running it in an OSX Terminal)
  2. In the bubbletea repo, go run examples/fullscreen/main.go

Expected result: Screen is cleared and you only see the countdown message

Actual result: Your terminal prompt, history, and whatever else is still visible alongside the output of the program.

However, once the program exits, the contents of the terminal are cleared.

This bug does not happen in tmux or in a bare Terminal.

Screenshot

(I ran ls just before running the program, to illustrate how it writes over top of the existing terminal contents)

Screen Shot 2021-07-13 at 9 38 44 PM

Non-Interactive in Windows Shells

Issue

Running the example in the README in Windows is not working as expected (see screenshot).

Screenshot 2020-10-12 185102

Expected Behavior

  • Build and run the example in the README.
  • Update the cursor position in the view via the up, down, j, k, keys, etc.
  • Exit cleanly by pressing Ctrl+c or q

Actual Behavior

  • Cursor position does not update on key press, using any of up, down, j, k, enter, space
  • Pressing q does not quit the application.
  • Pressing Ctrl+c exits with an error code.

Environment

Windows 10 19041.508
Go 1.15.2
Terminal applications tested:

  • cmd
  • Powershell
  • Windows terminal with Powershell and cmd
  • Cmder with cmd, Powershell, cygwin

Other Notes

I was able to get the same example running as expected using WSL Ubuntu 20.04 with Windows Terminal.

The project looks exciting and I'm interested in diving deeper. Please let me know if there's any additional info I can provide.

Need examples on lipgloss integration

lipgloss looks great, but its example seems to indicate dynamic, updateable, switchable widget-based content, which is not the case at all. The example is in fact completely static, and none of the bubbletea examples have any kind of layout or focus switching, so it's unclear if a dynamic, switchable version of the lipgloss example is even possible, or how the two projects might integrate together.

Optimize refresh on remote server or slow network

On the device with slow refresh rate, every "View()" will cause a blink since it re-renders the whole area or fullscreen. Do you have any plans to only re-render the the part it changes in order to avoid those blinks? (just like Kindle devices, not every page-up/page-down will cause a whole refresh)

Nested components

Hi there,

first of all, sorry because I do not know if this is the place where people usually ask for information. I am currently developing a complex cli and I just want to use this framework to build it. My main idea was to nest components inside other complex components as javascript framework does such as vue, react, etc...

It is possible to to something like this?

TextInput component definition

type TextInput struct {
	textinput textinput.Model
	err       error
}

func (model TextInput) Init() tea.Cmd {
	return textinput.Blink
}

func (model TextInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
			return model, tea.Quit
		}

	case error:
		model.err = msg
		return model, nil
	}

	model.textinput, cmd = model.textinput.Update(msg)
	return model, cmd
}

func (model TextInput) View() string {
	return fmt.Sprintf(
		"?\n\n%s\n\n%s",
		model.textinput.View(),
		"(esc to quit)",
	) + "\n"
}

func NewTextInput(placeholder string) TextInput {
	textinput := textinput.NewModel()
	textinput.Placeholder = placeholder
	textinput.CharLimit = TEXT_INPUT_LIMIT
	textinput.Width = TEXT_INPUT_WIDTH
	textinput.Focus()

	return TextInput{textinput, nil}
}

View definition

type husky struct {
	name TextInput
	stun TextInput
	peer TextInput

	current tea.Model
	cursor  int
}

func (view husky) Init() tea.Cmd {
	return view.current.Init()
}

func (view husky) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

	if view.name.textinput.Value() == "" {
		return view.name.Update(msg)
	}

	if view.stun.textinput.Value() == "" {
		return view.name.Update(msg)
	}

	if view.peer.textinput.Value() == "" {
		return view.name.Update(msg)
	}

	return nil, nil
}

func (view husky) View() string {
	return view.current.View()
}

func Husky() husky {
	name := NewTextInput("name")
	stun := NewTextInput("stun")
	peer := NewTextInput("peer")
	return husky{name, stun, peer, name, 0}
}

Thanks in advance! 😄

Read input events instead of raw bytes in Windows (mouse support/window change events/canceling inputloop)

Disclaimer: This issue is meant to supersede #103 and #24. The proposed change would be based on #120. This issue is based on a comment I made in #103.

Windows supports reading INPUT_RECORDs with ReadConsoleInput. However, this means that the Windows input event parsing logic (which would then be based on INPUT_RECORD structs) would have to be decoupled from the Unix logic.

Changing the input handling to use INPUT_RECORDs would allow bubbletea to receive MOUSE_EVENT_RECORDS (closing #103). It also enables the usage of PeekConsoleInput to augments the WaitForMultipleObjects mechanism introduced in #120 in order to solve #24 for the Windows Terminal where #120 does not work reliably.

INPUT_RECORD can also be a WINDOW_BUFFER_SIZE_EVENT which sounds like a good replacement for SIGWINCH on Windows.

There is also already a PR for the Go standard library to add ReadConsoleInput but it has become a bit stale over the last few months. However, we probably need PeekConsoleInput to augments the WaitForMultipleObjects anyway so the implementation has to use windows.NewLazySystemDLL("kernel32.dll").NewProc("...").Call(...) anyway.

Race condition - flush vs. write

Hi, it seems there is a race condition in the core library?

WARNING: DATA RACE
Read at 0x00c0000ee0a0 by goroutine 11:
  bytes.(*Buffer).Len()
      /home/chlunde/opt/go/src/bytes/buffer.go:73 +0x64
  github.com/charmbracelet/bubbletea.(*renderer).flush()
      /home/chlunde/src/bubbletea/renderer.go:91 +0x45
  github.com/charmbracelet/bubbletea.(*renderer).listen()
      /home/chlunde/src/bubbletea/renderer.go:76 +0x185

Previous write at 0x00c0000ee0a0 by main goroutine:
  bytes.(*Buffer).Reset()
      /home/chlunde/opt/go/src/bytes/buffer.go:98 +0xf5
  github.com/charmbracelet/bubbletea.(*renderer).write()
      /home/chlunde/src/bubbletea/renderer.go:195 +0x13c
  github.com/charmbracelet/bubbletea.(*Program).Start()
      /home/chlunde/src/bubbletea/tea.go:330 +0xbbb
  main.main()
      /home/chlunde/src/bubbletea/examples/spinner/main.go:27 +0x229

The fix seems to be to acquire the mutex a bit earlier in flush:

diff --git a/renderer.go b/renderer.go
index e919801..4a3263b 100644
--- a/renderer.go
+++ b/renderer.go
@@ -88,6 +88,9 @@ func (r *renderer) listen() {
 
 // flush renders the buffer.
 func (r *renderer) flush() {
+       r.mtx.Lock()
+       defer r.mtx.Unlock()
+
        if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
                // Nothing to do
                return
@@ -112,9 +115,6 @@ func (r *renderer) flush() {
 
        out := new(bytes.Buffer)
 
-       r.mtx.Lock()
-       defer r.mtx.Unlock()
-
        // Clear any lines we painted in the last render.
        if r.linesRendered > 0 {
                for i := r.linesRendered - 1; i > 0; i-- {
Full log
chlunde@fedora ~/.../examples/spinner$ go build -race .
go: downloading github.com/muesli/termenv v0.7.4
go: downloading github.com/charmbracelet/bubbles v0.7.6
chlunde@fedora ~/.../examples/spinner$ ./spinner 
==================
WARNING: DATA RACE
Read at 0x00c0000ee0a0 by goroutine 11:
  bytes.(*Buffer).Len()
      /home/chlunde/opt/go/src/bytes/buffer.go:73 +0x64
  github.com/charmbracelet/bubbletea.(*renderer).flush()
      /home/chlunde/src/bubbletea/renderer.go:91 +0x45
  github.com/charmbracelet/bubbletea.(*renderer).listen()
      /home/chlunde/src/bubbletea/renderer.go:76 +0x185

Previous write at 0x00c0000ee0a0 by main goroutine:
  bytes.(*Buffer).Reset()
      /home/chlunde/opt/go/src/bytes/buffer.go:98 +0xf5
  github.com/charmbracelet/bubbletea.(*renderer).write()
      /home/chlunde/src/bubbletea/renderer.go:195 +0x13c
  github.com/charmbracelet/bubbletea.(*Program).Start()
      /home/chlunde/src/bubbletea/tea.go:330 +0xbbb
  main.main()
      /home/chlunde/src/bubbletea/examples/spinner/main.go:27 +0x229

Goroutine 11 (running) created at:
  github.com/charmbracelet/bubbletea.(*renderer).start()
      /home/chlunde/src/bubbletea/renderer.go:61 +0xc5
  github.com/charmbracelet/bubbletea.(*Program).Start()
      /home/chlunde/src/bubbletea/tea.go:246 +0x664
  main.main()
      /home/chlunde/src/bubbletea/examples/spinner/main.go:27 +0x229
==================
==================
WARNING: DATA RACE
Read at 0x00c0000ee0b8 by goroutine 11:
  bytes.(*Buffer).Len()
      /home/chlunde/opt/go/src/bytes/buffer.go:73 +0x8d
  github.com/charmbracelet/bubbletea.(*renderer).flush()
      /home/chlunde/src/bubbletea/renderer.go:91 +0x45
  github.com/charmbracelet/bubbletea.(*renderer).listen()
      /home/chlunde/src/bubbletea/renderer.go:76 +0x185

Previous write at 0x00c0000ee0b8 by main goroutine:
  bytes.(*Buffer).Reset()
      /home/chlunde/opt/go/src/bytes/buffer.go:99 +0x112
  github.com/charmbracelet/bubbletea.(*renderer).write()
      /home/chlunde/src/bubbletea/renderer.go:195 +0x13c
  github.com/charmbracelet/bubbletea.(*Program).Start()
      /home/chlunde/src/bubbletea/tea.go:330 +0xbbb
  main.main()
      /home/chlunde/src/bubbletea/examples/spinner/main.go:27 +0x229

Goroutine 11 (running) created at:
  github.com/charmbracelet/bubbletea.(*renderer).start()
      /home/chlunde/src/bubbletea/renderer.go:61 +0xc5
  github.com/charmbracelet/bubbletea.(*Program).Start()
      /home/chlunde/src/bubbletea/tea.go:246 +0x664
  main.main()
      /home/chlunde/src/bubbletea/examples/spinner/main.go:27 +0x229
==================
==================
WARNING: DATA RACE
Read at 0x00c0000c20c0 by goroutine 11:
  runtime.slicebytetostring()
      /home/chlunde/opt/go/src/runtime/string.go:80 +0x0
  bytes.(*Buffer).String()
      /home/chlunde/opt/go/src/bytes/buffer.go:65 +0x137
  github.com/charmbracelet/bubbletea.(*renderer).flush()
      /home/chlunde/src/bubbletea/renderer.go:91 +0x45
  github.com/charmbracelet/bubbletea.(*renderer).listen()
      /home/chlunde/src/bubbletea/renderer.go:76 +0x185

Previous write at 0x00c0000c20c0 by main goroutine:
  runtime.slicecopy()
      /home/chlunde/opt/go/src/runtime/slice.go:247 +0x0
  bytes.(*Buffer).WriteString()
      /home/chlunde/opt/go/src/bytes/buffer.go:186 +0x147
  github.com/charmbracelet/bubbletea.(*renderer).write()
      /home/chlunde/src/bubbletea/renderer.go:196 +0x164
  github.com/charmbracelet/bubbletea.(*Program).Start()
      /home/chlunde/src/bubbletea/tea.go:330 +0xbbb
  main.main()
      /home/chlunde/src/bubbletea/examples/spinner/main.go:27 +0x229

Goroutine 11 (running) created at:
  github.com/charmbracelet/bubbletea.(*renderer).start()
      /home/chlunde/src/bubbletea/renderer.go:61 +0xc5
  github.com/charmbracelet/bubbletea.(*Program).Start()
      /home/chlunde/src/bubbletea/tea.go:246 +0x664
  main.main()
      /home/chlunde/src/bubbletea/examples/spinner/main.go:27 +0x229
==================


   ⡿  Loading forever...press q to quit


Found 3 data race(s)

PowerShell windows resize

Hi,

I'm playing with the new Windows Terminal and PowerShell. And looks like something is wrong with resizing there.

Here is what I'm getting after resizing the terminal in llama.
Снимок экрана (3)

Development hot reload

During development, I'd like to be able to make changes to the code and then run the program in order to visually see the results. Of course it gets repetitive:

  1. Make changes, save file(s)
  2. Run program
  3. Repeat

I'd like then a tool that rolls up these steps into just one step: make changes, save file(s); tool automatically restarts program. Ideally the two would run side-by-side on my monitor.

For example, with web development, there is livereload.js which embeds some javascript in the website you're developing, and when you make changes the website page automatically reloads.

Whereas for my bubbletea-based program I've tried using:

But none of them support running the program with a TTY. The following issue describes the problem (although it concludes it's not an insurmountable problem):

eradman/entr#81

So has anyone found a way to support this particular development feedback loop for programs developed with bubbletea? Or perhaps I'm missing an alternative approach that works just as well?

Note: I appreciate this is no substitute for unit tests.

Spawning editors

Hi,

Thanks for the awesome project!

Question: how to properly start subcommand with reuse of Stdout/Stdin?

I'm doing it like so:
https://github.com/antonmedv/llama/blob/58c042eb06a5a661a1388beb4ac09cedf2764ad0/main.go#L183-L190

func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	if m.editMode {
		return m, nil
	}
				cmd := exec.Command(lookup([]string{"LLAMA_EDITOR", "EDITOR"}, "less"), filepath.Join(m.path, m.cursorFileName()))
				cmd.Stdin = os.Stdin
				cmd.Stdout = os.Stdout
				// Note: no Stderr as redirect `llama 2> /tmp/path` can be used.
				m.editMode = true
				_ = cmd.Run()
				m.editMode = false
				return m, tea.HideCursor

But what to do if a redraw is needed after cmd exits? And is there a better way to halt the program until cmd is exited?

Thanks.

Originally posted by @antonmedv in #170

Display issue when line content is greater than terminal width

Hi!
Thanks for this awesome library but I experience an issue with my tui when the displayed content is longer than the terminal width.
Here a gif:
issue_bubbletea

Maybe it's from my side, here my View() code for the main menu:

s = fmt.Sprintf("\n🦄 Welcome to %s, the (soon) most powerful and versatile %s bot!\n\n", keyword("IGopher"), keyword("Instagram"))

for i, choice := range m.homeScreen.choices {
	cursor := " "
	if m.homeScreen.cursor == i {
		cursor = cursorColor(">")
	}
	s += fmt.Sprintf("%s %s\n", cursor, choice)
}

s += subtle("\nup/down: select") + dot + subtle("enter: choose") + dot + subtle("ctrl+c: quit")
break

Btw all of my tui code is available here : tui.go
If you have any ideas

Question: How print output from TODO

I try it example and it's great library, but I can't figure out how you think we can work with output.

Example:

I can just add print for selected output:

for i := range m.selected {
  fmt.Printf("\n%s", m.choices[i])
}

I can put into update function to quit and get ugly output:

What should we buy at the market?

> [x] Buy carrots
  [ ] Buy celery
  [x] Buy kohlrabi

Press q to quit.

Buy kohlrabi
            Buy carrots%

Do I need extra view for that? Or is some simple way how get pass output via program interface to main function to continue for example with other program? I see example with Msg but look overcomplicated for just work with output from CLI.

Allow to change output Writer

Hi,
I've come across this library lately while doing som TUI stuff and I really like the approach here.
I'm using mostly Windows 10, but was trying to use it on Windows 7 and there is a bit of problem, because Win7 doesn't support ANSI characters.
In reality Windows 7 is out of support, so I guess it's no big deal (I will be upgrading too anyway), but I was wondering if it wouldn't be worth to try to add a function which could override the default output *os.File. Maybe it's not worth just for that, but could be used also in other cases, when you want to dump it in a file or logger with your specific Writer (even though, I'm not sure if that's worth it or useful at all).

What I did was, I tried to run it with some Program function like this p.SetOutput(colorable.NewColorableStdout()), where colorable is github.com/mattn/go-colorable package. It allows colors even on older version of Windows console (translating ANSI to API calls to write the output) and makes it more usable. It still doesn't resolve all the problems, but makes some basic stuff work.
For simplicity, I just split output to original os.File for inputs and io.Writer for output.

Anyway, this is more like a suggestion or a thing to think about. So it's up to you, if you find it worth it. I don't mind and will definitely use it either way :)

Tea with Alt screen gets messed up after openning sub command.

Hi,

I'm starting tea with:

p := tea.NewProgram(m, tea.WithOutput(os.Stderr))

Then in Update() call sub process:

				cmd := exec.Command("vim")
				cmd.Stdin = os.Stdin
				cmd.Stdout = os.Stderr // Render to stderr.
				m.editMode = true
				_ = cmd.Run()
				m.editMode = false
				return m, tea.HideCursor

After the exit of vim tea program all messed up and no alt screen in place.

Not sure if it is supported at all. Please, help me to debug. Trying to add AltScreen to https://github.com/antonmedv/llama.

Thanks! 🙏

Allow unmanaged console output

Using bubbletea, I would like to have a progress bar similar to apt's:
apt-progress-bar

Progress bar is updated and the above text does not get cleared. I could not achieve this in bubbletea. The closest attempt I got resulted in an unexpected error (see this cast. the code is a modification of examples/spinners/main.go).

I have also tried using bubbletea.ScrollDown() but it instantly clears the screen. Any ideas ?

Where to look for components?

Sorry for such a question but I'm kind of lost in the docs as to where I'm supposed to look for docs on components like the list item, buttons, labels etc?

I want to make something to make bulk transferring files over MTP (to Android phones) easier and I'd like to use your library because of its aesthetics but have no idea where to start.

Thanks.

Node-based rendering

Hello, I am trying to make a complex TUI application but unfortunately the single view model that bubbletea offers is severely limiting and only allows for the creation of linear CLI apps. Manipulating a string to add a popup window to the view has proven to be very difficult and inefficient. My proposed solution is to add child views.

Awesome project

Thank you for the awesome project, really look forward to using it more. You are free to close this issue, I just wanted to give my appreciation. Also, was curious if you had considered making project stickers?

Altscreen enter and exit messages

Right now the only way to enter and exit the altscreen (full window mode) is via Program.EnterAltScreen() and Program.ExitAltScreen(). This means that, without employing hacks, one can only enter and exit when starting and quitting programs. It probably makes sense to have library-level commands, similar to tea.Quit, for entering and exiting the altscreen to give Bubble Tea programs the opportunity to jump in and out of the altscreen in-program.

Injecting messages from outside the program loop

I'm not sure I haven't overlooked something, but I miss a way of injecting messages into a running tea.Program from outside the program's execution loop. Currently, programs only process messages returned from either Init() or Update() or implicitly created for keyboard events and system signals.

In scenarios where a TUI is used to show progress (spinner, progress bar), some concurrent progress will need to indicate progress and/or having finished to the program. If there was a way of injecting messages, this could be done cleanly; as things are now, one needs to resort to atomic fields or channels in the model — or have I misunderstood or overlooked something?

Automatically set correct color profile

I want to create something like fzf. The fzf renders to stderr, while returning content to stdour.
So next is possible (fzf will open interactive TUI):

fzf | cat

But termenv do this by default:

func ColorProfile() Profile {
	if !isatty.IsTerminal(os.Stdout.Fd()) {
		return Ascii
	}

	return colorProfile()
}

I know what I can manually call lipgloss.SetColorProfile, but it will be cool if creating stderr apps will be easy, and the next code just work:

p := tea.NewProgram(m, tea.WithOutput(os.Stderr))

But now I need to copy-paste colorProfile() logic to be able to detect the correct profile.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.