The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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
├── commands.go
├── commands_test.go
├── examples
    ├── README.md
    ├── altscreen-toggle
    │   ├── README.md
    │   ├── altscreen-toggle.gif
    │   └── main.go
    ├── autocomplete
    │   └── main.go
    ├── cellbuffer
    │   └── main.go
    ├── chat
    │   ├── README.md
    │   ├── chat.gif
    │   └── main.go
    ├── composable-views
    │   ├── README.md
    │   ├── composable-views.gif
    │   └── main.go
    ├── credit-card-form
    │   ├── README.md
    │   ├── credit-card-form.gif
    │   └── main.go
    ├── debounce
    │   ├── README.md
    │   ├── debounce.gif
    │   └── main.go
    ├── exec
    │   ├── README.md
    │   ├── exec.gif
    │   └── main.go
    ├── file-picker
    │   └── main.go
    ├── focus-blur
    │   └── main.go
    ├── fullscreen
    │   ├── README.md
    │   ├── fullscreen.gif
    │   └── main.go
    ├── glamour
    │   ├── README.md
    │   ├── glamour.gif
    │   └── main.go
    ├── go.mod
    ├── go.sum
    ├── help
    │   ├── README.md
    │   ├── help.gif
    │   └── main.go
    ├── http
    │   ├── README.md
    │   ├── http.gif
    │   └── main.go
    ├── list-default
    │   ├── README.md
    │   ├── list-default.gif
    │   └── main.go
    ├── list-fancy
    │   ├── README.md
    │   ├── delegate.go
    │   ├── list-fancy.gif
    │   ├── main.go
    │   └── randomitems.go
    ├── list-simple
    │   ├── README.md
    │   ├── list-simple.gif
    │   └── main.go
    ├── mouse
    │   └── main.go
    ├── package-manager
    │   ├── README.md
    │   ├── main.go
    │   ├── package-manager.gif
    │   └── packages.go
    ├── pager
    │   ├── README.md
    │   ├── artichoke.md
    │   ├── main.go
    │   └── pager.gif
    ├── paginator
    │   ├── README.md
    │   ├── main.go
    │   └── paginator.gif
    ├── pipe
    │   ├── README.md
    │   ├── main.go
    │   └── pipe.gif
    ├── prevent-quit
    │   └── main.go
    ├── progress-animated
    │   ├── README.md
    │   ├── main.go
    │   └── progress-animated.gif
    ├── progress-download
    │   ├── README.md
    │   ├── main.go
    │   └── tui.go
    ├── progress-static
    │   ├── README.md
    │   ├── main.go
    │   └── progress-static.gif
    ├── realtime
    │   ├── README.md
    │   ├── main.go
    │   └── realtime.gif
    ├── result
    │   ├── README.md
    │   ├── main.go
    │   └── result.gif
    ├── send-msg
    │   ├── README.md
    │   ├── main.go
    │   └── send-msg.gif
    ├── sequence
    │   ├── README.md
    │   ├── main.go
    │   └── sequence.gif
    ├── set-window-title
    │   └── main.go
    ├── simple
    │   ├── README.md
    │   ├── main.go
    │   ├── main_test.go
    │   ├── simple.gif
    │   └── testdata
    │   │   └── TestApp.golden
    ├── spinner
    │   ├── README.md
    │   ├── main.go
    │   └── spinner.gif
    ├── spinners
    │   ├── README.md
    │   ├── main.go
    │   └── spinners.gif
    ├── split-editors
    │   ├── README.md
    │   ├── main.go
    │   └── split-editors.gif
    ├── stopwatch
    │   ├── README.md
    │   ├── main.go
    │   └── stopwatch.gif
    ├── suspend
    │   └── main.go
    ├── table-resize
    │   └── main.go
    ├── table
    │   ├── README.md
    │   ├── main.go
    │   └── table.gif
    ├── tabs
    │   ├── README.md
    │   ├── main.go
    │   └── tabs.gif
    ├── textarea
    │   ├── README.md
    │   ├── main.go
    │   └── textarea.gif
    ├── textinput
    │   ├── README.md
    │   ├── main.go
    │   └── textinput.gif
    ├── textinputs
    │   ├── README.md
    │   ├── main.go
    │   └── textinputs.gif
    ├── timer
    │   ├── README.md
    │   ├── main.go
    │   └── timer.gif
    ├── tui-daemon-combo
    │   ├── README.md
    │   ├── main.go
    │   └── tui-daemon-combo.gif
    ├── views
    │   ├── README.md
    │   ├── main.go
    │   └── views.gif
    └── window-size
    │   └── main.go
├── exec.go
├── exec_test.go
├── focus.go
├── go.mod
├── go.sum
├── inputreader_other.go
├── inputreader_windows.go
├── key.go
├── key_other.go
├── key_sequences.go
├── key_test.go
├── key_windows.go
├── logging.go
├── logging_test.go
├── mouse.go
├── mouse_test.go
├── nil_renderer.go
├── nil_renderer_test.go
├── options.go
├── options_test.go
├── renderer.go
├── screen.go
├── screen_test.go
├── signals_unix.go
├── signals_windows.go
├── standard_renderer.go
├── tea.go
├── tea_init.go
├── tea_test.go
├── tty.go
├── tty_unix.go
├── tty_windows.go
└── tutorials
    ├── basics
        ├── README.md
        └── main.go
    ├── commands
        ├── README.md
        └── main.go
    ├── go.mod
    └── go.sum


/.gitattributes:
--------------------------------------------------------------------------------
1 | *.golden -text
2 | 


--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | *  @meowgorithm @aymanbagabas
2 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
 1 | name: Bug Report
 2 | description: File a bug report
 3 | labels: [bug]
 4 | body:
 5 |   - type: markdown
 6 |     attributes:
 7 |       value: |
 8 |         Thanks for taking the time to fill out this bug report! Please fill the form below.
 9 |   - type: textarea
10 |     id: what-happened
11 |     attributes:
12 |       label: What happened?
13 |       description: Also tell us, what did you expect to happen?
14 |     validations:
15 |       required: true
16 |   - type: textarea
17 |     id: reproducible
18 |     attributes:
19 |       label: How can we reproduce this?
20 |       description: |
21 |         Please share a code snippet, gist, or public repository that reproduces the issue.
22 |         Make sure to make the reproducible as concise as possible,
23 |         with only the minimum required code to reproduce the issue.
24 |     validations:
25 |       required: true
26 |   - type: textarea
27 |     id: version
28 |     attributes:
29 |       label: Which version of bubbletea are you using?
30 |       description: ''
31 |       render: bash
32 |     validations:
33 |       required: true
34 |   - type: textarea
35 |     id: terminaal
36 |     attributes:
37 |       label: Which terminals did you reproduce this with?
38 |       description: |
39 |         Other helpful information:
40 |         was it over SSH?
41 |         On tmux?
42 |         Which version of said terminal?
43 |     validations:
44 |       required: true
45 |   - type: checkboxes
46 |     id: search
47 |     attributes:
48 |       label: Search
49 |       options:
50 |         - label: |
51 |            I searched for other open and closed issues and pull requests before opening this,
52 |            and didn't find anything that seems related.
53 |           required: true
54 |   - type: textarea
55 |     id: ctx
56 |     attributes:
57 |       label: Additional context
58 |       description: Anything else you would like to add
59 |     validations:
60 |       required: false
61 | 
62 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug report
 3 | about: Create a report to help us improve
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 | 
13 | **Setup**
14 | Please complete the following information along with version numbers, if applicable.
15 |  - OS [e.g. Ubuntu, macOS]
16 |  - Shell [e.g. zsh, fish]
17 |  - Terminal Emulator [e.g. kitty, iterm]
18 |  - Terminal Multiplexer [e.g. tmux]
19 | 
20 | **To Reproduce**
21 | Steps to reproduce the behavior:
22 | 1. Go to '...'
23 | 2. Click on '....'
24 | 3. Scroll down to '....'
25 | 4. See error
26 | 
27 | **Source Code**
28 | Please include source code if needed to reproduce the behavior. 
29 | 
30 | **Expected behavior**
31 | A clear and concise description of what you expected to happen.
32 | 
33 | **Screenshots**
34 | Add screenshots to help explain your problem.
35 | 
36 | **Additional context**
37 | Add any other context about the problem here.
38 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Discord
4 |   url: https://charm.sh/discord
5 |   about: Chat on our Discord.
6 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature request
 3 | about: Suggest an idea for this project
 4 | title: ''
 5 | labels: enhancement
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 | 
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 | 
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 | 
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
 1 | version: 2
 2 | 
 3 | updates:
 4 |   - package-ecosystem: "gomod"
 5 |     directory: "/"
 6 |     schedule:
 7 |       interval: "weekly"
 8 |       day: "monday"
 9 |       time: "05:00"
10 |       timezone: "America/New_York"
11 |     labels:
12 |       - "dependencies"
13 |     commit-message:
14 |       prefix: "chore"
15 |       include: "scope"
16 | 
17 |   - package-ecosystem: "github-actions"
18 |     directory: "/"
19 |     schedule:
20 |       interval: "weekly"
21 |       day: "monday"
22 |       time: "05:00"
23 |       timezone: "America/New_York"
24 |     labels:
25 |       - "dependencies"
26 |     commit-message:
27 |       prefix: "chore"
28 |       include: "scope"
29 | 
30 |   - package-ecosystem: "docker"
31 |     directory: "/"
32 |     schedule:
33 |       interval: "weekly"
34 |       day: "monday"
35 |       time: "05:00"
36 |       timezone: "America/New_York"
37 |     labels:
38 |       - "dependencies"
39 |     commit-message:
40 |       prefix: "chore"
41 |       include: "scope"
42 | 
43 |   - package-ecosystem: "gomod"
44 |     directory: "/examples"
45 |     schedule:
46 |       interval: "weekly"
47 |       day: "monday"
48 |       time: "05:00"
49 |       timezone: "America/New_York"
50 |     labels:
51 |       - "dependencies"
52 |     commit-message:
53 |       prefix: "chore"
54 |       include: "scope"
55 | 
56 |   - package-ecosystem: "gomod"
57 |     directory: "/tutorials"
58 |     schedule:
59 |       interval: "weekly"
60 |       day: "monday"
61 |       time: "05:00"
62 |       timezone: "America/New_York"
63 |     labels:
64 |       - "dependencies"
65 |     commit-message:
66 |       prefix: "chore"
67 |       include: "scope"
68 | 


--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
 1 | name: build
 2 | on: [push, pull_request]
 3 | 
 4 | jobs:
 5 |   build:
 6 |     uses: charmbracelet/meta/.github/workflows/build.yml@main
 7 | 
 8 |   build-go-mod:
 9 |     uses: charmbracelet/meta/.github/workflows/build.yml@main
10 |     with:
11 |       go-version: ""
12 |       go-version-file: ./go.mod
13 | 
14 |   build-examples:
15 |     uses: charmbracelet/meta/.github/workflows/build.yml@main
16 |     with:
17 |       go-version: ""
18 |       go-version-file: ./examples/go.mod
19 |       working-directory: ./examples
20 | 


--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
 1 | name: coverage
 2 | on: [push, pull_request]
 3 | 
 4 | jobs:
 5 |   coverage:
 6 |     strategy:
 7 |       matrix:
 8 |         go-version: [^1]
 9 |         os: [ubuntu-latest]
10 |     runs-on: ${{ matrix.os }}
11 |     env:
12 |       GO111MODULE: "on"
13 |     steps:
14 |       - name: Install Go
15 |         uses: actions/setup-go@v5
16 |         with:
17 |           go-version: ${{ matrix.go-version }}
18 | 
19 |       - name: Checkout code
20 |         uses: actions/checkout@v4
21 | 
22 |       - name: Coverage
23 |         run: |
24 |           go test -race -covermode=atomic -coverprofile=coverage.txt ./...
25 | 
26 |       - uses: codecov/codecov-action@v5
27 |         with:
28 |           file: ./coverage.txt
29 |           token: ${{ secrets.CODECOV_TOKEN }}
30 | 


--------------------------------------------------------------------------------
/.github/workflows/dependabot-sync.yml:
--------------------------------------------------------------------------------
 1 | name: dependabot-sync
 2 | on:
 3 |   schedule:
 4 |     - cron: "0 0 * * 0" # every Sunday at midnight
 5 |   workflow_dispatch: # allows manual triggering
 6 | 
 7 | permissions:
 8 |   contents: write
 9 |   pull-requests: write
10 | 
11 | jobs:
12 |   dependabot-sync:
13 |     uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
14 |     with:
15 |       repo_name: ${{ github.event.repository.name }}
16 |     secrets:
17 |       gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
18 | 


--------------------------------------------------------------------------------
/.github/workflows/examples.yml:
--------------------------------------------------------------------------------
 1 | name: examples
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - 'master'
 7 |     paths:
 8 |       - '.github/workflows/examples.yml'
 9 |       - './examples/go.mod'
10 |       - './examples/go.sum'
11 |       - './tutorials/go.mod'
12 |       - './tutorials/go.sum'
13 |       - './go.mod'
14 |       - './go.sum'
15 |   workflow_dispatch: {}
16 | 
17 | jobs:
18 |   tidy:
19 |     permissions:
20 |       contents: write
21 |     runs-on: ubuntu-latest
22 |     steps:
23 |       - uses: actions/checkout@v4
24 |       - uses: actions/setup-go@v5
25 |         with:
26 |           go-version: '^1'
27 |           cache: true
28 |       - shell: bash
29 |         run: |
30 |           (cd ./examples && go mod tidy)
31 |           (cd ./tutorials && go mod tidy)
32 |       - uses: stefanzweifel/git-auto-commit-action@v6
33 |         with:
34 |           commit_message: "chore: go mod tidy tutorials and examples"
35 |           branch: master
36 |           commit_user_name: actions-user
37 |           commit_user_email: actions@github.com
38 | 
39 | 


--------------------------------------------------------------------------------
/.github/workflows/lint-sync.yml:
--------------------------------------------------------------------------------
 1 | name: lint-sync
 2 | on:
 3 |   schedule:
 4 |     # every Sunday at midnight
 5 |     - cron: "0 0 * * 0"
 6 |   workflow_dispatch: # allows manual triggering
 7 | 
 8 | permissions:
 9 |   contents: write
10 |   pull-requests: write
11 | 
12 | jobs:
13 |   lint:
14 |     uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main
15 | 


--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 | on:
3 |   push:
4 |   pull_request:
5 | 
6 | jobs:
7 |   lint:
8 |     uses: charmbracelet/meta/.github/workflows/lint.yml@main
9 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
 1 | name: goreleaser
 2 | 
 3 | on:
 4 |   push:
 5 |     tags:
 6 |       - v*.*.*
 7 | 
 8 | concurrency:
 9 |   group: goreleaser
10 |   cancel-in-progress: true
11 | 
12 | jobs:
13 |   goreleaser:
14 |     uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main
15 |     secrets:
16 |       docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
17 |       docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
18 |       gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
19 |       goreleaser_key: ${{ secrets.GORELEASER_KEY }}
20 |       twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }}
21 |       twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }}
22 |       twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
23 |       twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
24 |       mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }}
25 |       mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }}
26 |       mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }}
27 |       discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
28 |       discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
29 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
30 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | .envrc
 3 | 
 4 | examples/fullscreen/fullscreen
 5 | examples/help/help
 6 | examples/http/http
 7 | examples/list-default/list-default
 8 | examples/list-fancy/list-fancy
 9 | examples/list-simple/list-simple
10 | examples/mouse/mouse
11 | examples/pager/pager
12 | examples/progress-download/color_vortex.blend
13 | examples/progress-download/progress-download
14 | examples/simple/simple
15 | examples/spinner/spinner
16 | examples/textinput/textinput
17 | examples/textinputs/textinputs
18 | examples/views/views
19 | tutorials/basics/basics
20 | tutorials/commands/commands
21 | .idea
22 | coverage.txt
23 | dist/
24 | 


--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
 1 | version: "2"
 2 | run:
 3 |   tests: false
 4 | linters:
 5 |   enable:
 6 |     - bodyclose
 7 |     - exhaustive
 8 |     - goconst
 9 |     - godot
10 |     - gomoddirectives
11 |     - goprintffuncname
12 |     - gosec
13 |     - misspell
14 |     - nakedret
15 |     - nestif
16 |     - nilerr
17 |     - noctx
18 |     - nolintlint
19 |     - prealloc
20 |     - revive
21 |     - rowserrcheck
22 |     - sqlclosecheck
23 |     - tparallel
24 |     - unconvert
25 |     - unparam
26 |     - whitespace
27 |     - wrapcheck
28 |   exclusions:
29 |     generated: lax
30 |     presets:
31 |       - common-false-positives
32 | issues:
33 |   max-issues-per-linter: 0
34 |   max-same-issues: 0
35 | formatters:
36 |   enable:
37 |     - gofumpt
38 |     - goimports
39 |   exclusions:
40 |     generated: lax
41 | 


--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
2 | version: 2
3 | includes:
4 |   - from_url:
5 |       url: charmbracelet/meta/main/goreleaser-lib.yaml
6 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2020-2023 Charmbracelet, Inc
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/Taskfile.yaml:
--------------------------------------------------------------------------------
 1 | # https://taskfile.dev
 2 | 
 3 | version: '3'
 4 | 
 5 | tasks:
 6 |   lint:
 7 |     desc: Run lint
 8 |     cmds:
 9 |       - golangci-lint run
10 | 
11 |   test:
12 |     desc: Run tests
13 |     cmds:
14 |       - go test ./... {{.CLI_ARGS}}
15 | 


--------------------------------------------------------------------------------
/commands_test.go:
--------------------------------------------------------------------------------
  1 | package tea
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"testing"
  6 | 	"time"
  7 | )
  8 | 
  9 | func TestEvery(t *testing.T) {
 10 | 	expected := "every ms"
 11 | 	msg := Every(time.Millisecond, func(t time.Time) Msg {
 12 | 		return expected
 13 | 	})()
 14 | 	if expected != msg {
 15 | 		t.Fatalf("expected a msg %v but got %v", expected, msg)
 16 | 	}
 17 | }
 18 | 
 19 | func TestTick(t *testing.T) {
 20 | 	expected := "tick"
 21 | 	msg := Tick(time.Millisecond, func(t time.Time) Msg {
 22 | 		return expected
 23 | 	})()
 24 | 	if expected != msg {
 25 | 		t.Fatalf("expected a msg %v but got %v", expected, msg)
 26 | 	}
 27 | }
 28 | 
 29 | func TestSequentially(t *testing.T) {
 30 | 	expectedErrMsg := fmt.Errorf("some err")
 31 | 	expectedStrMsg := "some msg"
 32 | 
 33 | 	nilReturnCmd := func() Msg {
 34 | 		return nil
 35 | 	}
 36 | 
 37 | 	tests := []struct {
 38 | 		name     string
 39 | 		cmds     []Cmd
 40 | 		expected Msg
 41 | 	}{
 42 | 		{
 43 | 			name:     "all nil",
 44 | 			cmds:     []Cmd{nilReturnCmd, nilReturnCmd},
 45 | 			expected: nil,
 46 | 		},
 47 | 		{
 48 | 			name:     "null cmds",
 49 | 			cmds:     []Cmd{nil, nil},
 50 | 			expected: nil,
 51 | 		},
 52 | 		{
 53 | 			name: "one error",
 54 | 			cmds: []Cmd{
 55 | 				nilReturnCmd,
 56 | 				func() Msg {
 57 | 					return expectedErrMsg
 58 | 				},
 59 | 				nilReturnCmd,
 60 | 			},
 61 | 			expected: expectedErrMsg,
 62 | 		},
 63 | 		{
 64 | 			name: "some msg",
 65 | 			cmds: []Cmd{
 66 | 				nilReturnCmd,
 67 | 				func() Msg {
 68 | 					return expectedStrMsg
 69 | 				},
 70 | 				nilReturnCmd,
 71 | 			},
 72 | 			expected: expectedStrMsg,
 73 | 		},
 74 | 	}
 75 | 	for _, test := range tests {
 76 | 		t.Run(test.name, func(t *testing.T) {
 77 | 			if msg := Sequentially(test.cmds...)(); msg != test.expected {
 78 | 				t.Fatalf("expected a msg %v but got %v", test.expected, msg)
 79 | 			}
 80 | 		})
 81 | 	}
 82 | }
 83 | 
 84 | func TestBatch(t *testing.T) {
 85 | 	t.Run("nil cmd", func(t *testing.T) {
 86 | 		if b := Batch(nil); b != nil {
 87 | 			t.Fatalf("expected nil, got %+v", b)
 88 | 		}
 89 | 	})
 90 | 	t.Run("empty cmd", func(t *testing.T) {
 91 | 		if b := Batch(); b != nil {
 92 | 			t.Fatalf("expected nil, got %+v", b)
 93 | 		}
 94 | 	})
 95 | 	t.Run("single cmd", func(t *testing.T) {
 96 | 		b := Batch(Quit)()
 97 | 		if _, ok := b.(QuitMsg); !ok {
 98 | 			t.Fatalf("expected a QuitMsg, got %T", b)
 99 | 		}
100 | 	})
101 | 	t.Run("mixed nil cmds", func(t *testing.T) {
102 | 		b := Batch(nil, Quit, nil, Quit, nil, nil)()
103 | 		if l := len(b.(BatchMsg)); l != 2 {
104 | 			t.Fatalf("expected a []Cmd with len 2, got %d", l)
105 | 		}
106 | 	})
107 | }
108 | 


--------------------------------------------------------------------------------
/examples/altscreen-toggle/README.md:
--------------------------------------------------------------------------------
1 | # Alt Screen Toggle
2 | 
3 | <img width="800" src="./altscreen-toggle.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/altscreen-toggle/altscreen-toggle.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/altscreen-toggle/altscreen-toggle.gif


--------------------------------------------------------------------------------
/examples/altscreen-toggle/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 
 7 | 	tea "github.com/charmbracelet/bubbletea"
 8 | 	"github.com/charmbracelet/lipgloss"
 9 | )
10 | 
11 | var (
12 | 	keywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("204")).Background(lipgloss.Color("235"))
13 | 	helpStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
14 | )
15 | 
16 | type model struct {
17 | 	altscreen  bool
18 | 	quitting   bool
19 | 	suspending bool
20 | }
21 | 
22 | func (m model) Init() tea.Cmd {
23 | 	return nil
24 | }
25 | 
26 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
27 | 	switch msg := msg.(type) {
28 | 	case tea.ResumeMsg:
29 | 		m.suspending = false
30 | 		return m, nil
31 | 	case tea.KeyMsg:
32 | 		switch msg.String() {
33 | 		case "q", "ctrl+c", "esc":
34 | 			m.quitting = true
35 | 			return m, tea.Quit
36 | 		case "ctrl+z":
37 | 			m.suspending = true
38 | 			return m, tea.Suspend
39 | 		case " ":
40 | 			var cmd tea.Cmd
41 | 			if m.altscreen {
42 | 				cmd = tea.ExitAltScreen
43 | 			} else {
44 | 				cmd = tea.EnterAltScreen
45 | 			}
46 | 			m.altscreen = !m.altscreen
47 | 			return m, cmd
48 | 		}
49 | 	}
50 | 	return m, nil
51 | }
52 | 
53 | func (m model) View() string {
54 | 	if m.suspending {
55 | 		return ""
56 | 	}
57 | 
58 | 	if m.quitting {
59 | 		return "Bye!\n"
60 | 	}
61 | 
62 | 	const (
63 | 		altscreenMode = " altscreen mode "
64 | 		inlineMode    = " inline mode "
65 | 	)
66 | 
67 | 	var mode string
68 | 	if m.altscreen {
69 | 		mode = altscreenMode
70 | 	} else {
71 | 		mode = inlineMode
72 | 	}
73 | 
74 | 	return fmt.Sprintf("\n\n  You're in %s\n\n\n", keywordStyle.Render(mode)) +
75 | 		helpStyle.Render("  space: switch modes • ctrl-z: suspend • q: exit\n")
76 | }
77 | 
78 | func main() {
79 | 	if _, err := tea.NewProgram(model{}).Run(); err != nil {
80 | 		fmt.Println("Error running program:", err)
81 | 		os.Exit(1)
82 | 	}
83 | }
84 | 


--------------------------------------------------------------------------------
/examples/autocomplete/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"fmt"
  6 | 	"io"
  7 | 	"log"
  8 | 	"net/http"
  9 | 
 10 | 	"github.com/charmbracelet/bubbles/help"
 11 | 	"github.com/charmbracelet/bubbles/key"
 12 | 	"github.com/charmbracelet/bubbles/textinput"
 13 | 	tea "github.com/charmbracelet/bubbletea"
 14 | 	"github.com/charmbracelet/lipgloss"
 15 | )
 16 | 
 17 | func main() {
 18 | 	p := tea.NewProgram(initialModel())
 19 | 	if _, err := p.Run(); err != nil {
 20 | 		log.Fatal(err)
 21 | 	}
 22 | }
 23 | 
 24 | type gotReposSuccessMsg []repo
 25 | type gotReposErrMsg error
 26 | 
 27 | type repo struct {
 28 | 	Name string `json:"name"`
 29 | }
 30 | 
 31 | const reposURL = "https://api.github.com/orgs/charmbracelet/repos"
 32 | 
 33 | func getRepos() tea.Msg {
 34 | 	req, err := http.NewRequest(http.MethodGet, reposURL, nil)
 35 | 	if err != nil {
 36 | 		return gotReposErrMsg(err)
 37 | 	}
 38 | 
 39 | 	req.Header.Add("Accept", "application/vnd.github+json")
 40 | 	req.Header.Add("X-GitHub-Api-Version", "2022-11-28")
 41 | 
 42 | 	resp, err := http.DefaultClient.Do(req)
 43 | 	if err != nil {
 44 | 		return gotReposErrMsg(err)
 45 | 	}
 46 | 	defer resp.Body.Close() // nolint: errcheck
 47 | 
 48 | 	data, err := io.ReadAll(resp.Body)
 49 | 	if err != nil {
 50 | 		return gotReposErrMsg(err)
 51 | 	}
 52 | 
 53 | 	var repos []repo
 54 | 
 55 | 	err = json.Unmarshal(data, &repos)
 56 | 	if err != nil {
 57 | 		return gotReposErrMsg(err)
 58 | 	}
 59 | 
 60 | 	return gotReposSuccessMsg(repos)
 61 | }
 62 | 
 63 | type model struct {
 64 | 	textInput textinput.Model
 65 | 	help      help.Model
 66 | 	keymap    keymap
 67 | }
 68 | 
 69 | type keymap struct{}
 70 | 
 71 | func (k keymap) ShortHelp() []key.Binding {
 72 | 	return []key.Binding{
 73 | 		key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete")),
 74 | 		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")),
 75 | 		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")),
 76 | 		key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
 77 | 	}
 78 | }
 79 | func (k keymap) FullHelp() [][]key.Binding {
 80 | 	return [][]key.Binding{k.ShortHelp()}
 81 | }
 82 | 
 83 | func initialModel() model {
 84 | 	ti := textinput.New()
 85 | 	ti.Placeholder = "repository"
 86 | 	ti.Prompt = "charmbracelet/"
 87 | 	ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
 88 | 	ti.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
 89 | 	ti.Focus()
 90 | 	ti.CharLimit = 50
 91 | 	ti.Width = 20
 92 | 	ti.ShowSuggestions = true
 93 | 
 94 | 	h := help.New()
 95 | 
 96 | 	km := keymap{}
 97 | 
 98 | 	return model{textInput: ti, help: h, keymap: km}
 99 | }
100 | 
101 | func (m model) Init() tea.Cmd {
102 | 	return tea.Batch(getRepos, textinput.Blink)
103 | }
104 | 
105 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
106 | 	switch msg := msg.(type) {
107 | 	case tea.KeyMsg:
108 | 		switch msg.Type {
109 | 		case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
110 | 			return m, tea.Quit
111 | 		}
112 | 	case gotReposSuccessMsg:
113 | 		var suggestions []string
114 | 		for _, r := range msg {
115 | 			suggestions = append(suggestions, r.Name)
116 | 		}
117 | 		m.textInput.SetSuggestions(suggestions)
118 | 	}
119 | 
120 | 	var cmd tea.Cmd
121 | 	m.textInput, cmd = m.textInput.Update(msg)
122 | 	return m, cmd
123 | }
124 | 
125 | func (m model) View() string {
126 | 	return fmt.Sprintf(
127 | 		"Pick a Charm™ repo:\n\n  %s\n\n%s\n\n",
128 | 		m.textInput.View(),
129 | 		m.help.View(m.keymap),
130 | 	)
131 | }
132 | 


--------------------------------------------------------------------------------
/examples/cellbuffer/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | // A simple example demonstrating how to draw and animate on a cellular grid.
  4 | // Note that the cellbuffer implementation in this example does not support
  5 | // double-width runes.
  6 | 
  7 | import (
  8 | 	"fmt"
  9 | 	"os"
 10 | 	"strings"
 11 | 	"time"
 12 | 
 13 | 	tea "github.com/charmbracelet/bubbletea"
 14 | 	"github.com/charmbracelet/harmonica"
 15 | )
 16 | 
 17 | const (
 18 | 	fps       = 60
 19 | 	frequency = 7.5
 20 | 	damping   = 0.15
 21 | 	asterisk  = "*"
 22 | )
 23 | 
 24 | func drawEllipse(cb *cellbuffer, xc, yc, rx, ry float64) {
 25 | 	var (
 26 | 		dx, dy, d1, d2 float64
 27 | 		x              float64
 28 | 		y              = ry
 29 | 	)
 30 | 
 31 | 	d1 = ry*ry - rx*rx*ry + 0.25*rx*rx
 32 | 	dx = 2 * ry * ry * x
 33 | 	dy = 2 * rx * rx * y
 34 | 
 35 | 	for dx < dy {
 36 | 		cb.set(int(x+xc), int(y+yc))
 37 | 		cb.set(int(-x+xc), int(y+yc))
 38 | 		cb.set(int(x+xc), int(-y+yc))
 39 | 		cb.set(int(-x+xc), int(-y+yc))
 40 | 		if d1 < 0 {
 41 | 			x++
 42 | 			dx = dx + (2 * ry * ry)
 43 | 			d1 = d1 + dx + (ry * ry)
 44 | 		} else {
 45 | 			x++
 46 | 			y--
 47 | 			dx = dx + (2 * ry * ry)
 48 | 			dy = dy - (2 * rx * rx)
 49 | 			d1 = d1 + dx - dy + (ry * ry)
 50 | 		}
 51 | 	}
 52 | 
 53 | 	d2 = ((ry * ry) * ((x + 0.5) * (x + 0.5))) + ((rx * rx) * ((y - 1) * (y - 1))) - (rx * rx * ry * ry)
 54 | 
 55 | 	for y >= 0 {
 56 | 		cb.set(int(x+xc), int(y+yc))
 57 | 		cb.set(int(-x+xc), int(y+yc))
 58 | 		cb.set(int(x+xc), int(-y+yc))
 59 | 		cb.set(int(-x+xc), int(-y+yc))
 60 | 		if d2 > 0 {
 61 | 			y--
 62 | 			dy = dy - (2 * rx * rx)
 63 | 			d2 = d2 + (rx * rx) - dy
 64 | 		} else {
 65 | 			y--
 66 | 			x++
 67 | 			dx = dx + (2 * ry * ry)
 68 | 			dy = dy - (2 * rx * rx)
 69 | 			d2 = d2 + dx - dy + (rx * rx)
 70 | 		}
 71 | 	}
 72 | }
 73 | 
 74 | type cellbuffer struct {
 75 | 	cells  []string
 76 | 	stride int
 77 | }
 78 | 
 79 | func (c *cellbuffer) init(w, h int) {
 80 | 	if w == 0 {
 81 | 		return
 82 | 	}
 83 | 	c.stride = w
 84 | 	c.cells = make([]string, w*h)
 85 | 	c.wipe()
 86 | }
 87 | 
 88 | func (c cellbuffer) set(x, y int) {
 89 | 	i := y*c.stride + x
 90 | 	if i > len(c.cells)-1 || x < 0 || y < 0 || x >= c.width() || y >= c.height() {
 91 | 		return
 92 | 	}
 93 | 	c.cells[i] = asterisk
 94 | }
 95 | 
 96 | func (c *cellbuffer) wipe() {
 97 | 	for i := range c.cells {
 98 | 		c.cells[i] = " "
 99 | 	}
100 | }
101 | 
102 | func (c cellbuffer) width() int {
103 | 	return c.stride
104 | }
105 | 
106 | func (c cellbuffer) height() int {
107 | 	h := len(c.cells) / c.stride
108 | 	if len(c.cells)%c.stride != 0 {
109 | 		h++
110 | 	}
111 | 	return h
112 | }
113 | 
114 | func (c cellbuffer) ready() bool {
115 | 	return len(c.cells) > 0
116 | }
117 | 
118 | func (c cellbuffer) String() string {
119 | 	var b strings.Builder
120 | 	for i := 0; i < len(c.cells); i++ {
121 | 		if i > 0 && i%c.stride == 0 && i < len(c.cells)-1 {
122 | 			b.WriteRune('\n')
123 | 		}
124 | 		b.WriteString(c.cells[i])
125 | 	}
126 | 	return b.String()
127 | }
128 | 
129 | type frameMsg struct{}
130 | 
131 | func animate() tea.Cmd {
132 | 	return tea.Tick(time.Second/fps, func(_ time.Time) tea.Msg {
133 | 		return frameMsg{}
134 | 	})
135 | }
136 | 
137 | type model struct {
138 | 	cells                cellbuffer
139 | 	spring               harmonica.Spring
140 | 	targetX, targetY     float64
141 | 	x, y                 float64
142 | 	xVelocity, yVelocity float64
143 | }
144 | 
145 | func (m model) Init() tea.Cmd {
146 | 	return animate()
147 | }
148 | 
149 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
150 | 	switch msg := msg.(type) {
151 | 	case tea.KeyMsg:
152 | 		return m, tea.Quit
153 | 	case tea.WindowSizeMsg:
154 | 		if !m.cells.ready() {
155 | 			m.targetX, m.targetY = float64(msg.Width)/2, float64(msg.Height)/2
156 | 		}
157 | 		m.cells.init(msg.Width, msg.Height)
158 | 		return m, nil
159 | 	case tea.MouseMsg:
160 | 		if !m.cells.ready() {
161 | 			return m, nil
162 | 		}
163 | 		m.targetX, m.targetY = float64(msg.X), float64(msg.Y)
164 | 		return m, nil
165 | 
166 | 	case frameMsg:
167 | 		if !m.cells.ready() {
168 | 			return m, nil
169 | 		}
170 | 
171 | 		m.cells.wipe()
172 | 		m.x, m.xVelocity = m.spring.Update(m.x, m.xVelocity, m.targetX)
173 | 		m.y, m.yVelocity = m.spring.Update(m.y, m.yVelocity, m.targetY)
174 | 		drawEllipse(&m.cells, m.x, m.y, 16, 8)
175 | 		return m, animate()
176 | 	default:
177 | 		return m, nil
178 | 	}
179 | }
180 | 
181 | func (m model) View() string {
182 | 	return m.cells.String()
183 | }
184 | 
185 | func main() {
186 | 	m := model{
187 | 		spring: harmonica.NewSpring(harmonica.FPS(fps), frequency, damping),
188 | 	}
189 | 
190 | 	p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
191 | 	if _, err := p.Run(); err != nil {
192 | 		fmt.Println("Uh oh:", err)
193 | 		os.Exit(1)
194 | 	}
195 | }
196 | 


--------------------------------------------------------------------------------
/examples/chat/README.md:
--------------------------------------------------------------------------------
1 | # Chat
2 | 
3 | <img width="800" src="./chat.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/chat/chat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/chat/chat.gif


--------------------------------------------------------------------------------
/examples/chat/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | // A simple program demonstrating the text area component from the Bubbles
  4 | // component library.
  5 | 
  6 | import (
  7 | 	"fmt"
  8 | 	"log"
  9 | 	"strings"
 10 | 
 11 | 	"github.com/charmbracelet/bubbles/textarea"
 12 | 	"github.com/charmbracelet/bubbles/viewport"
 13 | 	tea "github.com/charmbracelet/bubbletea"
 14 | 	"github.com/charmbracelet/lipgloss"
 15 | )
 16 | 
 17 | const gap = "\n\n"
 18 | 
 19 | func main() {
 20 | 	p := tea.NewProgram(initialModel())
 21 | 
 22 | 	if _, err := p.Run(); err != nil {
 23 | 		log.Fatal(err)
 24 | 	}
 25 | }
 26 | 
 27 | type (
 28 | 	errMsg error
 29 | )
 30 | 
 31 | type model struct {
 32 | 	viewport    viewport.Model
 33 | 	messages    []string
 34 | 	textarea    textarea.Model
 35 | 	senderStyle lipgloss.Style
 36 | 	err         error
 37 | }
 38 | 
 39 | func initialModel() model {
 40 | 	ta := textarea.New()
 41 | 	ta.Placeholder = "Send a message..."
 42 | 	ta.Focus()
 43 | 
 44 | 	ta.Prompt = "┃ "
 45 | 	ta.CharLimit = 280
 46 | 
 47 | 	ta.SetWidth(30)
 48 | 	ta.SetHeight(3)
 49 | 
 50 | 	// Remove cursor line styling
 51 | 	ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
 52 | 
 53 | 	ta.ShowLineNumbers = false
 54 | 
 55 | 	vp := viewport.New(30, 5)
 56 | 	vp.SetContent(`Welcome to the chat room!
 57 | Type a message and press Enter to send.`)
 58 | 
 59 | 	ta.KeyMap.InsertNewline.SetEnabled(false)
 60 | 
 61 | 	return model{
 62 | 		textarea:    ta,
 63 | 		messages:    []string{},
 64 | 		viewport:    vp,
 65 | 		senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
 66 | 		err:         nil,
 67 | 	}
 68 | }
 69 | 
 70 | func (m model) Init() tea.Cmd {
 71 | 	return textarea.Blink
 72 | }
 73 | 
 74 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 75 | 	var (
 76 | 		tiCmd tea.Cmd
 77 | 		vpCmd tea.Cmd
 78 | 	)
 79 | 
 80 | 	m.textarea, tiCmd = m.textarea.Update(msg)
 81 | 	m.viewport, vpCmd = m.viewport.Update(msg)
 82 | 
 83 | 	switch msg := msg.(type) {
 84 | 	case tea.WindowSizeMsg:
 85 | 		m.viewport.Width = msg.Width
 86 | 		m.textarea.SetWidth(msg.Width)
 87 | 		m.viewport.Height = msg.Height - m.textarea.Height() - lipgloss.Height(gap)
 88 | 
 89 | 		if len(m.messages) > 0 {
 90 | 			// Wrap content before setting it.
 91 | 			m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width).Render(strings.Join(m.messages, "\n")))
 92 | 		}
 93 | 		m.viewport.GotoBottom()
 94 | 	case tea.KeyMsg:
 95 | 		switch msg.Type {
 96 | 		case tea.KeyCtrlC, tea.KeyEsc:
 97 | 			fmt.Println(m.textarea.Value())
 98 | 			return m, tea.Quit
 99 | 		case tea.KeyEnter:
100 | 			m.messages = append(m.messages, m.senderStyle.Render("You: ")+m.textarea.Value())
101 | 			m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width).Render(strings.Join(m.messages, "\n")))
102 | 			m.textarea.Reset()
103 | 			m.viewport.GotoBottom()
104 | 		}
105 | 
106 | 	// We handle errors just like any other message
107 | 	case errMsg:
108 | 		m.err = msg
109 | 		return m, nil
110 | 	}
111 | 
112 | 	return m, tea.Batch(tiCmd, vpCmd)
113 | }
114 | 
115 | func (m model) View() string {
116 | 	return fmt.Sprintf(
117 | 		"%s%s%s",
118 | 		m.viewport.View(),
119 | 		gap,
120 | 		m.textarea.View(),
121 | 	)
122 | }
123 | 


--------------------------------------------------------------------------------
/examples/composable-views/README.md:
--------------------------------------------------------------------------------
1 | # Composable Views
2 | 
3 | <img width="800" src="./composable-views.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/composable-views/composable-views.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/composable-views/composable-views.gif


--------------------------------------------------------------------------------
/examples/composable-views/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"log"
  6 | 	"time"
  7 | 
  8 | 	"github.com/charmbracelet/bubbles/spinner"
  9 | 	"github.com/charmbracelet/bubbles/timer"
 10 | 	tea "github.com/charmbracelet/bubbletea"
 11 | 	"github.com/charmbracelet/lipgloss"
 12 | )
 13 | 
 14 | /*
 15 | This example assumes an existing understanding of commands and messages. If you
 16 | haven't already read our tutorials on the basics of Bubble Tea and working
 17 | with commands, we recommend reading those first.
 18 | 
 19 | Find them at:
 20 | https://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands
 21 | https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics
 22 | */
 23 | 
 24 | // sessionState is used to track which model is focused
 25 | type sessionState uint
 26 | 
 27 | const (
 28 | 	defaultTime              = time.Minute
 29 | 	timerView   sessionState = iota
 30 | 	spinnerView
 31 | )
 32 | 
 33 | var (
 34 | 	// Available spinners
 35 | 	spinners = []spinner.Spinner{
 36 | 		spinner.Line,
 37 | 		spinner.Dot,
 38 | 		spinner.MiniDot,
 39 | 		spinner.Jump,
 40 | 		spinner.Pulse,
 41 | 		spinner.Points,
 42 | 		spinner.Globe,
 43 | 		spinner.Moon,
 44 | 		spinner.Monkey,
 45 | 	}
 46 | 	modelStyle = lipgloss.NewStyle().
 47 | 			Width(15).
 48 | 			Height(5).
 49 | 			Align(lipgloss.Center, lipgloss.Center).
 50 | 			BorderStyle(lipgloss.HiddenBorder())
 51 | 	focusedModelStyle = lipgloss.NewStyle().
 52 | 				Width(15).
 53 | 				Height(5).
 54 | 				Align(lipgloss.Center, lipgloss.Center).
 55 | 				BorderStyle(lipgloss.NormalBorder()).
 56 | 				BorderForeground(lipgloss.Color("69"))
 57 | 	spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69"))
 58 | 	helpStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
 59 | )
 60 | 
 61 | type mainModel struct {
 62 | 	state   sessionState
 63 | 	timer   timer.Model
 64 | 	spinner spinner.Model
 65 | 	index   int
 66 | }
 67 | 
 68 | func newModel(timeout time.Duration) mainModel {
 69 | 	m := mainModel{state: timerView}
 70 | 	m.timer = timer.New(timeout)
 71 | 	m.spinner = spinner.New()
 72 | 	return m
 73 | }
 74 | 
 75 | func (m mainModel) Init() tea.Cmd {
 76 | 	// start the timer and spinner on program start
 77 | 	return tea.Batch(m.timer.Init(), m.spinner.Tick)
 78 | }
 79 | 
 80 | func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 81 | 	var cmd tea.Cmd
 82 | 	var cmds []tea.Cmd
 83 | 	switch msg := msg.(type) {
 84 | 	case tea.KeyMsg:
 85 | 		switch msg.String() {
 86 | 		case "ctrl+c", "q":
 87 | 			return m, tea.Quit
 88 | 		case "tab":
 89 | 			if m.state == timerView {
 90 | 				m.state = spinnerView
 91 | 			} else {
 92 | 				m.state = timerView
 93 | 			}
 94 | 		case "n":
 95 | 			if m.state == timerView {
 96 | 				m.timer = timer.New(defaultTime)
 97 | 				cmds = append(cmds, m.timer.Init())
 98 | 			} else {
 99 | 				m.Next()
100 | 				m.resetSpinner()
101 | 				cmds = append(cmds, m.spinner.Tick)
102 | 			}
103 | 		}
104 | 		switch m.state {
105 | 		// update whichever model is focused
106 | 		case spinnerView:
107 | 			m.spinner, cmd = m.spinner.Update(msg)
108 | 			cmds = append(cmds, cmd)
109 | 		default:
110 | 			m.timer, cmd = m.timer.Update(msg)
111 | 			cmds = append(cmds, cmd)
112 | 		}
113 | 	case spinner.TickMsg:
114 | 		m.spinner, cmd = m.spinner.Update(msg)
115 | 		cmds = append(cmds, cmd)
116 | 	case timer.TickMsg:
117 | 		m.timer, cmd = m.timer.Update(msg)
118 | 		cmds = append(cmds, cmd)
119 | 	}
120 | 	return m, tea.Batch(cmds...)
121 | }
122 | 
123 | func (m mainModel) View() string {
124 | 	var s string
125 | 	model := m.currentFocusedModel()
126 | 	if m.state == timerView {
127 | 		s += lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), modelStyle.Render(m.spinner.View()))
128 | 	} else {
129 | 		s += lipgloss.JoinHorizontal(lipgloss.Top, modelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), focusedModelStyle.Render(m.spinner.View()))
130 | 	}
131 | 	s += helpStyle.Render(fmt.Sprintf("\ntab: focus next • n: new %s • q: exit\n", model))
132 | 	return s
133 | }
134 | 
135 | func (m mainModel) currentFocusedModel() string {
136 | 	if m.state == timerView {
137 | 		return "timer"
138 | 	}
139 | 	return "spinner"
140 | }
141 | 
142 | func (m *mainModel) Next() {
143 | 	if m.index == len(spinners)-1 {
144 | 		m.index = 0
145 | 	} else {
146 | 		m.index++
147 | 	}
148 | }
149 | 
150 | func (m *mainModel) resetSpinner() {
151 | 	m.spinner = spinner.New()
152 | 	m.spinner.Style = spinnerStyle
153 | 	m.spinner.Spinner = spinners[m.index]
154 | }
155 | 
156 | func main() {
157 | 	p := tea.NewProgram(newModel(defaultTime))
158 | 
159 | 	if _, err := p.Run(); err != nil {
160 | 		log.Fatal(err)
161 | 	}
162 | }
163 | 


--------------------------------------------------------------------------------
/examples/credit-card-form/README.md:
--------------------------------------------------------------------------------
1 | # Credit Card Form
2 | 
3 | <img width="800" src="./credit-card-form.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/credit-card-form/credit-card-form.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/credit-card-form/credit-card-form.gif


--------------------------------------------------------------------------------
/examples/credit-card-form/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"log"
  6 | 	"strconv"
  7 | 	"strings"
  8 | 
  9 | 	"github.com/charmbracelet/bubbles/textinput"
 10 | 	tea "github.com/charmbracelet/bubbletea"
 11 | 	"github.com/charmbracelet/lipgloss"
 12 | )
 13 | 
 14 | func main() {
 15 | 	p := tea.NewProgram(initialModel())
 16 | 
 17 | 	if _, err := p.Run(); err != nil {
 18 | 		log.Fatal(err)
 19 | 	}
 20 | }
 21 | 
 22 | type (
 23 | 	errMsg error
 24 | )
 25 | 
 26 | const (
 27 | 	ccn = iota
 28 | 	exp
 29 | 	cvv
 30 | )
 31 | 
 32 | const (
 33 | 	hotPink  = lipgloss.Color("#FF06B7")
 34 | 	darkGray = lipgloss.Color("#767676")
 35 | )
 36 | 
 37 | var (
 38 | 	inputStyle    = lipgloss.NewStyle().Foreground(hotPink)
 39 | 	continueStyle = lipgloss.NewStyle().Foreground(darkGray)
 40 | )
 41 | 
 42 | type model struct {
 43 | 	inputs  []textinput.Model
 44 | 	focused int
 45 | 	err     error
 46 | }
 47 | 
 48 | // Validator functions to ensure valid input
 49 | func ccnValidator(s string) error {
 50 | 	// Credit Card Number should a string less than 20 digits
 51 | 	// It should include 16 integers and 3 spaces
 52 | 	if len(s) > 16+3 {
 53 | 		return fmt.Errorf("CCN is too long")
 54 | 	}
 55 | 
 56 | 	if len(s) == 0 || len(s)%5 != 0 && (s[len(s)-1] < '0' || s[len(s)-1] > '9') {
 57 | 		return fmt.Errorf("CCN is invalid")
 58 | 	}
 59 | 
 60 | 	// The last digit should be a number unless it is a multiple of 4 in which
 61 | 	// case it should be a space
 62 | 	if len(s)%5 == 0 && s[len(s)-1] != ' ' {
 63 | 		return fmt.Errorf("CCN must separate groups with spaces")
 64 | 	}
 65 | 
 66 | 	// The remaining digits should be integers
 67 | 	c := strings.ReplaceAll(s, " ", "")
 68 | 	_, err := strconv.ParseInt(c, 10, 64)
 69 | 
 70 | 	return err
 71 | }
 72 | 
 73 | func expValidator(s string) error {
 74 | 	// The 3 character should be a slash (/)
 75 | 	// The rest should be numbers
 76 | 	e := strings.ReplaceAll(s, "/", "")
 77 | 	_, err := strconv.ParseInt(e, 10, 64)
 78 | 	if err != nil {
 79 | 		return fmt.Errorf("EXP is invalid")
 80 | 	}
 81 | 
 82 | 	// There should be only one slash and it should be in the 2nd index (3rd character)
 83 | 	if len(s) >= 3 && (strings.Index(s, "/") != 2 || strings.LastIndex(s, "/") != 2) {
 84 | 		return fmt.Errorf("EXP is invalid")
 85 | 	}
 86 | 
 87 | 	return nil
 88 | }
 89 | 
 90 | func cvvValidator(s string) error {
 91 | 	// The CVV should be a number of 3 digits
 92 | 	// Since the input will already ensure that the CVV is a string of length 3,
 93 | 	// All we need to do is check that it is a number
 94 | 	_, err := strconv.ParseInt(s, 10, 64)
 95 | 	return err
 96 | }
 97 | 
 98 | func initialModel() model {
 99 | 	var inputs []textinput.Model = make([]textinput.Model, 3)
100 | 	inputs[ccn] = textinput.New()
101 | 	inputs[ccn].Placeholder = "4505 **** **** 1234"
102 | 	inputs[ccn].Focus()
103 | 	inputs[ccn].CharLimit = 20
104 | 	inputs[ccn].Width = 30
105 | 	inputs[ccn].Prompt = ""
106 | 	inputs[ccn].Validate = ccnValidator
107 | 
108 | 	inputs[exp] = textinput.New()
109 | 	inputs[exp].Placeholder = "MM/YY "
110 | 	inputs[exp].CharLimit = 5
111 | 	inputs[exp].Width = 5
112 | 	inputs[exp].Prompt = ""
113 | 	inputs[exp].Validate = expValidator
114 | 
115 | 	inputs[cvv] = textinput.New()
116 | 	inputs[cvv].Placeholder = "XXX"
117 | 	inputs[cvv].CharLimit = 3
118 | 	inputs[cvv].Width = 5
119 | 	inputs[cvv].Prompt = ""
120 | 	inputs[cvv].Validate = cvvValidator
121 | 
122 | 	return model{
123 | 		inputs:  inputs,
124 | 		focused: 0,
125 | 		err:     nil,
126 | 	}
127 | }
128 | 
129 | func (m model) Init() tea.Cmd {
130 | 	return textinput.Blink
131 | }
132 | 
133 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
134 | 	var cmds []tea.Cmd = make([]tea.Cmd, len(m.inputs))
135 | 
136 | 	switch msg := msg.(type) {
137 | 	case tea.KeyMsg:
138 | 		switch msg.Type {
139 | 		case tea.KeyEnter:
140 | 			if m.focused == len(m.inputs)-1 {
141 | 				return m, tea.Quit
142 | 			}
143 | 			m.nextInput()
144 | 		case tea.KeyCtrlC, tea.KeyEsc:
145 | 			return m, tea.Quit
146 | 		case tea.KeyShiftTab, tea.KeyCtrlP:
147 | 			m.prevInput()
148 | 		case tea.KeyTab, tea.KeyCtrlN:
149 | 			m.nextInput()
150 | 		}
151 | 		for i := range m.inputs {
152 | 			m.inputs[i].Blur()
153 | 		}
154 | 		m.inputs[m.focused].Focus()
155 | 
156 | 	// We handle errors just like any other message
157 | 	case errMsg:
158 | 		m.err = msg
159 | 		return m, nil
160 | 	}
161 | 
162 | 	for i := range m.inputs {
163 | 		m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
164 | 	}
165 | 	return m, tea.Batch(cmds...)
166 | }
167 | 
168 | func (m model) View() string {
169 | 	return fmt.Sprintf(
170 | 		` Total: $21.50:
171 | 
172 |  %s
173 |  %s
174 | 
175 |  %s  %s
176 |  %s  %s
177 | 
178 |  %s
179 | `,
180 | 		inputStyle.Width(30).Render("Card Number"),
181 | 		m.inputs[ccn].View(),
182 | 		inputStyle.Width(6).Render("EXP"),
183 | 		inputStyle.Width(6).Render("CVV"),
184 | 		m.inputs[exp].View(),
185 | 		m.inputs[cvv].View(),
186 | 		continueStyle.Render("Continue ->"),
187 | 	) + "\n"
188 | }
189 | 
190 | // nextInput focuses the next input field
191 | func (m *model) nextInput() {
192 | 	m.focused = (m.focused + 1) % len(m.inputs)
193 | }
194 | 
195 | // prevInput focuses the previous input field
196 | func (m *model) prevInput() {
197 | 	m.focused--
198 | 	// Wrap around
199 | 	if m.focused < 0 {
200 | 		m.focused = len(m.inputs) - 1
201 | 	}
202 | }
203 | 


--------------------------------------------------------------------------------
/examples/debounce/README.md:
--------------------------------------------------------------------------------
1 | # Debounce
2 | 
3 | <img width="800" src="./debounce.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/debounce/debounce.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/debounce/debounce.gif


--------------------------------------------------------------------------------
/examples/debounce/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // This example illustrates how to debounce commands.
 4 | //
 5 | // When the user presses a key we increment the "tag" value on the model and,
 6 | // after a short delay, we include that tag value in the message produced
 7 | // by the Tick command.
 8 | //
 9 | // In a subsequent Update, if the tag in the Msg matches current tag on the
10 | // model's state we know that the debouncing is complete and we can proceed as
11 | // normal. If not, we simply ignore the inbound message.
12 | 
13 | import (
14 | 	"fmt"
15 | 	"os"
16 | 	"time"
17 | 
18 | 	tea "github.com/charmbracelet/bubbletea"
19 | )
20 | 
21 | const debounceDuration = time.Second
22 | 
23 | type exitMsg int
24 | 
25 | type model struct {
26 | 	tag int
27 | }
28 | 
29 | func (m model) Init() tea.Cmd {
30 | 	return nil
31 | }
32 | 
33 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
34 | 	switch msg := msg.(type) {
35 | 	case tea.KeyMsg:
36 | 		// Increment the tag on the model...
37 | 		m.tag++
38 | 		return m, tea.Tick(debounceDuration, func(_ time.Time) tea.Msg {
39 | 			// ...and include a copy of that tag value in the message.
40 | 			return exitMsg(m.tag)
41 | 		})
42 | 	case exitMsg:
43 | 		// If the tag in the message doesn't match the tag on the model then we
44 | 		// know that this message was not the last one sent and another is on
45 | 		// the way. If that's the case we know, we can ignore this message.
46 | 		// Otherwise, the debounce timeout has passed and this message is a
47 | 		// valid debounced one.
48 | 		if int(msg) == m.tag {
49 | 			return m, tea.Quit
50 | 		}
51 | 	}
52 | 
53 | 	return m, nil
54 | }
55 | 
56 | func (m model) View() string {
57 | 	return fmt.Sprintf("Key presses: %d", m.tag) +
58 | 		"\nTo exit press any key, then wait for one second without pressing anything."
59 | }
60 | 
61 | func main() {
62 | 	if _, err := tea.NewProgram(model{}).Run(); err != nil {
63 | 		fmt.Println("uh oh:", err)
64 | 		os.Exit(1)
65 | 	}
66 | }
67 | 


--------------------------------------------------------------------------------
/examples/exec/README.md:
--------------------------------------------------------------------------------
1 | # Exec
2 | 
3 | <img width="800" src="./exec.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/exec/exec.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/exec/exec.gif


--------------------------------------------------------------------------------
/examples/exec/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 	"os/exec"
 7 | 
 8 | 	tea "github.com/charmbracelet/bubbletea"
 9 | )
10 | 
11 | type editorFinishedMsg struct{ err error }
12 | 
13 | func openEditor() tea.Cmd {
14 | 	editor := os.Getenv("EDITOR")
15 | 	if editor == "" {
16 | 		editor = "vim"
17 | 	}
18 | 	c := exec.Command(editor) //nolint:gosec
19 | 	return tea.ExecProcess(c, func(err error) tea.Msg {
20 | 		return editorFinishedMsg{err}
21 | 	})
22 | }
23 | 
24 | type model struct {
25 | 	altscreenActive bool
26 | 	err             error
27 | }
28 | 
29 | func (m model) Init() tea.Cmd {
30 | 	return nil
31 | }
32 | 
33 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
34 | 	switch msg := msg.(type) {
35 | 	case tea.KeyMsg:
36 | 		switch msg.String() {
37 | 		case "a":
38 | 			m.altscreenActive = !m.altscreenActive
39 | 			cmd := tea.EnterAltScreen
40 | 			if !m.altscreenActive {
41 | 				cmd = tea.ExitAltScreen
42 | 			}
43 | 			return m, cmd
44 | 		case "e":
45 | 			return m, openEditor()
46 | 		case "ctrl+c", "q":
47 | 			return m, tea.Quit
48 | 		}
49 | 	case editorFinishedMsg:
50 | 		if msg.err != nil {
51 | 			m.err = msg.err
52 | 			return m, tea.Quit
53 | 		}
54 | 	}
55 | 	return m, nil
56 | }
57 | 
58 | func (m model) View() string {
59 | 	if m.err != nil {
60 | 		return "Error: " + m.err.Error() + "\n"
61 | 	}
62 | 	return "Press 'e' to open your EDITOR.\nPress 'a' to toggle the altscreen\nPress 'q' to quit.\n"
63 | }
64 | 
65 | func main() {
66 | 	m := model{}
67 | 	if _, err := tea.NewProgram(m).Run(); err != nil {
68 | 		fmt.Println("Error running program:", err)
69 | 		os.Exit(1)
70 | 	}
71 | }
72 | 


--------------------------------------------------------------------------------
/examples/file-picker/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"fmt"
 6 | 	"os"
 7 | 	"strings"
 8 | 	"time"
 9 | 
10 | 	"github.com/charmbracelet/bubbles/filepicker"
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | )
13 | 
14 | type model struct {
15 | 	filepicker   filepicker.Model
16 | 	selectedFile string
17 | 	quitting     bool
18 | 	err          error
19 | }
20 | 
21 | type clearErrorMsg struct{}
22 | 
23 | func clearErrorAfter(t time.Duration) tea.Cmd {
24 | 	return tea.Tick(t, func(_ time.Time) tea.Msg {
25 | 		return clearErrorMsg{}
26 | 	})
27 | }
28 | 
29 | func (m model) Init() tea.Cmd {
30 | 	return m.filepicker.Init()
31 | }
32 | 
33 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
34 | 	switch msg := msg.(type) {
35 | 	case tea.KeyMsg:
36 | 		switch msg.String() {
37 | 		case "ctrl+c", "q":
38 | 			m.quitting = true
39 | 			return m, tea.Quit
40 | 		}
41 | 	case clearErrorMsg:
42 | 		m.err = nil
43 | 	}
44 | 
45 | 	var cmd tea.Cmd
46 | 	m.filepicker, cmd = m.filepicker.Update(msg)
47 | 
48 | 	// Did the user select a file?
49 | 	if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
50 | 		// Get the path of the selected file.
51 | 		m.selectedFile = path
52 | 	}
53 | 
54 | 	// Did the user select a disabled file?
55 | 	// This is only necessary to display an error to the user.
56 | 	if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect {
57 | 		// Let's clear the selectedFile and display an error.
58 | 		m.err = errors.New(path + " is not valid.")
59 | 		m.selectedFile = ""
60 | 		return m, tea.Batch(cmd, clearErrorAfter(2*time.Second))
61 | 	}
62 | 
63 | 	return m, cmd
64 | }
65 | 
66 | func (m model) View() string {
67 | 	if m.quitting {
68 | 		return ""
69 | 	}
70 | 	var s strings.Builder
71 | 	s.WriteString("\n  ")
72 | 	if m.err != nil {
73 | 		s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error()))
74 | 	} else if m.selectedFile == "" {
75 | 		s.WriteString("Pick a file:")
76 | 	} else {
77 | 		s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile))
78 | 	}
79 | 	s.WriteString("\n\n" + m.filepicker.View() + "\n")
80 | 	return s.String()
81 | }
82 | 
83 | func main() {
84 | 	fp := filepicker.New()
85 | 	fp.AllowedTypes = []string{".mod", ".sum", ".go", ".txt", ".md"}
86 | 	fp.CurrentDirectory, _ = os.UserHomeDir()
87 | 
88 | 	m := model{
89 | 		filepicker: fp,
90 | 	}
91 | 	tm, _ := tea.NewProgram(&m).Run()
92 | 	mm := tm.(model)
93 | 	fmt.Println("\n  You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n")
94 | }
95 | 


--------------------------------------------------------------------------------
/examples/focus-blur/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program that handled losing and acquiring focus.
 4 | 
 5 | import (
 6 | 	"log"
 7 | 
 8 | 	tea "github.com/charmbracelet/bubbletea"
 9 | )
10 | 
11 | func main() {
12 | 	p := tea.NewProgram(model{
13 | 		// assume we start focused...
14 | 		focused:   true,
15 | 		reporting: true,
16 | 	}, tea.WithReportFocus())
17 | 	if _, err := p.Run(); err != nil {
18 | 		log.Fatal(err)
19 | 	}
20 | }
21 | 
22 | type model struct {
23 | 	focused   bool
24 | 	reporting bool
25 | }
26 | 
27 | func (m model) Init() tea.Cmd {
28 | 	return nil
29 | }
30 | 
31 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
32 | 	switch msg := msg.(type) {
33 | 	case tea.FocusMsg:
34 | 		m.focused = true
35 | 	case tea.BlurMsg:
36 | 		m.focused = false
37 | 	case tea.KeyMsg:
38 | 		switch msg.String() {
39 | 		case "t":
40 | 			m.reporting = !m.reporting
41 | 		case "ctrl+c", "q":
42 | 			return m, tea.Quit
43 | 		}
44 | 	}
45 | 
46 | 	return m, nil
47 | }
48 | 
49 | func (m model) View() string {
50 | 	s := "Hi. Focus report is currently "
51 | 	if m.reporting {
52 | 		s += "enabled"
53 | 	} else {
54 | 		s += "disabled"
55 | 	}
56 | 	s += ".\n\n"
57 | 
58 | 	if m.reporting {
59 | 		if m.focused {
60 | 			s += "This program is currently focused!"
61 | 		} else {
62 | 			s += "This program is currently blurred!"
63 | 		}
64 | 	}
65 | 	return s + "\n\nTo quit sooner press ctrl-c, or t to toggle focus reporting...\n"
66 | }
67 | 


--------------------------------------------------------------------------------
/examples/fullscreen/README.md:
--------------------------------------------------------------------------------
1 | # Full Screen
2 | 
3 | <img width="800" src="./fullscreen.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/fullscreen/fullscreen.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/fullscreen/fullscreen.gif


--------------------------------------------------------------------------------
/examples/fullscreen/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program that opens the alternate screen buffer then counts down
 4 | // from 5 and then exits.
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"log"
 9 | 	"time"
10 | 
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | )
13 | 
14 | type model int
15 | 
16 | type tickMsg time.Time
17 | 
18 | func main() {
19 | 	p := tea.NewProgram(model(5), tea.WithAltScreen())
20 | 	if _, err := p.Run(); err != nil {
21 | 		log.Fatal(err)
22 | 	}
23 | }
24 | 
25 | func (m model) Init() tea.Cmd {
26 | 	return tick()
27 | }
28 | 
29 | func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
30 | 	switch msg := message.(type) {
31 | 	case tea.KeyMsg:
32 | 		switch msg.String() {
33 | 		case "q", "esc", "ctrl+c":
34 | 			return m, tea.Quit
35 | 		}
36 | 
37 | 	case tickMsg:
38 | 		m--
39 | 		if m <= 0 {
40 | 			return m, tea.Quit
41 | 		}
42 | 		return m, tick()
43 | 	}
44 | 
45 | 	return m, nil
46 | }
47 | 
48 | func (m model) View() string {
49 | 	return fmt.Sprintf("\n\n     Hi. This program will exit in %d seconds...", m)
50 | }
51 | 
52 | func tick() tea.Cmd {
53 | 	return tea.Tick(time.Second, func(t time.Time) tea.Msg {
54 | 		return tickMsg(t)
55 | 	})
56 | }
57 | 


--------------------------------------------------------------------------------
/examples/glamour/README.md:
--------------------------------------------------------------------------------
1 | # Glamour
2 | 
3 | <img width="800" src="./glamour.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/glamour/glamour.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/glamour/glamour.gif


