mirror of
https://github.com/charmbracelet/bubbles.git
synced 2026-07-21 02:59:28 -06:00
chore: merge branch 'master' into v2-exp
This commit is contained in:
commit
287c9c4ffd
@ -234,6 +234,12 @@ following requirements:
|
||||
|
||||
Thank you!
|
||||
|
||||
## Contributing
|
||||
|
||||
See [contributing][contribute].
|
||||
|
||||
[contribute]: https://github.com/charmbracelet/bubbles/contribute
|
||||
|
||||
## Feedback
|
||||
|
||||
We’d love to hear your thoughts on this project. Feel free to drop us a note!
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
@ -15,17 +15,10 @@ import (
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
var (
|
||||
lastID int
|
||||
idMtx sync.Mutex
|
||||
)
|
||||
var lastID int64
|
||||
|
||||
// Return the next ID we should use on the Model.
|
||||
func nextID() int {
|
||||
idMtx.Lock()
|
||||
defer idMtx.Unlock()
|
||||
lastID++
|
||||
return lastID
|
||||
return int(atomic.AddInt64(&lastID, 1))
|
||||
}
|
||||
|
||||
// New returns a new filepicker model with default styling and key bindings.
|
||||
|
||||
2
go.mod
2
go.mod
@ -9,7 +9,7 @@ require (
|
||||
github.com/charmbracelet/harmonica v0.2.0
|
||||
github.com/charmbracelet/lipgloss v0.13.0
|
||||
github.com/charmbracelet/x/ansi v0.3.2
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
|
||||
4
go.sum
4
go.sum
@ -16,8 +16,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA
|
||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
||||
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
|
||||
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
||||
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
|
||||
|
||||
60
help/help.go
60
help/help.go
@ -119,28 +119,23 @@ func (m Model) ShortHelpView(bindings []key.Binding) string {
|
||||
continue
|
||||
}
|
||||
|
||||
// Sep
|
||||
var sep string
|
||||
if totalWidth > 0 && i < len(bindings) {
|
||||
sep = separator
|
||||
}
|
||||
|
||||
// Item
|
||||
str := sep +
|
||||
m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " +
|
||||
m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc)
|
||||
|
||||
w := lipgloss.Width(str)
|
||||
|
||||
// If adding this help item would go over the available width, stop
|
||||
// drawing.
|
||||
if m.Width > 0 && totalWidth+w > m.Width {
|
||||
// Although if there's room for an ellipsis, print that.
|
||||
tail := " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis)
|
||||
tailWidth := lipgloss.Width(tail)
|
||||
|
||||
if totalWidth+tailWidth < m.Width {
|
||||
// Tail
|
||||
if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
|
||||
if tail != "" {
|
||||
b.WriteString(tail)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@ -165,8 +160,7 @@ func (m Model) FullHelpView(groups [][]key.Binding) string {
|
||||
out []string
|
||||
|
||||
totalWidth int
|
||||
sep = m.Styles.FullSeparator.Render(m.FullSeparator)
|
||||
sepWidth = lipgloss.Width(sep)
|
||||
separator = m.Styles.FullSeparator.Inline(true).Render(m.FullSeparator)
|
||||
)
|
||||
|
||||
// Iterate over groups to build columns
|
||||
@ -174,12 +168,17 @@ func (m Model) FullHelpView(groups [][]key.Binding) string {
|
||||
if group == nil || !shouldRenderColumn(group) {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
sep string
|
||||
keys []string
|
||||
descriptions []string
|
||||
)
|
||||
|
||||
// Sep
|
||||
if totalWidth > 0 && i < len(groups) {
|
||||
sep = separator
|
||||
}
|
||||
|
||||
// Separate keys and descriptions into different slices
|
||||
for _, kb := range group {
|
||||
if !kb.Enabled() {
|
||||
@ -189,33 +188,42 @@ func (m Model) FullHelpView(groups [][]key.Binding) string {
|
||||
descriptions = append(descriptions, kb.Help().Desc)
|
||||
}
|
||||
|
||||
// Column
|
||||
col := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
sep,
|
||||
m.Styles.FullKey.Render(strings.Join(keys, "\n")),
|
||||
m.Styles.FullKey.Render(" "),
|
||||
" ",
|
||||
m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")),
|
||||
)
|
||||
w := lipgloss.Width(col)
|
||||
|
||||
// Column
|
||||
totalWidth += lipgloss.Width(col)
|
||||
if m.Width > 0 && totalWidth > m.Width {
|
||||
// Tail
|
||||
if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
|
||||
if tail != "" {
|
||||
out = append(out, tail)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
totalWidth += w
|
||||
out = append(out, col)
|
||||
|
||||
// Separator
|
||||
if i < len(group)-1 {
|
||||
totalWidth += sepWidth
|
||||
if m.Width > 0 && totalWidth > m.Width {
|
||||
break
|
||||
}
|
||||
out = append(out, sep)
|
||||
}
|
||||
}
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, out...)
|
||||
}
|
||||
|
||||
func (m Model) shouldAddItem(totalWidth, width int) (tail string, ok bool) {
|
||||
// If there's room for an ellipsis, print that.
|
||||
if m.Width > 0 && totalWidth+width > m.Width {
|
||||
tail = " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis)
|
||||
|
||||
if totalWidth+lipgloss.Width(tail) < m.Width {
|
||||
return tail, false
|
||||
}
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
|
||||
func shouldRenderColumn(b []key.Binding) (ok bool) {
|
||||
for _, v := range b {
|
||||
if v.Enabled() {
|
||||
|
||||
38
help/help_test.go
Normal file
38
help/help_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package help
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/charmbracelet/x/exp/golden"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
)
|
||||
|
||||
func TestFullHelp(t *testing.T) {
|
||||
m := New()
|
||||
m.FullSeparator = " | "
|
||||
k := key.WithKeys("x")
|
||||
kb := [][]key.Binding{
|
||||
{
|
||||
key.NewBinding(k, key.WithHelp("enter", "continue")),
|
||||
},
|
||||
{
|
||||
key.NewBinding(k, key.WithHelp("esc", "back")),
|
||||
key.NewBinding(k, key.WithHelp("?", "help")),
|
||||
},
|
||||
{
|
||||
key.NewBinding(k, key.WithHelp("H", "home")),
|
||||
key.NewBinding(k, key.WithHelp("ctrl+c", "quit")),
|
||||
key.NewBinding(k, key.WithHelp("ctrl+l", "log")),
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range []int{20, 30, 40} {
|
||||
t.Run(fmt.Sprintf("full help %d width", w), func(t *testing.T) {
|
||||
m.Width = w
|
||||
s := m.FullHelpView(kb)
|
||||
golden.RequireEqual(t, []byte(s))
|
||||
})
|
||||
}
|
||||
}
|
||||
1
help/testdata/TestFullHelp/full_help_20_width.golden
vendored
Normal file
1
help/testdata/TestFullHelp/full_help_20_width.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
enter continue …
|
||||
2
help/testdata/TestFullHelp/full_help_30_width.golden
vendored
Normal file
2
help/testdata/TestFullHelp/full_help_30_width.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
enter continue | esc back …
|
||||
? help
|
||||
3
help/testdata/TestFullHelp/full_help_40_width.golden
vendored
Normal file
3
help/testdata/TestFullHelp/full_help_40_width.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
enter continue | esc back | H home
|
||||
? help ctrl+c quit
|
||||
ctrl+l log
|
||||
@ -17,7 +17,7 @@ type Item interface {
|
||||
```
|
||||
|
||||
```go
|
||||
// DefaultItem describes an items designed to work with DefaultDelegate.
|
||||
// DefaultItem describes an item designed to work with DefaultDelegate.
|
||||
type DefaultItem interface {
|
||||
Item
|
||||
Title() string
|
||||
|
||||
@ -61,7 +61,7 @@ func NewDefaultItemStyles() (s DefaultItemStyles) {
|
||||
return s
|
||||
}
|
||||
|
||||
// DefaultItem describes an items designed to work with DefaultDelegate.
|
||||
// DefaultItem describes an item designed to work with DefaultDelegate.
|
||||
type DefaultItem interface {
|
||||
Item
|
||||
Title() string
|
||||
|
||||
20
list/list.go
20
list/list.go
@ -53,6 +53,7 @@ type ItemDelegate interface {
|
||||
}
|
||||
|
||||
type filteredItem struct {
|
||||
index int // index in the unfiltered list
|
||||
item Item // item matched
|
||||
matches []int // rune indices of matched items
|
||||
}
|
||||
@ -479,12 +480,26 @@ func (m Model) MatchesForItem(index int) []int {
|
||||
return m.filteredItems[index].matches
|
||||
}
|
||||
|
||||
// Index returns the index of the currently selected item as it appears in the
|
||||
// entire slice of items.
|
||||
// Index returns the index of the currently selected item as it is stored in the
|
||||
// filtered list of items.
|
||||
// Using this value with SetItem() might be incorrect, consider using
|
||||
// GlobalIndex() instead.
|
||||
func (m Model) Index() int {
|
||||
return m.Paginator.Page*m.Paginator.PerPage + m.cursor
|
||||
}
|
||||
|
||||
// GlobalIndex returns the index of the currently selected item as it is stored
|
||||
// in the unfiltered list of items. This value can be used with SetItem().
|
||||
func (m Model) GlobalIndex() int {
|
||||
index := m.Index()
|
||||
|
||||
if m.filteredItems == nil || index >= len(m.filteredItems) {
|
||||
return index
|
||||
}
|
||||
|
||||
return m.filteredItems[index].index
|
||||
}
|
||||
|
||||
// Cursor returns the index of the cursor on the current page.
|
||||
func (m Model) Cursor() int {
|
||||
return m.cursor
|
||||
@ -1251,6 +1266,7 @@ func filterItems(m Model) tea.Cmd {
|
||||
filterMatches := []filteredItem{}
|
||||
for _, r := range m.Filter(m.FilterInput.Value(), targets) {
|
||||
filterMatches = append(filterMatches, filteredItem{
|
||||
index: r.Index,
|
||||
item: items[r.Index],
|
||||
matches: r.MatchedIndexes,
|
||||
})
|
||||
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
@ -17,17 +17,10 @@ import (
|
||||
|
||||
// Internal ID management. Used during animating to assure that frame messages
|
||||
// can only be received by progress components that sent them.
|
||||
var (
|
||||
lastID int
|
||||
idMtx sync.Mutex
|
||||
)
|
||||
var lastID int64
|
||||
|
||||
// Return the next ID we should use on the model.
|
||||
func nextID() int {
|
||||
idMtx.Lock()
|
||||
defer idMtx.Unlock()
|
||||
lastID++
|
||||
return lastID
|
||||
return int(atomic.AddInt64(&lastID, 1))
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package spinner
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
@ -10,17 +10,10 @@ import (
|
||||
|
||||
// Internal ID management. Used during animating to ensure that frame messages
|
||||
// are received only by spinner components that sent them.
|
||||
var (
|
||||
lastID int
|
||||
idMtx sync.Mutex
|
||||
)
|
||||
var lastID int64
|
||||
|
||||
// Return the next ID we should use on the Model.
|
||||
func nextID() int {
|
||||
idMtx.Lock()
|
||||
defer idMtx.Unlock()
|
||||
lastID++
|
||||
return lastID
|
||||
return int(atomic.AddInt64(&lastID, 1))
|
||||
}
|
||||
|
||||
// Spinner is a set of frames used in animating the spinner.
|
||||
|
||||
@ -2,22 +2,16 @@
|
||||
package stopwatch
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
lastID int
|
||||
idMtx sync.Mutex
|
||||
)
|
||||
var lastID int64
|
||||
|
||||
func nextID() int {
|
||||
idMtx.Lock()
|
||||
defer idMtx.Unlock()
|
||||
lastID++
|
||||
return lastID
|
||||
return int(atomic.AddInt64(&lastID, 1))
|
||||
}
|
||||
|
||||
// TickMsg is a message that is sent on every timer tick.
|
||||
|
||||
@ -218,8 +218,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.MoveUp(m.viewport.Height / 2)
|
||||
case key.Matches(msg, m.KeyMap.HalfPageDown):
|
||||
m.MoveDown(m.viewport.Height / 2)
|
||||
case key.Matches(msg, m.KeyMap.LineDown):
|
||||
m.MoveDown(1)
|
||||
case key.Matches(msg, m.KeyMap.GotoTop):
|
||||
m.GotoTop()
|
||||
case key.Matches(msg, m.KeyMap.GotoBottom):
|
||||
|
||||
@ -776,16 +776,29 @@ func (m Model) completionView(offset int) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// AvailableSuggestions returns the list of available suggestions.
|
||||
func (m *Model) AvailableSuggestions() []string {
|
||||
suggestions := make([]string, len(m.suggestions))
|
||||
for i, s := range m.suggestions {
|
||||
func (m *Model) getSuggestions(sugs [][]rune) []string {
|
||||
suggestions := make([]string, len(sugs))
|
||||
for i, s := range sugs {
|
||||
suggestions[i] = string(s)
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// AvailableSuggestions returns the list of available suggestions.
|
||||
func (m *Model) AvailableSuggestions() []string {
|
||||
return m.getSuggestions(m.suggestions)
|
||||
}
|
||||
|
||||
// MatchedSuggestions returns the list of matched suggestions.
|
||||
func (m *Model) MatchedSuggestions() []string {
|
||||
return m.getSuggestions(m.matchedSuggestions)
|
||||
}
|
||||
|
||||
// CurrentSuggestion returns the currently selected suggestion index.
|
||||
func (m *Model) CurrentSuggestionIndex() int {
|
||||
return m.currentSuggestionIndex
|
||||
}
|
||||
|
||||
// CurrentSuggestion returns the currently selected suggestion.
|
||||
func (m *Model) CurrentSuggestion() string {
|
||||
if m.currentSuggestionIndex >= len(m.matchedSuggestions) {
|
||||
|
||||
@ -2,22 +2,16 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
lastID int
|
||||
idMtx sync.Mutex
|
||||
)
|
||||
var lastID int64
|
||||
|
||||
func nextID() int {
|
||||
idMtx.Lock()
|
||||
defer idMtx.Unlock()
|
||||
lastID++
|
||||
return lastID
|
||||
return int(atomic.AddInt64(&lastID, 1))
|
||||
}
|
||||
|
||||
// Authors note with regard to start and stop commands:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user