Showing preview only (447K chars total). Download the full file or copy to clipboard to get everything.
Repository: charmbracelet/bubbletea
Branch: main
Commit: f8926a7aa8a5
Files: 191
Total size: 406.2 KB
Directory structure:
gitextract_mfu3e63q/
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.yml
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ ├── coverage.yml
│ ├── dependabot-sync.yml
│ ├── examples.yml
│ ├── lint-sync.yml
│ ├── lint.yml
│ └── release.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── LICENSE
├── README.md
├── Taskfile.yaml
├── UPGRADE_GUIDE_V2.md
├── clipboard.go
├── color.go
├── commands.go
├── commands_test.go
├── cursed_renderer.go
├── cursor.go
├── environ.go
├── examples/
│ ├── README.md
│ ├── altscreen-toggle/
│ │ ├── README.md
│ │ └── main.go
│ ├── autocomplete/
│ │ └── main.go
│ ├── canvas/
│ │ └── main.go
│ ├── capability/
│ │ └── main.go
│ ├── cellbuffer/
│ │ └── main.go
│ ├── chat/
│ │ ├── README.md
│ │ └── main.go
│ ├── clickable/
│ │ ├── main.go
│ │ └── words.go
│ ├── colorprofile/
│ │ └── main.go
│ ├── composable-views/
│ │ ├── README.md
│ │ └── main.go
│ ├── cursor-style/
│ │ └── main.go
│ ├── debounce/
│ │ ├── README.md
│ │ └── main.go
│ ├── doom-fire/
│ │ └── main.go
│ ├── exec/
│ │ ├── README.md
│ │ └── main.go
│ ├── eyes/
│ │ └── main.go
│ ├── file-picker/
│ │ └── main.go
│ ├── focus-blur/
│ │ └── main.go
│ ├── fullscreen/
│ │ ├── README.md
│ │ └── main.go
│ ├── glamour/
│ │ ├── README.md
│ │ └── main.go
│ ├── go.mod
│ ├── go.sum
│ ├── help/
│ │ ├── README.md
│ │ └── main.go
│ ├── http/
│ │ ├── README.md
│ │ └── main.go
│ ├── isbn-form/
│ │ ├── isbn-form.tape
│ │ └── main.go
│ ├── keyboard-enhancements/
│ │ └── main.go
│ ├── list-default/
│ │ ├── README.md
│ │ └── main.go
│ ├── list-fancy/
│ │ ├── README.md
│ │ ├── delegate.go
│ │ ├── main.go
│ │ └── randomitems.go
│ ├── list-simple/
│ │ ├── README.md
│ │ └── main.go
│ ├── mouse/
│ │ └── main.go
│ ├── package-manager/
│ │ ├── README.md
│ │ ├── main.go
│ │ └── packages.go
│ ├── pager/
│ │ ├── README.md
│ │ ├── artichoke.md
│ │ └── main.go
│ ├── paginator/
│ │ ├── README.md
│ │ └── main.go
│ ├── pipe/
│ │ ├── README.md
│ │ └── main.go
│ ├── prevent-quit/
│ │ └── main.go
│ ├── print-key/
│ │ └── main.go
│ ├── progress-animated/
│ │ ├── README.md
│ │ └── main.go
│ ├── progress-bar/
│ │ └── main.go
│ ├── progress-download/
│ │ ├── README.md
│ │ ├── main.go
│ │ └── tui.go
│ ├── progress-static/
│ │ ├── README.md
│ │ └── main.go
│ ├── query-term/
│ │ └── main.go
│ ├── realtime/
│ │ ├── README.md
│ │ └── main.go
│ ├── result/
│ │ ├── README.md
│ │ └── main.go
│ ├── send-msg/
│ │ ├── README.md
│ │ └── main.go
│ ├── sequence/
│ │ ├── README.md
│ │ └── main.go
│ ├── set-terminal-color/
│ │ └── main.go
│ ├── set-window-title/
│ │ └── main.go
│ ├── simple/
│ │ ├── README.md
│ │ ├── main.go
│ │ ├── main_test.go
│ │ └── testdata/
│ │ └── TestApp.golden
│ ├── space/
│ │ └── main.go
│ ├── spinner/
│ │ ├── README.md
│ │ └── main.go
│ ├── spinners/
│ │ ├── README.md
│ │ └── main.go
│ ├── splash/
│ │ └── main.go
│ ├── split-editors/
│ │ ├── README.md
│ │ └── main.go
│ ├── stopwatch/
│ │ ├── README.md
│ │ └── main.go
│ ├── suspend/
│ │ └── main.go
│ ├── table/
│ │ ├── README.md
│ │ └── main.go
│ ├── table-resize/
│ │ └── main.go
│ ├── tabs/
│ │ ├── README.md
│ │ └── main.go
│ ├── textarea/
│ │ ├── README.md
│ │ └── main.go
│ ├── textinput/
│ │ ├── README.md
│ │ └── main.go
│ ├── textinputs/
│ │ ├── README.md
│ │ └── main.go
│ ├── timer/
│ │ ├── README.md
│ │ └── main.go
│ ├── tui-daemon-combo/
│ │ ├── README.md
│ │ └── main.go
│ ├── vanish/
│ │ └── main.go
│ ├── views/
│ │ ├── README.md
│ │ └── main.go
│ └── window-size/
│ └── main.go
├── exec.go
├── exec_test.go
├── focus.go
├── go.mod
├── go.sum
├── input.go
├── key.go
├── keyboard.go
├── logging.go
├── logging_test.go
├── mod.go
├── mouse.go
├── nil_renderer.go
├── options.go
├── options_test.go
├── paste.go
├── profile.go
├── raw.go
├── renderer.go
├── screen.go
├── screen_test.go
├── signals_unix.go
├── signals_windows.go
├── tea.go
├── tea_test.go
├── termcap.go
├── termios_bsd.go
├── termios_other.go
├── termios_unix.go
├── termios_windows.go
├── testdata/
│ ├── TestClearMsg/
│ │ ├── bg_fg_cur_color.golden
│ │ ├── clear_screen.golden
│ │ └── read_set_clipboard.golden
│ └── TestViewModel/
│ ├── altscreen.golden
│ ├── altscreen_autoexit.golden
│ ├── bg_set_color.golden
│ ├── bp_stop_start.golden
│ ├── cursor_hide.golden
│ ├── cursor_hideshow.golden
│ ├── kitty_stop_startreleases.golden
│ ├── mouse_allmotion.golden
│ ├── mouse_cellmotion.golden
│ └── mouse_disable.golden
├── tty.go
├── tty_unix.go
├── tty_windows.go
├── tutorials/
│ ├── basics/
│ │ ├── README.md
│ │ └── main.go
│ ├── commands/
│ │ ├── README.md
│ │ └── main.go
│ ├── go.mod
│ └── go.sum
└── xterm.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.golden -text
================================================
FILE: .github/CODEOWNERS
================================================
* @meowgorithm @aymanbagabas
================================================
FILE: .github/ISSUE_TEMPLATE/bug.yml
================================================
name: Bug Report
description: File a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please fill the form below.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
validations:
required: true
- type: textarea
id: reproducible
attributes:
label: How can we reproduce this?
description: |
Please share a code snippet, gist, or public repository that reproduces the issue.
Make sure to make the reproducible as concise as possible,
with only the minimum required code to reproduce the issue.
validations:
required: true
- type: textarea
id: version
attributes:
label: Which version of bubbletea are you using?
description: ''
render: bash
validations:
required: true
- type: textarea
id: terminaal
attributes:
label: Which terminals did you reproduce this with?
description: |
Other helpful information:
was it over SSH?
On tmux?
Which version of said terminal?
validations:
required: true
- type: checkboxes
id: search
attributes:
label: Search
options:
- label: |
I searched for other open and closed issues and pull requests before opening this,
and didn't find anything that seems related.
required: true
- type: textarea
id: ctx
attributes:
label: Additional context
description: Anything else you would like to add
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Setup**
Please complete the following information along with version numbers, if applicable.
- OS [e.g. Ubuntu, macOS]
- Shell [e.g. zsh, fish]
- Terminal Emulator [e.g. kitty, iterm]
- Terminal Multiplexer [e.g. tmux]
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Source Code**
Please include source code if needed to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
Add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Discord
url: https://charm.sh/discord
about: Chat on our Discord.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
ignore:
- dependency-name: github.com/charmbracelet/bubbletea/v2
versions:
- v2.0.0-beta1
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "gomod"
directory: "/examples"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
- package-ecosystem: "gomod"
directory: "/tutorials"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
groups:
all:
patterns:
- "*"
================================================
FILE: .github/workflows/build.yml
================================================
name: build
on: [push, pull_request]
jobs:
build:
uses: charmbracelet/meta/.github/workflows/build.yml@main
build-go-mod:
uses: charmbracelet/meta/.github/workflows/build.yml@main
with:
go-version: ""
go-version-file: ./go.mod
build-examples:
uses: charmbracelet/meta/.github/workflows/build.yml@main
with:
go-version: ""
go-version-file: ./examples/go.mod
working-directory: ./examples
================================================
FILE: .github/workflows/coverage.yml
================================================
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@v6
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v6
- name: Coverage
run: |
go test -race -covermode=atomic -coverprofile=coverage.txt ./...
- uses: codecov/codecov-action@v5
with:
file: ./coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
================================================
FILE: .github/workflows/dependabot-sync.yml
================================================
name: dependabot-sync
on:
schedule:
- cron: "0 0 * * 0" # every Sunday at midnight
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
dependabot-sync:
uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
with:
repo_name: ${{ github.event.repository.name }}
secrets:
gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
================================================
FILE: .github/workflows/examples.yml
================================================
name: examples
on:
push:
branches:
- 'master'
paths:
- '.github/workflows/examples.yml'
- './examples/go.mod'
- './examples/go.sum'
- './tutorials/go.mod'
- './tutorials/go.sum'
- './go.mod'
- './go.sum'
workflow_dispatch: {}
jobs:
tidy:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '^1'
cache: true
- shell: bash
run: |
(cd ./examples && go mod tidy)
(cd ./tutorials && go mod tidy)
- uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore: go mod tidy tutorials and examples"
branch: master
commit_user_name: actions-user
commit_user_email: actions@github.com
================================================
FILE: .github/workflows/lint-sync.yml
================================================
name: lint-sync
on:
schedule:
# every Sunday at midnight
- cron: "0 0 * * 0"
workflow_dispatch: # allows manual triggering
permissions:
contents: write
pull-requests: write
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main
================================================
FILE: .github/workflows/lint.yml
================================================
name: lint
on:
push:
pull_request:
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint.yml@main
with:
golangci_path: .golangci.yml
golangci_version: v2.9
timeout: 10m
================================================
FILE: .github/workflows/release.yml
================================================
name: goreleaser
on:
push:
tags:
- v*.*.*
concurrency:
group: goreleaser
cancel-in-progress: true
jobs:
goreleaser:
uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }}
twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }}
twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }}
mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }}
mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }}
discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
================================================
FILE: .gitignore
================================================
.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/progress-download/color_vortex.blend
examples/progress-download/progress-download
examples/simple/simple
examples/spinner/spinner
examples/textinput/textinput
examples/textinputs/textinputs
examples/views/views
tutorials/basics/basics
tutorials/commands/commands
.idea
coverage.txt
dist/
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
tests: false
linters:
enable:
- bodyclose
- exhaustive
- goconst
- godot
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
================================================
FILE: .goreleaser.yml
================================================
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020-2026 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.
================================================
FILE: README.md
================================================
# Bubble Tea
<p>
<img src="https://github.com/user-attachments/assets/ad408275-8799-488f-9303-441e7f869535" width="350"><br>
<a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/charm.land/bubbletea/v2?tab=doc"><img src="https://godoc.org/charm.land/bubbletea/v2?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"></a>
</p>
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.
<p>
<img src="https://stuff.charm.sh/bubbletea/bubbletea-example.gif" width="100%" alt="Bubble Tea Example">
</p>
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 high-performance cell-based renderer, built-in color downsampling,
declarative views, high-fidelity keyboard and mouse handling, native clipboard
support, and more.
To get started, see the tutorial below, the [examples][examples], the
[docs][docs], and some common [resources](#libraries-we-use-with-bubble-tea).
> [!TIP]
>
> Upgrading from v1? Check out the [upgrade guide](./UPGRADE_GUIDE_V2.md), or
> point your LLM at it and let it go to town.
## By the way
Be sure to check out [Bubbles][bubbles], a library of common UI components for Bubble Tea.
<p>
<a href="https://github.com/charmbracelet/bubbles"><img src="https://github.com/user-attachments/assets/b6dc4638-b67a-4bfa-88d0-a8e8833c3ac9" width="172" alt="Bubbles Badge"></a>
<a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles-examples/textinput.gif" width="400" alt="Text Input Example from Bubbles"></a>
</p>
---
## Tutorial
Bubble Tea is based on the functional design paradigms of [The Elm
Architecture][elm], which happens to work nicely with Go. It's a delightful way
to build applications.
This tutorial assumes you have a working knowledge of Go.
By the way, the non-annotated source code for this program is available
[on GitHub][tut-source].
[elm]: https://guide.elm-lang.org/architecture/
[tut-source]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/basics
### 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
// These imports will be used later in the tutorial. If you save the file
// now, Go might complain they are unused, but that's fine.
// You may also need to run `go mod tidy` to download bubbletea and its
// dependencies.
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)
```
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. `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 initialModel() model {
return model{
// Our to-do list is a grocery list
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
// A map which indicates which choices are selected. We're using
// the map like a mathematical set. The keys refer to the indexes
// of the `choices` slice, above.
selected: make(map[int]struct{}),
}
}
```
After that, we’ll define our application’s initial state in 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.KeyPressMsg` 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.KeyPressMsg:
// 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 space bar toggle the selected state
// for the item that the cursor is pointing at.
case "enter", "space":
_, 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 <kbd>ctrl+c</kbd> and <kbd>q</kbd> above return
a `tea.Quit` command with the model. That’s a special command which instructs
the Bubble Tea runtime to quit, exiting the program.
### The View Method
At last, it’s time to render our UI. Of all the methods, the view is the
simplest. We look at the model in its current state and use it to build a
`tea.View`. The view declares our UI content and, optionally, terminal features
like alt screen mode, mouse tracking, cursor position, and more.
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() tea.View {
// 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 tea.NewView(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.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}
```
## What’s Next?
This tutorial covers the basics of building an interactive terminal UI, but
in the real world you'll also need to perform I/O. To learn about that have a
look at the [Command Tutorial][cmd]. It's pretty simple.
There are also several [Bubble Tea examples][examples] available and, of course,
there are [Go Docs][docs].
[cmd]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/commands/
[examples]: https://github.com/charmbracelet/bubbletea/tree/main/examples
[docs]: https://pkg.go.dev/charm.land/bubbletea/v2?tab=doc
## Debugging
### Debugging with Delve
Since Bubble Tea apps assume control of stdin and stdout, you’ll need to run
delve in headless mode and then connect to it:
```bash
# Start the debugger
$ dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
API server listening at: 127.0.0.1:43000
# Connect to it from another terminal
$ dlv connect 127.0.0.1:43000
```
If you do not explicitly supply the `--listen` flag, the port used will vary
per run, so passing this in makes the debugger easier to use from a script
or your IDE of choice.
Additionally, we pass in `--api-version=2` because delve defaults to version 1
for backwards compatibility reasons. However, delve recommends using version 2
for all new development and some clients may no longer work with version 1.
For more information, see the [Delve documentation](https://github.com/go-delve/delve/tree/master/Documentation/api).
### Logging Stuff
You can’t really log to stdout with Bubble Tea because your TUI is busy
occupying that! You can, however, log to a file by including something like
the following prior to starting your Bubble Tea program:
```go
if len(os.Getenv("DEBUG")) > 0 {
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
}
defer f.Close()
}
```
To see what’s being logged in real time, run `tail -f debug.log` while you run
your program in another window.
## Libraries we use with Bubble Tea
- [Bubbles][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
- [BubbleZone][bubblezone]: Easy mouse event tracking for Bubble Tea components
- [ntcharts][ntcharts]: A terminal charting library built for Bubble Tea and [Lip Gloss][lipgloss]
[bubbles]: https://github.com/charmbracelet/bubbles
[lipgloss]: https://github.com/charmbracelet/lipgloss
[harmonica]: https://github.com/charmbracelet/harmonica
[bubblezone]: https://github.com/lrstanley/bubblezone
[ntcharts]: https://github.com/NimbleMarkets/ntcharts
## Bubble Tea in the Wild
There are over [18,000 applications](https://github.com/charmbracelet/bubbletea/network/dependents) built with Bubble Tea! Here are a handful of ’em.
### Staff favourites
- [chezmoi](https://github.com/twpayne/chezmoi): securely manage your dotfiles across multiple machines
- [circumflex](https://github.com/bensadeh/circumflex): read Hacker News in the terminal
- [gh-dash](https://www.github.com/dlvhdr/gh-dash): a GitHub CLI extension for PRs and issues
- [Tetrigo](https://github.com/Broderick-Westrope/tetrigo): Tetris in the terminal
- [Signls](https://github.com/emprcl/signls): a generative midi sequencer designed for composition and live performance
- [Superfile](https://github.com/yorukot/superfile): a super file manager
### In Industry
- Microsoft Azure – [Aztify](https://github.com/Azure/aztfy): bring Microsoft Azure resources under Terraform
- Daytona – [Daytona](https://github.com/daytonaio/daytona): an AI infrastructure platform
- Cockroach Labs – [CockroachDB](https://github.com/cockroachdb/cockroach): a cloud-native, high-availability distributed SQL database
- Truffle Security Co. – [Trufflehog](https://github.com/trufflesecurity/trufflehog): find leaked credentials
- NVIDIA – [container-canary](https://github.com/NVIDIA/container-canary): a container validator
- AWS – [eks-node-viewer](https://github.com/awslabs/eks-node-viewer): a tool for visualizing dynamic node usage within an EKS cluster
- MinIO – [mc](https://github.com/minio/mc): the official [MinIO](https://min.io) client
- Ubuntu – [Authd](https://github.com/ubuntu/authd): an authentication daemon for cloud-based identity providers
### Charm stuff
- [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser, and online markdown stash
- [Huh?](https://github.com/charmbracelet/huh): an interactive prompt and form toolkit
- [Mods](https://github.com/charmbracelet/mods): AI on the CLI, built for pipelines
- [Wishlist](https://github.com/charmbracelet/wishlist): an SSH directory (and bastion!)
### There’s so much more where that came from
For more applications built with Bubble Tea see [Charm & Friends][community].
Is there something cool you made with Bubble Tea you want to share? [PRs][community] are
welcome!
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/bubbletea/contribute
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## 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. It’s
inspired by the many great [_Zeichenorientierte Benutzerschnittstellen_][zb]
of days past.
[elm]: https://guide.elm-lang.org/architecture/
[gotea]: https://github.com/tj/go-tea
[zb]: https://de.wikipedia.org/wiki/Zeichenorientierte_Benutzerschnittstelle
[community]: https://github.com/charm-and-friends/charm-in-the-wild
## License
[MIT](https://github.com/charmbracelet/bubbletea/raw/main/LICENSE)
---
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
================================================
FILE: Taskfile.yaml
================================================
# https://taskfile.dev
version: '3'
tasks:
lint:
desc: Run lint
cmds:
- golangci-lint run
test:
desc: Run tests
cmds:
- go test ./... {{.CLI_ARGS}}
================================================
FILE: UPGRADE_GUIDE_V2.md
================================================
# Bubble Tea v2 Upgrade Guide
This guide covers everything you need to change when upgrading from Bubble Tea v1 to v2. For a tour of all the exciting new features, check out the [What's New](https://github.com/charmbracelet/bubbletea/releases/tag/v2.0.0) doc.
> [!NOTE]
> We don't take API changes lightly and strive to make the upgrade process as simple as possible. If something feels way off, let us know.
## Migration Checklist
Here's the short version — a checklist you can follow top to bottom. Each item links to the relevant section below.
- [ ] [Update import paths](#import-paths)
- [ ] [Change `View() string` to `View() tea.View`](#view-returns-a-teaview-now)
- [ ] [Replace `tea.KeyMsg` with `tea.KeyPressMsg`](#key-messages)
- [ ] [Update key fields: `msg.Type` / `msg.Runes` / `msg.Alt`](#key-messages)
- [ ] [Replace `case " ":` with `case "space":`](#key-messages)
- [ ] [Update mouse message usage](#mouse-messages)
- [ ] [Rename mouse button constants](#mouse-messages)
- [ ] [Remove old program options → use View fields](#removed-program-options)
- [ ] [Remove imperative commands → use View fields](#removed-commands)
- [ ] [Remove old program methods](#removed-program-methods)
- [ ] [Rename `tea.WindowSize()` → `tea.RequestWindowSize`](#renamed-apis)
- [ ] [Replace `tea.Sequentially(...)` → `tea.Sequence(...)`](#renamed-apis)
## Import Paths
The module path changed to a vanity domain. Lip Gloss moved too.
```go
// Before
import tea "github.com/charmbracelet/bubbletea"
import "github.com/charmbracelet/lipgloss"
// After
import tea "charm.land/bubbletea/v2"
import "charm.land/lipgloss/v2"
```
## The Big Idea: Declarative Views
The single biggest change in v2 is the shift from **imperative commands** to **declarative View fields**. In v1, you'd use program options like `tea.WithAltScreen()` and commands like `tea.EnterAltScreen` to toggle terminal features on and off. In v2, you just set fields on the `tea.View` struct in your `View()` method and Bubble Tea handles the rest.
This means: no more startup option flags, no more toggle commands, no more fighting over state. Just declare what you want and Bubble Tea will make it so.
```go
// v1: imperative — scattered across NewProgram, Init, and Update
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
// v2: declarative — everything lives in View()
func (m model) View() tea.View {
v := tea.NewView("Hello!")
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
return v
}
```
Keep this in mind as you go through the rest of the guide — most of the "removed" things simply moved into View fields.
## View Returns a `tea.View` Now
The `View()` method no longer returns a `string`. It returns a `tea.View` struct.
```go
// Before:
func (m model) View() string {
return "Hello, world!"
}
// After:
func (m model) View() tea.View {
return tea.NewView("Hello, world!")
}
```
You can also use the longer form if you need to set additional fields:
```go
func (m model) View() tea.View {
var v tea.View
v.SetContent("Hello, world!")
v.AltScreen = true
return v
}
```
The `tea.View` struct has fields for everything that used to be controlled by options and commands:
| View Field | What It Does |
|---|---|
| `Content` | The rendered string (set via `SetContent()` or `NewView()`) |
| `AltScreen` | Enter/exit the alternate screen buffer |
| `MouseMode` | `MouseModeNone`, `MouseModeCellMotion`, or `MouseModeAllMotion` |
| `ReportFocus` | Enable focus/blur event reporting |
| `DisableBracketedPasteMode` | Disable bracketed paste |
| `WindowTitle` | Set the terminal window title |
| `Cursor` | Control cursor position, shape, color, and blink |
| `ForegroundColor` | Set the terminal foreground color |
| `BackgroundColor` | Set the terminal background color |
| `ProgressBar` | Show a native terminal progress bar |
| `KeyboardEnhancements` | Request keyboard enhancement features |
| `OnMouse` | Intercept mouse messages based on view content |
## Key Messages
Key messages got a major overhaul. Here's the quick rundown:
### `tea.KeyMsg` is now an interface
In v1, `tea.KeyMsg` was a struct you'd match on for key presses. In v2, it's an **interface** that covers both key presses and releases. For most code, you want `tea.KeyPressMsg`:
```go
// Before:
case tea.KeyMsg:
switch msg.String() {
case "q":
return m, tea.Quit
}
// After:
case tea.KeyPressMsg:
switch msg.String() {
case "q":
return m, tea.Quit
}
```
If you want to handle both presses and releases, use `tea.KeyMsg` and type-switch inside:
```go
case tea.KeyMsg:
switch key := msg.(type) {
case tea.KeyPressMsg:
// key press
case tea.KeyReleaseMsg:
// key release
}
```
### Key fields changed
| v1 | v2 | Notes |
|---|---|---|
| `msg.Type` | `msg.Code` | A `rune` — can be `tea.KeyEnter`, `'a'`, etc. |
| `msg.Runes` | `msg.Text` | Now a `string`, not `[]rune` |
| `msg.Alt` | `msg.Mod` | `msg.Mod.Contains(tea.ModAlt)` for alt, etc. |
| `tea.KeyRune` | — | Check `len(msg.Text) > 0` instead |
| `tea.KeyCtrlC` | — | Use `msg.String() == "ctrl+c"` or check `msg.Code` + `msg.Mod` |
### Space bar changed
Space bar now returns `"space"` instead of `" "` when using `msg.String()`:
```go
// Before:
case " ":
// After:
case "space":
```
`key.Code` is still `' '` and `key.Text` is still `" "`, but `String()` returns `"space"`.
### Ctrl+key matching
```go
// Before:
case tea.KeyCtrlC:
// ctrl+c
// After (option A — string matching):
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c":
// ctrl+c
}
// After (option B — field matching):
case tea.KeyPressMsg:
if msg.Code == 'c' && msg.Mod == tea.ModCtrl {
// ctrl+c
}
```
### New Key fields
These are new in v2 and don't have v1 equivalents:
- **`key.ShiftedCode`** — the shifted key code (e.g., `'B'` when pressing shift+b)
- **`key.BaseCode`** — the key on a US PC-101 layout (handy for international keyboards)
- **`key.IsRepeat`** — whether the key is auto-repeating (Kitty protocol / Windows Console only)
- **`key.Keystroke()`** — like `String()` but always includes modifier info
## Paste Messages
Paste events no longer come in as `tea.KeyMsg` with a `Paste` flag. They're now their own message types:
```go
// Before:
case tea.KeyMsg:
if msg.Paste {
m.text += string(msg.Runes)
}
// After:
case tea.PasteMsg:
m.text += msg.Content
case tea.PasteStartMsg:
// paste started
case tea.PasteEndMsg:
// paste ended
```
## Mouse Messages
### `tea.MouseMsg` is now an interface
In v1, `tea.MouseMsg` was a struct with `X`, `Y`, `Button`, etc. In v2, it's an **interface**. You get the coordinates by calling `msg.Mouse()`:
```go
// Before:
case tea.MouseMsg:
x, y := msg.X, msg.Y
// After:
case tea.MouseMsg:
mouse := msg.Mouse()
x, y := mouse.X, mouse.Y
```
### Mouse events are split by type
Instead of checking `msg.Action`, match on specific message types:
```go
// Before:
case tea.MouseMsg:
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
// left click
}
// After:
case tea.MouseClickMsg:
if msg.Button == tea.MouseLeft {
// left click
}
case tea.MouseReleaseMsg:
// release
case tea.MouseWheelMsg:
// scroll
case tea.MouseMotionMsg:
// movement
```
### Button constants renamed
| v1 | v2 |
|---|---|
| `tea.MouseButtonLeft` | `tea.MouseLeft` |
| `tea.MouseButtonRight` | `tea.MouseRight` |
| `tea.MouseButtonMiddle` | `tea.MouseMiddle` |
| `tea.MouseButtonWheelUp` | `tea.MouseWheelUp` |
| `tea.MouseButtonWheelDown` | `tea.MouseWheelDown` |
| `tea.MouseButtonWheelLeft` | `tea.MouseWheelLeft` |
| `tea.MouseButtonWheelRight` | `tea.MouseWheelRight` |
### `tea.MouseEvent` → `tea.Mouse`
The `MouseEvent` struct is gone. The new `Mouse` struct has `X`, `Y`, `Button`, and `Mod` fields.
### Mouse mode is now a View field
```go
// Before:
p := tea.NewProgram(model{}, tea.WithMouseCellMotion())
// After:
func (m model) View() tea.View {
v := tea.NewView("...")
v.MouseMode = tea.MouseModeCellMotion
return v
}
```
## Removed Program Options
These options no longer exist. They all moved to View fields.
| Removed Option | Do This Instead |
|---|---|
| `tea.WithAltScreen()` | `view.AltScreen = true` |
| `tea.WithMouseCellMotion()` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.WithMouseAllMotion()` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.WithReportFocus()` | `view.ReportFocus = true` |
| `tea.WithoutBracketedPaste()` | `view.DisableBracketedPasteMode = true` |
| `tea.WithInputTTY()` | Just remove it — v2 always opens the TTY for input automatically |
| `tea.WithANSICompressor()` | Just remove it — the new renderer handles optimization automatically |
## Removed Commands
These commands no longer exist. Set the corresponding View field instead.
| Removed Command | Do This Instead |
|---|---|
| `tea.EnterAltScreen` | `view.AltScreen = true` |
| `tea.ExitAltScreen` | `view.AltScreen = false` |
| `tea.EnableMouseCellMotion` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.EnableMouseAllMotion` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.DisableMouse` | `view.MouseMode = tea.MouseModeNone` |
| `tea.HideCursor` | `view.Cursor = nil` |
| `tea.ShowCursor` | `view.Cursor = &tea.Cursor{...}` or `tea.NewCursor(x, y)` |
| `tea.EnableBracketedPaste` | `view.DisableBracketedPasteMode = false` |
| `tea.DisableBracketedPaste` | `view.DisableBracketedPasteMode = true` |
| `tea.EnableReportFocus` | `view.ReportFocus = true` |
| `tea.DisableReportFocus` | `view.ReportFocus = false` |
| `tea.SetWindowTitle("...")` | `view.WindowTitle = "..."` |
## Removed Program Methods
These methods on `*Program` are gone.
| Removed Method | Do This Instead |
|---|---|
| `p.Start()` | `p.Run()` |
| `p.StartReturningModel()` | `p.Run()` |
| `p.EnterAltScreen()` | `view.AltScreen = true` in `View()` |
| `p.ExitAltScreen()` | `view.AltScreen = false` in `View()` |
| `p.EnableMouseCellMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseCellMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.EnableMouseAllMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseAllMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.SetWindowTitle(...)` | `view.WindowTitle` in `View()` |
## Renamed APIs
| v1 | v2 | Notes |
|---|---|---|
| `tea.Sequentially(...)` | `tea.Sequence(...)` | `Sequentially` was already deprecated in v1 |
| `tea.WindowSize()` | `tea.RequestWindowSize` | Now returns `Msg` directly, not a `Cmd` |
## New Program Options
These are new in v2:
| Option | What It Does |
|---|---|
| `tea.WithColorProfile(p)` | Force a specific color profile (great for testing) |
| `tea.WithWindowSize(w, h)` | Set initial terminal size (great for testing) |
## Complete Before & After
Here's a minimal but complete program showing the most common migration patterns side by side.
**v1:**
```go
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
count int
}
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":
return m, tea.Quit
case " ":
m.count++
}
case tea.MouseMsg:
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
m.count++
}
}
return m, nil
}
func (m model) View() string {
return fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count)
}
func main() {
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
**v2:**
```go
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)
type model struct {
count int
}
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.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "space":
m.count++
}
case tea.MouseClickMsg:
if msg.Button == tea.MouseLeft {
m.count++
}
}
return m, nil
}
func (m model) View() tea.View {
v := tea.NewView(fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count))
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
return v
}
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
Notice how the `NewProgram` call got simpler? All the terminal feature flags moved into `View()` where they belong.
## Quick Reference
A flat old → new lookup table. Handy for search-and-replace and LLM-assisted migration.
### Import Paths
| v1 | v2 |
|---|---|
| `github.com/charmbracelet/bubbletea` | `charm.land/bubbletea/v2` |
| `github.com/charmbracelet/lipgloss` | `charm.land/lipgloss/v2` |
### Model Interface
| v1 | v2 |
|---|---|
| `View() string` | `View() tea.View` |
### Key Events
| v1 | v2 |
|---|---|
| `tea.KeyMsg` (struct) | `tea.KeyPressMsg` for presses, `tea.KeyMsg` (interface) for both |
| `msg.Type` | `msg.Code` |
| `msg.Runes` | `msg.Text` (string, not `[]rune`) |
| `msg.Alt` | `msg.Mod.Contains(tea.ModAlt)` |
| `tea.KeyRune` | check `len(msg.Text) > 0` |
| `tea.KeyCtrlC` | `msg.Code == 'c' && msg.Mod == tea.ModCtrl` or `msg.String() == "ctrl+c"` |
| `case " ":` (space) | `case "space":` |
### Mouse Events
| v1 | v2 |
|---|---|
| `tea.MouseMsg` (struct) | `tea.MouseMsg` (interface) — call `.Mouse()` for the data |
| `tea.MouseEvent` | `tea.Mouse` |
| `tea.MouseButtonLeft` | `tea.MouseLeft` |
| `tea.MouseButtonRight` | `tea.MouseRight` |
| `tea.MouseButtonMiddle` | `tea.MouseMiddle` |
| `tea.MouseButtonWheelUp` | `tea.MouseWheelUp` |
| `tea.MouseButtonWheelDown` | `tea.MouseWheelDown` |
| `msg.X`, `msg.Y` (direct) | `msg.Mouse().X`, `msg.Mouse().Y` |
### Options → View Fields
| v1 Option | v2 View Field |
|---|---|
| `tea.WithAltScreen()` | `view.AltScreen = true` |
| `tea.WithMouseCellMotion()` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.WithMouseAllMotion()` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.WithReportFocus()` | `view.ReportFocus = true` |
| `tea.WithoutBracketedPaste()` | `view.DisableBracketedPasteMode = true` |
### Commands → View Fields
| v1 Command | v2 View Field |
|---|---|
| `tea.EnterAltScreen` / `tea.ExitAltScreen` | `view.AltScreen = true/false` |
| `tea.EnableMouseCellMotion` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.EnableMouseAllMotion` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.DisableMouse` | `view.MouseMode = tea.MouseModeNone` |
| `tea.HideCursor` / `tea.ShowCursor` | `view.Cursor = nil` / `view.Cursor = &tea.Cursor{...}` |
| `tea.EnableBracketedPaste` / `tea.DisableBracketedPaste` | `view.DisableBracketedPasteMode = false/true` |
| `tea.EnableReportFocus` / `tea.DisableReportFocus` | `view.ReportFocus = true/false` |
| `tea.SetWindowTitle("...")` | `view.WindowTitle = "..."` |
### Removed Options (No Replacement Needed)
| v1 Option | What Happened |
|---|---|
| `tea.WithInputTTY()` | v2 always opens the TTY for input automatically |
| `tea.WithANSICompressor()` | The new renderer handles optimization automatically |
### Removed Program Methods
| v1 Method | v2 Replacement |
|---|---|
| `p.Start()` | `p.Run()` |
| `p.StartReturningModel()` | `p.Run()` |
| `p.EnterAltScreen()` | `view.AltScreen = true` in `View()` |
| `p.ExitAltScreen()` | `view.AltScreen = false` in `View()` |
| `p.EnableMouseCellMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseCellMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.EnableMouseAllMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseAllMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.SetWindowTitle(...)` | `view.WindowTitle` in `View()` |
### Other Renames
| v1 | v2 |
|---|---|
| `tea.Sequentially(...)` | `tea.Sequence(...)` |
| `tea.WindowSize()` | `tea.RequestWindowSize` (now returns `Msg`, not `Cmd`) |
### New Program Options
| Option | Description |
|---|---|
| `tea.WithColorProfile(p)` | Force a specific color profile |
| `tea.WithWindowSize(w, h)` | Set initial window size (great for testing) |
## Feedback
Have thoughts on the v2 upgrade? We'd _love_ to hear about it. Let us know on…
- [Discord](https://charm.land/chat)
- [Matrix](https://charm.land/matrix)
- [Email](mailto:vt100@charm.land)
---
Part of [Charm](https://charm.land).
<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
================================================
FILE: clipboard.go
================================================
package tea
// ClipboardMsg is a clipboard read message event. This message is emitted when
// a terminal receives an OSC52 clipboard read message event.
type ClipboardMsg struct {
Content string
Selection byte
}
// Clipboard returns the clipboard selection type. This will be one of the
// following values:
//
// - c: System clipboard.
// - p: Primary clipboard (X11/Wayland only).
func (e ClipboardMsg) Clipboard() byte {
return e.Selection
}
// String returns the string representation of the clipboard message.
func (e ClipboardMsg) String() string {
return e.Content
}
// setClipboardMsg is an internal message used to set the system clipboard
// using OSC52.
type setClipboardMsg string
// SetClipboard produces a command that sets the system clipboard using OSC52.
// Note that OSC52 is not supported in all terminals.
func SetClipboard(s string) Cmd {
return func() Msg {
return setClipboardMsg(s)
}
}
// readClipboardMsg is an internal message used to read the system clipboard
// using OSC52.
type readClipboardMsg struct{}
// ReadClipboard produces a command that reads the system clipboard using OSC52.
// Note that OSC52 is not supported in all terminals.
func ReadClipboard() Msg {
return readClipboardMsg{}
}
// setPrimaryClipboardMsg is an internal message used to set the primary
// clipboard using OSC52.
type setPrimaryClipboardMsg string
// SetPrimaryClipboard produces a command that sets the primary clipboard using
// OSC52. Primary clipboard selection is a feature present in X11 and Wayland
// only.
// Note that OSC52 is not supported in all terminals.
func SetPrimaryClipboard(s string) Cmd {
return func() Msg {
return setPrimaryClipboardMsg(s)
}
}
// readPrimaryClipboardMsg is an internal message used to read the primary
// clipboard using OSC52.
type readPrimaryClipboardMsg struct{}
// ReadPrimaryClipboard produces a command that reads the primary clipboard
// using OSC52. Primary clipboard selection is a feature present in X11 and
// Wayland only.
// Note that OSC52 is not supported in all terminals.
func ReadPrimaryClipboard() Msg {
return readPrimaryClipboardMsg{}
}
================================================
FILE: color.go
================================================
package tea
import (
"image/color"
uv "github.com/charmbracelet/ultraviolet"
)
// backgroundColorMsg is a message that requests the terminal background color.
type backgroundColorMsg struct{}
// RequestBackgroundColor is a command that requests the terminal background color.
func RequestBackgroundColor() Msg {
return backgroundColorMsg{}
}
// foregroundColorMsg is a message that requests the terminal foreground color.
type foregroundColorMsg struct{}
// RequestForegroundColor is a command that requests the terminal foreground color.
func RequestForegroundColor() Msg {
return foregroundColorMsg{}
}
// cursorColorMsg is a message that requests the terminal cursor color.
type cursorColorMsg struct{}
// RequestCursorColor is a command that requests the terminal cursor color.
func RequestCursorColor() Msg {
return cursorColorMsg{}
}
// ForegroundColorMsg represents a foreground color message. This message is
// emitted when the program requests the terminal foreground color with the
// [RequestForegroundColor] Cmd.
type ForegroundColorMsg struct{ color.Color }
// String returns the hex representation of the color.
func (e ForegroundColorMsg) String() string {
return uv.ForegroundColorEvent(e).String()
}
// IsDark returns whether the color is dark.
func (e ForegroundColorMsg) IsDark() bool {
return uv.ForegroundColorEvent(e).IsDark()
}
// BackgroundColorMsg represents a background color message. This message is
// emitted when the program requests the terminal background color with the
// [RequestBackgroundColor] Cmd.
//
// This is commonly used in [Update.Init] to get the terminal background color
// for style definitions. For that you'll want to call
// [BackgroundColorMsg.IsDark] to determine if the color is dark or light. For
// example:
//
// func (m Model) Init() (Model, Cmd) {
// return m, RequestBackgroundColor()
// }
//
// func (m Model) Update(msg Msg) (Model, Cmd) {
// switch msg := msg.(type) {
// case BackgroundColorMsg:
// m.styles = newStyles(msg.IsDark())
// }
// }
type BackgroundColorMsg struct{ color.Color }
// String returns the hex representation of the color.
func (e BackgroundColorMsg) String() string {
return uv.BackgroundColorEvent(e).String()
}
// IsDark returns whether the color is dark.
func (e BackgroundColorMsg) IsDark() bool {
return uv.BackgroundColorEvent(e).IsDark()
}
// CursorColorMsg represents a cursor color change message. This message is
// emitted when the program requests the terminal cursor color.
type CursorColorMsg struct{ color.Color }
// String returns the hex representation of the color.
func (e CursorColorMsg) String() string {
return uv.CursorColorEvent(e).String()
}
// IsDark returns whether the color is dark.
func (e CursorColorMsg) IsDark() bool {
return uv.CursorColorEvent(e).IsDark()
}
================================================
FILE: commands.go
================================================
package tea
import (
"time"
)
// 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() (Model, Cmd) {
// return m, tea.Batch(someCommand, someOtherCommand)
// }
func Batch(cmds ...Cmd) Cmd {
return compactCmds[BatchMsg](cmds)
}
// BatchMsg is a message used to perform a bunch of commands concurrently with
// no ordering guarantees. You can send a BatchMsg with Batch.
type BatchMsg []Cmd
// Sequence runs the given commands one at a time, in order. Contrast this with
// Batch, which runs commands concurrently.
func Sequence(cmds ...Cmd) Cmd {
return compactCmds[sequenceMsg](cmds)
}
// sequenceMsg is used internally to run the given commands in order.
type sequenceMsg []Cmd
// compactCmds ignores any nil commands in cmds, and returns the most direct
// command possible. That is, considering the non-nil commands, if there are
// none it returns nil, if there is exactly one it returns that command
// directly, else it returns the non-nil commands as type T.
func compactCmds[T ~[]Cmd](cmds []Cmd) Cmd {
var validCmds []Cmd
for _, c := range cmds {
if c == nil {
continue
}
validCmds = append(validCmds, c)
}
switch len(validCmds) {
case 0:
return nil
case 1:
return validCmds[0]
default:
return func() Msg {
return T(validCmds)
}
}
}
// 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)
// })
//
// Beginners' note: Every sends a single message and won't automatically
// dispatch messages at an interval. To do that, you'll want to return another
// Every command after receiving your tick message. For example:
//
// type TickMsg time.Time
//
// // Send a message every second.
// func tickEvery() Cmd {
// return Every(time.Second, func(t time.Time) Msg {
// return TickMsg(t)
// })
// }
//
// func (m model) Init() (Model, Cmd) {
// // Start ticking.
// return m, tickEvery()
// }
//
// func (m model) Update(msg Msg) (Model, Cmd) {
// switch msg.(type) {
// case TickMsg:
// // Return your Every command again to loop.
// return m, tickEvery()
// }
// return m, nil
// }
//
// Every is analogous to Tick in the Elm Architecture.
func Every(duration time.Duration, fn func(time.Time) Msg) Cmd {
n := time.Now()
d := n.Truncate(duration).Add(duration).Sub(n)
t := time.NewTimer(d)
return func() Msg {
ts := <-t.C
t.Stop()
for len(t.C) > 0 {
<-t.C
}
return fn(ts)
}
}
// Tick produces a command at an interval independent of the system clock at
// the given duration. That is, the timer begins 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)
// })
//
// Beginners' note: Tick sends a single message and won't automatically
// dispatch messages at an interval. To do that, you'll want to return another
// Tick command after receiving your tick message. For example:
//
// type TickMsg time.Time
//
// func doTick() Cmd {
// return Tick(time.Second, func(t time.Time) Msg {
// return TickMsg(t)
// })
// }
//
// func (m model) Init() (Model, Cmd) {
// // Start ticking.
// return m, doTick()
// }
//
// func (m model) Update(msg Msg) (Model, Cmd) {
// switch msg.(type) {
// case TickMsg:
// // Return your Tick command again to loop.
// return m, doTick()
// }
// return m, nil
// }
func Tick(d time.Duration, fn func(time.Time) Msg) Cmd {
t := time.NewTimer(d)
return func() Msg {
ts := <-t.C
t.Stop()
for len(t.C) > 0 {
<-t.C
}
return fn(ts)
}
}
type windowSizeMsg struct{}
// RequestWindowSize is a command that queries the terminal for its current
// size. It delivers the results to Update via a [WindowSizeMsg]. Keep in mind
// that WindowSizeMsgs will automatically be delivered to Update when the
// [Program] starts and when the window dimensions change so in many cases you
// will not need to explicitly invoke this command.
func RequestWindowSize() Msg {
return windowSizeMsg{}
}
================================================
FILE: commands_test.go
================================================
package tea
import (
"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 TestBatch(t *testing.T) {
testMultipleCommands[BatchMsg](t, Batch)
}
func TestSequence(t *testing.T) {
testMultipleCommands[sequenceMsg](t, Sequence)
}
func testMultipleCommands[T ~[]Cmd](t *testing.T, createFn func(cmd ...Cmd) Cmd) {
t.Run("nil cmd", func(t *testing.T) {
if b := createFn(nil); b != nil {
t.Fatalf("expected nil, got %+v", b)
}
})
t.Run("empty cmd", func(t *testing.T) {
if b := createFn(); b != nil {
t.Fatalf("expected nil, got %+v", b)
}
})
t.Run("single cmd", func(t *testing.T) {
b := createFn(Quit)()
if _, ok := b.(QuitMsg); !ok {
t.Fatalf("expected a QuitMsg, got %T", b)
}
})
t.Run("mixed nil cmds", func(t *testing.T) {
b := createFn(nil, Quit, nil, Quit, nil, nil)()
if l := len(b.(T)); l != 2 {
t.Fatalf("expected a []Cmd with len 2, got %d", l)
}
})
}
================================================
FILE: cursed_renderer.go
================================================
package tea
import (
"bytes"
"fmt"
"image/color"
"io"
"runtime"
"strings"
"sync"
"github.com/charmbracelet/colorprofile"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/ansi"
"github.com/lucasb-eyer/go-colorful"
)
type cursedRenderer struct {
w io.Writer
buf bytes.Buffer // updates buffer to be flushed to [w]
scr *uv.TerminalRenderer
cellbuf uv.ScreenBuffer
lastView *View
env []string
term string // the terminal type $TERM
width, height int
mu sync.Mutex
profile colorprofile.Profile
logger uv.Logger
view View
hardTabs bool // whether to use hard tabs to optimize cursor movements
backspace bool // whether to use backspace to optimize cursor movements
mapnl bool
syncdUpdates bool // whether to use synchronized output mode for updates
starting bool // indicates whether the renderer is starting after being stopped
}
var _ renderer = &cursedRenderer{}
func newCursedRenderer(w io.Writer, env []string, width, height int) (s *cursedRenderer) {
s = new(cursedRenderer)
s.w = w
s.env = env
s.term = uv.Environ(env).Getenv("TERM")
s.width, s.height = width, height // This needs to happen before [cursedRenderer.reset].
s.cellbuf = uv.NewScreenBuffer(s.width, s.height)
reset(s)
return
}
// setLogger sets the logger for the renderer.
func (s *cursedRenderer) setLogger(logger uv.Logger) {
s.mu.Lock()
s.logger = logger
s.mu.Unlock()
}
// setOptimizations sets the cursor movement optimizations.
func (s *cursedRenderer) setOptimizations(hardTabs, backspace, mapnl bool) {
s.mu.Lock()
s.hardTabs = hardTabs
s.backspace = backspace
s.mapnl = mapnl
s.scr.SetTabStops(s.width)
s.scr.SetBackspace(s.backspace)
s.scr.SetMapNewline(s.mapnl)
s.mu.Unlock()
}
// start implements renderer.
func (s *cursedRenderer) start() {
s.mu.Lock()
defer s.mu.Unlock()
// Mark that we're starting. This is used to restore some state when
// starting the renderer again after it was stopped.
s.starting = true
if s.lastView == nil {
return
}
if s.lastView.AltScreen {
enableAltScreen(s, true, true)
}
enableTextCursor(s, s.lastView.Cursor != nil)
if s.lastView.Cursor != nil {
if s.lastView.Cursor.Color != nil {
col, ok := colorful.MakeColor(s.lastView.Cursor.Color)
if ok {
_, _ = s.scr.WriteString(ansi.SetCursorColor(col.Hex()))
}
}
curStyle := encodeCursorStyle(s.lastView.Cursor.Shape, s.lastView.Cursor.Blink)
if curStyle != 0 && curStyle != 1 {
_, _ = s.scr.WriteString(ansi.SetCursorStyle(curStyle))
}
}
if s.lastView.ForegroundColor != nil {
col, ok := colorful.MakeColor(s.lastView.ForegroundColor)
if ok {
_, _ = s.scr.WriteString(ansi.SetForegroundColor(col.Hex()))
}
}
if s.lastView.BackgroundColor != nil {
col, ok := colorful.MakeColor(s.lastView.BackgroundColor)
if ok {
_, _ = s.scr.WriteString(ansi.SetBackgroundColor(col.Hex()))
}
}
if !s.lastView.DisableBracketedPasteMode {
_, _ = s.scr.WriteString(ansi.SetModeBracketedPaste)
}
if s.lastView.ReportFocus {
_, _ = s.scr.WriteString(ansi.SetModeFocusEvent)
}
switch s.lastView.MouseMode {
case MouseModeNone:
case MouseModeCellMotion:
_, _ = s.scr.WriteString(ansi.SetModeMouseButtonEvent + ansi.SetModeMouseExtSgr)
case MouseModeAllMotion:
_, _ = s.scr.WriteString(ansi.SetModeMouseAnyEvent + ansi.SetModeMouseExtSgr)
}
if s.lastView.WindowTitle != "" {
_, _ = s.scr.WriteString(ansi.SetWindowTitle(s.lastView.WindowTitle))
}
if s.lastView.ProgressBar != nil {
setProgressBar(s, s.lastView.ProgressBar)
}
// Enable modifyOtherKeys and Kitty keyboard protocol.
// Both can coexist; terminals ignore what they don't support.
_, _ = s.scr.WriteString(ansi.SetModifyOtherKeys2)
kittyFlags := ansi.KittyDisambiguateEscapeCodes
if s.lastView.KeyboardEnhancements.ReportEventTypes {
kittyFlags |= ansi.KittyReportEventTypes
}
_, _ = s.scr.WriteString(ansi.KittyKeyboard(kittyFlags, 1))
}
// close implements renderer.
func (s *cursedRenderer) close() (err error) {
s.mu.Lock()
defer s.mu.Unlock()
// Exit the altScreen and show cursor before closing. It's important that
// we don't change the [cursedRenderer] altScreen and cursorHidden states
// so that we can restore them when we start the renderer again. This is
// used when the user suspends the program and then resumes it.
if lv := s.lastView; lv != nil { //nolint:nestif
// NOTE: The Kitty keyboard specs specify that the terminal should have
// two registries for the main and alt screens. We disable keyboard
// enhancements whenever we enter/exit alt screen mode in
// [cursedRenderer.flush].
// Here, we reset the keyboard protocol of the last screen used
// assuming the other screen is already reset when we switched screens.
_, _ = s.buf.WriteString(ansi.ResetModifyOtherKeys)
_, _ = s.buf.WriteString(ansi.KittyKeyboard(0, 1))
// Go to the bottom of the screen.
// We need to go to the bottom of the screen regardless of whether
// we're in alt screen mode or not to avoid leaving the cursor in the
// middle in terminals that don't support alt screen mode.
s.scr.MoveTo(0, s.cellbuf.Height()-1)
_ = s.scr.Flush() // we need to flush to write the cursor movement
if lv.AltScreen {
enableAltScreen(s, false, true)
} else {
_, _ = s.scr.WriteString(ansi.EraseScreenBelow)
}
if lv.Cursor == nil {
enableTextCursor(s, true)
}
if !lv.DisableBracketedPasteMode {
_, _ = s.scr.WriteString(ansi.ResetModeBracketedPaste)
}
if lv.ReportFocus {
_, _ = s.scr.WriteString(ansi.ResetModeFocusEvent)
}
switch lv.MouseMode {
case MouseModeNone:
case MouseModeCellMotion, MouseModeAllMotion:
_, _ = s.scr.WriteString(ansi.ResetModeMouseButtonEvent +
ansi.ResetModeMouseAnyEvent +
ansi.ResetModeMouseExtSgr)
}
if lv.WindowTitle != "" {
// Clear the window title if it was set.
_, _ = s.scr.WriteString(ansi.SetWindowTitle(""))
}
if lc := lv.Cursor; lc != nil {
curShape := encodeCursorStyle(lc.Shape, lc.Blink)
if curShape != 0 && curShape != 1 {
// Reset the cursor style to default if it was set to something other
// blinking block.
_, _ = s.scr.WriteString(ansi.SetCursorStyle(0))
}
if lc.Color != nil {
_, _ = s.scr.WriteString(ansi.ResetCursorColor)
}
}
if lv.BackgroundColor != nil {
_, _ = s.scr.WriteString(ansi.ResetBackgroundColor)
}
if lv.ForegroundColor != nil {
_, _ = s.scr.WriteString(ansi.ResetForegroundColor)
}
if lv.ProgressBar != nil && lv.ProgressBar.State != ProgressBarNone {
_, _ = s.scr.WriteString(ansi.ResetProgressBar)
}
}
if s.cellbuf.Method == ansi.GraphemeWidth {
// Make sure to turn off Unicode mode (2027)
_, _ = s.scr.WriteString(ansi.ResetModeUnicodeCore)
}
if err := s.scr.Flush(); err != nil {
return fmt.Errorf("bubbletea: error closing screen writer: %w", err)
}
if s.buf.Len() > 0 {
if s.logger != nil {
s.logger.Printf("output: %q", s.buf.String())
}
if _, err := io.Copy(s.w, &s.buf); err != nil {
return fmt.Errorf("bubbletea: error writing to screen: %w", err)
}
s.buf.Reset()
}
x, y := s.scr.Position()
// We want to clear the renderer state but not the cursor position. This is
// because we might be putting the tea process in the background, run some
// other process, and then return to the tea process. We want to keep the
// cursor position so that we can continue where we left off.
reset(s)
s.scr.SetPosition(x, y)
return nil
}
// writeString implements renderer.
func (s *cursedRenderer) writeString(str string) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.scr.WriteString(str) //nolint:wrapcheck
}
// flush implements renderer.
func (s *cursedRenderer) flush(closing bool) error {
s.mu.Lock()
defer s.mu.Unlock()
view := s.view
frameArea := uv.Rect(0, 0, s.width, s.height)
if len(view.Content) == 0 {
// If the component is nil, we should clear the screen buffer.
frameArea.Max.Y = 0
}
content := uv.NewStyledString(view.Content)
if !view.AltScreen {
// We need to resizes the screen based on the frame height and
// terminal width. This is because the frame height can change based on
// the content of the frame. For example, if the frame contains a list
// of items, the height of the frame will be the number of items in the
// list. This is different from the alt screen buffer, which has a
// fixed height and width.
frameHeight := content.Height()
if frameHeight != frameArea.Dy() {
frameArea.Max.Y = frameHeight
}
}
if !s.starting && !closing && s.lastView != nil && viewEquals(s.lastView, &view) && frameArea == s.cellbuf.Bounds() {
// No changes, nothing to do.
return nil
}
// We're no longer starting.
s.starting = false
if frameArea != s.cellbuf.Bounds() {
s.scr.Erase() // Force a full redraw to avoid artifacts.
// We need to reset the touched lines buffer to match the new height.
s.cellbuf.Touched = nil
// Resize the screen buffer to match the frame area. This is necessary
// to ensure that the screen buffer is the same size as the frame area
// and to avoid rendering issues when the frame area is smaller than
// the screen buffer.
s.cellbuf.Resize(frameArea.Dx(), frameArea.Dy())
}
// Clear our screen buffer before copying the new frame into it to ensure
// we erase any old content.
s.cellbuf.Clear()
content.Draw(s.cellbuf, s.cellbuf.Bounds())
// If the frame height is greater than the screen height, we drop the
// lines from the top of the buffer.
if frameHeight := frameArea.Dy(); frameHeight > s.height {
s.cellbuf.Lines = s.cellbuf.Lines[frameHeight-s.height:]
}
// Alt screen mode.
shouldUpdateAltScreen := (s.lastView == nil && view.AltScreen) || (s.lastView != nil && s.lastView.AltScreen != view.AltScreen)
if shouldUpdateAltScreen {
// We want to enter/exit altscreen mode but defer writing the actual
// sequences until we flush the rest of the updates. This is because we
// control the cursor visibility and we need to ensure that happens
// after entering/exiting alt screen mode. Some terminals have
// different cursor visibility states for main and alt screen modes and
// this ensures we handle that correctly.
enableAltScreen(s, view.AltScreen, false)
}
// bracketed paste mode.
if s.lastView == nil || view.DisableBracketedPasteMode != s.lastView.DisableBracketedPasteMode {
if !view.DisableBracketedPasteMode {
_, _ = s.scr.WriteString(ansi.SetModeBracketedPaste)
} else if s.lastView != nil {
_, _ = s.scr.WriteString(ansi.ResetModeBracketedPaste)
}
}
// report focus events mode.
if s.lastView == nil || s.lastView.ReportFocus != view.ReportFocus {
if view.ReportFocus {
_, _ = s.scr.WriteString(ansi.SetModeFocusEvent)
} else if s.lastView != nil {
_, _ = s.scr.WriteString(ansi.ResetModeFocusEvent)
}
}
// mouse events mode.
if s.lastView == nil || view.MouseMode != s.lastView.MouseMode {
switch view.MouseMode {
case MouseModeNone:
if s.lastView != nil && s.lastView.MouseMode != MouseModeNone {
_, _ = s.scr.WriteString(ansi.ResetModeMouseButtonEvent +
ansi.ResetModeMouseAnyEvent +
ansi.ResetModeMouseExtSgr)
}
case MouseModeCellMotion:
if s.lastView != nil && s.lastView.MouseMode == MouseModeAllMotion {
_, _ = s.scr.WriteString(ansi.ResetModeMouseAnyEvent)
}
_, _ = s.scr.WriteString(ansi.SetModeMouseButtonEvent + ansi.SetModeMouseExtSgr)
case MouseModeAllMotion:
if s.lastView != nil && s.lastView.MouseMode == MouseModeCellMotion {
_, _ = s.scr.WriteString(ansi.ResetModeMouseButtonEvent)
}
_, _ = s.scr.WriteString(ansi.SetModeMouseAnyEvent + ansi.SetModeMouseExtSgr)
}
}
// Set window title.
if s.lastView == nil || view.WindowTitle != s.lastView.WindowTitle {
if s.lastView != nil || view.WindowTitle != "" {
_, _ = s.scr.WriteString(ansi.SetWindowTitle(view.WindowTitle))
}
}
// kitty keyboard protocol
if s.lastView == nil || view.KeyboardEnhancements != s.lastView.KeyboardEnhancements ||
view.AltScreen != s.lastView.AltScreen {
// NOTE: We need to reset the keyboard protocol when switching
// between main and alt screen. This is because the specs specify
// two different states for the main and alt screen.
// Enable modifyOtherKeys and Kitty keyboard protocol.
_, _ = s.scr.WriteString(ansi.SetModifyOtherKeys2)
kittyFlags := ansi.KittyDisambiguateEscapeCodes // always enable basic key disambiguation
if view.KeyboardEnhancements.ReportEventTypes {
kittyFlags |= ansi.KittyReportEventTypes
}
_, _ = s.scr.WriteString(ansi.KittyKeyboard(kittyFlags, 1))
if !closing {
// Request keyboard enhancements when they change
_, _ = s.scr.WriteString(ansi.RequestKittyKeyboard)
}
}
// Set terminal colors.
var (
cc, lcc color.Color
lfg, lbg color.Color
)
if view.Cursor != nil {
cc = view.Cursor.Color
}
if s.lastView != nil {
if s.lastView.Cursor != nil {
lcc = s.lastView.Cursor.Color
}
lfg = s.lastView.ForegroundColor
lbg = s.lastView.BackgroundColor
}
for _, c := range []struct {
newColor color.Color
oldColor color.Color
reset string
setter func(string) string
}{
{newColor: cc, oldColor: lcc, reset: ansi.ResetCursorColor, setter: ansi.SetCursorColor},
{newColor: view.ForegroundColor, oldColor: lfg, reset: ansi.ResetForegroundColor, setter: ansi.SetForegroundColor},
{newColor: view.BackgroundColor, oldColor: lbg, reset: ansi.ResetBackgroundColor, setter: ansi.SetBackgroundColor},
} {
if c.newColor != c.oldColor {
if c.newColor == nil {
// Reset the color if it was set to nil.
_, _ = s.scr.WriteString(c.reset)
} else {
// Set the color.
col, ok := colorful.MakeColor(c.newColor)
if ok {
_, _ = s.scr.WriteString(c.setter(col.Hex()))
}
}
}
}
// Set cursor shape and blink if set.
var ccStyle, lcStyle int
var lcur *Cursor
ccur := view.Cursor
if lv := s.lastView; lv != nil {
lcur = lv.Cursor
}
if ccur != nil {
ccStyle = encodeCursorStyle(ccur.Shape, ccur.Blink)
}
if lcur != nil {
lcStyle = encodeCursorStyle(lcur.Shape, lcur.Blink)
}
if ccStyle != lcStyle {
_, _ = s.scr.WriteString(ansi.SetCursorStyle(ccStyle))
}
// Render progress bar if it's changed.
if (s.lastView == nil && view.ProgressBar != nil && view.ProgressBar.State != ProgressBarNone) ||
(s.lastView != nil && (s.lastView.ProgressBar == nil) != (view.ProgressBar == nil)) ||
(s.lastView != nil && s.lastView.ProgressBar != nil && view.ProgressBar != nil && *s.lastView.ProgressBar != *view.ProgressBar) {
// Render or clear the progress bar if it was added or removed.
setProgressBar(s, view.ProgressBar)
}
// Render and queue changes to the screen buffer.
s.scr.Render(s.cellbuf.RenderBuffer)
if cur := view.Cursor; cur != nil {
// MoveTo must come after [uv.TerminalRenderer.Render] because the
// cursor position might get updated during rendering.
s.scr.MoveTo(view.Cursor.X, view.Cursor.Y)
} else if !view.AltScreen {
// We don't want the cursor to be dangling at the end of the line in
// inline mode because it can cause unwanted line wraps in some
// terminals. So we move it to the beginning of the next line if
// necessary.
// This is only needed when the cursor is hidden because when it's
// visible, we already set its position above.
x, y := s.scr.Position()
if x >= s.width-1 {
s.scr.MoveTo(0, y)
}
}
if err := s.scr.Flush(); err != nil {
return fmt.Errorf("bubbletea: error flushing screen writer: %w", err)
}
// Check if we have any render updates to flush.
hasUpdates := s.buf.Len() > 0
// Cursor visibility.
didShowCursor := s.lastView != nil && s.lastView.Cursor != nil
showCursor := view.Cursor != nil
hideCursor := !showCursor
shouldUpdateCursorVis := (s.lastView == nil || didShowCursor != showCursor) || shouldUpdateAltScreen
// Build final output buffer with synchronized output or hide/show cursor
// updates. But first, enter/exit alt screen mode if needed.
//
// Here, we have two scenarios:
// 1. Synchronized output updates are supported. In this case, we want to
// wrap all updates, unless it's just a cursor visibility change, in
// synchronized output mode. This is because synchronized output mode
// takes care of rendering the updates atomically. In the case of
// just a cursor visibility change, we don't need to enter
// synchronized output mode because it's just a single sequence to
// flush out to the terminal.
//
// 2. We don't have synchronized output updates support. In this case, and
// if the cursor is visible or should be visible, we wrap the updates
// with hide/show cursor sequences to try and mitigate cursor
// flickering. This is terminal dependent and may still result in
// flickering in some terminals. It's the best effort we can do instead
// of showing the cursor flying around the screen during updates.
var buf bytes.Buffer
if shouldUpdateAltScreen {
// We always disable keyboard enhancements when switching screens
// because the terminal is expected to have two different keyboard
// registries for main and alt screens.
_, _ = buf.WriteString(ansi.ResetModifyOtherKeys)
_, _ = buf.WriteString(ansi.KittyKeyboard(0, 1))
if view.AltScreen {
// Entering alt screen mode.
buf.WriteString(ansi.SetModeAltScreenSaveCursor)
} else {
// Exiting alt screen mode.
buf.WriteString(ansi.ResetModeAltScreenSaveCursor)
}
}
if s.syncdUpdates {
if hasUpdates {
// We have synchronized output updates enabled.
buf.WriteString(ansi.SetModeSynchronizedOutput)
}
if shouldUpdateCursorVis && hideCursor {
// Do we need to update the cursor visibility to hidden? If so, do
// it here before writing any updates to the buffer.
_, _ = buf.WriteString(ansi.ResetModeTextCursorEnable)
}
} else if (shouldUpdateCursorVis && hideCursor) || (hasUpdates && showCursor && didShowCursor) {
_, _ = buf.WriteString(ansi.ResetModeTextCursorEnable)
}
if hasUpdates {
buf.Write(s.buf.Bytes())
}
if s.syncdUpdates {
if shouldUpdateCursorVis && showCursor {
// Do we need to update the cursor visibility to visible? If so, do
// it here after writing any updates to the buffer.
_, _ = buf.WriteString(ansi.SetModeTextCursorEnable)
}
if hasUpdates {
// Close synchronized output mode.
buf.WriteString(ansi.ResetModeSynchronizedOutput)
}
} else if (shouldUpdateCursorVis && showCursor) || (hasUpdates && showCursor && didShowCursor) {
_, _ = buf.WriteString(ansi.SetModeTextCursorEnable)
}
// Reset internal screen renderer buffer.
s.buf.Reset()
// If our updates flush buffer has content, write it to the output writer.
if buf.Len() > 0 {
if s.logger != nil {
s.logger.Printf("output: %q", buf.String())
}
if _, err := io.Copy(s.w, &buf); err != nil {
return fmt.Errorf("bubbletea: error flushing update to the writer: %w", err)
}
}
s.lastView = &view
return nil
}
// render implements renderer.
func (s *cursedRenderer) render(v View) {
s.mu.Lock()
defer s.mu.Unlock()
s.view = v
}
// reset implements renderer.
func (s *cursedRenderer) reset() {
s.mu.Lock()
reset(s)
s.mu.Unlock()
}
func reset(s *cursedRenderer) {
s.buf.Reset()
scr := uv.NewTerminalRenderer(&s.buf, s.env)
scr.SetColorProfile(s.profile)
scr.SetRelativeCursor(true) // Always start in inline mode
scr.SetFullscreen(false) // Always start in inline mode
scr.SetTabStops(s.width)
scr.SetBackspace(s.backspace)
scr.SetMapNewline(s.mapnl)
scr.SetScrollOptim(runtime.GOOS != "windows") // disable scroll optimization on Windows due to bugs in some terminals
s.scr = scr
}
// setColorProfile implements renderer.
func (s *cursedRenderer) setColorProfile(p colorprofile.Profile) {
s.mu.Lock()
s.profile = p
s.scr.SetColorProfile(p)
s.mu.Unlock()
}
// resize implements renderer.
func (s *cursedRenderer) resize(w, h int) {
s.mu.Lock()
// We need to mark the screen for clear to force a redraw. However, we
// only do so if we're using alt screen or the width has changed.
// That's because redrawing is expensive and we can avoid it if the
// width hasn't changed in inline mode. On the other hand, when using
// alt screen mode, we always want to redraw because some terminals
// would scroll the screen and our content would be lost.
s.scr.Erase()
s.width, s.height = w, h
s.scr.Resize(s.width, s.height)
s.mu.Unlock()
}
// clearScreen implements renderer.
func (s *cursedRenderer) clearScreen() {
s.mu.Lock()
// Move the cursor to the top left corner of the screen and trigger a full
// screen redraw.
s.scr.MoveTo(0, 0)
s.scr.Erase()
s.mu.Unlock()
}
// enableAltScreen sets the alt screen mode.
// Note that this writes to the buffer directly if write is true.
func enableAltScreen(s *cursedRenderer, enable bool, write bool) {
if enable {
enterAltScreen(s, write)
} else {
exitAltScreen(s, write)
}
}
func enterAltScreen(s *cursedRenderer, write bool) {
s.scr.SaveCursor()
if write {
s.buf.WriteString(ansi.SetModeAltScreenSaveCursor)
}
s.scr.SetFullscreen(true)
s.scr.SetRelativeCursor(false)
s.scr.Erase()
}
func exitAltScreen(s *cursedRenderer, write bool) {
s.scr.Erase()
s.scr.SetRelativeCursor(true)
s.scr.SetFullscreen(false)
if write {
s.buf.WriteString(ansi.ResetModeAltScreenSaveCursor)
}
s.scr.RestoreCursor()
}
// enableTextCursor sets the text cursor mode.
func enableTextCursor(s *cursedRenderer, enable bool) {
if enable {
_, _ = s.scr.WriteString(ansi.SetModeTextCursorEnable)
} else {
_, _ = s.scr.WriteString(ansi.ResetModeTextCursorEnable)
}
}
// setSyncdUpdates implements renderer.
func (s *cursedRenderer) setSyncdUpdates(syncd bool) {
s.mu.Lock()
s.syncdUpdates = syncd
s.mu.Unlock()
}
// setWidthMethod implements renderer.
func (s *cursedRenderer) setWidthMethod(method ansi.Method) {
s.mu.Lock()
if method == ansi.GraphemeWidth {
// Turn on Unicode mode (2027) for accurate grapheme width calculation.
// This is needed for proper rendering of wide characters and emojis.
_, _ = s.scr.WriteString(ansi.SetModeUnicodeCore)
} else if s.cellbuf.Method == ansi.GraphemeWidth {
// Turn off Unicode mode if we're switching away from grapheme width
// calculation to avoid issues with some terminals that might still be
// in Unicode mode and render characters incorrectly.
_, _ = s.scr.WriteString(ansi.ResetModeUnicodeCore)
}
s.cellbuf.Method = method
s.mu.Unlock()
}
// insertAbove implements renderer.
func (s *cursedRenderer) insertAbove(str string) error {
s.mu.Lock()
defer s.mu.Unlock()
if len(str) == 0 {
return nil
}
var sb strings.Builder
w, h := s.cellbuf.Width(), s.cellbuf.Height()
_, y := s.scr.Position()
// We need to scroll the screen up by the number of lines in the queue.
sb.WriteByte('\r')
down := h - y - 1
if down > 0 {
sb.WriteString(ansi.CursorDown(down))
}
lines := strings.Split(str, "\n")
offset := len(lines)
for _, line := range lines {
lineWidth := ansi.StringWidth(line)
if w > 0 && lineWidth > w {
offset += (lineWidth / w)
}
}
// Scroll the screen up by the offset to make room for the new lines.
sb.WriteString(strings.Repeat("\n", offset))
// XXX: Now go to the top of the screen, insert new lines, and write
// the queued strings. It is important to use [Screen.moveCursor]
// instead of [Screen.move] because we don't want to perform any checks
// on the cursor position.
up := offset + h - 1
sb.WriteString(ansi.CursorUp(up))
sb.WriteString(ansi.InsertLine(offset))
for _, line := range lines {
sb.WriteString(line)
sb.WriteString(ansi.EraseLineRight)
sb.WriteString("\r\n")
}
s.scr.SetPosition(0, 0)
if s.logger != nil {
s.logger.Printf("insert above: %q", sb.String())
}
_, err := io.WriteString(s.w, sb.String())
if err != nil {
return fmt.Errorf("bubbletea: error writing insert above to the writer: %w", err)
}
return nil
}
// onMouse implements renderer.
func (s *cursedRenderer) onMouse(m MouseMsg) Cmd {
if s.lastView != nil && s.lastView.OnMouse != nil {
return s.lastView.OnMouse(m)
}
return nil
}
func setProgressBar(s *cursedRenderer, pb *ProgressBar) {
if pb == nil {
_, _ = s.scr.WriteString(ansi.ResetProgressBar)
return
}
var seq string
switch pb.State {
case ProgressBarNone:
seq = ansi.ResetProgressBar
case ProgressBarDefault:
seq = ansi.SetProgressBar(pb.Value)
case ProgressBarError:
seq = ansi.SetErrorProgressBar(pb.Value)
case ProgressBarIndeterminate:
seq = ansi.SetIndeterminateProgressBar
case ProgressBarWarning:
seq = ansi.SetWarningProgressBar(pb.Value)
}
if seq != "" {
_, _ = s.scr.WriteString(seq)
}
}
func viewEquals(a, b *View) bool {
if a == nil || b == nil {
return false
}
if a.Content != b.Content ||
a.AltScreen != b.AltScreen ||
a.DisableBracketedPasteMode != b.DisableBracketedPasteMode ||
a.ReportFocus != b.ReportFocus ||
a.MouseMode != b.MouseMode ||
a.WindowTitle != b.WindowTitle ||
a.ForegroundColor != b.ForegroundColor ||
a.BackgroundColor != b.BackgroundColor ||
a.KeyboardEnhancements != b.KeyboardEnhancements {
return false
}
if (a.Cursor == nil) != (b.Cursor == nil) {
return false
}
if a.Cursor != nil && b.Cursor != nil {
if a.Cursor.X != b.Cursor.X ||
a.Cursor.Y != b.Cursor.Y ||
a.Cursor.Shape != b.Cursor.Shape ||
a.Cursor.Blink != b.Cursor.Blink ||
a.Cursor.Color != b.Cursor.Color {
return false
}
}
if (a.ProgressBar == nil) != (b.ProgressBar == nil) {
return false
}
if a.ProgressBar != nil && b.ProgressBar != nil {
if *a.ProgressBar != *b.ProgressBar {
return false
}
}
return true
}
================================================
FILE: cursor.go
================================================
package tea
// Position represents a position in the terminal.
type Position struct{ X, Y int }
// CursorPositionMsg is a message that represents the terminal cursor position.
type CursorPositionMsg struct {
X, Y int
}
// CursorShape represents a terminal cursor shape.
type CursorShape int
// Cursor shapes.
const (
CursorBlock CursorShape = iota
CursorUnderline
CursorBar
)
// requestCursorPosMsg is a message that requests the cursor position.
type requestCursorPosMsg struct{}
// RequestCursorPosition is a command that requests the cursor position.
// The cursor position will be sent as a [CursorPositionMsg] message.
func RequestCursorPosition() Msg {
return requestCursorPosMsg{}
}
================================================
FILE: environ.go
================================================
package tea
import uv "github.com/charmbracelet/ultraviolet"
// EnvMsg is a message that represents the environment variables of the
// program. This is useful for getting the environment variables of programs
// running in a remote session like SSH. In that case, using [os.Getenv] would
// return the server's environment variables, not the client's.
//
// This message is sent to the program when it starts.
//
// Example:
//
// switch msg := msg.(type) {
// case EnvMsg:
// // What terminal type is being used?
// term := msg.Getenv("TERM")
// }
type EnvMsg uv.Environ
// Getenv returns the value of the environment variable named by the key. If
// the variable is not present in the environment, the value returned will be
// the empty string.
func (msg EnvMsg) Getenv(key string) (v string) {
return uv.Environ(msg).Getenv(key)
}
// LookupEnv retrieves the value of the environment variable named by the key.
// If the variable is present in the environment the value (which may be empty)
// is returned and the boolean is true. Otherwise the returned value will be
// empty and the boolean will be false.
func (msg EnvMsg) LookupEnv(key string) (s string, v bool) {
return uv.Environ(msg).LookupEnv(key)
}
================================================
FILE: examples/README.md
================================================
# Examples
### Alt Screen Toggle
The `altscreen-toggle` example shows how to transition between the alternative
screen buffer and the normal screen buffer using Bubble Tea.
<a href="./altscreen-toggle/main.go">
<img width="750" src="./altscreen-toggle/altscreen-toggle.gif" />
</a>
### Chat
The `chat` examples shows a basic chat application with a multi-line `textarea`
input.
<a href="./chat/main.go">
<img width="750" src="./chat/chat.gif" />
</a>
### Composable Views
The `composable-views` example shows how to compose two bubble models (spinner
and timer) together in a single application and switch between them.
<a href="./composable-views/main.go">
<img width="750" src="./composable-views/composable-views.gif" />
</a>
### ISBN Book Form
The `isbn-form` example demonstrates how to build a multi-step form with
`textinput` bubbles and validation on the inputs.
<a href="./isbn-form/main.go">
<img width="750" src="./isbn-form/isbn-form.gif" />
</a>
### Debounce
The `debounce` example shows how to throttle key presses to avoid overloading
your Bubble Tea application.
<a href="./debounce/main.go">
<img width="750" src="./debounce/debounce.gif" />
</a>
### Exec
The `exec` example shows how to execute a running command during the execution
of a Bubble Tea application such as launching an `EDITOR`.
<a href="./exec/main.go">
<img width="750" src="./exec/exec.gif" />
</a>
### Full Screen
The `fullscreen` example shows how to make a Bubble Tea application fullscreen.
<a href="./fullscreen/main.go">
<img width="750" src="./fullscreen/fullscreen.gif" />
</a>
### Glamour
The `glamour` example shows how to use [Glamour](https://github.com/charmbracelet/glamour) inside a viewport bubble.
<a href="./glamour/main.go">
<img width="750" src="./glamour/glamour.gif" />
</a>
### Help
The `help` example shows how to use the `help` bubble to display help to the
user of your application.
<a href="./help/main.go">
<img width="750" src="./help/help.gif" />
</a>
### Http
The `http` example shows how to make an `http` call within your Bubble Tea
application.
<a href="./http/main.go">
<img width="750" src="./http/http.gif" />
</a>
### Default List
The `list-default` example shows how to use the list bubble.
<a href="./list-default/main.go">
<img width="750" src="./list-default/list-default.gif" />
</a>
### Fancy List
The `list-fancy` example shows how to use the list bubble with extra customizations.
<a href="./list-fancy/main.go">
<img width="750" src="./list-fancy/list-fancy.gif" />
</a>
### Simple List
The `list-simple` example shows how to use the list and customize it to have a simpler, more compact, appearance.
<a href="./list-simple/main.go">
<img width="750" src="./list-simple/list-simple.gif" />
</a>
### Mouse
The `mouse` example shows how to receive mouse events in a Bubble Tea
application.
<a href="./mouse/main.go">
Code
</a>
### Package Manager
The `package-manager` example shows how to build an interface for a package
manager using the `tea.Println` feature.
<a href="./package-manager/main.go">
<img width="750" src="./package-manager/package-manager.gif" />
</a>
### Pager
The `pager` example shows how to build a simple pager application similar to
`less`.
<a href="./pager/main.go">
<img width="750" src="./pager/pager.gif" />
</a>
### Paginator
The `paginator` example shows how to build a simple paginated list.
<a href="./paginator/main.go">
<img width="750" src="./paginator/paginator.gif" />
</a>
### Pipe
The `pipe` example demonstrates using shell pipes to communicate with Bubble
Tea applications.
<a href="./pipe/main.go">
<img width="750" src="./pipe/pipe.gif" />
</a>
### Animated Progress
The `progress-animated` example shows how to build a progress bar with an
animated progression.
<a href="./progress-animated/main.go">
<img width="750" src="./progress-animated/progress-animated.gif" />
</a>
### Download Progress
The `progress-download` example demonstrates how to download a file while
indicating download progress through Bubble Tea.
<a href="./progress-download/main.go">
Code
</a>
### Static Progress
The `progress-static` example shows a progress bar with static incrementation
of progress.
<a href="./progress-static/main.go">
<img width="750" src="./progress-static/progress-static.gif" />
</a>
### Real Time
The `realtime` example demonstrates the use of go channels to perform realtime
communication with a Bubble Tea application.
<a href="./realtime/main.go">
<img width="750" src="./realtime/realtime.gif" />
</a>
### Result
The `result` example shows a choice menu with the ability to select an option.
<a href="./result/main.go">
<img width="750" src="./result/result.gif" />
</a>
### Send Msg
The `send-msg` example demonstrates the usage of custom `tea.Msg`s.
<a href="./send-msg/main.go">
<img width="750" src="./send-msg/send-msg.gif" />
</a>
### Sequence
The `sequence` example demonstrates the `tea.Sequence` command.
<a href="./sequence/main.go">
<img width="750" src="./sequence/sequence.gif" />
</a>
### Simple
The `simple` example shows a very simple Bubble Tea application.
<a href="./simple/main.go">
<img width="750" src="./simple/simple.gif" />
</a>
### Spinner
The `spinner` example demonstrates a spinner bubble being used to indicate loading.
<a href="./spinner/main.go">
<img width="750" src="./spinner/spinner.gif" />
</a>
### Spinners
The `spinner` example shows various spinner types that are available.
<a href="./spinners/main.go">
<img width="750" src="./spinners/spinners.gif" />
</a>
### Split Editors
The `split-editors` example shows multiple `textarea`s being used in a single
application and being able to switch focus between them.
<a href="./split-editors/main.go">
<img width="750" src="./split-editors/split-editors.gif" />
</a>
### Stop Watch
The `stopwatch` example shows a sample stop watch built with Bubble Tea.
<a href="./stopwatch/main.go">
<img width="750" src="./stopwatch/stopwatch.gif" />
</a>
### Table
The `table` example demonstrates the table bubble being used to display tabular
data.
<a href="./table/main.go">
<img width="750" src="./table/table.gif" />
</a>
### Tabs
The `tabs` example demonstrates tabbed navigation styled with [Lip Gloss](https://github.com/charmbracelet/lipgloss).
<a href="./tabs/main.go">
<img width="750" src="./tabs/tabs.gif" />
</a>
### Text Area
The `textarea` example demonstrates a simple Bubble Tea application using a
`textarea` bubble.
<a href="./textarea/main.go">
<img width="750" src="./textarea/textarea.gif" />
</a>
### Text Input
The `textinput` example demonstrates a simple Bubble Tea application using a `textinput` bubble.
<a href="./textinput/main.go">
<img width="750" src="./textinput/textinput.gif" />
</a>
### Multiple Text Inputs
The `textinputs` example shows multiple `textinputs` and being able to switch
focus between them as well as changing the cursor mode.
<a href="./textinputs/main.go">
<img width="750" src="./textinputs/textinputs.gif" />
</a>
### Timer
The `timer` example shows a simple timer built with Bubble Tea.
<a href="./timer/main.go">
<img width="750" src="./timer/timer.gif" />
</a>
### TUI Daemon
The `tui-daemon-combo` demonstrates building a text-user interface along with a
daemon mode using Bubble Tea.
<a href="./tui-daemon-combo/main.go">
<img width="750" src="./tui-daemon-combo/tui-daemon-combo.gif" />
</a>
### Views
The `views` example demonstrates how to build a Bubble Tea application with
multiple views and switch between them.
<a href="./views/main.go">
<img width="750" src="./views/views.gif" />
</a>
================================================
FILE: examples/altscreen-toggle/README.md
================================================
# Alt Screen Toggle
<img width="800" src="./altscreen-toggle.gif" />
================================================
FILE: examples/altscreen-toggle/main.go
================================================
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
var (
keywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("204")).Background(lipgloss.Color("235"))
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
)
type model struct {
altscreen bool
quitting bool
suspending 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.ResumeMsg:
m.suspending = false
return m, nil
case tea.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
m.quitting = true
return m, tea.Quit
case "ctrl+z":
m.suspending = true
return m, tea.Suspend
case "space":
var cmd tea.Cmd
m.altscreen = !m.altscreen
return m, cmd
}
}
return m, nil
}
func (m model) View() tea.View {
if m.suspending {
v := tea.NewView("")
v.AltScreen = m.altscreen
return v
}
if m.quitting {
v := tea.NewView("Bye!\n")
v.AltScreen = m.altscreen
return v
}
const (
altscreenMode = " altscreen mode "
inlineMode = " inline mode "
)
var mode string
if m.altscreen {
mode = altscreenMode
} else {
mode = inlineMode
}
v := tea.NewView(fmt.Sprintf("\n\n You're in %s\n\n\n", keywordStyle.Render(mode)) +
helpStyle.Render(" space: switch modes • ctrl-z: suspend • q: exit\n"))
v.AltScreen = m.altscreen
return v
}
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
================================================
FILE: examples/autocomplete/main.go
================================================
package main
import (
"encoding/json"
"io"
"log"
"net/http"
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
type (
gotReposSuccessMsg []repo
gotReposErrMsg error
)
type repo struct {
Name string `json:"name"`
}
const reposURL = "https://api.github.com/orgs/charmbracelet/repos"
func getRepos() tea.Msg {
req, err := http.NewRequest(http.MethodGet, reposURL, nil)
if err != nil {
return gotReposErrMsg(err)
}
req.Header.Add("Accept", "application/vnd.github+json")
req.Header.Add("X-GitHub-Api-Version", "2022-11-28")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return gotReposErrMsg(err)
}
defer resp.Body.Close() // nolint: errcheck
data, err := io.ReadAll(resp.Body)
if err != nil {
return gotReposErrMsg(err)
}
var repos []repo
err = json.Unmarshal(data, &repos)
if err != nil {
return gotReposErrMsg(err)
}
return gotReposSuccessMsg(repos)
}
type model struct {
textInput textinput.Model
help help.Model
keymap keymap
}
type keymap struct {
complete, next, prev, quit key.Binding
}
func (k keymap) ShortHelp() []key.Binding {
return []key.Binding{
k.complete,
k.next,
k.prev,
k.quit,
}
}
func (k keymap) FullHelp() [][]key.Binding {
return [][]key.Binding{k.ShortHelp()}
}
func initialModel() model {
ti := textinput.New()
ti.Prompt = "charmbracelet/"
s := ti.Styles()
s.Focused.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginLeft(2)
s.Cursor.Color = lipgloss.Color("63")
ti.SetStyles(s)
ti.SetVirtualCursor(false)
ti.Focus()
ti.CharLimit = 50
ti.SetWidth(20)
ti.ShowSuggestions = true
km := keymap{
// XXX: we should be using the keybindings on the textinput model.
complete: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete"), key.WithDisabled()),
next: key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next"), key.WithDisabled()),
prev: key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev"), key.WithDisabled()),
quit: key.NewBinding(key.WithKeys("enter", "ctrl+c", "esc"), key.WithHelp("esc", "quit")),
}
return model{
textInput: ti,
keymap: km,
help: help.New(),
}
}
func (m model) Init() tea.Cmd {
return tea.Batch(getRepos, textinput.Blink)
}
func (m model) Cursor() *tea.Cursor {
c := m.textInput.Cursor()
if c != nil {
c.Y += lipgloss.Height(m.headerView())
}
return c
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case gotReposSuccessMsg:
var suggestions []string
for _, r := range msg {
suggestions = append(suggestions, r.Name)
}
m.textInput.SetSuggestions(suggestions)
case tea.KeyPressMsg:
switch {
case key.Matches(msg, m.keymap.quit):
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
// Determine whether to show completion keybindings.
//
// XXX: we should be using the keybindings on the textinput model.
hasChoices := len(m.textInput.MatchedSuggestions()) > 1
m.keymap.complete.SetEnabled(hasChoices)
m.keymap.next.SetEnabled(hasChoices)
m.keymap.prev.SetEnabled(hasChoices)
return m, cmd
}
func (m model) View() tea.View {
if len(m.textInput.AvailableSuggestions()) < 1 {
return tea.NewView("One sec, we're fetching completions...")
}
v := tea.NewView(lipgloss.JoinVertical(
lipgloss.Left,
m.headerView(),
m.textInput.View(),
m.footerView(),
))
c := m.textInput.Cursor()
if c != nil {
c.Y += lipgloss.Height(m.headerView())
}
v.Cursor = c
return v
}
func (m model) headerView() string { return "Enter a Charm™ repo:\n" }
func (m model) footerView() string { return "\n" + m.help.View(m.keymap) }
================================================
FILE: examples/canvas/main.go
================================================
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/exp/charmtone"
)
type model struct {
width int
flip 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.WindowSizeMsg:
m.width = msg.Width
return m, nil
case tea.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
m.quitting = true
return m, tea.Quit
default:
m.flip = !m.flip
}
}
return m, nil
}
func (m model) View() tea.View {
var view tea.View
if m.quitting {
return view
}
z := []int{0, 1}
if m.flip {
z = reverse(z)
}
footer := lipgloss.NewStyle().
Height(13).
Foreground(charmtone.Oyster).
AlignVertical(lipgloss.Bottom).
Render("Press any key to swap the cards, or q to quit.")
cardA := newCard("Hello").Z(z[0])
cardB := newCard("Goodbye").Z(z[1])
comp := lipgloss.NewCompositor(
lipgloss.NewLayer(footer),
cardA,
cardB.X(10).Y(2),
)
view.SetContent(comp.Render())
return view
}
func newCard(str string) *lipgloss.Layer {
return lipgloss.NewLayer(
lipgloss.NewStyle().
Width(20).
Height(10).
Border(lipgloss.RoundedBorder()).
BorderForeground(charmtone.Charple).
Align(lipgloss.Center, lipgloss.Center).
Render(str),
)
}
// Reverse a slice, returning a new slice.
func reverse[T any](s []T) []T {
n := len(s)
r := make([]T, n)
for i, v := range s {
r[n-1-i] = v
}
return r
}
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Fprintln(os.Stderr, "Urgh:", err)
os.Exit(1)
}
}
================================================
FILE: examples/capability/main.go
================================================
package main
import (
"fmt"
"os"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
type model struct {
input textinput.Model
width int
}
var _ tea.Model = model{}
// Init implements tea.Model.
func (m model) Init() tea.Cmd {
return m.input.Focus()
}
// Update implements tea.Model.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "esc":
return m, tea.Quit
case "enter":
input := m.input.Value()
m.input.Reset()
return m, tea.RequestCapability(input)
}
case tea.CapabilityMsg:
return m, tea.Printf("Got capability: %s", msg)
}
m.input, cmd = m.input.Update(msg)
return m, cmd
}
// View implements tea.Model.
func (m model) View() tea.View {
w := min(m.width, 60)
instructions := lipgloss.NewStyle().
Width(w).
Render("Query for terminal capabilities. You can enter things like 'TN', 'RGB', 'cols', and so on. This will not work in all terminals and multiplexers.")
return tea.NewView("\n" + instructions + "\n\n" +
m.input.View() +
"\n\nPress enter to request capability, or ctrl+c to quit.")
}
func main() {
m := model{}
m.input = textinput.New()
m.input.Placeholder = "Enter capability name to request"
m.input.Focus()
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Fprintln(os.Stderr, "Uh oh:", err)
os.Exit(1)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
================================================
FILE: examples/cellbuffer/main.go
================================================
package main
// A simple example demonstrating how to draw and animate on a cellular grid.
// Note that the cellbuffer implementation in this example does not support
// double-width runes.
import (
"fmt"
"os"
"strings"
"time"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/harmonica"
)
const (
fps = 60
frequency = 7.5
damping = 0.15
asterisk = "*"
)
func drawEllipse(cb *cellbuffer, xc, yc, rx, ry float64) {
var (
dx, dy, d1, d2 float64
x float64
y = ry
)
d1 = ry*ry - rx*rx*ry + 0.25*rx*rx
dx = 2 * ry * ry * x
dy = 2 * rx * rx * y
for dx < dy {
cb.set(int(x+xc), int(y+yc))
cb.set(int(-x+xc), int(y+yc))
cb.set(int(x+xc), int(-y+yc))
cb.set(int(-x+xc), int(-y+yc))
if d1 < 0 {
x++
dx = dx + (2 * ry * ry)
d1 = d1 + dx + (ry * ry)
} else {
x++
y--
dx = dx + (2 * ry * ry)
dy = dy - (2 * rx * rx)
d1 = d1 + dx - dy + (ry * ry)
}
}
d2 = ((ry * ry) * ((x + 0.5) * (x + 0.5))) + ((rx * rx) * ((y - 1) * (y - 1))) - (rx * rx * ry * ry)
for y >= 0 {
cb.set(int(x+xc), int(y+yc))
cb.set(int(-x+xc), int(y+yc))
cb.set(int(x+xc), int(-y+yc))
cb.set(int(-x+xc), int(-y+yc))
if d2 > 0 {
y--
dy = dy - (2 * rx * rx)
d2 = d2 + (rx * rx) - dy
} else {
y--
x++
dx = dx + (2 * ry * ry)
dy = dy - (2 * rx * rx)
d2 = d2 + dx - dy + (rx * rx)
}
}
}
type cellbuffer struct {
cells []string
stride int
}
func (c *cellbuffer) init(w, h int) {
if w == 0 {
return
}
c.stride = w
c.cells = make([]string, w*h)
c.wipe()
}
func (c cellbuffer) set(x, y int) {
i := y*c.stride + x
if i > len(c.cells)-1 || x < 0 || y < 0 || x >= c.width() || y >= c.height() {
return
}
c.cells[i] = asterisk
}
func (c *cellbuffer) wipe() {
for i := range c.cells {
c.cells[i] = " "
}
}
func (c cellbuffer) width() int {
return c.stride
}
func (c cellbuffer) height() int {
h := len(c.cells) / c.stride
if len(c.cells)%c.stride != 0 {
h++
}
return h
}
func (c cellbuffer) ready() bool {
return len(c.cells) > 0
}
func (c cellbuffer) String() string {
var b strings.Builder
for i := range c.cells {
if i > 0 && i%c.stride == 0 && i < len(c.cells)-1 {
b.WriteRune('\n')
}
b.WriteString(c.cells[i])
}
return b.String()
}
type frameMsg struct{}
func animate() tea.Cmd {
return tea.Tick(time.Second/fps, func(_ time.Time) tea.Msg {
return frameMsg{}
})
}
type model struct {
cells cellbuffer
spring harmonica.Spring
targetX, targetY float64
x, y float64
xVelocity, yVelocity float64
}
func (m model) Init() tea.Cmd {
return animate()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
return m, tea.Quit
case tea.WindowSizeMsg:
if !m.cells.ready() {
m.targetX, m.targetY = float64(msg.Width)/2, float64(msg.Height)/2
}
m.cells.init(msg.Width, msg.Height)
return m, nil
case tea.MouseMsg:
switch msg.(type) {
case tea.MouseClickMsg, tea.MouseMotionMsg:
default:
break
}
if !m.cells.ready() {
return m, nil
}
mouse := msg.Mouse()
m.targetX, m.targetY = float64(mouse.X), float64(mouse.Y)
return m, nil
case frameMsg:
if !m.cells.ready() {
return m, nil
}
m.cells.wipe()
m.x, m.xVelocity = m.spring.Update(m.x, m.xVelocity, m.targetX)
m.y, m.yVelocity = m.spring.Update(m.y, m.yVelocity, m.targetY)
drawEllipse(&m.cells, m.x, m.y, 16, 8)
return m, animate()
default:
return m, nil
}
}
func (m model) View() tea.View {
v := tea.NewView(m.cells.String())
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
return v
}
func main() {
m := model{
spring: harmonica.NewSpring(harmonica.FPS(fps), frequency, damping),
}
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, "Uh oh:", err)
os.Exit(1)
}
}
================================================
FILE: examples/chat/README.md
================================================
# Chat
<img width="800" src="./chat.gif" />
================================================
FILE: examples/chat/main.go
================================================
package main
// A simple program demonstrating the text area component from the Bubbles
// component library.
import (
"fmt"
"os"
"strings"
"charm.land/bubbles/v2/cursor"
"charm.land/bubbles/v2/textarea"
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Oof: %v\n", err)
}
}
type model struct {
viewport viewport.Model
messages []string
textarea textarea.Model
senderStyle lipgloss.Style
err error
}
func initialModel() model {
ta := textarea.New()
ta.Placeholder = "Send a message..."
ta.SetVirtualCursor(false)
ta.Focus()
ta.Prompt = "┃ "
ta.CharLimit = 280
ta.SetWidth(30)
ta.SetHeight(3)
// Remove cursor line styling
s := ta.Styles()
s.Focused.CursorLine = lipgloss.NewStyle()
ta.SetStyles(s)
ta.ShowLineNumbers = false
vp := viewport.New(viewport.WithWidth(30), viewport.WithHeight(5))
vp.SetContent(`Welcome to the chat room!
Type a message and press Enter to send.`)
vp.KeyMap.Left.SetEnabled(false)
vp.KeyMap.Right.SetEnabled(false)
ta.KeyMap.InsertNewline.SetEnabled(false)
return model{
textarea: ta,
messages: []string{},
viewport: vp,
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
err: nil,
}
}
func (m model) Init() tea.Cmd {
return textarea.Blink
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.viewport.SetWidth(msg.Width)
m.textarea.SetWidth(msg.Width)
m.viewport.SetHeight(msg.Height - m.textarea.Height())
if len(m.messages) > 0 {
// Wrap content before setting it.
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(strings.Join(m.messages, "\n")))
}
m.viewport.GotoBottom()
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "esc":
fmt.Println(m.textarea.Value())
return m, tea.Quit
case "enter":
m.messages = append(m.messages, m.senderStyle.Render("You: ")+m.textarea.Value())
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(strings.Join(m.messages, "\n")))
m.textarea.Reset()
m.viewport.GotoBottom()
return m, nil
default:
// Send all other keypresses to the textarea.
var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
case cursor.BlinkMsg:
// Textarea should also process cursor blinks.
var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
}
return m, nil
}
func (m model) View() tea.View {
viewportView := m.viewport.View()
v := tea.NewView(viewportView + "\n" + m.textarea.View())
c := m.textarea.Cursor()
if c != nil {
c.Y += lipgloss.Height(viewportView)
}
v.Cursor = c
v.AltScreen = true
return v
}
================================================
FILE: examples/clickable/main.go
================================================
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/segmentio/ksuid"
)
// LayerHitMsg is a message that is sent to the program when a layer is hit by
// a mouse event. This is used to determine which layer in a compostable view
// was hit by the mouse event. The layer is identified by its ID, which is a
// string that is unique to the layer.
type LayerHitMsg struct {
ID string
Mouse tea.MouseMsg
}
const maxDialogs = 999
// Styles
var (
bgTextStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("239")).
Padding(1, 2)
bgWhitespace = []lipgloss.WhitespaceOption{
lipgloss.WithWhitespaceChars("/"),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))),
}
dialogWordStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#E7E1CC"))
dialogStyle = dialogWordStyle.
Width(36).
Height(8).
Padding(1, 3).
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#874BFD"))
hoveredDialogStyle = dialogStyle.
BorderForeground(lipgloss.Color("#F25D94"))
specialWordLightColor = lipgloss.Color("#43BF6D")
specialWordDarkColor = lipgloss.Color("#73F59F")
buttonStyle = lipgloss.NewStyle().
Padding(0, 3).
Foreground(lipgloss.Color("#FFF7DB")).
Background(lipgloss.Color("#6124DF"))
hoveredButtonStyle = buttonStyle.
Background(lipgloss.Color("#FF5F87"))
)
// Model
type model struct {
specialWordStyle lipgloss.Style
width, height int
dialogs []dialog
mouseDown bool
pressID string
dragID string
dragOffsetX int
dragOffsetY int
}
func (m model) Init() tea.Cmd {
return tea.Batch(
tea.RequestBackgroundColor,
)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.BackgroundColorMsg:
if msg.IsDark() {
m.specialWordStyle = m.specialWordStyle.Foreground(specialWordDarkColor)
} else {
m.specialWordStyle = m.specialWordStyle.Foreground(specialWordLightColor)
}
case tea.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return m, tea.Quit
}
case LayerHitMsg:
mouse := msg.Mouse.Mouse()
switch msg.Mouse.(type) {
case tea.MouseClickMsg:
if mouse.Button != tea.MouseLeft {
break
}
// Initial press
if !m.mouseDown {
m.mouseDown = true
m.pressID = msg.ID
// Did we press on a dialog box?
for i, d := range m.dialogs {
if d.id != msg.ID {
continue
}
// Init drag
m.dragID = msg.ID
m.dragOffsetX = mouse.X - d.x
m.dragOffsetY = mouse.Y - d.y
if len(m.dialogs) < 2 {
break
}
// Move the one we're going to drag to the end of the slice
// so that it gets the highest z-index when we do
// compositing later. There are, of course, lots of other
// ways you could manage the z-index, too.
m.dialogs = m.removeDialog(i)
m.dialogs = append(m.dialogs, d)
break
}
break
}
// MouseMotion events are send when the mouse has moved and a mouse
// button is not pressed.
case tea.MouseMotionMsg:
// Dragging
if m.mouseDown && m.dragID != "" {
// Find the dialog box we're dragging
for i := range m.dialogs {
d := &m.dialogs[i]
if d.id != m.dragID {
continue
}
// Move the dialog box with the cursor
if m.dragID == d.id {
d.x = clamp(mouse.X-(m.dragOffsetX), 0, m.width-lipgloss.Width(d.windowView()))
d.y = clamp(mouse.Y-(m.dragOffsetY), 0, m.height-lipgloss.Height(d.windowView()))
}
break
}
}
// Are we hoving over a dialog box?
for i := range m.dialogs {
d := &m.dialogs[i]
d.hovering = false
d.hoveringButton = false
if d.id == msg.ID {
d.hovering = true
continue
}
if d.buttonID == msg.ID {
d.hovering = true
d.hoveringButton = true
continue
}
}
case tea.MouseReleaseMsg:
// Make sure we're releasing on something with an ID. A successful
// click is a press and release.
if m.pressID == "" {
break
}
// Did we click a button?
for i, d := range m.dialogs {
if msg.ID == d.buttonID && m.pressID == d.buttonID {
// "Close" the window
m.dialogs = m.removeDialog(i)
break
}
}
// Clicking the background spawns a new dialog
if msg.ID == "bg" && m.pressID == "bg" {
if len(m.dialogs) < maxDialogs {
m.dialogs = append(m.dialogs, m.newDialog(mouse.X, mouse.Y))
}
}
m.mouseDown = false
m.dragID = ""
m.pressID = ""
}
}
return m, nil
}
func (m model) View() tea.View {
var v tea.View
var body string
n := len(m.dialogs)
if n > 0 {
body += "Drag to move. "
}
if n == 0 && n < maxDialogs {
body += "Click to spawn."
} else if n >= 1 && n < maxDialogs {
body += fmt.Sprintf("Click to spawn up to %d more.", maxDialogs-len(m.dialogs))
}
body += "\n\nPress q to quit."
bg := lipgloss.Place(
m.width,
m.height,
lipgloss.Top,
lipgloss.Left,
bgTextStyle.Render(body),
bgWhitespace...,
)
root := lipgloss.NewLayer(bg).ID("bg")
for i, d := range m.dialogs {
root.AddLayers(d.view().Z(i + 1))
}
comp := lipgloss.NewCompositor(root)
v.MouseMode = tea.MouseModeAllMotion
v.AltScreen = true
v.OnMouse = func(msg tea.MouseMsg) tea.Cmd {
return func() tea.Msg {
mouse := msg.Mouse()
x, y := mouse.X, mouse.Y
if id := comp.Hit(x, y).ID(); id != "" {
return LayerHitMsg{
ID: id,
Mouse: msg,
}
}
return nil
}
}
v.SetContent(comp.Render())
return v
}
func (m *model) newDialog(x, y int) (d dialog) {
d.specialWordStyle = &m.specialWordStyle
dummyView := d.windowView()
w := lipgloss.Width(dummyView)
h := lipgloss.Height(dummyView)
d.x = clamp(x-w/2, 0, m.width-w)
d.y = clamp(y-h/2, 0, m.height-h)
d.text = nextRandomWord()
d.id = ksuid.New().String()
d.buttonID = ksuid.New().String()
return d
}
func (m model) removeDialog(index int) []dialog {
d := m.dialogs
if len(d) <= index {
return m.dialogs
}
copy(d[index:], d[index+1:]) // shift
d[len(d)-1] = dialog{} // nullify
return d[:len(d)-1] // truncate
}
// Dialog Windows
type dialog struct {
specialWordStyle *lipgloss.Style
id string
buttonID string
x, y int
text string
hovering bool
hoveringButton bool
}
func (d dialog) buttonView() string {
const label = "Run Away"
if d.hoveringButton {
return hoveredButtonStyle.Render(label)
}
return buttonStyle.Render(label)
}
func (d dialog) windowView() string {
var style lipgloss.Style
if d.hovering {
style = hoveredDialogStyle
} else {
style = dialogStyle
}
s := d.specialWordStyle.Render(d.text) + dialogWordStyle.Render(" draws near. Command?")
return style.Render(s)
}
func (d dialog) view() *lipgloss.Layer {
const hGap, vGap = 3, 1
window := d.windowView()
button := d.buttonView()
buttonX := lipgloss.Width(window) - lipgloss.Width(button) - 1 - hGap
buttonY := lipgloss.Height(window) - lipgloss.Height(button) - 1 - vGap
buttonLayer := lipgloss.NewLayer(button).
ID(d.buttonID).
X(buttonX).
Y(buttonY)
return lipgloss.NewLayer(window).
ID(d.id).
X(d.x).
Y(d.y).
AddLayers(buttonLayer)
}
// Main
func main() {
ksuid.SetRand(ksuid.FastRander)
path := os.Getenv("TEA_LOGFILE")
if path != "" {
f, err := tea.LogToFile(path, "layers")
if err != nil {
fmt.Println("could not open logfile:", err)
os.Exit(1)
}
defer f.Close()
}
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Println("Error while running program:", err)
os.Exit(1)
}
}
func clamp(n, min, max int) int {
if n < min {
return min
}
if n > max {
return max
}
return n
}
================================================
FILE: examples/clickable/words.go
================================================
package main
import (
"math/rand"
"strings"
"sync"
)
const uncapitalized = " of a an and ’n’ "
var (
adjectives = []string{
"a hot", "a cute", "a fresh", "a nice", "a lovely",
"an eager", "a soft", "an expensive", "a new", "an old", "a happy",
"a messy", "a good", "a bad", "a cheesy", "a friendly", "a free",
"a cold", "a gorgeous", "a glamorous", "a handsome", "an exquisite",
"a tantalizing", "a suspicious", "an american", "a wooden", "a golden",
"a dirty", "a hairy", "a lukewarm", "a burning hot", "a shiny",
"a rogue", "a green", "a late night", "a mass produced", "a handmade",
"a wild", "a clean", "a rugged", "the #1", "the best", "the worst",
"a famous", "an infamous", "a clever", "a microwaved", "a 3D printed",
"your favorite", "your least favorite", "someone’s", "a precious",
"a fake", "a genuine", "a bejeweled", "a good-smelling",
}
nouns = []string{
"pear", "banana", "bowl of ramen", "currywurst", "quince",
"pie", "cake", "burrito", "sushi", "basket of fish ’n’ chips", "burger",
"kohlrabi", "pineapple", "cantaloupe", "sausage roll", "yuzu",
"grapefruit", "espresso shot", "sandwich", "bowl of chow mein", "lemon",
"cup of coffee", "bottle of hot sauce", "can of beer", "glass of wine",
"muffin", "bagel", "glass of champagne", "bottle of rosé", "pengu",
"badger", "mango", "okonomiyaki", "meatball", "box of wine",
"artichoke", "TUI", "linux distro", "dotfile", "weißwurst", "computer",
}
shuffle sync.Once
nextWordMtx sync.Mutex
)
func nextRandomWord() string {
shuffle.Do(shuffleWords)
nextWordMtx.Lock()
defer nextWordMtx.Unlock()
adjectives = cycle(adjectives)
nouns = cycle(nouns)
return capitalize(adjectives[0] + " " + nouns[0])
}
func shuffleWords() {
shuf := func(x []string) {
rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] })
}
shuf(adjectives)
shuf(nouns)
}
func capitalize(s string) string {
words := strings.Fields(s)
for i, w := range words {
if i > 0 && strings.Contains(uncapitalized, " "+w+" ") {
words[i] = w
} else {
words[i] = strings.Title(w)
}
}
return strings.Join(words, " ")
}
func cycle(stack []string) []string {
return append(stack[1:], stack[0])
}
================================================
FILE: examples/colorprofile/main.go
================================================
package main
import (
"image/color"
"log"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/ansi"
"github.com/lucasb-eyer/go-colorful"
)
var myFancyColor color.Color
type model struct{}
var _ tea.Model = model{}
// Init implements tea.Model.
func (m model) Init() tea.Cmd {
return tea.Batch(
tea.RequestCapability("RGB"),
tea.RequestCapability("Tc"),
)
}
// Update implements tea.Model.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m, tea.Quit
case tea.ColorProfileMsg:
return m, tea.Println("Color profile manually set to ", msg)
}
return m, nil
}
// View implements tea.Model.
func (m model) View() tea.View {
return tea.NewView("This will produce the wrong colors on Apple Terminal :)\n\n" +
ansi.Style{}.ForegroundColor(myFancyColor).Styled("Howdy!") +
"\n\n" +
"Press any key to exit.")
}
func main() {
myFancyColor, _ = colorful.Hex("#6b50ff")
p := tea.NewProgram(model{}, tea.WithColorProfile(colorprofile.TrueColor))
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/composable-views/README.md
================================================
# Composable Views
<img width="800" src="./composable-views.gif" />
================================================
FILE: examples/composable-views/main.go
================================================
package main
import (
"fmt"
"log"
"strings"
"time"
"charm.land/bubbles/v2/spinner"
"charm.land/bubbles/v2/timer"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
/*
This example assumes an existing understanding of commands and messages. If you
haven't already read our tutorials on the basics of Bubble Tea and working with
commands, we recommend reading those first.
Find them at:
https://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands
https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics
*/
// sessionState is used to track which model is focused
type sessionState uint
const (
defaultTime = time.Minute
timerView sessionState = iota
spinnerView
)
var (
// Available spinners
spinners = []spinner.Spinner{
spinner.Line,
spinner.Dot,
spinner.MiniDot,
spinner.Jump,
spinner.Pulse,
spinner.Points,
spinner.Globe,
spinner.Moon,
spinner.Monkey,
}
modelStyle = lipgloss.NewStyle().
Width(15).
Height(5).
Align(lipgloss.Center, lipgloss.Center).
BorderStyle(lipgloss.HiddenBorder())
focusedModelStyle = lipgloss.NewStyle().
Width(15).
Height(5).
Align(lipgloss.Center, lipgloss.Center).
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("69"))
spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69"))
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
)
type mainModel struct {
state sessionState
timer timer.Model
spinner spinner.Model
index int
}
func newModel(timeout time.Duration) mainModel {
m := mainModel{state: timerView}
m.timer = timer.New(timeout)
m.spinner = spinner.New()
return m
}
func (m mainModel) Init() tea.Cmd {
// start the timer and spinner on program start
return tea.Batch(m.timer.Init(), m.spinner.Tick)
}
func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "tab":
if m.state == timerView {
m.state = spinnerView
} else {
m.state = timerView
}
case "n":
if m.state == timerView {
m.timer = timer.New(defaultTime)
cmds = append(cmds, m.timer.Init())
} else {
m.Next()
m.resetSpinner()
cmds = append(cmds, m.spinner.Tick)
}
}
switch m.state {
// update whichever model is focused
case spinnerView:
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
default:
m.timer, cmd = m.timer.Update(msg)
cmds = append(cmds, cmd)
}
case spinner.TickMsg:
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
case timer.TickMsg:
m.timer, cmd = m.timer.Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
func (m mainModel) View() tea.View {
var s strings.Builder
model := m.currentFocusedModel()
if m.state == timerView {
s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), modelStyle.Render(m.spinner.View())))
} else {
s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, modelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), focusedModelStyle.Render(m.spinner.View())))
}
s.WriteString(helpStyle.Render(fmt.Sprintf("\ntab: focus next • n: new %s • q: exit\n", model)))
return tea.NewView(s.String())
}
func (m mainModel) currentFocusedModel() string {
if m.state == timerView {
return "timer"
}
return "spinner"
}
func (m *mainModel) Next() {
if m.index == len(spinners)-1 {
m.index = 0
} else {
m.index++
}
}
func (m *mainModel) resetSpinner() {
m.spinner = spinner.New()
m.spinner.Style = spinnerStyle
m.spinner.Spinner = spinners[m.index]
}
func main() {
p := tea.NewProgram(newModel(defaultTime))
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
================================================
FILE: examples/cursor-style/main.go
================================================
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)
type model struct {
cursor tea.Cursor
blink 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.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "h", "left":
m.cursor.Shape--
if m.cursor.Shape < tea.CursorBlock {
m.cursor.Shape = tea.CursorBar
}
case "l", "right":
m.cursor.Shape++
if m.cursor.Shape > tea.CursorBar {
m.cursor.Shape = tea.CursorBlock
}
}
}
m.blink = !m.blink
return m, nil
}
func (m model) View() tea.View {
v := tea.NewView("Press left/right to change the cursor style, q or ctrl+c to quit." +
"\n\n" +
" <- This is the cursor (a " + m.describeCursor() + ")")
c := tea.NewCursor(0, 2)
c.Shape = m.cursor.Shape
c.Blink = m.blink
v.Cursor = c
return v
}
func (m model) describeCursor() string {
var adj, noun string
if m.blink {
adj = "blinking"
} else {
adj = "steady"
}
switch m.cursor.Shape {
case tea.CursorBlock:
noun = "block"
case tea.CursorUnderline:
noun = "underline"
case tea.CursorBar:
noun = "bar"
}
return fmt.Sprintf("%s %s", adj, noun)
}
func main() {
p := tea.NewProgram(model{blink: true})
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
os.Exit(1)
}
}
================================================
FILE: examples/debounce/README.md
================================================
# Debounce
<img width="800" src="./debounce.gif" />
================================================
FILE: examples/debounce/main.go
================================================
package main
// This example illustrates how to debounce commands.
//
// When the user presses a key we increment the "tag" value on the model and,
// after a short delay, we include that tag value in the message produced
// by the Tick command.
//
// In a subsequent Update, if the tag in the Msg matches current tag on the
// model's state we know that the debouncing is complete and we can proceed as
// normal. If not, we simply ignore the inbound message.
import (
"fmt"
"os"
"time"
tea "charm.land/bubbletea/v2"
)
const debounceDuration = time.Second
type exitMsg int
type model struct {
tag int
}
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.KeyPressMsg:
// Increment the tag on the model...
m.tag++
return m, tea.Tick(debounceDuration, func(_ time.Time) tea.Msg {
// ...and include a copy of that tag value in the message.
return exitMsg(m.tag)
})
case exitMsg:
// If the tag in the message doesn't match the tag on the model then we
// know that this message was not the last one sent and another is on
// the way. If that's the case we know, we can ignore this message.
// Otherwise, the debounce timeout has passed and this message is a
// valid debounced one.
if int(msg) == m.tag {
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() tea.View {
return tea.NewView(fmt.Sprintf("Key presses: %d", m.tag) +
"\nTo exit press any key, then wait for one second without pressing anything.")
}
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Println("uh oh:", err)
os.Exit(1)
}
}
================================================
FILE: examples/doom-fire/main.go
================================================
package main
import (
"fmt"
"math/rand"
"strings"
"time"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
// This Doom Fire implementation was ported from @const-void's Node version.
// See https://github.com/const-void/DOOM-fire-node
var whiteFg = lipgloss.NewStyle().Foreground(lipgloss.White)
type model struct {
screenBuf []int
width int
height int
firePalette []int
startTime 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.KeyPressMsg:
if msg.String() == "q" || msg.String() == "ctrl+c" {
return m, tea.Quit
}
case tickMsg:
m.spreadFire()
return m, tick
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height * 2 // Double height for half-block characters
m.screenBuf = make([]int, m.width*m.height)
// Initialize the bottom row with white (maximum intensity)
for i := range m.width {
m.screenBuf[(m.height-1)*m.width+i] = len(m.firePalette) - 1
}
}
return m, nil
}
func (m model) View() tea.View {
if m.width == 0 {
return tea.NewView("Initializing...")
}
var s strings.Builder
for y := 0; y < m.height-2; y += 2 {
for x := range m.width {
pixelHi := m.screenBuf[y*m.width+x]
pixelLo := m.screenBuf[(y+1)*m.width+x]
hiColor := m.firePalette[pixelHi]
loColor := m.firePalette[pixelLo]
s.WriteString(lipgloss.NewStyle().
Foreground(lipgloss.ANSIColor(hiColor)).
Background(lipgloss.ANSIColor(loColor)).
Render("▀"))
}
if y < m.height-2 {
s.WriteByte('\n')
}
}
elapsed := time.Since(m.startTime)
s.WriteString(whiteFg.Render("Press q or ctrl+c to quit. " + fmt.Sprintf("Elapsed: %s", elapsed.Round(time.Second))))
v := tea.NewView(s.String())
v.AltScreen = true
return v
}
func (m *model) spreadFire() {
for x := range m.width {
for y := range m.height {
m.spreadPixel(y*m.width + x)
}
}
}
func (m *model) spreadPixel(idx int) {
if idx < m.width {
return
}
pixel := m.screenBuf[idx]
if pixel == 0 {
m.screenBuf[idx-m.width] = 0
return
}
rnd := rand.Intn(3)
dst := idx - rnd + 1
if dst-m.width >= 0 && dst-m.width < len(m.screenBuf) {
decay := rnd & 1
newValue := max(pixel-decay, 0)
m.screenBuf[dst-m.width] = newValue
}
}
type tickMsg time.Time
func tick() tea.Msg {
time.Sleep(time.Millisecond * 50)
return tickMsg(time.Now())
}
func initialModel() model {
// Same color palette as the original
palette := []int{0, 233, 234, 52, 53, 88, 89, 94, 95, 96, 130, 131, 132, 133, 172, 214, 215, 220, 220, 221, 3, 226, 227, 230, 231, 7}
return model{
firePalette: palette,
startTime: time.Now(),
}
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Error running program: %v", err)
}
}
================================================
FILE: examples/exec/README.md
================================================
# Exec
<img width="800" src="./exec.gif" />
================================================
FILE: examples/exec/main.go
================================================
package main
import (
"cmp"
"fmt"
"os"
"os/exec"
tea "charm.land/bubbletea/v2"
)
type editorFinishedMsg struct{ err error }
func openEditor() tea.Cmd {
editor := cmp.Or(os.Getenv("EDITOR"), "vim")
c := exec.Command(editor) //nolint:gosec
return tea.ExecProcess(c, func(err error) tea.Msg {
return editorFinishedMsg{err}
})
}
type model struct {
altscreenActive bool
err error
}
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.KeyPressMsg:
switch msg.String() {
case "a":
m.altscreenActive = !m.altscreenActive
return m, nil
case "e":
return m, openEditor()
case "ctrl+c", "q":
return m, tea.Quit
}
case editorFinishedMsg:
if msg.err != nil {
m.err = msg.err
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() tea.View {
if m.err != nil {
v := tea.NewView("Error: " + m.err.Error() + "\n")
v.AltScreen = m.altscreenActive
return v
}
v := tea.NewView("Press 'e' to open your EDITOR.\nPress 'a' to toggle the altscreen\nPress 'q' to quit.\n")
v.AltScreen = m.altscreenActive
return v
}
func main() {
m := model{}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
================================================
FILE: examples/eyes/main.go
================================================
// roughly converted to Go from https://github.com/dmtrKovalenko/esp32-smooth-eye-blinking/blob/main/src/main.cpp
package main
import (
"fmt"
"math"
"math/rand"
"strings"
"time"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
const (
// Eye dimensions (corresponding to original EYE_WIDTH and EYE_HEIGHT)
eyeWidth = 15
eyeHeight = 12 // Increased height for taller eyes
eyeSpacing = 40
// Blink animation timing (matching original constants)
blinkFrames = 20
openTimeMin = 1000
openTimeMax = 4000
)
// Characters for drawing the eyes
const (
eyeChar = "●"
bgChar = " "
)
type model struct {
width int
height int
eyePositions [2]int
eyeY int
isBlinking bool
blinkState int
lastBlink time.Time
openTime time.Duration
}
type tickMsg time.Time
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Error running program: %v\n", err)
}
}
func initialModel() model {
m := model{
width: 80,
height: 24,
isBlinking: false,
blinkState: 0,
lastBlink: time.Now(),
openTime: time.Duration(rand.Intn(openTimeMax-openTimeMin)+openTimeMin) * time.Millisecond,
}
m.updateEyePositions()
return m
}
func (m *model) updateEyePositions() {
startX := (m.width - eyeSpacing) / 2
m.eyeY = m.height / 2
m.eyePositions[0] = startX
m.eyePositions[1] = startX + eyeSpacing
}
func (m model) Init() tea.Cmd {
return tickCmd()
}
func tickCmd() tea.Cmd {
return tea.Tick(50*time.Millisecond, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
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
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.updateEyePositions()
case tickMsg:
currentTime := time.Now()
if !m.isBlinking && currentTime.Sub(m.lastBlink) >= m.openTime {
m.isBlinking = true
m.blinkState = 0
}
if m.isBlinking {
m.blinkState++
if m.blinkState >= blinkFrames {
m.isBlinking = false
m.lastBlink = currentTime
m.openTime = time.Duration(rand.Intn(openTimeMax-openTimeMin)+openTimeMin) * time.Millisecond
// 10% chance of double blink (matching original logic)
if rand.Intn(10) == 0 {
m.openTime = 300 * time.Millisecond
}
}
}
}
return m, tickCmd()
}
func (m model) View() tea.View {
var v tea.View
v.AltScreen = true // Use alternate screen buffer
// Create empty canvas
canvas := make([][]string, m.height)
for y := range canvas {
canvas[y] = make([]string, m.width)
for x := range canvas[y] {
canvas[y][x] = bgChar
}
}
// Calculate current eye height based on blink state
currentHeight := eyeHeight
if m.isBlinking {
var blinkProgress float64
if m.blinkState < blinkFrames/2 {
// Closing eyes (with easing function from original)
blinkProgress = float64(m.blinkState) / float64(blinkFrames/2)
blinkProgress = 1.0 - (blinkProgress * blinkProgress)
} else {
// Opening eyes (with easing function from original)
blinkProgress = float64(m.blinkState-blinkFrames/2) / float64(blinkFrames/2)
blinkProgress = blinkProgress * (2.0 - blinkProgress)
}
currentHeight = int(math.Max(1, float64(eyeHeight)*blinkProgress))
}
// Draw both eyes
for i := 0; i < 2; i++ {
drawEllipse(canvas, m.eyePositions[i], m.eyeY, eyeWidth, currentHeight)
}
// Convert canvas to string
var s strings.Builder
for _, row := range canvas {
for _, cell := range row {
s.WriteString(cell)
}
s.WriteString("\n")
}
// Style output
style := lipgloss.NewStyle().
Foreground(lipgloss.Color("#F0F0F0"))
v.SetContent(style.Render(s.String()))
return v
}
func drawEllipse(canvas [][]string, x0, y0, rx, ry int) {
// Improved ellipse drawing algorithm with better angles
for y := -ry; y <= ry; y++ {
// Calculate the width at this y position for a smoother ellipse
// Use a slightly modified formula to improve the angles
width := int(float64(rx) * math.Sqrt(1.0-math.Pow(float64(y)/float64(ry), 2.0)))
for x := -width; x <= width; x++ {
// Calculate canvas position
canvasX := x0 + x
canvasY := y0 + y
// Make sure we're within canvas bounds
if canvasX >= 0 && canvasX < len(canvas[0]) && canvasY >= 0 && canvasY < len(canvas) {
canvas[canvasY][canvasX] = eyeChar
}
}
}
}
================================================
FILE: examples/file-picker/main.go
================================================
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"charm.land/bubbles/v2/filepicker"
tea "charm.land/bubbletea/v2"
)
type model struct {
filepicker filepicker.Model
selectedFile string
quitting bool
err error
}
type clearErrorMsg struct{}
func clearErrorAfter(t time.Duration) tea.Cmd {
return tea.Tick(t, func(_ time.Time) tea.Msg {
return clearErrorMsg{}
})
}
func (m model) Init() tea.Cmd {
return m.filepicker.Init()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "q":
m.quitting = true
return m, tea.Quit
}
case clearErrorMsg:
m.err = nil
}
var cmd tea.Cmd
m.filepicker, cmd = m.filepicker.Update(msg)
// Did the user select a file?
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
// Get the path of the selected file.
m.selectedFile = path
}
// Did the user select a disabled file?
// This is only necessary to display an error to the user.
if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect {
// Let's clear the selectedFile and display an error.
m.err = errors.New(path + " is not valid.")
m.selectedFile = ""
return m, tea.Batch(cmd, clearErrorAfter(2*time.Second))
}
return m, cmd
}
func (m model) View() tea.View {
if m.quitting {
return tea.NewView("")
}
var s strings.Builder
s.WriteString("\n ")
if m.err != nil {
s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error()))
} else if m.selectedFile == "" {
s.WriteString("Pick a file:")
} else {
s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile))
}
s.WriteString("\n\n" + m.filepicker.View() + "\n")
v := tea.NewView(s.String())
v.AltScreen = true
return v
}
func main() {
fp := filepicker.New()
fp.AllowedTypes = []string{".mod", ".sum", ".go", ".txt", ".md"}
fp.CurrentDirectory, _ = os.UserHomeDir()
m := model{filepicker: fp}
tm, _ := tea.NewProgram(m).Run()
mm := tm.(model)
fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n")
}
================================================
FILE: examples/focus-blur/main.go
================================================
package main
// A simple program that handled losing and acquiring focus.
import (
"log"
tea "charm.land/bubbletea/v2"
)
func main() {
p := tea.NewProgram(model{
focused: true,
reporting: true,
})
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
type model struct {
focused bool
reporting 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.FocusMsg:
m.focused = true
case tea.BlurMsg:
m.focused = false
case tea.KeyPressMsg:
switch msg.String() {
case "t":
m.reporting = !m.reporting
case "ctrl+c", "q":
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() tea.View {
s := "Hi. Focus report is currently "
if m.reporting {
s += "enabled"
} else {
s += "disabled"
}
s += ".\n\n"
if m.reporting {
if m.focused {
s += "This program is currently focused!"
} else {
s += "This program is currently blurred!"
}
}
v := tea.NewView(s + "\n\nTo quit sooner press ctrl-c, or t to toggle focus reporting...\n")
v.ReportFocus = m.reporting
return v
}
================================================
FILE: examples/fullscreen/README.md
================================================
# Full Screen
<img width="800" src="./fullscreen.gif" />
================================================
FILE: examples/fullscreen/main.go
================================================
package main
// A simple program that opens the alternate screen buffer then counts down
// from 5 and then exits.
import (
"fmt"
"log"
"time"
tea "charm.land/bubbletea/v2"
)
type model int
type tickMsg time.Time
func main() {
p := tea.NewProgram(model(5))
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
func (m model) Init() tea.Cmd {
return tick()
}
func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
switch msg := message.(type) {
case tea.KeyPressMsg:
switch msg.String() {
case "q", "esc", "ctrl+c":
return m, tea.Quit
}
case tickMsg:
m--
if m <= 0 {
return m, tea.Quit
}
return m, tick()
}
return m, nil
}
func (m model) View() tea.View {
v := tea.NewView(fmt.Sprintf("\n\n Hi. This program will exit in %d seconds...", m))
v.AltScreen = true
return v
}
func tick() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
================================================
FILE: examples/glamour/README.md
================================================
# Glamour
<img width="800" src="./glamour.gif" />
================================================
FILE: examples/glamour/main.go
================================================
package main
import (
"fmt"
"os"
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/glamour/v2"
"github.com/charmbracelet/glamour/v2/styles"
"charm.land/lipgloss/v2"
)
const content = `
# Today’s Menu
## Appetizers
| 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(isDark bool) (*example, error) {
const (
width = 78
height = 20
)
vp := viewport.New()
vp.SetWidth(width)
vp.SetHeight(height)
vp.Style = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("62")).
PaddingRight(2)
// We need to adjust the width of the glamour render from our main width
// to account for a few things:
//
// * The viewport border width
// * The viewport padding
// * The viewport margins
// * The gutter glamour applies to the left side of the content
//
const glamourGutter = 3
glamourRenderWidth := width - vp.Style.GetHorizontalFrameSize() - glamourGutter
style := styles.DarkStyleConfig
if !isDark {
style = styles.LightStyleConfig
}
renderer, err := glamour.NewTermRenderer(
glamour.WithStyles(style),
glamour.WithWordWrap(glamourRenderWidth),
)
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.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return e, tea.Quit
default:
var cmd tea.Cmd
e.viewport, cmd = e.viewport.Update(msg)
return e, cmd
}
default:
return e, nil
}
}
func (e example) View() tea.View {
return tea.NewView(e.viewport.View() + e.helpView())
}
func (e example) helpView() string {
return helpStyle("\n ↑/↓: Navigate • q: Quit\n")
}
func main() {
hasDarkBg := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
model, err := newExample(hasDarkBg)
if err != nil {
fmt.Println("Could not initialize Bubble Tea model:", err)
os.Exit(1)
}
if _, err := tea.NewProgram(model).Run(); err != nil {
fmt.Println("Bummer, there's been an error:", err)
os.Exit(1)
}
}
================================================
FILE: examples/go.mod
================================================
module examples
go 1.25.2
replace charm.land/bubbletea/v2 => ../
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.2
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930
github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250602192518-9e722df69bbb
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20
github.com/segmentio/ksuid v1.0.4
)
require (
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.24.0 // indirect
)
================================================
FILE: examples/go.sum
================================================
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
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/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930 h1:+47Z2jVAWPSLGjPRbfZizW3OpcAYsu7EUk2DR+66FyM=
github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930/go.mod h1:izs11tnkYaT3DTEH2E0V/lCb18VGZ7k9HLYEGuvgXGA=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250602192518-9e722df69bbb h1:oTM8tZxV7FY0ehvYjFuICouuhzE08UZYNqUIp/lDQdY=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250602192518-9e722df69bbb/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
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.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
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.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
================================================
FILE: examples/help/README.md
================================================
# Help
<img width="800" src="./help.gif" />
================================================
FILE: examples/help/main.go
================================================
package main
import (
"fmt"
"os"
"strings"
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
// 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.New(),
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 gracefully truncate
// its view as needed.
m.help.SetWidth(msg.Width)
case tea.KeyPressMsg:
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() tea.View {
if m.quitting {
return tea.NewView("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 tea.NewView(status + strings.Repeat("\n", height) + helpView)
}
func main() {
if os.Getenv("HELP_DEBUG") != "" {
f, err := tea.LogToFile("debug.log", "help")
if err != nil {
fmt.Println("Couldn't open a file for logging:", err)
os.Exit(1)
}
defer f.Close() // nolint:errcheck
}
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Printf("Could not start program :(\n%v\n", err)
os.Exit(1)
}
}
================================================
FILE: examples/http/README.md
================================================
# HTTP
<img width="800" src="./http.gif" />
================================================
FILE: examples/http/main.go
================================================
package main
// A simple program that makes a GET request and prints the response status.
import (
"fmt"
"log"
"net/http"
"time"
tea "charm.land/bubbletea/v2"
)
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.Run(); 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.KeyPressMsg:
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() tea.View {
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 tea.NewView(s + "\n")
}
func checkServer() tea.Msg {
c := &http.Client{
Timeout: 10 * time.Second,
}
res, err := c.Get(url)
if err != nil {
return errMsg{err}
}
defer res.Body.Close() // nolint:errcheck
return statusMsg(res.StatusCode)
}
================================================
FILE: examples/isbn-form/isbn-form.tape
================================================
Type "go run ./isbn-form"
Enter
Sleep 2s
Type "9783548372570"
Sleep 500ms
Sleep 1s
Down
Sleep 500ms
Type "my book title"
Sleep 500ms
Backspace 10
Type "title"
Sleep 500ms
Enter
Sleep 3s
Output isbn-form.gif
================================================
FILE: examples/isbn-form/main.go
================================================
package main
import (
"fmt"
"log"
"strings"
"unicode"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/exp/charmtone"
)
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
type (
errMsg error
)
var (
inputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(charmtone.Tang.Hex()))
continueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(charmtone.Anchovy.Hex()))
validStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(charmtone.Guac.Hex()))
errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(charmtone.Cherry.Hex()))
)
type model struct {
isbnInput textinput.Model
titleInput textinput.Model
focusedInput int
err error
}
// canFindBook returns whether the find button is to be pressed
func (m model) canFindBook() bool {
correctIsbnGiven := m.isbnInput.Err == nil && len(m.isbnInput.Value()) != 0
correctTitleGiven := m.titleInput.Err == nil && len(m.titleInput.Value()) != 0
return correctIsbnGiven && correctTitleGiven
}
// Validator function to ensure valid input
func isbn13Validator(s string) error {
// A valid ISBN looks like this:
// 978-3-548-37257-0 or
// 9783548372570 without any spaces
// Remove dashes
s = strings.ReplaceAll(s, "-", "")
if len(s) != 13 {
return fmt.Errorf("ISBN is of wrong length")
}
for _, c := range s {
if !unicode.IsDigit(c) {
return fmt.Errorf("ISBN contains invalid characters")
}
}
gs1Prefix := s[:3]
switch gs1Prefix {
case "978", "979":
break
default:
return fmt.Errorf("ISBN has invalid GS1 prefix")
}
// The last digit, the check digit,
// must make the checksum a multiple of 10.
// All digits are added up after being multiplied
// by either 1 or 3 alternately.
// So 9x1 + 7x3 + 8x1 + ... + 0 must be a multiple of 10.
sum := 0
for i, c := range s {
// Convert rune to int
n := int(c - '0')
// Multiply the uneven indices by 3
if i%2 != 0 {
n *= 3
}
sum += n
}
if sum%10 != 0 {
return fmt.Errorf("ISBN has invalid check digit")
}
return nil
}
var bannedTitleWords = []string{
"very",
"bad",
"words",
"that",
"should",
"not",
"appear",
"in",
"book",
"titles",
}
func bookTitleValidator(s string) error {
s = strings.TrimSpace(s)
if len(s) == 0 {
return fmt.Errorf("Book title is empty")
}
for _, bannedWord := range bannedTitleWords {
if strings.Contains(s, bannedWord) {
return fmt.Errorf("Book title contains banned word %q", bannedWord)
}
}
return nil
}
func initialModel() model {
isbnInput := textinput.New()
isbnInput.Focus()
isbnInput.Placeholder = "978-X-XXX-XXXXX-X"
isbnInput.CharLimit = 17
isbnInput.SetWidth(30)
isbnInput.Prompt = ""
isbnInput.Validate = isbn13Validator
titleInput := textinput.New()
titleInput.Blur()
titleInput.Placeholder = "Title"
titleInput.CharLimit = 100
titleInput.SetWidth(100)
titleInput.Prompt = ""
titleInput.Validate = bookTitleValidator
return model{
isbnInput: isbnInput,
titleInput: titleInput,
focusedInput: 0,
err: nil,
}
}
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.KeyPressMsg:
switch msg.String() {
case "up", "down":
// Switch between text inputs
switch m.focusedInput {
case 0:
m.focusedInput = 1
m.titleInput.Focus()
m.isbnInput.Blur()
case 1:
m.focusedInput = 0
m.isbnInput.Focus()
m.titleInput.Blur()
}
case "enter":
// Enter is blocked until all inputs are ok
if m.canFindBook() {
return m, tea.Quit
}
case "ctrl+c", "esc":
return m, tea.Quit
}
// We handle errors just like any other message
case errMsg:
m.err = msg
return m, nil
}
var isbnCommand tea.Cmd
m.isbnInput, isbnCommand = m.isbnInput.Update(msg)
var titleCommand tea.Cmd
m.titleInput, titleCommand = m.titleInput.Update(msg)
return m, tea.Batch(isbnCommand, titleCommand)
}
func (m model) View() tea.View {
var continueText string
if m.canFindBook() {
continueText = continueStyle.Render("Find ->")
}
var isbnErrorText string
if m.isbnInput.Value() != "" {
if m.isbnInput.Err != nil {
isbnErrorText = errStyle.Render(m.isbnInput.Err.Error())
} else {
isbnErrorText = validStyle.Render("Valid ISBN")
}
}
var titleErrorText string
if m.titleInput.Value() != "" {
if m.titleInput.Err != nil {
titleErrorText = errStyle.Render(m.titleInput.Err.Error())
} else {
titleErrorText = validStyle.Render("Valid title")
}
}
return tea.NewView(fmt.Sprintf(
` Search book:
%s
%s
%s
%s
%s
%s
%s
`,
inputStyle.Width(30).Render("ISBN"),
m.isbnInput.View(),
isbnErrorText,
inputStyle.Width(30).Render("Title"),
m.titleInput.View(),
titleErrorText,
continueText,
) + "\n")
}
================================================
FILE: examples/keyboard-enhancements/main.go
================================================
package main
// This is a simple example illustrating how to enable enhanced keyboard
// support.
import (
"fmt"
"os"
"strings"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
type styles struct {
ui lipgloss.Style
}
type model struct {
supportsDisambiguation bool
supportsEventTypes bool
styles styles
}
func (m model) Init() tea.Cmd {
return tea.RequestBackgroundColor
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// Bubble Tea will send a [tea.KeyboardEnhancementsMsg] on startup if the
// terminal supports keyboard enhancements features.
//
// These features extend the capabilities of keyboard input beyond the basic legacy
// support found in most terminals. This includes features like:
// - Key disambiguation: Improved ability to distinguish between certain key presses
// like "enter" and "shift+enter" or "tab" and "ctrl+i".
// - Key event types: The ability to report different types of key events such as
// key presses and key releases.
//
// This allows for more nuanced input handling in terminal applications.
// You can ask Bubble Tea to request additional keyboard enhancements
// features by setting fields on the [tea.View.KeyboardEnhancements] struct
// in your [tea.View] method.
case tea.KeyboardEnhancementsMsg:
// Check which features were able to be enabled.
m.supportsDisambiguation = true // This is always enabled when this msg is received.
m.supportsEventTypes = msg.SupportsEventTypes()
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
default:
return m, tea.Println(" press: " + msg.String())
}
case tea.KeyReleaseMsg:
return m, tea.Printf("release: %s", msg.String())
case tea.BackgroundColorMsg:
// Initialize styles.
m.updateStyles(msg.IsDark())
}
return m, nil
}
func (m model) View() tea.View {
var v tea.View
var b strings.Builder
fmt.Fprintf(&b, "Terminal supports key releases: %v\n", m.supportsEventTypes)
fmt.Fprintf(&b, "Terminal supports key disambiguation: %v\n", m.supportsDisambiguation)
fmt.Fprint(&b, "This demo logs key events. Press ctrl+c to quit.")
v.SetContent(b.String() + "\n")
// Attempt to enable reporting key event types (key presses and key
// releases). By default, only key disambiguation is enabled which improves
// the ability to distinguish between certain key presses like "enter" and
// "shift+enter" or "tab" and "ctrl+i".
v.KeyboardEnhancements.ReportEventTypes = true
return v
}
func (m *model) updateStyles(isDark bool) {
// Initialize styles.
lightDark := lipgloss.LightDark(isDark)
grey := lightDark(lipgloss.Color("239"), lipgloss.Color("245"))
darkGray := lightDark(lipgloss.Color("245"), lipgloss.Color("239"))
m.styles.ui = lipgloss.NewStyle().
Foreground(grey).
Border(lipgloss.NormalBorder(), true, false, false, false).
BorderForeground(darkGray)
}
func initialModel() model {
m := model{}
m.updateStyles(true) // default to dark styles.
return m
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Urgh: %v\n", err)
os.Exit(1)
}
}
================================================
FILE: examples/list-default/README.md
================================================
# Default List
<img width="800" src="./list-default.gif" />
================================================
FILE: examples/list-default/main.go
================================================
package main
import (
"fmt"
"os"
"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
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.KeyPressMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
h, v := docStyle.GetFrameSize()
m.list.SetSize(msg.Width-h, msg.Height-v)
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
v := tea.NewView(docStyle.Render(m.list.View()))
v.AltScreen = true
return v
}
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.New(items, list.NewDefaultDelegate(), 0, 0)}
m.list.Title = "My Fave Things"
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
================================================
FILE: examples/list-fancy/README.md
================================================
# Fancy List
<img width="800" src="./list-fancy.gif" />
================================================
FILE: examples/list-fancy/delegate.go
================================================
package main
import (
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
)
func newItemDelegate(keys *delegateKeyMap, styles *styles) 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.KeyPressMsg:
switch {
case key.Matches(msg, keys.choose):
return m.NewStatusMessage(styles.statusMessage.Render("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(styles.statusMessage.Render("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"),
),
}
}
================================================
FILE: examples/list-fancy/main.go
================================================
package main
import (
"fmt"
"os"
"sync"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
type styles struct {
app lipgloss.Style
title lipgloss.Style
statusMessage lipgloss.Style
}
func newStyles(darkBG bool) styles {
lightDark := lipgloss.LightDark(darkBG)
return styles{
app: lipgloss.NewStyle().
Padding(1, 2),
title: lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFDF5")).
Background(lipgloss.Color("#25A065")).
Padding(0, 1),
statusMessage: lipgloss.NewStyle().
Foreground(lightDark(lipgloss.Color("#04B575"), lipgloss.Color("#04B575"))),
}
}
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 {
styles styles
darkBG bool
width, height int
once *sync.Once
list list.Model
itemGenerator *randomItemGenerator
keys *listKeyMap
delegateKeys *delegateKeyMap
}
func (m model) Init() tea.Cmd {
return tea.Batch(
tea.RequestBackgroundColor,
)
}
func (m *model) updateListProperties() {
// Update list size.
h, v := m.styles.app.GetFrameSize()
m.list.SetSize(m.width-h, m.height-v)
// Update the model and list styles.
m.styles = newStyles(m.darkBG)
m.list.Styles.Title = m.styles.title
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.BackgroundColorMsg:
m.darkBG = msg.IsDark()
m.updateListProperties()
return m, nil
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
m.updateListProperties()
return m, nil
}
switch msg := msg.(type) {
case tea.KeyPressMsg:
// 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(m.styles.statusMessage.Render("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() tea.View {
v := tea.NewView(m.styles.app.Render(m.list.View()))
v.AltScreen = true
return v
}
func initialModel() model {
// Initialize the model and list.
m := model{}
m.styles = newStyles(false) // default to dark background styles
delegateKeys := newDelegateKeyMap()
listKeys := newListKeyMap()
// Make initial list of items.
var itemGenerator randomItemGenerator
const numItems = 24
items := make([]list.Item, numItems)
for i := range numItems {
items[i] = itemGenerator.next()
}
// Setup list.
delegate := newItemDelegate(delegateKeys, &m.styles)
groceryList := list.New(items, delegate, 0, 0)
groceryList.Title = "Groceries"
groceryList.Styles.Title = m.styles.title
groceryList.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
listKeys.toggleSpinner,
listKeys.insertItem,
listKeys.toggleTitleBar,
listKeys.toggleStatusBar,
listKeys.togglePagination,
listKeys.toggleHelpMenu,
}
}
m.list = groceryList
m.keys = listKeys
m.delegateKeys = delegateKeys
m.itemGenerator = &itemGenerator
return m
}
func main() {
if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
================================================
FILE: examples/list-fancy/randomitems.go
================================================
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
}
================================================
FILE: examples/list-simple/README.md
================================================
# Simple List
<img width="800" src="./list-simple.gif" />
================================================
FILE: examples/list-simple/main.go
================================================
package main
import (
"fmt"
"io"
"os"
"strings"
"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
const listHeight = 14
type styles struct {
title lipgloss.Style
item lipgloss.Style
selectedItem lipgloss.Style
pagination lipgloss.Style
help lipgloss.Style
quitText lipgloss.Style
}
func newStyles(darkBG bool) styles {
var s styles
s.title = lipgloss.NewStyle().MarginLeft(2)
s.item = lipgloss.NewStyle().PaddingLeft(4)
s.selectedItem = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
s.pagination = list.DefaultStyles(darkBG).PaginationStyle.PaddingLeft(4)
s.help = list.DefaultStyles(darkBG).HelpStyle.PaddingLeft(4).PaddingBottom(1)
s.quitText = lipgloss.NewStyle().Margin(1, 0, 2, 4)
return s
}
type item string
func (i item) FilterValue() string { return "" }
type itemDelegate struct {
styles *styles
}
func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 0 }
func (d itemDelegate) Update(_ tea.Msg, _ *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 := d.styles.item.Render
if index == m.Index() {
fn = func(s ...string) string {
return d.styles.selectedItem.Render("> " + strings.Join(s, " "))
}
}
fmt.Fprint(w, fn(str))
}
type model struct {
list list.Model
choice string
styles styles
quitting bool
}
func initialModel() model {
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.New(items, itemDelegate{}, defaultWidth, listHeight)
l.Title = "What do you want for dinner?"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
m := model{list: l}
m.updateStyles(true) // default to dark styles.
return m
}
func (m *model) updateStyles(isDark bool) {
m.styles = newStyles(isDark)
m.list.Styles.Title = m.styles.title
m.list.Styles.PaginationStyle = m.styles.pagination
m.list.Styles.HelpStyle = m.styles.help
m.list.SetDelegate(itemDelegate{styles: &m.styles})
}
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.KeyPressMsg:
switch keypress := msg.String(); keypress {
case "q", "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
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
if m.choice != "" {
return tea.NewView(m.styles.quitText.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice)))
}
if m.quitting {
return tea.NewView(m.styles.quitText.Render("Not hungry? That’s cool."))
}
return tea.NewView("\n" + m.list.View())
}
func main() {
if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
================================================
FILE: examples/mouse/main.go
================================================
package main
// A simple program that opens the alternate screen buffer and displays mouse
// coordinates and events.
import (
"log"
tea "charm.land/bubbletea/v2"
)
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
type model 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.KeyPressMsg:
if s := msg.String(); s == "ctrl+c" || s == "q" || s == "esc" {
return m, tea.Quit
}
case tea.MouseMsg:
mouse := msg.Mouse()
return m, tea.Printf("(X: %d, Y: %d) %s", mouse.X, mouse.Y, mouse)
}
return m, nil
}
func (m model) View() tea.View {
v := tea.NewView("Do mouse stuff. When you're done press q to quit.\n")
v.MouseMode = tea.MouseModeAllMotion
return v
}
================================================
FILE: examples/package-manager/README.md
================================================
# Package Manager
<img width="800" src="./package-manager.gif" />
================================================
FILE: examples/package-manager/main.go
================================================
package main
import (
"fmt"
"math/rand"
"os"
"strings"
"time"
"charm.land/bubbles/v2/progress"
"charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
type model struct {
packages []string
index int
width int
height int
spinner spinner.Model
progress progress.Model
done bool
}
var (
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
doneStyle = lipgloss.NewStyle().Margin(1, 2)
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
)
func newModel() model {
p := progress.New(
progress.WithDefaultBlend(),
progress.WithWidth(40),
progress.WithoutPercentage(),
)
s := spinner.New()
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
return model{
packages: getPackages(),
spinner: s,
progress: p,
}
}
func (m model) Init() tea.Cmd {
return tea.Batch(downloadAndInstall(m.packages[m.index]), m.spinner.Tick)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
return m, tea.Quit
}
case installedPkgMsg:
pkg := m.packages[m.index]
if m.index >= len(m.packages)-1 {
// Everything's been installed. We're done!
m.done = true
return m, tea.Sequence(
tea.Printf("%s %s", checkMark, pkg), // print the last success message
tea.Quit, // exit the program
)
}
// Update progress bar
m.index++
progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages)))
return m, tea.Batch(
progressCmd,
tea.Printf("%s %s", checkMark, pkg), // print success message above our program
downloadAndInstall(m.packages[m.index]), // download the next package
)
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case progress.FrameMsg:
var cmd tea.Cmd
m.progress, cmd = m.progress.Update(msg)
return m, cmd
}
return m, nil
}
func (m model) View() tea.View {
n := len(m.packages)
w := lipgloss.Width(fmt.Sprintf("%d", n))
if m.done {
return tea.NewView(doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n)))
}
pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n)
spin := m.spinner.View() + " "
prog := m.progress.View()
cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount))
pkgName := currentPkgNameStyle.Render(m.packages[m.index])
info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName)
cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount))
gap := strings.Repeat(" ", cellsRemaining)
return tea.NewView(spin + info + gap + prog + pkgCount)
}
type installedPkgMsg string
func downloadAndInstall(pkg string) tea.Cmd {
// This is where you'd do i/o stuff to download and install packages. In
// our case we're just pausing for a moment to simulate the process.
d := time.Millisecond * time.Duration(rand.Intn(500)) //nolint:gosec
return tea.Tick(d, func(t time.Time) tea.Msg {
return installedPkgMsg(pkg)
})
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
================================================
FILE: examples/package-manager/packages.go
================================================
package main
import (
"fmt"
"math/rand"
)
var packages = []string{
"vegeutils",
"libgardening",
"currykit",
"spicerack",
"fullenglish",
"eggy",
"bad-kitty",
"chai",
"hojicha",
"libtacos",
"babys-monads",
"libpurring",
"currywurst-devel",
"xmodmeow",
"licorice-utils",
"cashew-apple",
"rock-lobster",
"standmixer",
"coffee-CUPS",
"libesszet",
"zeichenorientierte-benutzerschnittstellen",
"schnurrkit",
"old-socks-devel",
"jalapeño",
"molasses-utils",
"xkohlrabi",
"party-gherkin",
"snow-peas",
"libyuzu",
}
func getPackages() []string {
pkgs := packages
copy(pkgs, packages)
rand.Shuffle(len(pkgs), func(i, j int) {
pkgs[i], pkgs[j] = pkgs[j], pkgs[i]
})
for k := range pkgs {
pkgs[k] += fmt.Sprintf("-%d.%d.%d", rand.Intn(10), rand.Intn(10), rand.Intn(10)) //nolint:gosec
}
return pkgs
}
================================================
FILE: examples/pager/README.md
================================================
# Pager
<img width="800" src="./pager.gif" />
================================================
FILE: examples/pager/artichoke.md
================================================
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 to 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.
================================================
FILE: examples/pager/main.go
================================================
package main
// An example program demonstrating the pager component from the Bubbles
// component library.
import (
"fmt"
"os"
"regexp"
"strings"
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.BorderStyle(b)
}()
)
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.KeyPressMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := 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.New(viewport.WithWidth(msg.Width), viewport.WithHeight(msg.Height-verticalMarginHeight))
m.viewport.YPosition = headerHeight
m.viewport.LeftGutterFunc = func(info viewport.GutterContext) string {
if info.Soft {
return " │ "
}
if info.Index >= info.TotalLines {
return " ~ │ "
}
return fmt.Sprintf("%4d │ ", info.Index+1)
}
m.viewport.HighlightStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("238")).Background(lipgloss.Color("34"))
m.viewport.SelectedHighlightStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("238")).Background(lipgloss.Color("47"))
m.viewport.SetContent(m.content)
m.viewport.SetHighlights(regexp.MustCompile("artichoke").FindAllStringIndex(m.content, -1))
m.viewport.HighlightNext()
m.ready = true
} else {
m.viewport.SetWidth(msg.Width)
m.viewport.SetHeight(msg.Height - verticalMarginHeight)
}
}
// Handle keyboard and mouse events in the viewport
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) View() tea.View {
var v tea.View
v.AltScreen = true // use the full size of the terminal in its "alternate screen buffer"
v.MouseMode = tea.MouseModeCellMotion // turn on mouse support so we can track the mouse wheel
if !m.ready {
v.SetContent("\n Initializing...")
} else {
v.SetContent(fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()))
}
return v
}
func (m model) headerView() string {
title := titleStyle.Render("Mr. Pager")
line := strings.Repeat("─", max(0, m.viewport.Width()-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (m model) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%:%3.f%%", m.viewport.ScrollPercent()*100, m.viewport.HorizontalScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width()-lipgloss.Width(info)))
re
gitextract_mfu3e63q/ ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── build.yml │ ├── coverage.yml │ ├── dependabot-sync.yml │ ├── examples.yml │ ├── lint-sync.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── Taskfile.yaml ├── UPGRADE_GUIDE_V2.md ├── clipboard.go ├── color.go ├── commands.go ├── commands_test.go ├── cursed_renderer.go ├── cursor.go ├── environ.go ├── examples/ │ ├── README.md │ ├── altscreen-toggle/ │ │ ├── README.md │ │ └── main.go │ ├── autocomplete/ │ │ └── main.go │ ├── canvas/ │ │ └── main.go │ ├── capability/ │ │ └── main.go │ ├── cellbuffer/ │ │ └── main.go │ ├── chat/ │ │ ├── README.md │ │ └── main.go │ ├── clickable/ │ │ ├── main.go │ │ └── words.go │ ├── colorprofile/ │ │ └── main.go │ ├── composable-views/ │ │ ├── README.md │ │ └── main.go │ ├── cursor-style/ │ │ └── main.go │ ├── debounce/ │ │ ├── README.md │ │ └── main.go │ ├── doom-fire/ │ │ └── main.go │ ├── exec/ │ │ ├── README.md │ │ └── main.go │ ├── eyes/ │ │ └── main.go │ ├── file-picker/ │ │ └── main.go │ ├── focus-blur/ │ │ └── main.go │ ├── fullscreen/ │ │ ├── README.md │ │ └── main.go │ ├── glamour/ │ │ ├── README.md │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── help/ │ │ ├── README.md │ │ └── main.go │ ├── http/ │ │ ├── README.md │ │ └── main.go │ ├── isbn-form/ │ │ ├── isbn-form.tape │ │ └── main.go │ ├── keyboard-enhancements/ │ │ └── main.go │ ├── list-default/ │ │ ├── README.md │ │ └── main.go │ ├── list-fancy/ │ │ ├── README.md │ │ ├── delegate.go │ │ ├── main.go │ │ └── randomitems.go │ ├── list-simple/ │ │ ├── README.md │ │ └── main.go │ ├── mouse/ │ │ └── main.go │ ├── package-manager/ │ │ ├── README.md │ │ ├── main.go │ │ └── packages.go │ ├── pager/ │ │ ├── README.md │ │ ├── artichoke.md │ │ └── main.go │ ├── paginator/ │ │ ├── README.md │ │ └── main.go │ ├── pipe/ │ │ ├── README.md │ │ └── main.go │ ├── prevent-quit/ │ │ └── main.go │ ├── print-key/ │ │ └── main.go │ ├── progress-animated/ │ │ ├── README.md │ │ └── main.go │ ├── progress-bar/ │ │ └── main.go │ ├── progress-download/ │ │ ├── README.md │ │ ├── main.go │ │ └── tui.go │ ├── progress-static/ │ │ ├── README.md │ │ └── main.go │ ├── query-term/ │ │ └── main.go │ ├── realtime/ │ │ ├── README.md │ │ └── main.go │ ├── result/ │ │ ├── README.md │ │ └── main.go │ ├── send-msg/ │ │ ├── README.md │ │ └── main.go │ ├── sequence/ │ │ ├── README.md │ │ └── main.go │ ├── set-terminal-color/ │ │ └── main.go │ ├── set-window-title/ │ │ └── main.go │ ├── simple/ │ │ ├── README.md │ │ ├── main.go │ │ ├── main_test.go │ │ └── testdata/ │ │ └── TestApp.golden │ ├── space/ │ │ └── main.go │ ├── spinner/ │ │ ├── README.md │ │ └── main.go │ ├── spinners/ │ │ ├── README.md │ │ └── main.go │ ├── splash/ │ │ └── main.go │ ├── split-editors/ │ │ ├── README.md │ │ └── main.go │ ├── stopwatch/ │ │ ├── README.md │ │ └── main.go │ ├── suspend/ │ │ └── main.go │ ├── table/ │ │ ├── README.md │ │ └── main.go │ ├── table-resize/ │ │ └── main.go │ ├── tabs/ │ │ ├── README.md │ │ └── main.go │ ├── textarea/ │ │ ├── README.md │ │ └── main.go │ ├── textinput/ │ │ ├── README.md │ │ └── main.go │ ├── textinputs/ │ │ ├── README.md │ │ └── main.go │ ├── timer/ │ │ ├── README.md │ │ └── main.go │ ├── tui-daemon-combo/ │ │ ├── README.md │ │ └── main.go │ ├── vanish/ │ │ └── main.go │ ├── views/ │ │ ├── README.md │ │ └── main.go │ └── window-size/ │ └── main.go ├── exec.go ├── exec_test.go ├── focus.go ├── go.mod ├── go.sum ├── input.go ├── key.go ├── keyboard.go ├── logging.go ├── logging_test.go ├── mod.go ├── mouse.go ├── nil_renderer.go ├── options.go ├── options_test.go ├── paste.go ├── profile.go ├── raw.go ├── renderer.go ├── screen.go ├── screen_test.go ├── signals_unix.go ├── signals_windows.go ├── tea.go ├── tea_test.go ├── termcap.go ├── termios_bsd.go ├── termios_other.go ├── termios_unix.go ├── termios_windows.go ├── testdata/ │ ├── TestClearMsg/ │ │ ├── bg_fg_cur_color.golden │ │ ├── clear_screen.golden │ │ └── read_set_clipboard.golden │ └── TestViewModel/ │ ├── altscreen.golden │ ├── altscreen_autoexit.golden │ ├── bg_set_color.golden │ ├── bp_stop_start.golden │ ├── cursor_hide.golden │ ├── cursor_hideshow.golden │ ├── kitty_stop_startreleases.golden │ ├── mouse_allmotion.golden │ ├── mouse_cellmotion.golden │ └── mouse_disable.golden ├── tty.go ├── tty_unix.go ├── tty_windows.go ├── tutorials/ │ ├── basics/ │ │ ├── README.md │ │ └── main.go │ ├── commands/ │ │ ├── README.md │ │ └── main.go │ ├── go.mod │ └── go.sum └── xterm.go
SYMBOL INDEX (1053 symbols across 108 files)
FILE: clipboard.go
type ClipboardMsg (line 5) | type ClipboardMsg struct
method Clipboard (line 15) | func (e ClipboardMsg) Clipboard() byte {
method String (line 20) | func (e ClipboardMsg) String() string {
type setClipboardMsg (line 26) | type setClipboardMsg
function SetClipboard (line 30) | func SetClipboard(s string) Cmd {
type readClipboardMsg (line 38) | type readClipboardMsg struct
function ReadClipboard (line 42) | func ReadClipboard() Msg {
type setPrimaryClipboardMsg (line 48) | type setPrimaryClipboardMsg
function SetPrimaryClipboard (line 54) | func SetPrimaryClipboard(s string) Cmd {
type readPrimaryClipboardMsg (line 62) | type readPrimaryClipboardMsg struct
function ReadPrimaryClipboard (line 68) | func ReadPrimaryClipboard() Msg {
FILE: color.go
type backgroundColorMsg (line 10) | type backgroundColorMsg struct
function RequestBackgroundColor (line 13) | func RequestBackgroundColor() Msg {
type foregroundColorMsg (line 18) | type foregroundColorMsg struct
function RequestForegroundColor (line 21) | func RequestForegroundColor() Msg {
type cursorColorMsg (line 26) | type cursorColorMsg struct
function RequestCursorColor (line 29) | func RequestCursorColor() Msg {
type ForegroundColorMsg (line 36) | type ForegroundColorMsg struct
method String (line 39) | func (e ForegroundColorMsg) String() string {
method IsDark (line 44) | func (e ForegroundColorMsg) IsDark() bool {
type BackgroundColorMsg (line 67) | type BackgroundColorMsg struct
method String (line 70) | func (e BackgroundColorMsg) String() string {
method IsDark (line 75) | func (e BackgroundColorMsg) IsDark() bool {
type CursorColorMsg (line 81) | type CursorColorMsg struct
method String (line 84) | func (e CursorColorMsg) String() string {
method IsDark (line 89) | func (e CursorColorMsg) IsDark() bool {
FILE: commands.go
function Batch (line 15) | func Batch(cmds ...Cmd) Cmd {
type BatchMsg (line 21) | type BatchMsg
function Sequence (line 25) | func Sequence(cmds ...Cmd) Cmd {
type sequenceMsg (line 30) | type sequenceMsg
function compactCmds (line 36) | func compactCmds[T ~[]Cmd](cmds []Cmd) Cmd {
function Every (line 102) | func Every(duration time.Duration, fn func(time.Time) Msg) Cmd {
function Tick (line 154) | func Tick(d time.Duration, fn func(time.Time) Msg) Cmd {
type windowSizeMsg (line 166) | type windowSizeMsg struct
function RequestWindowSize (line 173) | func RequestWindowSize() Msg {
FILE: commands_test.go
function TestEvery (line 8) | func TestEvery(t *testing.T) {
function TestTick (line 18) | func TestTick(t *testing.T) {
function TestBatch (line 28) | func TestBatch(t *testing.T) {
function TestSequence (line 32) | func TestSequence(t *testing.T) {
function testMultipleCommands (line 36) | func testMultipleCommands[T ~[]Cmd](t *testing.T, createFn func(cmd ...C...
FILE: cursed_renderer.go
type cursedRenderer (line 18) | type cursedRenderer struct
method setLogger (line 52) | func (s *cursedRenderer) setLogger(logger uv.Logger) {
method setOptimizations (line 59) | func (s *cursedRenderer) setOptimizations(hardTabs, backspace, mapnl b...
method start (line 71) | func (s *cursedRenderer) start() {
method close (line 142) | func (s *cursedRenderer) close() (err error) {
method writeString (line 248) | func (s *cursedRenderer) writeString(str string) (int, error) {
method flush (line 256) | func (s *cursedRenderer) flush(closing bool) error {
method render (line 576) | func (s *cursedRenderer) render(v View) {
method reset (line 584) | func (s *cursedRenderer) reset() {
method setColorProfile (line 604) | func (s *cursedRenderer) setColorProfile(p colorprofile.Profile) {
method resize (line 612) | func (s *cursedRenderer) resize(w, h int) {
method clearScreen (line 627) | func (s *cursedRenderer) clearScreen() {
method setSyncdUpdates (line 676) | func (s *cursedRenderer) setSyncdUpdates(syncd bool) {
method setWidthMethod (line 683) | func (s *cursedRenderer) setWidthMethod(method ansi.Method) {
method insertAbove (line 700) | func (s *cursedRenderer) insertAbove(str string) error {
method onMouse (line 759) | func (s *cursedRenderer) onMouse(m MouseMsg) Cmd {
function newCursedRenderer (line 40) | func newCursedRenderer(w io.Writer, env []string, width, height int) (s ...
function reset (line 590) | func reset(s *cursedRenderer) {
function enableAltScreen (line 638) | func enableAltScreen(s *cursedRenderer, enable bool, write bool) {
function enterAltScreen (line 646) | func enterAltScreen(s *cursedRenderer, write bool) {
function exitAltScreen (line 656) | func exitAltScreen(s *cursedRenderer, write bool) {
function enableTextCursor (line 667) | func enableTextCursor(s *cursedRenderer, enable bool) {
function setProgressBar (line 766) | func setProgressBar(s *cursedRenderer, pb *ProgressBar) {
function viewEquals (line 790) | func viewEquals(a, b *View) bool {
FILE: cursor.go
type Position (line 4) | type Position struct
type CursorPositionMsg (line 7) | type CursorPositionMsg struct
type CursorShape (line 12) | type CursorShape
constant CursorBlock (line 16) | CursorBlock CursorShape = iota
constant CursorUnderline (line 17) | CursorUnderline
constant CursorBar (line 18) | CursorBar
type requestCursorPosMsg (line 22) | type requestCursorPosMsg struct
function RequestCursorPosition (line 26) | func RequestCursorPosition() Msg {
FILE: environ.go
type EnvMsg (line 19) | type EnvMsg
method Getenv (line 24) | func (msg EnvMsg) Getenv(key string) (v string) {
method LookupEnv (line 32) | func (msg EnvMsg) LookupEnv(key string) (s string, v bool) {
FILE: examples/altscreen-toggle/main.go
type model (line 16) | type model struct
method Init (line 22) | func (m model) Init() tea.Cmd {
method Update (line 26) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 48) | func (m model) View() tea.View {
function main (line 79) | func main() {
FILE: examples/autocomplete/main.go
function main (line 16) | func main() {
type gotReposSuccessMsg (line 24) | type gotReposSuccessMsg
type gotReposErrMsg (line 25) | type gotReposErrMsg
type repo (line 28) | type repo struct
constant reposURL (line 32) | reposURL = "https://api.github.com/orgs/charmbracelet/repos"
function getRepos (line 34) | func getRepos() tea.Msg {
type model (line 64) | type model struct
method Init (line 118) | func (m model) Init() tea.Cmd {
method Cursor (line 122) | func (m model) Cursor() *tea.Cursor {
method Update (line 130) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 161) | func (m model) View() tea.View {
method headerView (line 181) | func (m model) headerView() string { return "Enter a Charm™ repo:\n" }
method footerView (line 182) | func (m model) footerView() string { return "\n" + m.help.View(m.keyma...
type keymap (line 70) | type keymap struct
method ShortHelp (line 74) | func (k keymap) ShortHelp() []key.Binding {
method FullHelp (line 83) | func (k keymap) FullHelp() [][]key.Binding {
function initialModel (line 87) | func initialModel() model {
FILE: examples/canvas/main.go
type model (line 12) | type model struct
method Init (line 18) | func (m model) Init() tea.Cmd {
method Update (line 22) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 39) | func (m model) View() tea.View {
function newCard (line 68) | func newCard(str string) *lipgloss.Layer {
function reverse (line 81) | func reverse[T any](s []T) []T {
function main (line 90) | func main() {
FILE: examples/capability/main.go
type model (line 12) | type model struct
method Init (line 20) | func (m model) Init() tea.Cmd {
method Update (line 25) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 47) | func (m model) View() tea.View {
function main (line 59) | func main() {
function min (line 71) | func min(a, b int) int {
FILE: examples/cellbuffer/main.go
constant fps (line 18) | fps = 60
constant frequency (line 19) | frequency = 7.5
constant damping (line 20) | damping = 0.15
constant asterisk (line 21) | asterisk = "*"
function drawEllipse (line 24) | func drawEllipse(cb *cellbuffer, xc, yc, rx, ry float64) {
type cellbuffer (line 74) | type cellbuffer struct
method init (line 79) | func (c *cellbuffer) init(w, h int) {
method set (line 88) | func (c cellbuffer) set(x, y int) {
method wipe (line 96) | func (c *cellbuffer) wipe() {
method width (line 102) | func (c cellbuffer) width() int {
method height (line 106) | func (c cellbuffer) height() int {
method ready (line 114) | func (c cellbuffer) ready() bool {
method String (line 118) | func (c cellbuffer) String() string {
type frameMsg (line 129) | type frameMsg struct
function animate (line 131) | func animate() tea.Cmd {
type model (line 137) | type model struct
method Init (line 145) | func (m model) Init() tea.Cmd {
method Update (line 149) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 187) | func (m model) View() tea.View {
function main (line 194) | func main() {
FILE: examples/chat/main.go
function main (line 18) | func main() {
type model (line 25) | type model struct
method Init (line 69) | func (m model) Init() tea.Cmd {
method Update (line 73) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 113) | func (m model) View() tea.View {
function initialModel (line 33) | func initialModel() model {
FILE: examples/clickable/main.go
type LayerHitMsg (line 16) | type LayerHitMsg struct
constant maxDialogs (line 21) | maxDialogs = 999
type model (line 61) | type model struct
method Init (line 72) | func (m model) Init() tea.Cmd {
method Update (line 78) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 210) | func (m model) View() tea.View {
method newDialog (line 261) | func (m *model) newDialog(x, y int) (d dialog) {
method removeDialog (line 274) | func (m model) removeDialog(index int) []dialog {
type dialog (line 288) | type dialog struct
method buttonView (line 298) | func (d dialog) buttonView() string {
method windowView (line 307) | func (d dialog) windowView() string {
method view (line 319) | func (d dialog) view() *lipgloss.Layer {
function main (line 342) | func main() {
function clamp (line 361) | func clamp(n, min, max int) int {
FILE: examples/clickable/words.go
constant uncapitalized (line 9) | uncapitalized = " of a an and ’n’ "
function nextRandomWord (line 41) | func nextRandomWord() string {
function shuffleWords (line 53) | func shuffleWords() {
function capitalize (line 61) | func capitalize(s string) string {
function cycle (line 75) | func cycle(stack []string) []string {
FILE: examples/colorprofile/main.go
type model (line 15) | type model struct
method Init (line 20) | func (m model) Init() tea.Cmd {
method Update (line 28) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 39) | func (m model) View() tea.View {
function main (line 46) | func main() {
FILE: examples/composable-views/main.go
type sessionState (line 26) | type sessionState
constant defaultTime (line 29) | defaultTime = time.Minute
constant timerView (line 30) | timerView sessionState = iota
constant spinnerView (line 31) | spinnerView
type mainModel (line 62) | type mainModel struct
method Init (line 76) | func (m mainModel) Init() tea.Cmd {
method Update (line 81) | func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 124) | func (m mainModel) View() tea.View {
method currentFocusedModel (line 136) | func (m mainModel) currentFocusedModel() string {
method Next (line 143) | func (m *mainModel) Next() {
method resetSpinner (line 151) | func (m *mainModel) resetSpinner() {
function newModel (line 69) | func newModel(timeout time.Duration) mainModel {
function main (line 157) | func main() {
FILE: examples/cursor-style/main.go
type model (line 10) | type model struct
method Init (line 15) | func (m model) Init() tea.Cmd {
method Update (line 19) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 41) | func (m model) View() tea.View {
method describeCursor (line 52) | func (m model) describeCursor() string {
function main (line 73) | func main() {
FILE: examples/debounce/main.go
constant debounceDuration (line 21) | debounceDuration = time.Second
type exitMsg (line 23) | type exitMsg
type model (line 25) | type model struct
method Init (line 29) | func (m model) Init() tea.Cmd {
method Update (line 33) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 56) | func (m model) View() tea.View {
function main (line 61) | func main() {
FILE: examples/doom-fire/main.go
type model (line 18) | type model struct
method Init (line 26) | func (m model) Init() tea.Cmd {
method Update (line 30) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 51) | func (m model) View() tea.View {
method spreadFire (line 83) | func (m *model) spreadFire() {
method spreadPixel (line 91) | func (m *model) spreadPixel(idx int) {
type tickMsg (line 111) | type tickMsg
function tick (line 113) | func tick() tea.Msg {
function initialModel (line 118) | func initialModel() model {
function main (line 128) | func main() {
FILE: examples/exec/main.go
type editorFinishedMsg (line 12) | type editorFinishedMsg struct
function openEditor (line 14) | func openEditor() tea.Cmd {
type model (line 22) | type model struct
method Init (line 27) | func (m model) Init() tea.Cmd {
method Update (line 31) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 52) | func (m model) View() tea.View {
function main (line 63) | func main() {
FILE: examples/eyes/main.go
constant eyeWidth (line 17) | eyeWidth = 15
constant eyeHeight (line 18) | eyeHeight = 12
constant eyeSpacing (line 19) | eyeSpacing = 40
constant blinkFrames (line 22) | blinkFrames = 20
constant openTimeMin (line 23) | openTimeMin = 1000
constant openTimeMax (line 24) | openTimeMax = 4000
constant eyeChar (line 29) | eyeChar = "●"
constant bgChar (line 30) | bgChar = " "
type model (line 33) | type model struct
method updateEyePositions (line 67) | func (m *model) updateEyePositions() {
method Init (line 75) | func (m model) Init() tea.Cmd {
method Update (line 85) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 125) | func (m model) View() tea.View {
type tickMsg (line 44) | type tickMsg
function main (line 46) | func main() {
function initialModel (line 53) | func initialModel() model {
function tickCmd (line 79) | func tickCmd() tea.Cmd {
function drawEllipse (line 179) | func drawEllipse(canvas [][]string, x0, y0, rx, ry int) {
FILE: examples/file-picker/main.go
type model (line 14) | type model struct
method Init (line 29) | func (m model) Init() tea.Cmd {
method Update (line 33) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 66) | func (m model) View() tea.View {
type clearErrorMsg (line 21) | type clearErrorMsg struct
function clearErrorAfter (line 23) | func clearErrorAfter(t time.Duration) tea.Cmd {
function main (line 85) | func main() {
FILE: examples/focus-blur/main.go
function main (line 11) | func main() {
type model (line 21) | type model struct
method Init (line 26) | func (m model) Init() tea.Cmd {
method Update (line 30) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 48) | func (m model) View() tea.View {
FILE: examples/fullscreen/main.go
type model (line 14) | type model
method Init (line 25) | func (m model) Init() tea.Cmd {
method Update (line 29) | func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
method View (line 48) | func (m model) View() tea.View {
type tickMsg (line 16) | type tickMsg
function main (line 18) | func main() {
function tick (line 54) | func tick() tea.Cmd {
FILE: examples/glamour/main.go
constant content (line 14) | content = `
type example (line 55) | type example struct
method Init (line 108) | func (e example) Init() tea.Cmd {
method Update (line 112) | func (e example) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 128) | func (e example) View() tea.View {
method helpView (line 132) | func (e example) helpView() string {
function newExample (line 59) | func newExample(isDark bool) (*example, error) {
function main (line 136) | func main() {
FILE: examples/help/main.go
type keyMap (line 16) | type keyMap struct
method ShortHelp (line 27) | func (k keyMap) ShortHelp() []key.Binding {
method FullHelp (line 33) | func (k keyMap) FullHelp() [][]key.Binding {
type model (line 67) | type model struct
method Init (line 83) | func (m model) Init() tea.Cmd {
method Update (line 87) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 115) | func (m model) View() tea.View {
function newModel (line 75) | func newModel() model {
function main (line 133) | func main() {
FILE: examples/http/main.go
constant url (line 14) | url = "https://charm.sh/"
type model (line 16) | type model struct
method Init (line 34) | func (m model) Init() tea.Cmd {
method Update (line 38) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 61) | func (m model) View() tea.View {
type statusMsg (line 21) | type statusMsg
type errMsg (line 23) | type errMsg struct
method Error (line 25) | func (e errMsg) Error() string { return e.error.Error() }
function main (line 27) | func main() {
function checkServer (line 71) | func checkServer() tea.Msg {
FILE: examples/isbn-form/main.go
function main (line 15) | func main() {
type errMsg (line 24) | type errMsg
type model (line 34) | type model struct
method canFindBook (line 42) | func (m model) canFindBook() bool {
method Init (line 155) | func (m model) Init() tea.Cmd {
method Update (line 159) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 199) | func (m model) View() tea.View {
function isbn13Validator (line 50) | func isbn13Validator(s string) error {
function bookTitleValidator (line 114) | func bookTitleValidator(s string) error {
function initialModel (line 130) | func initialModel() model {
FILE: examples/keyboard-enhancements/main.go
type styles (line 15) | type styles struct
type model (line 19) | type model struct
method Init (line 25) | func (m model) Init() tea.Cmd {
method Update (line 29) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 68) | func (m model) View() tea.View {
method updateStyles (line 85) | func (m *model) updateStyles(isDark bool) {
function initialModel (line 97) | func initialModel() model {
function main (line 103) | func main() {
FILE: examples/list-default/main.go
type item (line 14) | type item struct
method Title (line 18) | func (i item) Title() string { return i.title }
method Description (line 19) | func (i item) Description() string { return i.desc }
method FilterValue (line 20) | func (i item) FilterValue() string { return i.title }
type model (line 22) | type model struct
method Init (line 26) | func (m model) Init() tea.Cmd {
method Update (line 30) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 46) | func (m model) View() tea.View {
function main (line 52) | func main() {
FILE: examples/list-fancy/delegate.go
function newItemDelegate (line 9) | func newItemDelegate(keys *delegateKeyMap, styles *styles) list.DefaultD...
type delegateKeyMap (line 53) | type delegateKeyMap struct
method ShortHelp (line 60) | func (d delegateKeyMap) ShortHelp() []key.Binding {
method FullHelp (line 69) | func (d delegateKeyMap) FullHelp() [][]key.Binding {
function newDelegateKeyMap (line 78) | func newDelegateKeyMap() *delegateKeyMap {
FILE: examples/list-fancy/main.go
type styles (line 14) | type styles struct
function newStyles (line 20) | func newStyles(darkBG bool) styles {
type item (line 35) | type item struct
method Title (line 40) | func (i item) Title() string { return i.title }
method Description (line 41) | func (i item) Description() string { return i.description }
method FilterValue (line 42) | func (i item) FilterValue() string { return i.title }
type listKeyMap (line 44) | type listKeyMap struct
function newListKeyMap (line 53) | func newListKeyMap() *listKeyMap {
type model (line 82) | type model struct
method Init (line 93) | func (m model) Init() tea.Cmd {
method updateListProperties (line 99) | func (m *model) updateListProperties() {
method Update (line 109) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 172) | func (m model) View() tea.View {
function initialModel (line 178) | func initialModel() model {
function main (line 218) | func main() {
FILE: examples/list-fancy/randomitems.go
type randomItemGenerator (line 8) | type randomItemGenerator struct
method reset (line 17) | func (r *randomItemGenerator) reset() {
method next (line 140) | func (r *randomItemGenerator) next() item {
FILE: examples/list-simple/main.go
constant listHeight (line 14) | listHeight = 14
type styles (line 16) | type styles struct
function newStyles (line 25) | func newStyles(darkBG bool) styles {
type item (line 36) | type item
method FilterValue (line 38) | func (i item) FilterValue() string { return "" }
type itemDelegate (line 40) | type itemDelegate struct
method Height (line 44) | func (d itemDelegate) Height() int { retur...
method Spacing (line 45) | func (d itemDelegate) Spacing() int { retur...
method Update (line 46) | func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { retur...
method Render (line 47) | func (d itemDelegate) Render(w io.Writer, m list.Model, index int, lis...
type model (line 65) | type model struct
method updateStyles (line 98) | func (m *model) updateStyles(isDark bool) {
method Init (line 106) | func (m model) Init() tea.Cmd {
method Update (line 110) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 136) | func (m model) View() tea.View {
function initialModel (line 72) | func initialModel() model {
function main (line 146) | func main() {
FILE: examples/mouse/main.go
function main (line 12) | func main() {
type model (line 19) | type model struct
method Init (line 21) | func (m model) Init() tea.Cmd {
method Update (line 25) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 40) | func (m model) View() tea.View {
FILE: examples/package-manager/main.go
type model (line 16) | type model struct
method Init (line 47) | func (m model) Init() tea.Cmd {
method Update (line 51) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 92) | func (m model) View() tea.View {
function newModel (line 32) | func newModel() model {
type installedPkgMsg (line 115) | type installedPkgMsg
function downloadAndInstall (line 117) | func downloadAndInstall(pkg string) tea.Cmd {
function main (line 126) | func main() {
FILE: examples/package-manager/packages.go
function getPackages (line 40) | func getPackages() []string {
FILE: examples/pager/main.go
type model (line 31) | type model struct
method Init (line 37) | func (m model) Init() tea.Cmd {
method Update (line 41) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 94) | func (m model) View() tea.View {
method headerView (line 106) | func (m model) headerView() string {
method footerView (line 112) | func (m model) footerView() string {
function main (line 118) | func main() {
FILE: examples/paginator/main.go
type styles (line 17) | type styles struct
function newStyles (line 22) | func newStyles(bgIsDark bool) (s styles) {
type model (line 30) | type model struct
method updateStyles (line 56) | func (m *model) updateStyles(isDark bool) {
method Init (line 62) | func (m model) Init() tea.Cmd {
method Update (line 66) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 82) | func (m model) View() tea.View {
function newModel (line 35) | func newModel() model {
function main (line 94) | func main() {
FILE: examples/pipe/main.go
function main (line 20) | func main() {
type model (line 54) | type model struct
method Init (line 75) | func (m model) Init() tea.Cmd {
method Update (line 79) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 92) | func (m model) View() tea.View {
function newModel (line 58) | func newModel(initialValue string) (m model) {
FILE: examples/prevent-quit/main.go
function main (line 22) | func main() {
function filter (line 30) | func filter(teaModel tea.Model, msg tea.Msg) tea.Msg {
type model (line 43) | type model struct
method Init (line 78) | func (m model) Init() tea.Cmd {
method Update (line 82) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method updateTextView (line 90) | func (m model) updateTextView(msg tea.Msg) (tea.Model, tea.Cmd) {
method updatePromptView (line 120) | func (m model) updatePromptView(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 134) | func (m model) View() tea.View {
type keymap (line 52) | type keymap struct
function initialModel (line 57) | func initialModel() model {
FILE: examples/print-key/main.go
type model (line 9) | type model struct
method Init (line 11) | func (m model) Init() tea.Cmd {
method Update (line 15) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 40) | func (m model) View() tea.View {
function main (line 46) | func main() {
FILE: examples/progress-animated/main.go
constant padding (line 22) | padding = 2
constant maxWidth (line 23) | maxWidth = 80
function main (line 28) | func main() {
type tickMsg (line 39) | type tickMsg
type model (line 41) | type model struct
method Init (line 45) | func (m model) Init() tea.Cmd {
method Update (line 49) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 82) | func (m model) View() tea.View {
function tickCmd (line 89) | func tickCmd() tea.Cmd {
FILE: examples/progress-bar/main.go
type model (line 12) | type model struct
method Init (line 18) | func (m model) Init() tea.Cmd {
method Update (line 22) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 51) | func (m model) View() tea.View {
function main (line 60) | func main() {
FILE: examples/progress-download/main.go
type progressWriter (line 18) | type progressWriter struct
method Start (line 26) | func (pw *progressWriter) Start() {
method Write (line 34) | func (pw *progressWriter) Write(p []byte) (int, error) {
function getResponse (line 42) | func getResponse(url string) (*http.Response, error) {
function main (line 53) | func main() {
FILE: examples/progress-download/tui.go
constant padding (line 15) | padding = 2
constant maxWidth (line 16) | maxWidth = 80
type progressMsg (line 19) | type progressMsg
type progressErrMsg (line 21) | type progressErrMsg struct
function finalPause (line 23) | func finalPause() tea.Cmd {
type model (line 29) | type model struct
method Init (line 35) | func (m model) Init() tea.Cmd {
method Update (line 39) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 76) | func (m model) View() tea.View {
FILE: examples/progress-static/main.go
constant padding (line 31) | padding = 2
constant maxWidth (line 32) | maxWidth = 80
function main (line 41) | func main() {
type tickMsg (line 50) | type tickMsg
type model (line 52) | type model struct
method Init (line 57) | func (m model) Init() tea.Cmd {
method Update (line 61) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 86) | func (m model) View() tea.View {
function tickCmd (line 93) | func tickCmd() tea.Cmd {
FILE: examples/query-term/main.go
function newModel (line 18) | func newModel() model {
type model (line 27) | type model struct
method Init (line 32) | func (m model) Init() tea.Cmd {
method Update (line 36) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 85) | func (m model) View() tea.View {
function main (line 97) | func main() {
FILE: examples/realtime/main.go
type responseMsg (line 18) | type responseMsg struct
function listenForActivity (line 24) | func listenForActivity(sub chan struct{}) tea.Cmd {
function waitForActivity (line 34) | func waitForActivity(sub chan struct{}) tea.Cmd {
type model (line 40) | type model struct
method Init (line 47) | func (m model) Init() tea.Cmd {
method Update (line 55) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 72) | func (m model) View() tea.View {
function main (line 80) | func main() {
FILE: examples/result/main.go
type model (line 16) | type model struct
method Init (line 21) | func (m model) Init() tea.Cmd {
method Update (line 25) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 54) | func (m model) View() tea.View {
function main (line 72) | func main() {
FILE: examples/send-msg/main.go
type resultMsg (line 26) | type resultMsg struct
method String (line 31) | func (r resultMsg) String() string {
type model (line 39) | type model struct
method Init (line 55) | func (m model) Init() tea.Cmd {
method Update (line 59) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 76) | func (m model) View() tea.View {
function newModel (line 45) | func newModel() model {
function main (line 104) | func main() {
function randomFood (line 126) | func randomFood() string {
FILE: examples/sequence/main.go
type model (line 13) | type model struct
method Init (line 15) | func (m model) Init() tea.Cmd {
method Update (line 54) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 62) | func (m model) View() tea.View {
function SleepPrintln (line 46) | func SleepPrintln(s string, milisecond int) tea.Cmd {
function main (line 66) | func main() {
FILE: examples/set-terminal-color/main.go
type colorType (line 14) | type colorType
method String (line 22) | func (c colorType) String() string {
constant foreground (line 17) | foreground colorType = iota + 1
constant background (line 18) | background
constant cursor (line 19) | cursor
type state (line 35) | type state
constant chooseState (line 38) | chooseState state = iota
constant inputState (line 39) | inputState
type model (line 42) | type model struct
method Init (line 51) | func (m model) Init() tea.Cmd {
method Update (line 55) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 137) | func (m model) View() tea.View {
function main (line 187) | func main() {
FILE: examples/set-window-title/main.go
constant windowTitle (line 13) | windowTitle = "Hello, Bubble Tea"
type model (line 15) | type model struct
method Init (line 17) | func (m model) Init() tea.Cmd {
method Update (line 21) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 29) | func (m model) View() tea.View {
function main (line 37) | func main() {
FILE: examples/simple/main.go
function main (line 14) | func main() {
type model (line 34) | type model
method Init (line 38) | func (m model) Init() tea.Cmd {
method Update (line 45) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 67) | func (m model) View() tea.View {
type tickMsg (line 73) | type tickMsg
function tick (line 75) | func tick() tea.Msg {
FILE: examples/space/main.go
type model (line 20) | type model struct
method Init (line 29) | func (m model) Init() tea.Cmd {
method Update (line 43) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method setupColors (line 68) | func (m *model) setupColors() {
method View (line 98) | func (m model) View() tea.View {
function tickCmd (line 35) | func tickCmd() tea.Cmd {
type tickMsg (line 41) | type tickMsg struct
function clamp (line 88) | func clamp(value, min, max float64) float64 {
function main (line 126) | func main() {
FILE: examples/spinner/main.go
type errMsg (line 15) | type errMsg
type model (line 17) | type model struct
method Init (line 30) | func (m model) Init() tea.Cmd {
method Update (line 34) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 56) | func (m model) View() tea.View {
function initialModel (line 23) | func initialModel() model {
function main (line 67) | func main() {
FILE: examples/spinners/main.go
function main (line 31) | func main() {
type model (line 41) | type model struct
method Init (line 46) | func (m model) Init() tea.Cmd {
method Update (line 50) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method resetSpinner (line 82) | func (m *model) resetSpinner() {
method View (line 88) | func (m model) View() tea.View {
FILE: examples/splash/main.go
type model (line 34) | type model struct
method Init (line 40) | func (m model) Init() tea.Cmd {
method Update (line 44) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 57) | func (m model) View() tea.View {
method gradient (line 69) | func (m model) gradient() string {
function getGradientColor (line 120) | func getGradientColor(position float64) color.Color {
function interpolateColors (line 149) | func interpolateColors(color1, color2 color.Color, t float64) color.Color {
type tickMsg (line 164) | type tickMsg
function tick (line 166) | func tick() tea.Msg {
function main (line 170) | func main() {
FILE: examples/split-editors/main.go
constant initialInputs (line 15) | initialInputs = 2
constant maxInputs (line 16) | maxInputs = 6
constant minInputs (line 17) | minInputs = 1
constant helpHeight (line 18) | helpHeight = 5
function newTextarea (line 49) | func newTextarea() textarea.Model {
type model (line 75) | type model struct
method Init (line 119) | func (m model) Init() tea.Cmd {
method Update (line 123) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method sizeInputs (line 176) | func (m *model) sizeInputs() {
method updateKeybindings (line 183) | func (m *model) updateKeybindings() {
method inputViews (line 188) | func (m model) inputViews() []string {
method View (line 196) | func (m model) View() tea.View {
method Cursor (line 210) | func (m model) Cursor() *tea.Cursor {
function newModel (line 84) | func newModel() model {
function main (line 230) | func main() {
FILE: examples/stopwatch/main.go
type model (line 14) | type model struct
method Init (line 28) | func (m model) Init() tea.Cmd {
method View (line 32) | func (m model) View() tea.View {
method helpView (line 44) | func (m model) helpView() string {
method Update (line 53) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
type keymap (line 21) | type keymap struct
function main (line 73) | func main() {
FILE: examples/suspend/main.go
type model (line 11) | type model struct
method Init (line 16) | func (m model) Init() tea.Cmd {
method Update (line 20) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 41) | func (m model) View() tea.View {
function main (line 49) | func main() {
FILE: examples/table-resize/main.go
constant None (line 15) | None = ""
constant Bug (line 16) | Bug = "Bug"
constant Electric (line 17) | Electric = "Electric"
constant Fire (line 18) | Fire = "Fire"
constant Flying (line 19) | Flying = "Flying"
constant Grass (line 20) | Grass = "Grass"
constant Ground (line 21) | Ground = "Ground"
constant Normal (line 22) | Normal = "Normal"
constant Poison (line 23) | Poison = "Poison"
constant Water (line 24) | Water = "Water"
type model (line 27) | type model struct
method Init (line 31) | func (m model) Init() tea.Cmd { return nil }
method Update (line 33) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 49) | func (m model) View() tea.View {
function main (line 55) | func main() {
FILE: examples/table/main.go
type model (line 16) | type model struct
method Init (line 20) | func (m model) Init() tea.Cmd { return nil }
method Update (line 22) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 45) | func (m model) View() tea.View {
function main (line 49) | func main() {
FILE: examples/tabs/main.go
type styles (line 12) | type styles struct
function newStyles (line 20) | func newStyles(bgIsDark bool) *styles {
type model (line 45) | type model struct
method Init (line 52) | func (m model) Init() tea.Cmd {
method Update (line 56) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 82) | func (m model) View() tea.View {
function tabBorderWithBottom (line 74) | func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
function main (line 121) | func main() {
FILE: examples/textarea/main.go
function main (line 15) | func main() {
type errMsg (line 23) | type errMsg
type model (line 25) | type model struct
method Init (line 43) | func (m model) Init() tea.Cmd {
method Update (line 47) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method headerView (line 82) | func (m model) headerView() string {
method View (line 86) | func (m model) View() tea.View {
function initialModel (line 30) | func initialModel() model {
FILE: examples/textinput/main.go
function main (line 14) | func main() {
type errMsg (line 22) | type errMsg
type model (line 25) | type model struct
method Init (line 42) | func (m model) Init() tea.Cmd {
method Update (line 46) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 62) | func (m model) View() tea.View {
method headerView (line 79) | func (m model) headerView() string { return "What’s your favorite Poké...
method footerView (line 80) | func (m model) footerView() string { return "\n(esc to quit)" }
function initialModel (line 31) | func initialModel() model {
FILE: examples/textinputs/main.go
type model (line 29) | type model struct
method Init (line 73) | func (m model) Init() tea.Cmd {
method Update (line 77) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method updateInputs (line 143) | func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
method View (line 155) | func (m model) View() tea.View {
function initialModel (line 36) | func initialModel() model {
function main (line 191) | func main() {
FILE: examples/timer/main.go
constant timeout (line 14) | timeout = time.Second * 5
type model (line 16) | type model struct
method Init (line 30) | func (m model) Init() tea.Cmd {
method Update (line 34) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method helpView (line 67) | func (m model) helpView() string {
method View (line 76) | func (m model) View() tea.View {
type keymap (line 23) | type keymap struct
function main (line 93) | func main() {
FILE: examples/tui-daemon-combo/main.go
function main (line 23) | func main() {
type result (line 54) | type result struct
type model (line 59) | type model struct
method Init (line 77) | func (m model) Init() tea.Cmd {
method Update (line 85) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 105) | func (m model) View() tea.View {
function newModel (line 65) | func newModel() model {
type processFinishedMsg (line 127) | type processFinishedMsg
function runPretendProcess (line 130) | func runPretendProcess() tea.Msg {
function randomEmoji (line 136) | func randomEmoji() string {
FILE: examples/vanish/main.go
type model (line 10) | type model
method Init (line 12) | func (m model) Init() tea.Cmd {
method Update (line 16) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 24) | func (m model) View() tea.View {
function main (line 31) | func main() {
FILE: examples/views/main.go
constant progressBarWidth (line 23) | progressBarWidth = 71
constant progressFullChar (line 24) | progressFullChar = "█"
constant progressEmptyChar (line 25) | progressEmptyChar = "░"
constant dotChar (line 26) | dotChar = " • "
function main (line 43) | func main() {
type tickMsg (line 52) | type tickMsg struct
type frameMsg (line 53) | type frameMsg struct
function tick (line 56) | func tick() tea.Cmd {
function frame (line 62) | func frame() tea.Cmd {
type model (line 68) | type model struct
method Init (line 78) | func (m model) Init() tea.Cmd {
method Update (line 83) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 102) | func (m model) View() tea.View {
function updateChoices (line 118) | func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
function updateChosen (line 150) | func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
function choicesView (line 182) | func choicesView(m model) string {
function chosenView (line 204) | func chosenView(m model) string {
function checkbox (line 226) | func checkbox(label string, checked bool) string {
function progressbar (line 233) | func progressbar(percent float64) string {
function makeRampStyles (line 251) | func makeRampStyles(colorA, colorB string, steps float64) (s []lipgloss....
function colorToHex (line 263) | func colorToHex(c colorful.Color) string {
function colorFloatToHex (line 269) | func colorFloatToHex(f float64) (s string) {
FILE: examples/window-size/main.go
function main (line 11) | func main() {
type model (line 18) | type model struct
method Init (line 20) | func (m model) Init() tea.Cmd {
method Update (line 24) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 39) | func (m model) View() tea.View {
FILE: exec.go
type execMsg (line 10) | type execMsg struct
function Exec (line 22) | func Exec(c ExecCommand, fn ExecCallback) Cmd {
function ExecProcess (line 50) | func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd {
type ExecCallback (line 56) | type ExecCallback
type ExecCommand (line 60) | type ExecCommand interface
function wrapExecCommand (line 69) | func wrapExecCommand(c *exec.Cmd) ExecCommand {
type osExecCommand (line 75) | type osExecCommand struct
method SetStdin (line 78) | func (c *osExecCommand) SetStdin(r io.Reader) {
method SetStdout (line 86) | func (c *osExecCommand) SetStdout(w io.Writer) {
method SetStderr (line 94) | func (c *osExecCommand) SetStderr(w io.Writer) {
method exec (line 102) | func (p *Program) exec(c ExecCommand, fn ExecCallback) {
FILE: exec_test.go
type execFinishedMsg (line 10) | type execFinishedMsg struct
type testExecModel (line 12) | type testExecModel struct
method Init (line 17) | func (m *testExecModel) Init() Cmd {
method Update (line 24) | func (m *testExecModel) Update(msg Msg) (Model, Cmd) {
method View (line 36) | func (m *testExecModel) View() View {
type spyRenderer (line 40) | type spyRenderer struct
function TestTeaExec (line 45) | func TestTeaExec(t *testing.T) {
FILE: focus.go
type FocusMsg (line 5) | type FocusMsg struct
type BlurMsg (line 9) | type BlurMsg struct
FILE: input.go
method translateInputEvent (line 8) | func (p *Program) translateInputEvent(e uv.Event) Msg {
FILE: key.go
constant KeyExtended (line 12) | KeyExtended = uv.KeyExtended
constant KeyUp (line 20) | KeyUp = uv.KeyUp
constant KeyDown (line 21) | KeyDown = uv.KeyDown
constant KeyRight (line 22) | KeyRight = uv.KeyRight
constant KeyLeft (line 23) | KeyLeft = uv.KeyLeft
constant KeyBegin (line 24) | KeyBegin = uv.KeyBegin
constant KeyFind (line 25) | KeyFind = uv.KeyFind
constant KeyInsert (line 26) | KeyInsert = uv.KeyInsert
constant KeyDelete (line 27) | KeyDelete = uv.KeyDelete
constant KeySelect (line 28) | KeySelect = uv.KeySelect
constant KeyPgUp (line 29) | KeyPgUp = uv.KeyPgUp
constant KeyPgDown (line 30) | KeyPgDown = uv.KeyPgDown
constant KeyHome (line 31) | KeyHome = uv.KeyHome
constant KeyEnd (line 32) | KeyEnd = uv.KeyEnd
constant KeyKpEnter (line 36) | KeyKpEnter = uv.KeyKpEnter
constant KeyKpEqual (line 37) | KeyKpEqual = uv.KeyKpEqual
constant KeyKpMultiply (line 38) | KeyKpMultiply = uv.KeyKpMultiply
constant KeyKpPlus (line 39) | KeyKpPlus = uv.KeyKpPlus
constant KeyKpComma (line 40) | KeyKpComma = uv.KeyKpComma
constant KeyKpMinus (line 41) | KeyKpMinus = uv.KeyKpMinus
constant KeyKpDecimal (line 42) | KeyKpDecimal = uv.KeyKpDecimal
constant KeyKpDivide (line 43) | KeyKpDivide = uv.KeyKpDivide
constant KeyKp0 (line 44) | KeyKp0 = uv.KeyKp0
constant KeyKp1 (line 45) | KeyKp1 = uv.KeyKp1
constant KeyKp2 (line 46) | KeyKp2 = uv.KeyKp2
constant KeyKp3 (line 47) | KeyKp3 = uv.KeyKp3
constant KeyKp4 (line 48) | KeyKp4 = uv.KeyKp4
constant KeyKp5 (line 49) | KeyKp5 = uv.KeyKp5
constant KeyKp6 (line 50) | KeyKp6 = uv.KeyKp6
constant KeyKp7 (line 51) | KeyKp7 = uv.KeyKp7
constant KeyKp8 (line 52) | KeyKp8 = uv.KeyKp8
constant KeyKp9 (line 53) | KeyKp9 = uv.KeyKp9
constant KeyKpSep (line 57) | KeyKpSep = uv.KeyKpSep
constant KeyKpUp (line 58) | KeyKpUp = uv.KeyKpUp
constant KeyKpDown (line 59) | KeyKpDown = uv.KeyKpDown
constant KeyKpLeft (line 60) | KeyKpLeft = uv.KeyKpLeft
constant KeyKpRight (line 61) | KeyKpRight = uv.KeyKpRight
constant KeyKpPgUp (line 62) | KeyKpPgUp = uv.KeyKpPgUp
constant KeyKpPgDown (line 63) | KeyKpPgDown = uv.KeyKpPgDown
constant KeyKpHome (line 64) | KeyKpHome = uv.KeyKpHome
constant KeyKpEnd (line 65) | KeyKpEnd = uv.KeyKpEnd
constant KeyKpInsert (line 66) | KeyKpInsert = uv.KeyKpInsert
constant KeyKpDelete (line 67) | KeyKpDelete = uv.KeyKpDelete
constant KeyKpBegin (line 68) | KeyKpBegin = uv.KeyKpBegin
constant KeyF1 (line 72) | KeyF1 = uv.KeyF1
constant KeyF2 (line 73) | KeyF2 = uv.KeyF2
constant KeyF3 (line 74) | KeyF3 = uv.KeyF3
constant KeyF4 (line 75) | KeyF4 = uv.KeyF4
constant KeyF5 (line 76) | KeyF5 = uv.KeyF5
constant KeyF6 (line 77) | KeyF6 = uv.KeyF6
constant KeyF7 (line 78) | KeyF7 = uv.KeyF7
constant KeyF8 (line 79) | KeyF8 = uv.KeyF8
constant KeyF9 (line 80) | KeyF9 = uv.KeyF9
constant KeyF10 (line 81) | KeyF10 = uv.KeyF10
constant KeyF11 (line 82) | KeyF11 = uv.KeyF11
constant KeyF12 (line 83) | KeyF12 = uv.KeyF12
constant KeyF13 (line 84) | KeyF13 = uv.KeyF13
constant KeyF14 (line 85) | KeyF14 = uv.KeyF14
constant KeyF15 (line 86) | KeyF15 = uv.KeyF15
constant KeyF16 (line 87) | KeyF16 = uv.KeyF16
constant KeyF17 (line 88) | KeyF17 = uv.KeyF17
constant KeyF18 (line 89) | KeyF18 = uv.KeyF18
constant KeyF19 (line 90) | KeyF19 = uv.KeyF19
constant KeyF20 (line 91) | KeyF20 = uv.KeyF20
constant KeyF21 (line 92) | KeyF21 = uv.KeyF21
constant KeyF22 (line 93) | KeyF22 = uv.KeyF22
constant KeyF23 (line 94) | KeyF23 = uv.KeyF23
constant KeyF24 (line 95) | KeyF24 = uv.KeyF24
constant KeyF25 (line 96) | KeyF25 = uv.KeyF25
constant KeyF26 (line 97) | KeyF26 = uv.KeyF26
constant KeyF27 (line 98) | KeyF27 = uv.KeyF27
constant KeyF28 (line 99) | KeyF28 = uv.KeyF28
constant KeyF29 (line 100) | KeyF29 = uv.KeyF29
constant KeyF30 (line 101) | KeyF30 = uv.KeyF30
constant KeyF31 (line 102) | KeyF31 = uv.KeyF31
constant KeyF32 (line 103) | KeyF32 = uv.KeyF32
constant KeyF33 (line 104) | KeyF33 = uv.KeyF33
constant KeyF34 (line 105) | KeyF34 = uv.KeyF34
constant KeyF35 (line 106) | KeyF35 = uv.KeyF35
constant KeyF36 (line 107) | KeyF36 = uv.KeyF36
constant KeyF37 (line 108) | KeyF37 = uv.KeyF37
constant KeyF38 (line 109) | KeyF38 = uv.KeyF38
constant KeyF39 (line 110) | KeyF39 = uv.KeyF39
constant KeyF40 (line 111) | KeyF40 = uv.KeyF40
constant KeyF41 (line 112) | KeyF41 = uv.KeyF41
constant KeyF42 (line 113) | KeyF42 = uv.KeyF42
constant KeyF43 (line 114) | KeyF43 = uv.KeyF43
constant KeyF44 (line 115) | KeyF44 = uv.KeyF44
constant KeyF45 (line 116) | KeyF45 = uv.KeyF45
constant KeyF46 (line 117) | KeyF46 = uv.KeyF46
constant KeyF47 (line 118) | KeyF47 = uv.KeyF47
constant KeyF48 (line 119) | KeyF48 = uv.KeyF48
constant KeyF49 (line 120) | KeyF49 = uv.KeyF49
constant KeyF50 (line 121) | KeyF50 = uv.KeyF50
constant KeyF51 (line 122) | KeyF51 = uv.KeyF51
constant KeyF52 (line 123) | KeyF52 = uv.KeyF52
constant KeyF53 (line 124) | KeyF53 = uv.KeyF53
constant KeyF54 (line 125) | KeyF54 = uv.KeyF54
constant KeyF55 (line 126) | KeyF55 = uv.KeyF55
constant KeyF56 (line 127) | KeyF56 = uv.KeyF56
constant KeyF57 (line 128) | KeyF57 = uv.KeyF57
constant KeyF58 (line 129) | KeyF58 = uv.KeyF58
constant KeyF59 (line 130) | KeyF59 = uv.KeyF59
constant KeyF60 (line 131) | KeyF60 = uv.KeyF60
constant KeyF61 (line 132) | KeyF61 = uv.KeyF61
constant KeyF62 (line 133) | KeyF62 = uv.KeyF62
constant KeyF63 (line 134) | KeyF63 = uv.KeyF63
constant KeyCapsLock (line 139) | KeyCapsLock = uv.KeyCapsLock
constant KeyScrollLock (line 140) | KeyScrollLock = uv.KeyScrollLock
constant KeyNumLock (line 141) | KeyNumLock = uv.KeyNumLock
constant KeyPrintScreen (line 142) | KeyPrintScreen = uv.KeyPrintScreen
constant KeyPause (line 143) | KeyPause = uv.KeyPause
constant KeyMenu (line 144) | KeyMenu = uv.KeyMenu
constant KeyMediaPlay (line 146) | KeyMediaPlay = uv.KeyMediaPlay
constant KeyMediaPause (line 147) | KeyMediaPause = uv.KeyMediaPause
constant KeyMediaPlayPause (line 148) | KeyMediaPlayPause = uv.KeyMediaPlayPause
constant KeyMediaReverse (line 149) | KeyMediaReverse = uv.KeyMediaReverse
constant KeyMediaStop (line 150) | KeyMediaStop = uv.KeyMediaStop
constant KeyMediaFastForward (line 151) | KeyMediaFastForward = uv.KeyMediaFastForward
constant KeyMediaRewind (line 152) | KeyMediaRewind = uv.KeyMediaRewind
constant KeyMediaNext (line 153) | KeyMediaNext = uv.KeyMediaNext
constant KeyMediaPrev (line 154) | KeyMediaPrev = uv.KeyMediaPrev
constant KeyMediaRecord (line 155) | KeyMediaRecord
constant KeyLowerVol (line 157) | KeyLowerVol = uv.KeyLowerVol
constant KeyRaiseVol (line 158) | KeyRaiseVol = uv.KeyRaiseVol
constant KeyMute (line 159) | KeyMute = uv.KeyMute
constant KeyLeftShift (line 161) | KeyLeftShift = uv.KeyLeftShift
constant KeyLeftAlt (line 162) | KeyLeftAlt = uv.KeyLeftAlt
constant KeyLeftCtrl (line 163) | KeyLeftCtrl = uv.KeyLeftCtrl
constant KeyLeftSuper (line 164) | KeyLeftSuper = uv.KeyLeftSuper
constant KeyLeftHyper (line 165) | KeyLeftHyper = uv.KeyLeftHyper
constant KeyLeftMeta (line 166) | KeyLeftMeta = uv.KeyLeftMeta
constant KeyRightShift (line 167) | KeyRightShift = uv.KeyRightShift
constant KeyRightAlt (line 168) | KeyRightAlt = uv.KeyRightAlt
constant KeyRightCtrl (line 169) | KeyRightCtrl = uv.KeyRightCtrl
constant KeyRightSuper (line 170) | KeyRightSuper = uv.KeyRightSuper
constant KeyRightHyper (line 171) | KeyRightHyper = uv.KeyRightHyper
constant KeyRightMeta (line 172) | KeyRightMeta = uv.KeyRightMeta
constant KeyIsoLevel3Shift (line 173) | KeyIsoLevel3Shift = uv.KeyIsoLevel3Shift
constant KeyIsoLevel5Shift (line 174) | KeyIsoLevel5Shift = uv.KeyIsoLevel5Shift
constant KeyBackspace (line 178) | KeyBackspace = uv.KeyBackspace
constant KeyTab (line 179) | KeyTab = uv.KeyTab
constant KeyEnter (line 180) | KeyEnter = uv.KeyEnter
constant KeyReturn (line 181) | KeyReturn = uv.KeyReturn
constant KeyEscape (line 182) | KeyEscape = uv.KeyEscape
constant KeyEsc (line 183) | KeyEsc = uv.KeyEsc
constant KeySpace (line 187) | KeySpace = uv.KeySpace
type KeyPressMsg (line 191) | type KeyPressMsg
method String (line 195) | func (k KeyPressMsg) String() string {
method Keystroke (line 213) | func (k KeyPressMsg) Keystroke() string {
method Key (line 219) | func (k KeyPressMsg) Key() Key {
type KeyReleaseMsg (line 224) | type KeyReleaseMsg
method String (line 228) | func (k KeyReleaseMsg) String() string {
method Keystroke (line 246) | func (k KeyReleaseMsg) Keystroke() string {
method Key (line 253) | func (k KeyReleaseMsg) Key() Key {
type KeyMsg (line 259) | type KeyMsg interface
type Key (line 302) | type Key struct
method String (line 351) | func (k Key) String() string {
method Keystroke (line 369) | func (k Key) Keystroke() string {
FILE: keyboard.go
type KeyboardEnhancementsMsg (line 9) | type KeyboardEnhancementsMsg struct
method SupportsKeyDisambiguation (line 33) | func (k KeyboardEnhancementsMsg) SupportsKeyDisambiguation() bool {
method SupportsEventTypes (line 39) | func (k KeyboardEnhancementsMsg) SupportsEventTypes() bool {
FILE: logging.go
function LogToFile (line 23) | func LogToFile(path string, prefix string) (*os.File, error) {
type LogOptionsSetter (line 29) | type LogOptionsSetter interface
function LogToFileWith (line 35) | func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*o...
FILE: logging_test.go
function TestLogToFile (line 10) | func TestLogToFile(t *testing.T) {
FILE: mod.go
constant ModShift (line 10) | ModShift = uv.ModShift
constant ModAlt (line 11) | ModAlt = uv.ModAlt
constant ModCtrl (line 12) | ModCtrl = uv.ModCtrl
constant ModMeta (line 13) | ModMeta = uv.ModMeta
constant ModHyper (line 19) | ModHyper = uv.ModHyper
constant ModSuper (line 20) | ModSuper = uv.ModSuper
constant ModCapsLock (line 24) | ModCapsLock = uv.ModCapsLock
constant ModNumLock (line 25) | ModNumLock = uv.ModNumLock
constant ModScrollLock (line 26) | ModScrollLock = uv.ModScrollLock
FILE: mouse.go
constant MouseNone (line 30) | MouseNone = uv.MouseNone
constant MouseLeft (line 31) | MouseLeft = uv.MouseLeft
constant MouseMiddle (line 32) | MouseMiddle = uv.MouseMiddle
constant MouseRight (line 33) | MouseRight = uv.MouseRight
constant MouseWheelUp (line 34) | MouseWheelUp = uv.MouseWheelUp
constant MouseWheelDown (line 35) | MouseWheelDown = uv.MouseWheelDown
constant MouseWheelLeft (line 36) | MouseWheelLeft = uv.MouseWheelLeft
constant MouseWheelRight (line 37) | MouseWheelRight = uv.MouseWheelRight
constant MouseBackward (line 38) | MouseBackward = uv.MouseBackward
constant MouseForward (line 39) | MouseForward = uv.MouseForward
constant MouseButton10 (line 40) | MouseButton10 = uv.MouseButton10
constant MouseButton11 (line 41) | MouseButton11
type MouseMsg (line 46) | type MouseMsg interface
type Mouse (line 71) | type Mouse struct
method String (line 78) | func (m Mouse) String() (s string) {
type MouseClickMsg (line 83) | type MouseClickMsg
method String (line 86) | func (e MouseClickMsg) String() string {
method Mouse (line 93) | func (e MouseClickMsg) Mouse() Mouse {
type MouseReleaseMsg (line 98) | type MouseReleaseMsg
method String (line 101) | func (e MouseReleaseMsg) String() string {
method Mouse (line 108) | func (e MouseReleaseMsg) Mouse() Mouse {
type MouseWheelMsg (line 113) | type MouseWheelMsg
method String (line 116) | func (e MouseWheelMsg) String() string {
method Mouse (line 123) | func (e MouseWheelMsg) Mouse() Mouse {
type MouseMotionMsg (line 128) | type MouseMotionMsg
method String (line 131) | func (e MouseMotionMsg) String() string {
method Mouse (line 142) | func (e MouseMotionMsg) Mouse() Mouse {
FILE: nil_renderer.go
type nilRenderer (line 10) | type nilRenderer struct
method start (line 15) | func (n nilRenderer) start() {}
method clearScreen (line 18) | func (n nilRenderer) clearScreen() {}
method insertAbove (line 21) | func (n nilRenderer) insertAbove(string) error { return nil }
method resize (line 24) | func (n nilRenderer) resize(int, int) {}
method setColorProfile (line 27) | func (n nilRenderer) setColorProfile(colorprofile.Profile) {}
method flush (line 30) | func (nilRenderer) flush(bool) error { return nil }
method close (line 33) | func (nilRenderer) close() error { return nil }
method render (line 36) | func (nilRenderer) render(View) {}
method reset (line 39) | func (nilRenderer) reset() {}
method writeString (line 42) | func (nilRenderer) writeString(string) (int, error) { return 0, nil }
method setSyncdUpdates (line 45) | func (n nilRenderer) setSyncdUpdates(bool) {}
method setWidthMethod (line 48) | func (n nilRenderer) setWidthMethod(ansi.Method) {}
method onMouse (line 51) | func (n nilRenderer) onMouse(MouseMsg) Cmd {
FILE: options.go
type ProgramOption (line 17) | type ProgramOption
function WithContext (line 22) | func WithContext(ctx context.Context) ProgramOption {
function WithOutput (line 30) | func WithOutput(output io.Writer) ProgramOption {
function WithInput (line 40) | func WithInput(input io.Reader) ProgramOption {
function WithEnvironment (line 58) | func WithEnvironment(env []string) ProgramOption {
function WithoutSignalHandler (line 66) | func WithoutSignalHandler() ProgramOption {
function WithoutCatchPanics (line 76) | func WithoutCatchPanics() ProgramOption {
function WithoutSignals (line 84) | func WithoutSignals() ProgramOption {
function WithoutRenderer (line 98) | func WithoutRenderer() ProgramOption {
function WithFilter (line 133) | func WithFilter(filter func(Model, Msg) Msg) ProgramOption {
function WithFPS (line 142) | func WithFPS(fps int) ProgramOption {
function WithColorProfile (line 153) | func WithColorProfile(profile colorprofile.Profile) ProgramOption {
function WithWindowSize (line 163) | func WithWindowSize(width, height int) ProgramOption {
FILE: options_test.go
function TestOptions (line 11) | func TestOptions(t *testing.T) {
FILE: paste.go
type PasteMsg (line 5) | type PasteMsg struct
method String (line 10) | func (p PasteMsg) String() string {
type PasteStartMsg (line 16) | type PasteStartMsg struct
type PasteEndMsg (line 20) | type PasteEndMsg struct
FILE: profile.go
type ColorProfileMsg (line 13) | type ColorProfileMsg struct
FILE: raw.go
type RawMsg (line 5) | type RawMsg struct
function Raw (line 33) | func Raw(r any) Cmd {
FILE: renderer.go
constant defaultFPS (line 13) | defaultFPS = 60
constant maxFPS (line 14) | maxFPS = 120
type renderer (line 18) | type renderer interface
type printLineMessage (line 59) | type printLineMessage struct
function Println (line 70) | func Println(args ...any) Cmd {
function Printf (line 86) | func Printf(template string, args ...any) Cmd {
function encodeCursorStyle (line 96) | func encodeCursorStyle(style CursorShape, blink bool) int {
FILE: screen.go
type WindowSizeMsg (line 9) | type WindowSizeMsg struct
function ClearScreen (line 20) | func ClearScreen() Msg {
type clearScreenMsg (line 26) | type clearScreenMsg struct
type ModeReportMsg (line 62) | type ModeReportMsg struct
FILE: screen_test.go
type testViewOpts (line 12) | type testViewOpts struct
function testViewOptsCmds (line 21) | func testViewOptsCmds(opts ...testViewOpts) []Cmd {
type testViewModel (line 32) | type testViewModel struct
method Update (line 37) | func (m *testViewModel) Update(msg Msg) (Model, Cmd) {
method View (line 48) | func (m *testViewModel) View() View {
function TestViewModel (line 61) | func TestViewModel(t *testing.T) {
function TestClearMsg (line 160) | func TestClearMsg(t *testing.T) {
FILE: signals_unix.go
method listenForResize (line 15) | func (p *Program) listenForResize(done chan struct{}) {
FILE: signals_windows.go
method listenForResize (line 8) | func (p *Program) listenForResize(done chan struct{}) {
FILE: tea.go
type Model (line 53) | type Model interface
function NewView (line 76) | func NewView(s string) View {
type View (line 84) | type View struct
method SetContent (line 260) | func (v *View) SetContent(s string) {
type KeyboardEnhancements (line 241) | type KeyboardEnhancements struct
type MouseMode (line 265) | type MouseMode
constant MouseModeNone (line 269) | MouseModeNone MouseMode = iota
constant MouseModeCellMotion (line 278) | MouseModeCellMotion
constant MouseModeAllMotion (line 286) | MouseModeAllMotion
type ProgressBarState (line 290) | type ProgressBarState
method String (line 302) | func (s ProgressBarState) String() string {
constant ProgressBarNone (line 294) | ProgressBarNone ProgressBarState = iota
constant ProgressBarDefault (line 295) | ProgressBarDefault
constant ProgressBarError (line 296) | ProgressBarError
constant ProgressBarIndeterminate (line 297) | ProgressBarIndeterminate
constant ProgressBarWarning (line 298) | ProgressBarWarning
type ProgressBar (line 317) | type ProgressBar struct
function NewProgressBar (line 330) | func NewProgressBar(state ProgressBarState, value int) *ProgressBar {
type Cursor (line 338) | type Cursor struct
function NewCursor (line 355) | func NewCursor(x, y int) *Cursor {
type Cmd (line 371) | type Cmd
type channelHandlers (line 376) | type channelHandlers struct
method add (line 383) | func (h *channelHandlers) add(ch chan struct{}) {
method shutdown (line 390) | func (h *channelHandlers) shutdown() {
type Program (line 407) | type Program struct
method handleSignals (line 624) | func (p *Program) handleSignals() chan struct{} {
method handleResize (line 666) | func (p *Program) handleResize() chan struct{} {
method handleCommands (line 681) | func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
method eventLoop (line 724) | func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
method render (line 867) | func (p *Program) render(model Model) {
method execSequenceMsg (line 873) | func (p *Program) execSequenceMsg(msg sequenceMsg) {
method execBatchMsg (line 899) | func (p *Program) execBatchMsg(msg BatchMsg) {
method Run (line 972) | func (p *Program) Run() (returnModel Model, returnErr error) {
method Send (line 1164) | func (p *Program) Send(msg Msg) {
method Quit (line 1178) | func (p *Program) Quit() {
method Kill (line 1185) | func (p *Program) Kill() {
method Wait (line 1190) | func (p *Program) Wait() {
method execute (line 1195) | func (p *Program) execute(seq string) {
method flush (line 1202) | func (p *Program) flush() error {
method shutdown (line 1222) | func (p *Program) shutdown(kill bool) {
method recoverFromPanic (line 1250) | func (p *Program) recoverFromPanic(r interface{}) {
method recoverFromGoPanic (line 1275) | func (p *Program) recoverFromGoPanic(r interface{}) {
method ReleaseTerminal (line 1300) | func (p *Program) ReleaseTerminal() error {
method releaseTerminal (line 1304) | func (p *Program) releaseTerminal(reset bool) error {
method RestoreTerminal (line 1325) | func (p *Program) RestoreTerminal() error {
method Println (line 1351) | func (p *Program) Println(args ...any) {
method Printf (line 1365) | func (p *Program) Printf(template string, args ...any) {
method startRenderer (line 1372) | func (p *Program) startRenderer() {
method stopRenderer (line 1406) | func (p *Program) stopRenderer(kill bool) {
function Quit (line 536) | func Quit() Msg {
type QuitMsg (line 542) | type QuitMsg struct
function Suspend (line 545) | func Suspend() Msg {
type SuspendMsg (line 555) | type SuspendMsg struct
type ResumeMsg (line 559) | type ResumeMsg struct
type InterruptMsg (line 567) | type InterruptMsg struct
function Interrupt (line 571) | func Interrupt() Msg {
function NewProgram (line 576) | func NewProgram(model Model, opts ...ProgramOption) *Program {
function shouldQuerySynchronizedOutput (line 953) | func shouldQuerySynchronizedOutput(environ uv.Environ) bool {
FILE: tea_test.go
type ctxImplodeMsg (line 15) | type ctxImplodeMsg struct
type incrementMsg (line 19) | type incrementMsg struct
type panicMsg (line 21) | type panicMsg struct
function panicCmd (line 23) | func panicCmd() Msg {
type testModel (line 27) | type testModel struct
method Init (line 32) | func (m *testModel) Init() Cmd {
method Update (line 36) | func (m *testModel) Update(msg Msg) (Model, Cmd) {
method View (line 63) | func (m *testModel) View() View {
function TestTeaModel (line 68) | func TestTeaModel(t *testing.T) {
function TestTeaQuit (line 90) | func TestTeaQuit(t *testing.T) {
function TestTeaWaitQuit (line 114) | func TestTeaWaitQuit(t *testing.T) {
function TestTeaWaitKill (line 167) | func TestTeaWaitKill(t *testing.T) {
function TestTeaWithFilter (line 220) | func TestTeaWithFilter(t *testing.T) {
function testTeaWithFilter (line 226) | func testTeaWithFilter(t *testing.T, preventCount uint32) {
function TestTeaKill (line 262) | func TestTeaKill(t *testing.T) {
function TestTeaContext (line 294) | func TestTeaContext(t *testing.T) {
function TestTeaContextImplodeDeadlock (line 327) | func TestTeaContextImplodeDeadlock(t *testing.T) {
function TestTeaContextBatchDeadlock (line 353) | func TestTeaContextBatchDeadlock(t *testing.T) {
function TestTeaBatchMsg (line 388) | func TestTeaBatchMsg(t *testing.T) {
function TestTeaSequenceMsg (line 423) | func TestTeaSequenceMsg(t *testing.T) {
function TestTeaSequenceMsgWithBatchMsg (line 447) | func TestTeaSequenceMsgWithBatchMsg(t *testing.T) {
function TestTeaNestedSequenceMsg (line 474) | func TestTeaNestedSequenceMsg(t *testing.T) {
function TestTeaSend (line 498) | func TestTeaSend(t *testing.T) {
function TestTeaNoRun (line 519) | func TestTeaNoRun(t *testing.T) {
function TestTeaPanic (line 530) | func TestTeaPanic(t *testing.T) {
function TestTeaGoroutinePanic (line 560) | func TestTeaGoroutinePanic(t *testing.T) {
type benchModel (line 595) | type benchModel struct
method Init (line 599) | func (m benchModel) Init() Cmd {
method Update (line 603) | func (m benchModel) Update(msg Msg) (Model, Cmd) {
method View (line 615) | func (m benchModel) View() View {
function BenchmarkTeaRun (line 627) | func BenchmarkTeaRun(b *testing.B) {
FILE: termcap.go
type requestCapabilityMsg (line 5) | type requestCapabilityMsg
function RequestCapability (line 30) | func RequestCapability(s string) Cmd {
type CapabilityMsg (line 41) | type CapabilityMsg struct
method String (line 46) | func (c CapabilityMsg) String() string {
FILE: termios_bsd.go
method checkOptimizedMovements (line 11) | func (p *Program) checkOptimizedMovements(s *term.State) {
FILE: termios_other.go
method checkOptimizedMovements (line 8) | func (*Program) checkOptimizedMovements(*term.State) {}
FILE: termios_unix.go
method checkOptimizedMovements (line 11) | func (p *Program) checkOptimizedMovements(s *term.State) {
FILE: termios_windows.go
method checkOptimizedMovements (line 8) | func (p *Program) checkOptimizedMovements(*term.State) {
FILE: tty.go
method suspend (line 12) | func (p *Program) suspend() {
method initTerminal (line 24) | func (p *Program) initTerminal() error {
method restoreTerminalState (line 33) | func (p *Program) restoreTerminalState() error {
method restoreInput (line 41) | func (p *Program) restoreInput() error {
method initInputReader (line 56) | func (p *Program) initInputReader(cancel bool) error {
method readLoop (line 84) | func (p *Program) readLoop() {
method waitForReadLoop (line 97) | func (p *Program) waitForReadLoop() {
method checkResize (line 109) | func (p *Program) checkResize() {
function OpenTTY (line 130) | func OpenTTY() (*os.File, *os.File, error) {
FILE: tty_unix.go
method initInput (line 15) | func (p *Program) initInput() (err error) {
constant suspendSupported (line 37) | suspendSupported = true
function suspendProcess (line 40) | func suspendProcess() {
FILE: tty_windows.go
method initInput (line 13) | func (p *Program) initInput() (err error) {
constant suspendSupported (line 62) | suspendSupported = false
function suspendProcess (line 64) | func suspendProcess() {}
FILE: tutorials/basics/main.go
type model (line 10) | type model struct
method Init (line 28) | func (m model) Init() tea.Cmd {
method Update (line 32) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 59) | func (m model) View() tea.View {
function initialModel (line 16) | func initialModel() model {
function main (line 84) | func main() {
FILE: tutorials/commands/main.go
constant url (line 12) | url = "https://charm.sh/"
type model (line 14) | type model struct
method Init (line 38) | func (m model) Init() tea.Cmd {
method Update (line 42) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method View (line 61) | func (m model) View() tea.View {
function checkServer (line 19) | func checkServer() tea.Msg {
type statusMsg (line 30) | type statusMsg
type errMsg (line 32) | type errMsg struct
method Error (line 36) | func (e errMsg) Error() string { return e.err.Error() }
function main (line 73) | func main() {
FILE: xterm.go
type TerminalVersionMsg (line 4) | type TerminalVersionMsg struct
method String (line 9) | func (t TerminalVersionMsg) String() string {
type terminalVersion (line 15) | type terminalVersion struct
function RequestTerminalVersion (line 20) | func RequestTerminalVersion() Msg {
Condensed preview — 191 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (465K chars).
[
{
"path": ".gitattributes",
"chars": 15,
"preview": "*.golden -text\n"
},
{
"path": ".github/CODEOWNERS",
"chars": 30,
"preview": "* @meowgorithm @aymanbagabas\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug.yml",
"chars": 1705,
"preview": "name: Bug Report\ndescription: File a bug report\nlabels: [bug]\nbody:\n - type: markdown\n attributes:\n value: |\n "
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 828,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 120,
"preview": "blank_issues_enabled: true\ncontact_links:\n- name: Discord\n url: https://charm.sh/discord\n about: Chat on our Discord.\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 604,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
},
{
"path": ".github/dependabot.yml",
"chars": 1775,
"preview": "version: 2\n\nupdates:\n - package-ecosystem: \"gomod\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n day:"
},
{
"path": ".github/workflows/build.yml",
"chars": 446,
"preview": "name: build\non: [push, pull_request]\n\njobs:\n build:\n uses: charmbracelet/meta/.github/workflows/build.yml@main\n\n bu"
},
{
"path": ".github/workflows/coverage.yml",
"chars": 643,
"preview": "name: coverage\non: [push, pull_request]\n\njobs:\n coverage:\n strategy:\n matrix:\n go-version: [^1]\n "
},
{
"path": ".github/workflows/dependabot-sync.yml",
"chars": 419,
"preview": "name: dependabot-sync\non:\n schedule:\n - cron: \"0 0 * * 0\" # every Sunday at midnight\n workflow_dispatch: # allows m"
},
{
"path": ".github/workflows/examples.yml",
"chars": 881,
"preview": "name: examples\n\non:\n push:\n branches:\n - 'master'\n paths:\n - '.github/workflows/examples.yml'\n - '"
},
{
"path": ".github/workflows/lint-sync.yml",
"chars": 271,
"preview": "name: lint-sync\non:\n schedule:\n # every Sunday at midnight\n - cron: \"0 0 * * 0\"\n workflow_dispatch: # allows man"
},
{
"path": ".github/workflows/lint.yml",
"chars": 208,
"preview": "name: lint\non:\n push:\n pull_request:\n\njobs:\n lint:\n uses: charmbracelet/meta/.github/workflows/lint.yml@main\n w"
},
{
"path": ".github/workflows/release.yml",
"chars": 1106,
"preview": "name: goreleaser\n\non:\n push:\n tags:\n - v*.*.*\n\nconcurrency:\n group: goreleaser\n cancel-in-progress: true\n\njob"
},
{
"path": ".gitignore",
"chars": 525,
"preview": ".DS_Store\n.envrc\n\nexamples/fullscreen/fullscreen\nexamples/help/help\nexamples/http/http\nexamples/list-default/list-defaul"
},
{
"path": ".golangci.yml",
"chars": 695,
"preview": "version: \"2\"\nrun:\n tests: false\nlinters:\n enable:\n - bodyclose\n - exhaustive\n - goconst\n - godot\n - gom"
},
{
"path": ".goreleaser.yml",
"chars": 168,
"preview": "# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json\nversion: 2\nincludes:\n - from_url:\n u"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "MIT License\n\nCopyright (c) 2020-2026 Charmbracelet, Inc.\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 14791,
"preview": "# Bubble Tea\n\n<p>\n <img src=\"https://github.com/user-attachments/assets/ad408275-8799-488f-9303-441e7f869535\" width=\""
},
{
"path": "Taskfile.yaml",
"chars": 183,
"preview": "# https://taskfile.dev\n\nversion: '3'\n\ntasks:\n lint:\n desc: Run lint\n cmds:\n - golangci-lint run\n\n test:\n "
},
{
"path": "UPGRADE_GUIDE_V2.md",
"chars": 17115,
"preview": "# Bubble Tea v2 Upgrade Guide\n\nThis guide covers everything you need to change when upgrading from Bubble Tea v1 to v2. "
},
{
"path": "clipboard.go",
"chars": 2140,
"preview": "package tea\n\n// ClipboardMsg is a clipboard read message event. This message is emitted when\n// a terminal receives an O"
},
{
"path": "color.go",
"chars": 2824,
"preview": "package tea\n\nimport (\n\t\"image/color\"\n\n\tuv \"github.com/charmbracelet/ultraviolet\"\n)\n\n// backgroundColorMsg is a message t"
},
{
"path": "commands.go",
"chars": 4967,
"preview": "package tea\n\nimport (\n\t\"time\"\n)\n\n// Batch performs a bunch of commands concurrently with no ordering guarantees\n// about"
},
{
"path": "commands_test.go",
"chars": 1312,
"preview": "package tea\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestEvery(t *testing.T) {\n\texpected := \"every ms\"\n\tmsg := Every(time.Mi"
},
{
"path": "cursed_renderer.go",
"chars": 25785,
"preview": "package tea\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image/color\"\n\t\"io\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/charmbracelet/col"
},
{
"path": "cursor.go",
"chars": 701,
"preview": "package tea\n\n// Position represents a position in the terminal.\ntype Position struct{ X, Y int }\n\n// CursorPositionMsg i"
},
{
"path": "environ.go",
"chars": 1223,
"preview": "package tea\n\nimport uv \"github.com/charmbracelet/ultraviolet\"\n\n// EnvMsg is a message that represents the environment va"
},
{
"path": "examples/README.md",
"chars": 7703,
"preview": "# Examples\n\n### Alt Screen Toggle\n\nThe `altscreen-toggle` example shows how to transition between the alternative\nscreen"
},
{
"path": "examples/altscreen-toggle/README.md",
"chars": 70,
"preview": "# Alt Screen Toggle\n\n<img width=\"800\" src=\"./altscreen-toggle.gif\" />\n"
},
{
"path": "examples/altscreen-toggle/main.go",
"chars": 1558,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nvar (\n\tkeywordStyle = l"
},
{
"path": "examples/autocomplete/main.go",
"chars": 3891,
"preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/"
},
{
"path": "examples/canvas/main.go",
"chars": 1662,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/charmbracelet"
},
{
"path": "examples/capability/main.go",
"chars": 1558,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/bubbles/v2/textinput\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipg"
},
{
"path": "examples/cellbuffer/main.go",
"chars": 3888,
"preview": "package main\n\n// A simple example demonstrating how to draw and animate on a cellular grid.\n// Note that the cellbuffer "
},
{
"path": "examples/chat/README.md",
"chars": 45,
"preview": "# Chat\n\n<img width=\"800\" src=\"./chat.gif\" />\n"
},
{
"path": "examples/chat/main.go",
"chars": 2850,
"preview": "package main\n\n// A simple program demonstrating the text area component from the Bubbles\n// component library.\n\nimport ("
},
{
"path": "examples/clickable/main.go",
"chars": 7841,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/segmentio/ksu"
},
{
"path": "examples/clickable/words.go",
"chars": 2210,
"preview": "package main\n\nimport (\n\t\"math/rand\"\n\t\"strings\"\n\t\"sync\"\n)\n\nconst uncapitalized = \" of a an and ’n’ \"\n\nvar (\n\tadjectives ="
},
{
"path": "examples/colorprofile/main.go",
"chars": 1135,
"preview": "package main\n\nimport (\n\t\"image/color\"\n\t\"log\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"github.com/charmbracelet/colorprofile\"\n\t\""
},
{
"path": "examples/composable-views/README.md",
"chars": 69,
"preview": "# Composable Views\n\n<img width=\"800\" src=\"./composable-views.gif\" />\n"
},
{
"path": "examples/composable-views/main.go",
"chars": 3867,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/spinner\"\n\t\"charm.land/bubbles/v2/timer\""
},
{
"path": "examples/cursor-style/main.go",
"chars": 1404,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\ntype model struct {\n\tcursor tea.Cursor\n\tblink bo"
},
{
"path": "examples/debounce/README.md",
"chars": 53,
"preview": "# Debounce\n\n<img width=\"800\" src=\"./debounce.gif\" />\n"
},
{
"path": "examples/debounce/main.go",
"chars": 1677,
"preview": "package main\n\n// This example illustrates how to debounce commands.\n//\n// When the user presses a key we increment the \""
},
{
"path": "examples/doom-fire/main.go",
"chars": 2833,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n"
},
{
"path": "examples/exec/README.md",
"chars": 45,
"preview": "# Exec\n\n<img width=\"800\" src=\"./exec.gif\" />\n"
},
{
"path": "examples/exec/main.go",
"chars": 1310,
"preview": "package main\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\ntype editorFinishedMsg struct{ "
},
{
"path": "examples/eyes/main.go",
"chars": 4417,
"preview": "// roughly converted to Go from https://github.com/dmtrKovalenko/esp32-smooth-eye-blinking/blob/main/src/main.cpp\npackag"
},
{
"path": "examples/file-picker/main.go",
"chars": 2150,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/filepicker\"\n\ttea \"charm.land/b"
},
{
"path": "examples/focus-blur/main.go",
"chars": 1126,
"preview": "package main\n\n// A simple program that handled losing and acquiring focus.\n\nimport (\n\t\"log\"\n\n\ttea \"charm.land/bubbletea/"
},
{
"path": "examples/fullscreen/README.md",
"chars": 58,
"preview": "# Full Screen\n\n<img width=\"800\" src=\"./fullscreen.gif\" />\n"
},
{
"path": "examples/fullscreen/main.go",
"chars": 938,
"preview": "package main\n\n// A simple program that opens the alternate screen buffer then counts down\n// from 5 and then exits.\n\nimp"
},
{
"path": "examples/glamour/README.md",
"chars": 51,
"preview": "# Glamour\n\n<img width=\"800\" src=\"./glamour.gif\" />\n"
},
{
"path": "examples/glamour/main.go",
"chars": 3492,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"github.com/charm"
},
{
"path": "examples/go.mod",
"chars": 1939,
"preview": "module examples\n\ngo 1.25.2\n\nreplace charm.land/bubbletea/v2 => ../\n\nrequire (\n\tcharm.land/bubbles/v2 v2.0.0\n\tcharm.land/"
},
{
"path": "examples/go.sum",
"chars": 8352,
"preview": "charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=\ncharm.land/bubbles/v2 v2.0.0/go.mod h1:rCHo"
},
{
"path": "examples/help/README.md",
"chars": 45,
"preview": "# Help\n\n<img width=\"800\" src=\"./help.gif\" />\n"
},
{
"path": "examples/help/main.go",
"chars": 3272,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/key\"\n\ttea \"charm.l"
},
{
"path": "examples/http/README.md",
"chars": 45,
"preview": "# HTTP\n\n<img width=\"800\" src=\"./http.gif\" />\n"
},
{
"path": "examples/http/main.go",
"chars": 1374,
"preview": "package main\n\n// A simple program that makes a GET request and prints the response status.\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net"
},
{
"path": "examples/isbn-form/isbn-form.tape",
"chars": 207,
"preview": "Type \"go run ./isbn-form\"\nEnter\nSleep 2s\nType \"9783548372570\"\nSleep 500ms\nSleep 1s\nDown\nSleep 500ms\nType \"my book title\""
},
{
"path": "examples/isbn-form/main.go",
"chars": 4914,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"charm.land/bubbles/v2/textinput\"\n\ttea \"charm.land/bubblete"
},
{
"path": "examples/keyboard-enhancements/main.go",
"chars": 3191,
"preview": "package main\n\n// This is a simple example illustrating how to enable enhanced keyboard\n// support.\n\nimport (\n\t\"fmt\"\n\t\"os"
},
{
"path": "examples/list-default/README.md",
"chars": 61,
"preview": "# Default List\n\n<img width=\"800\" src=\"./list-default.gif\" />\n"
},
{
"path": "examples/list-default/main.go",
"chars": 2709,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/bubbles/v2/list\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/"
},
{
"path": "examples/list-fancy/README.md",
"chars": 57,
"preview": "# Fancy List\n\n<img width=\"800\" src=\"./list-fancy.gif\" />\n"
},
{
"path": "examples/list-fancy/delegate.go",
"chars": 1821,
"preview": "package main\n\nimport (\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/list\"\n\ttea \"charm.land/bubbletea/v2\"\n)\n\nfunc"
},
{
"path": "examples/list-fancy/main.go",
"chars": 5215,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/list\"\n\ttea \"charm.land"
},
{
"path": "examples/list-fancy/randomitems.go",
"chars": 2430,
"preview": "package main\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n)\n\ntype randomItemGenerator struct {\n\ttitles []string\n\tdescs []stri"
},
{
"path": "examples/list-simple/README.md",
"chars": 59,
"preview": "# Simple List\n\n<img width=\"800\" src=\"./list-simple.gif\" />\n"
},
{
"path": "examples/list-simple/main.go",
"chars": 3394,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/list\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"cha"
},
{
"path": "examples/mouse/main.go",
"chars": 834,
"preview": "package main\n\n// A simple program that opens the alternate screen buffer and displays mouse\n// coordinates and events.\n\n"
},
{
"path": "examples/package-manager/README.md",
"chars": 67,
"preview": "# Package Manager\n\n<img width=\"800\" src=\"./package-manager.gif\" />\n"
},
{
"path": "examples/package-manager/main.go",
"chars": 3335,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/progress\"\n\t\"charm.land/bubb"
},
{
"path": "examples/package-manager/packages.go",
"chars": 835,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nvar packages = []string{\n\t\"vegeutils\",\n\t\"libgardening\",\n\t\"currykit\",\n\t\"spi"
},
{
"path": "examples/pager/README.md",
"chars": 47,
"preview": "# Pager\n\n<img width=\"800\" src=\"./pager.gif\" />\n"
},
{
"path": "examples/pager/artichoke.md",
"chars": 1577,
"preview": "Glow\n====\n\nA casual introduction. 你好世界!\n\n## Let’s talk about artichokes\n\nThe _artichoke_ is mentioned as a garden plant "
},
{
"path": "examples/pager/main.go",
"chars": 3756,
"preview": "package main\n\n// An example program demonstrating the pager component from the Bubbles\n// component library.\n\nimport (\n\t"
},
{
"path": "examples/paginator/README.md",
"chars": 55,
"preview": "# Paginator\n\n<img width=\"800\" src=\"./paginator.gif\" />\n"
},
{
"path": "examples/paginator/main.go",
"chars": 2144,
"preview": "package main\n\n// A simple program demonstrating the paginator component from the Bubbles\n// component library.\n\nimport ("
},
{
"path": "examples/pipe/README.md",
"chars": 45,
"preview": "# Pipe\n\n<img width=\"800\" src=\"./pipe.gif\" />\n"
},
{
"path": "examples/pipe/main.go",
"chars": 1795,
"preview": "package main\n\n// An example illustrating how to pipe in data to a Bubble Tea application.\n// More so, this serves as pro"
},
{
"path": "examples/prevent-quit/main.go",
"chars": 3232,
"preview": "package main\n\n// A program demonstrating how to use the WithFilter option to intercept events.\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n"
},
{
"path": "examples/print-key/main.go",
"chars": 1063,
"preview": "package main\n\nimport (\n\t\"log\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\ntype model struct{}\n\nfunc (m model) Init() tea.Cmd {\n\tr"
},
{
"path": "examples/progress-animated/README.md",
"chars": 71,
"preview": "# Animated Progress\n\n<img width=\"800\" src=\"./progress-animated.gif\" />\n"
},
{
"path": "examples/progress-animated/main.go",
"chars": 1989,
"preview": "package main\n\n// A simple example that shows how to render an animated progress bar. In this\n// example we bump the prog"
},
{
"path": "examples/progress-bar/main.go",
"chars": 1381,
"preview": "package main\n\nimport (\n\t\"log\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nvar body = lipgloss.NewStyle("
},
{
"path": "examples/progress-download/README.md",
"chars": 1029,
"preview": "# Download Progress\n\nThis example demonstrates how to download a file from a URL and show its\nprogress with a [Progress "
},
{
"path": "examples/progress-download/main.go",
"chars": 2157,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"charm.land/bubbles/v2/progress\""
},
{
"path": "examples/progress-download/tui.go",
"chars": 1624,
"preview": "package main\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/progress\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land"
},
{
"path": "examples/progress-static/README.md",
"chars": 67,
"preview": "# Static Progress\n\n<img width=\"800\" src=\"./progress-static.gif\" />\n"
},
{
"path": "examples/progress-static/main.go",
"chars": 2133,
"preview": "package main\n\n// A simple example that shows how to render a progress bar in a \"pure\"\n// fashion. In this example we bum"
},
{
"path": "examples/query-term/main.go",
"chars": 2011,
"preview": "// This example uses a textinput to send the terminal ANSI sequences to query\n// it for capabilities.\npackage main\n\nimpo"
},
{
"path": "examples/realtime/README.md",
"chars": 54,
"preview": "# Real Time\n\n<img width=\"800\" src=\"./realtime.gif\" />\n"
},
{
"path": "examples/realtime/main.go",
"chars": 2182,
"preview": "package main\n\n// A simple example that shows how to send activity to Bubble Tea in real-time\n// through a channel.\n\nimpo"
},
{
"path": "examples/result/README.md",
"chars": 49,
"preview": "# Result\n\n<img width=\"800\" src=\"./result.gif\" />\n"
},
{
"path": "examples/result/main.go",
"chars": 1570,
"preview": "package main\n\n// A simple example that shows how to retrieve a value from a Bubble Tea\n// program after the Bubble Tea h"
},
{
"path": "examples/send-msg/README.md",
"chars": 53,
"preview": "# Send Msg\n\n<img width=\"800\" src=\"./send-msg.gif\" />\n"
},
{
"path": "examples/send-msg/main.go",
"chars": 2812,
"preview": "package main\n\n// A simple example that shows how to send messages to a Bubble Tea program\n// from outside the program us"
},
{
"path": "examples/sequence/README.md",
"chars": 53,
"preview": "# Sequence\n\n<img width=\"800\" src=\"./sequence.gif\" />\n"
},
{
"path": "examples/sequence/main.go",
"chars": 1412,
"preview": "package main\n\n// A simple example illustrating how to run a series of commands in order.\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n"
},
{
"path": "examples/set-terminal-color/main.go",
"chars": 3541,
"preview": "package main\n\nimport (\n\t\"image/color\"\n\t\"log\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/textinput\"\n\ttea \"charm.land/bubbletea/v"
},
{
"path": "examples/set-window-title/main.go",
"chars": 791,
"preview": "package main\n\n// A simple example illustrating how to set a window title.\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbl"
},
{
"path": "examples/simple/README.md",
"chars": 49,
"preview": "# Simple\n\n<img width=\"800\" src=\"./simple.gif\" />\n"
},
{
"path": "examples/simple/main.go",
"chars": 1947,
"preview": "package main\n\n// A simple program that counts down from 5 and then exits.\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\ttea \"c"
},
{
"path": "examples/simple/main_test.go",
"chars": 2239,
"preview": "package main\n\n/*\nfunc TestApp(t *testing.T) {\n\t// TODO: Enable this test again\n\t// Since we added colorprofile.Writer to"
},
{
"path": "examples/simple/testdata/TestApp.golden",
"chars": 180,
"preview": "\u001b[?2004h\u001b[?25l\u001b[=1;1u\u001b[?u\rHi. This program will exit in 10 seconds.\n\nTo quit sooner press ctrl-c, or press ctrl-z to sus"
},
{
"path": "examples/space/main.go",
"chars": 2671,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"math/rand\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"cha"
},
{
"path": "examples/spinner/README.md",
"chars": 51,
"preview": "# Spinner\n\n<img width=\"800\" src=\"./spinner.gif\" />\n"
},
{
"path": "examples/spinner/main.go",
"chars": 1307,
"preview": "package main\n\n// A simple program demonstrating the spinner component from the Bubbles\n// component library.\n\nimport (\n\t"
},
{
"path": "examples/spinners/README.md",
"chars": 53,
"preview": "# Spinners\n\n<img width=\"800\" src=\"./spinners.gif\" />\n"
},
{
"path": "examples/spinners/main.go",
"chars": 1898,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/bubbles/v2/spinner\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipglo"
},
{
"path": "examples/splash/main.go",
"chars": 3930,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lip"
},
{
"path": "examples/split-editors/README.md",
"chars": 63,
"preview": "# Split Editors\n\n<img width=\"800\" src=\"./split-editors.gif\" />\n"
},
{
"path": "examples/split-editors/main.go",
"chars": 5143,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/"
},
{
"path": "examples/stopwatch/README.md",
"chars": 55,
"preview": "# Stopwatch\n\n<img width=\"800\" src=\"./stopwatch.gif\" />\n"
},
{
"path": "examples/stopwatch/main.go",
"chars": 2148,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bub"
},
{
"path": "examples/suspend/main.go",
"chars": 981,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\ntype model struct {\n\tquitting bool\n\ts"
},
{
"path": "examples/table/README.md",
"chars": 47,
"preview": "# Table\n\n<img width=\"800\" src=\"./table.gif\" />\n"
},
{
"path": "examples/table/main.go",
"chars": 6252,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"charm.land/bubbles/v2/table\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss"
},
{
"path": "examples/table-resize/main.go",
"chars": 4607,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"charm.lan"
},
{
"path": "examples/tabs/README.md",
"chars": 45,
"preview": "# Tabs\n\n<img width=\"800\" src=\"./tabs.gif\" />\n"
},
{
"path": "examples/tabs/main.go",
"chars": 3160,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\ntype styles "
},
{
"path": "examples/textarea/README.md",
"chars": 54,
"preview": "# Text Area\n\n<img width=\"800\" src=\"./textarea.gif\" />\n"
},
{
"path": "examples/textarea/main.go",
"chars": 2047,
"preview": "package main\n\n// A simple program demonstrating the textarea component from the Bubbles\n// component library.\n\nimport (\n"
},
{
"path": "examples/textinput/README.md",
"chars": 56,
"preview": "# Text Input\n\n<img width=\"800\" src=\"./textinput.gif\" />\n"
},
{
"path": "examples/textinput/main.go",
"chars": 1471,
"preview": "package main\n\n// A simple program demonstrating the text input component from the Bubbles\n// component library.\n\nimport "
},
{
"path": "examples/textinputs/README.md",
"chars": 58,
"preview": "# Text Inputs\n\n<img width=\"800\" src=\"./textinputs.gif\" />\n"
},
{
"path": "examples/textinputs/main.go",
"chars": 4253,
"preview": "package main\n\n// A simple example demonstrating the use of multiple text input components\n// from the Bubbles component "
},
{
"path": "examples/timer/README.md",
"chars": 47,
"preview": "# Timer\n\n<img width=\"800\" src=\"./timer.gif\" />\n"
},
{
"path": "examples/timer/main.go",
"chars": 2374,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/help\"\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bub"
},
{
"path": "examples/tui-daemon-combo/README.md",
"chars": 63,
"preview": "# TUI Daemon\n\n<img width=\"800\" src=\"./tui-daemon-combo.gif\" />\n"
},
{
"path": "examples/tui-daemon-combo/main.go",
"chars": 2869,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math/rand\"\n\t\"os\"\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/spinner\"\n\ttea \"ch"
},
{
"path": "examples/vanish/main.go",
"chars": 596,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\ntype model bool\n\nfunc (m model) Init() tea.Cmd {\n"
},
{
"path": "examples/views/README.md",
"chars": 47,
"preview": "# Views\n\n<img width=\"800\" src=\"./views.gif\" />\n"
},
{
"path": "examples/views/main.go",
"chars": 6679,
"preview": "package main\n\n// An example demonstrating an application with multiple views.\n//\n// Note that this example was produced "
},
{
"path": "examples/window-size/main.go",
"chars": 801,
"preview": "package main\n\n// A simple program that queries and displays the window-size.\n\nimport (\n\t\"log\"\n\n\ttea \"charm.land/bubblete"
},
{
"path": "exec.go",
"chars": 3553,
"preview": "package tea\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n)\n\n// execMsg is used internally to run an ExecCommand sent with Exec.\ntype"
},
{
"path": "exec_test.go",
"chars": 1751,
"preview": "package tea\n\nimport (\n\t\"bytes\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"testing\"\n)\n\ntype execFinishedMsg struct{ err error }\n\ntype testEx"
},
{
"path": "focus.go",
"chars": 247,
"preview": "package tea\n\n// FocusMsg represents a terminal focus message.\n// This occurs when the terminal gains focus.\ntype FocusMs"
},
{
"path": "go.mod",
"chars": 1059,
"preview": "module charm.land/bubbletea/v2\n\nretract v2.0.0-beta1 // We add a \".\" after the \"beta\" in the version number.\n\ngo 1.24.6\n"
},
{
"path": "go.sum",
"chars": 3611,
"preview": "github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=\ngithub.com/aymanbagabas/go-udiff"
},
{
"path": "input.go",
"chars": 1351,
"preview": "package tea\n\nimport (\n\tuv \"github.com/charmbracelet/ultraviolet\"\n)\n\n// translateInputEvent translates an input event int"
},
{
"path": "key.go",
"chars": 10262,
"preview": "package tea\n\nimport (\n\t\"fmt\"\n\n\tuv \"github.com/charmbracelet/ultraviolet\"\n)\n\nconst (\n\t// KeyExtended is a special key cod"
},
{
"path": "keyboard.go",
"chars": 1238,
"preview": "package tea\n\nimport (\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\n// KeyboardEnhancementsMsg is a message that gets sent when "
},
{
"path": "logging.go",
"chars": 1389,
"preview": "package tea\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"unicode\"\n)\n\n// LogToFile sets up default logging to log to a file. Thi"
},
{
"path": "logging_test.go",
"chars": 532,
"preview": "package tea\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestLogToFile(t *testing.T) {\n\tpath := filepath.J"
},
{
"path": "mod.go",
"chars": 652,
"preview": "package tea\n\nimport uv \"github.com/charmbracelet/ultraviolet\"\n\n// KeyMod represents modifier keys.\ntype KeyMod = uv.KeyM"
},
{
"path": "mouse.go",
"chars": 3871,
"preview": "package tea\n\nimport (\n\t\"fmt\"\n\n\tuv \"github.com/charmbracelet/ultraviolet\"\n)\n\n// MouseButton represents the button that wa"
},
{
"path": "nil_renderer.go",
"chars": 1473,
"preview": "package tea\n\nimport (\n\t\"github.com/charmbracelet/colorprofile\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\n// nilRenderer is a"
},
{
"path": "options.go",
"chars": 5285,
"preview": "package tea\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"sync/atomic\"\n\n\t\"github.com/charmbracelet/colorprofile\"\n)\n\n// ProgramOption is u"
},
{
"path": "options_test.go",
"chars": 2412,
"preview": "package tea\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"sync/atomic\"\n\t\"testing\"\n)\n\nfunc TestOptions(t *testing.T) {\n\tt.Run(\"ou"
},
{
"path": "paste.go",
"chars": 520,
"preview": "package tea\n\n// PasteMsg is an message that is emitted when a terminal receives pasted text\n// using bracketed-paste.\nty"
},
{
"path": "profile.go",
"chars": 547,
"preview": "package tea\n\nimport \"github.com/charmbracelet/colorprofile\"\n\n// ColorProfileMsg is a message that describes the terminal"
},
{
"path": "raw.go",
"chars": 967,
"preview": "package tea\n\n// RawMsg is a message that contains a string to be printed to the terminal\n// without any intermediate pro"
},
{
"path": "renderer.go",
"chars": 2653,
"preview": "package tea\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/colorprofile\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\nconst (\n\t//"
},
{
"path": "screen.go",
"chars": 2072,
"preview": "package tea\n\nimport \"github.com/charmbracelet/x/ansi\"\n\n// WindowSizeMsg is used to report the terminal size. It's sent t"
},
{
"path": "screen_test.go",
"chars": 3986,
"preview": "package tea\n\nimport (\n\t\"bytes\"\n\t\"image/color\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/colorprofile\"\n\t\"github.com/charmbra"
},
{
"path": "signals_unix.go",
"chars": 662,
"preview": "//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos\n// +build darwin dragon"
},
{
"path": "signals_windows.go",
"chars": 225,
"preview": "//go:build windows\n// +build windows\n\npackage tea\n\n// listenForResize is not available on windows because windows does n"
},
{
"path": "tea.go",
"chars": 39829,
"preview": "// Package tea provides a framework for building rich terminal user interfaces\n// based on the paradigms of The Elm Arch"
},
{
"path": "tea_test.go",
"chars": 11243,
"preview": "package tea\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype c"
},
{
"path": "termcap.go",
"chars": 1543,
"preview": "package tea\n\n// requestCapabilityMsg is an internal message that requests the terminal to\n// send its Termcap/Terminfo r"
},
{
"path": "termios_bsd.go",
"chars": 257,
"preview": "//go:build dragonfly || freebsd\n// +build dragonfly freebsd\n\npackage tea\n\nimport (\n\t\"github.com/charmbracelet/x/term\"\n\t\""
},
{
"path": "termios_other.go",
"chars": 267,
"preview": "//go:build !windows && !darwin && !dragonfly && !freebsd && !linux && !solaris && !aix\n// +build !windows,!darwin,!drago"
},
{
"path": "termios_unix.go",
"chars": 326,
"preview": "//go:build darwin || linux || solaris || aix\n// +build darwin linux solaris aix\n\npackage tea\n\nimport (\n\t\"github.com/char"
},
{
"path": "termios_windows.go",
"chars": 197,
"preview": "//go:build windows\n// +build windows\n\npackage tea\n\nimport \"github.com/charmbracelet/x/term\"\n\nfunc (p *Program) checkOpti"
},
{
"path": "testdata/TestClearMsg/bg_fg_cur_color.golden",
"chars": 108,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p\u001b]10;?\u0007\u001b]11;?\u0007\u001b]12;?\u0007"
},
{
"path": "testdata/TestClearMsg/clear_screen.golden",
"chars": 87,
"preview": "\u001b[?25l\r\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestClearMsg/read_set_clipboard.golden",
"chars": 116,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p\u001b]52;c;?\u0007\u001b]52;c;c3VjY2Vzcw==\u0007"
},
{
"path": "testdata/TestViewModel/altscreen.golden",
"chars": 87,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestViewModel/altscreen_autoexit.golden",
"chars": 120,
"preview": "\u001b[>4m\u001b[=0;1u\u001b[?1049h\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\u001b[H\u001b[2Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[24d\u001b[?1049l\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestViewModel/bg_set_color.golden",
"chars": 106,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\u001b]11;#ffffff\u0007\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b]111\u0007\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestViewModel/bp_stop_start.golden",
"chars": 87,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestViewModel/cursor_hide.golden",
"chars": 87,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestViewModel/cursor_hideshow.golden",
"chars": 86,
"preview": "\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\u001b[1 q\r\u001b[Jsuccess\r\u001b[?25h\u001b[>4m\u001b[=0;1u\u001b[J\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestViewModel/kitty_stop_startreleases.golden",
"chars": 87,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=3;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "testdata/TestViewModel/mouse_allmotion.golden",
"chars": 127,
"preview": "\u001b[?25l\u001b[?2004h\u001b[?1003h\u001b[?1006h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?2026$p\u001b["
},
{
"path": "testdata/TestViewModel/mouse_cellmotion.golden",
"chars": 127,
"preview": "\u001b[?25l\u001b[?2004h\u001b[?1002h\u001b[?1006h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?2026$p\u001b["
},
{
"path": "testdata/TestViewModel/mouse_disable.golden",
"chars": 87,
"preview": "\u001b[?25l\u001b[?2004h\u001b[>4;2m\u001b[=1;1u\r\u001b[Jsuccess\u001b[>4m\u001b[=0;1u\r\u001b[J\u001b[?25h\u001b[?2004l\u001b[?2026$p\u001b[?2027$p"
},
{
"path": "tty.go",
"chars": 3125,
"preview": "package tea\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\tuv \"github.com/charmbracelet/ultraviolet\"\n\t\"github.com/charmbracelet/x/term"
},
{
"path": "tty_unix.go",
"chars": 1143,
"preview": "//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos\n// +build darwin dragon"
},
{
"path": "tty_windows.go",
"chars": 1802,
"preview": "//go:build windows\n// +build windows\n\npackage tea\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/x/term\"\n\t\"golang.org/x/sy"
},
{
"path": "tutorials/basics/README.md",
"chars": 7466,
"preview": "# Bubble Tea Basics\n\nBubble Tea is based on the functional design paradigms of [The Elm\nArchitecture][elm], which happen"
},
{
"path": "tutorials/basics/main.go",
"chars": 1624,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\ntype model struct {\n\tcursor int\n\tchoices []str"
},
{
"path": "tutorials/commands/README.md",
"chars": 7204,
"preview": "# Commands in Bubble Tea\n\nThis is the second tutorial for Bubble Tea covering commands, which deal with\nI/O. The tutoria"
},
{
"path": "tutorials/commands/main.go",
"chars": 1432,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n)\n\nconst url = \"https://charm.sh"
},
{
"path": "tutorials/go.mod",
"chars": 1104,
"preview": "module tutorial\n\ngo 1.24.2\n\ntoolchain go1.24.10\n\nrequire charm.land/bubbletea/v2 v2.0.0-00010101000000-000000000000\n\nreq"
},
{
"path": "tutorials/go.sum",
"chars": 3611,
"preview": "github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=\ngithub.com/aymanbagabas/go-udiff"
},
{
"path": "xterm.go",
"chars": 606,
"preview": "package tea\n\n// TerminalVersionMsg is a message that represents the terminal version.\ntype TerminalVersionMsg struct {\n\t"
}
]
About this extraction
This page contains the full source code of the charmbracelet/bubbletea GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 191 files (406.2 KB), approximately 126.5k tokens, and a symbol index with 1053 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.