pax_global_header00006660000000000000000000000064141405347270014521gustar00rootroot0000000000000052 comment=a55bf775cd3fb2418d6ac3498a7792d773b8643e bubbletea-0.19.1/000077500000000000000000000000001414053472700135365ustar00rootroot00000000000000bubbletea-0.19.1/.github/000077500000000000000000000000001414053472700150765ustar00rootroot00000000000000bubbletea-0.19.1/.github/workflows/000077500000000000000000000000001414053472700171335ustar00rootroot00000000000000bubbletea-0.19.1/.github/workflows/build.yml000066400000000000000000000013501414053472700207540ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: build: strategy: matrix: go-version: [~1.13, ^1] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Download Go modules run: go mod download - name: Build run: go build -v ./... - name: Build examples run: go build -v ./... working-directory: ./examples - name: Build tutorials run: go build -v ./... working-directory: ./tutorials bubbletea-0.19.1/.github/workflows/coverage.yml000066400000000000000000000013211414053472700214460ustar00rootroot00000000000000name: coverage on: [push, pull_request] jobs: coverage: strategy: matrix: go-version: [^1] os: [ubuntu-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go test -race -covermode atomic -coverprofile=profile.cov ./... GO111MODULE=off go get github.com/mattn/goveralls $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github bubbletea-0.19.1/.github/workflows/lint.yml000066400000000000000000000010561414053472700206260ustar00rootroot00000000000000name: lint on: [push, pull_request] jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: # Optional: golangci-lint command line arguments. args: --issues-exit-code=0 # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true bubbletea-0.19.1/.gitignore000066400000000000000000000006541414053472700155330ustar00rootroot00000000000000.DS_Store .envrc examples/fullscreen/fullscreen examples/help/help examples/http/http examples/list-default/list-default examples/list-fancy/list-fancy examples/list-simple/list-simple examples/mouse/mouse examples/pager/pager examples/simple/simple examples/spinner/spinner examples/textinput/textinput examples/textinputs/textinputs examples/views/views tutorials/basics/basics tutorials/commands/commands .idea coverage.txt bubbletea-0.19.1/.golangci.yml000066400000000000000000000007121414053472700161220ustar00rootroot00000000000000run: tests: false issues: include: - EXC0001 - EXC0005 - EXC0011 - EXC0012 - EXC0013 max-issues-per-linter: 0 max-same-issues: 0 linters: enable: - bodyclose - dupl - exportloopref - goconst - godot - godox - goimports - goprintffuncname - gosec - ifshort - misspell - prealloc - revive - rowserrcheck - sqlclosecheck - unconvert - unparam - whitespace bubbletea-0.19.1/LICENSE000066400000000000000000000020631414053472700145440ustar00rootroot00000000000000MIT License Copyright (c) 2020 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bubbletea-0.19.1/README.md000066400000000000000000000270721414053472700150250ustar00rootroot00000000000000Bubble Tea ==========

Bubble Tea Title Treatment
Latest Release GoDoc Build Status

The fun, functional and stateful way to build terminal apps. A Go framework based on [The Elm Architecture][elm]. 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][examples], the [docs][docs] and some common [resources](#libraries-we-use-with-bubble-tea). ## By the way Be sure to check out [Bubbles][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][elm] which happens work nicely with Go. It's a delightful way to build applications. By the way, the non-annotated source code for this program is available [on GitHub](https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics). This tutorial assumes you have a working knowledge of Go. [elm]: https://guide.elm-lang.org/architecture/ ## 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. ```go 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. ```go 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. ```go func initialModel() model { return model{ // Our shopping 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." ```go 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. ```go 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 it's 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. ```go 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: ```go func main() { p := tea.NewProgram(initialModel()) if err := p.Start(); 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][cmd]. It's pretty simple. There are also several [Bubble Tea examples][examples] available and, of course, there are [Go Docs][docs]. [cmd]: http://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands/ [examples]: http://github.com/charmbracelet/bubbletea/tree/master/examples [docs]: https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc ## Libraries we use with Bubble Tea * [Bubbles][bubbles]: Common Bubble Tea components such as text inputs, viewports, spinners and so on * [Lip Gloss][lipgloss]: Style, format and layout tools for terminal applications * [Harmonica][harmonica]: A spring animation library for smooth, natural motion * [Termenv][termenv]: Advanced ANSI styling for terminal applications * [Reflow][reflow]: Advanced ANSI-aware methods for working with text [bubbles]: https://github.com/charmbracelet/bubbles [lipgloss]: https://github.com/charmbracelet/lipgloss [harmonica]: https://github.com/charmbracelet/harmonica [termenv]: https://github.com/muesli/termenv [reflow]: https://github.com/muesli/reflow ## Bubble Tea in the Wild For some Bubble Tea programs in production, see: * [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser and online markdown stash * [The Charm Tool](https://github.com/charmbracelet/charm): the Charm user account manager * [kboard](https://github.com/CamiloGarciaLaRotta/kboard): a typing game * [tasktimer](https://github.com/caarlos0/tasktimer): a dead-simple task timer * [fork-cleaner](https://github.com/caarlos0/fork-cleaner): cleans up old and inactive forks in your GitHub account * [STTG](https://github.com/wille1101/sttg): teletext client for SVT, Sweden’s national public television station * [gitflow-toolkit](https://github.com/mritd/gitflow-toolkit): a GitFlow submission tool * [ticker](https://github.com/achannarasappa/ticker): a terminal stock watcher and stock position tracker * [tz](https://github.com/oz/tz): an aid for scheduling across multiple time zones * [httpit](https://github.com/gonetx/httpit): a rapid http(s) benchmark tool * [gembro](https://git.sr.ht/~rafael/gembro): a mouse-driven Gemini browser * [fm](https://github.com/knipferrc/fm): a terminal-based file manager * [StormForge Optimize – Controller](https://github.com/thestormforge/optimize-controller): a tool for experimenting with application configurations in Kubernetes * [Slides](https://github.com/maaslalani/slides): a markdown-based presentation tool * [Typer](https://github.com/maaslalani/typer): a typing test * [AT CLI](https://github.com/daskycodes/at_cli): a utility to for executing AT Commands via serial port connections * [Canard](https://github.com/mrusme/canard): an RSS client * [sqlite-tui](https://github.com/mathaou/sqlite-tui): an keyboard and mouse driven SQLite database browser ## Feedback We'd love to hear your thoughts on this tutorial. Feel free to drop us a note! * [Twitter](https://twitter.com/charmcli) * [The Fediverse](https://mastodon.technology/@charm) ## Acknowledgments Bubble Tea is based on the paradigms of [The Elm Architecture][elm] by Evan Czaplicki et alia and the excellent [go-tea][gotea] by TJ Holowaychuk. [elm]: https://guide.elm-lang.org/architecture/ [gotea]: https://github.com/tj/go-tea ## License [MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE) *** Part of [Charm](https://charm.sh). The Charm logo Charm热爱开源 • Charm loves open source bubbletea-0.19.1/cancelreader.go000066400000000000000000000032071414053472700164770ustar00rootroot00000000000000package tea import ( "fmt" "io" "sync" ) var errCanceled = fmt.Errorf("read cancelled") // cancelReader is a io.Reader whose Read() calls can be cancelled without data // being consumed. The cancelReader has to be closed. type cancelReader interface { io.ReadCloser // Cancel cancels ongoing and future reads an returns true if it succeeded. Cancel() bool } // fallbackCancelReader implements cancelReader but does not actually support // cancelation during an ongoing Read() call. Thus, Cancel() always returns // false. However, after calling Cancel(), new Read() calls immediately return // errCanceled and don't consume any data anymore. type fallbackCancelReader struct { r io.Reader cancelled bool } // newFallbackCancelReader is a fallback for newCancelReader that cannot // actually cancel an ongoing read but will immediately return on future reads // if it has been cancelled. func newFallbackCancelReader(reader io.Reader) (cancelReader, error) { return &fallbackCancelReader{r: reader}, nil } func (r *fallbackCancelReader) Read(data []byte) (int, error) { if r.cancelled { return 0, errCanceled } return r.r.Read(data) } func (r *fallbackCancelReader) Cancel() bool { r.cancelled = true return false } func (r *fallbackCancelReader) Close() error { return nil } // cancelMixin represents a goroutine-safe cancelation status. type cancelMixin struct { unsafeCancelled bool lock sync.Mutex } func (c *cancelMixin) isCancelled() bool { c.lock.Lock() defer c.lock.Unlock() return c.unsafeCancelled } func (c *cancelMixin) setCancelled() { c.lock.Lock() defer c.lock.Unlock() c.unsafeCancelled = true } bubbletea-0.19.1/cancelreader_bsd.go000066400000000000000000000062021414053472700173250ustar00rootroot00000000000000// +build darwin freebsd netbsd openbsd // nolint:revive package tea import ( "errors" "fmt" "io" "os" "strings" "golang.org/x/sys/unix" ) // newkqueueCancelReader returns a reader and a cancel function. If the input reader // is an *os.File, the cancel function can be used to interrupt a blocking call // read call. In this case, the cancel function returns true if the call was // cancelled successfully. If the input reader is not a *os.File, the cancel // function does nothing and always returns false. The BSD and macOS // implementation is based on the kqueue mechanism. func newCancelReader(reader io.Reader) (cancelReader, error) { file, ok := reader.(*os.File) if !ok { return newFallbackCancelReader(reader) } // kqueue returns instantly when polling /dev/tty so fallback to select if file.Name() == "/dev/tty" { return newSelectCancelReader(reader) } kQueue, err := unix.Kqueue() if err != nil { return nil, fmt.Errorf("create kqueue: %w", err) } r := &kqueueCancelReader{ file: file, kQueue: kQueue, } r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe() if err != nil { return nil, err } unix.SetKevent(&r.kQueueEvents[0], int(file.Fd()), unix.EVFILT_READ, unix.EV_ADD) unix.SetKevent(&r.kQueueEvents[1], int(r.cancelSignalReader.Fd()), unix.EVFILT_READ, unix.EV_ADD) return r, nil } type kqueueCancelReader struct { file *os.File cancelSignalReader *os.File cancelSignalWriter *os.File cancelMixin kQueue int kQueueEvents [2]unix.Kevent_t } func (r *kqueueCancelReader) Read(data []byte) (int, error) { if r.isCancelled() { return 0, errCanceled } err := r.wait() if err != nil { if errors.Is(err, errCanceled) { // remove signal from pipe var b [1]byte _, errRead := r.cancelSignalReader.Read(b[:]) if errRead != nil { return 0, fmt.Errorf("reading cancel signal: %w", errRead) } } return 0, err } return r.file.Read(data) } func (r *kqueueCancelReader) Cancel() bool { r.setCancelled() // send cancel signal _, err := r.cancelSignalWriter.Write([]byte{'c'}) return err == nil } func (r *kqueueCancelReader) Close() error { var errMsgs []string // close kqueue err := unix.Close(r.kQueue) if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing kqueue: %v", err)) } // close pipe err = r.cancelSignalWriter.Close() if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err)) } err = r.cancelSignalReader.Close() if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err)) } if len(errMsgs) > 0 { return fmt.Errorf(strings.Join(errMsgs, ", ")) } return nil } func (r *kqueueCancelReader) wait() error { events := make([]unix.Kevent_t, 1) for { _, err := unix.Kevent(r.kQueue, r.kQueueEvents[:], events, nil) if errors.Is(err, unix.EINTR) { continue // try again if the syscall was interrupted } if err != nil { return fmt.Errorf("kevent: %w", err) } break } switch events[0].Ident { case uint64(r.file.Fd()): return nil case uint64(r.cancelSignalReader.Fd()): return errCanceled } return fmt.Errorf("unknown error") } bubbletea-0.19.1/cancelreader_default.go000066400000000000000000000006461414053472700202070ustar00rootroot00000000000000//go:build !darwin && !windows && !linux && !solaris && !freebsd && !netbsd && !openbsd // +build !darwin,!windows,!linux,!solaris,!freebsd,!netbsd,!openbsd package tea import ( "io" ) // newCancelReader returns a fallbackCancelReader that satisfies the // cancelReader but does not actually support cancelation. func newCancelReader(reader io.Reader) (cancelReader, error) { return newFallbackCancelReader(reader) } bubbletea-0.19.1/cancelreader_linux.go000066400000000000000000000062741414053472700177250ustar00rootroot00000000000000//go:build linux // +build linux // nolint:revive package tea import ( "errors" "fmt" "io" "os" "strings" "golang.org/x/sys/unix" ) // newCancelReader returns a reader and a cancel function. If the input reader // is an *os.File, the cancel function can be used to interrupt a blocking call // read call. In this case, the cancel function returns true if the call was // cancelled successfully. If the input reader is not a *os.File, the cancel // function does nothing and always returns false. The linux implementation is // based on the epoll mechanism. func newCancelReader(reader io.Reader) (cancelReader, error) { file, ok := reader.(*os.File) if !ok { return newFallbackCancelReader(reader) } epoll, err := unix.EpollCreate1(0) if err != nil { return nil, fmt.Errorf("create epoll: %w", err) } r := &epollCancelReader{ file: file, epoll: epoll, } r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe() if err != nil { return nil, err } err = unix.EpollCtl(epoll, unix.EPOLL_CTL_ADD, int(file.Fd()), &unix.EpollEvent{ Events: unix.EPOLLIN, Fd: int32(file.Fd()), }) if err != nil { return nil, fmt.Errorf("add reader to epoll interrest list") } err = unix.EpollCtl(epoll, unix.EPOLL_CTL_ADD, int(r.cancelSignalReader.Fd()), &unix.EpollEvent{ Events: unix.EPOLLIN, Fd: int32(r.cancelSignalReader.Fd()), }) if err != nil { return nil, fmt.Errorf("add reader to epoll interrest list") } return r, nil } type epollCancelReader struct { file *os.File cancelSignalReader *os.File cancelSignalWriter *os.File cancelMixin epoll int } func (r *epollCancelReader) Read(data []byte) (int, error) { if r.isCancelled() { return 0, errCanceled } err := r.wait() if err != nil { if errors.Is(err, errCanceled) { // remove signal from pipe var b [1]byte _, readErr := r.cancelSignalReader.Read(b[:]) if readErr != nil { return 0, fmt.Errorf("reading cancel signal: %w", readErr) } } return 0, err } return r.file.Read(data) } func (r *epollCancelReader) Cancel() bool { r.setCancelled() // send cancel signal _, err := r.cancelSignalWriter.Write([]byte{'c'}) return err == nil } func (r *epollCancelReader) Close() error { var errMsgs []string // close kqueue err := unix.Close(r.epoll) if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing epoll: %v", err)) } // close pipe err = r.cancelSignalWriter.Close() if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err)) } err = r.cancelSignalReader.Close() if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err)) } if len(errMsgs) > 0 { return fmt.Errorf(strings.Join(errMsgs, ", ")) } return nil } func (r *epollCancelReader) wait() error { events := make([]unix.EpollEvent, 1) for { _, err := unix.EpollWait(r.epoll, events, -1) if errors.Is(err, unix.EINTR) { continue // try again if the syscall was interrupted } if err != nil { return fmt.Errorf("kevent: %w", err) } break } switch events[0].Fd { case int32(r.file.Fd()): return nil case int32(r.cancelSignalReader.Fd()): return errCanceled } return fmt.Errorf("unknown error") } bubbletea-0.19.1/cancelreader_select.go000066400000000000000000000060461414053472700200420ustar00rootroot00000000000000//go:build solaris || darwin || freebsd || netbsd || openbsd // +build solaris darwin freebsd netbsd openbsd // nolint:revive package tea import ( "errors" "fmt" "io" "os" "strings" "golang.org/x/sys/unix" ) // newSelectCancelReader returns a reader and a cancel function. If the input // reader is an *os.File, the cancel function can be used to interrupt a // blocking call read call. In this case, the cancel function returns true if // the call was cancelled successfully. If the input reader is not a *os.File or // the file descriptor is 1024 or larger, the cancel function does nothing and // always returns false. The generic unix implementation is based on the posix // select syscall. func newSelectCancelReader(reader io.Reader) (cancelReader, error) { file, ok := reader.(*os.File) if !ok || file.Fd() >= unix.FD_SETSIZE { return newFallbackCancelReader(reader) } r := &selectCancelReader{file: file} var err error r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe() if err != nil { return nil, err } return r, nil } type selectCancelReader struct { file *os.File cancelSignalReader *os.File cancelSignalWriter *os.File cancelMixin } func (r *selectCancelReader) Read(data []byte) (int, error) { if r.isCancelled() { return 0, errCanceled } for { err := waitForRead(r.file, r.cancelSignalReader) if err != nil { if errors.Is(err, unix.EINTR) { continue // try again if the syscall was interrupted } if errors.Is(err, errCanceled) { // remove signal from pipe var b [1]byte _, readErr := r.cancelSignalReader.Read(b[:]) if readErr != nil { return 0, fmt.Errorf("reading cancel signal: %w", readErr) } } return 0, err } return r.file.Read(data) } } func (r *selectCancelReader) Cancel() bool { r.setCancelled() // send cancel signal _, err := r.cancelSignalWriter.Write([]byte{'c'}) return err == nil } func (r *selectCancelReader) Close() error { var errMsgs []string // close pipe err := r.cancelSignalWriter.Close() if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err)) } err = r.cancelSignalReader.Close() if err != nil { errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err)) } if len(errMsgs) > 0 { return fmt.Errorf(strings.Join(errMsgs, ", ")) } return nil } func waitForRead(reader *os.File, abort *os.File) error { readerFd := int(reader.Fd()) abortFd := int(abort.Fd()) maxFd := readerFd if abortFd > maxFd { maxFd = abortFd } // this is a limitation of the select syscall if maxFd >= unix.FD_SETSIZE { return fmt.Errorf("cannot select on file descriptor %d which is larger than 1024", maxFd) } fdSet := &unix.FdSet{} fdSet.Set(int(reader.Fd())) fdSet.Set(int(abort.Fd())) _, err := unix.Select(maxFd+1, fdSet, nil, nil, nil) if err != nil { return fmt.Errorf("select: %w", err) } if fdSet.IsSet(abortFd) { return errCanceled } if fdSet.IsSet(readerFd) { return nil } return fmt.Errorf("select returned without setting a file descriptor") } bubbletea-0.19.1/cancelreader_unix.go000066400000000000000000000012361414053472700175420ustar00rootroot00000000000000//go:build solaris // +build solaris // nolint:revive package tea import ( "io" ) // newCancelReader returns a reader and a cancel function. If the input reader // is an *os.File, the cancel function can be used to interrupt a blocking call // read call. In this case, the cancel function returns true if the call was // cancelled successfully. If the input reader is not a *os.File or the file // descriptor is 1024 or larger, the cancel function does nothing and always // returns false. The generic unix implementation is based on the posix select // syscall. func newCancelReader(reader io.Reader) (cancelReader, error) { return newSelectCancelReader(reader) } bubbletea-0.19.1/cancelreader_windows.go000066400000000000000000000152561414053472700202600ustar00rootroot00000000000000//go:build windows // +build windows package tea import ( "fmt" "io" "os" "syscall" "time" "unicode/utf16" "golang.org/x/sys/windows" ) var fileShareValidFlags uint32 = 0x00000007 // newCancelReader returns a reader and a cancel function. If the input reader // is an *os.File with the same file descriptor as os.Stdin, the cancel function // can be used to interrupt a blocking call read call. In this case, the cancel // function returns true if the call was cancelled successfully. If the input // reader is not a *os.File with the same file descriptor as os.Stdin, the // cancel function does nothing and always returns false. The Windows // implementation is based on WaitForMultipleObject with overlapping reads from // CONIN$. func newCancelReader(reader io.Reader) (cancelReader, error) { if f, ok := reader.(*os.File); !ok || f.Fd() != os.Stdin.Fd() { return newFallbackCancelReader(reader) } // it is neccessary to open CONIN$ (NOT windows.STD_INPUT_HANDLE) in // overlapped mode to be able to use it with WaitForMultipleObjects. conin, err := windows.CreateFile( &(utf16.Encode([]rune("CONIN$\x00"))[0]), windows.GENERIC_READ|windows.GENERIC_WRITE, fileShareValidFlags, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED, 0) if err != nil { return nil, fmt.Errorf("open CONIN$ in overlapping mode: %w", err) } resetConsole, err := prepareConsole(conin) if err != nil { return nil, fmt.Errorf("prepare console: %w", err) } // flush input, otherwise it can contain events which trigger // WaitForMultipleObjects but which ReadFile cannot read, resulting in an // un-cancelable read err = flushConsoleInputBuffer(conin) if err != nil { return nil, fmt.Errorf("flush console input buffer: %w", err) } cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil) if err != nil { return nil, fmt.Errorf("create stop event: %w", err) } return &winCancelReader{ conin: conin, cancelEvent: cancelEvent, resetConsole: resetConsole, blockingReadSignal: make(chan struct{}, 1), }, nil } type winCancelReader struct { conin windows.Handle cancelEvent windows.Handle cancelMixin resetConsole func() error blockingReadSignal chan struct{} } func (r *winCancelReader) Read(data []byte) (int, error) { if r.isCancelled() { return 0, errCanceled } err := r.wait() if err != nil { return 0, err } if r.isCancelled() { return 0, errCanceled } // windows.Read does not work on overlapping windows.Handles return r.readAsync(data) } // Cancel cancels ongoing and future Read() calls and returns true if the // cancelation of the ongoing Read() was successful. On Windows Terminal, // WaitForMultipleObjects sometimes immediately returns without input being // available. In this case, graceful cancelation is not possible and Cancel() // returns false. func (r *winCancelReader) Cancel() bool { r.setCancelled() select { case r.blockingReadSignal <- struct{}{}: err := windows.SetEvent(r.cancelEvent) if err != nil { return false } <-r.blockingReadSignal case <-time.After(100 * time.Millisecond): // Read() hangs in a GetOverlappedResult which is likely due to // WaitForMultipleObjects returning without input being available // so we cannot cancel this ongoing read. return false } return true } func (r *winCancelReader) Close() error { err := windows.CloseHandle(r.cancelEvent) if err != nil { return fmt.Errorf("closing cancel event handle: %w", err) } err = r.resetConsole() if err != nil { return err } err = windows.Close(r.conin) if err != nil { return fmt.Errorf("closing CONIN$") } return nil } func (r *winCancelReader) wait() error { event, err := windows.WaitForMultipleObjects([]windows.Handle{r.conin, r.cancelEvent}, false, windows.INFINITE) switch { case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2: if event == windows.WAIT_OBJECT_0+1 { return errCanceled } if event == windows.WAIT_OBJECT_0 { return nil } return fmt.Errorf("unexpected wait object is ready: %d", event-windows.WAIT_OBJECT_0) case windows.WAIT_ABANDONED <= event && event < windows.WAIT_ABANDONED+2: return fmt.Errorf("abandoned") case event == uint32(windows.WAIT_TIMEOUT): return fmt.Errorf("timeout") case event == windows.WAIT_FAILED: return fmt.Errorf("failed") default: return fmt.Errorf("unexpected error: %w", error(err)) } } // readAsync is neccessary to read from a windows.Handle in overlapping mode. func (r *winCancelReader) readAsync(data []byte) (int, error) { hevent, err := windows.CreateEvent(nil, 0, 0, nil) if err != nil { return 0, fmt.Errorf("create event: %w", err) } overlapped := windows.Overlapped{ HEvent: hevent, } var n uint32 err = windows.ReadFile(r.conin, data, &n, &overlapped) if err != nil && err != windows.ERROR_IO_PENDING { return int(n), err } r.blockingReadSignal <- struct{}{} err = windows.GetOverlappedResult(r.conin, &overlapped, &n, true) if err != nil { return int(n), nil } <-r.blockingReadSignal return int(n), nil } func prepareConsole(input windows.Handle) (reset func() error, err error) { var originalMode uint32 err = windows.GetConsoleMode(input, &originalMode) if err != nil { return nil, fmt.Errorf("get console mode: %w", err) } var newMode uint32 newMode &^= windows.ENABLE_ECHO_INPUT newMode &^= windows.ENABLE_LINE_INPUT newMode &^= windows.ENABLE_MOUSE_INPUT newMode &^= windows.ENABLE_WINDOW_INPUT newMode &^= windows.ENABLE_PROCESSED_INPUT newMode |= windows.ENABLE_EXTENDED_FLAGS newMode |= windows.ENABLE_INSERT_MODE newMode |= windows.ENABLE_QUICK_EDIT_MODE // Enabling virutal terminal input is necessary for processing certain // types of input like X10 mouse events and arrows keys with the current // bytes-based input reader. It does, however, prevent cancelReader from // being able to cancel input. The planned solution for this is to read // Windows events in a more native fashion, rather than the current simple // bytes-based input reader which works well on unix systems. newMode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT err = windows.SetConsoleMode(input, newMode) if err != nil { return nil, fmt.Errorf("set console mode: %w", err) } return func() error { err := windows.SetConsoleMode(input, originalMode) if err != nil { return fmt.Errorf("reset console mode: %w", err) } return nil }, nil } var ( modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procFlushConsoleInputBuffer = modkernel32.NewProc("FlushConsoleInputBuffer") ) func flushConsoleInputBuffer(consoleInput windows.Handle) error { r, _, e := syscall.Syscall(procFlushConsoleInputBuffer.Addr(), 1, uintptr(consoleInput), 0, 0) if r == 0 { return error(e) } return nil } bubbletea-0.19.1/commands.go000066400000000000000000000041311414053472700156650ustar00rootroot00000000000000package tea // Convenience commands. Not part of the Bubble Tea core, but potentially // handy. import ( "time" ) // Every is a command that ticks in sync with the system clock. So, if you // wanted to tick with the system clock every second, minute or hour you // could use this. It's also handy for having different things tick in sync. // // Because we're ticking with the system clock the tick will likely not run for // the entire specified duration. For example, if we're ticking for one minute // and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40 // seconds later. // // To produce the command, pass a duration and a function which returns // a message containing the time at which the tick occurred. // // type TickMsg time.Time // // cmd := Every(time.Second, func(t time.Time) Msg { // return TickMsg(t) // }) func Every(duration time.Duration, fn func(time.Time) Msg) Cmd { return func() Msg { n := time.Now() d := n.Truncate(duration).Add(duration).Sub(n) t := time.NewTimer(d) return fn(<-t.C) } } // Tick produces a command at an interval independent of the system clock at // the given duration. That is, the timer begins when precisely when invoked, // and runs for its entire duration. // // To produce the command, pass a duration and a function which returns // a message containing the time at which the tick occurred. // // type TickMsg time.Time // // cmd := Tick(time.Second, func(t time.Time) Msg { // return TickMsg(t) // }) func Tick(d time.Duration, fn func(time.Time) Msg) Cmd { return func() Msg { t := time.NewTimer(d) return fn(<-t.C) } } // Sequentially produces a command that sequentially executes the given // commands. // The Msg returned is the first non-nil message returned by a Cmd. // // func saveStateCmd() Msg { // if err := save(); err != nil { // return errMsg{err} // } // return nil // } // // cmd := Sequentially(saveStateCmd, Quit) // func Sequentially(cmds ...Cmd) Cmd { return func() Msg { for _, cmd := range cmds { if msg := cmd(); msg != nil { return msg } } return nil } } bubbletea-0.19.1/commands_test.go000066400000000000000000000025501414053472700167270ustar00rootroot00000000000000package tea import ( "fmt" "testing" "time" ) func TestEvery(t *testing.T) { expected := "every ms" msg := Every(time.Millisecond, func(t time.Time) Msg { return expected })() if expected != msg { t.Fatalf("expected a msg %v but got %v", expected, msg) } } func TestTick(t *testing.T) { expected := "tick" msg := Tick(time.Millisecond, func(t time.Time) Msg { return expected })() if expected != msg { t.Fatalf("expected a msg %v but got %v", expected, msg) } } func TestSequentially(t *testing.T) { var expectedErrMsg = fmt.Errorf("some err") var expectedStrMsg = "some msg" var nilReturnCmd = func() Msg { return nil } tests := []struct { name string cmds []Cmd expected Msg }{ { name: "all nil", cmds: []Cmd{nilReturnCmd, nilReturnCmd}, expected: nil, }, { name: "one error", cmds: []Cmd{ nilReturnCmd, func() Msg { return expectedErrMsg }, nilReturnCmd, }, expected: expectedErrMsg, }, { name: "some msg", cmds: []Cmd{ nilReturnCmd, func() Msg { return expectedStrMsg }, nilReturnCmd, }, expected: expectedStrMsg, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if msg := Sequentially(test.cmds...)(); msg != test.expected { t.Fatalf("expected a msg %v but got %v", test.expected, msg) } }) } } bubbletea-0.19.1/examples/000077500000000000000000000000001414053472700153545ustar00rootroot00000000000000bubbletea-0.19.1/examples/altscreen-toggle/000077500000000000000000000000001414053472700206135ustar00rootroot00000000000000bubbletea-0.19.1/examples/altscreen-toggle/main.go000066400000000000000000000024201414053472700220640ustar00rootroot00000000000000package main import ( "fmt" "os" tea "github.com/charmbracelet/bubbletea" "github.com/muesli/termenv" ) var ( color = termenv.ColorProfile().Color keyword = termenv.Style{}.Foreground(color("204")).Background(color("235")).Styled help = termenv.Style{}.Foreground(color("241")).Styled ) type model struct { altscreen bool quitting bool } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c", "esc": m.quitting = true return m, tea.Quit case " ": var cmd tea.Cmd if m.altscreen { cmd = tea.ExitAltScreen } else { cmd = tea.EnterAltScreen } m.altscreen = !m.altscreen return m, cmd } } return m, nil } func (m model) View() string { if m.quitting { return "Bye!\n" } const ( altscreenMode = " altscreen mode " inlineMode = " inline mode " ) var mode string if m.altscreen { mode = altscreenMode } else { mode = inlineMode } return fmt.Sprintf("\n\n You're in %s\n\n\n", keyword(mode)) + help(" space: switch modes • q: exit\n") } func main() { if err := tea.NewProgram(model{}).Start(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } } bubbletea-0.19.1/examples/countdown/000077500000000000000000000000001414053472700173745ustar00rootroot00000000000000bubbletea-0.19.1/examples/countdown/main.go000066400000000000000000000021451414053472700206510ustar00rootroot00000000000000package main import ( "fmt" "os" "time" tea "github.com/charmbracelet/bubbletea" ) var ( duration = time.Second * 10 interval = time.Millisecond ) func main() { m := model{ timeout: time.Now().Add(duration), } if err := tea.NewProgram(m).Start(); err != nil { fmt.Println("Oh no, it didn't work:", err) os.Exit(1) } } type tickMsg time.Time type model struct { timeout time.Time lastTick time.Time } func (m model) Init() tea.Cmd { return tick() } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": return m, tea.Quit } case tickMsg: t := time.Time(msg) if t.After(m.timeout) { return m, tea.Quit } m.lastTick = t return m, tick() } return m, nil } func (m model) View() string { t := m.timeout.Sub(m.lastTick).Milliseconds() secs := t / 1000 millis := t % 1000 / 10 return fmt.Sprintf("This program will quit in %02d:%02d\n", secs, millis) } func tick() tea.Cmd { return tea.Tick(time.Duration(interval), func(t time.Time) tea.Msg { return tickMsg(t) }) } bubbletea-0.19.1/examples/fullscreen/000077500000000000000000000000001414053472700175165ustar00rootroot00000000000000bubbletea-0.19.1/examples/fullscreen/main.go000066400000000000000000000016751414053472700210020ustar00rootroot00000000000000package main // A simple program that opens the alternate screen buffer then counts down // from 5 and then exits. import ( "fmt" "log" "time" tea "github.com/charmbracelet/bubbletea" ) type model int type tickMsg time.Time func main() { p := tea.NewProgram(model(5), tea.WithAltScreen()) if err := p.Start(); err != nil { log.Fatal(err) } } func (m model) Init() tea.Cmd { return tea.Batch(tick(), tea.EnterAltScreen) } func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) { switch msg := message.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": return m, tea.Quit } case tickMsg: m -= 1 if m <= 0 { return m, tea.Quit } return m, tick() } return m, nil } func (m model) View() string { return fmt.Sprintf("\n\n Hi. This program will exit in %d seconds...", m) } func tick() tea.Cmd { return tea.Tick(time.Second, func(t time.Time) tea.Msg { return tickMsg(t) }) } bubbletea-0.19.1/examples/glamour/000077500000000000000000000000001414053472700170225ustar00rootroot00000000000000bubbletea-0.19.1/examples/glamour/main.go000066400000000000000000000052501414053472700202770ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" ) const content = ` Today’s Menu | Name | Price | Notes | | --- | --- | --- | | Tsukemono | $2 | Just an appetizer | | Tomato Soup | $4 | Made with San Marzano tomatoes | | Okonomiyaki | $4 | Takes a few minutes to make | | Curry | $3 | We can add squash if you’d like | Seasonal Dishes | Name | Price | Notes | | --- | --- | --- | | Steamed bitter melon | $2 | Not so bitter | | Takoyaki | $3 | Fun to eat | | Winter squash | $3 | Today it's pumpkin | Desserts | Name | Price | Notes | | --- | --- | --- | | Dorayaki | $4 | Looks good on rabbits | | Banana Split | $5 | A classic | | Cream Puff | $3 | Pretty creamy! | All our dishes are made in-house by Karen, our chef. Most of our ingredients are from our garden or the fish market down the street. Some famous people that have eaten here lately: * [x] René Redzepi * [x] David Chang * [ ] Jiro Ono (maybe some day) Bon appétit! ` var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render type example struct { viewport viewport.Model } func newExample() (*example, error) { vp := viewport.Model{Width: 78, Height: 20} renderer, err := glamour.NewTermRenderer(glamour.WithStylePath("notty")) if err != nil { return nil, err } str, err := renderer.Render(content) if err != nil { return nil, err } vp.SetContent(str) return &example{ viewport: vp, }, nil } func (e example) Init() tea.Cmd { return nil } func (e example) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: e.viewport.Width = msg.Width return e, nil case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c", "esc": return e, tea.Quit default: vp, cmd := e.viewport.Update(msg) e.viewport = vp return e, cmd } default: return e, nil } } func (e example) View() string { return e.viewport.View() + e.helpView() } func (e example) helpView() string { return helpStyle("\n ↑/↓: Navigate • q: Quit\n") } func main() { model, err := newExample() if err != nil { fmt.Println("Could not initialize Bubble Tea model:", err) os.Exit(1) } if err := tea.NewProgram(model).Start(); err != nil { fmt.Println("Bummer, there's been an error:", err) os.Exit(1) } } bubbletea-0.19.1/examples/go.mod000066400000000000000000000010251414053472700164600ustar00rootroot00000000000000module examples go 1.13 require ( github.com/charmbracelet/bubbles v0.9.1-0.20210917202525-0ac5ecdf8170 github.com/charmbracelet/bubbletea v0.17.0 github.com/charmbracelet/glamour v0.3.0 github.com/charmbracelet/lipgloss v0.4.0 github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-isatty v0.0.13 github.com/mattn/go-runewidth v0.0.13 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.9.0 ) replace github.com/charmbracelet/bubbletea => ../ bubbletea-0.19.1/examples/go.sum000066400000000000000000000217161414053472700165160ustar00rootroot00000000000000github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg= github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/bubbles v0.9.1-0.20210917202525-0ac5ecdf8170 h1:6gFVr0CeUQ4JHScJlBY/jeflgN70PLZ/gqG6st99tdo= github.com/charmbracelet/bubbles v0.9.1-0.20210917202525-0ac5ecdf8170/go.mod h1:NWT/c+0rYEnYChz5qCyX4Lj6fDw9gGToh9EFJPajghU= github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc= github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw= github.com/charmbracelet/harmonica v0.1.0 h1:lFKeSd6OAckQ/CEzPVd2mqj+YMEubQ/3FM2IYY3xNm0= github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.3.0/go.mod h1:VkhdBS2eNAmRkTwRKLJCFhCOVkjntMusBDxv7TXahuk= github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA= github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microcosm-cc/bluemonday v1.0.6 h1:ZOvqHKtnx0fUpnbQm3m3zKFWE+DRC+XB1onh8JoEObE= github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.3 h1:37BdQwPx8VOSic8eDSWee6QL9mRpZRm9VJp/QugNrW0= github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= bubbletea-0.19.1/examples/help/000077500000000000000000000000001414053472700163045ustar00rootroot00000000000000bubbletea-0.19.1/examples/help/main.go000066400000000000000000000063531414053472700175660ustar00rootroot00000000000000package main import ( "fmt" "os" "strings" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // keyMap defines a set of keybindings. To work for help it must satisfy // key.Map. It could also very easily be a map[string]key.Binding. type keyMap struct { Up key.Binding Down key.Binding Left key.Binding Right key.Binding Help key.Binding Quit key.Binding } // ShortHelp returns keybindings to be shown in the mini help view. It's part // of the key.Map interface. func (k keyMap) ShortHelp() []key.Binding { return []key.Binding{k.Help, k.Quit} } // FullHelp returns keybindings for the expanded help view. It's part of the // key.Map interface. func (k keyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.Up, k.Down, k.Left, k.Right}, // first column {k.Help, k.Quit}, // second column } } var keys = keyMap{ Up: key.NewBinding( key.WithKeys("up", "k"), key.WithHelp("↑/k", "move up"), ), Down: key.NewBinding( key.WithKeys("down", "j"), key.WithHelp("↓/j", "move down"), ), Left: key.NewBinding( key.WithKeys("left", "h"), key.WithHelp("←/h", "move left"), ), Right: key.NewBinding( key.WithKeys("right", "l"), key.WithHelp("→/l", "move right"), ), Help: key.NewBinding( key.WithKeys("?"), key.WithHelp("?", "toggle help"), ), Quit: key.NewBinding( key.WithKeys("q", "esc", "ctrl+c"), key.WithHelp("q", "quit"), ), } type model struct { keys keyMap help help.Model inputStyle lipgloss.Style lastKey string quitting bool } func newModel() model { return model{ keys: keys, help: help.NewModel(), inputStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF75B7")), } } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: // If we set a width on the help menu it can it can gracefully truncate // its view as needed. m.help.Width = msg.Width case tea.KeyMsg: switch { case key.Matches(msg, m.keys.Up): m.lastKey = "↑" case key.Matches(msg, m.keys.Down): m.lastKey = "↓" case key.Matches(msg, m.keys.Left): m.lastKey = "←" case key.Matches(msg, m.keys.Right): m.lastKey = "→" case key.Matches(msg, m.keys.Help): m.help.ShowAll = !m.help.ShowAll case key.Matches(msg, m.keys.Quit): m.quitting = true return m, tea.Quit } } return m, nil } func (m model) View() string { if m.quitting { return "Bye!\n" } var status string if m.lastKey == "" { status = "Waiting for input..." } else { status = "You chose: " + m.inputStyle.Render(m.lastKey) } helpView := m.help.View(m.keys) height := 8 - strings.Count(status, "\n") - strings.Count(helpView, "\n") return "\n" + status + strings.Repeat("\n", height) + helpView } func main() { if os.Getenv("HELP_DEBUG") != "" { if f, err := tea.LogToFile("debug.log", "help"); err != nil { fmt.Println("Couldn't open a file for logging:", err) os.Exit(1) } else { defer f.Close() } } if err := tea.NewProgram(newModel()).Start(); err != nil { fmt.Printf("Could not start program :(\n%v\n", err) os.Exit(1) } } bubbletea-0.19.1/examples/http/000077500000000000000000000000001414053472700163335ustar00rootroot00000000000000bubbletea-0.19.1/examples/http/main.go000066400000000000000000000024511414053472700176100ustar00rootroot00000000000000package main // A simple program that makes a GET request and prints the response status. import ( "fmt" "log" "net/http" "time" tea "github.com/charmbracelet/bubbletea" ) const url = "https://charm.sh/" type model struct { status int err error } type statusMsg int type errMsg struct{ error } func (e errMsg) Error() string { return e.error.Error() } func main() { p := tea.NewProgram(model{}) if err := p.Start(); err != nil { log.Fatal(err) } } func (m model) Init() tea.Cmd { return checkServer } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c", "esc": return m, tea.Quit default: return m, nil } case statusMsg: m.status = int(msg) return m, tea.Quit case errMsg: m.err = msg return m, nil default: return m, nil } } func (m model) View() string { s := fmt.Sprintf("Checking %s...", url) if m.err != nil { s += fmt.Sprintf("something went wrong: %s", m.err) } else if m.status != 0 { s += fmt.Sprintf("%d %s", m.status, http.StatusText(m.status)) } return s + "\n" } func checkServer() tea.Msg { c := &http.Client{ Timeout: 10 * time.Second, } res, err := c.Get(url) if err != nil { return errMsg{err} } return statusMsg(res.StatusCode) } bubbletea-0.19.1/examples/list-default/000077500000000000000000000000001414053472700177515ustar00rootroot00000000000000bubbletea-0.19.1/examples/list-default/main.go000066400000000000000000000053051414053472700212270ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var docStyle = lipgloss.NewStyle().Margin(1, 2) type item struct { title, desc string } func (i item) Title() string { return i.title } func (i item) Description() string { return i.desc } func (i item) FilterValue() string { return i.title } type model struct { list list.Model } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "ctrl+c" { return m, nil } case tea.WindowSizeMsg: top, right, bottom, left := docStyle.GetMargin() m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom) } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd } func (m model) View() string { return docStyle.Render(m.list.View()) } func main() { items := []list.Item{ item{title: "Raspberry Pi’s", desc: "I have ’em all over my house"}, item{title: "Nutella", desc: "It's good on toast"}, item{title: "Bitter melon", desc: "It cools you down"}, item{title: "Nice socks", desc: "And by that I mean socks without holes"}, item{title: "Eight hours of sleep", desc: "I had this once"}, item{title: "Cats", desc: "Usually"}, item{title: "Plantasia, the album", desc: "My plants love it too"}, item{title: "Pour over coffee", desc: "It takes forever to make though"}, item{title: "VR", desc: "Virtual reality...what is there to say?"}, item{title: "Noguchi Lamps", desc: "Such pleasing organic forms"}, item{title: "Linux", desc: "Pretty much the best OS"}, item{title: "Business school", desc: "Just kidding"}, item{title: "Pottery", desc: "Wet clay is a great feeling"}, item{title: "Shampoo", desc: "Nothing like clean hair"}, item{title: "Table tennis", desc: "It’s surprisingly exhausting"}, item{title: "Milk crates", desc: "Great for packing in your extra stuff"}, item{title: "Afternoon tea", desc: "Especially the tea sandwich part"}, item{title: "Stickers", desc: "The thicker the vinyl the better"}, item{title: "20° Weather", desc: "Celsius, not Fahrenheit"}, item{title: "Warm light", desc: "Like around 2700 Kelvin"}, item{title: "The vernal equinox", desc: "The autumnal equinox is pretty good too"}, item{title: "Gaffer’s tape", desc: "Basically sticky fabric"}, item{title: "Terrycloth", desc: "In other words, towel fabric"}, } m := model{list: list.NewModel(items, list.NewDefaultDelegate(), 0, 0)} m.list.Title = "My Fave Things" p := tea.NewProgram(m) p.EnterAltScreen() if err := p.Start(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } } bubbletea-0.19.1/examples/list-fancy/000077500000000000000000000000001414053472700174255ustar00rootroot00000000000000bubbletea-0.19.1/examples/list-fancy/delegate.go000066400000000000000000000034271414053472700215340ustar00rootroot00000000000000package main import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { d := list.NewDefaultDelegate() d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { var title string if i, ok := m.SelectedItem().(item); ok { title = i.Title() } else { return nil } switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, keys.choose): return m.NewStatusMessage(statusMessageStyle("You chose " + title)) case key.Matches(msg, keys.remove): index := m.Index() m.RemoveItem(index) if len(m.Items()) == 0 { keys.remove.SetEnabled(false) } return m.NewStatusMessage(statusMessageStyle("Deleted " + title)) } } return nil } help := []key.Binding{keys.choose, keys.remove} d.ShortHelpFunc = func() []key.Binding { return help } d.FullHelpFunc = func() [][]key.Binding { return [][]key.Binding{help} } return d } type delegateKeyMap struct { choose key.Binding remove key.Binding } // Additional short help entries. This satisfies the help.KeyMap interface and // is entirely optional. func (d delegateKeyMap) ShortHelp() []key.Binding { return []key.Binding{ d.choose, d.remove, } } // Additional full help entries. This satisfies the help.KeyMap interface and // is entirely optional. func (d delegateKeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ { d.choose, d.remove, }, } } func newDelegateKeyMap() *delegateKeyMap { return &delegateKeyMap{ choose: key.NewBinding( key.WithKeys("enter"), key.WithHelp("enter", "choose"), ), remove: key.NewBinding( key.WithKeys("x", "backspace"), key.WithHelp("x", "delete"), ), } } bubbletea-0.19.1/examples/list-fancy/main.go000066400000000000000000000106521414053472700207040ustar00rootroot00000000000000package main import ( "fmt" "math/rand" "os" "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( appStyle = lipgloss.NewStyle().Padding(1, 2) titleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFDF5")). Background(lipgloss.Color("#25A065")). Padding(0, 1) statusMessageStyle = lipgloss.NewStyle(). Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). Render ) type item struct { title string description string } func (i item) Title() string { return i.title } func (i item) Description() string { return i.description } func (i item) FilterValue() string { return i.title } type listKeyMap struct { toggleSpinner key.Binding toggleTitleBar key.Binding toggleStatusBar key.Binding togglePagination key.Binding toggleHelpMenu key.Binding insertItem key.Binding } func newListKeyMap() *listKeyMap { return &listKeyMap{ insertItem: key.NewBinding( key.WithKeys("a"), key.WithHelp("a", "add item"), ), toggleSpinner: key.NewBinding( key.WithKeys("s"), key.WithHelp("s", "toggle spinner"), ), toggleTitleBar: key.NewBinding( key.WithKeys("T"), key.WithHelp("T", "toggle title"), ), toggleStatusBar: key.NewBinding( key.WithKeys("S"), key.WithHelp("S", "toggle status"), ), togglePagination: key.NewBinding( key.WithKeys("P"), key.WithHelp("P", "toggle pagination"), ), toggleHelpMenu: key.NewBinding( key.WithKeys("H"), key.WithHelp("H", "toggle help"), ), } } type model struct { list list.Model itemGenerator *randomItemGenerator keys *listKeyMap delegateKeys *delegateKeyMap } func newModel() model { var ( itemGenerator randomItemGenerator delegateKeys = newDelegateKeyMap() listKeys = newListKeyMap() ) // Make initial list of items const numItems = 24 items := make([]list.Item, numItems) for i := 0; i < numItems; i++ { items[i] = itemGenerator.next() } // Setup list delegate := newItemDelegate(delegateKeys) groceryList := list.NewModel(items, delegate, 0, 0) groceryList.Title = "Groceries" groceryList.Styles.Title = titleStyle groceryList.AdditionalFullHelpKeys = func() []key.Binding { return []key.Binding{ listKeys.toggleSpinner, listKeys.insertItem, listKeys.toggleTitleBar, listKeys.toggleStatusBar, listKeys.togglePagination, listKeys.toggleHelpMenu, } } return model{ list: groceryList, keys: listKeys, delegateKeys: delegateKeys, itemGenerator: &itemGenerator, } } func (m model) Init() tea.Cmd { return tea.EnterAltScreen } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: topGap, rightGap, bottomGap, leftGap := appStyle.GetPadding() m.list.SetSize(msg.Width-leftGap-rightGap, msg.Height-topGap-bottomGap) case tea.KeyMsg: // Don't match any of the keys below if we're actively filtering. if m.list.FilterState() == list.Filtering { break } switch { case key.Matches(msg, m.keys.toggleSpinner): cmd := m.list.ToggleSpinner() return m, cmd case key.Matches(msg, m.keys.toggleTitleBar): v := !m.list.ShowTitle() m.list.SetShowTitle(v) m.list.SetShowFilter(v) m.list.SetFilteringEnabled(v) return m, nil case key.Matches(msg, m.keys.toggleStatusBar): m.list.SetShowStatusBar(!m.list.ShowStatusBar()) return m, nil case key.Matches(msg, m.keys.togglePagination): m.list.SetShowPagination(!m.list.ShowPagination()) return m, nil case key.Matches(msg, m.keys.toggleHelpMenu): m.list.SetShowHelp(!m.list.ShowHelp()) return m, nil case key.Matches(msg, m.keys.insertItem): m.delegateKeys.remove.SetEnabled(true) newItem := m.itemGenerator.next() insCmd := m.list.InsertItem(0, newItem) statusCmd := m.list.NewStatusMessage(statusMessageStyle("Added " + newItem.Title())) return m, tea.Batch(insCmd, statusCmd) } } // This will also call our delegate's update function. newListModel, cmd := m.list.Update(msg) m.list = newListModel cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m model) View() string { return appStyle.Render(m.list.View()) } func main() { rand.Seed(time.Now().UTC().UnixNano()) if err := tea.NewProgram(newModel()).Start(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } } bubbletea-0.19.1/examples/list-fancy/randomitems.go000066400000000000000000000046011414053472700222770ustar00rootroot00000000000000package main import ( "math/rand" "sync" ) type randomItemGenerator struct { titles []string descs []string titleIndex int descIndex int mtx *sync.Mutex shuffle *sync.Once } func (r *randomItemGenerator) reset() { r.mtx = &sync.Mutex{} r.shuffle = &sync.Once{} r.titles = []string{ "Artichoke", "Baking Flour", "Bananas", "Barley", "Bean Sprouts", "Bitter Melon", "Black Cod", "Blood Orange", "Brown Sugar", "Cashew Apple", "Cashews", "Cat Food", "Coconut Milk", "Cucumber", "Curry Paste", "Currywurst", "Dill", "Dragonfruit", "Dried Shrimp", "Eggs", "Fish Cake", "Furikake", "Garlic", "Gherkin", "Ginger", "Granulated Sugar", "Grapefruit", "Green Onion", "Hazelnuts", "Heavy whipping cream", "Honey Dew", "Horseradish", "Jicama", "Kohlrabi", "Leeks", "Lentils", "Licorice Root", "Meyer Lemons", "Milk", "Molasses", "Muesli", "Nectarine", "Niagamo Root", "Nopal", "Nutella", "Oat Milk", "Oatmeal", "Olives", "Papaya", "Party Gherkin", "Peppers", "Persian Lemons", "Pickle", "Pineapple", "Plantains", "Pocky", "Powdered Sugar", "Quince", "Radish", "Ramps", "Star Anise", "Sweet Potato", "Tamarind", "Unsalted Butter", "Watermelon", "Weißwurst", "Yams", "Yeast", "Yuzu", "Snow Peas", } r.descs = []string{ "A little weird", "Bold flavor", "Can’t get enough", "Delectable", "Expensive", "Expired", "Exquisite", "Fresh", "Gimme", "In season", "Kind of spicy", "Looks fresh", "Looks good to me", "Maybe not", "My favorite", "Oh my", "On sale", "Organic", "Questionable", "Really fresh", "Refreshing", "Salty", "Scrumptious", "Delectable", "Slightly sweet", "Smells great", "Tasty", "Too ripe", "At last", "What?", "Wow", "Yum", "Maybe", "Sure, why not?", } r.shuffle.Do(func() { shuf := func(x []string) { rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] }) } shuf(r.titles) shuf(r.descs) }) } func (r *randomItemGenerator) next() item { if r.mtx == nil { r.reset() } r.mtx.Lock() defer r.mtx.Unlock() i := item{ title: r.titles[r.titleIndex], description: r.descs[r.descIndex], } r.titleIndex++ if r.titleIndex >= len(r.titles) { r.titleIndex = 0 } r.descIndex++ if r.descIndex >= len(r.descs) { r.descIndex = 0 } return i } bubbletea-0.19.1/examples/list-simple/000077500000000000000000000000001414053472700176165ustar00rootroot00000000000000bubbletea-0.19.1/examples/list-simple/main.go000066400000000000000000000057151414053472700211010ustar00rootroot00000000000000package main import ( "fmt" "io" "os" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) const listHeight = 14 var ( titleStyle = lipgloss.NewStyle().MarginLeft(2) itemStyle = lipgloss.NewStyle().PaddingLeft(4) selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) ) type item string func (i item) FilterValue() string { return string(i) } type itemDelegate struct{} func (d itemDelegate) Height() int { return 1 } func (d itemDelegate) Spacing() int { return 0 } func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { i, ok := listItem.(item) if !ok { return } str := fmt.Sprintf("%d. %s", index+1, i) fn := itemStyle.Render if index == m.Index() { fn = func(s string) string { return selectedItemStyle.Render("> " + s) } } fmt.Fprintf(w, fn(str)) } type model struct { list list.Model items []item choice string quitting bool } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.list.SetWidth(msg.Width) return m, nil case tea.KeyMsg: switch keypress := msg.String(); keypress { case "ctrl+c": m.quitting = true return m, tea.Quit case "enter": i, ok := m.list.SelectedItem().(item) if ok { m.choice = string(i) } return m, tea.Quit default: if !m.list.SettingFilter() && (keypress == "q" || keypress == "esc") { m.quitting = true return m, tea.Quit } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd } default: return m, nil } } func (m model) View() string { if m.choice != "" { return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice)) } if m.quitting { return quitTextStyle.Render("Not hungry? That’s cool.") } return "\n" + m.list.View() } func main() { items := []list.Item{ item("Ramen"), item("Tomato Soup"), item("Hamburgers"), item("Cheeseburgers"), item("Currywurst"), item("Okonomiyaki"), item("Pasta"), item("Fillet Mignon"), item("Caviar"), item("Just Wine"), } const defaultWidth = 20 l := list.NewModel(items, itemDelegate{}, defaultWidth, listHeight) l.Title = "What do you want for dinner?" l.SetShowStatusBar(false) l.SetFilteringEnabled(false) l.Styles.Title = titleStyle l.Styles.PaginationStyle = paginationStyle l.Styles.HelpStyle = helpStyle m := model{list: l} if err := tea.NewProgram(m).Start(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } } bubbletea-0.19.1/examples/mouse/000077500000000000000000000000001414053472700165045ustar00rootroot00000000000000bubbletea-0.19.1/examples/mouse/main.go000066400000000000000000000016541414053472700177650ustar00rootroot00000000000000package main // A simple program that opens the alternate screen buffer and displays mouse // coordinates and events. import ( "fmt" "log" tea "github.com/charmbracelet/bubbletea" ) func main() { p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseAllMotion()) if err := p.Start(); err != nil { log.Fatal(err) } } type model struct { init bool mouseEvent tea.MouseEvent } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if s := msg.String(); s == "ctrl+c" || s == "q" || s == "esc" { return m, tea.Quit } case tea.MouseMsg: m.init = true m.mouseEvent = tea.MouseEvent(msg) } return m, nil } func (m model) View() string { s := "Do mouse stuff. When you're done press q to quit.\n\n" if m.init { e := m.mouseEvent s += fmt.Sprintf("(X: %d, Y: %d) %s", e.X, e.Y, e) } return s } bubbletea-0.19.1/examples/pager/000077500000000000000000000000001414053472700164525ustar00rootroot00000000000000bubbletea-0.19.1/examples/pager/artichoke.md000066400000000000000000000031021414053472700207410ustar00rootroot00000000000000Glow ==== A casual introduction. 你好世界! ## Let’s talk about artichokes The _artichoke_ is mentioned as a garden plant in the 8th century BC by Homer **and** Hesiod. The naturally occurring variant of the artichoke, the cardoon, which is native the the Mediterranean area, also has records of use as a food among the ancient Greeks and Romans. Pliny the Elder mentioned growing of _carduus_ in Carthage and Cordoba. > He holds him with a skinny hand, > ‘There was a ship,’ quoth he. > ‘Hold off! unhand me, grey-beard loon!’ > An artichoke, dropt he. --Samuel Taylor Coleridge, [The Rime of the Ancient Mariner][rime] [rime]: https://poetryfoundation.org/poems/43997/ ## Other foods worth mentioning 1. Carrots 1. Celery 1. Tacos * Soft * Hard 1. Cucumber ## Things to eat today * [x] Carrots * [x] Ramen * [ ] Currywurst ### Power levels of the aforementioned foods | Name | Power | Comment | | --- | --- | --- | | Carrots | 9001 | It’s over 9000?! | | Ramen | 9002 | Also over 9000?! | | Currywurst | 10000 | What?! | ## Currying Artichokes Here’s a bit of code in [Haskell](https://haskell.org), because we are fancy. Remember that to compile Haskell you’ll need `ghc`. ```haskell module Main where import Data.Function ( (&) ) import Data.List ( intercalculate ) hello :: String -> String hello s = "Hello, " ++ s ++ "." main :: IO () main = map hello [ "artichoke", "alcachofa" ] & intercalculate "\n" & putStrLn ``` *** _Alcachofa_, if you were wondering, is artichoke in Spanish. bubbletea-0.19.1/examples/pager/main.go000066400000000000000000000106131414053472700177260ustar00rootroot00000000000000package main // An example program demonstrating the pager component from the Bubbles // component library. import ( "fmt" "io/ioutil" "os" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/mattn/go-runewidth" ) const ( // You generally won't need this unless you're processing stuff with some // pretty complicated ANSI escape sequences. Turn it on if you notice // flickering. // // Also note that high performance rendering only works for programs that // use the full size of the terminal. We're enabling that below with // tea.EnterAltScreen(). useHighPerformanceRenderer = false headerHeight = 3 footerHeight = 3 ) func main() { // Load some text to render content, err := ioutil.ReadFile("artichoke.md") if err != nil { fmt.Println("could not load file:", err) os.Exit(1) } // Set PAGER_LOG to a path to log to a file. For example: // // export PAGER_LOG=debug.log // // This becomes handy when debugging stuff since you can't debug to stdout // because the UI is occupying it! path := os.Getenv("PAGER_LOG") if path != "" { f, err := tea.LogToFile(path, "pager") if err != nil { fmt.Printf("Could not open file %s: %v", path, err) os.Exit(1) } defer f.Close() } p := tea.NewProgram( model{content: string(content)}, // Use the full size of the terminal in its "alternate screen buffer" tea.WithAltScreen(), // Also turn on mouse support so we can track the mouse wheel tea.WithMouseCellMotion(), ) if err := p.Start(); err != nil { fmt.Println("could not run program:", err) os.Exit(1) } } type model struct { content string ready bool viewport viewport.Model } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) switch msg := msg.(type) { case tea.KeyMsg: if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" { return m, tea.Quit } case tea.WindowSizeMsg: verticalMargins := headerHeight + footerHeight if !m.ready { // Since this program is using the full size of the viewport we need // to wait until we've received the window dimensions before we // can initialize the viewport. The initial dimensions come in // quickly, though asynchronously, which is why we wait for them // here. m.viewport = viewport.Model{Width: msg.Width, Height: msg.Height - verticalMargins} m.viewport.HighPerformanceRendering = useHighPerformanceRenderer m.viewport.SetContent(m.content) m.ready = true // This is only necessary for high performance rendering, which in // most cases you won't need. // // Render the viewport one line below the header. m.viewport.YPosition = headerHeight + 1 } else { m.viewport.Width = msg.Width m.viewport.Height = msg.Height - verticalMargins } if useHighPerformanceRenderer { // Render (or re-render) the whole viewport. Necessary both to // initialize the viewport and when the window is resized. // // This is needed for high-performance rendering only. cmds = append(cmds, viewport.Sync(m.viewport)) } } // Because we're using the viewport's default update function (with pager- // style navigation) it's important that the viewport's update function: // // * Receives messages from the Bubble Tea runtime // * Returns commands to the Bubble Tea runtime // m.viewport, cmd = m.viewport.Update(msg) if useHighPerformanceRenderer { cmds = append(cmds, cmd) } return m, tea.Batch(cmds...) } func (m model) View() string { if !m.ready { return "\n Initializing..." } headerTop := "╭───────────╮" headerMid := "│ Mr. Pager ├" headerBot := "╰───────────╯" headerMid += strings.Repeat("─", m.viewport.Width-runewidth.StringWidth(headerMid)) header := fmt.Sprintf("%s\n%s\n%s", headerTop, headerMid, headerBot) footerTop := "╭──────╮" footerMid := fmt.Sprintf("┤ %3.f%% │", m.viewport.ScrollPercent()*100) footerBot := "╰──────╯" gapSize := m.viewport.Width - runewidth.StringWidth(footerMid) footerTop = strings.Repeat(" ", gapSize) + footerTop footerMid = strings.Repeat("─", gapSize) + footerMid footerBot = strings.Repeat(" ", gapSize) + footerBot footer := fmt.Sprintf("%s\n%s\n%s", footerTop, footerMid, footerBot) return fmt.Sprintf("%s\n%s\n%s", header, m.viewport.View(), footer) } bubbletea-0.19.1/examples/paginator/000077500000000000000000000000001414053472700173405ustar00rootroot00000000000000bubbletea-0.19.1/examples/paginator/main.go000066400000000000000000000031541414053472700206160ustar00rootroot00000000000000package main // A simple program demonstrating the paginator component from the Bubbles // component library. import ( "fmt" "log" "strings" "github.com/charmbracelet/bubbles/paginator" "github.com/charmbracelet/lipgloss" tea "github.com/charmbracelet/bubbletea" ) func newModel() model { var items []string for i := 1; i < 101; i++ { text := fmt.Sprintf("Item %d", i) items = append(items, text) } p := paginator.NewModel() p.Type = paginator.Dots p.PerPage = 10 p.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•") p.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•") p.SetTotalPages(len(items)) return model{ paginator: p, items: items, } } type model struct { items []string paginator paginator.Model } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": return m, tea.Quit } } m.paginator, cmd = m.paginator.Update(msg) return m, cmd } func (m model) View() string { var b strings.Builder b.WriteString("\n Paginator Example\n\n") start, end := m.paginator.GetSliceBounds(len(m.items)) for _, item := range m.items[start:end] { b.WriteString(" • " + item + "\n\n") } b.WriteString(" " + m.paginator.View()) b.WriteString("\n\n h/l ←/→ page • q: quit\n") return b.String() } func main() { p := tea.NewProgram(newModel()) if err := p.Start(); err != nil { log.Fatal(err) } } bubbletea-0.19.1/examples/pipe/000077500000000000000000000000001414053472700163115ustar00rootroot00000000000000bubbletea-0.19.1/examples/pipe/main.go000066400000000000000000000034341414053472700175700ustar00rootroot00000000000000package main // An example illustating how to pipe in data to a Bubble Tea application. // Moreso, this serves as proof that Bubble Tea will automatically listen for // keystrokes when input is not a TTY, such as when data is piped or redirected // in. import ( "bufio" "fmt" "io" "os" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) func main() { stat, err := os.Stdin.Stat() if err != nil { panic(err) } if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 { fmt.Println("Try piping in some text.") os.Exit(1) } reader := bufio.NewReader(os.Stdin) var b strings.Builder for { r, _, err := reader.ReadRune() if err != nil && err == io.EOF { break } _, err = b.WriteRune(r) if err != nil { fmt.Println("Error getting input:", err) os.Exit(1) } } model := newModel(strings.TrimSpace(b.String())) if err := tea.NewProgram(model).Start(); err != nil { fmt.Println("Couldn't start program:", err) os.Exit(1) } } type model struct { userInput textinput.Model } func newModel(initialValue string) (m model) { i := textinput.NewModel() i.Prompt = "" i.CursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) i.Width = 48 i.SetValue(initialValue) i.CursorEnd() i.Focus() m.userInput = i return } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.Type { case tea.KeyCtrlC, tea.KeyEscape, tea.KeyEnter: return m, tea.Quit } } var cmd tea.Cmd m.userInput, cmd = m.userInput.Update(msg) return m, cmd } func (m model) View() string { return fmt.Sprintf( "\nYou piped in: %s\n\nPress ^C to exit", m.userInput.View(), ) } bubbletea-0.19.1/examples/progress-animated/000077500000000000000000000000001414053472700210005ustar00rootroot00000000000000bubbletea-0.19.1/examples/progress-animated/main.go000066400000000000000000000037631414053472700222640ustar00rootroot00000000000000package main // A simple example that shows how to render an animated progress bar. In this // example we bump the progress by 25% every two seconds, animating our // progress bar to its new target state. // // It's also possible to render a progress bar in a more static fashion without // transitions. For details on that approach see the progress-static example. import ( "fmt" "os" "strings" "time" "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) const ( padding = 2 maxWidth = 80 ) var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render func main() { m := model{ progress: progress.NewModel(progress.WithDefaultGradient()), } if err := tea.NewProgram(m).Start(); err != nil { fmt.Println("Oh no!", err) os.Exit(1) } } type tickMsg time.Time type model struct { progress progress.Model } func (_ model) Init() tea.Cmd { return tickCmd() } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: return m, tea.Quit case tea.WindowSizeMsg: m.progress.Width = msg.Width - padding*2 - 4 if m.progress.Width > maxWidth { m.progress.Width = maxWidth } return m, nil case tickMsg: if m.progress.Percent() == 1.0 { return m, tea.Quit } // Note that you can also use progress.Model.SetPercent to set the // percentage value explicitly, too. cmd := m.progress.IncrPercent(0.25) return m, tea.Batch(tickCmd(), cmd) // FrameMsg is sent when the progress bar wants to animate itself case progress.FrameMsg: progressModel, cmd := m.progress.Update(msg) m.progress = progressModel.(progress.Model) return m, cmd default: return m, nil } } func (e model) View() string { pad := strings.Repeat(" ", padding) return "\n" + pad + e.progress.View() + "\n\n" + pad + helpStyle("Press any key to quit") } func tickCmd() tea.Cmd { return tea.Tick(time.Second*1, func(t time.Time) tea.Msg { return tickMsg(t) }) } bubbletea-0.19.1/examples/progress-static/000077500000000000000000000000001414053472700205055ustar00rootroot00000000000000bubbletea-0.19.1/examples/progress-static/main.go000066400000000000000000000040031414053472700217550ustar00rootroot00000000000000package main // A simple example that shows how to render a progress bar in a "pure" // fashion. In this example we bump the progress by 25% every second, // maintaining the progress state on our top level model using the progress bar // model's ViewAs method only for rendering. // // The signature for ViewAs is: // // func (m Model) ViewAs(percent float64) string // // So it takes a float between 0 and 1, and renders the progress bar // accordingly. When using the progress bar in this "pure" fashion and there's // no need to call an Update method. // // The progress bar is also able to animate itself, however. For details see // the progress-animated example. import ( "fmt" "os" "strings" "time" "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) const ( padding = 2 maxWidth = 80 ) var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render func main() { prog := progress.NewModel(progress.WithScaledGradient("#FF7CCB", "#FDFF8C")) if err := tea.NewProgram(model{progress: prog}).Start(); err != nil { fmt.Println("Oh no!", err) os.Exit(1) } } type tickMsg time.Time type model struct { percent float64 progress progress.Model } func (_ model) Init() tea.Cmd { return tickCmd() } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: return m, tea.Quit case tea.WindowSizeMsg: m.progress.Width = msg.Width - padding*2 - 4 if m.progress.Width > maxWidth { m.progress.Width = maxWidth } return m, nil case tickMsg: m.percent += 0.25 if m.percent > 1.0 { m.percent = 1.0 return m, tea.Quit } return m, tickCmd() default: return m, nil } } func (e model) View() string { pad := strings.Repeat(" ", padding) return "\n" + pad + e.progress.ViewAs(e.percent) + "\n\n" + pad + helpStyle("Press any key to quit") } func tickCmd() tea.Cmd { return tea.Tick(time.Second, func(t time.Time) tea.Msg { return tickMsg(t) }) } bubbletea-0.19.1/examples/realtime/000077500000000000000000000000001414053472700171565ustar00rootroot00000000000000bubbletea-0.19.1/examples/realtime/main.go000066400000000000000000000042211414053472700204300ustar00rootroot00000000000000package main // A simple example that shows how to send activity to Bubble Tea in real-time // through a channel. import ( "fmt" "math/rand" "os" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" ) // A message used to indicate that activity has occurred. In the real world (for // example, chat) this would contain actual data. type responseMsg struct{} // Simulate a process that sends events at an irregular interval in real time. // In this case, we'll send events on the channel at a random interval between // 100 to 1000 milliseconds. As a command, Bubble Tea will run this // asynchronously. func listenForActivity(sub chan struct{}) tea.Cmd { return func() tea.Msg { for { time.Sleep(time.Millisecond * time.Duration(rand.Int63n(900)+100)) sub <- struct{}{} } } } // A command that waits for the activity on a channel. func waitForActivity(sub chan struct{}) tea.Cmd { return func() tea.Msg { return responseMsg(<-sub) } } type model struct { sub chan struct{} // where we'll receive activity notifications responses int // how many responses we've received spinner spinner.Model quitting bool } func (m model) Init() tea.Cmd { return tea.Batch( spinner.Tick, listenForActivity(m.sub), // generate activity waitForActivity(m.sub), // wait for activity ) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.(type) { case tea.KeyMsg: m.quitting = true return m, tea.Quit case responseMsg: m.responses++ // record external activity return m, waitForActivity(m.sub) // wait for next event case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } func (m model) View() string { s := fmt.Sprintf("\n %s Events received: %d\n\n Press any key to exit\n", m.spinner.View(), m.responses) if m.quitting { s += "\n" } return s } func main() { rand.Seed(time.Now().UTC().UnixNano()) p := tea.NewProgram(model{ sub: make(chan struct{}), spinner: spinner.NewModel(), }) if p.Start() != nil { fmt.Println("could not start program") os.Exit(1) } } bubbletea-0.19.1/examples/result/000077500000000000000000000000001414053472700166725ustar00rootroot00000000000000bubbletea-0.19.1/examples/result/main.go000066400000000000000000000034771414053472700201600ustar00rootroot00000000000000package main // A simple example that shows how to retrieve a value from a Bubble Tea // program after the Bubble Tea has exited. // // Thanks to Treilik for this one. import ( "fmt" "os" "strings" tea "github.com/charmbracelet/bubbletea" ) var choices = []string{"Taro", "Coffee", "Lychee"} type model struct { cursor int choice chan string } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": close(m.choice) // If we're quitting just close the channel. return m, tea.Quit case "enter": // Send the choice on the channel and exit. m.choice <- choices[m.cursor] return m, tea.Quit case "down", "j": m.cursor++ if m.cursor >= len(choices) { m.cursor = 0 } case "up", "k": m.cursor-- if m.cursor < 0 { m.cursor = len(choices) - 1 } } } return m, nil } func (m model) View() string { s := strings.Builder{} s.WriteString("What kind of Bubble Tea would you like to order?\n\n") for i := 0; i < len(choices); i++ { if m.cursor == i { s.WriteString("(•) ") } else { s.WriteString("( ) ") } s.WriteString(choices[i]) s.WriteString("\n") } s.WriteString("\n(press q to quit)\n") return s.String() } func main() { // This is where we'll listen for the choice the user makes in the Bubble // Tea program. result := make(chan string, 1) // Pass the channel to the initialize function so our Bubble Tea program // can send the final choice along when the time comes. p := tea.NewProgram(model{cursor: 0, choice: result}) if err := p.Start(); err != nil { fmt.Println("Oh no:", err) os.Exit(1) } // Print out the final choice. if r := <-result; r != "" { fmt.Printf("\n---\nYou chose %s!\n", r) } } bubbletea-0.19.1/examples/send-msg/000077500000000000000000000000001414053472700170715ustar00rootroot00000000000000bubbletea-0.19.1/examples/send-msg/main.go000066400000000000000000000057221414053472700203520ustar00rootroot00000000000000package main // A simple example that shows how to send messages to a Bubble Tea program // from outside the program using Program.Send(Msg). import ( "fmt" "math/rand" "os" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0) dotStyle = helpStyle.Copy().UnsetMargins() foodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("36")) durationStyle = dotStyle.Copy() appStyle = lipgloss.NewStyle().Margin(1, 2, 0, 2) ) type resultMsg struct { duration time.Duration food string } func (r resultMsg) String() string { if r.duration == 0 { return dotStyle.Render(strings.Repeat(".", 30)) } return fmt.Sprintf("🍔 Ate %s %s", r.food, durationStyle.Render(r.duration.String())) } type model struct { spinner spinner.Model results []resultMsg quitting bool } func newModel() model { const numLastResults = 5 s := spinner.NewModel() s.Style = spinnerStyle return model{ spinner: s, results: make([]resultMsg, numLastResults), } } func (m model) Init() tea.Cmd { return spinner.Tick } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: m.quitting = true return m, tea.Quit case resultMsg: m.results = append(m.results[1:], msg) return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } func (m model) View() string { var s string if m.quitting { s += "That’s all for today!" } else { s += m.spinner.View() + " Eating food..." } s += "\n\n" for _, res := range m.results { s += res.String() + "\n" } if !m.quitting { s += helpStyle.Render("Press any key to exit") } if m.quitting { s += "\n" } return appStyle.Render(s) } func main() { rand.Seed(time.Now().UTC().UnixNano()) done := make(chan struct{}) p := tea.NewProgram(newModel()) go func() { if err := p.Start(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } close(done) }() // Simulate activity go func() { for { pause := time.Duration(rand.Int63n(899)+100) * time.Millisecond time.Sleep(pause) // Send the Bubble Tea program a message from outside the program. p.Send(resultMsg{food: randomFood(), duration: pause}) } }() <-done } func randomEmoji() string { emojis := []rune("🍦🧋🍡🤠👾😭🦊🐯🦆🥨🎏🍔🍒🍥🎮📦🦁🐶🐸🍕🥐🧲🚒🥇🏆🌽") return string(emojis[rand.Intn(len(emojis))]) } func randomFood() string { food := []string{"an apple", "a pear", "a gherkin", "a party gherkin", "a kohlrabi", "some spaghetti", "tacos", "a currywurst", "some curry", "a sandwich", "some peanut butter", "some cashews", "some ramen"} return string(food[rand.Intn(len(food))]) } bubbletea-0.19.1/examples/simple/000077500000000000000000000000001414053472700166455ustar00rootroot00000000000000bubbletea-0.19.1/examples/simple/main.go000066400000000000000000000034171414053472700201250ustar00rootroot00000000000000package main // A simple program that counts down from 5 and then exits. import ( "fmt" "log" "os" "time" tea "github.com/charmbracelet/bubbletea" ) func main() { // Log to a file. Useful in debugging since you can't really log to stdout. // Not required. logfilePath := os.Getenv("BUBBLETEA_LOG") if logfilePath != "" { if _, err := tea.LogToFile(logfilePath, "simple"); err != nil { log.Fatal(err) } } // Initialize our program p := tea.NewProgram(model(5)) if err := p.Start(); err != nil { log.Fatal(err) } } // A model can be more or less any type of data. It holds all the data for a // program, so often it's a struct. For this simple example, however, all // we'll need is a simple integer. type model int // Init optionally returns an initial command we should run. In this case we // want to start the timer. func (m model) Init() tea.Cmd { return tick } // Update is called when messages are received. The idea is that you inspect the // message and send back an updated model accordingly. You can also return // a command, which is a function that performs I/O and returns a message. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.(type) { case tea.KeyMsg: return m, tea.Quit case tickMsg: m -= 1 if m <= 0 { return m, tea.Quit } return m, tick } return m, nil } // Views return a string based on data in the model. That string which will be // rendered to the terminal. func (m model) View() string { return fmt.Sprintf("Hi. This program will exit in %d seconds. To quit sooner press any key.\n", m) } // Messages are events that we respond to in our Update function. This // particular one indicates that the timer has ticked. type tickMsg time.Time func tick() tea.Msg { time.Sleep(time.Second) return tickMsg{} } bubbletea-0.19.1/examples/spinner/000077500000000000000000000000001414053472700170325ustar00rootroot00000000000000bubbletea-0.19.1/examples/spinner/main.go000066400000000000000000000024221414053472700203050ustar00rootroot00000000000000package main // A simple program demonstrating the spinner component from the Bubbles // component library. import ( "fmt" "os" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type errMsg error type model struct { spinner spinner.Model quitting bool err error } func initialModel() model { s := spinner.NewModel() s.Spinner = spinner.Dot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) return model{spinner: s} } func (m model) Init() tea.Cmd { return spinner.Tick } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.quitting = true return m, tea.Quit default: return m, nil } case errMsg: m.err = msg return m, nil default: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } } func (m model) View() string { if m.err != nil { return m.err.Error() } str := fmt.Sprintf("\n\n %s Loading forever...press q to quit\n\n", m.spinner.View()) if m.quitting { return str + "\n" } return str } func main() { p := tea.NewProgram(initialModel()) if err := p.Start(); err != nil { fmt.Println(err) os.Exit(1) } } bubbletea-0.19.1/examples/spinners/000077500000000000000000000000001414053472700172155ustar00rootroot00000000000000bubbletea-0.19.1/examples/spinners/main.go000066400000000000000000000035601414053472700204740ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( // Available spinners spinners = []spinner.Spinner{ spinner.Line, spinner.Dot, spinner.MiniDot, spinner.Jump, spinner.Pulse, spinner.Points, spinner.Globe, spinner.Moon, spinner.Monkey, } textStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Render spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69")) helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render ) func main() { m := model{} m.resetSpinner() if err := tea.NewProgram(m).Start(); err != nil { fmt.Println("could not run program:", err) os.Exit(1) } } type model struct { index int spinner spinner.Model } func (m model) Init() tea.Cmd { return spinner.Tick } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": return m, tea.Quit case "h", "left": m.index-- if m.index <= 0 { m.index = len(spinners) - 1 } m.resetSpinner() return m, spinner.Tick case "l", "right": m.index++ if m.index >= len(spinners) { m.index = 0 } m.resetSpinner() return m, spinner.Tick default: return m, nil } case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd default: return m, nil } } func (m *model) resetSpinner() { m.spinner = spinner.NewModel() m.spinner.Style = spinnerStyle m.spinner.Spinner = spinners[m.index] } func (m model) View() (s string) { var gap string switch m.index { case 1: gap = "" default: gap = " " } s += fmt.Sprintf("\n %s%s%s\n\n", m.spinner.View(), gap, textStyle("Spinning...")) s += helpStyle("h/l, ←/→: change spinner • q: exit\n") return } bubbletea-0.19.1/examples/textinput/000077500000000000000000000000001414053472700174205ustar00rootroot00000000000000bubbletea-0.19.1/examples/textinput/main.go000066400000000000000000000023071414053472700206750ustar00rootroot00000000000000package main // A simple program demonstrating the text input component from the Bubbles // component library. import ( "fmt" "log" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) func main() { p := tea.NewProgram(initialModel()) if err := p.Start(); err != nil { log.Fatal(err) } } type tickMsg struct{} type errMsg error type model struct { textInput textinput.Model err error } func initialModel() model { ti := textinput.NewModel() ti.Placeholder = "Pikachu" ti.Focus() ti.CharLimit = 156 ti.Width = 20 return model{ textInput: ti, err: nil, } } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) 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 m, tea.Quit } // We handle errors just like any other message case errMsg: m.err = msg return m, nil } m.textInput, cmd = m.textInput.Update(msg) return m, cmd } func (m model) View() string { return fmt.Sprintf( "What’s your favorite Pokémon?\n\n%s\n\n%s", m.textInput.View(), "(esc to quit)", ) + "\n" } bubbletea-0.19.1/examples/textinputs/000077500000000000000000000000001414053472700176035ustar00rootroot00000000000000bubbletea-0.19.1/examples/textinputs/main.go000066400000000000000000000076651414053472700210740ustar00rootroot00000000000000package main // A simple example demonstrating the use of multiple text input components // from the Bubbles component library. import ( "fmt" "os" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) cursorStyle = focusedStyle.Copy() noStyle = lipgloss.NewStyle() helpStyle = blurredStyle.Copy() cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) focusedButton = focusedStyle.Copy().Render("[ Submit ]") blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit")) ) type model struct { focusIndex int inputs []textinput.Model cursorMode textinput.CursorMode } func initialModel() model { m := model{ inputs: make([]textinput.Model, 3), } var t textinput.Model for i := range m.inputs { t = textinput.NewModel() t.CursorStyle = cursorStyle t.CharLimit = 32 switch i { case 0: t.Placeholder = "Nickname" t.Focus() t.PromptStyle = focusedStyle t.TextStyle = focusedStyle case 1: t.Placeholder = "Email" t.CharLimit = 64 case 2: t.Placeholder = "Password" t.EchoMode = textinput.EchoPassword t.EchoCharacter = '•' } m.inputs[i] = t } return m } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": return m, tea.Quit // Change cursor mode case "ctrl+r": m.cursorMode++ if m.cursorMode > textinput.CursorHide { m.cursorMode = textinput.CursorBlink } cmds := make([]tea.Cmd, len(m.inputs)) for i := range m.inputs { cmds[i] = m.inputs[i].SetCursorMode(m.cursorMode) } return m, tea.Batch(cmds...) // Set focus to next input case "tab", "shift+tab", "enter", "up", "down": s := msg.String() // Did the user press enter while the submit button was focused? // If so, exit. if s == "enter" && m.focusIndex == len(m.inputs) { return m, tea.Quit } // Cycle indexes if s == "up" || s == "shift+tab" { m.focusIndex-- } else { m.focusIndex++ } if m.focusIndex > len(m.inputs) { m.focusIndex = 0 } else if m.focusIndex < 0 { m.focusIndex = len(m.inputs) } cmds := make([]tea.Cmd, len(m.inputs)) for i := 0; i <= len(m.inputs)-1; i++ { if i == m.focusIndex { // Set focused state cmds[i] = m.inputs[i].Focus() m.inputs[i].PromptStyle = focusedStyle m.inputs[i].TextStyle = focusedStyle continue } // Remove focused state m.inputs[i].Blur() m.inputs[i].PromptStyle = noStyle m.inputs[i].TextStyle = noStyle } return m, tea.Batch(cmds...) } } // Handle character input and blinking cmd := m.updateInputs(msg) return m, cmd } func (m *model) updateInputs(msg tea.Msg) tea.Cmd { var cmds = make([]tea.Cmd, len(m.inputs)) // Only text inputs with Focus() set will respond, so it's safe to simply // update all of them here without any further logic. for i := range m.inputs { m.inputs[i], cmds[i] = m.inputs[i].Update(msg) } return tea.Batch(cmds...) } func (m model) View() string { var b strings.Builder for i := range m.inputs { b.WriteString(m.inputs[i].View()) if i < len(m.inputs)-1 { b.WriteRune('\n') } } button := &blurredButton if m.focusIndex == len(m.inputs) { button = &focusedButton } fmt.Fprintf(&b, "\n\n%s\n\n", *button) b.WriteString(helpStyle.Render("cursor mode is ")) b.WriteString(cursorModeHelpStyle.Render(m.cursorMode.String())) b.WriteString(helpStyle.Render(" (ctrl+r to change style)")) return b.String() } func main() { if err := tea.NewProgram(initialModel()).Start(); err != nil { fmt.Printf("could not start program: %s\n", err) os.Exit(1) } } bubbletea-0.19.1/examples/tui-daemon-combo/000077500000000000000000000000001414053472700205135ustar00rootroot00000000000000bubbletea-0.19.1/examples/tui-daemon-combo/main.go000066400000000000000000000056361414053472700220000ustar00rootroot00000000000000package main import ( "flag" "fmt" "io/ioutil" "log" "math/rand" "os" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/mattn/go-isatty" "github.com/muesli/reflow/indent" ) var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render func main() { rand.Seed(time.Now().UTC().UnixNano()) var ( daemonMode bool showHelp bool opts []tea.ProgramOption ) flag.BoolVar(&daemonMode, "d", false, "run as a daemon") flag.BoolVar(&showHelp, "h", false, "show help") flag.Parse() if showHelp { flag.Usage() os.Exit(0) } if daemonMode || !isatty.IsTerminal(os.Stdout.Fd()) { // If we're in daemon mode don't render the TUI opts = []tea.ProgramOption{tea.WithoutRenderer()} } else { // If we're in TUI mode, discard log output log.SetOutput(ioutil.Discard) } p := tea.NewProgram(newModel(), opts...) if err := p.Start(); err != nil { fmt.Println("Error starting Bubble Tea program:", err) os.Exit(1) } } type result struct { duration time.Duration emoji string } type model struct { spinner spinner.Model results []result quitting bool } func newModel() model { const showLastResults = 5 sp := spinner.NewModel() sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("206")) return model{ spinner: sp, results: make([]result, showLastResults), } } func (m model) Init() tea.Cmd { log.Println("Starting work...") return tea.Batch( spinner.Tick, runPretendProcess, ) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: m.quitting = true return m, tea.Quit case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd case processFinishedMsg: d := time.Duration(msg) res := result{emoji: randomEmoji(), duration: d} log.Printf("%s Job finished in %s", res.emoji, res.duration) m.results = append(m.results[1:], res) return m, runPretendProcess default: return m, nil } } func (m model) View() string { s := "\n" + m.spinner.View() + " Doing some work...\n\n" for _, res := range m.results { if res.duration == 0 { s += "........................\n" } else { s += fmt.Sprintf("%s Job finished in %s\n", res.emoji, res.duration) } } s += helpStyle("\nPress any key to exit\n") if m.quitting { s += "\n" } return indent.String(s, 1) } // processFinishedMsg is send when a pretend process completes. type processFinishedMsg time.Duration // pretendProcess simulates a long-running process. func runPretendProcess() tea.Msg { pause := time.Duration(rand.Int63n(899)+100) * time.Millisecond time.Sleep(pause) return processFinishedMsg(pause) } func randomEmoji() string { emojis := []rune("🍦🧋🍡🤠👾😭🦊🐯🦆🥨🎏🍔🍒🍥🎮📦🦁🐶🐸🍕🥐🧲🚒🥇🏆🌽") return string(emojis[rand.Intn(len(emojis))]) } bubbletea-0.19.1/examples/views/000077500000000000000000000000001414053472700165115ustar00rootroot00000000000000bubbletea-0.19.1/examples/views/main.go000066400000000000000000000153001414053472700177630ustar00rootroot00000000000000package main // An example demonstrating an application with multiple views. // // Note that this example was produced before the Bubbles progress component // was available (github.com/charmbracelet/bubbles/progress) and thus, we're // implementing a progress bar from scratch here. import ( "fmt" "math" "strconv" "strings" "time" tea "github.com/charmbracelet/bubbletea" "github.com/fogleman/ease" "github.com/lucasb-eyer/go-colorful" "github.com/muesli/reflow/indent" "github.com/muesli/termenv" ) const ( progressBarWidth = 71 progressFullChar = "█" progressEmptyChar = "░" ) // General stuff for styling the view var ( term = termenv.ColorProfile() keyword = makeFgStyle("211") subtle = makeFgStyle("241") progressEmpty = subtle(progressEmptyChar) dot = colorFg(" • ", "236") // Gradient colors we'll use for the progress bar ramp = makeRamp("#B14FFF", "#00FFA3", progressBarWidth) ) func main() { initialModel := model{0, false, 10, 0, 0, false, false} p := tea.NewProgram(initialModel) if err := p.Start(); err != nil { fmt.Println("could not start program:", err) } } type tickMsg struct{} type frameMsg struct{} func tick() tea.Cmd { return tea.Tick(time.Second, func(time.Time) tea.Msg { return tickMsg{} }) } func frame() tea.Cmd { return tea.Tick(time.Second/60, func(time.Time) tea.Msg { return frameMsg{} }) } type model struct { Choice int Chosen bool Ticks int Frames int Progress float64 Loaded bool Quitting bool } func (m model) Init() tea.Cmd { return tick() } // Main update function. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Make sure these keys always quit if msg, ok := msg.(tea.KeyMsg); ok { k := msg.String() if k == "q" || k == "esc" || k == "ctrl+c" { m.Quitting = true return m, tea.Quit } } // Hand off the message and model to the appropriate update function for the // appropriate view based on the current state. if !m.Chosen { return updateChoices(msg, m) } return updateChosen(msg, m) } // The main view, which just calls the appropriate sub-view func (m model) View() string { var s string if m.Quitting { return "\n See you later!\n\n" } if !m.Chosen { s = choicesView(m) } else { s = chosenView(m) } return indent.String("\n"+s+"\n\n", 2) } // Sub-update functions // Update loop for the first view where you're choosing a task. func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "j", "down": m.Choice += 1 if m.Choice > 3 { m.Choice = 3 } case "k", "up": m.Choice -= 1 if m.Choice < 0 { m.Choice = 0 } case "enter": m.Chosen = true return m, frame() } case tickMsg: if m.Ticks == 0 { m.Quitting = true return m, tea.Quit } m.Ticks -= 1 return m, tick() } return m, nil } // Update loop for the second view after a choice has been made func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) { switch msg.(type) { case frameMsg: if !m.Loaded { m.Frames += 1 m.Progress = ease.OutBounce(float64(m.Frames) / float64(100)) if m.Progress >= 1 { m.Progress = 1 m.Loaded = true m.Ticks = 3 return m, tick() } return m, frame() } case tickMsg: if m.Loaded { if m.Ticks == 0 { m.Quitting = true return m, tea.Quit } m.Ticks -= 1 return m, tick() } } return m, nil } // Sub-views // The first view, where you're choosing a task func choicesView(m model) string { c := m.Choice tpl := "What to do today?\n\n" tpl += "%s\n\n" tpl += "Program quits in %s seconds\n\n" tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") choices := fmt.Sprintf( "%s\n%s\n%s\n%s", checkbox("Plant carrots", c == 0), checkbox("Go to the market", c == 1), checkbox("Read something", c == 2), checkbox("See friends", c == 3), ) return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Ticks), "79")) } // The second view, after a task has been chosen func chosenView(m model) string { var msg string switch m.Choice { case 0: msg = fmt.Sprintf("Carrot planting?\n\nCool, we'll need %s and %s...", keyword("libgarden"), keyword("vegeutils")) case 1: msg = fmt.Sprintf("A trip to the market?\n\nOkay, then we should install %s and %s...", keyword("marketkit"), keyword("libshopping")) case 2: msg = fmt.Sprintf("Reading time?\n\nOkay, cool, then we’ll need a library. Yes, an %s.", keyword("actual library")) default: msg = fmt.Sprintf("It’s always good to see friends.\n\nFetching %s and %s...", keyword("social-skills"), keyword("conversationutils")) } label := "Downloading..." if m.Loaded { label = fmt.Sprintf("Downloaded. Exiting in %s seconds...", colorFg(strconv.Itoa(m.Ticks), "79")) } return msg + "\n\n" + label + "\n" + progressbar(80, m.Progress) + "%" } func checkbox(label string, checked bool) string { if checked { return colorFg("[x] "+label, "212") } return fmt.Sprintf("[ ] %s", label) } func progressbar(width int, percent float64) string { w := float64(progressBarWidth) fullSize := int(math.Round(w * percent)) var fullCells string for i := 0; i < fullSize; i++ { fullCells += termenv.String(progressFullChar).Foreground(term.Color(ramp[i])).String() } emptySize := int(w) - fullSize emptyCells := strings.Repeat(progressEmpty, emptySize) return fmt.Sprintf("%s%s %3.0f", fullCells, emptyCells, math.Round(percent*100)) } // Utils // Color a string's foreground with the given value. func colorFg(val, color string) string { return termenv.String(val).Foreground(term.Color(color)).String() } // Return a function that will colorize the foreground of a given string. func makeFgStyle(color string) func(string) string { return termenv.Style{}.Foreground(term.Color(color)).Styled } // Color a string's foreground and background with the given value. func makeFgBgStyle(fg, bg string) func(string) string { return termenv.Style{}. Foreground(term.Color(fg)). Background(term.Color(bg)). Styled } // Generate a blend of colors. func makeRamp(colorA, colorB string, steps float64) (s []string) { cA, _ := colorful.Hex(colorA) cB, _ := colorful.Hex(colorB) for i := 0.0; i < steps; i++ { c := cA.BlendLuv(cB, i/steps) s = append(s, colorToHex(c)) } return } // Convert a colorful.Color to a hexadecimal format compatible with termenv. func colorToHex(c colorful.Color) string { return fmt.Sprintf("#%s%s%s", colorFloatToHex(c.R), colorFloatToHex(c.G), colorFloatToHex(c.B)) } // Helper function for converting colors to hex. Assumes a value between 0 and // 1. func colorFloatToHex(f float64) (s string) { s = strconv.FormatInt(int64(f*255), 16) if len(s) == 1 { s = "0" + s } return } bubbletea-0.19.1/go.mod000066400000000000000000000005631414053472700146500ustar00rootroot00000000000000module github.com/charmbracelet/bubbletea go 1.13 require ( github.com/containerd/console v1.0.2 github.com/mattn/go-isatty v0.0.13 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.9.0 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c golang.org/x/term v0.0.0-20210422114643-f5beecf764ed ) bubbletea-0.19.1/go.sum000066400000000000000000000045431414053472700146770ustar00rootroot00000000000000github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= bubbletea-0.19.1/key.go000066400000000000000000000244731414053472700146670ustar00rootroot00000000000000package tea import ( "errors" "fmt" "io" "unicode/utf8" ) // KeyMsg contains information about a keypress. KeyMsgs are always sent to // the program's update function. There are a couple general patterns you could // use to check for keypresses: // // // Switch on the string representation of the key (shorter) // switch msg := msg.(type) { // case KeyMsg: // switch msg.String() { // case "enter": // fmt.Println("you pressed enter!") // case "a": // fmt.Println("you pressed a!") // } // } // // // Switch on the key type (more foolproof) // switch msg := msg.(type) { // case KeyMsg: // switch msg.Type { // case KeyEnter: // fmt.Println("you pressed enter!") // case KeyRunes: // switch string(msg.Runes) { // case "a": // fmt.Println("you pressed a!") // } // } // } // // Note that Key.Runes will always contain at least one character, so you can // always safely call Key.Runes[0]. In most cases Key.Runes will only contain // one character, though certain input method editors (most notably Chinese // IMEs) can input multiple runes at once. type KeyMsg Key // String returns a string representation for a key message. It's safe (and // encouraged) for use in key comparison. func (k KeyMsg) String() (str string) { return Key(k).String() } // Key contains information about a keypress. type Key struct { Type KeyType Runes []rune Alt bool } // String returns a friendly string representation for a key. It's safe (and // encouraged) for use in key comparison. // // k := Key{Type: KeyEnter} // fmt.Println(k) // // Output: enter // func (k Key) String() (str string) { if k.Alt { str += "alt+" } if k.Type == KeyRunes { str += string(k.Runes) return str } else if s, ok := keyNames[k.Type]; ok { str += s return str } return "" } // KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC. // All other keys will be type KeyRunes. To get the rune value, check the Rune // method on a Key struct, or use the Key.String() method: // // k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true} // if k.Type == KeyRunes { // // fmt.Println(k.Runes) // // Output: a // // fmt.Println(k.String()) // // Output: alt+a // // } type KeyType int func (k KeyType) String() (str string) { if s, ok := keyNames[k]; ok { return s } return "" } // Control keys. We could do this with an iota, but the values are very // specific, so we set the values explicitly to avoid any confusion. // // See also: // https://en.wikipedia.org/wiki/C0_and_C1_control_codes const ( keyNUL KeyType = 0 // null, \0 keySOH KeyType = 1 // start of heading keySTX KeyType = 2 // start of text keyETX KeyType = 3 // break, ctrl+c keyEOT KeyType = 4 // end of transmission keyENQ KeyType = 5 // enquiry keyACK KeyType = 6 // acknowledge keyBEL KeyType = 7 // bell, \a keyBS KeyType = 8 // backspace keyHT KeyType = 9 // horizontal tabulation, \t keyLF KeyType = 10 // line feed, \n keyVT KeyType = 11 // vertical tabulation \v keyFF KeyType = 12 // form feed \f keyCR KeyType = 13 // carriage return, \r keySO KeyType = 14 // shift out keySI KeyType = 15 // shift in keyDLE KeyType = 16 // data link escape keyDC1 KeyType = 17 // device control one keyDC2 KeyType = 18 // device control two keyDC3 KeyType = 19 // device control three keyDC4 KeyType = 20 // device control four keyNAK KeyType = 21 // negative acknowledge keySYN KeyType = 22 // synchronous idle keyETB KeyType = 23 // end of transmission block keyCAN KeyType = 24 // cancel keyEM KeyType = 25 // end of medium keySUB KeyType = 26 // substitution keyESC KeyType = 27 // escape, \e keyFS KeyType = 28 // file separator keyGS KeyType = 29 // group separator keyRS KeyType = 30 // record separator keyUS KeyType = 31 // unit separator keySP KeyType = 32 // space keyDEL KeyType = 127 // delete. on most systems this is mapped to backspace, I hear ) // Control key aliases. const ( KeyNull KeyType = keyNUL KeyBreak KeyType = keyETX KeyEnter KeyType = keyCR KeyBackspace KeyType = keyDEL KeyTab KeyType = keyHT KeySpace KeyType = keySP KeyEsc KeyType = keyESC KeyEscape KeyType = keyESC KeyCtrlAt KeyType = keyNUL // ctrl+@ KeyCtrlA KeyType = keySOH KeyCtrlB KeyType = keySTX KeyCtrlC KeyType = keyETX KeyCtrlD KeyType = keyEOT KeyCtrlE KeyType = keyENQ KeyCtrlF KeyType = keyACK KeyCtrlG KeyType = keyBEL KeyCtrlH KeyType = keyBS KeyCtrlI KeyType = keyHT KeyCtrlJ KeyType = keyLF KeyCtrlK KeyType = keyVT KeyCtrlL KeyType = keyFF KeyCtrlM KeyType = keyCR KeyCtrlN KeyType = keySO KeyCtrlO KeyType = keySI KeyCtrlP KeyType = keyDLE KeyCtrlQ KeyType = keyDC1 KeyCtrlR KeyType = keyDC2 KeyCtrlS KeyType = keyDC3 KeyCtrlT KeyType = keyDC4 KeyCtrlU KeyType = keyNAK KeyCtrlV KeyType = keySYN KeyCtrlW KeyType = keyETB KeyCtrlX KeyType = keyCAN KeyCtrlY KeyType = keyEM KeyCtrlZ KeyType = keySUB KeyCtrlOpenBracket KeyType = keyESC // ctrl+[ KeyCtrlBackslash KeyType = keyFS // ctrl+\ KeyCtrlCloseBracket KeyType = keyGS // ctrl+] KeyCtrlCaret KeyType = keyRS // ctrl+^ KeyCtrlUnderscore KeyType = keyUS // ctrl+_ KeyCtrlQuestionMark KeyType = keyDEL // ctrl+? ) // Other keys. const ( KeyRunes KeyType = -(iota + 1) KeyUp KeyDown KeyRight KeyLeft KeyShiftTab KeyHome KeyEnd KeyPgUp KeyPgDown KeyDelete ) // Mapping for control keys to friendly consts. var keyNames = map[KeyType]string{ keyNUL: "ctrl+@", // also ctrl+` keySOH: "ctrl+a", keySTX: "ctrl+b", keyETX: "ctrl+c", keyEOT: "ctrl+d", keyENQ: "ctrl+e", keyACK: "ctrl+f", keyBEL: "ctrl+g", keyBS: "ctrl+h", keyHT: "tab", // also ctrl+i keyLF: "ctrl+j", keyVT: "ctrl+k", keyFF: "ctrl+l", keyCR: "enter", keySO: "ctrl+n", keySI: "ctrl+o", keyDLE: "ctrl+p", keyDC1: "ctrl+q", keyDC2: "ctrl+r", keyDC3: "ctrl+s", keyDC4: "ctrl+t", keyNAK: "ctrl+u", keySYN: "ctrl+v", keyETB: "ctrl+w", keyCAN: "ctrl+x", keyEM: "ctrl+y", keySUB: "ctrl+z", keyESC: "esc", keyFS: "ctrl+\\", keyGS: "ctrl+]", keyRS: "ctrl+^", keyUS: "ctrl+_", keySP: "space", keyDEL: "backspace", KeyRunes: "runes", KeyUp: "up", KeyDown: "down", KeyRight: "right", KeyLeft: "left", KeyShiftTab: "shift+tab", KeyHome: "home", KeyEnd: "end", KeyPgUp: "pgup", KeyPgDown: "pgdown", } // Mapping for sequences to consts. var sequences = map[string]KeyType{ "\x1b[A": KeyUp, "\x1b[B": KeyDown, "\x1b[C": KeyRight, "\x1b[D": KeyLeft, } // Mapping for hex codes to consts. Unclear why these won't register as // sequences. var hexes = map[string]Key{ "1b5b5a": {Type: KeyShiftTab}, "1b5b337e": {Type: KeyDelete}, "1b0d": {Type: KeyEnter, Alt: true}, "1b7f": {Type: KeyBackspace, Alt: true}, "1b5b48": {Type: KeyHome}, "1b5b377e": {Type: KeyHome}, // urxvt "1b5b313b3348": {Type: KeyHome, Alt: true}, "1b1b5b377e": {Type: KeyHome, Alt: true}, // urxvt "1b5b46": {Type: KeyEnd}, "1b5b387e": {Type: KeyEnd}, // urxvt "1b5b313b3346": {Type: KeyEnd, Alt: true}, "1b1b5b387e": {Type: KeyEnd, Alt: true}, // urxvt "1b5b357e": {Type: KeyPgUp}, "1b5b353b337e": {Type: KeyPgUp, Alt: true}, "1b1b5b357e": {Type: KeyPgUp, Alt: true}, // urxvt "1b5b367e": {Type: KeyPgDown}, "1b5b363b337e": {Type: KeyPgDown, Alt: true}, "1b1b5b367e": {Type: KeyPgDown, Alt: true}, // urxvt "1b5b313b3341": {Type: KeyUp, Alt: true}, "1b5b313b3342": {Type: KeyDown, Alt: true}, "1b5b313b3343": {Type: KeyRight, Alt: true}, "1b5b313b3344": {Type: KeyLeft, Alt: true}, // Powershell "1b4f41": {Type: KeyUp, Alt: false}, "1b4f42": {Type: KeyDown, Alt: false}, "1b4f43": {Type: KeyRight, Alt: false}, "1b4f44": {Type: KeyLeft, Alt: false}, } // readInput reads keypress and mouse input from a TTY and returns a message // containing information about the key or mouse event accordingly. func readInput(input io.Reader) (Msg, error) { var buf [256]byte // Read and block numBytes, err := input.Read(buf[:]) if err != nil { return nil, err } // See if it's a mouse event. For now we're parsing X10-type mouse events // only. mouseEvent, err := parseX10MouseEvent(buf[:numBytes]) if err == nil { return MouseMsg(mouseEvent), nil } // Is it a special sequence, like an arrow key? if k, ok := sequences[string(buf[:numBytes])]; ok { return KeyMsg(Key{Type: k}), nil } // Some of these need special handling hex := fmt.Sprintf("%x", buf[:numBytes]) if k, ok := hexes[hex]; ok { return KeyMsg(k), nil } // Is the alt key pressed? The buffer will be prefixed with an escape // sequence if so. if numBytes > 1 && buf[0] == 0x1b { // Now remove the initial escape sequence and re-process to get the // character being pressed in combination with alt. c, _ := utf8.DecodeRune(buf[1:]) if c == utf8.RuneError { return nil, errors.New("could not decode rune after removing initial escape") } return KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: []rune{c}}), nil } var runes []rune b := buf[:numBytes] // Translate input into runes. In most cases we'll receive exactly one // rune, but there are cases, particularly when an input method editor is // used, where we can receive multiple runes at once. for i, w := 0, 0; i < len(b); i += w { r, width := utf8.DecodeRune(b[i:]) if r == utf8.RuneError { return nil, errors.New("could not decode rune") } runes = append(runes, r) w = width } if len(runes) == 0 { return nil, errors.New("received 0 runes from input") } else if len(runes) > 1 { // We received multiple runes, so we know this isn't a control // character, sequence, and so on. return KeyMsg(Key{Type: KeyRunes, Runes: runes}), nil } // Is the first rune a control character? r := KeyType(runes[0]) if numBytes == 1 && r <= keyUS || r == keyDEL { return KeyMsg(Key{Type: r}), nil } // Welp, it's just a regular, ol' single rune return KeyMsg(Key{Type: KeyRunes, Runes: runes}), nil } bubbletea-0.19.1/key_test.go000066400000000000000000000032561414053472700157220ustar00rootroot00000000000000package tea import ( "bytes" "testing" ) func TestKeyString(t *testing.T) { t.Run("alt+space", func(t *testing.T) { if got := KeyMsg(Key{ Type: keySP, Alt: true, }).String(); got != "alt+space" { t.Fatalf(`expected a "alt+space", got %q`, got) } }) t.Run("runes", func(t *testing.T) { if got := KeyMsg(Key{ Type: KeyRunes, Runes: []rune{'a'}, }).String(); got != "a" { t.Fatalf(`expected an "a", got %q`, got) } }) t.Run("invalid", func(t *testing.T) { if got := KeyMsg(Key{ Type: KeyType(99999), }).String(); got != "" { t.Fatalf(`expected a "", got %q`, got) } }) } func TestKeyTypeString(t *testing.T) { t.Run("space", func(t *testing.T) { if got := keySP.String(); got != "space" { t.Fatalf(`expected a "space", got %q`, got) } }) t.Run("invalid", func(t *testing.T) { if got := KeyType(99999).String(); got != "" { t.Fatalf(`expected a "", got %q`, got) } }) } func TestReadInput(t *testing.T) { for out, in := range map[string][]byte{ "a": {'a'}, "ctrl+a": {byte(keySOH)}, "alt+a": {0x1b, 'a'}, "abcd": {'a', 'b', 'c', 'd'}, "up": []byte("\x1b[A"), "wheel up": {'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, "shift+tab": {'\x1b', '[', 'Z'}, } { t.Run(out, func(t *testing.T) { msg, err := readInput(bytes.NewReader(in)) if err != nil { t.Fatalf("unexpected error: %v", err) } if m, ok := msg.(KeyMsg); ok && m.String() != out { t.Fatalf(`expected a keymsg %q, got %q`, out, m) } if m, ok := msg.(MouseMsg); ok && mouseEventTypes[m.Type] != out { t.Fatalf(`expected a mousemsg %q, got %q`, out, mouseEventTypes[m.Type]) } }) } } bubbletea-0.19.1/logging.go000066400000000000000000000016241414053472700155160ustar00rootroot00000000000000package tea import ( "log" "os" "unicode" ) // LogToFile sets up default logging to log to a file. This is helpful as we // can't print to the terminal since our TUI is occupying it. If the file // doesn't exist it will be created. // // Don't forget to close the file when you're done with it. // // f, err := LogToFile("debug.log", "debug") // if err != nil { // fmt.Println("fatal:", err) // os.Exit(1) // } // defer f.Close() func LogToFile(path string, prefix string) (*os.File, error) { f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return nil, err } log.SetOutput(f) // Add a space after the prefix if a prefix is being specified and it // doesn't already have a trailing space. if len(prefix) > 0 { finalChar := prefix[len(prefix)-1] if !unicode.IsSpace(rune(finalChar)) { prefix += " " } } log.SetPrefix(prefix) return f, nil } bubbletea-0.19.1/logging_test.go000066400000000000000000000010051414053472700165460ustar00rootroot00000000000000package tea import ( "log" "os" "path/filepath" "testing" ) func TestLogToFile(t *testing.T) { path := filepath.Join(t.TempDir(), "log.txt") prefix := "logprefix" f, err := LogToFile(path, prefix) if err != nil { t.Error(err) } log.SetFlags(log.Lmsgprefix) log.Println("some test log") if err := f.Close(); err != nil { t.Error(err) } out, err := os.ReadFile(path) if err != nil { t.Error(err) } if string(out) != prefix+" some test log\n" { t.Fatalf("wrong log msg: %q", string(out)) } } bubbletea-0.19.1/mouse.go000066400000000000000000000053531414053472700152230ustar00rootroot00000000000000package tea import "errors" // MouseMsg contains information about a mouse event and are sent to a programs // update function when mouse activity occurs. Note that the mouse must first // be enabled via in order the mouse events to be received. type MouseMsg MouseEvent // MouseEvent represents a mouse event, which could be a click, a scroll wheel // movement, a cursor movement, or a combination. type MouseEvent struct { X int Y int Type MouseEventType Alt bool Ctrl bool } // String returns a string representation of a mouse event. func (m MouseEvent) String() (s string) { if m.Ctrl { s += "ctrl+" } if m.Alt { s += "alt+" } s += mouseEventTypes[m.Type] return s } // MouseEventType indicates the type of mouse event occurring. type MouseEventType int // Mouse event types. const ( MouseUnknown MouseEventType = iota MouseLeft MouseRight MouseMiddle MouseRelease MouseWheelUp MouseWheelDown MouseMotion ) var mouseEventTypes = map[MouseEventType]string{ MouseUnknown: "unknown", MouseLeft: "left", MouseRight: "right", MouseMiddle: "middle", MouseRelease: "release", MouseWheelUp: "wheel up", MouseWheelDown: "wheel down", MouseMotion: "motion", } // Parse an X10-encoded mouse event; the simplest kind. The last release of // X10 was December 1986, by the way. // // X10 mouse events look like: // // ESC [M Cb Cx Cy // // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking func parseX10MouseEvent(buf []byte) (m MouseEvent, err error) { if len(buf) != 6 || string(buf[:3]) != "\x1b[M" { return m, errors.New("not an X10 mouse event") } const byteOffset = 32 e := buf[3] - byteOffset const ( bitShift = 0b0000_0100 bitAlt = 0b0000_1000 bitCtrl = 0b0001_0000 bitMotion = 0b0010_0000 bitWheel = 0b0100_0000 bitsMask = 0b0000_0011 bitsLeft = 0b0000_0000 bitsMiddle = 0b0000_0001 bitsRight = 0b0000_0010 bitsRelease = 0b0000_0011 bitsWheelUp = 0b0000_0000 bitsWheelDown = 0b0000_0001 ) if e&bitWheel != 0 { // Check the low two bits. switch e & bitsMask { case bitsWheelUp: m.Type = MouseWheelUp case bitsWheelDown: m.Type = MouseWheelDown } } else { // Check the low two bits. // We do not separate clicking and dragging. switch e & bitsMask { case bitsLeft: m.Type = MouseLeft case bitsMiddle: m.Type = MouseMiddle case bitsRight: m.Type = MouseRight case bitsRelease: if e&bitMotion != 0 { m.Type = MouseMotion } else { m.Type = MouseRelease } } } if e&bitAlt != 0 { m.Alt = true } if e&bitCtrl != 0 { m.Ctrl = true } // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). m.X = int(buf[4]) - byteOffset - 1 m.Y = int(buf[5]) - byteOffset - 1 return m, nil } bubbletea-0.19.1/mouse_test.go000066400000000000000000000145511414053472700162620ustar00rootroot00000000000000package tea import "testing" func TestMouseEvent_String(t *testing.T) { tt := []struct { name string event MouseEvent expected string }{ { name: "unknown", event: MouseEvent{Type: MouseUnknown}, expected: "unknown", }, { name: "left", event: MouseEvent{Type: MouseLeft}, expected: "left", }, { name: "right", event: MouseEvent{Type: MouseRight}, expected: "right", }, { name: "middle", event: MouseEvent{Type: MouseMiddle}, expected: "middle", }, { name: "release", event: MouseEvent{Type: MouseRelease}, expected: "release", }, { name: "wheel up", event: MouseEvent{Type: MouseWheelUp}, expected: "wheel up", }, { name: "wheel down", event: MouseEvent{Type: MouseWheelDown}, expected: "wheel down", }, { name: "motion", event: MouseEvent{Type: MouseMotion}, expected: "motion", }, { name: "alt+left", event: MouseEvent{ Type: MouseLeft, Alt: true, }, expected: "alt+left", }, { name: "ctrl+left", event: MouseEvent{ Type: MouseLeft, Ctrl: true, }, expected: "ctrl+left", }, { name: "ctrl+alt+left", event: MouseEvent{ Type: MouseLeft, Alt: true, Ctrl: true, }, expected: "ctrl+alt+left", }, { name: "ignore coordinates", event: MouseEvent{ X: 100, Y: 200, Type: MouseLeft, }, expected: "left", }, { name: "broken type", event: MouseEvent{ Type: MouseEventType(-1000), }, expected: "", }, } for i := range tt { tc := tt[i] t.Run(tc.name, func(t *testing.T) { actual := tc.event.String() if tc.expected != actual { t.Fatalf("expected %q but got %q", tc.expected, actual, ) } }) } } func TestParseX10MouseEvent(t *testing.T) { encode := func(b byte, x, y int) []byte { return []byte{ '\x1b', '[', 'M', byte(32) + b, byte(x + 32 + 1), byte(y + 32 + 1), } } tt := []struct { name string buf []byte expected MouseEvent }{ // Position. { name: "zero position", buf: encode(0b0010_0000, 0, 0), expected: MouseEvent{ X: 0, Y: 0, Type: MouseLeft, }, }, { name: "max position", buf: encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1. expected: MouseEvent{ X: 222, Y: 222, Type: MouseLeft, }, }, // Simple. { name: "left", buf: encode(0b0000_0000, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseLeft, }, }, { name: "left in motion", buf: encode(0b0010_0000, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseLeft, }, }, { name: "middle", buf: encode(0b0000_0001, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseMiddle, }, }, { name: "middle in motion", buf: encode(0b0010_0001, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseMiddle, }, }, { name: "right", buf: encode(0b0000_0010, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseRight, }, }, { name: "right in motion", buf: encode(0b0010_0010, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseRight, }, }, { name: "motion", buf: encode(0b0010_0011, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseMotion, }, }, { name: "wheel up", buf: encode(0b0100_0000, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseWheelUp, }, }, { name: "wheel down", buf: encode(0b0100_0001, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseWheelDown, }, }, { name: "release", buf: encode(0b0000_0011, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseRelease, }, }, // Combinations. { name: "alt+right", buf: encode(0b0010_1010, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseRight, Alt: true, }, }, { name: "ctrl+right", buf: encode(0b0011_0010, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseRight, Ctrl: true, }, }, { name: "ctrl+alt+right", buf: encode(0b0011_1010, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseRight, Alt: true, Ctrl: true, }, }, { name: "alt+wheel down", buf: encode(0b0100_1001, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseWheelDown, Alt: true, }, }, { name: "ctrl+wheel down", buf: encode(0b0101_0001, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseWheelDown, Ctrl: true, }, }, { name: "ctrl+alt+wheel down", buf: encode(0b0101_1001, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseWheelDown, Alt: true, Ctrl: true, }, }, // Unknown. { name: "wheel with unknown bit", buf: encode(0b0100_0010, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseUnknown, }, }, { name: "unknown with modifier", buf: encode(0b0100_1010, 32, 16), expected: MouseEvent{ X: 32, Y: 16, Type: MouseUnknown, Alt: true, }, }, // Overflow position. { name: "overflow position", buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1. expected: MouseEvent{ X: -6, Y: -33, Type: MouseLeft, }, }, } for i := range tt { tc := tt[i] t.Run(tc.name, func(t *testing.T) { actual, err := parseX10MouseEvent(tc.buf) if err != nil { t.Fatalf("unexpected error: %v", err, ) } if tc.expected != actual { t.Fatalf("expected %#v but got %#v", tc.expected, actual, ) } }) } } func TestParseX10MouseEvent_error(t *testing.T) { tt := []struct { name string buf []byte }{ { name: "empty buf", buf: nil, }, { name: "wrong high bit", buf: []byte("\x1a[M@A1"), }, { name: "short buf", buf: []byte("\x1b[M@A"), }, { name: "long buf", buf: []byte("\x1b[M@A11"), }, } for i := range tt { tc := tt[i] t.Run(tc.name, func(t *testing.T) { _, err := parseX10MouseEvent(tc.buf) if err == nil { t.Fatalf("expected error but got nil") } }) } } bubbletea-0.19.1/nil_renderer.go000066400000000000000000000005611414053472700165370ustar00rootroot00000000000000package tea type nilRenderer struct{} func (n nilRenderer) start() {} func (n nilRenderer) stop() {} func (n nilRenderer) kill() {} func (n nilRenderer) write(v string) {} func (n nilRenderer) repaint() {} func (n nilRenderer) altScreen() bool { return false } func (n nilRenderer) setAltScreen(v bool) {} bubbletea-0.19.1/options.go000066400000000000000000000106671414053472700155720ustar00rootroot00000000000000package tea import "io" // ProgramOption is used to set options when initializing a Program. Program can // accept a variable number of options. // // Example usage: // // p := NewProgram(model, WithInput(someInput), WithOutput(someOutput)) // type ProgramOption func(*Program) // WithOutput sets the output which, by default, is stdout. In most cases you // won't need to use this. func WithOutput(output io.Writer) ProgramOption { return func(m *Program) { m.output = output } } // WithInput sets the input which, by default, is stdin. In most cases you // won't need to use this. func WithInput(input io.Reader) ProgramOption { return func(m *Program) { m.input = input m.startupOptions |= withCustomInput } } // WithInputTTY open a new TTY for input (or console input device on Windows). func WithInputTTY() ProgramOption { return func(p *Program) { p.startupOptions |= withInputTTY } } // WithoutCatchPanics disables the panic catching that Bubble Tea does by // default. If panic catching is disabled the terminal will be in a fairly // unusable state after a panic because Bubble Tea will not perform its usual // cleanup on exit. func WithoutCatchPanics() ProgramOption { return func(m *Program) { m.CatchPanics = false } } // WithAltScreen starts the program with the alternate screen buffer enabled // (i.e. the program starts in full window mode). Note that the altscreen will // be automatically exited when the program quits. // // Example: // // p := tea.NewProgram(Model{}, tea.WithAltScreen()) // if err := p.Start(); err != nil { // fmt.Println("Error running program:", err) // os.Exit(1) // } // // To enter the altscreen once the program has already started running use the // EnterAltScreen command. func WithAltScreen() ProgramOption { return func(p *Program) { p.startupOptions |= withAltScreen } } // WithMouseCellMotion starts the program with the mouse enabled in "cell // motion" mode. // // Cell motion mode enables mouse click, release, and wheel events. Mouse // movement events are also captured if a mouse button is pressed (i.e., drag // events). Cell motion mode is better supported than all motion mode. // // To enable mouse cell motion once the program has already started running use // the EnableMouseCellMotion command. To disable the mouse when the program is // running use the DisableMouse command. // // The mouse will be automatically disabled when the program exits. func WithMouseCellMotion() ProgramOption { return func(p *Program) { p.startupOptions |= withMouseCellMotion // set p.startupOptions &^= withMouseAllMotion // clear } } // WithMouseAllMotion starts the program with the mouse enabled in "all motion" // mode. // // EnableMouseAllMotion is a special command that enables mouse click, release, // wheel, and motion events, which are delivered regardless of whether a mouse // button is pressed, effectively enabling support for hover interactions. // // Many modern terminals support this, but not all. If in doubt, use // EnableMouseCellMotion instead. // // To enable the mouse once the program has already started running use the // EnableMouseAllMotion command. To disable the mouse when the program is // running use the DisableMouse command. // // The mouse will be automatically disabled when the program exits. func WithMouseAllMotion() ProgramOption { return func(p *Program) { p.startupOptions |= withMouseAllMotion // set p.startupOptions &^= withMouseCellMotion // clear } } // WithoutRenderer disables the renderer. When this is set output and log // statements will be plainly sent to stdout (or another output if one is set) // without any rendering and redrawing logic. In other words, printing and // logging will behave the same way it would in a non-TUI commandline tool. // This can be useful if you want to use the Bubble Tea framework for a non-TUI // application, or to provide an additional non-TUI mode to your Bubble Tea // programs. For example, your program could behave like a daemon if output is // not a TTY. func WithoutRenderer() ProgramOption { return func(m *Program) { m.renderer = &nilRenderer{} } } // WithANSICompressor removes redundant ANSI sequences to produce potentially // smaller output, at the cost of some processing overhead. // // This feature is provisional, and may be changed removed in a future version // of this package. func WithANSICompressor() ProgramOption { return func(p *Program) { p.startupOptions |= withANSICompressor } } bubbletea-0.19.1/renderer.go000066400000000000000000000012311414053472700156700ustar00rootroot00000000000000package tea // renderer is the interface for Bubble Tea renderers. type renderer interface { // Start the renderer. start() // Stop the renderer, but render the final frame in the buffer, if any. stop() // Stop the renderer without doing any final rendering. kill() // Write a frame to the renderer. The renderer can write this data to // output at its discretion. write(string) // Request a full re-render. repaint() // Whether or not the alternate screen buffer is enabled. altScreen() bool // Record internally that the alternate screen buffer is enabled. This // does not actually toggle the alternate screen buffer. setAltScreen(bool) } bubbletea-0.19.1/screen.go000066400000000000000000000015231414053472700153450ustar00rootroot00000000000000package tea import ( "fmt" "io" te "github.com/muesli/termenv" ) func hideCursor(w io.Writer) { fmt.Fprintf(w, te.CSI+te.HideCursorSeq) } func showCursor(w io.Writer) { fmt.Fprintf(w, te.CSI+te.ShowCursorSeq) } func clearLine(w io.Writer) { fmt.Fprintf(w, te.CSI+te.EraseLineSeq, 2) } func cursorUp(w io.Writer) { fmt.Fprintf(w, te.CSI+te.CursorUpSeq, 1) } func cursorDown(w io.Writer) { fmt.Fprintf(w, te.CSI+te.CursorDownSeq, 1) } func insertLine(w io.Writer, numLines int) { fmt.Fprintf(w, te.CSI+"%dL", numLines) } func moveCursor(w io.Writer, row, col int) { fmt.Fprintf(w, te.CSI+te.CursorPositionSeq, row, col) } func changeScrollingRegion(w io.Writer, top, bottom int) { fmt.Fprintf(w, te.CSI+te.ChangeScrollingRegionSeq, top, bottom) } func cursorBack(w io.Writer, n int) { fmt.Fprintf(w, te.CSI+te.CursorBackSeq, n) } bubbletea-0.19.1/signals_unix.go000066400000000000000000000015541414053472700165750ustar00rootroot00000000000000//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris // +build darwin dragonfly freebsd linux netbsd openbsd solaris package tea import ( "context" "os" "os/signal" "syscall" "golang.org/x/term" ) // listenForResize sends messages (or errors) when the terminal resizes. // Argument output should be the file descriptor for the terminal; usually // os.Stdout. func listenForResize(ctx context.Context, output *os.File, msgs chan Msg, errs chan error, done chan struct{}) { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGWINCH) defer func() { signal.Stop(sig) close(done) }() for { select { case <-ctx.Done(): return case <-sig: } w, h, err := term.GetSize(int(output.Fd())) if err != nil { errs <- err } select { case <-ctx.Done(): return case msgs <- WindowSizeMsg{w, h}: } } } bubbletea-0.19.1/signals_windows.go000066400000000000000000000004701414053472700173000ustar00rootroot00000000000000//go:build windows // +build windows package tea import ( "context" "os" ) // listenForResize is not available on windows because windows does not // implement syscall.SIGWINCH. func listenForResize(ctx context.Context, output *os.File, msgs chan Msg, errs chan error, done chan struct{}) { close(done) } bubbletea-0.19.1/standard_renderer.go000066400000000000000000000303631414053472700175600ustar00rootroot00000000000000package tea import ( "bytes" "io" "strings" "sync" "time" "github.com/muesli/ansi/compressor" "github.com/muesli/reflow/truncate" ) const ( // defaultFramerate specifies the maximum interval at which we should // update the view. defaultFramerate = time.Second / 60 ) // standardRenderer is a framerate-based terminal renderer, updating the view // at a given framerate to avoid overloading the terminal emulator. // // In cases where very high performance is needed the renderer can be told // to exclude ranges of lines, allowing them to be written to directly. type standardRenderer struct { out io.Writer buf bytes.Buffer framerate time.Duration ticker *time.Ticker mtx *sync.Mutex done chan struct{} lastRender string linesRendered int useANSICompressor bool // essentially whether or not we're using the full size of the terminal altScreenActive bool // renderer dimensions; usually the size of the window width int height int // lines explicitly set not to render ignoreLines map[int]struct{} } // newRenderer creates a new renderer. Normally you'll want to initialize it // with os.Stdout as the first argument. func newRenderer(out io.Writer, mtx *sync.Mutex, useANSICompressor bool) renderer { r := &standardRenderer{ out: out, mtx: mtx, framerate: defaultFramerate, useANSICompressor: useANSICompressor, } if r.useANSICompressor { r.out = &compressor.Writer{Forward: out} } return r } // start starts the renderer. func (r *standardRenderer) start() { if r.ticker == nil { r.ticker = time.NewTicker(r.framerate) } r.done = make(chan struct{}) go r.listen() } // stop permanently halts the renderer, rendering the final frame. func (r *standardRenderer) stop() { r.flush() clearLine(r.out) close(r.done) if r.useANSICompressor { if w, ok := r.out.(io.WriteCloser); ok { _ = w.Close() } } } // kill halts the renderer. The final frame will not be rendered. func (r *standardRenderer) kill() { clearLine(r.out) close(r.done) } // listen waits for ticks on the ticker, or a signal to stop the renderer. func (r *standardRenderer) listen() { for { select { case <-r.ticker.C: if r.ticker != nil { r.flush() } case <-r.done: r.ticker.Stop() r.ticker = nil return } } } // flush renders the buffer. func (r *standardRenderer) flush() { r.mtx.Lock() defer r.mtx.Unlock() if r.buf.Len() == 0 || r.buf.String() == r.lastRender { // Nothing to do return } // Output buffer out := new(bytes.Buffer) newLines := strings.Split(r.buf.String(), "\n") oldLines := strings.Split(r.lastRender, "\n") skipLines := make(map[int]struct{}) // Clear any lines we painted in the last render. if r.linesRendered > 0 { for i := r.linesRendered - 1; i > 0; i-- { // If the number of lines we want to render hasn't increased and // new line is the same as the old line we can skip rendering for // this line as a performance optimization. if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) { skipLines[i] = struct{}{} } else if _, exists := r.ignoreLines[i]; !exists { clearLine(out) } cursorUp(out) } if _, exists := r.ignoreLines[0]; !exists { // We need to return to the start of the line here to properly // erase it. Going back the entire width of the terminal will // usually be farther than we need to go, but terminal emulators // will stop the cursor at the start of the line as a rule. // // We use this sequence in particular because it's part of the ANSI // standard (whereas others are proprietary to, say, VT100/VT52). // If cursor previous line (ESC[ + + F) were better supported // we could use that above to eliminate this step. cursorBack(out, r.width) clearLine(out) } } // Merge the set of lines we're skipping as a rendering optimization with // the set of lines we've explicitly asked the renderer to ignore. if r.ignoreLines != nil { for k, v := range r.ignoreLines { skipLines[k] = v } } r.linesRendered = 0 // Paint new lines for i := 0; i < len(newLines); i++ { if _, skip := skipLines[r.linesRendered]; skip { // Unless this is the last line, move the cursor down. if i < len(newLines)-1 { cursorDown(out) } } else { line := newLines[i] // Truncate lines wider than the width of the window to avoid // wrapping, which will mess up rendering. If we don't have the // width of the window this will be ignored. // // Note that on Windows we only get the width of the window on // program initialization, so after a resize this won't perform // correctly (signal SIGWINCH is not supported on Windows). if r.width > 0 { line = truncate.String(line, uint(r.width)) } _, _ = io.WriteString(out, line) if i < len(newLines)-1 { _, _ = io.WriteString(out, "\r\n") } } r.linesRendered++ } // Make sure the cursor is at the start of the last line to keep rendering // behavior consistent. if r.altScreenActive { // This case fixes a bug in macOS terminal. In other terminals the // other case seems to do the job regardless of whether or not we're // using the full terminal window. moveCursor(out, r.linesRendered, 0) } else { cursorBack(out, r.width) } _, _ = r.out.Write(out.Bytes()) r.lastRender = r.buf.String() r.buf.Reset() } // write writes to the internal buffer. The buffer will be outputted via the // ticker which calls flush(). func (r *standardRenderer) write(s string) { r.mtx.Lock() defer r.mtx.Unlock() r.buf.Reset() // If an empty string was passed we should clear existing output and // rendering nothing. Rather than introduce additional state to manage // this, we render a single space as a simple (albeit less correct) // solution. if s == "" { s = " " } _, _ = r.buf.WriteString(s) } func (r *standardRenderer) repaint() { r.lastRender = "" } func (r *standardRenderer) altScreen() bool { return r.altScreenActive } func (r *standardRenderer) setAltScreen(v bool) { r.altScreenActive = v } // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea // renderer. func (r *standardRenderer) setIgnoredLines(from int, to int) { // Lock if we're going to be clearing some lines since we don't want // anything jacking our cursor. if r.linesRendered > 0 { r.mtx.Lock() defer r.mtx.Unlock() } if r.ignoreLines == nil { r.ignoreLines = make(map[int]struct{}) } for i := from; i < to; i++ { r.ignoreLines[i] = struct{}{} } // Erase ignored lines if r.linesRendered > 0 { out := new(bytes.Buffer) for i := r.linesRendered - 1; i >= 0; i-- { if _, exists := r.ignoreLines[i]; exists { clearLine(out) } cursorUp(out) } moveCursor(out, r.linesRendered, 0) // put cursor back _, _ = r.out.Write(out.Bytes()) } } // clearIgnoredLines returns control of any ignored lines to the standard // Bubble Tea renderer. That is, any lines previously set to be ignored can be // rendered to again. func (r *standardRenderer) clearIgnoredLines() { r.ignoreLines = nil } // insertTop effectively scrolls up. It inserts lines at the top of a given // area designated to be a scrollable region, pushing everything else down. // This is roughly how ncurses does it. // // To call this function use command ScrollUp(). // // For this to work renderer.ignoreLines must be set to ignore the scrollable // region since we are bypassing the normal Bubble Tea renderer here. // // Because this method relies on the terminal dimensions, it's only valid for // full-window applications (generally those that use the alternate screen // buffer). // // This method bypasses the normal rendering buffer and is philosophically // different than the normal way we approach rendering in Bubble Tea. It's for // use in high-performance rendering, such as a pager that could potentially // be rendering very complicated ansi. In cases where the content is simpler // standard Bubble Tea rendering should suffice. func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) { r.mtx.Lock() defer r.mtx.Unlock() b := new(bytes.Buffer) changeScrollingRegion(b, topBoundary, bottomBoundary) moveCursor(b, topBoundary, 0) insertLine(b, len(lines)) _, _ = io.WriteString(b, strings.Join(lines, "\r\n")) changeScrollingRegion(b, 0, r.height) // Move cursor back to where the main rendering routine expects it to be moveCursor(b, r.linesRendered, 0) _, _ = r.out.Write(b.Bytes()) } // insertBottom effectively scrolls down. It inserts lines at the bottom of // a given area designated to be a scrollable region, pushing everything else // up. This is roughly how ncurses does it. // // To call this function use the command ScrollDown(). // // See note in insertTop() for caveats, how this function only makes sense for // full-window applications, and how it differs from the normal way we do // rendering in Bubble Tea. func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) { r.mtx.Lock() defer r.mtx.Unlock() b := new(bytes.Buffer) changeScrollingRegion(b, topBoundary, bottomBoundary) moveCursor(b, bottomBoundary, 0) _, _ = io.WriteString(b, "\r\n"+strings.Join(lines, "\r\n")) changeScrollingRegion(b, 0, r.height) // Move cursor back to where the main rendering routine expects it to be moveCursor(b, r.linesRendered, 0) _, _ = r.out.Write(b.Bytes()) } // handleMessages handles internal messages for the renderer. func (r *standardRenderer) handleMessages(msg Msg) { switch msg := msg.(type) { case WindowSizeMsg: r.mtx.Lock() r.width = msg.Width r.height = msg.Height r.mtx.Unlock() case clearScrollAreaMsg: r.clearIgnoredLines() // Force a repaint on the area where the scrollable stuff was in this // update cycle r.mtx.Lock() r.lastRender = "" r.mtx.Unlock() case syncScrollAreaMsg: // Re-render scrolling area r.clearIgnoredLines() r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary) r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary) // Force non-scrolling stuff to repaint in this update cycle r.mtx.Lock() r.lastRender = "" r.mtx.Unlock() case scrollUpMsg: r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary) case scrollDownMsg: r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary) } } // HIGH-PERFORMANCE RENDERING STUFF type syncScrollAreaMsg struct { lines []string topBoundary int bottomBoundary int } // SyncScrollArea performs a paint of the entire region designated to be the // scrollable area. This is required to initialize the scrollable region and // should also be called on resize (WindowSizeMsg). // // For high-performance, scroll-based rendering only. func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd { return func() Msg { return syncScrollAreaMsg{ lines: lines, topBoundary: topBoundary, bottomBoundary: bottomBoundary, } } } type clearScrollAreaMsg struct{} // ClearScrollArea deallocates the scrollable region and returns the control of // those lines to the main rendering routine. // // For high-performance, scroll-based rendering only. func ClearScrollArea() Msg { return clearScrollAreaMsg{} } type scrollUpMsg struct { lines []string topBoundary int bottomBoundary int } // ScrollUp adds lines to the top of the scrollable region, pushing existing // lines below down. Lines that are pushed out the scrollable region disappear // from view. // // For high-performance, scroll-based rendering only. func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd { return func() Msg { return scrollUpMsg{ lines: newLines, topBoundary: topBoundary, bottomBoundary: bottomBoundary, } } } type scrollDownMsg struct { lines []string topBoundary int bottomBoundary int } // ScrollDown adds lines to the bottom of the scrollable region, pushing // existing lines above up. Lines that are pushed out of the scrollable region // disappear from view. // // For high-performance, scroll-based rendering only. func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd { return func() Msg { return scrollDownMsg{ lines: newLines, topBoundary: topBoundary, bottomBoundary: bottomBoundary, } } } bubbletea-0.19.1/tea.go000066400000000000000000000436651414053472700146540ustar00rootroot00000000000000// Package tea provides a framework for building rich terminal user interfaces // based on the paradigms of The Elm Architecture. It's well-suited for simple // and complex terminal applications, either inline, full-window, or a mix of // both. It's been battle-tested in several large projects and is // production-ready. // // A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials // // Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples package tea import ( "context" "errors" "fmt" "io" "os" "os/signal" "runtime/debug" "sync" "syscall" "time" "github.com/containerd/console" isatty "github.com/mattn/go-isatty" te "github.com/muesli/termenv" "golang.org/x/term" ) // Msg contain data from the result of a IO operation. Msgs trigger the update // function and, henceforth, the UI. type Msg interface{} // Model contains the program's state as well as its core functions. type Model interface { // Init is the first function that will be called. It returns an optional // initial command. To not perform an initial command return nil. Init() Cmd // Update is called when a message is received. Use it to inspect messages // and, in response, update the model and/or send a command. Update(Msg) (Model, Cmd) // View renders the program's UI, which is just a string. The view is // rendered after every Update. View() string } // Cmd is an IO operation that returns a message when it's complete. If it's // nil it's considered a no-op. Use it for things like HTTP requests, timers, // saving and loading from disk, and so on. // // Note that there's almost never a reason to use a command to send a message // to another part of your program. That can almost always be done in the // update function. type Cmd func() Msg // Options to customize the program during its initialization. These are // generally set with ProgramOptions. // // The options here are treated as bits. type startupOptions byte func (s startupOptions) has(option startupOptions) bool { return s&option != 0 } const ( withAltScreen startupOptions = 1 << iota withMouseCellMotion withMouseAllMotion withInputTTY withCustomInput withANSICompressor ) // Program is a terminal user interface. type Program struct { initialModel Model // Configuration options that will set as the program is initializing, // treated as bits. These options can be set via various ProgramOptions. startupOptions startupOptions mtx *sync.Mutex msgs chan Msg output io.Writer // where to send output. this will usually be os.Stdout. input io.Reader // this will usually be os.Stdin. renderer renderer altScreenActive bool // CatchPanics is incredibly useful for restoring the terminal to a usable // state after a panic occurs. When this is set, Bubble Tea will recover // from panics, print the stack trace, and disable raw mode. This feature // is on by default. CatchPanics bool console console.Console // Stores the original reference to stdin for cases where input is not a // TTY on windows and we've automatically opened CONIN$ to receive input. // When the program exits this will be restored. // // Lint ignore note: the linter will find false positive on unix systems // as this value only comes into play on Windows, hence the ignore comment // below. windowsStdin *os.File //nolint:golint,structcheck,unused } // Batch performs a bunch of commands concurrently with no ordering guarantees // about the results. Use a Batch to return several commands. // // Example: // // func (m model) Init() Cmd { // return tea.Batch(someCommand, someOtherCommand) // } // func Batch(cmds ...Cmd) Cmd { if len(cmds) == 0 { return nil } return func() Msg { return batchMsg(cmds) } } // batchMsg is the internal message used to perform a bunch of commands. You // can send a batchMsg with Batch. type batchMsg []Cmd // Quit is a special command that tells the Bubble Tea program to exit. func Quit() Msg { return quitMsg{} } // quitMsg in an internal message signals that the program should quit. You can // send a quitMsg with Quit. type quitMsg struct{} // EnterAltScreen is a special command that tells the Bubble Tea program to // enter the alternate screen buffer. // // Because commands run asynchronously, this command should not be used in your // model's Init function. To initialize your program with the altscreen enabled // use the WithAltScreen ProgramOption instead. func EnterAltScreen() Msg { return enterAltScreenMsg{} } // enterAltScreenMsg in an internal message signals that the program should // enter alternate screen buffer. You can send a enterAltScreenMsg with // EnterAltScreen. type enterAltScreenMsg struct{} // ExitAltScreen is a special command that tells the Bubble Tea program to exit // the alternate screen buffer. This command should be used to exit the // alternate screen buffer while the program is running. // // Note that the alternate screen buffer will be automatically exited when the // program quits. func ExitAltScreen() Msg { return exitAltScreenMsg{} } // exitAltScreenMsg in an internal message signals that the program should exit // alternate screen buffer. You can send a exitAltScreenMsg with ExitAltScreen. type exitAltScreenMsg struct{} // EnableMouseCellMotion is a special command that enables mouse click, // release, and wheel events. Mouse movement events are also captured if // a mouse button is pressed (i.e., drag events). // // Because commands run asynchronously, this command should not be used in your // model's Init function. Use the WithMouseCellMotion ProgramOption instead. func EnableMouseCellMotion() Msg { return enableMouseCellMotionMsg{} } // enableMouseCellMotionMsg is a special command that signals to start // listening for "cell motion" type mouse events (ESC[?1002l). To send an // enableMouseCellMotionMsg, use the EnableMouseCellMotion command. type enableMouseCellMotionMsg struct{} // EnableMouseAllMotion is a special command that enables mouse click, release, // wheel, and motion events, which are delivered regardless of whether a mouse // button is pressed, effectively enabling support for hover interactions. // // Many modern terminals support this, but not all. If in doubt, use // EnableMouseCellMotion instead. // // Because commands run asynchronously, this command should not be used in your // model's Init function. Use the WithMouseAllMotion ProgramOption instead. func EnableMouseAllMotion() Msg { return enableMouseAllMotionMsg{} } // enableMouseAllMotionMsg is a special command that signals to start listening // for "all motion" type mouse events (ESC[?1003l). To send an // enableMouseAllMotionMsg, use the EnableMouseAllMotion command. type enableMouseAllMotionMsg struct{} // DisableMouse is a special command that stops listening for mouse events. func DisableMouse() Msg { return disableMouseMsg{} } // disableMouseMsg is an internal message that that signals to stop listening // for mouse events. To send a disableMouseMsg, use the DisableMouse command. type disableMouseMsg struct{} // WindowSizeMsg is used to report the terminal size. It's sent to Update once // initially and then on every terminal resize. Note that Windows does not // have support for reporting when resizes occur as it does not support the // SIGWINCH signal. type WindowSizeMsg struct { Width int Height int } // HideCursor is a special command for manually instructing Bubble Tea to hide // the cursor. In some rare cases, certain operations will cause the terminal // to show the cursor, which is normally hidden for the duration of a Bubble // Tea program's lifetime. You will most likely not need to use this command. func HideCursor() Msg { return hideCursorMsg{} } // hideCursorMsg is an internal command used to hide the cursor. You can send // this message with HideCursor. type hideCursorMsg struct{} // NewProgram creates a new Program. func NewProgram(model Model, opts ...ProgramOption) *Program { p := &Program{ mtx: &sync.Mutex{}, initialModel: model, output: os.Stdout, input: os.Stdin, CatchPanics: true, } // Apply all options to the program. for _, opt := range opts { opt(p) } return p } // StartReturningModel initializes the program. Returns the final model. func (p *Program) StartReturningModel() (Model, error) { p.msgs = make(chan Msg) var ( cmds = make(chan Cmd) errs = make(chan error) ) // Channels for managing goroutine lifecycles. var ( readLoopDone = make(chan struct{}) sigintLoopDone = make(chan struct{}) cmdLoopDone = make(chan struct{}) resizeLoopDone = make(chan struct{}) initSignalDone = make(chan struct{}) waitForGoroutines = func(withReadLoop bool) { if withReadLoop { select { case <-readLoopDone: case <-time.After(500 * time.Millisecond): // The read loop hangs, which means the input // cancelReader's cancel function has returned true even // though it was not able to cancel the read. } } <-cmdLoopDone <-resizeLoopDone <-sigintLoopDone <-initSignalDone } ) ctx, cancelContext := context.WithCancel(context.Background()) defer cancelContext() switch { case p.startupOptions.has(withInputTTY): // Open a new TTY, by request f, err := openInputTTY() if err != nil { return p.initialModel, err } defer f.Close() // nolint:errcheck p.input = f case !p.startupOptions.has(withCustomInput): // If the user hasn't set a custom input, and input's not a terminal, // open a TTY so we can capture input as normal. This will allow things // to "just work" in cases where data was piped or redirected into this // application. f, isFile := p.input.(*os.File) if !isFile { break } if isatty.IsTerminal(f.Fd()) { break } f, err := openInputTTY() if err != nil { return p.initialModel, err } defer f.Close() // nolint:errcheck p.input = f } // Listen for SIGINT. Note that in most cases ^C will not send an // interrupt because the terminal will be in raw mode and thus capture // that keystroke and send it along to Program.Update. If input is not a // TTY, however, ^C will be caught here. go func() { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT) defer func() { signal.Stop(sig) close(sigintLoopDone) }() select { case <-ctx.Done(): case <-sig: p.msgs <- quitMsg{} } }() if p.CatchPanics { defer func() { if r := recover(); r != nil { p.shutdown(true) fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r) debug.PrintStack() return } }() } // Check if output is a TTY before entering raw mode, hiding the cursor and // so on. if err := p.initTerminal(); err != nil { return p.initialModel, err } // If no renderer is set use the standard one. if p.renderer == nil { p.renderer = newRenderer(p.output, p.mtx, p.startupOptions.has(withANSICompressor)) } // Honor program startup options. if p.startupOptions&withAltScreen != 0 { p.EnterAltScreen() } if p.startupOptions&withMouseCellMotion != 0 { p.EnableMouseCellMotion() } else if p.startupOptions&withMouseAllMotion != 0 { p.EnableMouseAllMotion() } // Initialize the program. model := p.initialModel if initCmd := model.Init(); initCmd != nil { go func() { defer close(initSignalDone) select { case cmds <- initCmd: case <-ctx.Done(): } }() } else { close(initSignalDone) } // Start the renderer. p.renderer.start() p.renderer.setAltScreen(p.altScreenActive) // Render the initial view. p.renderer.write(model.View()) cancelReader, err := newCancelReader(p.input) if err != nil { return model, err } defer cancelReader.Close() // nolint:errcheck // Subscribe to user input. if p.input != nil { go func() { defer close(readLoopDone) for { if ctx.Err() != nil { return } msg, err := readInput(cancelReader) if err != nil { if !errors.Is(err, io.EOF) && !errors.Is(err, errCanceled) { errs <- err } return } p.msgs <- msg } }() } else { defer close(readLoopDone) } if f, ok := p.output.(*os.File); ok { // Get the initial terminal size and send it to the program. go func() { w, h, err := term.GetSize(int(f.Fd())) if err != nil { errs <- err } select { case <-ctx.Done(): case p.msgs <- WindowSizeMsg{w, h}: } }() // Listen for window resizes. go listenForResize(ctx, f, p.msgs, errs, resizeLoopDone) } else { close(resizeLoopDone) } // Process commands. go func() { defer close(cmdLoopDone) for { select { case <-ctx.Done(): return case cmd := <-cmds: if cmd == nil { continue } // Don't wait on these goroutines, otherwise the shutdown // latency would get too large as a Cmd can run for some time // (e.g. tick commands that sleep for half a second). It's not // possible to cancel them so we'll have to leak the goroutine // until Cmd returns. go func() { select { case p.msgs <- cmd(): case <-ctx.Done(): } }() } } }() // Handle updates and draw. for { select { case err := <-errs: cancelContext() waitForGoroutines(cancelReader.Cancel()) p.shutdown(false) return model, err case msg := <-p.msgs: // Handle special internal messages. switch msg := msg.(type) { case quitMsg: cancelContext() waitForGoroutines(cancelReader.Cancel()) p.shutdown(false) return model, nil case batchMsg: for _, cmd := range msg { cmds <- cmd } continue case WindowSizeMsg: p.renderer.repaint() case enterAltScreenMsg: p.EnterAltScreen() case exitAltScreenMsg: p.ExitAltScreen() case enableMouseCellMotionMsg: p.EnableMouseCellMotion() case enableMouseAllMotionMsg: p.EnableMouseAllMotion() case disableMouseMsg: p.DisableMouseCellMotion() p.DisableMouseAllMotion() case hideCursorMsg: hideCursor(p.output) } // Process internal messages for the renderer. if r, ok := p.renderer.(*standardRenderer); ok { r.handleMessages(msg) } var cmd Cmd model, cmd = model.Update(msg) // run update cmds <- cmd // process command (if any) p.renderer.write(model.View()) // send view to renderer } } } // Start initializes the program. Ignores the final model. func (p *Program) Start() error { _, err := p.StartReturningModel() return err } // Send sends a message to the main update function, effectively allowing // messages to be injected from outside the program for interoperability // purposes. // // If the program is not running this this will be a no-op, so it's safe to // send messages if the program is unstarted, or has exited. // // This method is currently provisional. The method signature may alter // slightly, or it may be removed in a future version of this package. func (p *Program) Send(msg Msg) { if p.msgs != nil { p.msgs <- msg } } // Quit is a convenience function for quitting Bubble Tea programs. Use it // when you need to shut down a Bubble Tea program from the outside. // // If you wish to quit from within a Bubble Tea program use the Quit command. // // If the program is not running this will be a no-op, so it's safe to call // if the program is unstarted or has already exited. // // This method is currently provisional. The method signature may alter // slightly, or it may be removed in a future version of this package. func (p *Program) Quit() { p.Send(Quit()) } // shutdown performs operations to free up resources and restore the terminal // to its original state. func (p *Program) shutdown(kill bool) { if kill { p.renderer.kill() } else { p.renderer.stop() } p.ExitAltScreen() p.DisableMouseCellMotion() p.DisableMouseAllMotion() _ = p.restoreTerminal() } // EnterAltScreen enters the alternate screen buffer, which consumes the entire // terminal window. ExitAltScreen will return the terminal to its former state. // // Deprecated. Use the WithAltScreen ProgramOption instead. func (p *Program) EnterAltScreen() { p.mtx.Lock() defer p.mtx.Unlock() if p.altScreenActive { return } fmt.Fprintf(p.output, te.CSI+te.AltScreenSeq) moveCursor(p.output, 0, 0) p.altScreenActive = true if p.renderer != nil { p.renderer.setAltScreen(p.altScreenActive) } } // ExitAltScreen exits the alternate screen buffer. // // Deprecated. The altscreen will exited automatically when the program exits. func (p *Program) ExitAltScreen() { p.mtx.Lock() defer p.mtx.Unlock() if !p.altScreenActive { return } fmt.Fprintf(p.output, te.CSI+te.ExitAltScreenSeq) p.altScreenActive = false if p.renderer != nil { p.renderer.setAltScreen(p.altScreenActive) } } // EnableMouseCellMotion enables mouse click, release, wheel and motion events // if a mouse button is pressed (i.e., drag events). // // Deprecated. Use the WithMouseCellMotion ProgramOption instead. func (p *Program) EnableMouseCellMotion() { p.mtx.Lock() defer p.mtx.Unlock() fmt.Fprintf(p.output, te.CSI+te.EnableMouseCellMotionSeq) } // DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be // called automatically when exiting a Bubble Tea program. // // Deprecated. The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseCellMotion() { p.mtx.Lock() defer p.mtx.Unlock() fmt.Fprintf(p.output, te.CSI+te.DisableMouseCellMotionSeq) } // EnableMouseAllMotion enables mouse click, release, wheel and motion events, // regardless of whether a mouse button is pressed. Many modern terminals // support this, but not all. // // Deprecated. Use the WithMouseAllMotion ProgramOption instead. func (p *Program) EnableMouseAllMotion() { p.mtx.Lock() defer p.mtx.Unlock() fmt.Fprintf(p.output, te.CSI+te.EnableMouseAllMotionSeq) } // DisableMouseAllMotion disables All Motion mouse tracking. This will be // called automatically when exiting a Bubble Tea program. // // Deprecated. The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseAllMotion() { p.mtx.Lock() defer p.mtx.Unlock() fmt.Fprintf(p.output, te.CSI+te.DisableMouseAllMotionSeq) } bubbletea-0.19.1/tty.go000066400000000000000000000006441414053472700147110ustar00rootroot00000000000000package tea func (p *Program) initTerminal() error { err := p.initInput() if err != nil { return err } if p.console != nil { err = p.console.SetRaw() if err != nil { return err } } hideCursor(p.output) return nil } func (p Program) restoreTerminal() error { showCursor(p.output) if p.console != nil { err := p.console.Reset() if err != nil { return err } } return p.restoreInput() } bubbletea-0.19.1/tty_unix.go000066400000000000000000000016461414053472700157570ustar00rootroot00000000000000//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris // +build darwin dragonfly freebsd linux netbsd openbsd solaris package tea import ( "os" "github.com/containerd/console" ) func (p *Program) initInput() error { // If input's a file, use console to manage it if f, ok := p.input.(*os.File); ok { c, err := console.ConsoleFromFile(f) if err != nil { return nil } p.console = c } return nil } // On unix systems, RestoreInput closes any TTYs we opened for input. Note that // we don't do this on Windows as it causes the prompt to not be drawn until // the terminal receives a keypress rather than appearing promptly after the // program exits. func (p *Program) restoreInput() error { if p.console != nil { return p.console.Reset() } return nil } func openInputTTY() (*os.File, error) { f, err := os.Open("/dev/tty") if err != nil { return nil, err } return f, nil } bubbletea-0.19.1/tty_windows.go000066400000000000000000000027761414053472700164730ustar00rootroot00000000000000//go:build windows // +build windows package tea import ( "io" "os" "github.com/containerd/console" "golang.org/x/sys/windows" ) func (p *Program) initInput() error { // If input's a file, use console to manage it if f, ok := p.input.(*os.File); ok { // Save a reference to the current stdin then replace stdin with our // input. We do this so we can hand input off to containerd/console to // set raw mode, and do it in this fashion because the method // console.ConsoleFromFile isn't supported on Windows. p.windowsStdin = os.Stdin os.Stdin = f // Note: this will panic if it fails. c := console.Current() p.console = c } enableAnsiColors(p.output) return nil } // restoreInput restores stdout in the event that we placed it aside to handle // input with CONIN$, above. func (p *Program) restoreInput() error { if p.windowsStdin != nil { os.Stdin = p.windowsStdin } return nil } // Open the Windows equivalent of a TTY. func openInputTTY() (*os.File, error) { f, err := os.OpenFile("CONIN$", os.O_RDWR, 0644) if err != nil { return nil, err } return f, nil } // enableAnsiColors enables support for ANSI color sequences in Windows // default console. Note that this only works with Windows 10. func enableAnsiColors(w io.Writer) { f, ok := w.(*os.File) if !ok { return } stdout := windows.Handle(f.Fd()) var originalMode uint32 _ = windows.GetConsoleMode(stdout, &originalMode) _ = windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) } bubbletea-0.19.1/tutorials/000077500000000000000000000000001414053472700155645ustar00rootroot00000000000000bubbletea-0.19.1/tutorials/basics/000077500000000000000000000000001414053472700170305ustar00rootroot00000000000000bubbletea-0.19.1/tutorials/basics/README.md000066400000000000000000000163451414053472700203200ustar00rootroot00000000000000Bubble Tea Basics ================= Bubble Tea is based on the functional design paradigms of [The Elm Architecture][elm] which happens work nicely with Go. It's a delightful way to build applications. By the way, the non-annotated source code for this program is available [on GitHub](https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics). This tutorial assumes you have a working knowledge of Go. [elm]: https://guide.elm-lang.org/architecture/ ## 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. ```go 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. ```go 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. ```go func initialModel() model { return model{ // Our shopping 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." ```go 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. ```go 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 it's 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. ```go 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: ```go func main() { p := tea.NewProgram(initialModel()) if err := p.Start(); 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][cmd]. It's pretty simple. There are also several [Bubble Tea examples][examples] available and, of course, there are [Go Docs][docs]. [cmd]: http://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands/ [examples]: http://github.com/charmbracelet/bubbletea/tree/master/examples [docs]: https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc ## Additional Resources * [Libraries we use with Bubble Tea](https://github.com/charmbracelet/bubbletea/#libraries-we-use-with-bubble-tea) * [Bubble Tea in the Wild](https://github.com/charmbracelet/bubbletea/#bubble-tea-in-the-wild) ### Feedback We'd love to hear your thoughts on this tutorial. Feel free to drop us a note! * [Twitter](https://twitter.com/charmcli) * [The Fediverse](https://mastodon.technology/@charm) *** Part of [Charm](https://charm.sh). The Charm logo Charm热爱开源 • Charm loves open source bubbletea-0.19.1/tutorials/basics/main.go000066400000000000000000000027731414053472700203140ustar00rootroot00000000000000package main import ( "fmt" "os" tea "github.com/charmbracelet/bubbletea" ) type model struct { cursor int choices []string selected map[int]struct{} } func initialModel() model { return model{ 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{}), } } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.choices)-1 { m.cursor++ } case "enter", " ": _, ok := m.selected[m.cursor] if ok { delete(m.selected, m.cursor) } else { m.selected[m.cursor] = struct{}{} } } } return m, nil } func (m model) View() string { s := "What should we buy at the market?\n\n" for i, choice := range m.choices { cursor := " " if m.cursor == i { cursor = ">" } checked := " " if _, ok := m.selected[i]; ok { checked = "x" } s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice) } s += "\nPress q to quit.\n" return s } func main() { p := tea.NewProgram(initialModel()) if err := p.Start(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } } bubbletea-0.19.1/tutorials/commands/000077500000000000000000000000001414053472700173655ustar00rootroot00000000000000bubbletea-0.19.1/tutorials/commands/README.md000066400000000000000000000155121414053472700206500ustar00rootroot00000000000000Commands in Bubble Tea ====================== This is the second tutorial for Bubble Tea covering commands, which deal with I/O. The tutorial assumes you have a working knowlege of Go and a decent understanding of [the first tutorial][basics]. You can find the non-annotated version of this program [on GitHub][source]. [basics]: http://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics [source]: https://github.com/charmbracelet/bubbletea/master/tutorials/commands ## Let's Go! For this tutorial we're building a very simple program that makes an HTTP request to a server and reports the status code of the response. We'll import a few necessary packages and put the URL we're going to check in a `const`. ```go package main import ( "fmt" "net/http" "os" "time" tea "github.com/charmbracelet/bubbletea" ) const url = "https://charm.sh/" ``` ## The Model Next we'll define our model. The only things we need to store are the status code of the HTTP response and a possible error. ```go type model struct { status int err error } ``` ## Commands and Messages `Cmd`s are functions that perform some I/O and then return a `Msg`. Checking the time, ticking a timer, reading from the disk, and network stuff are all I/O and should be run through commands. That might sound harsh, but it will keep your Bubble Tea program staightforward and simple. Anyway, let's write a `Cmd` that makes a request to a server and returns the result as a `Msg`. ```go func checkServer() tea.Msg { // Create an HTTP client and make a GET request. c := &http.Client{Timeout: 10 * time.Second} res, err := c.Get(url) if err != nil { // There was an error making our request. Wrap the error we received // in a message and return it. return errMsg{err} } // We received a response from the server. Return the HTTP status code // as a message. return statusMsg(res.StatusCode) } type statusMsg int type errMsg struct{ err error } // For messages that contain errors it's often handy to also implement the // error interface on the message. func (e errMsg) Error() string { return e.err.Error() } ``` And notice that we've defined two new `Msg` types. They can be any type, even an empty struct. We'll come back to them later later in our update function. First, let's write our initialization function. ## The Initialization Method The initilization method is very simple: we return the `Cmd` we made earlier. Note that we don't call the function; the Bubble Tea runtime will do that when the time is right. ```go func (m model) Init() (tea.Cmd) { return checkServer } ``` ## The Update Method Internally, `Cmd`s run asynchronously in a goroutine. The `Msg` they return is collected and sent to our update function for handling. Remember those message types we made earlier when we were making the `checkServer` command? We handle them here. This makes dealing with many asynchronous operations very easy. ```go func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case statusMsg: // The server returned a status message. Save it to our model. Also // tell the Bubble Tea runtime we want to exit because we have nothing // else to do. We'll still be able to render a final view with our // status message. m.status = int(msg) return m, tea.Quit case errMsg: // There was an error. Note it in the model. And tell the runtime // we're done and want to quit. m.err = msg return m, tea.Quit case tea.KeyMsg: // Ctrl+c exits. Even with short running programs it's good to have // a quit key, just incase your logic is off. Users will be very // annoyed if they can't exit. if msg.Type == tea.KeyCtrlC { return m, tea.Quit } } // If we happen to get any other messages, don't do anything. return m, nil } ``` ## The View Function Our view is very straightforward. We look at the current model and build a string accordingly: ```go func (m model) View() string { // If there's an error, print it out and don't do anything else. if m.err != nil { return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err) } // Tell the user we're doing something. s := fmt.Sprintf("Checking %s ... ", url) // When the server responds with a status, add it to the current line. if m.status > 0 { s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status)) } // Send off whatever we came up with above for rendering. return "\n" + s + "\n\n" } ``` ## Run the program The only thing left to do is run the program, so let's do that! Our initial model doesn't need any data at all in this case, we just initialize it with as a `struct` with defaults. ```go func main() { if err := tea.NewProgram(model{}).Start(); err != nil { fmt.Printf("Uh oh, there was an error: %v\n", err) os.Exit(1) } } ``` And that's that. There's one more thing you that is helpful to know about `Cmd`s, though. ## One More Thing About Commands `Cmd`s are defined in Bubble Tea as `type Cmd func() Msg`. So they're just functions that don't take any arguments and return a `Msg`, which can be any type. If you need to pass arguments to a command, you just make a function that returns a command. For example: ```go func cmdWithArg(id int) tea.Cmd { return func() tea.Msg { return someMsg{id: int} } } ``` A more real-world example looks like: ```go func checkSomeUrl(url string) tea.Cmd { return func() tea.Msg { c := &http.Client{Timeout: 10 * time.Second} res, err := c.Get(url) if err != nil { return errMsg(err) } return statusMsg(res.StatusCode) } } ``` Anyway, just make sure you do as much stuff as you can in the innermost function, because that's the one that runs asynchronously. ## Now What? After doing this tutorial and [the previous one][basics] you should be ready to build a Bubble Tea program of your own. We also recommend that you look at the Bubble Tea [example programs][examples] as well as [Bubbles][bubbles], a component library for Bubble Tea. And, of course, check out the [Go Docs][docs]. ## Additional Resources * [Libraries we use with Bubble Tea](https://github.com/charmbracelet/bubbletea/#libraries-we-use-with-bubble-tea) * [Bubble Tea in the Wild](https://github.com/charmbracelet/bubbletea/#bubble-tea-in-the-wild) ### Feedback We'd love to hear your thoughts on this tutorial. Feel free to drop us a note! * [Twitter](https://twitter.com/charmcli) * [The Fediverse](https://mastodon.technology/@charm) *** Part of [Charm](https://charm.sh). The Charm logo Charm热爱开源 • Charm loves open source bubbletea-0.19.1/tutorials/commands/main.go000066400000000000000000000025051414053472700206420ustar00rootroot00000000000000package main import ( "fmt" "net/http" "os" "time" tea "github.com/charmbracelet/bubbletea" ) const url = "https://charm.sh/" type model struct { status int err error } func checkServer() tea.Msg { c := &http.Client{Timeout: 10 * time.Second} res, err := c.Get(url) if err != nil { return errMsg{err} } return statusMsg(res.StatusCode) } type statusMsg int type errMsg struct{ err error } // For messages that contain errors it's often handy to also implement the // error interface on the message. func (e errMsg) Error() string { return e.err.Error() } func (m model) Init() tea.Cmd { return checkServer } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case statusMsg: m.status = int(msg) return m, tea.Quit case errMsg: m.err = msg return m, tea.Quit case tea.KeyMsg: if msg.Type == tea.KeyCtrlC { return m, tea.Quit } } return m, nil } func (m model) View() string { if m.err != nil { return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err) } s := fmt.Sprintf("Checking %s ... ", url) if m.status > 0 { s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status)) } return "\n" + s + "\n\n" } func main() { if err := tea.NewProgram(model{}).Start(); err != nil { fmt.Printf("Uh oh, there was an error: %v\n", err) os.Exit(1) } } bubbletea-0.19.1/tutorials/go.mod000066400000000000000000000002001414053472700166620ustar00rootroot00000000000000module tutorial go 1.14 require github.com/charmbracelet/bubbletea v0.17.0 replace github.com/charmbracelet/bubbletea => ../ bubbletea-0.19.1/tutorials/go.sum000066400000000000000000000045431414053472700167250ustar00rootroot00000000000000github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=