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 256 additions and 66 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 ""
} }
// 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,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) if d.ShowDescription {
desc = truncate.StringWithTail(desc, textwidth, ellipsis) 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 {
} }
} }
return titleBarStyle.Render(view) if len(view) > 0 {
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

@@ -37,8 +37,6 @@ const (
defaultDamping = 1.0 defaultDamping = 1.0
) )
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(
@@ -110,6 +108,13 @@ func WithSpringOptions(frequency, damping float64) Option {
} }
} }
// WithColorProfile sets the color profile to use for the progress bar.
func WithColorProfile(p termenv.Profile) Option {
return func(m *Model) {
m.colorProfile = p
}
}
// FrameMsg indicates that an animation step should occur. // FrameMsg indicates that an animation step should occur.
type FrameMsg struct { type FrameMsg struct {
id int id int
@@ -157,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.
@@ -170,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)
@@ -299,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))
} }
@@ -338,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

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
@@ -83,10 +98,9 @@ type Model struct {
// https://github.com/charmbracelet/lipgloss // https://github.com/charmbracelet/lipgloss
Style lipgloss.Style Style lipgloss.Style
frame int frame int
startTime time.Time id int
id int tag int
tag int
} }
// ID returns the spinner's unique ID. // ID returns the spinner's unique ID.
@@ -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"),
), ),
} }
} }