17 Commits

Author SHA1 Message Date
Charlie Roth
42f85b4a1b docs: fix paginator example link (#177) 2022-06-21 09:23:57 -04:00
vzvu3k6k
4d0a0ea9d8 docs(spinner): correct comment about internal ID (#171) 2022-06-21 09:17:48 -04:00
Weslei Juan Novaes Pereira
658a4febc7 feat: new Hamburger + Meter spinners (#172)
Co-authored-by: Maas Lalani <maas@lalani.dev>
2022-06-21 08:59:23 -04:00
Maas Lalani
93e464296e docs(list): fix linting errors 2022-06-16 18:47:55 -04:00
Weslei Juan Novaes Pereira
57d79daf4d feat(list): ability to SetStatusBarItemName (#169) 2022-06-16 18:14:47 -04:00
IllusionMan1212
e57fd292cc feat: added Validate function for textinput 2022-06-10 12:17:24 -04:00
Hironao OTSUBO
54869f7a1d docs(spinner): remove obsolete comment (#168)
The doc for spinner.Model.Update is obsolete as per 35c3cd626d,
which made Update aware of Msg's type.
2022-06-10 10:03:55 -04:00
Ayman Bagabas
7959eb4867 fix(progress): set a custom termenv color profile (#152) 2022-06-01 18:04:33 -07:00
Christian Rocha
fd03b6195d chore: bump bubbletea, harmonica, lipgloss and termenv deps 2022-06-01 17:53:14 -07:00
Christian Rocha
a1e1b461b6 fix(textinput): support KeySpace in both present and future Bubble Tea versions
Closes #144
2022-05-29 08:25:34 -07:00
Carlos Alexandro Becker
cd2593cfb7 feat: allow to set the height of the item (#155)
* feat: allow to set the height of the item

The user might want to show more than 2 lines, and, right now, if they
try to do so, things break.

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>

* fix: short-circuit if width <= 0

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>

* fix: height check

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
2022-05-24 15:00:35 -03:00
Tim Adler
e1871db6d3 fix: padding in titlebar when nothing is displayed (#139)
* Don't render anything, when nothing is shown

* fix linter issue
2022-05-24 09:05:39 -03:00
Christian Rocha
2a8d463bd1 chore: shorten half page up/down help widths 2022-04-29 12:20:18 -04:00
Ayman Bagabas
c214837839 fix: add viewport keymap help 2022-04-29 12:20:18 -04:00
Ayman Bagabas
f5ac64216b fix: lint warn 2022-04-29 12:20:18 -04:00
Christian Rocha
292a1dd7ba fix(spinner): remove unused member in model 2022-04-12 10:12:14 -04:00
Christian Rocha
154cdbc53a docs: add Tyler's Teacup library to resources 2022-04-12 10:11:59 -04:00
11 changed files with 258 additions and 104 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/pager/main.go) * [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/paginator/main.go)
## Viewport ## Viewport
@@ -171,6 +171,8 @@ 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
@@ -181,6 +183,8 @@ 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.19.3 github.com/charmbracelet/bubbletea v0.21.0
github.com/charmbracelet/harmonica v0.1.0 github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/lipgloss v0.4.0 github.com/charmbracelet/lipgloss v0.5.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.9.0 github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
github.com/sahilm/fuzzy v0.1.0 github.com/sahilm/fuzzy v0.1.0
) )

42
go.sum
View File

@@ -1,32 +1,33 @@
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.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw= github.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI=
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/harmonica v0.1.0 h1:lFKeSd6OAckQ/CEzPVd2mqj+YMEubQ/3FM2IYY3xNm0= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
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.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
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=
@@ -35,10 +36,11 @@ 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/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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,11 +164,10 @@ func (m Model) FullHelpView(groups [][]key.Binding) string {
return "" return ""
} }
var (
// Linter note: at this time we don't think it's worth the additional // Linter note: at this time we don't think it's worth the additional
// code complexity involved in preallocating this slice. // code complexity involved in preallocating this slice.
//
//nolint:prealloc //nolint:prealloc
var (
out []string out []string
totalWidth int totalWidth int

View File

@@ -3,6 +3,7 @@ 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"
@@ -86,6 +87,7 @@ 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
} }
@@ -94,14 +96,22 @@ 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 2 //nolint:gomnd return d.height
} }
return 1 return 1
} }
@@ -139,11 +149,23 @@ 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
if m.width > 0 {
textwidth := uint(m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight()) textwidth := uint(m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight())
title = truncate.StringWithTail(title, textwidth, ellipsis) title = truncate.StringWithTail(title, textwidth, ellipsis)
desc = truncate.StringWithTail(desc, textwidth, ellipsis) if d.ShowDescription {
var lines []string
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.Matches = fuzzy.Find(term, targets) var ranks = 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,6 +129,9 @@ type Model struct {
showHelp bool showHelp bool
filteringEnabled bool filteringEnabled bool
itemNameSingular string
itemNamePlural string
Title string Title string
Styles Styles Styles Styles
@@ -202,6 +205,8 @@ 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,
@@ -286,7 +291,19 @@ func (m Model) ShowStatusBar() bool {
return m.showStatusBar return m.showStatusBar
} }
// ShowingPagination hides or shoes the paginator. Note that pagination will // SetStatusBarItemName defines a replacement for the items identifier.
// 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
@@ -552,7 +569,7 @@ func (m *Model) StopSpinner() {
m.showSpinner = false m.showSpinner = false
} }
// Helper for disabling the keybindings used for quitting, incase you want to // Helper for disabling the keybindings used for quitting, in case 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
@@ -1036,7 +1053,10 @@ 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 {
@@ -1045,21 +1065,25 @@ func (m Model) statusView() string {
totalItems := len(m.items) totalItems := len(m.items)
visibleItems := len(m.VisibleItems()) visibleItems := len(m.VisibleItems())
plural := "" var itemName string
if visibleItems != 1 { if visibleItems != 1 {
plural = "s" itemName = m.itemNamePlural
} 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 = fmt.Sprintf("%d item%s", visibleItems, plural) status = itemsDisplay
} }
} 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 items") status = m.Styles.StatusEmpty.Render("No " + m.itemNamePlural)
} else { } else {
// Normal // Normal
filtered := m.FilterState() == FilterApplied filtered := m.FilterState() == FilterApplied
@@ -1070,7 +1094,7 @@ func (m Model) statusView() string {
status += fmt.Sprintf("“%s” ", f) status += fmt.Sprintf("“%s” ", f)
} }
status += fmt.Sprintf("%d item%s", visibleItems, plural) status += itemsDisplay
} }
numFiltered := totalItems - visibleItems numFiltered := totalItems - visibleItems
@@ -1114,7 +1138,7 @@ func (m Model) populatedView() string {
if m.filterState == Filtering { if m.filterState == Filtering {
return "" return ""
} }
return m.Styles.NoItems.Render("No items found.") return m.Styles.NoItems.Render("No " + m.itemNamePlural + " found.")
} }
if len(items) > 0 { if len(items) > 0 {

74
list/list_test.go Normal file
View File

@@ -0,0 +1,74 @@
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

@@ -35,11 +35,8 @@ const (
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(
@@ -111,11 +108,10 @@ func WithSpringOptions(frequency, damping float64) Option {
} }
} }
// WithAnimationThreshold sets the percent chagne threshold necessary to // WithColorProfile sets the color profile to use for the progress bar.
// trigger an animated transition. func WithColorProfile(p termenv.Profile) Option {
func WithAnimationThreshold(ratio float64) Option {
return func(m *Model) { return func(m *Model) {
m.SetAnimationThreshold(ratio) m.colorProfile = p
} }
} }
@@ -150,17 +146,13 @@ 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
// Settings for animated transitions. // Members 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
@@ -170,6 +162,9 @@ 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.
@@ -183,6 +178,7 @@ 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)
@@ -250,14 +246,7 @@ 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 {
// If the value is at or below the animation threshold, don't animate m.targetPercent = math.Max(0, math.Min(1, p))
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()
} }
@@ -278,18 +267,6 @@ 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 {
@@ -331,18 +308,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(color(c)). Foreground(m.color(c)).
String(), String(),
) )
} }
} else { } else {
// Solid fill // Solid fill
s := termenv.String(string(m.Full)).Foreground(color(m.FullColor)).String() s := termenv.String(string(m.Full)).Foreground(m.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(color(m.EmptyColor)).String() e := termenv.String(string(m.Empty)).Foreground(m.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))
} }
@@ -370,6 +347,10 @@ 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
@@ -383,7 +364,3 @@ 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 for text inputs. Necessary for blink integrity when // Internal ID management. Used during animating to ensure that frame messages
// multiple text inputs are involved. // are received only by spinner components that sent them.
var ( var (
lastID int lastID int
idMtx sync.Mutex idMtx sync.Mutex
@@ -67,12 +67,27 @@ 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
@@ -84,7 +99,6 @@ type Model struct {
Style lipgloss.Style Style lipgloss.Style
frame int frame int
startTime time.Time
id int id int
tag int tag int
} }
@@ -114,9 +128,7 @@ type TickMsg struct {
ID int ID int
} }
// Update is the Tea update function. This will advance the spinner one frame // Update is the Tea update function.
// 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:

