mirror of
https://github.com/Maks1mS/bubbles.git
synced 2025-10-18 16:38:56 +03:00
Compare commits
12 Commits
v0.10.3
...
progress-a
Author | SHA1 | Date | |
---|---|---|---|
|
6d6149cea8 | ||
|
00d61decf4 | ||
|
430b7b5d36 | ||
|
00ec90b59f | ||
|
aa0744fd8d | ||
|
cf1fe5f9ce | ||
|
6c18900279 | ||
|
d897463138 | ||
|
88562515cf | ||
|
e349920524 | ||
|
64b9e0582f | ||
|
057f7b9a4d |
24
README.md
24
README.md
@@ -171,18 +171,24 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
## Additional Bubbles
|
||||
|
||||
* [promptkit](https://github.com/erikgeiser/promptkit): A collection of common
|
||||
prompts for cases like selection, text input, and confirmation. Each prompt
|
||||
comes with sensible defaults, remappable keybindings, any many customization
|
||||
options.
|
||||
* [76creates/stickers](https://github.com/76creates/stickers): Responsive
|
||||
flexbox and table components.
|
||||
* [calyptia/go-bubble-table](https://github.com/calyptia/go-bubble-table): An
|
||||
interactive, customizable, scrollable table component.
|
||||
* [erikgeiser/promptkit](https://github.com/erikgeiser/promptkit): A collection
|
||||
of common prompts for cases like selection, text input, and confirmation.
|
||||
Each prompt comes with sensible defaults, remappable keybindings, any many
|
||||
customization options.
|
||||
* [evertras/bubble-table](https://github.com/Evertras/bubble-table): Interactive,
|
||||
customizable, paginated tables.
|
||||
* [mritd/bubbles](https://github.com/mritd/bubbles): Some general-purpose
|
||||
bubbles. Inputs with validation, menu selection, a modified progressbar, and
|
||||
so on.
|
||||
* [bubblelister](https://github.com/treilik/bubblelister): An alternate list
|
||||
that is scrollable without pagination and has the ability to contain other
|
||||
bubbles as list items.
|
||||
* [bubbleboxer](https://github.com/treilik/bubbleboxer): Layout multiple bubbles
|
||||
side-by-side in a layout-tree.
|
||||
* [treilik/bubbleboxer](https://github.com/treilik/bubbleboxer): Layout
|
||||
multiple bubbles side-by-side in a layout-tree.
|
||||
* [treilik/bubblelister](https://github.com/treilik/bubblelister): An alternate
|
||||
list that is scrollable without pagination and has the ability to contain
|
||||
other bubbles as list items.
|
||||
|
||||
If you’ve built a Bubble you think should be listed here,
|
||||
[let us know](mailto:vt100@charm.sh).
|
||||
|
39
list/list.go
39
list/list.go
@@ -71,6 +71,34 @@ func (f filteredItems) items() []Item {
|
||||
// message should be routed to Update for processing.
|
||||
type FilterMatchesMsg []filteredItem
|
||||
|
||||
// FilterFunc takes a term and a list of strings to search through
|
||||
// (defined by Item#FilterValue).
|
||||
// It should return a sorted list of ranks.
|
||||
type FilterFunc func(string, []string) []Rank
|
||||
|
||||
// Rank defines a rank for a given item.
|
||||
type Rank struct {
|
||||
// The index of the item in the original input.
|
||||
Index int
|
||||
// Indices of the actual word that were matched against the filter term.
|
||||
MatchedIndexes []int
|
||||
}
|
||||
|
||||
// DefaultFilter uses the sahilm/fuzzy to filter through the list.
|
||||
// This is set by default.
|
||||
func DefaultFilter(term string, targets []string) []Rank {
|
||||
var ranks fuzzy.Matches = fuzzy.Find(term, targets)
|
||||
sort.Stable(ranks)
|
||||
result := make([]Rank, len(ranks))
|
||||
for i, r := range ranks {
|
||||
result[i] = Rank{
|
||||
Index: r.Index,
|
||||
MatchedIndexes: r.MatchedIndexes,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type statusMessageTimeoutMsg struct{}
|
||||
|
||||
// FilterState describes the current filtering state on the model.
|
||||
@@ -107,6 +135,9 @@ type Model struct {
|
||||
// Key mappings for navigating the list.
|
||||
KeyMap KeyMap
|
||||
|
||||
// Filter is used to filter the list.
|
||||
Filter FilterFunc
|
||||
|
||||
disableQuitKeybindings bool
|
||||
|
||||
// Additional key mappings for the short and full help views. This allows
|
||||
@@ -173,6 +204,7 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model {
|
||||
showHelp: true,
|
||||
filteringEnabled: true,
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Filter: DefaultFilter,
|
||||
Styles: styles,
|
||||
Title: "List",
|
||||
FilterInput: filterInput,
|
||||
@@ -1082,7 +1114,7 @@ func (m Model) populatedView() string {
|
||||
if m.filterState == Filtering {
|
||||
return ""
|
||||
}
|
||||
m.Styles.NoItems.Render("No items found.")
|
||||
return m.Styles.NoItems.Render("No items found.")
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
@@ -1133,11 +1165,8 @@ func filterItems(m Model) tea.Cmd {
|
||||
targets = append(targets, t.FilterValue())
|
||||
}
|
||||
|
||||
var ranks fuzzy.Matches = fuzzy.Find(m.FilterInput.Value(), targets)
|
||||
sort.Stable(ranks)
|
||||
|
||||
filterMatches := []filteredItem{}
|
||||
for _, r := range ranks {
|
||||
for _, r := range m.Filter(m.FilterInput.Value(), targets) {
|
||||
filterMatches = append(filterMatches, filteredItem{
|
||||
item: items[r.Index],
|
||||
matches: r.MatchedIndexes,
|
||||
|
@@ -31,10 +31,11 @@ func nextID() int {
|
||||
}
|
||||
|
||||
const (
|
||||
fps = 60
|
||||
defaultWidth = 40
|
||||
defaultFrequency = 18.0
|
||||
defaultDamping = 1.0
|
||||
fps = 60
|
||||
defaultWidth = 40
|
||||
defaultFrequency = 18.0
|
||||
defaultDamping = 1.0
|
||||
defaultAnimThreshold = 0.08
|
||||
)
|
||||
|
||||
var color func(string) termenv.Color = termenv.ColorProfile().Color
|
||||
@@ -110,6 +111,14 @@ func WithSpringOptions(frequency, damping float64) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithAnimationThreshold sets the percent chagne threshold necessary to
|
||||
// trigger an animated transition.
|
||||
func WithAnimationThreshold(ratio float64) Option {
|
||||
return func(m *Model) {
|
||||
m.SetAnimationThreshold(ratio)
|
||||
}
|
||||
}
|
||||
|
||||
// FrameMsg indicates that an animation step should occur.
|
||||
type FrameMsg struct {
|
||||
id int
|
||||
@@ -141,13 +150,17 @@ type Model struct {
|
||||
PercentFormat string // a fmt string for a float
|
||||
PercentageStyle lipgloss.Style
|
||||
|
||||
// Members for animated transitions.
|
||||
// Settings for animated transitions.
|
||||
spring harmonica.Spring
|
||||
springCustomized bool
|
||||
percent float64
|
||||
targetPercent float64
|
||||
percentShown float64 // percent currently displaying
|
||||
targetPercent float64 // percent to which we're animating
|
||||
velocity float64
|
||||
|
||||
// The amount of change required to trigger an animated transition. Should
|
||||
// be a float between 0 and 1.
|
||||
animThreshold float64
|
||||
|
||||
// Gradient settings
|
||||
useRamp bool
|
||||
rampColorA colorful.Color
|
||||
@@ -203,12 +216,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// If we've more or less reached equilibrium, stop updating.
|
||||
dist := math.Abs(m.percent - m.targetPercent)
|
||||
dist := math.Abs(m.percentShown - m.targetPercent)
|
||||
if dist < 0.001 && m.velocity < 0.01 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.percent, m.velocity = m.spring.Update(m.percent, m.velocity, m.targetPercent)
|
||||
m.percentShown, m.velocity = m.spring.Update(m.percentShown, m.velocity, m.targetPercent)
|
||||
return m, m.nextFrame()
|
||||
|
||||
default:
|
||||
@@ -224,7 +237,7 @@ func (m *Model) SetSpringOptions(frequency, damping float64) {
|
||||
m.spring = harmonica.NewSpring(harmonica.FPS(fps), frequency, damping)
|
||||
}
|
||||
|
||||
// Percent returns the current percentage state of the model. This is only
|
||||
// Percent returns the current visible percentage on the model. This is only
|
||||
// relevant when you're animating the progress bar.
|
||||
//
|
||||
// If you're rendering with ViewAs you won't need this.
|
||||
@@ -237,7 +250,14 @@ func (m Model) Percent() float64 {
|
||||
//
|
||||
// If you're rendering with ViewAs you won't need this.
|
||||
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++
|
||||
return m.nextFrame()
|
||||
}
|
||||
@@ -258,10 +278,22 @@ func (m *Model) DecrPercent(v float64) tea.Cmd {
|
||||
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
|
||||
// a static progress bar based on your own calculations use ViewAs instead.
|
||||
func (m Model) View() string {
|
||||
return m.ViewAs(m.percent)
|
||||
return m.ViewAs(m.percentShown)
|
||||
}
|
||||
|
||||
// ViewAs renders the progress bar with a given percentage.
|
||||
@@ -351,3 +383,7 @@ func min(a, b int) int {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func asRatio(v float64) float64 {
|
||||
return math.Max(math.Min(v, 1), 0)
|
||||
}
|
||||
|
@@ -1,13 +1,11 @@
|
||||
package spinner
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/reflow/ansi"
|
||||
)
|
||||
|
||||
// Internal ID management for text inputs. Necessary for blink integrity when
|
||||
@@ -85,114 +83,17 @@ type Model struct {
|
||||
// https://github.com/charmbracelet/lipgloss
|
||||
Style lipgloss.Style
|
||||
|
||||
// MinimumLifetime is the minimum amount of time the spinner can run. Any
|
||||
// logic around this can be implemented in view that implements this
|
||||
// spinner. If HideFor is set MinimumLifetime will be added on top of
|
||||
// HideFor. In other words, if HideFor is 100ms and MinimumLifetime is
|
||||
// 200ms then MinimumLifetime will expire after 300ms.
|
||||
//
|
||||
// MinimumLifetime is optional.
|
||||
//
|
||||
// This is considered experimental and may not appear in future versions of
|
||||
// this library.
|
||||
MinimumLifetime time.Duration
|
||||
|
||||
// HideFor can be used to wait to show the spinner until a certain amount
|
||||
// of time has passed. This can be useful for preventing flicking when load
|
||||
// times are very fast.
|
||||
// Optional.
|
||||
//
|
||||
// This is considered experimental and may not appear in future versions of
|
||||
// this library.
|
||||
HideFor time.Duration
|
||||
|
||||
frame int
|
||||
startTime time.Time
|
||||
id int
|
||||
tag int
|
||||
}
|
||||
|
||||
// Start resets resets the spinner start time. For use with MinimumLifetime and
|
||||
// MinimumStartTime. Optional.
|
||||
//
|
||||
// This function is optional and generally considered for advanced use only.
|
||||
// Most of the time your application logic will obviate the need for this
|
||||
// method.
|
||||
//
|
||||
// This is considered experimental and may not appear in future versions of
|
||||
// this library.
|
||||
func (m *Model) Start() {
|
||||
m.startTime = time.Now()
|
||||
}
|
||||
|
||||
// Finish sets the internal timer to a completed state so long as the spinner
|
||||
// isn't flagged to be showing. If it is showing, finish has no effect. The
|
||||
// idea here is that you call Finish if your operation has completed and, if
|
||||
// the spinner isn't showing yet (by virtue of HideFor) then Visible() doesn't
|
||||
// show the spinner at all.
|
||||
//
|
||||
// This is intended to work in conjunction with MinimumLifetime and
|
||||
// MinimumStartTime, is completely optional.
|
||||
//
|
||||
// This function is optional and generally considered for advanced use only.
|
||||
// Most of the time your application logic will obviate the need for this
|
||||
// method.
|
||||
//
|
||||
// This is considered experimental and may not appear in future versions of
|
||||
// this library.
|
||||
func (m *Model) Finish() {
|
||||
if m.hidden() {
|
||||
m.startTime = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the spinner's unique ID.
|
||||
func (m Model) ID() int {
|
||||
return m.id
|
||||
}
|
||||
|
||||
// advancedMode returns whether or not the user is making use of HideFor and
|
||||
// MinimumLifetime properties.
|
||||
func (m Model) advancedMode() bool {
|
||||
return m.HideFor > 0 && m.MinimumLifetime > 0
|
||||
}
|
||||
|
||||
// hidden returns whether or not Model.HideFor is in effect.
|
||||
func (m Model) hidden() bool {
|
||||
if m.startTime.IsZero() {
|
||||
return false
|
||||
}
|
||||
if m.HideFor == 0 {
|
||||
return false
|
||||
}
|
||||
return m.startTime.Add(m.HideFor).After(time.Now())
|
||||
}
|
||||
|
||||
// finished returns whether the minimum lifetime of this spinner has been
|
||||
// exceeded.
|
||||
func (m Model) finished() bool {
|
||||
if m.startTime.IsZero() || m.MinimumLifetime == 0 {
|
||||
return true
|
||||
}
|
||||
return m.startTime.Add(m.HideFor).Add(m.MinimumLifetime).Before(time.Now())
|
||||
}
|
||||
|
||||
// Visible returns whether or not the view should be rendered. Works in
|
||||
// conjunction with Model.HideFor and Model.MinimumLifetimeReached. You should
|
||||
// use this method directly to determine whether or not to render this view in
|
||||
// the parent view and whether to continue sending spin messaging in the
|
||||
// parent update function.
|
||||
//
|
||||
// This function is optional and generally considered for advanced use only.
|
||||
// Most of the time your application logic will obviate the need for this
|
||||
// method.
|
||||
//
|
||||
// This is considered experimental and may not appear in future versions of
|
||||
// this library.
|
||||
func (m Model) Visible() bool {
|
||||
return !m.hidden() && !m.finished()
|
||||
}
|
||||
|
||||
// New returns a model with default values.
|
||||
func New() Model {
|
||||
return Model{
|
||||
@@ -250,16 +151,7 @@ func (m Model) View() string {
|
||||
return "(error)"
|
||||
}
|
||||
|
||||
frame := m.Spinner.Frames[m.frame]
|
||||
|
||||
// If we're using the fine-grained hide/show spinner rules and those rules
|
||||
// deem that the spinner should be hidden, draw an empty space in place of
|
||||
// the spinner.
|
||||
if m.advancedMode() && !m.Visible() {
|
||||
frame = strings.Repeat(" ", ansi.PrintableRuneWidth(frame))
|
||||
}
|
||||
|
||||
return m.Style.Render(frame)
|
||||
return m.Style.Render(m.Spinner.Frames[m.frame])
|
||||
}
|
||||
|
||||
// Tick is the command used to advance the spinner one frame. Use this command
|
||||
|
@@ -203,6 +203,11 @@ func (m Model) Cursor() int {
|
||||
return m.pos
|
||||
}
|
||||
|
||||
// Blink returns whether or not to draw the cursor.
|
||||
func (m Model) Blink() bool {
|
||||
return m.blink
|
||||
}
|
||||
|
||||
// SetCursor moves the cursor to the given position. If the position is
|
||||
// out of bounds the cursor will be moved to the start or end accordingly.
|
||||
func (m *Model) SetCursor(pos int) {
|
||||
|
Reference in New Issue
Block a user