--------------------------------------------------------------------------------
/examples/glamour/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 
  7 | 	"github.com/charmbracelet/bubbles/viewport"
  8 | 	tea "github.com/charmbracelet/bubbletea"
  9 | 	"github.com/charmbracelet/glamour"
 10 | 	"github.com/charmbracelet/lipgloss"
 11 | )
 12 | 
 13 | const content = `
 14 | # Today’s Menu
 15 | 
 16 | ## Appetizers
 17 | 
 18 | | Name        | Price | Notes                           |
 19 | | ---         | ---   | ---                             |
 20 | | Tsukemono   | $2    | Just an appetizer               |
 21 | | Tomato Soup | $4    | Made with San Marzano tomatoes  |
 22 | | Okonomiyaki | $4    | Takes a few minutes to make     |
 23 | | Curry       | $3    | We can add squash if you’d like |
 24 | 
 25 | ## Seasonal Dishes
 26 | 
 27 | | Name                 | Price | Notes              |
 28 | | ---                  | ---   | ---                |
 29 | | Steamed bitter melon | $2    | Not so bitter      |
 30 | | Takoyaki             | $3    | Fun to eat         |
 31 | | Winter squash        | $3    | Today it's pumpkin |
 32 | 
 33 | ## Desserts
 34 | 
 35 | | Name         | Price | Notes                 |
 36 | | ---          | ---   | ---                   |
 37 | | Dorayaki     | $4    | Looks good on rabbits |
 38 | | Banana Split | $5    | A classic             |
 39 | | Cream Puff   | $3    | Pretty creamy!        |
 40 | 
 41 | All our dishes are made in-house by Karen, our chef. Most of our ingredients
 42 | are from our garden or the fish market down the street.
 43 | 
 44 | Some famous people that have eaten here lately:
 45 | 
 46 | * [x] René Redzepi
 47 | * [x] David Chang
 48 | * [ ] Jiro Ono (maybe some day)
 49 | 
 50 | Bon appétit!
 51 | `
 52 | 
 53 | var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render
 54 | 
 55 | type example struct {
 56 | 	viewport viewport.Model
 57 | }
 58 | 
 59 | func newExample() (*example, error) {
 60 | 	const width = 78
 61 | 
 62 | 	vp := viewport.New(width, 20)
 63 | 	vp.Style = lipgloss.NewStyle().
 64 | 		BorderStyle(lipgloss.RoundedBorder()).
 65 | 		BorderForeground(lipgloss.Color("62")).
 66 | 		PaddingRight(2)
 67 | 
 68 | 	// We need to adjust the width of the glamour render from our main width
 69 | 	// to account for a few things:
 70 | 	//
 71 | 	//  * The viewport border width
 72 | 	//  * The viewport padding
 73 | 	//  * The viewport margins
 74 | 	//  * The gutter glamour applies to the left side of the content
 75 | 	//
 76 | 	const glamourGutter = 2
 77 | 	glamourRenderWidth := width - vp.Style.GetHorizontalFrameSize() - glamourGutter
 78 | 
 79 | 	renderer, err := glamour.NewTermRenderer(
 80 | 		glamour.WithAutoStyle(),
 81 | 		glamour.WithWordWrap(glamourRenderWidth),
 82 | 	)
 83 | 	if err != nil {
 84 | 		return nil, err
 85 | 	}
 86 | 
 87 | 	str, err := renderer.Render(content)
 88 | 	if err != nil {
 89 | 		return nil, err
 90 | 	}
 91 | 
 92 | 	vp.SetContent(str)
 93 | 
 94 | 	return &example{
 95 | 		viewport: vp,
 96 | 	}, nil
 97 | }
 98 | 
 99 | func (e example) Init() tea.Cmd {
100 | 	return nil
101 | }
102 | 
103 | func (e example) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
104 | 	switch msg := msg.(type) {
105 | 	case tea.KeyMsg:
106 | 		switch msg.String() {
107 | 		case "q", "ctrl+c", "esc":
108 | 			return e, tea.Quit
109 | 		default:
110 | 			var cmd tea.Cmd
111 | 			e.viewport, cmd = e.viewport.Update(msg)
112 | 			return e, cmd
113 | 		}
114 | 	default:
115 | 		return e, nil
116 | 	}
117 | }
118 | 
119 | func (e example) View() string {
120 | 	return e.viewport.View() + e.helpView()
121 | }
122 | 
123 | func (e example) helpView() string {
124 | 	return helpStyle("\n  ↑/↓: Navigate • q: Quit\n")
125 | }
126 | 
127 | func main() {
128 | 	model, err := newExample()
129 | 	if err != nil {
130 | 		fmt.Println("Could not initialize Bubble Tea model:", err)
131 | 		os.Exit(1)
132 | 	}
133 | 
134 | 	if _, err := tea.NewProgram(model).Run(); err != nil {
135 | 		fmt.Println("Bummer, there's been an error:", err)
136 | 		os.Exit(1)
137 | 	}
138 | }
139 | 


--------------------------------------------------------------------------------
/examples/go.mod:
--------------------------------------------------------------------------------
 1 | module examples
 2 | 
 3 | go 1.23.0
 4 | 
 5 | toolchain go1.24.1
 6 | 
 7 | require (
 8 | 	github.com/charmbracelet/bubbles v0.21.0
 9 | 	github.com/charmbracelet/bubbletea v1.3.4
10 | 	github.com/charmbracelet/glamour v0.10.0
11 | 	github.com/charmbracelet/harmonica v0.2.0
12 | 	github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
13 | 	github.com/charmbracelet/x/exp/teatest v0.0.0-20240521184646-23081fb03b28
14 | 	github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
15 | 	github.com/lucasb-eyer/go-colorful v1.2.0
16 | 	github.com/mattn/go-isatty v0.0.20
17 | )
18 | 
19 | require (
20 | 	github.com/alecthomas/chroma/v2 v2.14.0 // indirect
21 | 	github.com/atotto/clipboard v0.1.4 // indirect
22 | 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
23 | 	github.com/aymanbagabas/go-udiff v0.2.0 // indirect
24 | 	github.com/aymerick/douceur v0.2.0 // indirect
25 | 	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
26 | 	github.com/charmbracelet/x/ansi v0.9.2 // indirect
27 | 	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
28 | 	github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
29 | 	github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
30 | 	github.com/charmbracelet/x/term v0.2.1 // indirect
31 | 	github.com/dlclark/regexp2 v1.11.0 // indirect
32 | 	github.com/dustin/go-humanize v1.0.1 // indirect
33 | 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
34 | 	github.com/gorilla/css v1.0.1 // indirect
35 | 	github.com/mattn/go-localereader v0.0.1 // indirect
36 | 	github.com/mattn/go-runewidth v0.0.16 // indirect
37 | 	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
38 | 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
39 | 	github.com/muesli/cancelreader v0.2.2 // indirect
40 | 	github.com/muesli/reflow v0.3.0 // indirect
41 | 	github.com/muesli/termenv v0.16.0 // indirect
42 | 	github.com/rivo/uniseg v0.4.7 // indirect
43 | 	github.com/sahilm/fuzzy v0.1.1 // indirect
44 | 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
45 | 	github.com/yuin/goldmark v1.7.8 // indirect
46 | 	github.com/yuin/goldmark-emoji v1.0.5 // indirect
47 | 	golang.org/x/net v0.38.0 // indirect
48 | 	golang.org/x/sync v0.14.0 // indirect
49 | 	golang.org/x/sys v0.33.0 // indirect
50 | 	golang.org/x/term v0.31.0 // indirect
51 | 	golang.org/x/text v0.24.0 // indirect
52 | )
53 | 
54 | replace github.com/charmbracelet/bubbletea => ../
55 | 


--------------------------------------------------------------------------------
/examples/help/README.md:
--------------------------------------------------------------------------------
1 | # Help
2 | 
3 | <img width="800" src="./help.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/help/help.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/help/help.gif


--------------------------------------------------------------------------------
/examples/help/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 	"strings"
  7 | 
  8 | 	"github.com/charmbracelet/bubbles/help"
  9 | 	"github.com/charmbracelet/bubbles/key"
 10 | 	tea "github.com/charmbracelet/bubbletea"
 11 | 	"github.com/charmbracelet/lipgloss"
 12 | )
 13 | 
 14 | // keyMap defines a set of keybindings. To work for help it must satisfy
 15 | // key.Map. It could also very easily be a map[string]key.Binding.
 16 | type keyMap struct {
 17 | 	Up    key.Binding
 18 | 	Down  key.Binding
 19 | 	Left  key.Binding
 20 | 	Right key.Binding
 21 | 	Help  key.Binding
 22 | 	Quit  key.Binding
 23 | }
 24 | 
 25 | // ShortHelp returns keybindings to be shown in the mini help view. It's part
 26 | // of the key.Map interface.
 27 | func (k keyMap) ShortHelp() []key.Binding {
 28 | 	return []key.Binding{k.Help, k.Quit}
 29 | }
 30 | 
 31 | // FullHelp returns keybindings for the expanded help view. It's part of the
 32 | // key.Map interface.
 33 | func (k keyMap) FullHelp() [][]key.Binding {
 34 | 	return [][]key.Binding{
 35 | 		{k.Up, k.Down, k.Left, k.Right}, // first column
 36 | 		{k.Help, k.Quit},                // second column
 37 | 	}
 38 | }
 39 | 
 40 | var keys = keyMap{
 41 | 	Up: key.NewBinding(
 42 | 		key.WithKeys("up", "k"),
 43 | 		key.WithHelp("↑/k", "move up"),
 44 | 	),
 45 | 	Down: key.NewBinding(
 46 | 		key.WithKeys("down", "j"),
 47 | 		key.WithHelp("↓/j", "move down"),
 48 | 	),
 49 | 	Left: key.NewBinding(
 50 | 		key.WithKeys("left", "h"),
 51 | 		key.WithHelp("←/h", "move left"),
 52 | 	),
 53 | 	Right: key.NewBinding(
 54 | 		key.WithKeys("right", "l"),
 55 | 		key.WithHelp("→/l", "move right"),
 56 | 	),
 57 | 	Help: key.NewBinding(
 58 | 		key.WithKeys("?"),
 59 | 		key.WithHelp("?", "toggle help"),
 60 | 	),
 61 | 	Quit: key.NewBinding(
 62 | 		key.WithKeys("q", "esc", "ctrl+c"),
 63 | 		key.WithHelp("q", "quit"),
 64 | 	),
 65 | }
 66 | 
 67 | type model struct {
 68 | 	keys       keyMap
 69 | 	help       help.Model
 70 | 	inputStyle lipgloss.Style
 71 | 	lastKey    string
 72 | 	quitting   bool
 73 | }
 74 | 
 75 | func newModel() model {
 76 | 	return model{
 77 | 		keys:       keys,
 78 | 		help:       help.New(),
 79 | 		inputStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF75B7")),
 80 | 	}
 81 | }
 82 | 
 83 | func (m model) Init() tea.Cmd {
 84 | 	return nil
 85 | }
 86 | 
 87 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 88 | 	switch msg := msg.(type) {
 89 | 	case tea.WindowSizeMsg:
 90 | 		// If we set a width on the help menu it can gracefully truncate
 91 | 		// its view as needed.
 92 | 		m.help.Width = msg.Width
 93 | 
 94 | 	case tea.KeyMsg:
 95 | 		switch {
 96 | 		case key.Matches(msg, m.keys.Up):
 97 | 			m.lastKey = "↑"
 98 | 		case key.Matches(msg, m.keys.Down):
 99 | 			m.lastKey = "↓"
100 | 		case key.Matches(msg, m.keys.Left):
101 | 			m.lastKey = "←"
102 | 		case key.Matches(msg, m.keys.Right):
103 | 			m.lastKey = "→"
104 | 		case key.Matches(msg, m.keys.Help):
105 | 			m.help.ShowAll = !m.help.ShowAll
106 | 		case key.Matches(msg, m.keys.Quit):
107 | 			m.quitting = true
108 | 			return m, tea.Quit
109 | 		}
110 | 	}
111 | 
112 | 	return m, nil
113 | }
114 | 
115 | func (m model) View() string {
116 | 	if m.quitting {
117 | 		return "Bye!\n"
118 | 	}
119 | 
120 | 	var status string
121 | 	if m.lastKey == "" {
122 | 		status = "Waiting for input..."
123 | 	} else {
124 | 		status = "You chose: " + m.inputStyle.Render(m.lastKey)
125 | 	}
126 | 
127 | 	helpView := m.help.View(m.keys)
128 | 	height := 8 - strings.Count(status, "\n") - strings.Count(helpView, "\n")
129 | 
130 | 	return "\n" + status + strings.Repeat("\n", height) + helpView
131 | }
132 | 
133 | func main() {
134 | 	if os.Getenv("HELP_DEBUG") != "" {
135 | 		f, err := tea.LogToFile("debug.log", "help")
136 | 		if err != nil {
137 | 			fmt.Println("Couldn't open a file for logging:", err)
138 | 			os.Exit(1)
139 | 		}
140 | 		defer f.Close() // nolint:errcheck
141 | 	}
142 | 
143 | 	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
144 | 		fmt.Printf("Could not start program :(\n%v\n", err)
145 | 		os.Exit(1)
146 | 	}
147 | }
148 | 


--------------------------------------------------------------------------------
/examples/http/README.md:
--------------------------------------------------------------------------------
1 | # HTTP
2 | 
3 | <img width="800" src="./http.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/http/http.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/http/http.gif


--------------------------------------------------------------------------------
/examples/http/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program that makes a GET request and prints the response status.
 4 | 
 5 | import (
 6 | 	"fmt"
 7 | 	"log"
 8 | 	"net/http"
 9 | 	"time"
10 | 
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | )
13 | 
14 | const url = "https://charm.sh/"
15 | 
16 | type model struct {
17 | 	status int
18 | 	err    error
19 | }
20 | 
21 | type statusMsg int
22 | 
23 | type errMsg struct{ error }
24 | 
25 | func (e errMsg) Error() string { return e.error.Error() }
26 | 
27 | func main() {
28 | 	p := tea.NewProgram(model{})
29 | 	if _, err := p.Run(); err != nil {
30 | 		log.Fatal(err)
31 | 	}
32 | }
33 | 
34 | func (m model) Init() tea.Cmd {
35 | 	return checkServer
36 | }
37 | 
38 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
39 | 	switch msg := msg.(type) {
40 | 	case tea.KeyMsg:
41 | 		switch msg.String() {
42 | 		case "q", "ctrl+c", "esc":
43 | 			return m, tea.Quit
44 | 		default:
45 | 			return m, nil
46 | 		}
47 | 
48 | 	case statusMsg:
49 | 		m.status = int(msg)
50 | 		return m, tea.Quit
51 | 
52 | 	case errMsg:
53 | 		m.err = msg
54 | 		return m, nil
55 | 
56 | 	default:
57 | 		return m, nil
58 | 	}
59 | }
60 | 
61 | func (m model) View() string {
62 | 	s := fmt.Sprintf("Checking %s...", url)
63 | 	if m.err != nil {
64 | 		s += fmt.Sprintf("something went wrong: %s", m.err)
65 | 	} else if m.status != 0 {
66 | 		s += fmt.Sprintf("%d %s", m.status, http.StatusText(m.status))
67 | 	}
68 | 	return s + "\n"
69 | }
70 | 
71 | func checkServer() tea.Msg {
72 | 	c := &http.Client{
73 | 		Timeout: 10 * time.Second,
74 | 	}
75 | 	res, err := c.Get(url)
76 | 	if err != nil {
77 | 		return errMsg{err}
78 | 	}
79 | 	defer res.Body.Close() // nolint:errcheck
80 | 
81 | 	return statusMsg(res.StatusCode)
82 | }
83 | 


--------------------------------------------------------------------------------
/examples/list-default/README.md:
--------------------------------------------------------------------------------
1 | # Default List
2 | 
3 | <img width="800" src="./list-default.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/list-default/list-default.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/list-default/list-default.gif


--------------------------------------------------------------------------------
/examples/list-default/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 
 7 | 	"github.com/charmbracelet/bubbles/list"
 8 | 	tea "github.com/charmbracelet/bubbletea"
 9 | 	"github.com/charmbracelet/lipgloss"
10 | )
11 | 
12 | var docStyle = lipgloss.NewStyle().Margin(1, 2)
13 | 
14 | type item struct {
15 | 	title, desc string
16 | }
17 | 
18 | func (i item) Title() string       { return i.title }
19 | func (i item) Description() string { return i.desc }
20 | func (i item) FilterValue() string { return i.title }
21 | 
22 | type model struct {
23 | 	list list.Model
24 | }
25 | 
26 | func (m model) Init() tea.Cmd {
27 | 	return nil
28 | }
29 | 
30 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
31 | 	switch msg := msg.(type) {
32 | 	case tea.KeyMsg:
33 | 		if msg.String() == "ctrl+c" {
34 | 			return m, tea.Quit
35 | 		}
36 | 	case tea.WindowSizeMsg:
37 | 		h, v := docStyle.GetFrameSize()
38 | 		m.list.SetSize(msg.Width-h, msg.Height-v)
39 | 	}
40 | 
41 | 	var cmd tea.Cmd
42 | 	m.list, cmd = m.list.Update(msg)
43 | 	return m, cmd
44 | }
45 | 
46 | func (m model) View() string {
47 | 	return docStyle.Render(m.list.View())
48 | }
49 | 
50 | func main() {
51 | 	items := []list.Item{
52 | 		item{title: "Raspberry Pi’s", desc: "I have ’em all over my house"},
53 | 		item{title: "Nutella", desc: "It's good on toast"},
54 | 		item{title: "Bitter melon", desc: "It cools you down"},
55 | 		item{title: "Nice socks", desc: "And by that I mean socks without holes"},
56 | 		item{title: "Eight hours of sleep", desc: "I had this once"},
57 | 		item{title: "Cats", desc: "Usually"},
58 | 		item{title: "Plantasia, the album", desc: "My plants love it too"},
59 | 		item{title: "Pour over coffee", desc: "It takes forever to make though"},
60 | 		item{title: "VR", desc: "Virtual reality...what is there to say?"},
61 | 		item{title: "Noguchi Lamps", desc: "Such pleasing organic forms"},
62 | 		item{title: "Linux", desc: "Pretty much the best OS"},
63 | 		item{title: "Business school", desc: "Just kidding"},
64 | 		item{title: "Pottery", desc: "Wet clay is a great feeling"},
65 | 		item{title: "Shampoo", desc: "Nothing like clean hair"},
66 | 		item{title: "Table tennis", desc: "It’s surprisingly exhausting"},
67 | 		item{title: "Milk crates", desc: "Great for packing in your extra stuff"},
68 | 		item{title: "Afternoon tea", desc: "Especially the tea sandwich part"},
69 | 		item{title: "Stickers", desc: "The thicker the vinyl the better"},
70 | 		item{title: "20° Weather", desc: "Celsius, not Fahrenheit"},
71 | 		item{title: "Warm light", desc: "Like around 2700 Kelvin"},
72 | 		item{title: "The vernal equinox", desc: "The autumnal equinox is pretty good too"},
73 | 		item{title: "Gaffer’s tape", desc: "Basically sticky fabric"},
74 | 		item{title: "Terrycloth", desc: "In other words, towel fabric"},
75 | 	}
76 | 
77 | 	m := model{list: list.New(items, list.NewDefaultDelegate(), 0, 0)}
78 | 	m.list.Title = "My Fave Things"
79 | 
80 | 	p := tea.NewProgram(m, tea.WithAltScreen())
81 | 
82 | 	if _, err := p.Run(); err != nil {
83 | 		fmt.Println("Error running program:", err)
84 | 		os.Exit(1)
85 | 	}
86 | }
87 | 


--------------------------------------------------------------------------------
/examples/list-fancy/README.md:
--------------------------------------------------------------------------------
1 | # Fancy List
2 | 
3 | <img width="800" src="./list-fancy.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/list-fancy/delegate.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/bubbles/key"
 5 | 	"github.com/charmbracelet/bubbles/list"
 6 | 	tea "github.com/charmbracelet/bubbletea"
 7 | )
 8 | 
 9 | func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
10 | 	d := list.NewDefaultDelegate()
11 | 
12 | 	d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd {
13 | 		var title string
14 | 
15 | 		if i, ok := m.SelectedItem().(item); ok {
16 | 			title = i.Title()
17 | 		} else {
18 | 			return nil
19 | 		}
20 | 
21 | 		switch msg := msg.(type) {
22 | 		case tea.KeyMsg:
23 | 			switch {
24 | 			case key.Matches(msg, keys.choose):
25 | 				return m.NewStatusMessage(statusMessageStyle("You chose " + title))
26 | 
27 | 			case key.Matches(msg, keys.remove):
28 | 				index := m.Index()
29 | 				m.RemoveItem(index)
30 | 				if len(m.Items()) == 0 {
31 | 					keys.remove.SetEnabled(false)
32 | 				}
33 | 				return m.NewStatusMessage(statusMessageStyle("Deleted " + title))
34 | 			}
35 | 		}
36 | 
37 | 		return nil
38 | 	}
39 | 
40 | 	help := []key.Binding{keys.choose, keys.remove}
41 | 
42 | 	d.ShortHelpFunc = func() []key.Binding {
43 | 		return help
44 | 	}
45 | 
46 | 	d.FullHelpFunc = func() [][]key.Binding {
47 | 		return [][]key.Binding{help}
48 | 	}
49 | 
50 | 	return d
51 | }
52 | 
53 | type delegateKeyMap struct {
54 | 	choose key.Binding
55 | 	remove key.Binding
56 | }
57 | 
58 | // Additional short help entries. This satisfies the help.KeyMap interface and
59 | // is entirely optional.
60 | func (d delegateKeyMap) ShortHelp() []key.Binding {
61 | 	return []key.Binding{
62 | 		d.choose,
63 | 		d.remove,
64 | 	}
65 | }
66 | 
67 | // Additional full help entries. This satisfies the help.KeyMap interface and
68 | // is entirely optional.
69 | func (d delegateKeyMap) FullHelp() [][]key.Binding {
70 | 	return [][]key.Binding{
71 | 		{
72 | 			d.choose,
73 | 			d.remove,
74 | 		},
75 | 	}
76 | }
77 | 
78 | func newDelegateKeyMap() *delegateKeyMap {
79 | 	return &delegateKeyMap{
80 | 		choose: key.NewBinding(
81 | 			key.WithKeys("enter"),
82 | 			key.WithHelp("enter", "choose"),
83 | 		),
84 | 		remove: key.NewBinding(
85 | 			key.WithKeys("x", "backspace"),
86 | 			key.WithHelp("x", "delete"),
87 | 		),
88 | 	}
89 | }
90 | 


--------------------------------------------------------------------------------
/examples/list-fancy/list-fancy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/list-fancy/list-fancy.gif


--------------------------------------------------------------------------------
/examples/list-fancy/randomitems.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"math/rand"
  5 | 	"sync"
  6 | )
  7 | 
  8 | type randomItemGenerator struct {
  9 | 	titles     []string
 10 | 	descs      []string
 11 | 	titleIndex int
 12 | 	descIndex  int
 13 | 	mtx        *sync.Mutex
 14 | 	shuffle    *sync.Once
 15 | }
 16 | 
 17 | func (r *randomItemGenerator) reset() {
 18 | 	r.mtx = &sync.Mutex{}
 19 | 	r.shuffle = &sync.Once{}
 20 | 
 21 | 	r.titles = []string{
 22 | 		"Artichoke",
 23 | 		"Baking Flour",
 24 | 		"Bananas",
 25 | 		"Barley",
 26 | 		"Bean Sprouts",
 27 | 		"Bitter Melon",
 28 | 		"Black Cod",
 29 | 		"Blood Orange",
 30 | 		"Brown Sugar",
 31 | 		"Cashew Apple",
 32 | 		"Cashews",
 33 | 		"Cat Food",
 34 | 		"Coconut Milk",
 35 | 		"Cucumber",
 36 | 		"Curry Paste",
 37 | 		"Currywurst",
 38 | 		"Dill",
 39 | 		"Dragonfruit",
 40 | 		"Dried Shrimp",
 41 | 		"Eggs",
 42 | 		"Fish Cake",
 43 | 		"Furikake",
 44 | 		"Garlic",
 45 | 		"Gherkin",
 46 | 		"Ginger",
 47 | 		"Granulated Sugar",
 48 | 		"Grapefruit",
 49 | 		"Green Onion",
 50 | 		"Hazelnuts",
 51 | 		"Heavy whipping cream",
 52 | 		"Honey Dew",
 53 | 		"Horseradish",
 54 | 		"Jicama",
 55 | 		"Kohlrabi",
 56 | 		"Leeks",
 57 | 		"Lentils",
 58 | 		"Licorice Root",
 59 | 		"Meyer Lemons",
 60 | 		"Milk",
 61 | 		"Molasses",
 62 | 		"Muesli",
 63 | 		"Nectarine",
 64 | 		"Niagamo Root",
 65 | 		"Nopal",
 66 | 		"Nutella",
 67 | 		"Oat Milk",
 68 | 		"Oatmeal",
 69 | 		"Olives",
 70 | 		"Papaya",
 71 | 		"Party Gherkin",
 72 | 		"Peppers",
 73 | 		"Persian Lemons",
 74 | 		"Pickle",
 75 | 		"Pineapple",
 76 | 		"Plantains",
 77 | 		"Pocky",
 78 | 		"Powdered Sugar",
 79 | 		"Quince",
 80 | 		"Radish",
 81 | 		"Ramps",
 82 | 		"Star Anise",
 83 | 		"Sweet Potato",
 84 | 		"Tamarind",
 85 | 		"Unsalted Butter",
 86 | 		"Watermelon",
 87 | 		"Weißwurst",
 88 | 		"Yams",
 89 | 		"Yeast",
 90 | 		"Yuzu",
 91 | 		"Snow Peas",
 92 | 	}
 93 | 
 94 | 	r.descs = []string{
 95 | 		"A little weird",
 96 | 		"Bold flavor",
 97 | 		"Can’t get enough",
 98 | 		"Delectable",
 99 | 		"Expensive",
100 | 		"Expired",
101 | 		"Exquisite",
102 | 		"Fresh",
103 | 		"Gimme",
104 | 		"In season",
105 | 		"Kind of spicy",
106 | 		"Looks fresh",
107 | 		"Looks good to me",
108 | 		"Maybe not",
109 | 		"My favorite",
110 | 		"Oh my",
111 | 		"On sale",
112 | 		"Organic",
113 | 		"Questionable",
114 | 		"Really fresh",
115 | 		"Refreshing",
116 | 		"Salty",
117 | 		"Scrumptious",
118 | 		"Delectable",
119 | 		"Slightly sweet",
120 | 		"Smells great",
121 | 		"Tasty",
122 | 		"Too ripe",
123 | 		"At last",
124 | 		"What?",
125 | 		"Wow",
126 | 		"Yum",
127 | 		"Maybe",
128 | 		"Sure, why not?",
129 | 	}
130 | 
131 | 	r.shuffle.Do(func() {
132 | 		shuf := func(x []string) {
133 | 			rand.Shuffle(len(x), func(i, j int) { x[i], x[j] = x[j], x[i] })
134 | 		}
135 | 		shuf(r.titles)
136 | 		shuf(r.descs)
137 | 	})
138 | }
139 | 
140 | func (r *randomItemGenerator) next() item {
141 | 	if r.mtx == nil {
142 | 		r.reset()
143 | 	}
144 | 
145 | 	r.mtx.Lock()
146 | 	defer r.mtx.Unlock()
147 | 
148 | 	i := item{
149 | 		title:       r.titles[r.titleIndex],
150 | 		description: r.descs[r.descIndex],
151 | 	}
152 | 
153 | 	r.titleIndex++
154 | 	if r.titleIndex >= len(r.titles) {
155 | 		r.titleIndex = 0
156 | 	}
157 | 
158 | 	r.descIndex++
159 | 	if r.descIndex >= len(r.descs) {
160 | 		r.descIndex = 0
161 | 	}
162 | 
163 | 	return i
164 | }
165 | 


--------------------------------------------------------------------------------
/examples/list-simple/README.md:
--------------------------------------------------------------------------------
1 | # Simple List
2 | 
3 | <img width="800" src="./list-simple.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/list-simple/list-simple.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/list-simple/list-simple.gif


--------------------------------------------------------------------------------
/examples/list-simple/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"io"
  6 | 	"os"
  7 | 	"strings"
  8 | 
  9 | 	"github.com/charmbracelet/bubbles/list"
 10 | 	tea "github.com/charmbracelet/bubbletea"
 11 | 	"github.com/charmbracelet/lipgloss"
 12 | )
 13 | 
 14 | const listHeight = 14
 15 | 
 16 | var (
 17 | 	titleStyle        = lipgloss.NewStyle().MarginLeft(2)
 18 | 	itemStyle         = lipgloss.NewStyle().PaddingLeft(4)
 19 | 	selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
 20 | 	paginationStyle   = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
 21 | 	helpStyle         = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
 22 | 	quitTextStyle     = lipgloss.NewStyle().Margin(1, 0, 2, 4)
 23 | )
 24 | 
 25 | type item string
 26 | 
 27 | func (i item) FilterValue() string { return "" }
 28 | 
 29 | type itemDelegate struct{}
 30 | 
 31 | func (d itemDelegate) Height() int                             { return 1 }
 32 | func (d itemDelegate) Spacing() int                            { return 0 }
 33 | func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
 34 | func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
 35 | 	i, ok := listItem.(item)
 36 | 	if !ok {
 37 | 		return
 38 | 	}
 39 | 
 40 | 	str := fmt.Sprintf("%d. %s", index+1, i)
 41 | 
 42 | 	fn := itemStyle.Render
 43 | 	if index == m.Index() {
 44 | 		fn = func(s ...string) string {
 45 | 			return selectedItemStyle.Render("> " + strings.Join(s, " "))
 46 | 		}
 47 | 	}
 48 | 
 49 | 	fmt.Fprint(w, fn(str))
 50 | }
 51 | 
 52 | type model struct {
 53 | 	list     list.Model
 54 | 	choice   string
 55 | 	quitting bool
 56 | }
 57 | 
 58 | func (m model) Init() tea.Cmd {
 59 | 	return nil
 60 | }
 61 | 
 62 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 63 | 	switch msg := msg.(type) {
 64 | 	case tea.WindowSizeMsg:
 65 | 		m.list.SetWidth(msg.Width)
 66 | 		return m, nil
 67 | 
 68 | 	case tea.KeyMsg:
 69 | 		switch keypress := msg.String(); keypress {
 70 | 		case "q", "ctrl+c":
 71 | 			m.quitting = true
 72 | 			return m, tea.Quit
 73 | 
 74 | 		case "enter":
 75 | 			i, ok := m.list.SelectedItem().(item)
 76 | 			if ok {
 77 | 				m.choice = string(i)
 78 | 			}
 79 | 			return m, tea.Quit
 80 | 		}
 81 | 	}
 82 | 
 83 | 	var cmd tea.Cmd
 84 | 	m.list, cmd = m.list.Update(msg)
 85 | 	return m, cmd
 86 | }
 87 | 
 88 | func (m model) View() string {
 89 | 	if m.choice != "" {
 90 | 		return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice))
 91 | 	}
 92 | 	if m.quitting {
 93 | 		return quitTextStyle.Render("Not hungry? That’s cool.")
 94 | 	}
 95 | 	return "\n" + m.list.View()
 96 | }
 97 | 
 98 | func main() {
 99 | 	items := []list.Item{
100 | 		item("Ramen"),
101 | 		item("Tomato Soup"),
102 | 		item("Hamburgers"),
103 | 		item("Cheeseburgers"),
104 | 		item("Currywurst"),
105 | 		item("Okonomiyaki"),
106 | 		item("Pasta"),
107 | 		item("Fillet Mignon"),
108 | 		item("Caviar"),
109 | 		item("Just Wine"),
110 | 	}
111 | 
112 | 	const defaultWidth = 20
113 | 
114 | 	l := list.New(items, itemDelegate{}, defaultWidth, listHeight)
115 | 	l.Title = "What do you want for dinner?"
116 | 	l.SetShowStatusBar(false)
117 | 	l.SetFilteringEnabled(false)
118 | 	l.Styles.Title = titleStyle
119 | 	l.Styles.PaginationStyle = paginationStyle
120 | 	l.Styles.HelpStyle = helpStyle
121 | 
122 | 	m := model{list: l}
123 | 
124 | 	if _, err := tea.NewProgram(m).Run(); err != nil {
125 | 		fmt.Println("Error running program:", err)
126 | 		os.Exit(1)
127 | 	}
128 | }
129 | 


