chore: merge branch 'master' into v2-exp

This commit is contained in:
Ayman Bagabas 2024-10-21 14:16:22 -04:00
commit 287c9c4ffd
No known key found for this signature in database
GPG Key ID: C8D51D2157C919AC
18 changed files with 140 additions and 88 deletions

View File

@ -234,6 +234,12 @@ following requirements:
Thank you!
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/bubbles/contribute
## Feedback
Wed love to hear your thoughts on this project. Feel free to drop us a note!

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
View 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))
})
}
}

View File

@ -0,0 +1 @@
enter continue …

View File

@ -0,0 +1,2 @@
enter continue | esc back …
? help

View File

@ -0,0 +1,3 @@
enter continue | esc back | H home
? help ctrl+c quit
ctrl+l log

View File

@ -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

View File

@ -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

View File

@ -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,
})

View File

@ -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 (

View File

@ -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.

View File

@ -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.

View File

@ -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):

View File

@ -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) {

View File

@ -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: