Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6a799f4
docs(readme): update header image, minor corrections
meowgorithm Feb 24, 2026
8d9107f
chore: bump bubbletea and lipgloss to v2.0.0
aymanbagabas Feb 24, 2026
c2c79e3
chore(deps): bump the all group with 2 updates (#902)
dependabot[bot] Mar 9, 2026
d2b804e
chore(deps): bump the all group with 2 updates (#908)
dependabot[bot] Mar 16, 2026
f1daacf
feat(textarea): dynamic height (#910)
meowgorithm Mar 25, 2026
a13f110
docs: fix typo in README (#921)
Rohan5commit Mar 30, 2026
204774b
chore(deps): bump github.com/mattn/go-runewidth in the all group (#945)
dependabot[bot] Apr 6, 2026
0d62ce2
chore(deps): bump dependabot/fetch-metadata from 2 to 3 in the all gr…
dependabot[bot] Apr 13, 2026
42130e8
chore(deps): bump github.com/mattn/go-runewidth in the all group (#952)
dependabot[bot] Apr 13, 2026
6feda24
chore(deps): bump the all group with 3 updates (#958)
dependabot[bot] Apr 20, 2026
ef0090e
chore: remove CODEOWNERS
andreynering Apr 22, 2026
8585ef8
chore(deps): bump github.com/sahilm/fuzzy in the all group (#969)
dependabot[bot] May 11, 2026
40d145e
chore(deps): bump the all group with 2 updates (#996)
dependabot[bot] Jun 2, 2026
492ed7d
chore(deps): bump the all group with 2 updates (#1004)
dependabot[bot] Jun 15, 2026
0dafc72
chore(deps): bump actions/checkout from 6 to 7 in the all group (#1006)
dependabot[bot] Jun 22, 2026
d2b2217
fix(textarea): apply prompt style on all lines, including fillers
meowgorithm Jul 4, 2026
2a7b6fe
chore(deps): bump the all group with 2 updates (#1013)
dependabot[bot] Jul 6, 2026
b52e21a
chore(deps): bump actions/setup-go from 6 to 7 in the all group (#1016)
dependabot[bot] Jul 20, 2026
d36683e
chore(deps): bump github.com/mattn/go-runewidth in the all group (#1021)
dependabot[bot] Jul 27, 2026
8cea431
feat(textarea): support ctrl + left/right/backspace (#1020)
zoriya Aug 4, 2026
355f960
ci: sync dependabot config (#896)
charmcli Aug 12, 2026
6484e49
fix(filepicker): don't blank the view when height is unset (#1026)
andrinoff Aug 20, 2026
037ad37
feat: textarea selection (#1029)
andrinoff Aug 21, 2026
a9d1dd2
chore: bump golangci (#1039)
andrinoff Aug 21, 2026
e97af08
feat: tree bubble (#893)
dlvhdr Aug 21, 2026
ef8af03
v2.2.0
andrinoff Aug 21, 2026
87d4458
docs: update godoc link in readme to new package
meowgorithm Aug 21, 2026
10489b5
readme: drop tree section for now
meowgorithm Aug 21, 2026
903e1d5
chore(golint): bumop golangci (#1041)
andrinoff Aug 24, 2026
f613411
fix(textarea): stop word-left at input boundary (#1036)
OxQuasar Aug 24, 2026
4909481
v2.2.1
andrinoff Aug 24, 2026
0a69b19
chore(deps): bump the all group with 4 updates (#1047)
dependabot[bot] Sep 1, 2026
c3bde0d
chore(deps): bump the all group with 4 updates (#1062)
dependabot[bot] Oct 1, 2026
033985b
ci: sync golangci-lint config
aymanbagabas Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/CODEOWNERS

This file was deleted.

19 changes: 7 additions & 12 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
interval: "monthly"
labels:
- "dependencies"
commit-message:
Expand All @@ -17,14 +14,15 @@ updates:
all:
patterns:
- "*"
ignore:
- dependency-name: github.com/charmbracelet/bubbletea/v2
versions:
- v2.0.0-beta1

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
interval: "monthly"
labels:
- "dependencies"
commit-message:
Expand All @@ -38,10 +36,7 @@ updates:
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
interval: "monthly"
labels:
- "dependencies"
commit-message:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go-version }}

- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Download Go modules
run: go mod download
Expand All @@ -36,7 +36,7 @@ jobs:
if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}
steps:
- id: metadata
uses: dependabot/fetch-metadata@v2
uses: dependabot/fetch-metadata@v3
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go-version }}

- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Coverage
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ jobs:
uses: charmbracelet/meta/.github/workflows/lint.yml@main
with:
golangci_path: .golangci.yml
golangci_version: v2.9
golangci_version: v2.13.1
timeout: 10m
12 changes: 7 additions & 5 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ linters:
- exhaustive
- goconst
- godot
- godox
- gomoddirectives
- goprintffuncname
- gosec
Expand All @@ -27,13 +26,16 @@ linters:
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
rules:
- linters:
- revive
text: "var-naming"
settings:
exhaustive:
default-signifies-exhaustive: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Bubbles

<img src="https://github.com/user-attachments/assets/09d46497-9b4e-4de9-bb8e-685ef5c80c96" width="350" />
<img src="https://github.com/user-attachments/assets/b89fa46e-d451-4b33-a009-c68d4765520f" width="350" />

[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/charm.land/bubbles/v2)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
[![Go ReportCard](https://goreportcard.com/badge/charmbracelet/bubbles)](https://goreportcard.com/report/charmbracelet/bubbles)

Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea)
Primitives for [Bubble Tea](https://github.com/charmbracelet/bubbletea)
applications. These components are used in production in [Crush][crush], and [many other applications][otherstuff].

> [!TIP]
Expand Down Expand Up @@ -194,7 +194,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
To check out community-maintained Bubbles see [Charm & Friends][charmandfriends].
Made a cool Bubble that you want to share? [PRs][prs] are welcome!

[charmandfriends]:
[charmandfriends]: https://github.com/charm-and-friends/additional-bubbles
[prs]: https://github.com/charm-and-friends/additional-bubbles?tab=readme-ov-file#what-is-a-complete-project

## Contributing
Expand Down
28 changes: 19 additions & 9 deletions filepicker/filepicker.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,20 @@ func (m Model) readDir(path string, showHidden bool) tea.Cmd {
func (m *Model) SetHeight(h int) {
m.height = h
if m.maxIdx > m.height-1 {
m.maxIdx = m.minIdx + m.height - 1
m.maxIdx = m.bottomIdx(m.minIdx)
}
}

// bottomIdx returns the index of the last visible entry for a viewport whose
// first visible entry is top. If the height has not been set yet, one entry is
// shown so that the view is never blank.
func (m Model) bottomIdx(top int) int {
if m.height < 1 {
return top
}
return top + m.height - 1
}

// Height returns the height of the file picker.
func (m Model) Height() int {
return m.height
Expand All @@ -250,21 +260,21 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
break
}
m.files = msg.entries
m.maxIdx = max(m.maxIdx, m.Height()-1)
m.maxIdx = max(m.maxIdx, m.bottomIdx(m.minIdx))
case tea.WindowSizeMsg:
if m.AutoHeight {
m.SetHeight(msg.Height - marginBottom)
}
m.maxIdx = m.Height() - 1
m.maxIdx = m.bottomIdx(m.minIdx)
case tea.KeyPressMsg:
switch {
case key.Matches(msg, m.KeyMap.GoToTop):
m.selected = 0
m.minIdx = 0
m.maxIdx = m.Height() - 1
m.maxIdx = m.bottomIdx(0)
case key.Matches(msg, m.KeyMap.GoToLast):
m.selected = len(m.files) - 1
m.minIdx = len(m.files) - m.Height()
m.minIdx = max(len(m.files)-m.Height(), 0)
m.maxIdx = len(m.files) - 1
case key.Matches(msg, m.KeyMap.Down):
m.selected++
Expand Down Expand Up @@ -294,7 +304,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {

if m.maxIdx >= len(m.files) {
m.maxIdx = len(m.files) - 1
m.minIdx = m.maxIdx - m.Height()
m.minIdx = max(m.maxIdx-m.Height(), 0)
}
case key.Matches(msg, m.KeyMap.PageUp):
m.selected -= m.Height()
Expand All @@ -315,7 +325,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
} else {
m.selected = 0
m.minIdx = 0
m.maxIdx = m.Height() - 1
m.maxIdx = m.bottomIdx(0)
}
return m, m.readDir(m.CurrentDirectory, m.ShowHidden)
case key.Matches(msg, m.KeyMap.Open):
Expand Down Expand Up @@ -357,7 +367,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.pushView(m.selected, m.minIdx, m.maxIdx)
m.selected = 0
m.minIdx = 0
m.maxIdx = m.Height() - 1
m.maxIdx = m.bottomIdx(0)
return m, m.readDir(m.CurrentDirectory, m.ShowHidden)
}
}
Expand Down Expand Up @@ -495,7 +505,7 @@ func (m Model) didSelectFile(msg tea.Msg) (bool, string) {
}
}

if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) && m.Path != "" {
if ((!isDir && m.FileAllowed) || (isDir && m.DirAllowed)) && m.Path != "" {
return true, m.Path
}

Expand Down
86 changes: 86 additions & 0 deletions filepicker/filepicker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package filepicker

import (
"os"
"path/filepath"
"strings"
"testing"

tea "charm.land/bubbletea/v2"
)

// newTestDir creates a directory containing a subdirectory with a file in it.
func newTestDir(tb testing.TB) string {
tb.Helper()
root := tb.TempDir()
sub := filepath.Join(root, "subdir")
if err := os.Mkdir(sub, 0o755); err != nil {
tb.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sub, "file.go"), []byte("package main"), 0o644); err != nil {
tb.Fatal(err)
}
return root
}

// update sends msg to the model and runs the returned command, if any, feeding
// its message back into the model.
func update(tb testing.TB, m Model, msg tea.Msg) Model {
tb.Helper()
m, cmd := m.Update(msg)
if cmd != nil {
m, _ = m.Update(cmd())
}
return m
}

// TestNavigateWithoutWindowSize asserts that a file picker which never receives
// a tea.WindowSizeMsg still renders its entries after entering a directory.
func TestNavigateWithoutWindowSize(t *testing.T) {
m := New()
m.CurrentDirectory = newTestDir(t)

m = update(t, m, m.Init()())
if got := m.View(); !strings.Contains(got, "subdir") {
t.Fatalf("expected view to contain %q, got %q", "subdir", got)
}

m = update(t, m, tea.KeyPressMsg{Code: 'l', Text: "l"})
if !strings.HasSuffix(m.CurrentDirectory, "subdir") {
t.Fatalf("expected to descend into subdir, got %q", m.CurrentDirectory)
}
if m.maxIdx < m.minIdx {
t.Fatalf("maxIdx (%d) must not be less than minIdx (%d)", m.maxIdx, m.minIdx)
}
if got := m.View(); !strings.Contains(got, "file.go") {
t.Fatalf("expected view to contain %q, got %q", "file.go", got)
}
}

// TestDidSelectFileRequiresPath asserts that a selection is only reported once
// a path has actually been selected.
func TestDidSelectFileRequiresPath(t *testing.T) {
m := New()
m.CurrentDirectory = filepath.Join(newTestDir(t), "subdir")
m.AllowedTypes = []string{".txt"}

m = update(t, m, m.Init()())

enter := tea.KeyPressMsg{Code: tea.KeyEnter}
if didSelect, path := m.DidSelectFile(enter); didSelect {
t.Fatalf("expected no selection for a disallowed type, got %q", path)
}
if didSelect, path := m.DidSelectDisabledFile(enter); didSelect {
t.Fatalf("expected no disabled selection before a path is set, got %q", path)
}

m.AllowedTypes = []string{".go"}
m = update(t, m, enter)
didSelect, path := m.DidSelectFile(enter)
if !didSelect {
t.Fatal("expected file.go to be selected")
}
if !strings.HasSuffix(path, "file.go") {
t.Fatalf("expected path to end in file.go, got %q", path)
}
}
34 changes: 16 additions & 18 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
module charm.land/bubbles/v2

go 1.24.2
go 1.26.0

require (
charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106192006-06c0cda318b3
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7
charm.land/bubbletea/v2 v2.0.10
charm.land/lipgloss/v2 v2.0.6
github.com/MakeNowJust/heredoc v1.0.0
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
github.com/dustin/go-humanize v1.0.1
github.com/mattn/go-runewidth v0.0.20
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/exp/golden v0.1.0
github.com/dustin/go-humanize v1.1.0
github.com/mattn/go-runewidth v0.0.30
github.com/rivo/uniseg v0.4.7
github.com/sahilm/fuzzy v0.1.1
github.com/sahilm/fuzzy v0.1.3
)

require (
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect
github.com/aymanbagabas/go-udiff v0.4.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)
Loading