--------------------------------------------------------------------------------
/examples/mouse/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program that opens the alternate screen buffer and displays mouse
 4 | // coordinates and events.
 5 | 
 6 | import (
 7 | 	"log"
 8 | 
 9 | 	tea "github.com/charmbracelet/bubbletea"
10 | )
11 | 
12 | func main() {
13 | 	p := tea.NewProgram(model{}, tea.WithMouseAllMotion())
14 | 	if _, err := p.Run(); err != nil {
15 | 		log.Fatal(err)
16 | 	}
17 | }
18 | 
19 | type model struct {
20 | 	mouseEvent tea.MouseEvent
21 | }
22 | 
23 | func (m model) Init() tea.Cmd {
24 | 	return nil
25 | }
26 | 
27 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
28 | 	switch msg := msg.(type) {
29 | 	case tea.KeyMsg:
30 | 		if s := msg.String(); s == "ctrl+c" || s == "q" || s == "esc" {
31 | 			return m, tea.Quit
32 | 		}
33 | 
34 | 	case tea.MouseMsg:
35 | 		return m, tea.Printf("(X: %d, Y: %d) %s", msg.X, msg.Y, tea.MouseEvent(msg))
36 | 	}
37 | 
38 | 	return m, nil
39 | }
40 | 
41 | func (m model) View() string {
42 | 	s := "Do mouse stuff. When you're done press q to quit.\n"
43 | 
44 | 	return s
45 | }
46 | 


--------------------------------------------------------------------------------
/examples/package-manager/README.md:
--------------------------------------------------------------------------------
1 | # Package Manager
2 | 
3 | <img width="800" src="./package-manager.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/package-manager/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"math/rand"
  6 | 	"os"
  7 | 	"strings"
  8 | 	"time"
  9 | 
 10 | 	"github.com/charmbracelet/bubbles/progress"
 11 | 	"github.com/charmbracelet/bubbles/spinner"
 12 | 	tea "github.com/charmbracelet/bubbletea"
 13 | 	"github.com/charmbracelet/lipgloss"
 14 | )
 15 | 
 16 | type model struct {
 17 | 	packages []string
 18 | 	index    int
 19 | 	width    int
 20 | 	height   int
 21 | 	spinner  spinner.Model
 22 | 	progress progress.Model
 23 | 	done     bool
 24 | }
 25 | 
 26 | var (
 27 | 	currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
 28 | 	doneStyle           = lipgloss.NewStyle().Margin(1, 2)
 29 | 	checkMark           = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
 30 | )
 31 | 
 32 | func newModel() model {
 33 | 	p := progress.New(
 34 | 		progress.WithDefaultGradient(),
 35 | 		progress.WithWidth(40),
 36 | 		progress.WithoutPercentage(),
 37 | 	)
 38 | 	s := spinner.New()
 39 | 	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
 40 | 	return model{
 41 | 		packages: getPackages(),
 42 | 		spinner:  s,
 43 | 		progress: p,
 44 | 	}
 45 | }
 46 | 
 47 | func (m model) Init() tea.Cmd {
 48 | 	return tea.Batch(downloadAndInstall(m.packages[m.index]), m.spinner.Tick)
 49 | }
 50 | 
 51 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 52 | 	switch msg := msg.(type) {
 53 | 	case tea.WindowSizeMsg:
 54 | 		m.width, m.height = msg.Width, msg.Height
 55 | 	case tea.KeyMsg:
 56 | 		switch msg.String() {
 57 | 		case "ctrl+c", "esc", "q":
 58 | 			return m, tea.Quit
 59 | 		}
 60 | 	case installedPkgMsg:
 61 | 		pkg := m.packages[m.index]
 62 | 		if m.index >= len(m.packages)-1 {
 63 | 			// Everything's been installed. We're done!
 64 | 			m.done = true
 65 | 			return m, tea.Sequence(
 66 | 				tea.Printf("%s %s", checkMark, pkg), // print the last success message
 67 | 				tea.Quit,                            // exit the program
 68 | 			)
 69 | 		}
 70 | 
 71 | 		// Update progress bar
 72 | 		m.index++
 73 | 		progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages)))
 74 | 
 75 | 		return m, tea.Batch(
 76 | 			progressCmd,
 77 | 			tea.Printf("%s %s", checkMark, pkg),     // print success message above our program
 78 | 			downloadAndInstall(m.packages[m.index]), // download the next package
 79 | 		)
 80 | 	case spinner.TickMsg:
 81 | 		var cmd tea.Cmd
 82 | 		m.spinner, cmd = m.spinner.Update(msg)
 83 | 		return m, cmd
 84 | 	case progress.FrameMsg:
 85 | 		newModel, cmd := m.progress.Update(msg)
 86 | 		if newModel, ok := newModel.(progress.Model); ok {
 87 | 			m.progress = newModel
 88 | 		}
 89 | 		return m, cmd
 90 | 	}
 91 | 	return m, nil
 92 | }
 93 | 
 94 | func (m model) View() string {
 95 | 	n := len(m.packages)
 96 | 	w := lipgloss.Width(fmt.Sprintf("%d", n))
 97 | 
 98 | 	if m.done {
 99 | 		return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n))
100 | 	}
101 | 
102 | 	pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n)
103 | 
104 | 	spin := m.spinner.View() + " "
105 | 	prog := m.progress.View()
106 | 	cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount))
107 | 
108 | 	pkgName := currentPkgNameStyle.Render(m.packages[m.index])
109 | 	info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName)
110 | 
111 | 	cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount))
112 | 	gap := strings.Repeat(" ", cellsRemaining)
113 | 
114 | 	return spin + info + gap + prog + pkgCount
115 | }
116 | 
117 | type installedPkgMsg string
118 | 
119 | func downloadAndInstall(pkg string) tea.Cmd {
120 | 	// This is where you'd do i/o stuff to download and install packages. In
121 | 	// our case we're just pausing for a moment to simulate the process.
122 | 	d := time.Millisecond * time.Duration(rand.Intn(500)) //nolint:gosec
123 | 	return tea.Tick(d, func(t time.Time) tea.Msg {
124 | 		return installedPkgMsg(pkg)
125 | 	})
126 | }
127 | 
128 | func max(a, b int) int {
129 | 	if a > b {
130 | 		return a
131 | 	}
132 | 	return b
133 | }
134 | 
135 | func main() {
136 | 	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
137 | 		fmt.Println("Error running program:", err)
138 | 		os.Exit(1)
139 | 	}
140 | }
141 | 


--------------------------------------------------------------------------------
/examples/package-manager/package-manager.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/package-manager/package-manager.gif


--------------------------------------------------------------------------------
/examples/package-manager/packages.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"math/rand"
 6 | )
 7 | 
 8 | var packages = []string{
 9 | 	"vegeutils",
10 | 	"libgardening",
11 | 	"currykit",
12 | 	"spicerack",
13 | 	"fullenglish",
14 | 	"eggy",
15 | 	"bad-kitty",
16 | 	"chai",
17 | 	"hojicha",
18 | 	"libtacos",
19 | 	"babys-monads",
20 | 	"libpurring",
21 | 	"currywurst-devel",
22 | 	"xmodmeow",
23 | 	"licorice-utils",
24 | 	"cashew-apple",
25 | 	"rock-lobster",
26 | 	"standmixer",
27 | 	"coffee-CUPS",
28 | 	"libesszet",
29 | 	"zeichenorientierte-benutzerschnittstellen",
30 | 	"schnurrkit",
31 | 	"old-socks-devel",
32 | 	"jalapeño",
33 | 	"molasses-utils",
34 | 	"xkohlrabi",
35 | 	"party-gherkin",
36 | 	"snow-peas",
37 | 	"libyuzu",
38 | }
39 | 
40 | func getPackages() []string {
41 | 	pkgs := packages
42 | 	copy(pkgs, packages)
43 | 
44 | 	rand.Shuffle(len(pkgs), func(i, j int) {
45 | 		pkgs[i], pkgs[j] = pkgs[j], pkgs[i]
46 | 	})
47 | 
48 | 	for k := range pkgs {
49 | 		pkgs[k] += fmt.Sprintf("-%d.%d.%d", rand.Intn(10), rand.Intn(10), rand.Intn(10)) //nolint:gosec
50 | 	}
51 | 	return pkgs
52 | }
53 | 


--------------------------------------------------------------------------------
/examples/pager/README.md:
--------------------------------------------------------------------------------
1 | # Pager
2 | 
3 | <img width="800" src="./pager.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/pager/artichoke.md:
--------------------------------------------------------------------------------
 1 | Glow
 2 | ====
 3 | 
 4 | A casual introduction. 你好世界!
 5 | 
 6 | ## Let’s talk about artichokes
 7 | 
 8 | The _artichoke_ is mentioned as a garden plant in the 8th century BC by Homer
 9 | **and** Hesiod. The naturally occurring variant of the artichoke, the cardoon,
10 | which is native to the Mediterranean area, also has records of use as a food
11 | among the ancient Greeks and Romans. Pliny the Elder mentioned growing of
12 | _carduus_ in Carthage and Cordoba.
13 | 
14 | > He holds him with a skinny hand,
15 | > ‘There was a ship,’ quoth he.
16 | > ‘Hold off! unhand me, grey-beard loon!’
17 | > An artichoke, dropt he.
18 | 
19 | --Samuel Taylor Coleridge, [The Rime of the Ancient Mariner][rime]
20 | 
21 | [rime]: https://poetryfoundation.org/poems/43997/
22 | 
23 | ## Other foods worth mentioning
24 | 
25 | 1. Carrots
26 | 1. Celery
27 | 1. Tacos
28 |     * Soft
29 |     * Hard
30 | 1. Cucumber
31 | 
32 | ## Things to eat today
33 | 
34 | * [x] Carrots
35 | * [x] Ramen
36 | * [ ] Currywurst
37 | 
38 | ### Power levels of the aforementioned foods
39 | 
40 | | Name       | Power | Comment          |
41 | | ---        | ---   | ---              |
42 | | Carrots    | 9001  | It’s over 9000?! |
43 | | Ramen      | 9002  | Also over 9000?! |
44 | | Currywurst | 10000 | What?!           |
45 | 
46 | ## Currying Artichokes
47 | 
48 | Here’s a bit of code in [Haskell](https://haskell.org), because we are fancy.
49 | Remember that to compile Haskell you’ll need `ghc`.
50 | 
51 | ```haskell
52 | module Main where
53 | 
54 | import Data.Function ( (&) )
55 | import Data.List ( intercalculate )
56 | 
57 | hello :: String -> String
58 | hello s =
59 |     "Hello, " ++ s ++ "."
60 | 
61 | main :: IO ()
62 | main =
63 |     map hello [ "artichoke", "alcachofa" ] & intercalculate "\n" & putStrLn
64 | ```
65 | 
66 | ***
67 | 
68 | _Alcachofa_, if you were wondering, is artichoke in Spanish.
69 | 


--------------------------------------------------------------------------------
/examples/pager/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | // An example program demonstrating the pager component from the Bubbles
  4 | // component library.
  5 | 
  6 | import (
  7 | 	"fmt"
  8 | 	"os"
  9 | 	"strings"
 10 | 
 11 | 	"github.com/charmbracelet/bubbles/viewport"
 12 | 	tea "github.com/charmbracelet/bubbletea"
 13 | 	"github.com/charmbracelet/lipgloss"
 14 | )
 15 | 
 16 | var (
 17 | 	titleStyle = func() lipgloss.Style {
 18 | 		b := lipgloss.RoundedBorder()
 19 | 		b.Right = "├"
 20 | 		return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
 21 | 	}()
 22 | 
 23 | 	infoStyle = func() lipgloss.Style {
 24 | 		b := lipgloss.RoundedBorder()
 25 | 		b.Left = "┤"
 26 | 		return titleStyle.BorderStyle(b)
 27 | 	}()
 28 | )
 29 | 
 30 | type model struct {
 31 | 	content  string
 32 | 	ready    bool
 33 | 	viewport viewport.Model
 34 | }
 35 | 
 36 | func (m model) Init() tea.Cmd {
 37 | 	return nil
 38 | }
 39 | 
 40 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 41 | 	var (
 42 | 		cmd  tea.Cmd
 43 | 		cmds []tea.Cmd
 44 | 	)
 45 | 
 46 | 	switch msg := msg.(type) {
 47 | 	case tea.KeyMsg:
 48 | 		if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
 49 | 			return m, tea.Quit
 50 | 		}
 51 | 
 52 | 	case tea.WindowSizeMsg:
 53 | 		headerHeight := lipgloss.Height(m.headerView())
 54 | 		footerHeight := lipgloss.Height(m.footerView())
 55 | 		verticalMarginHeight := headerHeight + footerHeight
 56 | 
 57 | 		if !m.ready {
 58 | 			// Since this program is using the full size of the viewport we
 59 | 			// need to wait until we've received the window dimensions before
 60 | 			// we can initialize the viewport. The initial dimensions come in
 61 | 			// quickly, though asynchronously, which is why we wait for them
 62 | 			// here.
 63 | 			m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
 64 | 			m.viewport.YPosition = headerHeight
 65 | 			m.viewport.SetContent(m.content)
 66 | 			m.ready = true
 67 | 		} else {
 68 | 			m.viewport.Width = msg.Width
 69 | 			m.viewport.Height = msg.Height - verticalMarginHeight
 70 | 		}
 71 | 	}
 72 | 
 73 | 	// Handle keyboard and mouse events in the viewport
 74 | 	m.viewport, cmd = m.viewport.Update(msg)
 75 | 	cmds = append(cmds, cmd)
 76 | 
 77 | 	return m, tea.Batch(cmds...)
 78 | }
 79 | 
 80 | func (m model) View() string {
 81 | 	if !m.ready {
 82 | 		return "\n  Initializing..."
 83 | 	}
 84 | 	return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
 85 | }
 86 | 
 87 | func (m model) headerView() string {
 88 | 	title := titleStyle.Render("Mr. Pager")
 89 | 	line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
 90 | 	return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
 91 | }
 92 | 
 93 | func (m model) footerView() string {
 94 | 	info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
 95 | 	line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
 96 | 	return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
 97 | }
 98 | 
 99 | func max(a, b int) int {
100 | 	if a > b {
101 | 		return a
102 | 	}
103 | 	return b
104 | }
105 | 
106 | func main() {
107 | 	// Load some text for our viewport
108 | 	content, err := os.ReadFile("artichoke.md")
109 | 	if err != nil {
110 | 		fmt.Println("could not load file:", err)
111 | 		os.Exit(1)
112 | 	}
113 | 
114 | 	p := tea.NewProgram(
115 | 		model{content: string(content)},
116 | 		tea.WithAltScreen(),       // use the full size of the terminal in its "alternate screen buffer"
117 | 		tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
118 | 	)
119 | 
120 | 	if _, err := p.Run(); err != nil {
121 | 		fmt.Println("could not run program:", err)
122 | 		os.Exit(1)
123 | 	}
124 | }
125 | 


--------------------------------------------------------------------------------
/examples/pager/pager.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/pager/pager.gif


--------------------------------------------------------------------------------
/examples/paginator/README.md:
--------------------------------------------------------------------------------
1 | # Paginator
2 | 
3 | <img width="800" src="./paginator.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/paginator/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program demonstrating the paginator component from the Bubbles
 4 | // component library.
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"log"
 9 | 	"strings"
10 | 
11 | 	"github.com/charmbracelet/bubbles/paginator"
12 | 	"github.com/charmbracelet/lipgloss"
13 | 
14 | 	tea "github.com/charmbracelet/bubbletea"
15 | )
16 | 
17 | func newModel() model {
18 | 	var items []string
19 | 	for i := 1; i < 101; i++ {
20 | 		text := fmt.Sprintf("Item %d", i)
21 | 		items = append(items, text)
22 | 	}
23 | 
24 | 	p := paginator.New()
25 | 	p.Type = paginator.Dots
26 | 	p.PerPage = 10
27 | 	p.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•")
28 | 	p.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•")
29 | 	p.SetTotalPages(len(items))
30 | 
31 | 	return model{
32 | 		paginator: p,
33 | 		items:     items,
34 | 	}
35 | }
36 | 
37 | type model struct {
38 | 	items     []string
39 | 	paginator paginator.Model
40 | }
41 | 
42 | func (m model) Init() tea.Cmd {
43 | 	return nil
44 | }
45 | 
46 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
47 | 	var cmd tea.Cmd
48 | 	switch msg := msg.(type) {
49 | 	case tea.KeyMsg:
50 | 		switch msg.String() {
51 | 		case "q", "esc", "ctrl+c":
52 | 			return m, tea.Quit
53 | 		}
54 | 	}
55 | 	m.paginator, cmd = m.paginator.Update(msg)
56 | 	return m, cmd
57 | }
58 | 
59 | func (m model) View() string {
60 | 	var b strings.Builder
61 | 	b.WriteString("\n  Paginator Example\n\n")
62 | 	start, end := m.paginator.GetSliceBounds(len(m.items))
63 | 	for _, item := range m.items[start:end] {
64 | 		b.WriteString("  • " + item + "\n\n")
65 | 	}
66 | 	b.WriteString("  " + m.paginator.View())
67 | 	b.WriteString("\n\n  h/l ←/→ page • q: quit\n")
68 | 	return b.String()
69 | }
70 | 
71 | func main() {
72 | 	p := tea.NewProgram(newModel())
73 | 	if _, err := p.Run(); err != nil {
74 | 		log.Fatal(err)
75 | 	}
76 | }
77 | 


--------------------------------------------------------------------------------
/examples/paginator/paginator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/paginator/paginator.gif


--------------------------------------------------------------------------------
/examples/pipe/README.md:
--------------------------------------------------------------------------------
1 | # Pipe
2 | 
3 | <img width="800" src="./pipe.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/pipe/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // An example illustrating how to pipe in data to a Bubble Tea application.
 4 | // More so, this serves as proof that Bubble Tea will automatically listen for
 5 | // keystrokes when input is not a TTY, such as when data is piped or redirected
 6 | // in.
 7 | 
 8 | import (
 9 | 	"bufio"
10 | 	"fmt"
11 | 	"io"
12 | 	"os"
13 | 	"strings"
14 | 
15 | 	"github.com/charmbracelet/bubbles/textinput"
16 | 	tea "github.com/charmbracelet/bubbletea"
17 | 	"github.com/charmbracelet/lipgloss"
18 | )
19 | 
20 | func main() {
21 | 	stat, err := os.Stdin.Stat()
22 | 	if err != nil {
23 | 		panic(err)
24 | 	}
25 | 
26 | 	if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {
27 | 		fmt.Println("Try piping in some text.")
28 | 		os.Exit(1)
29 | 	}
30 | 
31 | 	reader := bufio.NewReader(os.Stdin)
32 | 	var b strings.Builder
33 | 
34 | 	for {
35 | 		r, _, err := reader.ReadRune()
36 | 		if err != nil && err == io.EOF {
37 | 			break
38 | 		}
39 | 		_, err = b.WriteRune(r)
40 | 		if err != nil {
41 | 			fmt.Println("Error getting input:", err)
42 | 			os.Exit(1)
43 | 		}
44 | 	}
45 | 
46 | 	model := newModel(strings.TrimSpace(b.String()))
47 | 
48 | 	if _, err := tea.NewProgram(model).Run(); err != nil {
49 | 		fmt.Println("Couldn't start program:", err)
50 | 		os.Exit(1)
51 | 	}
52 | }
53 | 
54 | type model struct {
55 | 	userInput textinput.Model
56 | }
57 | 
58 | func newModel(initialValue string) (m model) {
59 | 	i := textinput.New()
60 | 	i.Prompt = ""
61 | 	i.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
62 | 	i.Width = 48
63 | 	i.SetValue(initialValue)
64 | 	i.CursorEnd()
65 | 	i.Focus()
66 | 
67 | 	m.userInput = i
68 | 	return
69 | }
70 | 
71 | func (m model) Init() tea.Cmd {
72 | 	return textinput.Blink
73 | }
74 | 
75 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
76 | 	if key, ok := msg.(tea.KeyMsg); ok {
77 | 		switch key.Type {
78 | 		case tea.KeyCtrlC, tea.KeyEscape, tea.KeyEnter:
79 | 			return m, tea.Quit
80 | 		}
81 | 	}
82 | 
83 | 	var cmd tea.Cmd
84 | 	m.userInput, cmd = m.userInput.Update(msg)
85 | 	return m, cmd
86 | }
87 | 
88 | func (m model) View() string {
89 | 	return fmt.Sprintf(
90 | 		"\nYou piped in: %s\n\nPress ^C to exit",
91 | 		m.userInput.View(),
92 | 	)
93 | }
94 | 


--------------------------------------------------------------------------------
/examples/pipe/pipe.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/pipe/pipe.gif


--------------------------------------------------------------------------------
/examples/prevent-quit/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | // A program demonstrating how to use the WithFilter option to intercept events.
  4 | 
  5 | import (
  6 | 	"fmt"
  7 | 	"log"
  8 | 
  9 | 	"github.com/charmbracelet/bubbles/help"
 10 | 	"github.com/charmbracelet/bubbles/key"
 11 | 	"github.com/charmbracelet/bubbles/textarea"
 12 | 	tea "github.com/charmbracelet/bubbletea"
 13 | 	"github.com/charmbracelet/lipgloss"
 14 | )
 15 | 
 16 | var (
 17 | 	choiceStyle   = lipgloss.NewStyle().PaddingLeft(1).Foreground(lipgloss.Color("241"))
 18 | 	saveTextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("170"))
 19 | 	quitViewStyle = lipgloss.NewStyle().Padding(1).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("170"))
 20 | )
 21 | 
 22 | func main() {
 23 | 	p := tea.NewProgram(initialModel(), tea.WithFilter(filter))
 24 | 
 25 | 	if _, err := p.Run(); err != nil {
 26 | 		log.Fatal(err)
 27 | 	}
 28 | }
 29 | 
 30 | func filter(teaModel tea.Model, msg tea.Msg) tea.Msg {
 31 | 	if _, ok := msg.(tea.QuitMsg); !ok {
 32 | 		return msg
 33 | 	}
 34 | 
 35 | 	m := teaModel.(model)
 36 | 	if m.hasChanges {
 37 | 		return nil
 38 | 	}
 39 | 
 40 | 	return msg
 41 | }
 42 | 
 43 | type model struct {
 44 | 	textarea   textarea.Model
 45 | 	help       help.Model
 46 | 	keymap     keymap
 47 | 	saveText   string
 48 | 	hasChanges bool
 49 | 	quitting   bool
 50 | }
 51 | 
 52 | type keymap struct {
 53 | 	save key.Binding
 54 | 	quit key.Binding
 55 | }
 56 | 
 57 | func initialModel() model {
 58 | 	ti := textarea.New()
 59 | 	ti.Placeholder = "Only the best words"
 60 | 	ti.Focus()
 61 | 
 62 | 	return model{
 63 | 		textarea: ti,
 64 | 		help:     help.New(),
 65 | 		keymap: keymap{
 66 | 			save: key.NewBinding(
 67 | 				key.WithKeys("ctrl+s"),
 68 | 				key.WithHelp("ctrl+s", "save"),
 69 | 			),
 70 | 			quit: key.NewBinding(
 71 | 				key.WithKeys("esc", "ctrl+c"),
 72 | 				key.WithHelp("esc", "quit"),
 73 | 			),
 74 | 		},
 75 | 	}
 76 | }
 77 | 
 78 | func (m model) Init() tea.Cmd {
 79 | 	return textarea.Blink
 80 | }
 81 | 
 82 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 83 | 	if m.quitting {
 84 | 		return m.updatePromptView(msg)
 85 | 	}
 86 | 
 87 | 	return m.updateTextView(msg)
 88 | }
 89 | 
 90 | func (m model) updateTextView(msg tea.Msg) (tea.Model, tea.Cmd) {
 91 | 	var cmds []tea.Cmd
 92 | 	var cmd tea.Cmd
 93 | 
 94 | 	switch msg := msg.(type) {
 95 | 	case tea.KeyMsg:
 96 | 		m.saveText = ""
 97 | 		switch {
 98 | 		case key.Matches(msg, m.keymap.save):
 99 | 			m.saveText = "Changes saved!"
100 | 			m.hasChanges = false
101 | 		case key.Matches(msg, m.keymap.quit):
102 | 			m.quitting = true
103 | 			return m, tea.Quit
104 | 		case msg.Type == tea.KeyRunes:
105 | 			m.saveText = ""
106 | 			m.hasChanges = true
107 | 			fallthrough
108 | 		default:
109 | 			if !m.textarea.Focused() {
110 | 				cmd = m.textarea.Focus()
111 | 				cmds = append(cmds, cmd)
112 | 			}
113 | 		}
114 | 	}
115 | 	m.textarea, cmd = m.textarea.Update(msg)
116 | 	cmds = append(cmds, cmd)
117 | 	return m, tea.Batch(cmds...)
118 | }
119 | 
120 | func (m model) updatePromptView(msg tea.Msg) (tea.Model, tea.Cmd) {
121 | 	switch msg := msg.(type) {
122 | 	case tea.KeyMsg:
123 | 		// For simplicity's sake, we'll treat any key besides "y" as "no"
124 | 		if key.Matches(msg, m.keymap.quit) || msg.String() == "y" {
125 | 			m.hasChanges = false
126 | 			return m, tea.Quit
127 | 		}
128 | 		m.quitting = false
129 | 	}
130 | 
131 | 	return m, nil
132 | }
133 | 
134 | func (m model) View() string {
135 | 	if m.quitting {
136 | 		if m.hasChanges {
137 | 			text := lipgloss.JoinHorizontal(lipgloss.Top, "You have unsaved changes. Quit without saving?", choiceStyle.Render("[yn]"))
138 | 			return quitViewStyle.Render(text)
139 | 		}
140 | 		return "Very important, thank you\n"
141 | 	}
142 | 
143 | 	helpView := m.help.ShortHelpView([]key.Binding{
144 | 		m.keymap.save,
145 | 		m.keymap.quit,
146 | 	})
147 | 
148 | 	return fmt.Sprintf(
149 | 		"\nType some important things.\n\n%s\n\n %s\n %s",
150 | 		m.textarea.View(),
151 | 		saveTextStyle.Render(m.saveText),
152 | 		helpView,
153 | 	) + "\n\n"
154 | }
155 | 


--------------------------------------------------------------------------------
/examples/progress-animated/README.md:
--------------------------------------------------------------------------------
1 | # Animated Progress
2 | 
3 | <img width="800" src="./progress-animated.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/progress-animated/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple example that shows how to render an animated progress bar. In this
 4 | // example we bump the progress by 25% every two seconds, animating our
 5 | // progress bar to its new target state.
 6 | //
 7 | // It's also possible to render a progress bar in a more static fashion without
 8 | // transitions. For details on that approach see the progress-static example.
 9 | 
10 | import (
11 | 	"fmt"
12 | 	"os"
13 | 	"strings"
14 | 	"time"
15 | 
16 | 	"github.com/charmbracelet/bubbles/progress"
17 | 	tea "github.com/charmbracelet/bubbletea"
18 | 	"github.com/charmbracelet/lipgloss"
19 | )
20 | 
21 | const (
22 | 	padding  = 2
23 | 	maxWidth = 80
24 | )
25 | 
26 | var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
27 | 
28 | func main() {
29 | 	m := model{
30 | 		progress: progress.New(progress.WithDefaultGradient()),
31 | 	}
32 | 
33 | 	if _, err := tea.NewProgram(m).Run(); err != nil {
34 | 		fmt.Println("Oh no!", err)
35 | 		os.Exit(1)
36 | 	}
37 | }
38 | 
39 | type tickMsg time.Time
40 | 
41 | type model struct {
42 | 	progress progress.Model
43 | }
44 | 
45 | func (m model) Init() tea.Cmd {
46 | 	return tickCmd()
47 | }
48 | 
49 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
50 | 	switch msg := msg.(type) {
51 | 	case tea.KeyMsg:
52 | 		return m, tea.Quit
53 | 
54 | 	case tea.WindowSizeMsg:
55 | 		m.progress.Width = msg.Width - padding*2 - 4
56 | 		if m.progress.Width > maxWidth {
57 | 			m.progress.Width = maxWidth
58 | 		}
59 | 		return m, nil
60 | 
61 | 	case tickMsg:
62 | 		if m.progress.Percent() == 1.0 {
63 | 			return m, tea.Quit
64 | 		}
65 | 
66 | 		// Note that you can also use progress.Model.SetPercent to set the
67 | 		// percentage value explicitly, too.
68 | 		cmd := m.progress.IncrPercent(0.25)
69 | 		return m, tea.Batch(tickCmd(), cmd)
70 | 
71 | 	// FrameMsg is sent when the progress bar wants to animate itself
72 | 	case progress.FrameMsg:
73 | 		progressModel, cmd := m.progress.Update(msg)
74 | 		m.progress = progressModel.(progress.Model)
75 | 		return m, cmd
76 | 
77 | 	default:
78 | 		return m, nil
79 | 	}
80 | }
81 | 
82 | func (m model) View() string {
83 | 	pad := strings.Repeat(" ", padding)
84 | 	return "\n" +
85 | 		pad + m.progress.View() + "\n\n" +
86 | 		pad + helpStyle("Press any key to quit")
87 | }
88 | 
89 | func tickCmd() tea.Cmd {
90 | 	return tea.Tick(time.Second*1, func(t time.Time) tea.Msg {
91 | 		return tickMsg(t)
92 | 	})
93 | }
94 | 


--------------------------------------------------------------------------------
/examples/progress-animated/progress-animated.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/progress-animated/progress-animated.gif


--------------------------------------------------------------------------------
/examples/progress-download/README.md:
--------------------------------------------------------------------------------
 1 | # Download Progress
 2 | 
 3 | This example demonstrates how to download a file from a URL and show its
 4 | progress with a [Progress Bubble][progress].
 5 | 
 6 | In this case we're getting download progress with an [`io.TeeReader`][tee] and
 7 | sending progress `Msg`s to the `Program` with `Program.Send()`.
 8 | 
 9 | ## How to Run
10 | 
11 | Build the application with `go build .`, then run with a `--url` argument
12 | specifying the URL of the file to download. For example:
13 | 
14 | ```
15 | ./progress-download --url="https://download.blender.org/demo/color_vortex.blend"
16 | ```
17 | 
18 | Note that in this example a TUI will not be shown for URLs that do not respond
19 | with a ContentLength header.
20 | 
21 | * * *
22 | 
23 | This example originally came from [this discussion][discussion].
24 | 
25 | * * *
26 | 
27 | <a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
28 | 
29 | Charm热爱开源 • Charm loves open source
30 | 
31 | 
32 | [progress]: https://github.com/charmbracelet/bubbles/
33 | [tee]: https://pkg.go.dev/io#TeeReader
34 | [discussion]: https://github.com/charmbracelet/bubbles/discussions/127
35 | 


--------------------------------------------------------------------------------
/examples/progress-download/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"flag"
  5 | 	"fmt"
  6 | 	"io"
  7 | 	"log"
  8 | 	"net/http"
  9 | 	"os"
 10 | 	"path/filepath"
 11 | 
 12 | 	"github.com/charmbracelet/bubbles/progress"
 13 | 	tea "github.com/charmbracelet/bubbletea"
 14 | )
 15 | 
 16 | var p *tea.Program
 17 | 
 18 | type progressWriter struct {
 19 | 	total      int
 20 | 	downloaded int
 21 | 	file       *os.File
 22 | 	reader     io.Reader
 23 | 	onProgress func(float64)
 24 | }
 25 | 
 26 | func (pw *progressWriter) Start() {
 27 | 	// TeeReader calls pw.Write() each time a new response is received
 28 | 	_, err := io.Copy(pw.file, io.TeeReader(pw.reader, pw))
 29 | 	if err != nil {
 30 | 		p.Send(progressErrMsg{err})
 31 | 	}
 32 | }
 33 | 
 34 | func (pw *progressWriter) Write(p []byte) (int, error) {
 35 | 	pw.downloaded += len(p)
 36 | 	if pw.total > 0 && pw.onProgress != nil {
 37 | 		pw.onProgress(float64(pw.downloaded) / float64(pw.total))
 38 | 	}
 39 | 	return len(p), nil
 40 | }
 41 | 
 42 | func getResponse(url string) (*http.Response, error) {
 43 | 	resp, err := http.Get(url) // nolint:gosec
 44 | 	if err != nil {
 45 | 		log.Fatal(err)
 46 | 	}
 47 | 	if resp.StatusCode != http.StatusOK {
 48 | 		return nil, fmt.Errorf("receiving status of %d for url: %s", resp.StatusCode, url)
 49 | 	}
 50 | 	return resp, nil
 51 | }
 52 | 
 53 | func main() {
 54 | 	url := flag.String("url", "", "url for the file to download")
 55 | 	flag.Parse()
 56 | 
 57 | 	if *url == "" {
 58 | 		flag.Usage()
 59 | 		os.Exit(1)
 60 | 	}
 61 | 
 62 | 	resp, err := getResponse(*url)
 63 | 	if err != nil {
 64 | 		fmt.Println("could not get response", err)
 65 | 		os.Exit(1)
 66 | 	}
 67 | 	defer resp.Body.Close() // nolint:errcheck
 68 | 
 69 | 	// Don't add TUI if the header doesn't include content size
 70 | 	// it's impossible see progress without total
 71 | 	if resp.ContentLength <= 0 {
 72 | 		fmt.Println("can't parse content length, aborting download")
 73 | 		os.Exit(1)
 74 | 	}
 75 | 
 76 | 	filename := filepath.Base(*url)
 77 | 	file, err := os.Create(filename)
 78 | 	if err != nil {
 79 | 		fmt.Println("could not create file:", err)
 80 | 		os.Exit(1)
 81 | 	}
 82 | 	defer file.Close() // nolint:errcheck
 83 | 
 84 | 	pw := &progressWriter{
 85 | 		total:  int(resp.ContentLength),
 86 | 		file:   file,
 87 | 		reader: resp.Body,
 88 | 		onProgress: func(ratio float64) {
 89 | 			p.Send(progressMsg(ratio))
 90 | 		},
 91 | 	}
 92 | 
 93 | 	m := model{
 94 | 		pw:       pw,
 95 | 		progress: progress.New(progress.WithDefaultGradient()),
 96 | 	}
 97 | 	// Start Bubble Tea
 98 | 	p = tea.NewProgram(m)
 99 | 
100 | 	// Start the download
101 | 	go pw.Start()
102 | 
103 | 	if _, err := p.Run(); err != nil {
104 | 		fmt.Println("error running program:", err)
105 | 		os.Exit(1)
106 | 	}
107 | }
108 | 


--------------------------------------------------------------------------------
/examples/progress-download/tui.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"strings"
 5 | 	"time"
 6 | 
 7 | 	"github.com/charmbracelet/bubbles/progress"
 8 | 	tea "github.com/charmbracelet/bubbletea"
 9 | 	"github.com/charmbracelet/lipgloss"
10 | )
11 | 
12 | var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
13 | 
14 | const (
15 | 	padding  = 2
16 | 	maxWidth = 80
17 | )
18 | 
19 | type progressMsg float64
20 | 
21 | type progressErrMsg struct{ err error }
22 | 
23 | func finalPause() tea.Cmd {
24 | 	return tea.Tick(time.Millisecond*750, func(_ time.Time) tea.Msg {
25 | 		return nil
26 | 	})
27 | }
28 | 
29 | type model struct {
30 | 	pw       *progressWriter
31 | 	progress progress.Model
32 | 	err      error
33 | }
34 | 
35 | func (m model) Init() tea.Cmd {
36 | 	return nil
37 | }
38 | 
39 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
40 | 	switch msg := msg.(type) {
41 | 	case tea.KeyMsg:
42 | 		return m, tea.Quit
43 | 
44 | 	case tea.WindowSizeMsg:
45 | 		m.progress.Width = msg.Width - padding*2 - 4
46 | 		if m.progress.Width > maxWidth {
47 | 			m.progress.Width = maxWidth
48 | 		}
49 | 		return m, nil
50 | 
51 | 	case progressErrMsg:
52 | 		m.err = msg.err
53 | 		return m, tea.Quit
54 | 
55 | 	case progressMsg:
56 | 		var cmds []tea.Cmd
57 | 
58 | 		if msg >= 1.0 {
59 | 			cmds = append(cmds, tea.Sequence(finalPause(), tea.Quit))
60 | 		}
61 | 
62 | 		cmds = append(cmds, m.progress.SetPercent(float64(msg)))
63 | 		return m, tea.Batch(cmds...)
64 | 
65 | 	// FrameMsg is sent when the progress bar wants to animate itself
66 | 	case progress.FrameMsg:
67 | 		progressModel, cmd := m.progress.Update(msg)
68 | 		m.progress = progressModel.(progress.Model)
69 | 		return m, cmd
70 | 
71 | 	default:
72 | 		return m, nil
73 | 	}
74 | }
75 | 
76 | func (m model) View() string {
77 | 	if m.err != nil {
78 | 		return "Error downloading: " + m.err.Error() + "\n"
79 | 	}
80 | 
81 | 	pad := strings.Repeat(" ", padding)
82 | 	return "\n" +
83 | 		pad + m.progress.View() + "\n\n" +
84 | 		pad + helpStyle("Press any key to quit")
85 | }
86 | 


--------------------------------------------------------------------------------
/examples/progress-static/README.md:
--------------------------------------------------------------------------------
1 | # Static Progress
2 | 
3 | <img width="800" src="./progress-static.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/progress-static/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple example that shows how to render a progress bar in a "pure"
 4 | // fashion. In this example we bump the progress by 25% every second,
 5 | // maintaining the progress state on our top level model using the progress bar
 6 | // model's ViewAs method only for rendering.
 7 | //
 8 | // The signature for ViewAs is:
 9 | //
10 | //     func (m Model) ViewAs(percent float64) string
11 | //
12 | // So it takes a float between 0 and 1, and renders the progress bar
13 | // accordingly. When using the progress bar in this "pure" fashion and there's
14 | // no need to call an Update method.
15 | //
16 | // The progress bar is also able to animate itself, however. For details see
17 | // the progress-animated example.
18 | 
19 | import (
20 | 	"fmt"
21 | 	"os"
22 | 	"strings"
23 | 	"time"
24 | 
25 | 	"github.com/charmbracelet/bubbles/progress"
26 | 	tea "github.com/charmbracelet/bubbletea"
27 | 	"github.com/charmbracelet/lipgloss"
28 | )
29 | 
30 | const (
31 | 	padding  = 2
32 | 	maxWidth = 80
33 | )
34 | 
35 | var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
36 | 
37 | func main() {
38 | 	prog := progress.New(progress.WithScaledGradient("#FF7CCB", "#FDFF8C"))
39 | 
40 | 	if _, err := tea.NewProgram(model{progress: prog}).Run(); err != nil {
41 | 		fmt.Println("Oh no!", err)
42 | 		os.Exit(1)
43 | 	}
44 | }
45 | 
46 | type tickMsg time.Time
47 | 
48 | type model struct {
49 | 	percent  float64
50 | 	progress progress.Model
51 | }
52 | 
53 | func (m model) Init() tea.Cmd {
54 | 	return tickCmd()
55 | }
56 | 
57 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
58 | 	switch msg := msg.(type) {
59 | 	case tea.KeyMsg:
60 | 		return m, tea.Quit
61 | 
62 | 	case tea.WindowSizeMsg:
63 | 		m.progress.Width = msg.Width - padding*2 - 4
64 | 		if m.progress.Width > maxWidth {
65 | 			m.progress.Width = maxWidth
66 | 		}
67 | 		return m, nil
68 | 
69 | 	case tickMsg:
70 | 		m.percent += 0.25
71 | 		if m.percent > 1.0 {
72 | 			m.percent = 1.0
73 | 			return m, tea.Quit
74 | 		}
75 | 		return m, tickCmd()
76 | 
77 | 	default:
78 | 		return m, nil
79 | 	}
80 | }
81 | 
82 | func (m model) View() string {
83 | 	pad := strings.Repeat(" ", padding)
84 | 	return "\n" +
85 | 		pad + m.progress.ViewAs(m.percent) + "\n\n" +
86 | 		pad + helpStyle("Press any key to quit")
87 | }
88 | 
89 | func tickCmd() tea.Cmd {
90 | 	return tea.Tick(time.Second, func(t time.Time) tea.Msg {
91 | 		return tickMsg(t)
92 | 	})
93 | }
94 | 


--------------------------------------------------------------------------------
/examples/progress-static/progress-static.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/progress-static/progress-static.gif


--------------------------------------------------------------------------------
/examples/realtime/README.md:
--------------------------------------------------------------------------------
1 | # Real Time
2 | 
3 | <img width="800" src="./realtime.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/realtime/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple example that shows how to send activity to Bubble Tea in real-time
 4 | // through a channel.
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"math/rand"
 9 | 	"os"
10 | 	"time"
11 | 
12 | 	"github.com/charmbracelet/bubbles/spinner"
13 | 	tea "github.com/charmbracelet/bubbletea"
14 | )
15 | 
16 | // A message used to indicate that activity has occurred. In the real world (for
17 | // example, chat) this would contain actual data.
18 | type responseMsg struct{}
19 | 
20 | // Simulate a process that sends events at an irregular interval in real time.
21 | // In this case, we'll send events on the channel at a random interval between
22 | // 100 to 1000 milliseconds. As a command, Bubble Tea will run this
23 | // asynchronously.
24 | func listenForActivity(sub chan struct{}) tea.Cmd {
25 | 	return func() tea.Msg {
26 | 		for {
27 | 			time.Sleep(time.Millisecond * time.Duration(rand.Int63n(900)+100)) // nolint:gosec
28 | 			sub <- struct{}{}
29 | 		}
30 | 	}
31 | }
32 | 
33 | // A command that waits for the activity on a channel.
34 | func waitForActivity(sub chan struct{}) tea.Cmd {
35 | 	return func() tea.Msg {
36 | 		return responseMsg(<-sub)
37 | 	}
38 | }
39 | 
40 | type model struct {
41 | 	sub       chan struct{} // where we'll receive activity notifications
42 | 	responses int           // how many responses we've received
43 | 	spinner   spinner.Model
44 | 	quitting  bool
45 | }
46 | 
47 | func (m model) Init() tea.Cmd {
48 | 	return tea.Batch(
49 | 		m.spinner.Tick,
50 | 		listenForActivity(m.sub), // generate activity
51 | 		waitForActivity(m.sub),   // wait for activity
52 | 	)
53 | }
54 | 
55 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
56 | 	switch msg.(type) {
57 | 	case tea.KeyMsg:
58 | 		m.quitting = true
59 | 		return m, tea.Quit
60 | 	case responseMsg:
61 | 		m.responses++                    // record external activity
62 | 		return m, waitForActivity(m.sub) // wait for next event
63 | 	case spinner.TickMsg:
64 | 		var cmd tea.Cmd
65 | 		m.spinner, cmd = m.spinner.Update(msg)
66 | 		return m, cmd
67 | 	default:
68 | 		return m, nil
69 | 	}
70 | }
71 | 
72 | func (m model) View() string {
73 | 	s := fmt.Sprintf("\n %s Events received: %d\n\n Press any key to exit\n", m.spinner.View(), m.responses)
74 | 	if m.quitting {
75 | 		s += "\n"
76 | 	}
77 | 	return s
78 | }
79 | 
80 | func main() {
81 | 	p := tea.NewProgram(model{
82 | 		sub:     make(chan struct{}),
83 | 		spinner: spinner.New(),
84 | 	})
85 | 
86 | 	if _, err := p.Run(); err != nil {
87 | 		fmt.Println("could not start program:", err)
88 | 		os.Exit(1)
89 | 	}
90 | }
91 | 


--------------------------------------------------------------------------------
/examples/realtime/realtime.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/realtime/realtime.gif


--------------------------------------------------------------------------------
/examples/result/README.md:
--------------------------------------------------------------------------------
1 | # Result
2 | 
3 | <img width="800" src="./result.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/result/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple example that shows how to retrieve a value from a Bubble Tea
 4 | // program after the Bubble Tea has exited.
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | 	"strings"
10 | 
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | )
13 | 
14 | var choices = []string{"Taro", "Coffee", "Lychee"}
15 | 
16 | type model struct {
17 | 	cursor int
18 | 	choice string
19 | }
20 | 
21 | func (m model) Init() tea.Cmd {
22 | 	return nil
23 | }
24 | 
25 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
26 | 	switch msg := msg.(type) {
27 | 	case tea.KeyMsg:
28 | 		switch msg.String() {
29 | 		case "ctrl+c", "q", "esc":
30 | 			return m, tea.Quit
31 | 
32 | 		case "enter":
33 | 			// Send the choice on the channel and exit.
34 | 			m.choice = choices[m.cursor]
35 | 			return m, tea.Quit
36 | 
37 | 		case "down", "j":
38 | 			m.cursor++
39 | 			if m.cursor >= len(choices) {
40 | 				m.cursor = 0
41 | 			}
42 | 
43 | 		case "up", "k":
44 | 			m.cursor--
45 | 			if m.cursor < 0 {
46 | 				m.cursor = len(choices) - 1
47 | 			}
48 | 		}
49 | 	}
50 | 
51 | 	return m, nil
52 | }
53 | 
54 | func (m model) View() string {
55 | 	s := strings.Builder{}
56 | 	s.WriteString("What kind of Bubble Tea would you like to order?\n\n")
57 | 
58 | 	for i := 0; i < len(choices); i++ {
59 | 		if m.cursor == i {
60 | 			s.WriteString("(•) ")
61 | 		} else {
62 | 			s.WriteString("( ) ")
63 | 		}
64 | 		s.WriteString(choices[i])
65 | 		s.WriteString("\n")
66 | 	}
67 | 	s.WriteString("\n(press q to quit)\n")
68 | 
69 | 	return s.String()
70 | }
71 | 
72 | func main() {
73 | 	p := tea.NewProgram(model{})
74 | 
75 | 	// Run returns the model as a tea.Model.
76 | 	m, err := p.Run()
77 | 	if err != nil {
78 | 		fmt.Println("Oh no:", err)
79 | 		os.Exit(1)
80 | 	}
81 | 
82 | 	// Assert the final tea.Model to our local model and print the choice.
83 | 	if m, ok := m.(model); ok && m.choice != "" {
84 | 		fmt.Printf("\n---\nYou chose %s!\n", m.choice)
85 | 	}
86 | }
87 | 


--------------------------------------------------------------------------------
/examples/result/result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/result/result.gif


--------------------------------------------------------------------------------
/examples/send-msg/README.md:
--------------------------------------------------------------------------------
1 | # Send Msg
2 | 
3 | <img width="800" src="./send-msg.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/send-msg/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | // A simple example that shows how to send messages to a Bubble Tea program
  4 | // from outside the program using Program.Send(Msg).
  5 | 
  6 | import (
  7 | 	"fmt"
  8 | 	"math/rand"
  9 | 	"os"
 10 | 	"strings"
 11 | 	"time"
 12 | 
 13 | 	"github.com/charmbracelet/bubbles/spinner"
 14 | 	tea "github.com/charmbracelet/bubbletea"
 15 | 	"github.com/charmbracelet/lipgloss"
 16 | )
 17 | 
 18 | var (
 19 | 	spinnerStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
 20 | 	helpStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0)
 21 | 	dotStyle      = helpStyle.UnsetMargins()
 22 | 	durationStyle = dotStyle
 23 | 	appStyle      = lipgloss.NewStyle().Margin(1, 2, 0, 2)
 24 | )
 25 | 
 26 | type resultMsg struct {
 27 | 	duration time.Duration
 28 | 	food     string
 29 | }
 30 | 
 31 | func (r resultMsg) String() string {
 32 | 	if r.duration == 0 {
 33 | 		return dotStyle.Render(strings.Repeat(".", 30))
 34 | 	}
 35 | 	return fmt.Sprintf("🍔 Ate %s %s", r.food,
 36 | 		durationStyle.Render(r.duration.String()))
 37 | }
 38 | 
 39 | type model struct {
 40 | 	spinner  spinner.Model
 41 | 	results  []resultMsg
 42 | 	quitting bool
 43 | }
 44 | 
 45 | func newModel() model {
 46 | 	const numLastResults = 5
 47 | 	s := spinner.New()
 48 | 	s.Style = spinnerStyle
 49 | 	return model{
 50 | 		spinner: s,
 51 | 		results: make([]resultMsg, numLastResults),
 52 | 	}
 53 | }
 54 | 
 55 | func (m model) Init() tea.Cmd {
 56 | 	return m.spinner.Tick
 57 | }
 58 | 
 59 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 60 | 	switch msg := msg.(type) {
 61 | 	case tea.KeyMsg:
 62 | 		m.quitting = true
 63 | 		return m, tea.Quit
 64 | 	case resultMsg:
 65 | 		m.results = append(m.results[1:], msg)
 66 | 		return m, nil
 67 | 	case spinner.TickMsg:
 68 | 		var cmd tea.Cmd
 69 | 		m.spinner, cmd = m.spinner.Update(msg)
 70 | 		return m, cmd
 71 | 	default:
 72 | 		return m, nil
 73 | 	}
 74 | }
 75 | 
 76 | func (m model) View() string {
 77 | 	var s string
 78 | 
 79 | 	if m.quitting {
 80 | 		s += "That’s all for today!"
 81 | 	} else {
 82 | 		s += m.spinner.View() + " Eating food..."
 83 | 	}
 84 | 
 85 | 	s += "\n\n"
 86 | 
 87 | 	for _, res := range m.results {
 88 | 		s += res.String() + "\n"
 89 | 	}
 90 | 
 91 | 	if !m.quitting {
 92 | 		s += helpStyle.Render("Press any key to exit")
 93 | 	}
 94 | 
 95 | 	if m.quitting {
 96 | 		s += "\n"
 97 | 	}
 98 | 
 99 | 	return appStyle.Render(s)
100 | }
101 | 
102 | func main() {
103 | 	p := tea.NewProgram(newModel())
104 | 
105 | 	// Simulate activity
106 | 	go func() {
107 | 		for {
108 | 			pause := time.Duration(rand.Int63n(899)+100) * time.Millisecond // nolint:gosec
109 | 			time.Sleep(pause)
110 | 
111 | 			// Send the Bubble Tea program a message from outside the
112 | 			// tea.Program. This will block until it is ready to receive
113 | 			// messages.
114 | 			p.Send(resultMsg{food: randomFood(), duration: pause})
115 | 		}
116 | 	}()
117 | 
118 | 	if _, err := p.Run(); err != nil {
119 | 		fmt.Println("Error running program:", err)
120 | 		os.Exit(1)
121 | 	}
122 | }
123 | 
124 | func randomFood() string {
125 | 	food := []string{
126 | 		"an apple", "a pear", "a gherkin", "a party gherkin",
127 | 		"a kohlrabi", "some spaghetti", "tacos", "a currywurst", "some curry",
128 | 		"a sandwich", "some peanut butter", "some cashews", "some ramen",
129 | 	}
130 | 	return food[rand.Intn(len(food))] // nolint:gosec
131 | }
132 | 


--------------------------------------------------------------------------------
/examples/send-msg/send-msg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/send-msg/send-msg.gif


--------------------------------------------------------------------------------
/examples/sequence/README.md:
--------------------------------------------------------------------------------
1 | # Sequence
2 | 
3 | <img width="800" src="./sequence.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/sequence/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple example illustrating how to run a series of commands in order.
 4 | 
 5 | import (
 6 | 	"fmt"
 7 | 	"os"
 8 | 
 9 | 	tea "github.com/charmbracelet/bubbletea"
10 | )
11 | 
12 | type model struct{}
13 | 
14 | func (m model) Init() tea.Cmd {
15 | 	return tea.Sequence(
16 | 		tea.Batch(
17 | 			tea.Println("A"),
18 | 			tea.Println("B"),
19 | 			tea.Println("C"),
20 | 		),
21 | 		tea.Println("Z"),
22 | 		tea.Quit,
23 | 	)
24 | }
25 | 
26 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
27 | 	switch msg.(type) {
28 | 	case tea.KeyMsg:
29 | 		return m, tea.Quit
30 | 	}
31 | 	return m, nil
32 | }
33 | 
34 | func (m model) View() string {
35 | 	return ""
36 | }
37 | 
38 | func main() {
39 | 	if _, err := tea.NewProgram(model{}).Run(); err != nil {
40 | 		fmt.Println("Uh oh:", err)
41 | 		os.Exit(1)
42 | 	}
43 | }
44 | 


--------------------------------------------------------------------------------
/examples/sequence/sequence.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/sequence/sequence.gif


--------------------------------------------------------------------------------
/examples/set-window-title/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple example illustrating how to set a window title.
 4 | 
 5 | import (
 6 | 	"fmt"
 7 | 	"os"
 8 | 
 9 | 	tea "github.com/charmbracelet/bubbletea"
10 | )
11 | 
12 | type model struct{}
13 | 
14 | func (m model) Init() tea.Cmd {
15 | 	return tea.SetWindowTitle("Bubble Tea Example")
16 | }
17 | 
18 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
19 | 	switch msg.(type) {
20 | 	case tea.KeyMsg:
21 | 		return m, tea.Quit
22 | 	}
23 | 	return m, nil
24 | }
25 | 
26 | func (m model) View() string {
27 | 	return "\nPress any key to quit."
28 | }
29 | 
30 | func main() {
31 | 	if _, err := tea.NewProgram(model{}).Run(); err != nil {
32 | 		fmt.Println("Uh oh:", err)
33 | 		os.Exit(1)
34 | 	}
35 | }
36 | 


--------------------------------------------------------------------------------
/examples/simple/README.md:
--------------------------------------------------------------------------------
1 | # Simple
2 | 
3 | <img width="800" src="./simple.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/simple/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program that counts down from 5 and then exits.
 4 | 
 5 | import (
 6 | 	"fmt"
 7 | 	"log"
 8 | 	"os"
 9 | 	"time"
10 | 
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | )
13 | 
14 | func main() {
15 | 	// Log to a file. Useful in debugging since you can't really log to stdout.
16 | 	// Not required.
17 | 	logfilePath := os.Getenv("BUBBLETEA_LOG")
18 | 	if logfilePath != "" {
19 | 		if _, err := tea.LogToFile(logfilePath, "simple"); err != nil {
20 | 			log.Fatal(err)
21 | 		}
22 | 	}
23 | 
24 | 	// Initialize our program
25 | 	p := tea.NewProgram(model(5))
26 | 	if _, err := p.Run(); err != nil {
27 | 		log.Fatal(err)
28 | 	}
29 | }
30 | 
31 | // A model can be more or less any type of data. It holds all the data for a
32 | // program, so often it's a struct. For this simple example, however, all
33 | // we'll need is a simple integer.
34 | type model int
35 | 
36 | // Init optionally returns an initial command we should run. In this case we
37 | // want to start the timer.
38 | func (m model) Init() tea.Cmd {
39 | 	return tick
40 | }
41 | 
42 | // Update is called when messages are received. The idea is that you inspect the
43 | // message and send back an updated model accordingly. You can also return
44 | // a command, which is a function that performs I/O and returns a message.
45 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
46 | 	switch msg := msg.(type) {
47 | 	case tea.KeyMsg:
48 | 		switch msg.String() {
49 | 		case "ctrl+c", "q":
50 | 			return m, tea.Quit
51 | 		case "ctrl+z":
52 | 			return m, tea.Suspend
53 | 		}
54 | 
55 | 	case tickMsg:
56 | 		m--
57 | 		if m <= 0 {
58 | 			return m, tea.Quit
59 | 		}
60 | 		return m, tick
61 | 	}
62 | 	return m, nil
63 | }
64 | 
65 | // View returns a string based on data in the model. That string which will be
66 | // rendered to the terminal.
67 | func (m model) View() string {
68 | 	return fmt.Sprintf("Hi. This program will exit in %d seconds.\n\nTo quit sooner press ctrl-c, or press ctrl-z to suspend...\n", m)
69 | }
70 | 
71 | // Messages are events that we respond to in our Update function. This
72 | // particular one indicates that the timer has ticked.
73 | type tickMsg time.Time
74 | 
75 | func tick() tea.Msg {
76 | 	time.Sleep(time.Second)
77 | 	return tickMsg{}
78 | }
79 | 


--------------------------------------------------------------------------------
/examples/simple/main_test.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"bytes"
 5 | 	"io"
 6 | 	"regexp"
 7 | 	"testing"
 8 | 	"time"
 9 | 
10 | 	tea "github.com/charmbracelet/bubbletea"
11 | 	"github.com/charmbracelet/x/exp/teatest"
12 | )
13 | 
14 | func TestApp(t *testing.T) {
15 | 	m := model(10)
16 | 	tm := teatest.NewTestModel(
17 | 		t, m,
18 | 		teatest.WithInitialTermSize(70, 30),
19 | 	)
20 | 	t.Cleanup(func() {
21 | 		if err := tm.Quit(); err != nil {
22 | 			t.Fatal(err)
23 | 		}
24 | 	})
25 | 
26 | 	time.Sleep(time.Second + time.Millisecond*200)
27 | 	tm.Type("I'm typing things, but it'll be ignored by my program")
28 | 	tm.Send("ignored msg")
29 | 	tm.Send(tea.KeyMsg{
30 | 		Type: tea.KeyEnter,
31 | 	})
32 | 
33 | 	if err := tm.Quit(); err != nil {
34 | 		t.Fatal(err)
35 | 	}
36 | 
37 | 	out := readBts(t, tm.FinalOutput(t))
38 | 	if !regexp.MustCompile(`This program will exit in \d+ seconds`).Match(out) {
39 | 		t.Fatalf("output does not match the given regular expression: %s", string(out))
40 | 	}
41 | 	teatest.RequireEqualOutput(t, out)
42 | 
43 | 	if tm.FinalModel(t).(model) != 9 {
44 | 		t.Errorf("expected model to be 10, was %d", m)
45 | 	}
46 | }
47 | 
48 | func TestAppInteractive(t *testing.T) {
49 | 	m := model(10)
50 | 	tm := teatest.NewTestModel(
51 | 		t, m,
52 | 		teatest.WithInitialTermSize(70, 30),
53 | 	)
54 | 
55 | 	time.Sleep(time.Second + time.Millisecond*200)
56 | 	tm.Send("ignored msg")
57 | 
58 | 	if bts := readBts(t, tm.Output()); !bytes.Contains(bts, []byte("This program will exit in 9 seconds")) {
59 | 		t.Fatalf("output does not match: expected %q", string(bts))
60 | 	}
61 | 
62 | 	teatest.WaitFor(t, tm.Output(), func(out []byte) bool {
63 | 		return bytes.Contains(out, []byte("This program will exit in 7 seconds"))
64 | 	}, teatest.WithDuration(5*time.Second))
65 | 
66 | 	tm.Send(tea.KeyMsg{
67 | 		Type: tea.KeyEnter,
68 | 	})
69 | 
70 | 	if err := tm.Quit(); err != nil {
71 | 		t.Fatal(err)
72 | 	}
73 | 
74 | 	if tm.FinalModel(t).(model) != 7 {
75 | 		t.Errorf("expected model to be 7, was %d", m)
76 | 	}
77 | }
78 | 
79 | func readBts(tb testing.TB, r io.Reader) []byte {
80 | 	tb.Helper()
81 | 	bts, err := io.ReadAll(r)
82 | 	if err != nil {
83 | 		tb.Fatal(err)
84 | 	}
85 | 	return bts
86 | }
87 | 


--------------------------------------------------------------------------------
/examples/simple/simple.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/simple/simple.gif


--------------------------------------------------------------------------------
/examples/simple/testdata/TestApp.golden:
--------------------------------------------------------------------------------
1 | [?25l[?2004h
Hi. This program will exit in 10 seconds.
2 | 
3 | To quit sooner press ctrl-c, or press ctrl-z to suspend...
4 | Hi. This program will exit in 9 seconds.
5 | 
6 | 
7 | 
[?2004l[?25h[?1002l[?1003l[?1006l


--------------------------------------------------------------------------------
/examples/spinner/README.md:
--------------------------------------------------------------------------------
1 | # Spinner
2 | 
3 | <img width="800" src="./spinner.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/spinner/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program demonstrating the spinner component from the Bubbles
 4 | // component library.
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | 
10 | 	"github.com/charmbracelet/bubbles/spinner"
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | 	"github.com/charmbracelet/lipgloss"
13 | )
14 | 
15 | type errMsg error
16 | 
17 | type model struct {
18 | 	spinner  spinner.Model
19 | 	quitting bool
20 | 	err      error
21 | }
22 | 
23 | func initialModel() model {
24 | 	s := spinner.New()
25 | 	s.Spinner = spinner.Dot
26 | 	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
27 | 	return model{spinner: s}
28 | }
29 | 
30 | func (m model) Init() tea.Cmd {
31 | 	return m.spinner.Tick
32 | }
33 | 
34 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
35 | 	switch msg := msg.(type) {
36 | 	case tea.KeyMsg:
37 | 		switch msg.String() {
38 | 		case "q", "esc", "ctrl+c":
39 | 			m.quitting = true
40 | 			return m, tea.Quit
41 | 		default:
42 | 			return m, nil
43 | 		}
44 | 
45 | 	case errMsg:
46 | 		m.err = msg
47 | 		return m, nil
48 | 
49 | 	default:
50 | 		var cmd tea.Cmd
51 | 		m.spinner, cmd = m.spinner.Update(msg)
52 | 		return m, cmd
53 | 	}
54 | }
55 | 
56 | func (m model) View() string {
57 | 	if m.err != nil {
58 | 		return m.err.Error()
59 | 	}
60 | 	str := fmt.Sprintf("\n\n   %s Loading forever...press q to quit\n\n", m.spinner.View())
61 | 	if m.quitting {
62 | 		return str + "\n"
63 | 	}
64 | 	return str
65 | }
66 | 
67 | func main() {
68 | 	p := tea.NewProgram(initialModel())
69 | 	if _, err := p.Run(); err != nil {
70 | 		fmt.Println(err)
71 | 		os.Exit(1)
72 | 	}
73 | }
74 | 


--------------------------------------------------------------------------------
/examples/spinner/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/spinner/spinner.gif


--------------------------------------------------------------------------------
/examples/spinners/README.md:
--------------------------------------------------------------------------------
1 | # Spinners
2 | 
3 | <img width="800" src="./spinners.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/spinners/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 
  7 | 	"github.com/charmbracelet/bubbles/spinner"
  8 | 	tea "github.com/charmbracelet/bubbletea"
  9 | 	"github.com/charmbracelet/lipgloss"
 10 | )
 11 | 
 12 | var (
 13 | 	// Available spinners
 14 | 	spinners = []spinner.Spinner{
 15 | 		spinner.Line,
 16 | 		spinner.Dot,
 17 | 		spinner.MiniDot,
 18 | 		spinner.Jump,
 19 | 		spinner.Pulse,
 20 | 		spinner.Points,
 21 | 		spinner.Globe,
 22 | 		spinner.Moon,
 23 | 		spinner.Monkey,
 24 | 	}
 25 | 
 26 | 	textStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Render
 27 | 	spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69"))
 28 | 	helpStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render
 29 | )
 30 | 
 31 | func main() {
 32 | 	m := model{}
 33 | 	m.resetSpinner()
 34 | 
 35 | 	if _, err := tea.NewProgram(m).Run(); err != nil {
 36 | 		fmt.Println("could not run program:", err)
 37 | 		os.Exit(1)
 38 | 	}
 39 | }
 40 | 
 41 | type model struct {
 42 | 	index   int
 43 | 	spinner spinner.Model
 44 | }
 45 | 
 46 | func (m model) Init() tea.Cmd {
 47 | 	return m.spinner.Tick
 48 | }
 49 | 
 50 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 51 | 	switch msg := msg.(type) {
 52 | 	case tea.KeyMsg:
 53 | 		switch msg.String() {
 54 | 		case "ctrl+c", "q", "esc":
 55 | 			return m, tea.Quit
 56 | 		case "h", "left":
 57 | 			m.index--
 58 | 			if m.index < 0 {
 59 | 				m.index = len(spinners) - 1
 60 | 			}
 61 | 			m.resetSpinner()
 62 | 			return m, m.spinner.Tick
 63 | 		case "l", "right":
 64 | 			m.index++
 65 | 			if m.index >= len(spinners) {
 66 | 				m.index = 0
 67 | 			}
 68 | 			m.resetSpinner()
 69 | 			return m, m.spinner.Tick
 70 | 		default:
 71 | 			return m, nil
 72 | 		}
 73 | 	case spinner.TickMsg:
 74 | 		var cmd tea.Cmd
 75 | 		m.spinner, cmd = m.spinner.Update(msg)
 76 | 		return m, cmd
 77 | 	default:
 78 | 		return m, nil
 79 | 	}
 80 | }
 81 | 
 82 | func (m *model) resetSpinner() {
 83 | 	m.spinner = spinner.New()
 84 | 	m.spinner.Style = spinnerStyle
 85 | 	m.spinner.Spinner = spinners[m.index]
 86 | }
 87 | 
 88 | func (m model) View() (s string) {
 89 | 	var gap string
 90 | 	switch m.index {
 91 | 	case 1:
 92 | 		gap = ""
 93 | 	default:
 94 | 		gap = " "
 95 | 	}
 96 | 
 97 | 	s += fmt.Sprintf("\n %s%s%s\n\n", m.spinner.View(), gap, textStyle("Spinning..."))
 98 | 	s += helpStyle("h/l, ←/→: change spinner • q: exit\n")
 99 | 	return
100 | }
101 | 


--------------------------------------------------------------------------------
/examples/spinners/spinners.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/spinners/spinners.gif


--------------------------------------------------------------------------------
/examples/split-editors/README.md:
--------------------------------------------------------------------------------
1 | # Split Editors
2 | 
3 | <img width="800" src="./split-editors.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/split-editors/split-editors.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/split-editors/split-editors.gif


--------------------------------------------------------------------------------
/examples/stopwatch/README.md:
--------------------------------------------------------------------------------
1 | # Stopwatch
2 | 
3 | <img width="800" src="./stopwatch.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/stopwatch/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 	"time"
  7 | 
  8 | 	"github.com/charmbracelet/bubbles/help"
  9 | 	"github.com/charmbracelet/bubbles/key"
 10 | 	"github.com/charmbracelet/bubbles/stopwatch"
 11 | 	tea "github.com/charmbracelet/bubbletea"
 12 | )
 13 | 
 14 | type model struct {
 15 | 	stopwatch stopwatch.Model
 16 | 	keymap    keymap
 17 | 	help      help.Model
 18 | 	quitting  bool
 19 | }
 20 | 
 21 | type keymap struct {
 22 | 	start key.Binding
 23 | 	stop  key.Binding
 24 | 	reset key.Binding
 25 | 	quit  key.Binding
 26 | }
 27 | 
 28 | func (m model) Init() tea.Cmd {
 29 | 	return m.stopwatch.Init()
 30 | }
 31 | 
 32 | func (m model) View() string {
 33 | 	// Note: you could further customize the time output by getting the
 34 | 	// duration from m.stopwatch.Elapsed(), which returns a time.Duration, and
 35 | 	// skip m.stopwatch.View() altogether.
 36 | 	s := m.stopwatch.View() + "\n"
 37 | 	if !m.quitting {
 38 | 		s = "Elapsed: " + s
 39 | 		s += m.helpView()
 40 | 	}
 41 | 	return s
 42 | }
 43 | 
 44 | func (m model) helpView() string {
 45 | 	return "\n" + m.help.ShortHelpView([]key.Binding{
 46 | 		m.keymap.start,
 47 | 		m.keymap.stop,
 48 | 		m.keymap.reset,
 49 | 		m.keymap.quit,
 50 | 	})
 51 | }
 52 | 
 53 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 54 | 	switch msg := msg.(type) {
 55 | 	case tea.KeyMsg:
 56 | 		switch {
 57 | 		case key.Matches(msg, m.keymap.quit):
 58 | 			m.quitting = true
 59 | 			return m, tea.Quit
 60 | 		case key.Matches(msg, m.keymap.reset):
 61 | 			return m, m.stopwatch.Reset()
 62 | 		case key.Matches(msg, m.keymap.start, m.keymap.stop):
 63 | 			m.keymap.stop.SetEnabled(!m.stopwatch.Running())
 64 | 			m.keymap.start.SetEnabled(m.stopwatch.Running())
 65 | 			return m, m.stopwatch.Toggle()
 66 | 		}
 67 | 	}
 68 | 	var cmd tea.Cmd
 69 | 	m.stopwatch, cmd = m.stopwatch.Update(msg)
 70 | 	return m, cmd
 71 | }
 72 | 
 73 | func main() {
 74 | 	m := model{
 75 | 		stopwatch: stopwatch.NewWithInterval(time.Millisecond),
 76 | 		keymap: keymap{
 77 | 			start: key.NewBinding(
 78 | 				key.WithKeys("s"),
 79 | 				key.WithHelp("s", "start"),
 80 | 			),
 81 | 			stop: key.NewBinding(
 82 | 				key.WithKeys("s"),
 83 | 				key.WithHelp("s", "stop"),
 84 | 			),
 85 | 			reset: key.NewBinding(
 86 | 				key.WithKeys("r"),
 87 | 				key.WithHelp("r", "reset"),
 88 | 			),
 89 | 			quit: key.NewBinding(
 90 | 				key.WithKeys("ctrl+c", "q"),
 91 | 				key.WithHelp("q", "quit"),
 92 | 			),
 93 | 		},
 94 | 		help: help.New(),
 95 | 	}
 96 | 
 97 | 	m.keymap.start.SetEnabled(false)
 98 | 
 99 | 	if _, err := tea.NewProgram(m).Run(); err != nil {
100 | 		fmt.Println("Oh no, it didn't work:", err)
101 | 		os.Exit(1)
102 | 	}
103 | }
104 | 


--------------------------------------------------------------------------------
/examples/stopwatch/stopwatch.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/stopwatch/stopwatch.gif


--------------------------------------------------------------------------------
/examples/suspend/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"fmt"
 6 | 	"os"
 7 | 
 8 | 	tea "github.com/charmbracelet/bubbletea"
 9 | )
