2 Commits

Author SHA1 Message Date
Christian Rocha
6d6149cea8 fix(progress): update progress bar threshold method per changes in d897463 2022-03-31 15:15:59 -04:00
Christian Rocha
00d61decf4 Add minimum percent change needed to trigger an animation in progress 2022-03-31 15:15:59 -04:00
13 changed files with 108 additions and 347 deletions

View File

@@ -66,7 +66,7 @@ Supports "dot-style" pagination (similar to what you might see on iOS) and
numeric page numbering, but you could also just use this component for the numeric page numbering, but you could also just use this component for the
logic and visualize pagination however you like. logic and visualize pagination however you like.
* [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/paginator/main.go) * [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/pager/main.go)
## Viewport ## Viewport
@@ -171,8 +171,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
## Additional Bubbles ## Additional Bubbles
<!-- in alphabetical order by author -->
* [76creates/stickers](https://github.com/76creates/stickers): Responsive * [76creates/stickers](https://github.com/76creates/stickers): Responsive
flexbox and table components. flexbox and table components.
* [calyptia/go-bubble-table](https://github.com/calyptia/go-bubble-table): An * [calyptia/go-bubble-table](https://github.com/calyptia/go-bubble-table): An
@@ -183,8 +181,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
customization options. customization options.
* [evertras/bubble-table](https://github.com/Evertras/bubble-table): Interactive, * [evertras/bubble-table](https://github.com/Evertras/bubble-table): Interactive,
customizable, paginated tables. customizable, paginated tables.
* [knipferrc/teacup](https://github.com/knipferrc/teacup): Various handy
bubbles and utilities for building Bubble Tea applications.
* [mritd/bubbles](https://github.com/mritd/bubbles): Some general-purpose * [mritd/bubbles](https://github.com/mritd/bubbles): Some general-purpose
bubbles. Inputs with validation, menu selection, a modified progressbar, and bubbles. Inputs with validation, menu selection, a modified progressbar, and
so on. so on.

8
go.mod
View File

@@ -4,13 +4,13 @@ go 1.13
require ( require (
github.com/atotto/clipboard v0.1.4 github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbletea v0.21.0 github.com/charmbracelet/bubbletea v0.19.3
github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/harmonica v0.1.0
github.com/charmbracelet/lipgloss v0.5.0 github.com/charmbracelet/lipgloss v0.4.0
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.13 github.com/mattn/go-runewidth v0.0.13
github.com/muesli/reflow v0.3.0 github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 github.com/muesli/termenv v0.9.0
github.com/sahilm/fuzzy v0.1.0 github.com/sahilm/fuzzy v0.1.0
) )

42
go.sum
View File

@@ -1,33 +1,32 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI= github.com/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.1.0 h1:lFKeSd6OAckQ/CEzPVd2mqj+YMEubQ/3FM2IYY3xNm0=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.0 h1:SOpr+CfyVNce341kKqvbhhzQhBPyJRXQaCtn03Pae1Q=
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@@ -36,11 +35,10 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@@ -164,10 +164,11 @@ func (m Model) FullHelpView(groups [][]key.Binding) string {
return "" return ""
} }
// Linter note: at this time we don't think it's worth the additional
// code complexity involved in preallocating this slice.
//nolint:prealloc
var ( var (
// Linter note: at this time we don't think it's worth the additional
// code complexity involved in preallocating this slice.
//
//nolint:prealloc
out []string out []string
totalWidth int totalWidth int

View File

@@ -3,7 +3,6 @@ package list
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -87,7 +86,6 @@ type DefaultDelegate struct {
UpdateFunc func(tea.Msg, *Model) tea.Cmd UpdateFunc func(tea.Msg, *Model) tea.Cmd
ShortHelpFunc func() []key.Binding ShortHelpFunc func() []key.Binding
FullHelpFunc func() [][]key.Binding FullHelpFunc func() [][]key.Binding
height int
spacing int spacing int
} }
@@ -96,22 +94,14 @@ func NewDefaultDelegate() DefaultDelegate {
return DefaultDelegate{ return DefaultDelegate{
ShowDescription: true, ShowDescription: true,
Styles: NewDefaultItemStyles(), Styles: NewDefaultItemStyles(),
height: 2,
spacing: 1, spacing: 1,
} }
} }
// SetHeight sets delegate's preferred height.
func (d *DefaultDelegate) SetHeight(i int) {
d.height = i
}
// Height returns the delegate's preferred height. // Height returns the delegate's preferred height.
// This has effect only if ShowDescription is true,
// otherwise height is always 1.
func (d DefaultDelegate) Height() int { func (d DefaultDelegate) Height() int {
if d.ShowDescription { if d.ShowDescription {
return d.height return 2 //nolint:gomnd
} }
return 1 return 1
} }
@@ -149,23 +139,11 @@ func (d DefaultDelegate) Render(w io.Writer, m Model, index int, item Item) {
return return
} }
if m.width <= 0 {
// short-circuit
return
}
// Prevent text from exceeding list width // Prevent text from exceeding list width
textwidth := uint(m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight()) if m.width > 0 {
title = truncate.StringWithTail(title, textwidth, ellipsis) textwidth := uint(m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight())
if d.ShowDescription { title = truncate.StringWithTail(title, textwidth, ellipsis)
var lines []string desc = truncate.StringWithTail(desc, textwidth, ellipsis)
for i, line := range strings.Split(desc, "\n") {
if i >= d.height-1 {
break
}
lines = append(lines, truncate.StringWithTail(line, textwidth, ellipsis))
}
desc = strings.Join(lines, "\n")
} }
// Conditions // Conditions

View File

@@ -87,7 +87,7 @@ type Rank struct {
// DefaultFilter uses the sahilm/fuzzy to filter through the list. // DefaultFilter uses the sahilm/fuzzy to filter through the list.
// This is set by default. // This is set by default.
func DefaultFilter(term string, targets []string) []Rank { func DefaultFilter(term string, targets []string) []Rank {
var ranks = fuzzy.Find(term, targets) var ranks fuzzy.Matches = fuzzy.Find(term, targets)
sort.Stable(ranks) sort.Stable(ranks)
result := make([]Rank, len(ranks)) result := make([]Rank, len(ranks))
for i, r := range ranks { for i, r := range ranks {
@@ -129,9 +129,6 @@ type Model struct {
showHelp bool showHelp bool
filteringEnabled bool filteringEnabled bool
itemNameSingular string
itemNamePlural string
Title string Title string
Styles Styles Styles Styles
@@ -205,8 +202,6 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model {
showStatusBar: true, showStatusBar: true,
showPagination: true, showPagination: true,
showHelp: true, showHelp: true,
itemNameSingular: "item",
itemNamePlural: "items",
filteringEnabled: true, filteringEnabled: true,
KeyMap: DefaultKeyMap(), KeyMap: DefaultKeyMap(),
Filter: DefaultFilter, Filter: DefaultFilter,
@@ -291,19 +286,7 @@ func (m Model) ShowStatusBar() bool {
return m.showStatusBar return m.showStatusBar
} }
// SetStatusBarItemName defines a replacement for the items identifier. // ShowingPagination hides or shoes the paginator. Note that pagination will
// Defaults to item/items.
func (m *Model) SetStatusBarItemName(singular, plural string) {
m.itemNameSingular = singular
m.itemNamePlural = plural
}
// StatusBarItemName returns singular and plural status bar item names.
func (m Model) StatusBarItemName() (string, string) {
return m.itemNameSingular, m.itemNamePlural
}
// SetShowPagination hides or shoes the paginator. Note that pagination will
// still be active, it simply won't be displayed. // still be active, it simply won't be displayed.
func (m *Model) SetShowPagination(v bool) { func (m *Model) SetShowPagination(v bool) {
m.showPagination = v m.showPagination = v
@@ -569,7 +552,7 @@ func (m *Model) StopSpinner() {
m.showSpinner = false m.showSpinner = false
} }
// Helper for disabling the keybindings used for quitting, in case you want to // Helper for disabling the keybindings used for quitting, incase you want to
// handle this elsewhere in your application. // handle this elsewhere in your application.
func (m *Model) DisableQuitKeybindings() { func (m *Model) DisableQuitKeybindings() {
m.disableQuitKeybindings = true m.disableQuitKeybindings = true
@@ -1053,10 +1036,7 @@ func (m Model) titleView() string {
} }
} }
if len(view) > 0 { return titleBarStyle.Render(view)
return titleBarStyle.Render(view)
}
return view
} }
func (m Model) statusView() string { func (m Model) statusView() string {
@@ -1065,25 +1045,21 @@ func (m Model) statusView() string {
totalItems := len(m.items) totalItems := len(m.items)
visibleItems := len(m.VisibleItems()) visibleItems := len(m.VisibleItems())
var itemName string plural := ""
if visibleItems != 1 { if visibleItems != 1 {
itemName = m.itemNamePlural plural = "s"
} else {
itemName = m.itemNameSingular
} }
itemsDisplay := fmt.Sprintf("%d %s", visibleItems, itemName)
if m.filterState == Filtering { if m.filterState == Filtering {
// Filter results // Filter results
if visibleItems == 0 { if visibleItems == 0 {
status = m.Styles.StatusEmpty.Render("Nothing matched") status = m.Styles.StatusEmpty.Render("Nothing matched")
} else { } else {
status = itemsDisplay status = fmt.Sprintf("%d item%s", visibleItems, plural)
} }
} else if len(m.items) == 0 { } else if len(m.items) == 0 {
// Not filtering: no items. // Not filtering: no items.
status = m.Styles.StatusEmpty.Render("No " + m.itemNamePlural) status = m.Styles.StatusEmpty.Render("No items")
} else { } else {
// Normal // Normal
filtered := m.FilterState() == FilterApplied filtered := m.FilterState() == FilterApplied
@@ -1094,7 +1070,7 @@ func (m Model) statusView() string {
status += fmt.Sprintf("“%s” ", f) status += fmt.Sprintf("“%s” ", f)
} }
status += itemsDisplay status += fmt.Sprintf("%d item%s", visibleItems, plural)
} }
numFiltered := totalItems - visibleItems numFiltered := totalItems - visibleItems
@@ -1138,7 +1114,7 @@ func (m Model) populatedView() string {
if m.filterState == Filtering { if m.filterState == Filtering {
return "" return ""
} }
return m.Styles.NoItems.Render("No " + m.itemNamePlural + " found.") return m.Styles.NoItems.Render("No items found.")
} }
if len(items) > 0 { if len(items) > 0 {

View File

@@ -1,74 +0,0 @@
package list
import (
"fmt"
"io"
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
)
type item string
func (i item) FilterValue() string { return "" }
type itemDelegate struct{}
func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 0 }
func (d itemDelegate) Update(msg tea.Msg, m *Model) tea.Cmd { return nil }
func (d itemDelegate) Render(w io.Writer, m Model, index int, listItem Item) {
i, ok := listItem.(item)
if !ok {
return
}
str := fmt.Sprintf("%d. %s", index+1, i)
fmt.Fprint(w, m.Styles.TitleBar.Render(str))
}
func TestStatusBarItemName(t *testing.T) {
list := New([]Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10)
expected := "2 items"
if !strings.Contains(list.statusView(), expected) {
t.Fatalf("Error: expected view to contain %s", expected)
}
list.SetItems([]Item{item("foo")})
expected = "1 item"
if !strings.Contains(list.statusView(), expected) {
t.Fatalf("Error: expected view to contain %s", expected)
}
}
func TestStatusBarWithoutItems(t *testing.T) {
list := New([]Item{}, itemDelegate{}, 10, 10)
expected := "No items"
if !strings.Contains(list.statusView(), expected) {
t.Fatalf("Error: expected view to contain %s", expected)
}
}
func TestCustomStatusBarItemName(t *testing.T) {
list := New([]Item{item("foo"), item("bar")}, itemDelegate{}, 10, 10)
list.SetStatusBarItemName("connection", "connections")
expected := "2 connections"
if !strings.Contains(list.statusView(), expected) {
t.Fatalf("Error: expected view to contain %s", expected)
}
list.SetItems([]Item{item("foo")})
expected = "1 connection"
if !strings.Contains(list.statusView(), expected) {
t.Fatalf("Error: expected view to contain %s", expected)
}
list.SetItems([]Item{})
expected = "No connections"
if !strings.Contains(list.statusView(), expected) {
t.Fatalf("Error: expected view to contain %s", expected)
}
}

View File

@@ -31,12 +31,15 @@ func nextID() int {
} }
const ( const (
fps = 60 fps = 60
defaultWidth = 40 defaultWidth = 40
defaultFrequency = 18.0 defaultFrequency = 18.0
defaultDamping = 1.0 defaultDamping = 1.0
defaultAnimThreshold = 0.08
) )
var color func(string) termenv.Color = termenv.ColorProfile().Color
// Option is used to set options in NewModel. For example: // Option is used to set options in NewModel. For example:
// //
// progress := NewModel( // progress := NewModel(
@@ -108,10 +111,11 @@ func WithSpringOptions(frequency, damping float64) Option {
} }
} }
// WithColorProfile sets the color profile to use for the progress bar. // WithAnimationThreshold sets the percent chagne threshold necessary to
func WithColorProfile(p termenv.Profile) Option { // trigger an animated transition.
func WithAnimationThreshold(ratio float64) Option {
return func(m *Model) { return func(m *Model) {
m.colorProfile = p m.SetAnimationThreshold(ratio)
} }
} }
@@ -146,13 +150,17 @@ type Model struct {
PercentFormat string // a fmt string for a float PercentFormat string // a fmt string for a float
PercentageStyle lipgloss.Style PercentageStyle lipgloss.Style
// Members for animated transitions. // Settings for animated transitions.
spring harmonica.Spring spring harmonica.Spring
springCustomized bool springCustomized bool
percentShown float64 // percent currently displaying percentShown float64 // percent currently displaying
targetPercent float64 // percent to which we're animating targetPercent float64 // percent to which we're animating
velocity float64 velocity float64
// The amount of change required to trigger an animated transition. Should
// be a float between 0 and 1.
animThreshold float64
// Gradient settings // Gradient settings
useRamp bool useRamp bool
rampColorA colorful.Color rampColorA colorful.Color
@@ -162,9 +170,6 @@ type Model struct {
// of the progress bar. When false, the width of the gradient will be set // of the progress bar. When false, the width of the gradient will be set
// to the full width of the progress bar. // to the full width of the progress bar.
scaleRamp bool scaleRamp bool
// Color profile for the progress bar.
colorProfile termenv.Profile
} }
// New returns a model with default values. // New returns a model with default values.
@@ -178,7 +183,6 @@ func New(opts ...Option) Model {
EmptyColor: "#606060", EmptyColor: "#606060",
ShowPercentage: true, ShowPercentage: true,
PercentFormat: " %3.0f%%", PercentFormat: " %3.0f%%",
colorProfile: termenv.ColorProfile(),
} }
if !m.springCustomized { if !m.springCustomized {
m.SetSpringOptions(defaultFrequency, defaultDamping) m.SetSpringOptions(defaultFrequency, defaultDamping)
@@ -246,7 +250,14 @@ func (m Model) Percent() float64 {
// //
// If you're rendering with ViewAs you won't need this. // If you're rendering with ViewAs you won't need this.
func (m *Model) SetPercent(p float64) tea.Cmd { func (m *Model) SetPercent(p float64) tea.Cmd {
m.targetPercent = math.Max(0, math.Min(1, p)) // If the value is at or below the animation threshold, don't animate
if math.Abs(p-m.percentShown) <= m.animThreshold {
m.percentShown = asRatio(p)
m.targetPercent = asRatio(p)
return nil
}
m.targetPercent = asRatio(p)
m.tag++ m.tag++
return m.nextFrame() return m.nextFrame()
} }
@@ -267,6 +278,18 @@ func (m *Model) DecrPercent(v float64) tea.Cmd {
return m.SetPercent(m.Percent() - v) return m.SetPercent(m.Percent() - v)
} }
// SetAnimationThreshold sets the percent chagne threshold necessary to trigger
// an animated transition.
func (m *Model) SetAnimationThreshold(v float64) {
m.animThreshold = asRatio(v)
}
// AnimationThreshold returns the percent change necessary to trigger an
// animated transition.
func (m *Model) AnimationThreshold() float64 {
return m.animThreshold
}
// View renders the an animated progress bar in its current state. To render // View renders the an animated progress bar in its current state. To render
// a static progress bar based on your own calculations use ViewAs instead. // a static progress bar based on your own calculations use ViewAs instead.
func (m Model) View() string { func (m Model) View() string {
@@ -308,18 +331,18 @@ func (m Model) barView(b *strings.Builder, percent float64, textWidth int) {
c := m.rampColorA.BlendLuv(m.rampColorB, p).Hex() c := m.rampColorA.BlendLuv(m.rampColorB, p).Hex()
b.WriteString(termenv. b.WriteString(termenv.
String(string(m.Full)). String(string(m.Full)).
Foreground(m.color(c)). Foreground(color(c)).
String(), String(),
) )
} }
} else { } else {
// Solid fill // Solid fill
s := termenv.String(string(m.Full)).Foreground(m.color(m.FullColor)).String() s := termenv.String(string(m.Full)).Foreground(color(m.FullColor)).String()
b.WriteString(strings.Repeat(s, fw)) b.WriteString(strings.Repeat(s, fw))
} }
// Empty fill // Empty fill
e := termenv.String(string(m.Empty)).Foreground(m.color(m.EmptyColor)).String() e := termenv.String(string(m.Empty)).Foreground(color(m.EmptyColor)).String()
n := max(0, tw-fw) n := max(0, tw-fw)
b.WriteString(strings.Repeat(e, n)) b.WriteString(strings.Repeat(e, n))
} }
@@ -347,10 +370,6 @@ func (m *Model) setRamp(colorA, colorB string, scaled bool) {
m.rampColorB = b m.rampColorB = b
} }
func (m Model) color(c string) termenv.Color {
return m.colorProfile.Color(c)
}
func max(a, b int) int { func max(a, b int) int {
if a > b { if a > b {
return a return a
@@ -364,3 +383,7 @@ func min(a, b int) int {
} }
return b return b
} }
func asRatio(v float64) float64 {
return math.Max(math.Min(v, 1), 0)
}

View File

@@ -8,8 +8,8 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
// Internal ID management. Used during animating to ensure that frame messages // Internal ID management for text inputs. Necessary for blink integrity when
// are received only by spinner components that sent them. // multiple text inputs are involved.
var ( var (
lastID int lastID int
idMtx sync.Mutex idMtx sync.Mutex
@@ -67,27 +67,12 @@ var (
Frames: []string{"🙈", "🙉", "🙊"}, Frames: []string{"🙈", "🙉", "🙊"},
FPS: time.Second / 3, //nolint:gomnd FPS: time.Second / 3, //nolint:gomnd
} }
Meter = Spinner{
Frames: []string{
"▱▱▱",
"▰▱▱",
"▰▰▱",
"▰▰▰",
"▰▰▱",
"▰▱▱",
"▱▱▱",
},
FPS: time.Second / 7, //nolint:gomnd
}
Hamburger = Spinner{
Frames: []string{"☱", "☲", "☴", "☲"},
FPS: time.Second / 3, //nolint:gomnd
}
) )
// Model contains the state for the spinner. Use NewModel to create new models // Model contains the state for the spinner. Use NewModel to create new models
// rather than using Model as a struct literal. // rather than using Model as a struct literal.
type Model struct { type Model struct {
// Spinner settings to use. See type Spinner. // Spinner settings to use. See type Spinner.
Spinner Spinner Spinner Spinner
@@ -98,9 +83,10 @@ type Model struct {
// https://github.com/charmbracelet/lipgloss // https://github.com/charmbracelet/lipgloss
Style lipgloss.Style Style lipgloss.Style
frame int frame int
id int startTime time.Time
tag int id int
tag int
} }
// ID returns the spinner's unique ID. // ID returns the spinner's unique ID.
@@ -109,17 +95,11 @@ func (m Model) ID() int {
} }
// New returns a model with default values. // New returns a model with default values.
func New(opts ...Option) Model { func New() Model {
m := Model{ return Model{
Spinner: Line, Spinner: Line,
id: nextID(), id: nextID(),
} }
for _, opt := range opts {
opt(&m)
}
return m
} }
// NewModel returns a model with default values. // NewModel returns a model with default values.
@@ -134,7 +114,9 @@ type TickMsg struct {
ID int ID int
} }
// Update is the Tea update function. // Update is the Tea update function. This will advance the spinner one frame
// every time it's called, regardless the message passed, so be sure the logic
// is setup so as not to call this Update needlessly.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case TickMsg: case TickMsg:
@@ -205,23 +187,3 @@ func (m Model) tick(id, tag int) tea.Cmd {
func Tick() tea.Msg { func Tick() tea.Msg {
return TickMsg{Time: time.Now()} return TickMsg{Time: time.Now()}
} }
// Option is used to set options in New. For example:
//
// spinner := New(WithSpinner(Dot))
//
type Option func(*Model)
// WithSpinner is an option to set the spinner.
func WithSpinner(spinner Spinner) Option {
return func(m *Model) {
m.Spinner = spinner
}
}
// WithStyle is an option to set the spinner style.
func WithStyle(style lipgloss.Style) Option {
return func(m *Model) {
m.Style = style
}
}

View File

@@ -1,61 +0,0 @@
package spinner_test
import (
"testing"
"github.com/charmbracelet/bubbles/spinner"
)
func TestSpinnerNew(t *testing.T) {
assertEqualSpinner := func(t *testing.T, exp, got spinner.Spinner) {
t.Helper()
if exp.FPS != got.FPS {
t.Errorf("expecting %d FPS, got %d", exp.FPS, got.FPS)
}
if e, g := len(exp.Frames), len(got.Frames); e != g {
t.Fatalf("expecting %d frames, got %d", e, g)
}
for i, e := range exp.Frames {
if g := got.Frames[i]; e != g {
t.Errorf("expecting frame index %d with value %q, got %q", i, e, g)
}
}
}
t.Run("default", func(t *testing.T) {
s := spinner.New()
assertEqualSpinner(t, spinner.Line, s.Spinner)
})
t.Run("WithSpinner", func(t *testing.T) {
customSpinner := spinner.Spinner{
Frames: []string{"a", "b", "c", "d"},
FPS: 16,
}
s := spinner.New(spinner.WithSpinner(customSpinner))
assertEqualSpinner(t, customSpinner, s.Spinner)
})
tests := map[string]spinner.Spinner{
"Line": spinner.Line,
"Dot": spinner.Dot,
"MiniDot": spinner.MiniDot,
"Jump": spinner.Jump,
"Pulse": spinner.Pulse,
"Points": spinner.Points,
"Globe": spinner.Globe,
"Moon": spinner.Moon,
"Monkey": spinner.Monkey,
}
for name, s := range tests {
t.Run(name, func(t *testing.T) {
assertEqualSpinner(t, spinner.New(spinner.WithSpinner(s)).Spinner, s)
})
}
}

View File

@@ -91,9 +91,6 @@ func (c CursorMode) String() string {
}[c] }[c]
} }
// ValidateFunc is a function that returns an error if the input is invalid.
type ValidateFunc func(string) error
// Model is the Bubble Tea model for this text input element. // Model is the Bubble Tea model for this text input element.
type Model struct { type Model struct {
Err error Err error
@@ -153,15 +150,9 @@ type Model struct {
// cursorMode determines the behavior of the cursor // cursorMode determines the behavior of the cursor
cursorMode CursorMode cursorMode CursorMode
// Validate is a function that checks whether or not the text within the
// input is valid. If it is not valid, the `Err` field will be set to the
// error returned by the function. If the function is not defined, all
// input is considered valid.
Validate ValidateFunc
} }
// New creates a new model with default settings. // NewModel creates a new model with default settings.
func New() Model { func New() Model {
return Model{ return Model{
Prompt: "> ", Prompt: "> ",
@@ -190,15 +181,6 @@ var NewModel = New
// SetValue sets the value of the text input. // SetValue sets the value of the text input.
func (m *Model) SetValue(s string) { func (m *Model) SetValue(s string) {
if m.Validate != nil {
if err := m.Validate(s); err != nil {
m.Err = err
return
}
}
m.Err = nil
runes := []rune(s) runes := []rune(s)
if m.CharLimit > 0 && len(runes) > m.CharLimit { if m.CharLimit > 0 && len(runes) > m.CharLimit {
m.value = runes[:m.CharLimit] m.value = runes[:m.CharLimit]
@@ -268,7 +250,7 @@ func (m Model) CursorMode() CursorMode {
return m.cursorMode return m.cursorMode
} }
// SetCursorMode sets the model's cursor mode. This method returns a command. // CursorMode sets the model's cursor mode. This method returns a command.
// //
// For available cursor modes, see type CursorMode. // For available cursor modes, see type CursorMode.
func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd { func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
@@ -344,8 +326,6 @@ func (m *Model) handlePaste(v string) bool {
tail := make([]rune, len(tailSrc)) tail := make([]rune, len(tailSrc))
copy(tail, tailSrc) copy(tail, tailSrc)
oldPos := m.pos
// Insert pasted runes // Insert pasted runes
for _, r := range paste { for _, r := range paste {
head = append(head, r) head = append(head, r)
@@ -359,12 +339,7 @@ func (m *Model) handlePaste(v string) bool {
} }
// Put it all back together // Put it all back together
value := append(head, tail...) m.value = append(head, tail...)
m.SetValue(string(value))
if m.Err != nil {
m.pos = oldPos
}
// Reset blink state if necessary and run overflow checks // Reset blink state if necessary and run overflow checks
return m.setCursor(m.pos) return m.setCursor(m.pos)
@@ -612,8 +587,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyBackspace: // delete character before cursor case tea.KeyBackspace: // delete character before cursor
m.Err = nil
if msg.Alt { if msg.Alt {
resetBlink = m.deleteWordLeft() resetBlink = m.deleteWordLeft()
} else { } else {
@@ -656,7 +629,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
resetBlink = m.deleteBeforeCursor() resetBlink = m.deleteBeforeCursor()
case tea.KeyCtrlV: // ^V paste case tea.KeyCtrlV: // ^V paste
return m, Paste return m, Paste
case tea.KeyRunes, tea.KeySpace: // input regular characters case tea.KeyRunes: // input regular characters
if msg.Alt && len(msg.Runes) == 1 { if msg.Alt && len(msg.Runes) == 1 {
if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor
resetBlink = m.deleteWordRight() resetBlink = m.deleteWordRight()
@@ -674,15 +647,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// Input a regular character // Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit { if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
runes := msg.Runes m.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...)
resetBlink = m.setCursor(m.pos + len(msg.Runes))
value := make([]rune, len(m.value))
copy(value, m.value)
value = append(value[:m.pos], append(runes, value[m.pos:]...)...)
m.SetValue(string(value))
if m.Err == nil {
resetBlink = m.setCursor(m.pos + len(runes))
}
} }
} }

View File

@@ -162,7 +162,9 @@ func (m *Model) Start() tea.Cmd {
// Stop pauses the timer. Has no effect if the timer has timed out. // Stop pauses the timer. Has no effect if the timer has timed out.
func (m *Model) Stop() tea.Cmd { func (m *Model) Stop() tea.Cmd {
return m.startStop(false) return func() tea.Msg {
return m.startStop(false)
}
} }
// Toggle stops the timer if it's running and starts it if it's stopped. // Toggle stops the timer if it's running and starts it if it's stopped.

View File

@@ -22,27 +22,21 @@ func DefaultKeyMap() KeyMap {
return KeyMap{ return KeyMap{
PageDown: key.NewBinding( PageDown: key.NewBinding(
key.WithKeys("pgdown", spacebar, "f"), key.WithKeys("pgdown", spacebar, "f"),
key.WithHelp("f/pgdn", "page down"),
), ),
PageUp: key.NewBinding( PageUp: key.NewBinding(
key.WithKeys("pgup", "b"), key.WithKeys("pgup", "b"),
key.WithHelp("b/pgup", "page up"),
), ),
HalfPageUp: key.NewBinding( HalfPageUp: key.NewBinding(
key.WithKeys("u", "ctrl+u"), key.WithKeys("u", "ctrl+u"),
key.WithHelp("u", "½ page up"),
), ),
HalfPageDown: key.NewBinding( HalfPageDown: key.NewBinding(
key.WithKeys("d", "ctrl+d"), key.WithKeys("d", "ctrl+d"),
key.WithHelp("d", "½ page down"),
), ),
Up: key.NewBinding( Up: key.NewBinding(
key.WithKeys("up", "k"), key.WithKeys("up", "k"),
key.WithHelp("↑/k", "up"),
), ),
Down: key.NewBinding( Down: key.NewBinding(
key.WithKeys("down", "j"), key.WithKeys("down", "j"),
key.WithHelp("↓/j", "down"),
), ),
} }
} }