pax_global_header 0000666 0000000 0000000 00000000064 14140534727 0014521 g ustar 00root root 0000000 0000000 52 comment=a55bf775cd3fb2418d6ac3498a7792d773b8643e
bubbletea-0.19.1/ 0000775 0000000 0000000 00000000000 14140534727 0013536 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/.github/ 0000775 0000000 0000000 00000000000 14140534727 0015076 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14140534727 0017133 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/.github/workflows/build.yml 0000664 0000000 0000000 00000001350 14140534727 0020754 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001321 14140534727 0021446 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001056 14140534727 0020626 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000654 14140534727 0015533 0 ustar 00root root 0000000 0000000 .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.yml 0000664 0000000 0000000 00000000712 14140534727 0016122 0 ustar 00root root 0000000 0000000 run:
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/LICENSE 0000664 0000000 0000000 00000002063 14140534727 0014544 0 ustar 00root root 0000000 0000000 MIT 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.md 0000664 0000000 0000000 00000027072 14140534727 0015025 0 ustar 00root root 0000000 0000000 Bubble Tea
==========

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 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.
* * *
## 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).
Charm热爱开源 • Charm loves open source
bubbletea-0.19.1/cancelreader.go 0000664 0000000 0000000 00000003207 14140534727 0016477 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006202 14140534727 0017325 0 ustar 00root root 0000000 0000000 // +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.go 0000664 0000000 0000000 00000000646 14140534727 0020207 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000006274 14140534727 0017725 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000006046 14140534727 0020042 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000001236 14140534727 0017542 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000015256 14140534727 0020260 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000004131 14140534727 0015665 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002550 14140534727 0016727 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0015354 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/altscreen-toggle/ 0000775 0000000 0000000 00000000000 14140534727 0020613 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/altscreen-toggle/main.go 0000664 0000000 0000000 00000002420 14140534727 0022064 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017374 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/countdown/main.go 0000664 0000000 0000000 00000002145 14140534727 0020651 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017516 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/fullscreen/main.go 0000664 0000000 0000000 00000001675 14140534727 0021002 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017022 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/glamour/main.go 0000664 0000000 0000000 00000005250 14140534727 0020277 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000001025 14140534727 0016460 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000021716 14140534727 0016516 0 ustar 00root root 0000000 0000000 github.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/ 0000775 0000000 0000000 00000000000 14140534727 0016304 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/help/main.go 0000664 0000000 0000000 00000006353 14140534727 0017566 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0016333 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/http/main.go 0000664 0000000 0000000 00000002451 14140534727 0017610 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017751 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/list-default/main.go 0000664 0000000 0000000 00000005305 14140534727 0021227 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017425 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/list-fancy/delegate.go 0000664 0000000 0000000 00000003427 14140534727 0021534 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010652 14140534727 0020704 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004601 14140534727 0022277 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017616 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/list-simple/main.go 0000664 0000000 0000000 00000005715 14140534727 0021101 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0016504 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/mouse/main.go 0000664 0000000 0000000 00000001654 14140534727 0017765 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0016452 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/pager/artichoke.md 0000664 0000000 0000000 00000003102 14140534727 0020741 0 ustar 00root root 0000000 0000000 Glow
====
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.go 0000664 0000000 0000000 00000010613 14140534727 0017726 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017340 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/paginator/main.go 0000664 0000000 0000000 00000003154 14140534727 0020616 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0016311 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/pipe/main.go 0000664 0000000 0000000 00000003434 14140534727 0017570 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0021000 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/progress-animated/main.go 0000664 0000000 0000000 00000003763 14140534727 0022264 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0020505 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/progress-static/main.go 0000664 0000000 0000000 00000004003 14140534727 0021755 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017156 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/realtime/main.go 0000664 0000000 0000000 00000004221 14140534727 0020430 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0016672 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/result/main.go 0000664 0000000 0000000 00000003477 14140534727 0020160 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017071 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/send-msg/main.go 0000664 0000000 0000000 00000005722 14140534727 0020352 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0016645 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/simple/main.go 0000664 0000000 0000000 00000003417 14140534727 0020125 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017032 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/spinner/main.go 0000664 0000000 0000000 00000002422 14140534727 0020305 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017215 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/spinners/main.go 0000664 0000000 0000000 00000003560 14140534727 0020474 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017420 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/textinput/main.go 0000664 0000000 0000000 00000002307 14140534727 0020675 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017603 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/textinputs/main.go 0000664 0000000 0000000 00000007665 14140534727 0021074 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0020513 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/tui-daemon-combo/main.go 0000664 0000000 0000000 00000005636 14140534727 0022000 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0016511 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/examples/views/main.go 0000664 0000000 0000000 00000015300 14140534727 0017763 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000000563 14140534727 0014650 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000004543 14140534727 0014677 0 ustar 00root root 0000000 0000000 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/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.go 0000664 0000000 0000000 00000024473 14140534727 0014667 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003256 14140534727 0015722 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001624 14140534727 0015516 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001005 14140534727 0016546 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005353 14140534727 0015223 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014551 14140534727 0016262 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000561 14140534727 0016537 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010667 14140534727 0015572 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001231 14140534727 0015670 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001523 14140534727 0015345 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001554 14140534727 0016575 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000000470 14140534727 0017300 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000030363 14140534727 0017560 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000043665 14140534727 0014654 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000000644 14140534727 0014711 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001646 14140534727 0015757 0 ustar 00root root 0000000 0000000 //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.go 0000664 0000000 0000000 00000002776 14140534727 0016473 0 ustar 00root root 0000000 0000000 //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/ 0000775 0000000 0000000 00000000000 14140534727 0015564 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/tutorials/basics/ 0000775 0000000 0000000 00000000000 14140534727 0017030 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/tutorials/basics/README.md 0000664 0000000 0000000 00000016345 14140534727 0020320 0 ustar 00root root 0000000 0000000 Bubble 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).
Charm热爱开源 • Charm loves open source
bubbletea-0.19.1/tutorials/basics/main.go 0000664 0000000 0000000 00000002773 14140534727 0020314 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14140534727 0017365 5 ustar 00root root 0000000 0000000 bubbletea-0.19.1/tutorials/commands/README.md 0000664 0000000 0000000 00000015512 14140534727 0020650 0 ustar 00root root 0000000 0000000 Commands 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).
Charm热爱开源 • Charm loves open source
bubbletea-0.19.1/tutorials/commands/main.go 0000664 0000000 0000000 00000002505 14140534727 0020642 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000000200 14140534727 0016662 0 ustar 00root root 0000000 0000000 module tutorial
go 1.14
require github.com/charmbracelet/bubbletea v0.17.0
replace github.com/charmbracelet/bubbletea => ../
bubbletea-0.19.1/tutorials/go.sum 0000664 0000000 0000000 00000004543 14140534727 0016725 0 ustar 00root root 0000000 0000000 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/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=