10 | 
11 | type model struct {
12 | 	quitting   bool
13 | 	suspending bool
14 | }
15 | 
16 | func (m model) Init() tea.Cmd {
17 | 	return nil
18 | }
19 | 
20 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
21 | 	switch msg := msg.(type) {
22 | 	case tea.ResumeMsg:
23 | 		m.suspending = false
24 | 		return m, nil
25 | 	case tea.KeyMsg:
26 | 		switch msg.String() {
27 | 		case "q", "esc":
28 | 			m.quitting = true
29 | 			return m, tea.Quit
30 | 		case "ctrl+c":
31 | 			m.quitting = true
32 | 			return m, tea.Interrupt
33 | 		case "ctrl+z":
34 | 			m.suspending = true
35 | 			return m, tea.Suspend
36 | 		}
37 | 	}
38 | 	return m, nil
39 | }
40 | 
41 | func (m model) View() string {
42 | 	if m.suspending || m.quitting {
43 | 		return ""
44 | 	}
45 | 
46 | 	return "\nPress ctrl-z to suspend, ctrl+c to interrupt, q, or esc to exit\n"
47 | }
48 | 
49 | func main() {
50 | 	if _, err := tea.NewProgram(model{}).Run(); err != nil {
51 | 		fmt.Println("Error running program:", err)
52 | 		if errors.Is(err, tea.ErrInterrupted) {
53 | 			os.Exit(130)
54 | 		}
55 | 		os.Exit(1)
56 | 	}
57 | }
58 | 


--------------------------------------------------------------------------------
/examples/table-resize/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 
  7 | 	tea "github.com/charmbracelet/bubbletea"
  8 | 	"github.com/charmbracelet/lipgloss"
  9 | 	"github.com/charmbracelet/lipgloss/table"
 10 | )
 11 | 
 12 | type model struct {
 13 | 	table *table.Table
 14 | }
 15 | 
 16 | func (m model) Init() tea.Cmd { return nil }
 17 | 
 18 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 19 | 	var cmd tea.Cmd
 20 | 	switch msg := msg.(type) {
 21 | 	case tea.WindowSizeMsg:
 22 | 		m.table = m.table.Width(msg.Width)
 23 | 		m.table = m.table.Height(msg.Height)
 24 | 	case tea.KeyMsg:
 25 | 		switch msg.String() {
 26 | 		case "q", "ctrl+c":
 27 | 			return m, tea.Quit
 28 | 		case "enter":
 29 | 		}
 30 | 	}
 31 | 	return m, cmd
 32 | }
 33 | 
 34 | func (m model) View() string {
 35 | 	return "\n" + m.table.String() + "\n"
 36 | }
 37 | 
 38 | func main() {
 39 | 	re := lipgloss.NewRenderer(os.Stdout)
 40 | 	baseStyle := re.NewStyle().Padding(0, 1)
 41 | 	headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true)
 42 | 	selectedStyle := baseStyle.Foreground(lipgloss.Color("#01BE85")).Background(lipgloss.Color("#00432F"))
 43 | 	typeColors := map[string]lipgloss.Color{
 44 | 		"Bug":      lipgloss.Color("#D7FF87"),
 45 | 		"Electric": lipgloss.Color("#FDFF90"),
 46 | 		"Fire":     lipgloss.Color("#FF7698"),
 47 | 		"Flying":   lipgloss.Color("#FF87D7"),
 48 | 		"Grass":    lipgloss.Color("#75FBAB"),
 49 | 		"Ground":   lipgloss.Color("#FF875F"),
 50 | 		"Normal":   lipgloss.Color("#929292"),
 51 | 		"Poison":   lipgloss.Color("#7D5AFC"),
 52 | 		"Water":    lipgloss.Color("#00E2C7"),
 53 | 	}
 54 | 	dimTypeColors := map[string]lipgloss.Color{
 55 | 		"Bug":      lipgloss.Color("#97AD64"),
 56 | 		"Electric": lipgloss.Color("#FCFF5F"),
 57 | 		"Fire":     lipgloss.Color("#BA5F75"),
 58 | 		"Flying":   lipgloss.Color("#C97AB2"),
 59 | 		"Grass":    lipgloss.Color("#59B980"),
 60 | 		"Ground":   lipgloss.Color("#C77252"),
 61 | 		"Normal":   lipgloss.Color("#727272"),
 62 | 		"Poison":   lipgloss.Color("#634BD0"),
 63 | 		"Water":    lipgloss.Color("#439F8E"),
 64 | 	}
 65 | 	headers := []string{"#", "NAME", "TYPE 1", "TYPE 2", "JAPANESE", "OFFICIAL ROM."}
 66 | 	rows := [][]string{
 67 | 		{"1", "Bulbasaur", "Grass", "Poison", "フシギダネ", "Bulbasaur"},
 68 | 		{"2", "Ivysaur", "Grass", "Poison", "フシギソウ", "Ivysaur"},
 69 | 		{"3", "Venusaur", "Grass", "Poison", "フシギバナ", "Venusaur"},
 70 | 		{"4", "Charmander", "Fire", "", "ヒトカゲ", "Hitokage"},
 71 | 		{"5", "Charmeleon", "Fire", "", "リザード", "Lizardo"},
 72 | 		{"6", "Charizard", "Fire", "Flying", "リザードン", "Lizardon"},
 73 | 		{"7", "Squirtle", "Water", "", "ゼニガメ", "Zenigame"},
 74 | 		{"8", "Wartortle", "Water", "", "カメール", "Kameil"},
 75 | 		{"9", "Blastoise", "Water", "", "カメックス", "Kamex"},
 76 | 		{"10", "Caterpie", "Bug", "", "キャタピー", "Caterpie"},
 77 | 		{"11", "Metapod", "Bug", "", "トランセル", "Trancell"},
 78 | 		{"12", "Butterfree", "Bug", "Flying", "バタフリー", "Butterfree"},
 79 | 		{"13", "Weedle", "Bug", "Poison", "ビードル", "Beedle"},
 80 | 		{"14", "Kakuna", "Bug", "Poison", "コクーン", "Cocoon"},
 81 | 		{"15", "Beedrill", "Bug", "Poison", "スピアー", "Spear"},
 82 | 		{"16", "Pidgey", "Normal", "Flying", "ポッポ", "Poppo"},
 83 | 		{"17", "Pidgeotto", "Normal", "Flying", "ピジョン", "Pigeon"},
 84 | 		{"18", "Pidgeot", "Normal", "Flying", "ピジョット", "Pigeot"},
 85 | 		{"19", "Rattata", "Normal", "", "コラッタ", "Koratta"},
 86 | 		{"20", "Raticate", "Normal", "", "ラッタ", "Ratta"},
 87 | 		{"21", "Spearow", "Normal", "Flying", "オニスズメ", "Onisuzume"},
 88 | 		{"22", "Fearow", "Normal", "Flying", "オニドリル", "Onidrill"},
 89 | 		{"23", "Ekans", "Poison", "", "アーボ", "Arbo"},
 90 | 		{"24", "Arbok", "Poison", "", "アーボック", "Arbok"},
 91 | 		{"25", "Pikachu", "Electric", "", "ピカチュウ", "Pikachu"},
 92 | 		{"26", "Raichu", "Electric", "", "ライチュウ", "Raichu"},
 93 | 		{"27", "Sandshrew", "Ground", "", "サンド", "Sand"},
 94 | 		{"28", "Sandslash", "Ground", "", "サンドパン", "Sandpan"},
 95 | 	}
 96 | 
 97 | 	t := table.New().
 98 | 		Headers(headers...).
 99 | 		Rows(rows...).
100 | 		Border(lipgloss.NormalBorder()).
101 | 		BorderStyle(re.NewStyle().Foreground(lipgloss.Color("238"))).
102 | 		StyleFunc(func(row, col int) lipgloss.Style {
103 | 			if row == 0 {
104 | 				return headerStyle
105 | 			}
106 | 
107 | 			rowIndex := row - 1
108 | 			if rowIndex < 0 || rowIndex >= len(rows) {
109 | 				return baseStyle
110 | 			}
111 | 
112 | 			if rows[rowIndex][1] == "Pikachu" {
113 | 				return selectedStyle
114 | 			}
115 | 
116 | 			even := row%2 == 0
117 | 
118 | 			switch col {
119 | 			case 2, 3: // Type 1 + 2
120 | 				c := typeColors
121 | 				if even {
122 | 					c = dimTypeColors
123 | 				}
124 | 
125 | 				if col >= len(rows[rowIndex]) {
126 | 					return baseStyle
127 | 				}
128 | 
129 | 				color, ok := c[rows[rowIndex][col]]
130 | 				if !ok {
131 | 					return baseStyle
132 | 				}
133 | 				return baseStyle.Foreground(color)
134 | 			}
135 | 
136 | 			if even {
137 | 				return baseStyle.Foreground(lipgloss.Color("245"))
138 | 			}
139 | 			return baseStyle.Foreground(lipgloss.Color("252"))
140 | 		}).
141 | 		Border(lipgloss.ThickBorder())
142 | 
143 | 	m := model{t}
144 | 	if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
145 | 		fmt.Println("Error running program:", err)
146 | 		os.Exit(1)
147 | 	}
148 | }
149 | 


--------------------------------------------------------------------------------
/examples/table/README.md:
--------------------------------------------------------------------------------
1 | # Table
2 | 
3 | <img width="800" src="./table.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/table/table.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/table/table.gif


--------------------------------------------------------------------------------
/examples/tabs/README.md:
--------------------------------------------------------------------------------
1 | # Tabs
2 | 
3 | <img width="800" src="./tabs.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/tabs/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 	"strings"
  7 | 
  8 | 	tea "github.com/charmbracelet/bubbletea"
  9 | 	"github.com/charmbracelet/lipgloss"
 10 | )
 11 | 
 12 | type model struct {
 13 | 	Tabs       []string
 14 | 	TabContent []string
 15 | 	activeTab  int
 16 | }
 17 | 
 18 | func (m model) Init() tea.Cmd {
 19 | 	return nil
 20 | }
 21 | 
 22 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 23 | 	switch msg := msg.(type) {
 24 | 	case tea.KeyMsg:
 25 | 		switch keypress := msg.String(); keypress {
 26 | 		case "ctrl+c", "q":
 27 | 			return m, tea.Quit
 28 | 		case "right", "l", "n", "tab":
 29 | 			m.activeTab = min(m.activeTab+1, len(m.Tabs)-1)
 30 | 			return m, nil
 31 | 		case "left", "h", "p", "shift+tab":
 32 | 			m.activeTab = max(m.activeTab-1, 0)
 33 | 			return m, nil
 34 | 		}
 35 | 	}
 36 | 
 37 | 	return m, nil
 38 | }
 39 | 
 40 | func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
 41 | 	border := lipgloss.RoundedBorder()
 42 | 	border.BottomLeft = left
 43 | 	border.Bottom = middle
 44 | 	border.BottomRight = right
 45 | 	return border
 46 | }
 47 | 
 48 | var (
 49 | 	inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴")
 50 | 	activeTabBorder   = tabBorderWithBottom("┘", " ", "└")
 51 | 	docStyle          = lipgloss.NewStyle().Padding(1, 2, 1, 2)
 52 | 	highlightColor    = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
 53 | 	inactiveTabStyle  = lipgloss.NewStyle().Border(inactiveTabBorder, true).BorderForeground(highlightColor).Padding(0, 1)
 54 | 	activeTabStyle    = inactiveTabStyle.Border(activeTabBorder, true)
 55 | 	windowStyle       = lipgloss.NewStyle().BorderForeground(highlightColor).Padding(2, 0).Align(lipgloss.Center).Border(lipgloss.NormalBorder()).UnsetBorderTop()
 56 | )
 57 | 
 58 | func (m model) View() string {
 59 | 	doc := strings.Builder{}
 60 | 
 61 | 	var renderedTabs []string
 62 | 
 63 | 	for i, t := range m.Tabs {
 64 | 		var style lipgloss.Style
 65 | 		isFirst, isLast, isActive := i == 0, i == len(m.Tabs)-1, i == m.activeTab
 66 | 		if isActive {
 67 | 			style = activeTabStyle
 68 | 		} else {
 69 | 			style = inactiveTabStyle
 70 | 		}
 71 | 		border, _, _, _, _ := style.GetBorder()
 72 | 		if isFirst && isActive {
 73 | 			border.BottomLeft = "│"
 74 | 		} else if isFirst && !isActive {
 75 | 			border.BottomLeft = "├"
 76 | 		} else if isLast && isActive {
 77 | 			border.BottomRight = "│"
 78 | 		} else if isLast && !isActive {
 79 | 			border.BottomRight = "┤"
 80 | 		}
 81 | 		style = style.Border(border)
 82 | 		renderedTabs = append(renderedTabs, style.Render(t))
 83 | 	}
 84 | 
 85 | 	row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
 86 | 	doc.WriteString(row)
 87 | 	doc.WriteString("\n")
 88 | 	doc.WriteString(windowStyle.Width((lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize())).Render(m.TabContent[m.activeTab]))
 89 | 	return docStyle.Render(doc.String())
 90 | }
 91 | 
 92 | func main() {
 93 | 	tabs := []string{"Lip Gloss", "Blush", "Eye Shadow", "Mascara", "Foundation"}
 94 | 	tabContent := []string{"Lip Gloss Tab", "Blush Tab", "Eye Shadow Tab", "Mascara Tab", "Foundation Tab"}
 95 | 	m := model{Tabs: tabs, TabContent: tabContent}
 96 | 	if _, err := tea.NewProgram(m).Run(); err != nil {
 97 | 		fmt.Println("Error running program:", err)
 98 | 		os.Exit(1)
 99 | 	}
100 | }
101 | 
102 | func max(a, b int) int {
103 | 	if a > b {
104 | 		return a
105 | 	}
106 | 	return b
107 | }
108 | 
109 | func min(a, b int) int {
110 | 	if a < b {
111 | 		return a
112 | 	}
113 | 	return b
114 | }
115 | 


--------------------------------------------------------------------------------
/examples/tabs/tabs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/tabs/tabs.gif


--------------------------------------------------------------------------------
/examples/textarea/README.md:
--------------------------------------------------------------------------------
1 | # Text Area
2 | 
3 | <img width="800" src="./textarea.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/textarea/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program demonstrating the textarea component from the Bubbles
 4 | // component library.
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"log"
 9 | 
10 | 	"github.com/charmbracelet/bubbles/textarea"
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | )
13 | 
14 | func main() {
15 | 	p := tea.NewProgram(initialModel())
16 | 
17 | 	if _, err := p.Run(); err != nil {
18 | 		log.Fatal(err)
19 | 	}
20 | }
21 | 
22 | type errMsg error
23 | 
24 | type model struct {
25 | 	textarea textarea.Model
26 | 	err      error
27 | }
28 | 
29 | func initialModel() model {
30 | 	ti := textarea.New()
31 | 	ti.Placeholder = "Once upon a time..."
32 | 	ti.Focus()
33 | 
34 | 	return model{
35 | 		textarea: ti,
36 | 		err:      nil,
37 | 	}
38 | }
39 | 
40 | func (m model) Init() tea.Cmd {
41 | 	return textarea.Blink
42 | }
43 | 
44 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
45 | 	var cmds []tea.Cmd
46 | 	var cmd tea.Cmd
47 | 
48 | 	switch msg := msg.(type) {
49 | 	case tea.KeyMsg:
50 | 		switch msg.Type {
51 | 		case tea.KeyEsc:
52 | 			if m.textarea.Focused() {
53 | 				m.textarea.Blur()
54 | 			}
55 | 		case tea.KeyCtrlC:
56 | 			return m, tea.Quit
57 | 		default:
58 | 			if !m.textarea.Focused() {
59 | 				cmd = m.textarea.Focus()
60 | 				cmds = append(cmds, cmd)
61 | 			}
62 | 		}
63 | 
64 | 	// We handle errors just like any other message
65 | 	case errMsg:
66 | 		m.err = msg
67 | 		return m, nil
68 | 	}
69 | 
70 | 	m.textarea, cmd = m.textarea.Update(msg)
71 | 	cmds = append(cmds, cmd)
72 | 	return m, tea.Batch(cmds...)
73 | }
74 | 
75 | func (m model) View() string {
76 | 	return fmt.Sprintf(
77 | 		"Tell me a story.\n\n%s\n\n%s",
78 | 		m.textarea.View(),
79 | 		"(ctrl+c to quit)",
80 | 	) + "\n\n"
81 | }
82 | 


--------------------------------------------------------------------------------
/examples/textarea/textarea.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/textarea/textarea.gif


--------------------------------------------------------------------------------
/examples/textinput/README.md:
--------------------------------------------------------------------------------
1 | # Text Input
2 | 
3 | <img width="800" src="./textinput.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/textinput/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program demonstrating the text input component from the Bubbles
 4 | // component library.
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"log"
 9 | 
10 | 	"github.com/charmbracelet/bubbles/textinput"
11 | 	tea "github.com/charmbracelet/bubbletea"
12 | )
13 | 
14 | func main() {
15 | 	p := tea.NewProgram(initialModel())
16 | 	if _, err := p.Run(); err != nil {
17 | 		log.Fatal(err)
18 | 	}
19 | }
20 | 
21 | type (
22 | 	errMsg error
23 | )
24 | 
25 | type model struct {
26 | 	textInput textinput.Model
27 | 	err       error
28 | }
29 | 
30 | func initialModel() model {
31 | 	ti := textinput.New()
32 | 	ti.Placeholder = "Pikachu"
33 | 	ti.Focus()
34 | 	ti.CharLimit = 156
35 | 	ti.Width = 20
36 | 
37 | 	return model{
38 | 		textInput: ti,
39 | 		err:       nil,
40 | 	}
41 | }
42 | 
43 | func (m model) Init() tea.Cmd {
44 | 	return textinput.Blink
45 | }
46 | 
47 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
48 | 	var cmd tea.Cmd
49 | 
50 | 	switch msg := msg.(type) {
51 | 	case tea.KeyMsg:
52 | 		switch msg.Type {
53 | 		case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
54 | 			return m, tea.Quit
55 | 		}
56 | 
57 | 	// We handle errors just like any other message
58 | 	case errMsg:
59 | 		m.err = msg
60 | 		return m, nil
61 | 	}
62 | 
63 | 	m.textInput, cmd = m.textInput.Update(msg)
64 | 	return m, cmd
65 | }
66 | 
67 | func (m model) View() string {
68 | 	return fmt.Sprintf(
69 | 		"What’s your favorite Pokémon?\n\n%s\n\n%s",
70 | 		m.textInput.View(),
71 | 		"(esc to quit)",
72 | 	) + "\n"
73 | }
74 | 


--------------------------------------------------------------------------------
/examples/textinput/textinput.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/textinput/textinput.gif


--------------------------------------------------------------------------------
/examples/textinputs/README.md:
--------------------------------------------------------------------------------
1 | # Text Inputs
2 | 
3 | <img width="800" src="./textinputs.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/textinputs/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | // A simple example demonstrating the use of multiple text input components
  4 | // from the Bubbles component library.
  5 | 
  6 | import (
  7 | 	"fmt"
  8 | 	"os"
  9 | 	"strings"
 10 | 
 11 | 	"github.com/charmbracelet/bubbles/cursor"
 12 | 	"github.com/charmbracelet/bubbles/textinput"
 13 | 	tea "github.com/charmbracelet/bubbletea"
 14 | 	"github.com/charmbracelet/lipgloss"
 15 | )
 16 | 
 17 | var (
 18 | 	focusedStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
 19 | 	blurredStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
 20 | 	cursorStyle         = focusedStyle
 21 | 	noStyle             = lipgloss.NewStyle()
 22 | 	helpStyle           = blurredStyle
 23 | 	cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
 24 | 
 25 | 	focusedButton = focusedStyle.Render("[ Submit ]")
 26 | 	blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit"))
 27 | )
 28 | 
 29 | type model struct {
 30 | 	focusIndex int
 31 | 	inputs     []textinput.Model
 32 | 	cursorMode cursor.Mode
 33 | }
 34 | 
 35 | func initialModel() model {
 36 | 	m := model{
 37 | 		inputs: make([]textinput.Model, 3),
 38 | 	}
 39 | 
 40 | 	var t textinput.Model
 41 | 	for i := range m.inputs {
 42 | 		t = textinput.New()
 43 | 		t.Cursor.Style = cursorStyle
 44 | 		t.CharLimit = 32
 45 | 
 46 | 		switch i {
 47 | 		case 0:
 48 | 			t.Placeholder = "Nickname"
 49 | 			t.Focus()
 50 | 			t.PromptStyle = focusedStyle
 51 | 			t.TextStyle = focusedStyle
 52 | 		case 1:
 53 | 			t.Placeholder = "Email"
 54 | 			t.CharLimit = 64
 55 | 		case 2:
 56 | 			t.Placeholder = "Password"
 57 | 			t.EchoMode = textinput.EchoPassword
 58 | 			t.EchoCharacter = '•'
 59 | 		}
 60 | 
 61 | 		m.inputs[i] = t
 62 | 	}
 63 | 
 64 | 	return m
 65 | }
 66 | 
 67 | func (m model) Init() tea.Cmd {
 68 | 	return textinput.Blink
 69 | }
 70 | 
 71 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 72 | 	switch msg := msg.(type) {
 73 | 	case tea.KeyMsg:
 74 | 		switch msg.String() {
 75 | 		case "ctrl+c", "esc":
 76 | 			return m, tea.Quit
 77 | 
 78 | 		// Change cursor mode
 79 | 		case "ctrl+r":
 80 | 			m.cursorMode++
 81 | 			if m.cursorMode > cursor.CursorHide {
 82 | 				m.cursorMode = cursor.CursorBlink
 83 | 			}
 84 | 			cmds := make([]tea.Cmd, len(m.inputs))
 85 | 			for i := range m.inputs {
 86 | 				cmds[i] = m.inputs[i].Cursor.SetMode(m.cursorMode)
 87 | 			}
 88 | 			return m, tea.Batch(cmds...)
 89 | 
 90 | 		// Set focus to next input
 91 | 		case "tab", "shift+tab", "enter", "up", "down":
 92 | 			s := msg.String()
 93 | 
 94 | 			// Did the user press enter while the submit button was focused?
 95 | 			// If so, exit.
 96 | 			if s == "enter" && m.focusIndex == len(m.inputs) {
 97 | 				return m, tea.Quit
 98 | 			}
 99 | 
100 | 			// Cycle indexes
101 | 			if s == "up" || s == "shift+tab" {
102 | 				m.focusIndex--
103 | 			} else {
104 | 				m.focusIndex++
105 | 			}
106 | 
107 | 			if m.focusIndex > len(m.inputs) {
108 | 				m.focusIndex = 0
109 | 			} else if m.focusIndex < 0 {
110 | 				m.focusIndex = len(m.inputs)
111 | 			}
112 | 
113 | 			cmds := make([]tea.Cmd, len(m.inputs))
114 | 			for i := 0; i <= len(m.inputs)-1; i++ {
115 | 				if i == m.focusIndex {
116 | 					// Set focused state
117 | 					cmds[i] = m.inputs[i].Focus()
118 | 					m.inputs[i].PromptStyle = focusedStyle
119 | 					m.inputs[i].TextStyle = focusedStyle
120 | 					continue
121 | 				}
122 | 				// Remove focused state
123 | 				m.inputs[i].Blur()
124 | 				m.inputs[i].PromptStyle = noStyle
125 | 				m.inputs[i].TextStyle = noStyle
126 | 			}
127 | 
128 | 			return m, tea.Batch(cmds...)
129 | 		}
130 | 	}
131 | 
132 | 	// Handle character input and blinking
133 | 	cmd := m.updateInputs(msg)
134 | 
135 | 	return m, cmd
136 | }
137 | 
138 | func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
139 | 	cmds := make([]tea.Cmd, len(m.inputs))
140 | 
141 | 	// Only text inputs with Focus() set will respond, so it's safe to simply
142 | 	// update all of them here without any further logic.
143 | 	for i := range m.inputs {
144 | 		m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
145 | 	}
146 | 
147 | 	return tea.Batch(cmds...)
148 | }
149 | 
150 | func (m model) View() string {
151 | 	var b strings.Builder
152 | 
153 | 	for i := range m.inputs {
154 | 		b.WriteString(m.inputs[i].View())
155 | 		if i < len(m.inputs)-1 {
156 | 			b.WriteRune('\n')
157 | 		}
158 | 	}
159 | 
160 | 	button := &blurredButton
161 | 	if m.focusIndex == len(m.inputs) {
162 | 		button = &focusedButton
163 | 	}
164 | 	fmt.Fprintf(&b, "\n\n%s\n\n", *button)
165 | 
166 | 	b.WriteString(helpStyle.Render("cursor mode is "))
167 | 	b.WriteString(cursorModeHelpStyle.Render(m.cursorMode.String()))
168 | 	b.WriteString(helpStyle.Render(" (ctrl+r to change style)"))
169 | 
170 | 	return b.String()
171 | }
172 | 
173 | func main() {
174 | 	if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
175 | 		fmt.Printf("could not start program: %s\n", err)
176 | 		os.Exit(1)
177 | 	}
178 | }
179 | 


--------------------------------------------------------------------------------
/examples/textinputs/textinputs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/textinputs/textinputs.gif


--------------------------------------------------------------------------------
/examples/timer/README.md:
--------------------------------------------------------------------------------
1 | # Timer
2 | 
3 | <img width="800" src="./timer.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/timer/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 	"time"
  7 | 
  8 | 	"github.com/charmbracelet/bubbles/help"
  9 | 	"github.com/charmbracelet/bubbles/key"
 10 | 	"github.com/charmbracelet/bubbles/timer"
 11 | 	tea "github.com/charmbracelet/bubbletea"
 12 | )
 13 | 
 14 | const timeout = time.Second * 5
 15 | 
 16 | type model struct {
 17 | 	timer    timer.Model
 18 | 	keymap   keymap
 19 | 	help     help.Model
 20 | 	quitting bool
 21 | }
 22 | 
 23 | type keymap struct {
 24 | 	start key.Binding
 25 | 	stop  key.Binding
 26 | 	reset key.Binding
 27 | 	quit  key.Binding
 28 | }
 29 | 
 30 | func (m model) Init() tea.Cmd {
 31 | 	return m.timer.Init()
 32 | }
 33 | 
 34 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 35 | 	switch msg := msg.(type) {
 36 | 	case timer.TickMsg:
 37 | 		var cmd tea.Cmd
 38 | 		m.timer, cmd = m.timer.Update(msg)
 39 | 		return m, cmd
 40 | 
 41 | 	case timer.StartStopMsg:
 42 | 		var cmd tea.Cmd
 43 | 		m.timer, cmd = m.timer.Update(msg)
 44 | 		m.keymap.stop.SetEnabled(m.timer.Running())
 45 | 		m.keymap.start.SetEnabled(!m.timer.Running())
 46 | 		return m, cmd
 47 | 
 48 | 	case timer.TimeoutMsg:
 49 | 		m.quitting = true
 50 | 		return m, tea.Quit
 51 | 
 52 | 	case tea.KeyMsg:
 53 | 		switch {
 54 | 		case key.Matches(msg, m.keymap.quit):
 55 | 			m.quitting = true
 56 | 			return m, tea.Quit
 57 | 		case key.Matches(msg, m.keymap.reset):
 58 | 			m.timer.Timeout = timeout
 59 | 		case key.Matches(msg, m.keymap.start, m.keymap.stop):
 60 | 			return m, m.timer.Toggle()
 61 | 		}
 62 | 	}
 63 | 
 64 | 	return m, nil
 65 | }
 66 | 
 67 | func (m model) helpView() string {
 68 | 	return "\n" + m.help.ShortHelpView([]key.Binding{
 69 | 		m.keymap.start,
 70 | 		m.keymap.stop,
 71 | 		m.keymap.reset,
 72 | 		m.keymap.quit,
 73 | 	})
 74 | }
 75 | 
 76 | func (m model) View() string {
 77 | 	// For a more detailed timer view you could read m.timer.Timeout to get
 78 | 	// the remaining time as a time.Duration and skip calling m.timer.View()
 79 | 	// entirely.
 80 | 	s := m.timer.View()
 81 | 
 82 | 	if m.timer.Timedout() {
 83 | 		s = "All done!"
 84 | 	}
 85 | 	s += "\n"
 86 | 	if !m.quitting {
 87 | 		s = "Exiting in " + s
 88 | 		s += m.helpView()
 89 | 	}
 90 | 	return s
 91 | }
 92 | 
 93 | func main() {
 94 | 	m := model{
 95 | 		timer: timer.NewWithInterval(timeout, time.Millisecond),
 96 | 		keymap: keymap{
 97 | 			start: key.NewBinding(
 98 | 				key.WithKeys("s"),
 99 | 				key.WithHelp("s", "start"),
100 | 			),
101 | 			stop: key.NewBinding(
102 | 				key.WithKeys("s"),
103 | 				key.WithHelp("s", "stop"),
104 | 			),
105 | 			reset: key.NewBinding(
106 | 				key.WithKeys("r"),
107 | 				key.WithHelp("r", "reset"),
108 | 			),
109 | 			quit: key.NewBinding(
110 | 				key.WithKeys("q", "ctrl+c"),
111 | 				key.WithHelp("q", "quit"),
112 | 			),
113 | 		},
114 | 		help: help.New(),
115 | 	}
116 | 	m.keymap.start.SetEnabled(false)
117 | 
118 | 	if _, err := tea.NewProgram(m).Run(); err != nil {
119 | 		fmt.Println("Uh oh, we encountered an error:", err)
120 | 		os.Exit(1)
121 | 	}
122 | }
123 | 


