From 4382fdf1c6cfe18d34615c1e8b94b068022a461a Mon Sep 17 00:00:00 2001 From: David Luevano Alvarado Date: Wed, 17 Jul 2024 04:29:41 -0600 Subject: [PATCH 1/8] feat(textinput): expose matched suggestions and index --- textinput/textinput.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/textinput/textinput.go b/textinput/textinput.go index 93bc150..cf944b8 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -813,16 +813,29 @@ func (m Model) completionView(offset int) string { return "" } -// AvailableSuggestions returns the list of available suggestions. -func (m *Model) AvailableSuggestions() []string { - suggestions := make([]string, len(m.suggestions)) - for i, s := range m.suggestions { +func (m *Model) getSuggestions(sugs [][]rune) []string { + suggestions := make([]string, len(sugs)) + for i, s := range sugs { suggestions[i] = string(s) } - return suggestions } +// AvailableSuggestions returns the list of available suggestions. +func (m *Model) AvailableSuggestions() []string { + return m.getSuggestions(m.suggestions) +} + +// MatchedSuggestions returns the list of matched suggestions. +func (m *Model) MatchedSuggestions() []string { + return m.getSuggestions(m.matchedSuggestions) +} + +// CurrentSuggestion returns the currently selected suggestion index. +func (m *Model) CurrentSuggestionIndex() int { + return m.currentSuggestionIndex +} + // CurrentSuggestion returns the currently selected suggestion. func (m *Model) CurrentSuggestion() string { if m.currentSuggestionIndex >= len(m.matchedSuggestions) { From 9a262e9b0fc34b27b4f7cf7e60e2c48a7c310696 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 7 Oct 2024 15:26:12 -0300 Subject: [PATCH 2/8] fix: use atomic for ids (#634) closes #622 Signed-off-by: Carlos Alexandro Becker Co-authored-by: Ville Valkonen --- filepicker/filepicker.go | 13 +++---------- progress/progress.go | 13 +++---------- spinner/spinner.go | 13 +++---------- stopwatch/stopwatch.go | 12 +++--------- timer/timer.go | 12 +++--------- 5 files changed, 15 insertions(+), 48 deletions(-) diff --git a/filepicker/filepicker.go b/filepicker/filepicker.go index 2288a5d..57fe02d 100644 --- a/filepicker/filepicker.go +++ b/filepicker/filepicker.go @@ -7,7 +7,7 @@ import ( "sort" "strconv" "strings" - "sync" + "sync/atomic" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -15,17 +15,10 @@ import ( "github.com/dustin/go-humanize" ) -var ( - lastID int - idMtx sync.Mutex -) +var lastID int64 -// Return the next ID we should use on the Model. func nextID() int { - idMtx.Lock() - defer idMtx.Unlock() - lastID++ - return lastID + return int(atomic.AddInt64(&lastID, 1)) } // New returns a new filepicker model with default styling and key bindings. diff --git a/progress/progress.go b/progress/progress.go index defa981..cf3dbb4 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -4,7 +4,7 @@ import ( "fmt" "math" "strings" - "sync" + "sync/atomic" "time" tea "github.com/charmbracelet/bubbletea" @@ -17,17 +17,10 @@ import ( // Internal ID management. Used during animating to assure that frame messages // can only be received by progress components that sent them. -var ( - lastID int - idMtx sync.Mutex -) +var lastID int64 -// Return the next ID we should use on the model. func nextID() int { - idMtx.Lock() - defer idMtx.Unlock() - lastID++ - return lastID + return int(atomic.AddInt64(&lastID, 1)) } const ( diff --git a/spinner/spinner.go b/spinner/spinner.go index bb53597..7138af1 100644 --- a/spinner/spinner.go +++ b/spinner/spinner.go @@ -1,7 +1,7 @@ package spinner import ( - "sync" + "sync/atomic" "time" tea "github.com/charmbracelet/bubbletea" @@ -10,17 +10,10 @@ import ( // Internal ID management. Used during animating to ensure that frame messages // are received only by spinner components that sent them. -var ( - lastID int - idMtx sync.Mutex -) +var lastID int64 -// Return the next ID we should use on the Model. func nextID() int { - idMtx.Lock() - defer idMtx.Unlock() - lastID++ - return lastID + return int(atomic.AddInt64(&lastID, 1)) } // Spinner is a set of frames used in animating the spinner. diff --git a/stopwatch/stopwatch.go b/stopwatch/stopwatch.go index 6b298f7..bf4e39c 100644 --- a/stopwatch/stopwatch.go +++ b/stopwatch/stopwatch.go @@ -2,22 +2,16 @@ package stopwatch import ( - "sync" + "sync/atomic" "time" tea "github.com/charmbracelet/bubbletea" ) -var ( - lastID int - idMtx sync.Mutex -) +var lastID int64 func nextID() int { - idMtx.Lock() - defer idMtx.Unlock() - lastID++ - return lastID + return int(atomic.AddInt64(&lastID, 1)) } // TickMsg is a message that is sent on every timer tick. diff --git a/timer/timer.go b/timer/timer.go index eb085e0..076ee6f 100644 --- a/timer/timer.go +++ b/timer/timer.go @@ -2,22 +2,16 @@ package timer import ( - "sync" + "sync/atomic" "time" tea "github.com/charmbracelet/bubbletea" ) -var ( - lastID int - idMtx sync.Mutex -) +var lastID int64 func nextID() int { - idMtx.Lock() - defer idMtx.Unlock() - lastID++ - return lastID + return int(atomic.AddInt64(&lastID, 1)) } // Authors note with regard to start and stop commands: From cf3f46ab316fc263a88a0dfd368011f8868101f5 Mon Sep 17 00:00:00 2001 From: Aashutosh Date: Tue, 8 Oct 2024 00:11:58 +0545 Subject: [PATCH 3/8] docs(list): fix grammar in doc comment and README (#627) --- list/README.md | 2 +- list/defaultitem.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/list/README.md b/list/README.md index 60e802c..ad073ed 100644 --- a/list/README.md +++ b/list/README.md @@ -17,7 +17,7 @@ type Item interface { ``` ```go -// DefaultItem describes an items designed to work with DefaultDelegate. +// DefaultItem describes an item designed to work with DefaultDelegate. type DefaultItem interface { Item Title() string diff --git a/list/defaultitem.go b/list/defaultitem.go index 3f07cef..763dad1 100644 --- a/list/defaultitem.go +++ b/list/defaultitem.go @@ -61,7 +61,7 @@ func NewDefaultItemStyles() (s DefaultItemStyles) { return s } -// DefaultItem describes an items designed to work with DefaultDelegate. +// DefaultItem describes an item designed to work with DefaultDelegate. type DefaultItem interface { Item Title() string From 076b578512a8d2226f6d7c226533eeebab071bbb Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 8 Oct 2024 02:29:06 +0800 Subject: [PATCH 4/8] chore: Remove duplicate and redundant code (#626) Signed-off-by: cuishuang --- table/table.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/table/table.go b/table/table.go index 6103c83..73687b9 100644 --- a/table/table.go +++ b/table/table.go @@ -219,8 +219,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.MoveUp(m.viewport.Height / 2) case key.Matches(msg, m.KeyMap.HalfPageDown): m.MoveDown(m.viewport.Height / 2) - case key.Matches(msg, m.KeyMap.LineDown): - m.MoveDown(1) case key.Matches(msg, m.KeyMap.GotoTop): m.GotoTop() case key.Matches(msg, m.KeyMap.GotoBottom): From 171a9d61a0ce4fff88db8e16d03c3bac3f35f264 Mon Sep 17 00:00:00 2001 From: nobe4 Date: Thu, 10 Oct 2024 01:59:04 +0200 Subject: [PATCH 5/8] feat(list): implement GlobalIndex helper (#574) * feat(list): implement GlobalIndex helper This commit introduces a new `filteredItem` field called `index` which stores the index of the filtered item in the unfiltered list. This allows to get at runtime the unfiltered list index for the selected (and possibly filtered) item. This is the only solution I found to use `SetItem` with a filtered list. The name `GlobalIndex` might not be ideal, I'm happy to change it to something else. (`UnfilteredIndex`?) Fixes: #550 * docs(list): improve Index() documentation --- list/list.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/list/list.go b/list/list.go index 00d27a0..9ee1643 100644 --- a/list/list.go +++ b/list/list.go @@ -53,6 +53,7 @@ type ItemDelegate interface { } type filteredItem struct { + index int // index in the unfiltered list item Item // item matched matches []int // rune indices of matched items } @@ -484,12 +485,26 @@ func (m Model) MatchesForItem(index int) []int { return m.filteredItems[index].matches } -// Index returns the index of the currently selected item as it appears in the -// entire slice of items. +// Index returns the index of the currently selected item as it is stored in the +// filtered list of items. +// Using this value with SetItem() might be incorrect, consider using +// GlobalIndex() instead. func (m Model) Index() int { return m.Paginator.Page*m.Paginator.PerPage + m.cursor } +// GlobalIndex returns the index of the currently selected item as it is stored +// in the unfiltered list of items. This value can be used with SetItem(). +func (m Model) GlobalIndex() int { + index := m.Index() + + if m.filteredItems == nil || index >= len(m.filteredItems) { + return index + } + + return m.filteredItems[index].index +} + // Cursor returns the index of the cursor on the current page. func (m Model) Cursor() int { return m.cursor @@ -1256,6 +1271,7 @@ func filterItems(m Model) tea.Cmd { filterMatches := []filteredItem{} for _, r := range m.Filter(m.FilterInput.Value(), targets) { filterMatches = append(filterMatches, filteredItem{ + index: r.Index, item: items[r.Index], matches: r.MatchedIndexes, }) From 81d444a207009d0621988c9e08e827aa803ee04e Mon Sep 17 00:00:00 2001 From: David Luevano Alvarado Date: Sun, 14 Jul 2024 07:15:11 -0600 Subject: [PATCH 6/8] fix(help): wrong full help sep rendering --- help/help.go | 60 +++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/help/help.go b/help/help.go index f4e1c97..85afbee 100644 --- a/help/help.go +++ b/help/help.go @@ -124,28 +124,23 @@ func (m Model) ShortHelpView(bindings []key.Binding) string { continue } + // Sep var sep string if totalWidth > 0 && i < len(bindings) { sep = separator } + // Item str := sep + m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " + m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc) - w := lipgloss.Width(str) - // If adding this help item would go over the available width, stop - // drawing. - if m.Width > 0 && totalWidth+w > m.Width { - // Although if there's room for an ellipsis, print that. - tail := " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis) - tailWidth := lipgloss.Width(tail) - - if totalWidth+tailWidth < m.Width { + // Tail + if tail, ok := m.shouldAddItem(totalWidth, w); !ok { + if tail != "" { b.WriteString(tail) } - break } @@ -170,8 +165,7 @@ func (m Model) FullHelpView(groups [][]key.Binding) string { out []string totalWidth int - sep = m.Styles.FullSeparator.Render(m.FullSeparator) - sepWidth = lipgloss.Width(sep) + separator = m.Styles.FullSeparator.Inline(true).Render(m.FullSeparator) ) // Iterate over groups to build columns @@ -179,12 +173,17 @@ func (m Model) FullHelpView(groups [][]key.Binding) string { if group == nil || !shouldRenderColumn(group) { continue } - var ( + sep string keys []string descriptions []string ) + // Sep + if totalWidth > 0 && i < len(groups) { + sep = separator + } + // Separate keys and descriptions into different slices for _, kb := range group { if !kb.Enabled() { @@ -194,33 +193,42 @@ func (m Model) FullHelpView(groups [][]key.Binding) string { descriptions = append(descriptions, kb.Help().Desc) } + // Column col := lipgloss.JoinHorizontal(lipgloss.Top, + sep, m.Styles.FullKey.Render(strings.Join(keys, "\n")), - m.Styles.FullKey.Render(" "), + " ", m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")), ) + w := lipgloss.Width(col) - // Column - totalWidth += lipgloss.Width(col) - if m.Width > 0 && totalWidth > m.Width { + // Tail + if tail, ok := m.shouldAddItem(totalWidth, w); !ok { + if tail != "" { + out = append(out, tail) + } break } + totalWidth += w out = append(out, col) - - // Separator - if i < len(group)-1 { - totalWidth += sepWidth - if m.Width > 0 && totalWidth > m.Width { - break - } - out = append(out, sep) - } } return lipgloss.JoinHorizontal(lipgloss.Top, out...) } +func (m Model) shouldAddItem(totalWidth, width int) (tail string, ok bool) { + // If there's room for an ellipsis, print that. + if m.Width > 0 && totalWidth+width > m.Width { + tail = " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis) + + if totalWidth+lipgloss.Width(tail) < m.Width { + return tail, false + } + } + return "", true +} + func shouldRenderColumn(b []key.Binding) (ok bool) { for _, v := range b { if v.Enabled() { From 60796500b2e255dcb9d68b916a6fc5aa49cf5b47 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 10 Oct 2024 15:29:23 -0400 Subject: [PATCH 7/8] chore(help): add full help test to support #554 --- go.mod | 2 +- go.sum | 4 +- help/help_test.go | 38 +++++++++++++++++++ .../TestFullHelp/full_help_20_width.golden | 1 + .../TestFullHelp/full_help_30_width.golden | 2 + .../TestFullHelp/full_help_40_width.golden | 3 ++ 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 help/help_test.go create mode 100644 help/testdata/TestFullHelp/full_help_20_width.golden create mode 100644 help/testdata/TestFullHelp/full_help_30_width.golden create mode 100644 help/testdata/TestFullHelp/full_help_40_width.golden diff --git a/go.mod b/go.mod index 1b53f26..95d6817 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/harmonica v0.2.0 github.com/charmbracelet/lipgloss v0.13.0 github.com/charmbracelet/x/ansi v0.3.2 - github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 github.com/dustin/go-humanize v1.0.1 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-runewidth v0.0.16 diff --git a/go.sum b/go.sum index fafd9e2..bbb72b1 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= diff --git a/help/help_test.go b/help/help_test.go new file mode 100644 index 0000000..79601d7 --- /dev/null +++ b/help/help_test.go @@ -0,0 +1,38 @@ +package help + +import ( + "fmt" + "testing" + + "github.com/charmbracelet/x/exp/golden" + + "github.com/charmbracelet/bubbles/key" +) + +func TestFullHelp(t *testing.T) { + m := New() + m.FullSeparator = " | " + k := key.WithKeys("x") + kb := [][]key.Binding{ + { + key.NewBinding(k, key.WithHelp("enter", "continue")), + }, + { + key.NewBinding(k, key.WithHelp("esc", "back")), + key.NewBinding(k, key.WithHelp("?", "help")), + }, + { + key.NewBinding(k, key.WithHelp("H", "home")), + key.NewBinding(k, key.WithHelp("ctrl+c", "quit")), + key.NewBinding(k, key.WithHelp("ctrl+l", "log")), + }, + } + + for _, w := range []int{20, 30, 40} { + t.Run(fmt.Sprintf("full help %d width", w), func(t *testing.T) { + m.Width = w + s := m.FullHelpView(kb) + golden.RequireEqual(t, []byte(s)) + }) + } +} diff --git a/help/testdata/TestFullHelp/full_help_20_width.golden b/help/testdata/TestFullHelp/full_help_20_width.golden new file mode 100644 index 0000000..e8c569b --- /dev/null +++ b/help/testdata/TestFullHelp/full_help_20_width.golden @@ -0,0 +1 @@ +enter continue … \ No newline at end of file diff --git a/help/testdata/TestFullHelp/full_help_30_width.golden b/help/testdata/TestFullHelp/full_help_30_width.golden new file mode 100644 index 0000000..1183a10 --- /dev/null +++ b/help/testdata/TestFullHelp/full_help_30_width.golden @@ -0,0 +1,2 @@ +enter continue | esc back … + ? help \ No newline at end of file diff --git a/help/testdata/TestFullHelp/full_help_40_width.golden b/help/testdata/TestFullHelp/full_help_40_width.golden new file mode 100644 index 0000000..e0227d0 --- /dev/null +++ b/help/testdata/TestFullHelp/full_help_40_width.golden @@ -0,0 +1,3 @@ +enter continue | esc back | H home + ? help ctrl+c quit + ctrl+l log \ No newline at end of file From 178590b4469b2386726cff8da7c479615a746a94 Mon Sep 17 00:00:00 2001 From: bashbunni <15822994+bashbunni@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:57:39 -0700 Subject: [PATCH 8/8] docs: update contributing guidelines (#640) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f6e4d21..66abbd7 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,12 @@ following requirements: Thank you! +## Contributing + +See [contributing][contribute]. + +[contribute]: https://github.com/charmbracelet/bubbles/contribute + ## Feedback We’d love to hear your thoughts on this project. Feel free to drop us a note!