View File

@@ -91,6 +91,9 @@ 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
@@ -150,9 +153,15 @@ 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
} }
// NewModel creates a new model with default settings. // New creates a new model with default settings.
func New() Model { func New() Model {
return Model{ return Model{
Prompt: "> ", Prompt: "> ",
@@ -181,13 +190,22 @@ 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]
} else { } else {
m.value = runes m.value = runes
} }
if m.pos == 0 || m.pos > len(m.value) { if (m.pos == 0 && len(m.value) == 0) || m.pos > len(m.value) {
m.setCursor(len(m.value)) m.setCursor(len(m.value))
} }
m.handleOverflow() m.handleOverflow()
@@ -250,7 +268,7 @@ func (m Model) CursorMode() CursorMode {
return m.cursorMode return m.cursorMode
} }
// CursorMode sets the model's cursor mode. This method returns a command. // SetCursorMode 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 {
@@ -326,6 +344,8 @@ 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)
@@ -339,7 +359,12 @@ func (m *Model) handlePaste(v string) bool {
} }
// Put it all back together // Put it all back together
m.value = append(head, tail...) 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)
@@ -587,6 +612,8 @@ 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 {
@@ -629,7 +656,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: // input regular characters case tea.KeyRunes, tea.KeySpace: // 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()
@@ -647,8 +674,15 @@ 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 {
m.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...) runes := msg.Runes
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

@@ -22,21 +22,27 @@ 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"),
), ),
} }
} }