--------------------------------------------------------------------------------
/examples/timer/timer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/timer/timer.gif


--------------------------------------------------------------------------------
/examples/tui-daemon-combo/README.md:
--------------------------------------------------------------------------------
1 | # TUI Daemon
2 | 
3 | <img width="800" src="./tui-daemon-combo.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/tui-daemon-combo/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"flag"
  5 | 	"fmt"
  6 | 	"io"
  7 | 	"log"
  8 | 	"math/rand"
  9 | 	"os"
 10 | 	"time"
 11 | 
 12 | 	"github.com/charmbracelet/bubbles/spinner"
 13 | 	tea "github.com/charmbracelet/bubbletea"
 14 | 	"github.com/charmbracelet/lipgloss"
 15 | 	"github.com/mattn/go-isatty"
 16 | )
 17 | 
 18 | var (
 19 | 	helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render
 20 | 	mainStyle = lipgloss.NewStyle().MarginLeft(1)
 21 | )
 22 | 
 23 | func main() {
 24 | 	var (
 25 | 		daemonMode bool
 26 | 		showHelp   bool
 27 | 		opts       []tea.ProgramOption
 28 | 	)
 29 | 
 30 | 	flag.BoolVar(&daemonMode, "d", false, "run as a daemon")
 31 | 	flag.BoolVar(&showHelp, "h", false, "show help")
 32 | 	flag.Parse()
 33 | 
 34 | 	if showHelp {
 35 | 		flag.Usage()
 36 | 		os.Exit(0)
 37 | 	}
 38 | 
 39 | 	if daemonMode || !isatty.IsTerminal(os.Stdout.Fd()) {
 40 | 		// If we're in daemon mode don't render the TUI
 41 | 		opts = []tea.ProgramOption{tea.WithoutRenderer()}
 42 | 	} else {
 43 | 		// If we're in TUI mode, discard log output
 44 | 		log.SetOutput(io.Discard)
 45 | 	}
 46 | 
 47 | 	p := tea.NewProgram(newModel(), opts...)
 48 | 	if _, err := p.Run(); err != nil {
 49 | 		fmt.Println("Error starting Bubble Tea program:", err)
 50 | 		os.Exit(1)
 51 | 	}
 52 | }
 53 | 
 54 | type result struct {
 55 | 	duration time.Duration
 56 | 	emoji    string
 57 | }
 58 | 
 59 | type model struct {
 60 | 	spinner  spinner.Model
 61 | 	results  []result
 62 | 	quitting bool
 63 | }
 64 | 
 65 | func newModel() model {
 66 | 	const showLastResults = 5
 67 | 
 68 | 	sp := spinner.New()
 69 | 	sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("206"))
 70 | 
 71 | 	return model{
 72 | 		spinner: sp,
 73 | 		results: make([]result, showLastResults),
 74 | 	}
 75 | }
 76 | 
 77 | func (m model) Init() tea.Cmd {
 78 | 	log.Println("Starting work...")
 79 | 	return tea.Batch(
 80 | 		m.spinner.Tick,
 81 | 		runPretendProcess,
 82 | 	)
 83 | }
 84 | 
 85 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 86 | 	switch msg := msg.(type) {
 87 | 	case tea.KeyMsg:
 88 | 		m.quitting = true
 89 | 		return m, tea.Quit
 90 | 	case spinner.TickMsg:
 91 | 		var cmd tea.Cmd
 92 | 		m.spinner, cmd = m.spinner.Update(msg)
 93 | 		return m, cmd
 94 | 	case processFinishedMsg:
 95 | 		d := time.Duration(msg)
 96 | 		res := result{emoji: randomEmoji(), duration: d}
 97 | 		log.Printf("%s Job finished in %s", res.emoji, res.duration)
 98 | 		m.results = append(m.results[1:], res)
 99 | 		return m, runPretendProcess
100 | 	default:
101 | 		return m, nil
102 | 	}
103 | }
104 | 
105 | func (m model) View() string {
106 | 	s := "\n" +
107 | 		m.spinner.View() + " Doing some work...\n\n"
108 | 
109 | 	for _, res := range m.results {
110 | 		if res.duration == 0 {
111 | 			s += "........................\n"
112 | 		} else {
113 | 			s += fmt.Sprintf("%s Job finished in %s\n", res.emoji, res.duration)
114 | 		}
115 | 	}
116 | 
117 | 	s += helpStyle("\nPress any key to exit\n")
118 | 
119 | 	if m.quitting {
120 | 		s += "\n"
121 | 	}
122 | 
123 | 	return mainStyle.Render(s)
124 | }
125 | 
126 | // processFinishedMsg is sent when a pretend process completes.
127 | type processFinishedMsg time.Duration
128 | 
129 | // pretendProcess simulates a long-running process.
130 | func runPretendProcess() tea.Msg {
131 | 	pause := time.Duration(rand.Int63n(899)+100) * time.Millisecond // nolint:gosec
132 | 	time.Sleep(pause)
133 | 	return processFinishedMsg(pause)
134 | }
135 | 
136 | func randomEmoji() string {
137 | 	emojis := []rune("🍦🧋🍡🤠👾😭🦊🐯🦆🥨🎏🍔🍒🍥🎮📦🦁🐶🐸🍕🥐🧲🚒🥇🏆🌽")
138 | 	return string(emojis[rand.Intn(len(emojis))]) // nolint:gosec
139 | }
140 | 


--------------------------------------------------------------------------------
/examples/tui-daemon-combo/tui-daemon-combo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/tui-daemon-combo/tui-daemon-combo.gif


--------------------------------------------------------------------------------
/examples/views/README.md:
--------------------------------------------------------------------------------
1 | # Views
2 | 
3 | <img width="800" src="./views.gif" />
4 | 


--------------------------------------------------------------------------------
/examples/views/views.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charmbracelet/bubbletea/a8c4763874571ea5e0db22608e2c6ae3e44b2ae2/examples/views/views.gif


--------------------------------------------------------------------------------
/examples/window-size/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | // A simple program that queries and displays the window-size.
 4 | 
 5 | import (
 6 | 	"log"
 7 | 
 8 | 	tea "github.com/charmbracelet/bubbletea"
 9 | )
10 | 
11 | func main() {
12 | 	p := tea.NewProgram(model{})
13 | 	if _, err := p.Run(); err != nil {
14 | 		log.Fatal(err)
15 | 	}
16 | }
17 | 
18 | type model struct{}
19 | 
20 | func (m model) Init() tea.Cmd {
21 | 	return nil
22 | }
23 | 
24 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
25 | 	switch msg := msg.(type) {
26 | 	case tea.KeyMsg:
27 | 		if s := msg.String(); s == "ctrl+c" || s == "q" || s == "esc" {
28 | 			return m, tea.Quit
29 | 		}
30 | 
31 | 		return m, tea.WindowSize()
32 | 
33 | 	case tea.WindowSizeMsg:
34 | 		return m, tea.Printf("%dx%d", msg.Width, msg.Height)
35 | 	}
36 | 
37 | 	return m, nil
38 | }
39 | 
40 | func (m model) View() string {
41 | 	s := "When you're done press q to quit. Press any other key to query the window-size.\n"
42 | 
43 | 	return s
44 | }
45 | 


--------------------------------------------------------------------------------
/exec.go:
--------------------------------------------------------------------------------
  1 | package tea
  2 | 
  3 | import (
  4 | 	"io"
  5 | 	"os"
  6 | 	"os/exec"
  7 | )
  8 | 
  9 | // execMsg is used internally to run an ExecCommand sent with Exec.
 10 | type execMsg struct {
 11 | 	cmd ExecCommand
 12 | 	fn  ExecCallback
 13 | }
 14 | 
 15 | // Exec is used to perform arbitrary I/O in a blocking fashion, effectively
 16 | // pausing the Program while execution is running and resuming it when
 17 | // execution has completed.
 18 | //
 19 | // Most of the time you'll want to use ExecProcess, which runs an exec.Cmd.
 20 | //
 21 | // For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
 22 | func Exec(c ExecCommand, fn ExecCallback) Cmd {
 23 | 	return func() Msg {
 24 | 		return execMsg{cmd: c, fn: fn}
 25 | 	}
 26 | }
 27 | 
 28 | // ExecProcess runs the given *exec.Cmd in a blocking fashion, effectively
 29 | // pausing the Program while the command is running. After the *exec.Cmd exists
 30 | // the Program resumes. It's useful for spawning other interactive applications
 31 | // such as editors and shells from within a Program.
 32 | //
 33 | // To produce the command, pass an *exec.Cmd and a function which returns
 34 | // a message containing the error which may have occurred when running the
 35 | // ExecCommand.
 36 | //
 37 | //	type VimFinishedMsg struct { err error }
 38 | //
 39 | //	c := exec.Command("vim", "file.txt")
 40 | //
 41 | //	cmd := ExecProcess(c, func(err error) Msg {
 42 | //	    return VimFinishedMsg{err: err}
 43 | //	})
 44 | //
 45 | // Or, if you don't care about errors, you could simply:
 46 | //
 47 | //	cmd := ExecProcess(exec.Command("vim", "file.txt"), nil)
 48 | //
 49 | // For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
 50 | func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd {
 51 | 	return Exec(wrapExecCommand(c), fn)
 52 | }
 53 | 
 54 | // ExecCallback is used when executing an *exec.Command to return a message
 55 | // with an error, which may or may not be nil.
 56 | type ExecCallback func(error) Msg
 57 | 
 58 | // ExecCommand can be implemented to execute things in a blocking fashion in
 59 | // the current terminal.
 60 | type ExecCommand interface {
 61 | 	Run() error
 62 | 	SetStdin(io.Reader)
 63 | 	SetStdout(io.Writer)
 64 | 	SetStderr(io.Writer)
 65 | }
 66 | 
 67 | // wrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand
 68 | // interface so it can be used with Exec.
 69 | func wrapExecCommand(c *exec.Cmd) ExecCommand {
 70 | 	return &osExecCommand{Cmd: c}
 71 | }
 72 | 
 73 | // osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand
 74 | // interface.
 75 | type osExecCommand struct{ *exec.Cmd }
 76 | 
 77 | // SetStdin sets stdin on underlying exec.Cmd to the given io.Reader.
 78 | func (c *osExecCommand) SetStdin(r io.Reader) {
 79 | 	// If unset, have the command use the same input as the terminal.
 80 | 	if c.Stdin == nil {
 81 | 		c.Stdin = r
 82 | 	}
 83 | }
 84 | 
 85 | // SetStdout sets stdout on underlying exec.Cmd to the given io.Writer.
 86 | func (c *osExecCommand) SetStdout(w io.Writer) {
 87 | 	// If unset, have the command use the same output as the terminal.
 88 | 	if c.Stdout == nil {
 89 | 		c.Stdout = w
 90 | 	}
 91 | }
 92 | 
 93 | // SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer.
 94 | func (c *osExecCommand) SetStderr(w io.Writer) {
 95 | 	// If unset, use stderr for the command's stderr
 96 | 	if c.Stderr == nil {
 97 | 		c.Stderr = w
 98 | 	}
 99 | }
100 | 
101 | // exec runs an ExecCommand and delivers the results to the program as a Msg.
102 | func (p *Program) exec(c ExecCommand, fn ExecCallback) {
103 | 	if err := p.ReleaseTerminal(); err != nil {
104 | 		// If we can't release input, abort.
105 | 		if fn != nil {
106 | 			go p.Send(fn(err))
107 | 		}
108 | 		return
109 | 	}
110 | 
111 | 	c.SetStdin(p.input)
112 | 	c.SetStdout(p.output)
113 | 	c.SetStderr(os.Stderr)
114 | 
115 | 	// Execute system command.
116 | 	if err := c.Run(); err != nil {
117 | 		p.renderer.resetLinesRendered()
118 | 		_ = p.RestoreTerminal() // also try to restore the terminal.
119 | 		if fn != nil {
120 | 			go p.Send(fn(err))
121 | 		}
122 | 		return
123 | 	}
124 | 
125 | 	// Maintain the existing output from the command
126 | 	p.renderer.resetLinesRendered()
127 | 
128 | 	// Have the program re-capture input.
129 | 	err := p.RestoreTerminal()
130 | 	if fn != nil {
131 | 		go p.Send(fn(err))
132 | 	}
133 | }
134 | 


--------------------------------------------------------------------------------
/exec_test.go:
--------------------------------------------------------------------------------
  1 | package tea
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"os/exec"
  6 | 	"runtime"
  7 | 	"testing"
  8 | )
  9 | 
 10 | type execFinishedMsg struct{ err error }
 11 | 
 12 | type testExecModel struct {
 13 | 	cmd string
 14 | 	err error
 15 | }
 16 | 
 17 | func (m testExecModel) Init() Cmd {
 18 | 	c := exec.Command(m.cmd) //nolint:gosec
 19 | 	return ExecProcess(c, func(err error) Msg {
 20 | 		return execFinishedMsg{err}
 21 | 	})
 22 | }
 23 | 
 24 | func (m *testExecModel) Update(msg Msg) (Model, Cmd) {
 25 | 	switch msg := msg.(type) {
 26 | 	case execFinishedMsg:
 27 | 		if msg.err != nil {
 28 | 			m.err = msg.err
 29 | 		}
 30 | 		return m, Quit
 31 | 	}
 32 | 
 33 | 	return m, nil
 34 | }
 35 | 
 36 | func (m *testExecModel) View() string {
 37 | 	return "\n"
 38 | }
 39 | 
 40 | type spyRenderer struct {
 41 | 	renderer
 42 | 	calledReset bool
 43 | }
 44 | 
 45 | func (r *spyRenderer) resetLinesRendered() {
 46 | 	r.calledReset = true
 47 | 	r.renderer.resetLinesRendered()
 48 | }
 49 | 
 50 | func TestTeaExec(t *testing.T) {
 51 | 	type test struct {
 52 | 		name      string
 53 | 		cmd       string
 54 | 		expectErr bool
 55 | 	}
 56 | 	tests := []test{
 57 | 		{
 58 | 			name:      "invalid command",
 59 | 			cmd:       "invalid",
 60 | 			expectErr: true,
 61 | 		},
 62 | 	}
 63 | 
 64 | 	if runtime.GOOS != "windows" {
 65 | 		tests = append(tests, []test{
 66 | 			{
 67 | 				name:      "true",
 68 | 				cmd:       "true",
 69 | 				expectErr: false,
 70 | 			},
 71 | 			{
 72 | 				name:      "false",
 73 | 				cmd:       "false",
 74 | 				expectErr: true,
 75 | 			},
 76 | 		}...)
 77 | 	}
 78 | 
 79 | 	for _, test := range tests {
 80 | 		t.Run(test.name, func(t *testing.T) {
 81 | 			var buf bytes.Buffer
 82 | 			var in bytes.Buffer
 83 | 
 84 | 			m := &testExecModel{cmd: test.cmd}
 85 | 			p := NewProgram(m, WithInput(&in), WithOutput(&buf))
 86 | 			if _, err := p.Run(); err != nil {
 87 | 				t.Error(err)
 88 | 			}
 89 | 			p.renderer = &spyRenderer{renderer: p.renderer}
 90 | 
 91 | 			if m.err != nil && !test.expectErr {
 92 | 				t.Errorf("expected no error, got %v", m.err)
 93 | 
 94 | 				if !p.renderer.(*spyRenderer).calledReset {
 95 | 					t.Error("expected renderer to be reset")
 96 | 				}
 97 | 			}
 98 | 			if m.err == nil && test.expectErr {
 99 | 				t.Error("expected error, got nil")
100 | 			}
101 | 		})
102 | 	}
103 | }
104 | 


--------------------------------------------------------------------------------
/focus.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | // FocusMsg represents a terminal focus message.
 4 | // This occurs when the terminal gains focus.
 5 | type FocusMsg struct{}
 6 | 
 7 | // BlurMsg represents a terminal blur message.
 8 | // This occurs when the terminal loses focus.
 9 | type BlurMsg struct{}
10 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/charmbracelet/bubbletea
 2 | 
 3 | go 1.23.0
 4 | 
 5 | toolchain go1.23.7
 6 | 
 7 | require (
 8 | 	github.com/charmbracelet/lipgloss v1.1.0
 9 | 	github.com/charmbracelet/x/ansi v0.9.3
10 | 	github.com/charmbracelet/x/term v0.2.1
11 | 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f
12 | 	github.com/mattn/go-localereader v0.0.1
13 | 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
14 | 	github.com/muesli/cancelreader v0.2.2
15 | 	golang.org/x/sync v0.16.0
16 | 	golang.org/x/sys v0.34.0
17 | )
18 | 
19 | require (
20 | 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
21 | 	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
22 | 	github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
23 | 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
24 | 	github.com/mattn/go-isatty v0.0.20 // indirect
25 | 	github.com/mattn/go-runewidth v0.0.16 // indirect
26 | 	github.com/muesli/termenv v0.16.0 // indirect
27 | 	github.com/rivo/uniseg v0.4.7 // indirect
28 | 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
29 | 	golang.org/x/text v0.3.8 // indirect
30 | )
31 | 


--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
 1 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 3 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
 4 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
 5 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
 6 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
 7 | github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
 8 | github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
 9 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
10 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
11 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
12 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
13 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
14 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
15 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
16 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
17 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
18 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
19 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
20 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
21 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
22 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
23 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
24 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
25 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
26 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
27 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
28 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
29 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
30 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
31 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
32 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
33 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
34 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
35 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
36 | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
37 | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
38 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
41 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
42 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
43 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
44 | 


--------------------------------------------------------------------------------
/inputreader_other.go:
--------------------------------------------------------------------------------
 1 | //go:build !windows
 2 | // +build !windows
 3 | 
 4 | package tea
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"io"
 9 | 
10 | 	"github.com/muesli/cancelreader"
11 | )
12 | 
13 | func newInputReader(r io.Reader, _ bool) (cancelreader.CancelReader, error) {
14 | 	cr, err := cancelreader.NewReader(r)
15 | 	if err != nil {
16 | 		return nil, fmt.Errorf("bubbletea: error creating cancel reader: %w", err)
17 | 	}
18 | 	return cr, nil
19 | }
20 | 


--------------------------------------------------------------------------------
/inputreader_windows.go:
--------------------------------------------------------------------------------
  1 | //go:build windows
  2 | // +build windows
  3 | 
  4 | package tea
  5 | 
  6 | import (
  7 | 	"fmt"
  8 | 	"io"
  9 | 	"os"
 10 | 	"sync"
 11 | 
 12 | 	"github.com/charmbracelet/x/term"
 13 | 	"github.com/erikgeiser/coninput"
 14 | 	"github.com/muesli/cancelreader"
 15 | 	"golang.org/x/sys/windows"
 16 | )
 17 | 
 18 | type conInputReader struct {
 19 | 	cancelMixin
 20 | 
 21 | 	conin windows.Handle
 22 | 
 23 | 	originalMode uint32
 24 | }
 25 | 
 26 | var _ cancelreader.CancelReader = &conInputReader{}
 27 | 
 28 | func newInputReader(r io.Reader, enableMouse bool) (cancelreader.CancelReader, error) {
 29 | 	fallback := func(io.Reader) (cancelreader.CancelReader, error) {
 30 | 		return cancelreader.NewReader(r)
 31 | 	}
 32 | 	if f, ok := r.(term.File); !ok || f.Fd() != os.Stdin.Fd() {
 33 | 		return fallback(r)
 34 | 	}
 35 | 
 36 | 	conin, err := coninput.NewStdinHandle()
 37 | 	if err != nil {
 38 | 		return fallback(r)
 39 | 	}
 40 | 
 41 | 	modes := []uint32{
 42 | 		windows.ENABLE_WINDOW_INPUT,
 43 | 		windows.ENABLE_EXTENDED_FLAGS,
 44 | 	}
 45 | 
 46 | 	// Since we have options to enable mouse events, [WithMouseCellMotion],
 47 | 	// [WithMouseAllMotion], and [EnableMouseCellMotion],
 48 | 	// [EnableMouseAllMotion], and [DisableMouse], we need to check if the user
 49 | 	// has enabled mouse events and add the appropriate mode accordingly.
 50 | 	// Otherwise, mouse events will be enabled all the time.
 51 | 	if enableMouse {
 52 | 		modes = append(modes, windows.ENABLE_MOUSE_INPUT)
 53 | 	}
 54 | 
 55 | 	originalMode, err := prepareConsole(conin, modes...)
 56 | 	if err != nil {
 57 | 		return nil, fmt.Errorf("failed to prepare console input: %w", err)
 58 | 	}
 59 | 
 60 | 	return &conInputReader{
 61 | 		conin:        conin,
 62 | 		originalMode: originalMode,
 63 | 	}, nil
 64 | }
 65 | 
 66 | // Cancel implements cancelreader.CancelReader.
 67 | func (r *conInputReader) Cancel() bool {
 68 | 	r.setCanceled()
 69 | 
 70 | 	// Warning: These cancel methods do not reliably work on console input
 71 | 	// 			and should not be counted on.
 72 | 	return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
 73 | }
 74 | 
 75 | // Close implements cancelreader.CancelReader.
 76 | func (r *conInputReader) Close() error {
 77 | 	if r.originalMode != 0 {
 78 | 		err := windows.SetConsoleMode(r.conin, r.originalMode)
 79 | 		if err != nil {
 80 | 			return fmt.Errorf("reset console mode: %w", err)
 81 | 		}
 82 | 	}
 83 | 
 84 | 	return nil
 85 | }
 86 | 
 87 | // Read implements cancelreader.CancelReader.
 88 | func (r *conInputReader) Read(_ []byte) (n int, err error) {
 89 | 	if r.isCanceled() {
 90 | 		err = cancelreader.ErrCanceled
 91 | 	}
 92 | 	return
 93 | }
 94 | 
 95 | func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
 96 | 	err = windows.GetConsoleMode(input, &originalMode)
 97 | 	if err != nil {
 98 | 		return 0, fmt.Errorf("get console mode: %w", err)
 99 | 	}
100 | 
101 | 	newMode := coninput.AddInputModes(0, modes...)
102 | 
103 | 	err = windows.SetConsoleMode(input, newMode)
104 | 	if err != nil {
105 | 		return 0, fmt.Errorf("set console mode: %w", err)
106 | 	}
107 | 
108 | 	return originalMode, nil
109 | }
110 | 
111 | // cancelMixin represents a goroutine-safe cancelation status.
112 | type cancelMixin struct {
113 | 	unsafeCanceled bool
114 | 	lock           sync.Mutex
115 | }
116 | 
117 | func (c *cancelMixin) setCanceled() {
118 | 	c.lock.Lock()
119 | 	defer c.lock.Unlock()
120 | 
121 | 	c.unsafeCanceled = true
122 | }
123 | 
124 | func (c *cancelMixin) isCanceled() bool {
125 | 	c.lock.Lock()
126 | 	defer c.lock.Unlock()
127 | 
128 | 	return c.unsafeCanceled
129 | }
130 | 


--------------------------------------------------------------------------------
/key_other.go:
--------------------------------------------------------------------------------
 1 | //go:build !windows
 2 | // +build !windows
 3 | 
 4 | package tea
 5 | 
 6 | import (
 7 | 	"context"
 8 | 	"io"
 9 | )
10 | 
11 | func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
12 | 	return readAnsiInputs(ctx, msgs, input)
13 | }
14 | 


--------------------------------------------------------------------------------
/key_sequences.go:
--------------------------------------------------------------------------------
  1 | package tea
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"sort"
  6 | 	"unicode/utf8"
  7 | )
  8 | 
  9 | // extSequences is used by the map-based algorithm below. It contains
 10 | // the sequences plus their alternatives with an escape character
 11 | // prefixed, plus the control chars, plus the space.
 12 | // It does not contain the NUL character, which is handled specially
 13 | // by detectOneMsg.
 14 | var extSequences = func() map[string]Key {
 15 | 	s := map[string]Key{}
 16 | 	for seq, key := range sequences {
 17 | 		key := key
 18 | 		s[seq] = key
 19 | 		if !key.Alt {
 20 | 			key.Alt = true
 21 | 			s["\x1b"+seq] = key
 22 | 		}
 23 | 	}
 24 | 	for i := keyNUL + 1; i <= keyDEL; i++ {
 25 | 		if i == keyESC {
 26 | 			continue
 27 | 		}
 28 | 		s[string([]byte{byte(i)})] = Key{Type: i}
 29 | 		s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true}
 30 | 		if i == keyUS {
 31 | 			i = keyDEL - 1
 32 | 		}
 33 | 	}
 34 | 	s[" "] = Key{Type: KeySpace, Runes: spaceRunes}
 35 | 	s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes}
 36 | 	s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true}
 37 | 	return s
 38 | }()
 39 | 
 40 | // seqLengths is the sizes of valid sequences, starting with the
 41 | // largest size.
 42 | var seqLengths = func() []int {
 43 | 	sizes := map[int]struct{}{}
 44 | 	for seq := range extSequences {
 45 | 		sizes[len(seq)] = struct{}{}
 46 | 	}
 47 | 	lsizes := make([]int, 0, len(sizes))
 48 | 	for sz := range sizes {
 49 | 		lsizes = append(lsizes, sz)
 50 | 	}
 51 | 	sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] })
 52 | 	return lsizes
 53 | }()
 54 | 
 55 | // detectSequence uses a longest prefix match over the input
 56 | // sequence and a hash map.
 57 | func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) {
 58 | 	seqs := extSequences
 59 | 	for _, sz := range seqLengths {
 60 | 		if sz > len(input) {
 61 | 			continue
 62 | 		}
 63 | 		prefix := input[:sz]
 64 | 		key, ok := seqs[string(prefix)]
 65 | 		if ok {
 66 | 			return true, sz, KeyMsg(key)
 67 | 		}
 68 | 	}
 69 | 	// Is this an unknown CSI sequence?
 70 | 	if loc := unknownCSIRe.FindIndex(input); loc != nil {
 71 | 		return true, loc[1], unknownCSISequenceMsg(input[:loc[1]])
 72 | 	}
 73 | 
 74 | 	return false, 0, nil
 75 | }
 76 | 
 77 | // detectBracketedPaste detects an input pasted while bracketed
 78 | // paste mode was enabled.
 79 | //
 80 | // Note: this function is a no-op if bracketed paste was not enabled
 81 | // on the terminal, since in that case we'd never see this
 82 | // particular escape sequence.
 83 | func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) {
 84 | 	// Detect the start sequence.
 85 | 	const bpStart = "\x1b[200~"
 86 | 	if len(input) < len(bpStart) || string(input[:len(bpStart)]) != bpStart {
 87 | 		return false, 0, nil
 88 | 	}
 89 | 
 90 | 	// Skip over the start sequence.
 91 | 	input = input[len(bpStart):]
 92 | 
 93 | 	// If we saw the start sequence, then we must have an end sequence
 94 | 	// as well. Find it.
 95 | 	const bpEnd = "\x1b[201~"
 96 | 	idx := bytes.Index(input, []byte(bpEnd))
 97 | 	inputLen := len(bpStart) + idx + len(bpEnd)
 98 | 	if idx == -1 {
 99 | 		// We have encountered the end of the input buffer without seeing
100 | 		// the marker for the end of the bracketed paste.
101 | 		// Tell the outer loop we have done a short read and we want more.
102 | 		return true, 0, nil
103 | 	}
104 | 
105 | 	// The paste is everything in-between.
106 | 	paste := input[:idx]
107 | 
108 | 	// All there is in-between is runes, not to be interpreted further.
109 | 	k := Key{Type: KeyRunes, Paste: true}
110 | 	for len(paste) > 0 {
111 | 		r, w := utf8.DecodeRune(paste)
112 | 		if r != utf8.RuneError {
113 | 			k.Runes = append(k.Runes, r)
114 | 		}
115 | 		paste = paste[w:]
116 | 	}
117 | 
118 | 	return true, inputLen, KeyMsg(k)
119 | }
120 | 
121 | // detectReportFocus detects a focus report sequence.
122 | func detectReportFocus(input []byte) (hasRF bool, width int, msg Msg) {
123 | 	switch {
124 | 	case bytes.Equal(input, []byte("\x1b[I")):
125 | 		return true, 3, FocusMsg{} //nolint:mnd
126 | 	case bytes.Equal(input, []byte("\x1b[O")):
127 | 		return true, 3, BlurMsg{} //nolint:mnd
128 | 	}
129 | 	return false, 0, nil
130 | }
131 | 


--------------------------------------------------------------------------------
/logging.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"io"
 6 | 	"log"
 7 | 	"os"
 8 | 	"unicode"
 9 | )
10 | 
11 | // LogToFile sets up default logging to log to a file. This is helpful as we
12 | // can't print to the terminal since our TUI is occupying it. If the file
13 | // doesn't exist it will be created.
14 | //
15 | // Don't forget to close the file when you're done with it.
16 | //
17 | //	  f, err := LogToFile("debug.log", "debug")
18 | //	  if err != nil {
19 | //			fmt.Println("fatal:", err)
20 | //			os.Exit(1)
21 | //	  }
22 | //	  defer f.Close()
23 | func LogToFile(path string, prefix string) (*os.File, error) {
24 | 	return LogToFileWith(path, prefix, log.Default())
25 | }
26 | 
27 | // LogOptionsSetter is an interface implemented by stdlib's log and charm's log
28 | // libraries.
29 | type LogOptionsSetter interface {
30 | 	SetOutput(io.Writer)
31 | 	SetPrefix(string)
32 | }
33 | 
34 | // LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter.
35 | func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) {
36 | 	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:mnd
37 | 	if err != nil {
38 | 		return nil, fmt.Errorf("error opening file for logging: %w", err)
39 | 	}
40 | 	log.SetOutput(f)
41 | 
42 | 	// Add a space after the prefix if a prefix is being specified and it
43 | 	// doesn't already have a trailing space.
44 | 	if len(prefix) > 0 {
45 | 		finalChar := prefix[len(prefix)-1]
46 | 		if !unicode.IsSpace(rune(finalChar)) {
47 | 			prefix += " "
48 | 		}
49 | 	}
50 | 	log.SetPrefix(prefix)
51 | 
52 | 	return f, nil
53 | }
54 | 


--------------------------------------------------------------------------------
/logging_test.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | import (
 4 | 	"log"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | 	"testing"
 8 | )
 9 | 
10 | func TestLogToFile(t *testing.T) {
11 | 	path := filepath.Join(t.TempDir(), "log.txt")
12 | 	prefix := "logprefix"
13 | 	f, err := LogToFile(path, prefix)
14 | 	if err != nil {
15 | 		t.Error(err)
16 | 	}
17 | 	log.SetFlags(log.Lmsgprefix)
18 | 	log.Println("some test log")
19 | 	if err := f.Close(); err != nil {
20 | 		t.Error(err)
21 | 	}
22 | 	out, err := os.ReadFile(path)
23 | 	if err != nil {
24 | 		t.Error(err)
25 | 	}
26 | 	if string(out) != prefix+" some test log\n" {
27 | 		t.Fatalf("wrong log msg: %q", string(out))
28 | 	}
29 | }
30 | 


--------------------------------------------------------------------------------
/nil_renderer.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | type nilRenderer struct{}
 4 | 
 5 | func (n nilRenderer) start()                     {}
 6 | func (n nilRenderer) stop()                      {}
 7 | func (n nilRenderer) kill()                      {}
 8 | func (n nilRenderer) write(_ string)             {}
 9 | func (n nilRenderer) repaint()                   {}
10 | func (n nilRenderer) clearScreen()               {}
11 | func (n nilRenderer) altScreen() bool            { return false }
12 | func (n nilRenderer) enterAltScreen()            {}
13 | func (n nilRenderer) exitAltScreen()             {}
14 | func (n nilRenderer) showCursor()                {}
15 | func (n nilRenderer) hideCursor()                {}
16 | func (n nilRenderer) enableMouseCellMotion()     {}
17 | func (n nilRenderer) disableMouseCellMotion()    {}
18 | func (n nilRenderer) enableMouseAllMotion()      {}
19 | func (n nilRenderer) disableMouseAllMotion()     {}
20 | func (n nilRenderer) enableBracketedPaste()      {}
21 | func (n nilRenderer) disableBracketedPaste()     {}
22 | func (n nilRenderer) enableMouseSGRMode()        {}
23 | func (n nilRenderer) disableMouseSGRMode()       {}
24 | func (n nilRenderer) bracketedPasteActive() bool { return false }
25 | func (n nilRenderer) setWindowTitle(_ string)    {}
26 | func (n nilRenderer) reportFocus() bool          { return false }
27 | func (n nilRenderer) enableReportFocus()         {}
28 | func (n nilRenderer) disableReportFocus()        {}
29 | func (n nilRenderer) resetLinesRendered()        {}
30 | 


--------------------------------------------------------------------------------
/nil_renderer_test.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | import "testing"
 4 | 
 5 | func TestNilRenderer(t *testing.T) {
 6 | 	r := nilRenderer{}
 7 | 	r.start()
 8 | 	r.stop()
 9 | 	r.kill()
10 | 	r.write("a")
11 | 	r.repaint()
12 | 	r.enterAltScreen()
13 | 	if r.altScreen() {
14 | 		t.Errorf("altScreen should always return false")
15 | 	}
16 | 	r.exitAltScreen()
17 | 	r.clearScreen()
18 | 	r.showCursor()
19 | 	r.hideCursor()
20 | 	r.enableMouseCellMotion()
21 | 	r.disableMouseCellMotion()
22 | 	r.enableMouseAllMotion()
23 | 	r.disableMouseAllMotion()
24 | }
25 | 


--------------------------------------------------------------------------------
/options_test.go:
--------------------------------------------------------------------------------
  1 | package tea
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"context"
  6 | 	"os"
  7 | 	"sync/atomic"
  8 | 	"testing"
  9 | )
 10 | 
 11 | func TestOptions(t *testing.T) {
 12 | 	t.Run("output", func(t *testing.T) {
 13 | 		var b bytes.Buffer
 14 | 		p := NewProgram(nil, WithOutput(&b))
 15 | 		if f, ok := p.output.(*os.File); ok {
 16 | 			t.Errorf("expected output to custom, got %v", f.Fd())
 17 | 		}
 18 | 	})
 19 | 
 20 | 	t.Run("custom input", func(t *testing.T) {
 21 | 		var b bytes.Buffer
 22 | 		p := NewProgram(nil, WithInput(&b))
 23 | 		if p.input != &b {
 24 | 			t.Errorf("expected input to custom, got %v", p.input)
 25 | 		}
 26 | 		if p.inputType != customInput {
 27 | 			t.Errorf("expected startup options to have custom input set, got %v", p.input)
 28 | 		}
 29 | 	})
 30 | 
 31 | 	t.Run("renderer", func(t *testing.T) {
 32 | 		p := NewProgram(nil, WithoutRenderer())
 33 | 		switch p.renderer.(type) {
 34 | 		case *nilRenderer:
 35 | 			return
 36 | 		default:
 37 | 			t.Errorf("expected renderer to be a nilRenderer, got %v", p.renderer)
 38 | 		}
 39 | 	})
 40 | 
 41 | 	t.Run("without signals", func(t *testing.T) {
 42 | 		p := NewProgram(nil, WithoutSignals())
 43 | 		if atomic.LoadUint32(&p.ignoreSignals) == 0 {
 44 | 			t.Errorf("ignore signals should have been set")
 45 | 		}
 46 | 	})
 47 | 
 48 | 	t.Run("filter", func(t *testing.T) {
 49 | 		p := NewProgram(nil, WithFilter(func(_ Model, msg Msg) Msg { return msg }))
 50 | 		if p.filter == nil {
 51 | 			t.Errorf("expected filter to be set")
 52 | 		}
 53 | 	})
 54 | 
 55 | 	t.Run("external context", func(t *testing.T) {
 56 | 		extCtx, extCancel := context.WithCancel(context.Background())
 57 | 		defer extCancel()
 58 | 
 59 | 		p := NewProgram(nil, WithContext(extCtx))
 60 | 		if p.externalCtx != extCtx || p.externalCtx == context.Background() {
 61 | 			t.Errorf("expected passed in external context, got default (nil)")
 62 | 		}
 63 | 	})
 64 | 
 65 | 	t.Run("input options", func(t *testing.T) {
 66 | 		exercise := func(t *testing.T, opt ProgramOption, expect inputType) {
 67 | 			p := NewProgram(nil, opt)
 68 | 			if p.inputType != expect {
 69 | 				t.Errorf("expected input type %s, got %s", expect, p.inputType)
 70 | 			}
 71 | 		}
 72 | 
 73 | 		t.Run("tty input", func(t *testing.T) {
 74 | 			exercise(t, WithInputTTY(), ttyInput)
 75 | 		})
 76 | 
 77 | 		t.Run("custom input", func(t *testing.T) {
 78 | 			var b bytes.Buffer
 79 | 			exercise(t, WithInput(&b), customInput)
 80 | 		})
 81 | 	})
 82 | 
 83 | 	t.Run("startup options", func(t *testing.T) {
 84 | 		exercise := func(t *testing.T, opt ProgramOption, expect startupOptions) {
 85 | 			p := NewProgram(nil, opt)
 86 | 			if !p.startupOptions.has(expect) {
 87 | 				t.Errorf("expected startup options have %v, got %v", expect, p.startupOptions)
 88 | 			}
 89 | 		}
 90 | 
 91 | 		t.Run("alt screen", func(t *testing.T) {
 92 | 			exercise(t, WithAltScreen(), withAltScreen)
 93 | 		})
 94 | 
 95 | 		t.Run("bracketed paste disabled", func(t *testing.T) {
 96 | 			exercise(t, WithoutBracketedPaste(), withoutBracketedPaste)
 97 | 		})
 98 | 
 99 | 		t.Run("ansi compression", func(t *testing.T) {
100 | 			exercise(t, WithANSICompressor(), withANSICompressor)
101 | 		})
102 | 
103 | 		t.Run("without catch panics", func(t *testing.T) {
104 | 			exercise(t, WithoutCatchPanics(), withoutCatchPanics)
105 | 		})
106 | 
107 | 		t.Run("without signal handler", func(t *testing.T) {
108 | 			exercise(t, WithoutSignalHandler(), withoutSignalHandler)
109 | 		})
110 | 
111 | 		t.Run("mouse cell motion", func(t *testing.T) {
112 | 			p := NewProgram(nil, WithMouseAllMotion(), WithMouseCellMotion())
113 | 			if !p.startupOptions.has(withMouseCellMotion) {
114 | 				t.Errorf("expected startup options have %v, got %v", withMouseCellMotion, p.startupOptions)
115 | 			}
116 | 			if p.startupOptions.has(withMouseAllMotion) {
117 | 				t.Errorf("expected startup options not have %v, got %v", withMouseAllMotion, p.startupOptions)
118 | 			}
119 | 		})
120 | 
121 | 		t.Run("mouse all motion", func(t *testing.T) {
122 | 			p := NewProgram(nil, WithMouseCellMotion(), WithMouseAllMotion())
123 | 			if !p.startupOptions.has(withMouseAllMotion) {
124 | 				t.Errorf("expected startup options have %v, got %v", withMouseAllMotion, p.startupOptions)
125 | 			}
126 | 			if p.startupOptions.has(withMouseCellMotion) {
127 | 				t.Errorf("expected startup options not have %v, got %v", withMouseCellMotion, p.startupOptions)
128 | 			}
129 | 		})
130 | 	})
131 | 
132 | 	t.Run("multiple", func(t *testing.T) {
133 | 		p := NewProgram(nil, WithMouseAllMotion(), WithoutBracketedPaste(), WithAltScreen(), WithInputTTY())
134 | 		for _, opt := range []startupOptions{withMouseAllMotion, withoutBracketedPaste, withAltScreen} {
135 | 			if !p.startupOptions.has(opt) {
136 | 				t.Errorf("expected startup options have %v, got %v", opt, p.startupOptions)
137 | 			}
138 | 			if p.inputType != ttyInput {
139 | 				t.Errorf("expected input to be %v, got %v", opt, p.startupOptions)
140 | 			}
141 | 		}
142 | 	})
143 | }
144 | 


--------------------------------------------------------------------------------
/renderer.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | // renderer is the interface for Bubble Tea renderers.
 4 | type renderer interface {
 5 | 	// Start the renderer.
 6 | 	start()
 7 | 
 8 | 	// Stop the renderer, but render the final frame in the buffer, if any.
 9 | 	stop()
10 | 
11 | 	// Stop the renderer without doing any final rendering.
12 | 	kill()
13 | 
14 | 	// Write a frame to the renderer. The renderer can write this data to
15 | 	// output at its discretion.
16 | 	write(string)
17 | 
18 | 	// Request a full re-render. Note that this will not trigger a render
19 | 	// immediately. Rather, this method causes the next render to be a full
20 | 	// repaint. Because of this, it's safe to call this method multiple times
21 | 	// in succession.
22 | 	repaint()
23 | 
24 | 	// Clears the terminal.
25 | 	clearScreen()
26 | 
27 | 	// Whether or not the alternate screen buffer is enabled.
28 | 	altScreen() bool
29 | 	// Enable the alternate screen buffer.
30 | 	enterAltScreen()
31 | 	// Disable the alternate screen buffer.
32 | 	exitAltScreen()
33 | 
34 | 	// Show the cursor.
35 | 	showCursor()
36 | 	// Hide the cursor.
37 | 	hideCursor()
38 | 
39 | 	// enableMouseCellMotion enables mouse click, release, wheel and motion
40 | 	// events if a mouse button is pressed (i.e., drag events).
41 | 	enableMouseCellMotion()
42 | 
43 | 	// disableMouseCellMotion disables Mouse Cell Motion tracking.
44 | 	disableMouseCellMotion()
45 | 
46 | 	// enableMouseAllMotion enables mouse click, release, wheel and motion
47 | 	// events, regardless of whether a mouse button is pressed. Many modern
48 | 	// terminals support this, but not all.
49 | 	enableMouseAllMotion()
50 | 
51 | 	// disableMouseAllMotion disables All Motion mouse tracking.
52 | 	disableMouseAllMotion()
53 | 
54 | 	// enableMouseSGRMode enables mouse extended mode (SGR).
55 | 	enableMouseSGRMode()
56 | 
57 | 	// disableMouseSGRMode disables mouse extended mode (SGR).
58 | 	disableMouseSGRMode()
59 | 
60 | 	// enableBracketedPaste enables bracketed paste, where characters
61 | 	// inside the input are not interpreted when pasted as a whole.
62 | 	enableBracketedPaste()
63 | 
64 | 	// disableBracketedPaste disables bracketed paste.
65 | 	disableBracketedPaste()
66 | 
67 | 	// bracketedPasteActive reports whether bracketed paste mode is
68 | 	// currently enabled.
69 | 	bracketedPasteActive() bool
70 | 
71 | 	// setWindowTitle sets the terminal window title.
72 | 	setWindowTitle(string)
73 | 
74 | 	// reportFocus returns whether reporting focus events is enabled.
75 | 	reportFocus() bool
76 | 
77 | 	// enableReportFocus reports focus events to the program.
78 | 	enableReportFocus()
79 | 
80 | 	// disableReportFocus stops reporting focus events to the program.
81 | 	disableReportFocus()
82 | 
83 | 	// resetLinesRendered ensures exec output remains on screen on exit
84 | 	resetLinesRendered()
85 | }
86 | 
87 | // repaintMsg forces a full repaint.
88 | type repaintMsg struct{}
89 | 


--------------------------------------------------------------------------------
/screen_test.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | import (
 4 | 	"bytes"
 5 | 	"testing"
 6 | )
 7 | 
 8 | func TestClearMsg(t *testing.T) {
 9 | 	tests := []struct {
10 | 		name     string
11 | 		cmds     sequenceMsg
12 | 		expected string
13 | 	}{
14 | 		{
15 | 			name:     "clear_screen",
16 | 			cmds:     []Cmd{ClearScreen},
17 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[H\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
18 | 		},
19 | 		{
20 | 			name:     "altscreen",
21 | 			cmds:     []Cmd{EnterAltScreen, ExitAltScreen},
22 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[H\x1b[?25l\x1b[?1049l\x1b[?25l\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
23 | 		},
24 | 		{
25 | 			name:     "altscreen_autoexit",
26 | 			cmds:     []Cmd{EnterAltScreen},
27 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[H\x1b[?25l\x1b[H\rsuccess\x1b[K\r\n\x1b[K\x1b[2;H\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h",
28 | 		},
29 | 		{
30 | 			name:     "mouse_cellmotion",
31 | 			cmds:     []Cmd{EnableMouseCellMotion},
32 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?1002h\x1b[?1006h\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
33 | 		},
34 | 		{
35 | 			name:     "mouse_allmotion",
36 | 			cmds:     []Cmd{EnableMouseAllMotion},
37 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
38 | 		},
39 | 		{
40 | 			name:     "mouse_disable",
41 | 			cmds:     []Cmd{EnableMouseAllMotion, DisableMouse},
42 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006l\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
43 | 		},
44 | 		{
45 | 			name:     "cursor_hide",
46 | 			cmds:     []Cmd{HideCursor},
47 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
48 | 		},
49 | 		{
50 | 			name:     "cursor_hideshow",
51 | 			cmds:     []Cmd{HideCursor, ShowCursor},
52 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\x1b[?25h\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
53 | 		},
54 | 		{
55 | 			name:     "bp_stop_start",
56 | 			cmds:     []Cmd{DisableBracketedPaste, EnableBracketedPaste},
57 | 			expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004h\rsuccess\x1b[K\r\n\x1b[K\x1b[80D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
58 | 		},
59 | 	}
60 | 
61 | 	for _, test := range tests {
62 | 		t.Run(test.name, func(t *testing.T) {
63 | 			var buf bytes.Buffer
64 | 			var in bytes.Buffer
65 | 
66 | 			m := &testModel{}
67 | 			p := NewProgram(m, WithInput(&in), WithOutput(&buf))
68 | 
69 | 			test.cmds = append([]Cmd{func() Msg { return WindowSizeMsg{80, 24} }}, test.cmds...)
70 | 			test.cmds = append(test.cmds, Quit)
71 | 			go p.Send(test.cmds)
72 | 
73 | 			if _, err := p.Run(); err != nil {
74 | 				t.Fatal(err)
75 | 			}
76 | 
77 | 			if buf.String() != test.expected {
78 | 				t.Errorf("expected embedded sequence:\n%q\ngot:\n%q", test.expected, buf.String())
79 | 			}
80 | 		})
81 | 	}
82 | }
83 | 


--------------------------------------------------------------------------------
/signals_unix.go:
--------------------------------------------------------------------------------
 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
 3 | 
 4 | package tea
 5 | 
 6 | import (
 7 | 	"os"
 8 | 	"os/signal"
 9 | 	"syscall"
10 | )
11 | 
12 | // listenForResize sends messages (or errors) when the terminal resizes.
13 | // Argument output should be the file descriptor for the terminal; usually
14 | // os.Stdout.
15 | func (p *Program) listenForResize(done chan struct{}) {
16 | 	sig := make(chan os.Signal, 1)
17 | 	signal.Notify(sig, syscall.SIGWINCH)
18 | 
19 | 	defer func() {
20 | 		signal.Stop(sig)
21 | 		close(done)
22 | 	}()
23 | 
24 | 	for {
25 | 		select {
26 | 		case <-p.ctx.Done():
27 | 			return
28 | 		case <-sig:
29 | 		}
30 | 
31 | 		p.checkResize()
32 | 	}
33 | }
34 | 


--------------------------------------------------------------------------------
/signals_windows.go:
--------------------------------------------------------------------------------
 1 | //go:build windows
 2 | // +build windows
 3 | 
 4 | package tea
 5 | 
 6 | // listenForResize is not available on windows because windows does not
 7 | // implement syscall.SIGWINCH.
 8 | func (p *Program) listenForResize(done chan struct{}) {
 9 | 	close(done)
10 | }
11 | 


--------------------------------------------------------------------------------
/tea_init.go:
--------------------------------------------------------------------------------
 1 | package tea
 2 | 
 3 | import (
 4 | 	"github.com/charmbracelet/lipgloss"
 5 | )
 6 | 
 7 | func init() {
 8 | 	// XXX: This is a workaround to make assure that Lip Gloss and Termenv
 9 | 	// query the terminal before any Bubble Tea Program runs and acquires the
10 | 	// terminal. Without this, Programs that use Lip Gloss/Termenv might hang
11 | 	// while waiting for a a [termenv.OSCTimeout] while querying the terminal
12 | 	// for its background/foreground colors.
13 | 	//
14 | 	// This happens because Bubble Tea acquires the terminal before termenv
15 | 	// reads any responses.
16 | 	//
17 | 	// Note that this will only affect programs running on the default IO i.e.
18 | 	// [os.Stdout] and [os.Stdin].
19 | 	//
20 | 	// This workaround will be removed in v2.
21 | 	_ = lipgloss.HasDarkBackground()
22 | }
23 | 


--------------------------------------------------------------------------------
/tty.go:
--------------------------------------------------------------------------------
  1 | package tea
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"fmt"
  6 | 	"io"
  7 | 	"time"
  8 | 
  9 | 	"github.com/charmbracelet/x/term"
 10 | 	"github.com/muesli/cancelreader"
 11 | )
 12 | 
 13 | func (p *Program) suspend() {
 14 | 	if err := p.ReleaseTerminal(); err != nil {
 15 | 		// If we can't release input, abort.
 16 | 		return
 17 | 	}
 18 | 
 19 | 	suspendProcess()
 20 | 
 21 | 	_ = p.RestoreTerminal()
 22 | 	go p.Send(ResumeMsg{})
 23 | }
 24 | 
 25 | func (p *Program) initTerminal() error {
 26 | 	if _, ok := p.renderer.(*nilRenderer); ok {
 27 | 		// No need to initialize the terminal if we're not rendering
 28 | 		return nil
 29 | 	}
 30 | 
 31 | 	if err := p.initInput(); err != nil {
 32 | 		return err
 33 | 	}
 34 | 
 35 | 	p.renderer.hideCursor()
 36 | 	return nil
 37 | }
 38 | 
 39 | // restoreTerminalState restores the terminal to the state prior to running the
 40 | // Bubble Tea program.
 41 | func (p *Program) restoreTerminalState() error {
 42 | 	if p.renderer != nil {
 43 | 		p.renderer.disableBracketedPaste()
 44 | 		p.renderer.showCursor()
 45 | 		p.disableMouse()
 46 | 
 47 | 		if p.renderer.reportFocus() {
 48 | 			p.renderer.disableReportFocus()
 49 | 		}
 50 | 
 51 | 		if p.renderer.altScreen() {
 52 | 			p.renderer.exitAltScreen()
 53 | 
 54 | 			// give the terminal a moment to catch up
 55 | 			time.Sleep(time.Millisecond * 10) //nolint:mnd
 56 | 		}
 57 | 	}
 58 | 
 59 | 	return p.restoreInput()
 60 | }
 61 | 
 62 | // restoreInput restores the tty input to its original state.
 63 | func (p *Program) restoreInput() error {
 64 | 	if p.ttyInput != nil && p.previousTtyInputState != nil {
 65 | 		if err := term.Restore(p.ttyInput.Fd(), p.previousTtyInputState); err != nil {
 66 | 			return fmt.Errorf("error restoring console: %w", err)
 67 | 		}
 68 | 	}
 69 | 	if p.ttyOutput != nil && p.previousOutputState != nil {
 70 | 		if err := term.Restore(p.ttyOutput.Fd(), p.previousOutputState); err != nil {
 71 | 			return fmt.Errorf("error restoring console: %w", err)
 72 | 		}
 73 | 	}
 74 | 	return nil
 75 | }
 76 | 
 77 | // initCancelReader (re)commences reading inputs.
 78 | func (p *Program) initCancelReader(cancel bool) error {
 79 | 	if cancel && p.cancelReader != nil {
 80 | 		p.cancelReader.Cancel()
 81 | 		p.waitForReadLoop()
 82 | 	}
 83 | 
 84 | 	var err error
 85 | 	p.cancelReader, err = newInputReader(p.input, p.mouseMode)
 86 | 	if err != nil {
 87 | 		return fmt.Errorf("error creating cancelreader: %w", err)
 88 | 	}
 89 | 
 90 | 	p.readLoopDone = make(chan struct{})
 91 | 	go p.readLoop()
 92 | 
 93 | 	return nil
 94 | }
 95 | 
 96 | func (p *Program) readLoop() {
 97 | 	defer close(p.readLoopDone)
 98 | 
 99 | 	err := readInputs(p.ctx, p.msgs, p.cancelReader)
100 | 	if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
101 | 		select {
102 | 		case <-p.ctx.Done():
103 | 		case p.errs <- err:
104 | 		}
105 | 	}
106 | }
107 | 
108 | // waitForReadLoop waits for the cancelReader to finish its read loop.
109 | func (p *Program) waitForReadLoop() {
110 | 	select {
111 | 	case <-p.readLoopDone:
112 | 	case <-time.After(500 * time.Millisecond): //nolint:mnd
113 | 		// The read loop hangs, which means the input
114 | 		// cancelReader's cancel function has returned true even
115 | 		// though it was not able to cancel the read.
116 | 	}
117 | }
118 | 
119 | // checkResize detects the current size of the output and informs the program
120 | // via a WindowSizeMsg.
121 | func (p *Program) checkResize() {
122 | 	if p.ttyOutput == nil {
123 | 		// can't query window size
124 | 		return
125 | 	}
126 | 
127 | 	w, h, err := term.GetSize(p.ttyOutput.Fd())
128 | 	if err != nil {
129 | 		select {
130 | 		case <-p.ctx.Done():
131 | 		case p.errs <- err:
132 | 		}
133 | 
134 | 		return
135 | 	}
136 | 
137 | 	p.Send(WindowSizeMsg{
138 | 		Width:  w,
139 | 		Height: h,
140 | 	})
141 | }
142 | 


--------------------------------------------------------------------------------
/tty_unix.go:
--------------------------------------------------------------------------------
 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
 3 | 
 4 | package tea
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | 	"os/signal"
10 | 	"syscall"
11 | 
12 | 	"github.com/charmbracelet/x/term"
13 | )
14 | 
15 | func (p *Program) initInput() (err error) {
16 | 	// Check if input is a terminal
17 | 	if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
18 | 		p.ttyInput = f
19 | 		p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
20 | 		if err != nil {
21 | 			return fmt.Errorf("error entering raw mode: %w", err)
22 | 		}
23 | 	}
24 | 
25 | 	if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
26 | 		p.ttyOutput = f
27 | 	}
28 | 
29 | 	return nil
30 | }
31 | 
32 | func openInputTTY() (*os.File, error) {
33 | 	f, err := os.Open("/dev/tty")
34 | 	if err != nil {
35 | 		return nil, fmt.Errorf("could not open a new TTY: %w", err)
36 | 	}
37 | 	return f, nil
38 | }
39 | 
40 | const suspendSupported = true
41 | 
42 | // Send SIGTSTP to the entire process group.
43 | func suspendProcess() {
44 | 	c := make(chan os.Signal, 1)
45 | 	signal.Notify(c, syscall.SIGCONT)
46 | 	_ = syscall.Kill(0, syscall.SIGTSTP)
47 | 	// blocks until a CONT happens...
48 | 	<-c
49 | }
50 | 


--------------------------------------------------------------------------------
/tty_windows.go:
--------------------------------------------------------------------------------
 1 | //go:build windows
 2 | // +build windows
 3 | 
 4 | package tea
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | 
10 | 	"github.com/charmbracelet/x/term"
11 | 	"golang.org/x/sys/windows"
12 | )
13 | 
14 | func (p *Program) initInput() (err error) {
15 | 	// Save stdin state and enable VT input
16 | 	// We also need to enable VT
17 | 	// input here.
18 | 	if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
19 | 		p.ttyInput = f
20 | 		p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
21 | 		if err != nil {
22 | 			return fmt.Errorf("error making raw: %w", err)
23 | 		}
24 | 
25 | 		// Enable VT input
26 | 		var mode uint32
27 | 		if err := windows.GetConsoleMode(windows.Handle(p.ttyInput.Fd()), &mode); err != nil {
28 | 			return fmt.Errorf("error getting console mode: %w", err)
29 | 		}
30 | 
31 | 		if err := windows.SetConsoleMode(windows.Handle(p.ttyInput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
32 | 			return fmt.Errorf("error setting console mode: %w", err)
33 | 		}
34 | 	}
35 | 
36 | 	// Save output screen buffer state and enable VT processing.
37 | 	if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
38 | 		p.ttyOutput = f
39 | 		p.previousOutputState, err = term.GetState(f.Fd())
40 | 		if err != nil {
41 | 			return fmt.Errorf("error getting state: %w", err)
42 | 		}
43 | 
44 | 		var mode uint32
45 | 		if err := windows.GetConsoleMode(windows.Handle(p.ttyOutput.Fd()), &mode); err != nil {
46 | 			return fmt.Errorf("error getting console mode: %w", err)
47 | 		}
48 | 
49 | 		if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil {
50 | 			return fmt.Errorf("error setting console mode: %w", err)
51 | 		}
52 | 	}
53 | 
54 | 	return nil
55 | }
56 | 
57 | // Open the Windows equivalent of a TTY.
58 | func openInputTTY() (*os.File, error) {
59 | 	f, err := os.OpenFile("CONIN
quot;, os.O_RDWR, 0o644)
60 | 	if err != nil {
61 | 		return nil, fmt.Errorf("error opening file: %w", err)
62 | 	}
63 | 	return f, nil
64 | }
65 | 
66 | const suspendSupported = false
67 | 
68 | func suspendProcess() {}
69 | 


--------------------------------------------------------------------------------
/tutorials/basics/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 
 7 | 	tea "github.com/charmbracelet/bubbletea"
 8 | )
 9 | 
10 | type model struct {
11 | 	cursor   int
12 | 	choices  []string
13 | 	selected map[int]struct{}
14 | }
15 | 
16 | func initialModel() model {
17 | 	return model{
18 | 		choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
19 | 
20 | 		// A map which indicates which choices are selected. We're using
21 | 		// the map like a mathematical set. The keys refer to the indexes
22 | 		// of the `choices` slice, above.
23 | 		selected: make(map[int]struct{}),
24 | 	}
25 | }
26 | 
27 | func (m model) Init() tea.Cmd {
28 | 	return tea.SetWindowTitle("Grocery List")
29 | }
30 | 
31 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
32 | 	switch msg := msg.(type) {
33 | 	case tea.KeyMsg:
34 | 		switch msg.String() {
35 | 		case "ctrl+c", "q":
36 | 			return m, tea.Quit
37 | 		case "up", "k":
38 | 			if m.cursor > 0 {
39 | 				m.cursor--
40 | 			}
41 | 		case "down", "j":
42 | 			if m.cursor < len(m.choices)-1 {
43 | 				m.cursor++
44 | 			}
45 | 		case "enter", " ":
46 | 			_, ok := m.selected[m.cursor]
47 | 			if ok {
48 | 				delete(m.selected, m.cursor)
49 | 			} else {
50 | 				m.selected[m.cursor] = struct{}{}
51 | 			}
52 | 		}
53 | 	}
54 | 
55 | 	return m, nil
56 | }
57 | 
58 | func (m model) View() string {
59 | 	s := "What should we buy at the market?\n\n"
60 | 
61 | 	for i, choice := range m.choices {
62 | 		cursor := " "
63 | 		if m.cursor == i {
64 | 			cursor = ">"
65 | 		}
66 | 
67 | 		checked := " "
68 | 		if _, ok := m.selected[i]; ok {
69 | 			checked = "x"
70 | 		}
71 | 
72 | 		s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
73 | 	}
74 | 
75 | 	s += "\nPress q to quit.\n"
76 | 
77 | 	return s
78 | }
79 | 
80 | func main() {
81 | 	p := tea.NewProgram(initialModel())
82 | 	if _, err := p.Run(); err != nil {
83 | 		fmt.Printf("Alas, there's been an error: %v", err)
84 | 		os.Exit(1)
85 | 	}
86 | }
87 | 


--------------------------------------------------------------------------------
/tutorials/commands/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"net/http"
 6 | 	"os"
 7 | 	"time"
 8 | 
 9 | 	tea "github.com/charmbracelet/bubbletea"
10 | )
11 | 
12 | const url = "https://charm.sh/"
13 | 
14 | type model struct {
15 | 	status int
16 | 	err    error
17 | }
18 | 
19 | func checkServer() tea.Msg {
20 | 	c := &http.Client{Timeout: 10 * time.Second}
21 | 	res, err := c.Get(url)
22 | 	if err != nil {
23 | 		return errMsg{err}
24 | 	}
25 | 	defer res.Body.Close() // nolint:errcheck
26 | 
27 | 	return statusMsg(res.StatusCode)
28 | }
29 | 
30 | type statusMsg int
31 | 
32 | type errMsg struct{ err error }
33 | 
34 | // For messages that contain errors it's often handy to also implement the
35 | // error interface on the message.
36 | func (e errMsg) Error() string { return e.err.Error() }
37 | 
38 | func (m model) Init() tea.Cmd {
39 | 	return checkServer
40 | }
41 | 
42 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
43 | 	switch msg := msg.(type) {
44 | 	case statusMsg:
45 | 		m.status = int(msg)
46 | 		return m, tea.Quit
47 | 
48 | 	case errMsg:
49 | 		m.err = msg
50 | 		return m, tea.Quit
51 | 
52 | 	case tea.KeyMsg:
53 | 		if msg.Type == tea.KeyCtrlC {
54 | 			return m, tea.Quit
55 | 		}
56 | 	}
57 | 
58 | 	return m, nil
59 | }
60 | 
61 | func (m model) View() string {
62 | 	if m.err != nil {
63 | 		return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err)
64 | 	}
65 | 
66 | 	s := fmt.Sprintf("Checking %s ... ", url)
67 | 	if m.status > 0 {
68 | 		s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status))
69 | 	}
70 | 	return "\n" + s + "\n\n"
71 | }
72 | 
73 | func main() {
74 | 	if _, err := tea.NewProgram(model{}).Run(); err != nil {
75 | 		fmt.Printf("Uh oh, there was an error: %v\n", err)
76 | 		os.Exit(1)
77 | 	}
78 | }
79 | 


--------------------------------------------------------------------------------
/tutorials/go.mod:
--------------------------------------------------------------------------------
 1 | module tutorial
 2 | 
 3 | go 1.18
 4 | 
 5 | require github.com/charmbracelet/bubbletea v0.25.0
 6 | 
 7 | require (
 8 | 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 9 | 	github.com/charmbracelet/lipgloss v0.13.1 // indirect
10 | 	github.com/charmbracelet/x/ansi v0.4.0 // indirect
11 | 	github.com/charmbracelet/x/term v0.2.0 // indirect
12 | 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
13 | 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
14 | 	github.com/mattn/go-isatty v0.0.20 // indirect
15 | 	github.com/mattn/go-localereader v0.0.1 // indirect
16 | 	github.com/mattn/go-runewidth v0.0.15 // indirect
17 | 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
18 | 	github.com/muesli/cancelreader v0.2.2 // indirect
19 | 	github.com/muesli/termenv v0.15.2 // indirect
20 | 	github.com/rivo/uniseg v0.4.7 // indirect
21 | 	golang.org/x/sync v0.8.0 // indirect
22 | 	golang.org/x/sys v0.26.0 // indirect
23 | 	golang.org/x/text v0.13.0 // indirect
24 | )
25 | 
26 | replace github.com/charmbracelet/bubbletea => ../
27 | 


--------------------------------------------------------------------------------
/tutorials/go.sum:
--------------------------------------------------------------------------------
 1 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 3 | github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A=
 4 | github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U=
 5 | github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU=
 6 | github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
 7 | github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
 8 | github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
 9 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
10 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
11 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
12 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
13 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
14 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
15 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
16 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
17 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
18 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
19 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
20 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
21 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
22 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
23 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
24 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
25 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
26 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
27 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
28 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
29 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
30 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
31 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
32 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
33 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
34 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
35 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
36 | 


--------------------------------------------------------------------------------