mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 20:56:17 +00:00
merge master
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
name: lint-soft
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint-soft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.18
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
# Optional: golangci-lint command line arguments.
|
||||
args: --config .golangci-soft.yml --issues-exit-code=0
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
only-new-issues: true
|
||||
@@ -0,0 +1,14 @@
|
||||
name: lint-sync
|
||||
on:
|
||||
schedule:
|
||||
# every Sunday at midnight
|
||||
- cron: "0 0 * * 0"
|
||||
workflow_dispatch: # allows manual triggering
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main
|
||||
@@ -3,26 +3,6 @@ on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.18
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
# Optional: golangci-lint command line arguments.
|
||||
#args:
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
only-new-issues: true
|
||||
lint:
|
||||
uses: charmbracelet/meta/.github/workflows/lint.yml@main
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
.DS_Store
|
||||
dist/
|
||||
|
||||
+1
-5
@@ -1,5 +1,6 @@
|
||||
run:
|
||||
tests: false
|
||||
issues-exit-code: 0
|
||||
|
||||
issues:
|
||||
include:
|
||||
@@ -14,16 +15,13 @@ issues:
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# - dupl
|
||||
- exhaustive
|
||||
# - exhaustivestruct
|
||||
- goconst
|
||||
- godot
|
||||
- godox
|
||||
- mnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
# - lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
@@ -39,6 +37,4 @@ linters:
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- predeclared
|
||||
|
||||
@@ -15,12 +15,10 @@ issues:
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- exportloopref
|
||||
- gofumpt
|
||||
- goimports
|
||||
- gosec
|
||||
- nilerr
|
||||
- predeclared
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
@@ -28,6 +26,3 @@ linters:
|
||||
- unconvert
|
||||
- unparam
|
||||
- whitespace
|
||||
# We're isabling predeclared because the linter is running a newer version
|
||||
# of Go than then Go module.
|
||||
# - predeclared
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
version: 2
|
||||
includes:
|
||||
- from_url:
|
||||
url: charmbracelet/meta/main/goreleaser-lib.yaml
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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.
|
||||
@@ -377,7 +370,7 @@ func (m Model) View() string {
|
||||
var symlinkPath string
|
||||
info, _ := f.Info()
|
||||
isSymlink := info.Mode()&os.ModeSymlink != 0
|
||||
size := strings.Replace(humanize.Bytes(uint64(info.Size())), " ", "", 1)
|
||||
size := strings.Replace(humanize.Bytes(uint64(info.Size())), " ", "", 1) //nolint:gosec
|
||||
name := f.Name()
|
||||
|
||||
if isSymlink {
|
||||
@@ -386,7 +379,7 @@ func (m Model) View() string {
|
||||
|
||||
disabled := !m.canSelect(name) && !f.IsDir()
|
||||
|
||||
if m.selected == i {
|
||||
if m.selected == i { //nolint:nestif
|
||||
selected := ""
|
||||
if m.ShowPermissions {
|
||||
selected += " " + info.Mode().String()
|
||||
|
||||
@@ -5,11 +5,11 @@ go 1.18
|
||||
require (
|
||||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/charmbracelet/bubbletea v1.1.0
|
||||
github.com/charmbracelet/bubbletea v1.1.2
|
||||
github.com/charmbracelet/harmonica v0.2.0
|
||||
github.com/charmbracelet/lipgloss v0.13.1-0.20240924011622-17b470d8991e
|
||||
github.com/charmbracelet/x/ansi v0.3.2
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
github.com/charmbracelet/x/ansi v0.4.2
|
||||
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
|
||||
@@ -29,6 +29,6 @@ require (
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
|
||||
@@ -6,16 +6,16 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
|
||||
github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
|
||||
github.com/charmbracelet/bubbletea v1.1.2 h1:naQXF2laRxyLyil/i7fxdpiz1/k06IKquhm4vBfHsIc=
|
||||
github.com/charmbracelet/bubbletea v1.1.2/go.mod h1:9HIU/hBV24qKjlehyj8z1r/tR9TYTQEag+cWZnuXo8E=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.13.1-0.20240924011622-17b470d8991e h1:sc5K4dGwO7gpZsNNigIaDOgpMoe2JLuHXekLGD9EdwA=
|
||||
github.com/charmbracelet/lipgloss v0.13.1-0.20240924011622-17b470d8991e/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U=
|
||||
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/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
|
||||
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
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=
|
||||
@@ -47,7 +47,7 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
|
||||
+34
-26
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
enter continue …
|
||||
@@ -0,0 +1,2 @@
|
||||
enter continue | esc back …
|
||||
? help
|
||||
@@ -0,0 +1,3 @@
|
||||
enter continue | esc back | H home
|
||||
? help ctrl+c quit
|
||||
ctrl+l log
|
||||
+1
-1
@@ -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
|
||||
|
||||
+7
-5
@@ -35,7 +35,7 @@ type DefaultItemStyles struct {
|
||||
func NewDefaultItemStyles() (s DefaultItemStyles) {
|
||||
s.NormalTitle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}).
|
||||
Padding(0, 0, 0, 2)
|
||||
Padding(0, 0, 0, 2) //nolint:mnd
|
||||
|
||||
s.NormalDesc = s.NormalTitle.
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"})
|
||||
@@ -51,7 +51,7 @@ func NewDefaultItemStyles() (s DefaultItemStyles) {
|
||||
|
||||
s.DimmedTitle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
|
||||
Padding(0, 0, 0, 2)
|
||||
Padding(0, 0, 0, 2) //nolint:mnd
|
||||
|
||||
s.DimmedDesc = s.DimmedTitle.
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"})
|
||||
@@ -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
|
||||
@@ -93,11 +93,13 @@ type DefaultDelegate struct {
|
||||
|
||||
// NewDefaultDelegate creates a new delegate with default styles.
|
||||
func NewDefaultDelegate() DefaultDelegate {
|
||||
const defaultHeight = 2
|
||||
const defaultSpacing = 1
|
||||
return DefaultDelegate{
|
||||
ShowDescription: true,
|
||||
Styles: NewDefaultItemStyles(),
|
||||
height: 2,
|
||||
spacing: 1,
|
||||
height: defaultHeight,
|
||||
spacing: defaultSpacing,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-8
@@ -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
|
||||
}
|
||||
@@ -272,7 +273,7 @@ func (m *Model) SetShowTitle(v bool) {
|
||||
|
||||
// SetFilterText explicitly sets the filter text without relying on user input.
|
||||
// It also sets the filterState to a sane default of FilterApplied, but this
|
||||
// can be changed with SetFilterState
|
||||
// can be changed with SetFilterState.
|
||||
func (m *Model) SetFilterText(filter string) {
|
||||
m.filterState = Filtering
|
||||
m.FilterInput.SetValue(filter)
|
||||
@@ -288,7 +289,7 @@ func (m *Model) SetFilterText(filter string) {
|
||||
m.updateKeybindings()
|
||||
}
|
||||
|
||||
// Helper method for setting the filtering state manually
|
||||
// Helper method for setting the filtering state manually.
|
||||
func (m *Model) SetFilterState(state FilterState) {
|
||||
m.Paginator.Page = 0
|
||||
m.cursor = 0
|
||||
@@ -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
|
||||
@@ -707,7 +722,7 @@ func (m Model) itemsAsFilterItems() filteredItems {
|
||||
|
||||
// Set keybindings according to the filter state.
|
||||
func (m *Model) updateKeybindings() {
|
||||
switch m.filterState {
|
||||
switch m.filterState { //nolint:exhaustive
|
||||
case Filtering:
|
||||
m.KeyMap.CursorUp.SetEnabled(false)
|
||||
m.KeyMap.CursorDown.SetEnabled(false)
|
||||
@@ -1138,7 +1153,7 @@ func (m Model) statusView() string {
|
||||
|
||||
itemsDisplay := fmt.Sprintf("%d %s", visibleItems, itemName)
|
||||
|
||||
if m.filterState == Filtering {
|
||||
if m.filterState == Filtering { //nolint:nestif
|
||||
// Filter results
|
||||
if visibleItems == 0 {
|
||||
status = m.Styles.StatusEmpty.Render("Nothing matched")
|
||||
@@ -1154,7 +1169,7 @@ func (m Model) statusView() string {
|
||||
|
||||
if filtered {
|
||||
f := strings.TrimSpace(m.FilterInput.Value())
|
||||
f = ansi.Truncate(f, 10, "…")
|
||||
f = ansi.Truncate(f, 10, "…") //nolint:mnd
|
||||
status += fmt.Sprintf("“%s” ", f)
|
||||
}
|
||||
|
||||
@@ -1171,7 +1186,7 @@ func (m Model) statusView() string {
|
||||
}
|
||||
|
||||
func (m Model) paginationView() string {
|
||||
if m.Paginator.TotalPages < 2 { //nolint:gomnd
|
||||
if m.Paginator.TotalPages < 2 { //nolint:mnd
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
+4
-4
@@ -45,7 +45,7 @@ func DefaultStyles() (s Styles) {
|
||||
verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
|
||||
subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}
|
||||
|
||||
s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2)
|
||||
s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2) //nolint:mnd
|
||||
|
||||
s.Title = lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("62")).
|
||||
@@ -65,7 +65,7 @@ func DefaultStyles() (s Styles) {
|
||||
|
||||
s.StatusBar = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
|
||||
Padding(0, 0, 1, 2)
|
||||
Padding(0, 0, 1, 2) //nolint:mnd
|
||||
|
||||
s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor)
|
||||
|
||||
@@ -79,9 +79,9 @@ func DefaultStyles() (s Styles) {
|
||||
|
||||
s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor)
|
||||
|
||||
s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:gomnd
|
||||
s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:mnd
|
||||
|
||||
s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2)
|
||||
s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) //nolint:mnd
|
||||
|
||||
s.ActivePaginationDot = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}).
|
||||
|
||||
@@ -189,7 +189,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
|
||||
// View renders the pagination to a string.
|
||||
func (m Model) View() string {
|
||||
switch m.Type {
|
||||
switch m.Type { //nolint:exhaustive
|
||||
case Dots:
|
||||
return m.dotsView()
|
||||
default:
|
||||
|
||||
+4
-11
@@ -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 (
|
||||
@@ -342,7 +335,7 @@ func (m Model) percentageView(percent float64) string {
|
||||
return ""
|
||||
}
|
||||
percent = math.Max(0, math.Min(1, percent))
|
||||
percentage := fmt.Sprintf(m.PercentFormat, percent*100) //nolint:gomnd
|
||||
percentage := fmt.Sprintf(m.PercentFormat, percent*100) //nolint:mnd
|
||||
percentage = m.PercentageStyle.Inline(true).Render(percentage)
|
||||
return percentage
|
||||
}
|
||||
|
||||
+15
-22
@@ -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.
|
||||
@@ -33,39 +26,39 @@ type Spinner struct {
|
||||
var (
|
||||
Line = Spinner{
|
||||
Frames: []string{"|", "/", "-", "\\"},
|
||||
FPS: time.Second / 10, //nolint:gomnd
|
||||
FPS: time.Second / 10, //nolint:mnd
|
||||
}
|
||||
Dot = Spinner{
|
||||
Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
|
||||
FPS: time.Second / 10, //nolint:gomnd
|
||||
FPS: time.Second / 10, //nolint:mnd
|
||||
}
|
||||
MiniDot = Spinner{
|
||||
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
||||
FPS: time.Second / 12, //nolint:gomnd
|
||||
FPS: time.Second / 12, //nolint:mnd
|
||||
}
|
||||
Jump = Spinner{
|
||||
Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
|
||||
FPS: time.Second / 10, //nolint:gomnd
|
||||
FPS: time.Second / 10, //nolint:mnd
|
||||
}
|
||||
Pulse = Spinner{
|
||||
Frames: []string{"█", "▓", "▒", "░"},
|
||||
FPS: time.Second / 8, //nolint:gomnd
|
||||
FPS: time.Second / 8, //nolint:mnd
|
||||
}
|
||||
Points = Spinner{
|
||||
Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
|
||||
FPS: time.Second / 7, //nolint:gomnd
|
||||
FPS: time.Second / 7, //nolint:mnd
|
||||
}
|
||||
Globe = Spinner{
|
||||
Frames: []string{"🌍", "🌎", "🌏"},
|
||||
FPS: time.Second / 4, //nolint:gomnd
|
||||
FPS: time.Second / 4, //nolint:mnd
|
||||
}
|
||||
Moon = Spinner{
|
||||
Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
|
||||
FPS: time.Second / 8, //nolint:gomnd
|
||||
FPS: time.Second / 8, //nolint:mnd
|
||||
}
|
||||
Monkey = Spinner{
|
||||
Frames: []string{"🙈", "🙉", "🙊"},
|
||||
FPS: time.Second / 3, //nolint:gomnd
|
||||
FPS: time.Second / 3, //nolint:mnd
|
||||
}
|
||||
Meter = Spinner{
|
||||
Frames: []string{
|
||||
@@ -77,15 +70,15 @@ var (
|
||||
"▰▱▱",
|
||||
"▱▱▱",
|
||||
},
|
||||
FPS: time.Second / 7, //nolint:gomnd
|
||||
FPS: time.Second / 7, //nolint:mnd
|
||||
}
|
||||
Hamburger = Spinner{
|
||||
Frames: []string{"☱", "☲", "☴", "☲"},
|
||||
FPS: time.Second / 3, //nolint:gomnd
|
||||
FPS: time.Second / 3, //nolint:mnd
|
||||
}
|
||||
Ellipsis = Spinner{
|
||||
Frames: []string{"", ".", "..", "..."},
|
||||
FPS: time.Second / 3, //nolint:gomnd
|
||||
FPS: time.Second / 3, //nolint:mnd
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+19
-14
@@ -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.
|
||||
@@ -29,7 +23,8 @@ type TickMsg struct {
|
||||
// Note, however, that a stopwatch will reject ticks from other
|
||||
// stopwatches, so it's safe to flow all TickMsgs through all stopwatches
|
||||
// and have them still behave appropriately.
|
||||
ID int
|
||||
ID int
|
||||
tag int
|
||||
}
|
||||
|
||||
// StartStopMsg is sent when the stopwatch should start or stop.
|
||||
@@ -47,6 +42,7 @@ type ResetMsg struct {
|
||||
type Model struct {
|
||||
d time.Duration
|
||||
id int
|
||||
tag int
|
||||
running bool
|
||||
|
||||
// How long to wait before every tick. Defaults to 1 second.
|
||||
@@ -81,7 +77,7 @@ func (m Model) Init() tea.Cmd {
|
||||
func (m Model) Start() tea.Cmd {
|
||||
return tea.Batch(func() tea.Msg {
|
||||
return StartStopMsg{ID: m.id, running: true}
|
||||
}, tick(m.id, m.Interval))
|
||||
}, tick(m.id, m.tag, m.Interval))
|
||||
}
|
||||
|
||||
// Stop stops the stopwatch.
|
||||
@@ -128,8 +124,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if !m.running || msg.ID != m.id {
|
||||
break
|
||||
}
|
||||
|
||||
// If a tag is set, and it's not the one we expect, reject the message.
|
||||
// This prevents the stopwatch from receiving too many messages and
|
||||
// thus ticking too fast.
|
||||
if msg.tag > 0 && msg.tag != m.tag {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.d += m.Interval
|
||||
return m, tick(m.id, m.Interval)
|
||||
m.tag++
|
||||
return m, tick(m.id, m.tag, m.Interval)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -145,8 +150,8 @@ func (m Model) View() string {
|
||||
return m.d.String()
|
||||
}
|
||||
|
||||
func tick(id int, d time.Duration) tea.Cmd {
|
||||
func tick(id int, tag int, d time.Duration) tea.Cmd {
|
||||
return tea.Tick(d, func(_ time.Time) tea.Msg {
|
||||
return TickMsg{ID: id}
|
||||
return TickMsg{ID: id, tag: tag}
|
||||
})
|
||||
}
|
||||
|
||||
+18
-18
@@ -24,9 +24,12 @@ const (
|
||||
minHeight = 1
|
||||
defaultHeight = 6
|
||||
defaultWidth = 40
|
||||
defaultCharLimit = 400
|
||||
defaultCharLimit = 0 // no limit
|
||||
defaultMaxHeight = 99
|
||||
defaultMaxWidth = 500
|
||||
|
||||
// XXX: in v2, make max lines dynamic and default max lines configurable.
|
||||
maxLines = 10000
|
||||
)
|
||||
|
||||
// Internal messages for clipboard operations.
|
||||
@@ -290,13 +293,13 @@ func New() Model {
|
||||
style: &blurredStyle,
|
||||
FocusedStyle: focusedStyle,
|
||||
BlurredStyle: blurredStyle,
|
||||
cache: memoization.NewMemoCache[line, [][]rune](defaultMaxHeight),
|
||||
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
|
||||
EndOfBufferCharacter: ' ',
|
||||
ShowLineNumbers: true,
|
||||
Cursor: cur,
|
||||
KeyMap: DefaultKeyMap,
|
||||
|
||||
value: make([][]rune, minHeight, defaultMaxHeight),
|
||||
value: make([][]rune, minHeight, maxLines),
|
||||
focus: false,
|
||||
col: 0,
|
||||
row: 0,
|
||||
@@ -360,9 +363,8 @@ func (m *Model) insertRunesFromUserInput(runes []rune) {
|
||||
// whatnot.
|
||||
runes = m.san().Sanitize(runes)
|
||||
|
||||
var availSpace int
|
||||
if m.CharLimit > 0 {
|
||||
availSpace = m.CharLimit - m.Length()
|
||||
availSpace := m.CharLimit - m.Length()
|
||||
// If the char limit's been reached, cancel.
|
||||
if availSpace <= 0 {
|
||||
return
|
||||
@@ -393,9 +395,9 @@ func (m *Model) insertRunesFromUserInput(runes []rune) {
|
||||
lines = append(lines, runes[lstart:])
|
||||
}
|
||||
|
||||
// Obey the maximum height limit.
|
||||
if m.MaxHeight > 0 && len(m.value)+len(lines)-1 > m.MaxHeight {
|
||||
allowedHeight := max(0, m.MaxHeight-len(m.value)+1)
|
||||
// Obey the maximum line limit.
|
||||
if maxLines > 0 && len(m.value)+len(lines)-1 > maxLines {
|
||||
allowedHeight := max(0, maxLines-len(m.value)+1)
|
||||
lines = lines[:allowedHeight]
|
||||
}
|
||||
|
||||
@@ -492,7 +494,8 @@ func (m *Model) CursorDown() {
|
||||
// Move the cursor to the start of the next line so that we can get
|
||||
// the line information. We need to add 2 columns to account for the
|
||||
// trailing space wrapping.
|
||||
m.col = min(li.StartColumn+li.Width+2, len(m.value[m.row])-1)
|
||||
const trailingSpace = 2
|
||||
m.col = min(li.StartColumn+li.Width+trailingSpace, len(m.value[m.row])-1)
|
||||
}
|
||||
|
||||
nli := m.LineInfo()
|
||||
@@ -526,7 +529,8 @@ func (m *Model) CursorUp() {
|
||||
// This can be done by moving the cursor to the start of the line and
|
||||
// then subtracting 2 to account for the trailing space we keep on
|
||||
// soft-wrapped lines.
|
||||
m.col = li.StartColumn - 2
|
||||
const trailingSpace = 2
|
||||
m.col = li.StartColumn - trailingSpace
|
||||
}
|
||||
|
||||
nli := m.LineInfo()
|
||||
@@ -588,11 +592,7 @@ func (m *Model) Blur() {
|
||||
|
||||
// Reset sets the input to its default state with no input.
|
||||
func (m *Model) Reset() {
|
||||
startCap := m.MaxHeight
|
||||
if startCap <= 0 {
|
||||
startCap = defaultMaxHeight
|
||||
}
|
||||
m.value = make([][]rune, minHeight, startCap)
|
||||
m.value = make([][]rune, minHeight, maxLines)
|
||||
m.col = 0
|
||||
m.row = 0
|
||||
m.viewport.GotoTop()
|
||||
@@ -1119,7 +1119,7 @@ func (m Model) View() string {
|
||||
displayLine++
|
||||
|
||||
var ln string
|
||||
if m.ShowLineNumbers {
|
||||
if m.ShowLineNumbers { //nolint:nestif
|
||||
if wl == 0 {
|
||||
if m.row == l {
|
||||
ln = style.Render(m.style.computedCursorLineNumber().Render(m.formatLineNumber(l + 1)))
|
||||
@@ -1198,7 +1198,7 @@ func (m Model) View() string {
|
||||
}
|
||||
|
||||
// formatLineNumber formats the line number for display dynamically based on
|
||||
// the maximum number of lines
|
||||
// the maximum number of lines.
|
||||
func (m Model) formatLineNumber(x any) string {
|
||||
// XXX: ultimately we should use a max buffer height, which has yet to be
|
||||
// implemented.
|
||||
@@ -1407,7 +1407,7 @@ func wrap(runes []rune, width int) [][]rune {
|
||||
word = append(word, r)
|
||||
}
|
||||
|
||||
if spaces > 0 {
|
||||
if spaces > 0 { //nolint:nestif
|
||||
if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces > width {
|
||||
row++
|
||||
lines = append(lines, []rune{})
|
||||
|
||||
+23
-8
@@ -565,7 +565,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
|
||||
// Let's remember where the position of the cursor currently is so that if
|
||||
// the cursor position changes, we can reset the blink.
|
||||
oldPos := m.pos //nolint
|
||||
oldPos := m.pos
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
@@ -658,7 +658,7 @@ func (m Model) View() string {
|
||||
pos := max(0, m.pos-m.offset)
|
||||
v := styleText(m.echoTransform(string(value[:pos])))
|
||||
|
||||
if pos < len(value) {
|
||||
if pos < len(value) { //nolint:nestif
|
||||
char := m.echoTransform(string(value[pos]))
|
||||
m.Cursor.SetChar(char)
|
||||
v += m.Cursor.View() // cursor and text under it
|
||||
@@ -700,10 +700,12 @@ func (m Model) View() string {
|
||||
func (m Model) placeholderView() string {
|
||||
var (
|
||||
v string
|
||||
p = []rune(m.Placeholder)
|
||||
style = m.PlaceholderStyle.Inline(true).Render
|
||||
)
|
||||
|
||||
p := make([]rune, m.Width+1)
|
||||
copy(p, []rune(m.Placeholder))
|
||||
|
||||
m.Cursor.TextStyle = m.PlaceholderStyle
|
||||
m.Cursor.SetChar(string(p[:1]))
|
||||
v += m.Cursor.View()
|
||||
@@ -813,16 +815,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) {
|
||||
|
||||
@@ -30,3 +30,10 @@ func Test_CurrentSuggestion(t *testing.T) {
|
||||
t.Fatalf("Error: expected first suggestion but was %s", suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SlicingOutsideCap(t *testing.T) {
|
||||
textinput := New()
|
||||
textinput.Placeholder = "作業ディレクトリを指定してください"
|
||||
textinput.Width = 32
|
||||
textinput.View()
|
||||
}
|
||||
|
||||
+14
-10
@@ -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:
|
||||
@@ -67,6 +61,8 @@ type TickMsg struct {
|
||||
// Timeout returns whether or not this tick is a timeout tick. You can
|
||||
// alternatively listen for TimeoutMsg.
|
||||
Timeout bool
|
||||
|
||||
tag int
|
||||
}
|
||||
|
||||
// TimeoutMsg is a message that is sent once when the timer times out.
|
||||
@@ -86,6 +82,7 @@ type Model struct {
|
||||
Interval time.Duration
|
||||
|
||||
id int
|
||||
tag int
|
||||
running bool
|
||||
}
|
||||
|
||||
@@ -143,6 +140,13 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
break
|
||||
}
|
||||
|
||||
// If a tag is set, and it's not the one we expect, reject the message.
|
||||
// This prevents the ticker from receiving too many messages and
|
||||
// thus ticking too fast.
|
||||
if msg.tag > 0 && msg.tag != m.tag {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.Timeout -= m.Interval
|
||||
return m, tea.Batch(m.tick(), m.timedout())
|
||||
}
|
||||
@@ -172,7 +176,7 @@ func (m *Model) Toggle() tea.Cmd {
|
||||
|
||||
func (m Model) tick() tea.Cmd {
|
||||
return tea.Tick(m.Interval, func(_ time.Time) tea.Msg {
|
||||
return TickMsg{ID: m.id, Timeout: m.Timedout()}
|
||||
return TickMsg{ID: m.id, tag: m.tag, Timeout: m.Timedout()}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+20
-5
@@ -50,6 +50,8 @@ type Model struct {
|
||||
//
|
||||
// This should only be used in program occupying the entire terminal,
|
||||
// which is usually via the alternate screen buffer.
|
||||
//
|
||||
// Deprecated: high performance rendering is now deprecated in Bubble Tea.
|
||||
HighPerformanceRendering bool
|
||||
|
||||
initialized bool
|
||||
@@ -97,8 +99,7 @@ func (m Model) ScrollPercent() float64 {
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
// SetContent set the pager's text content. For high performance rendering the
|
||||
// Sync command should also be called.
|
||||
// SetContent set the pager's text content.
|
||||
func (m *Model) SetContent(s string) {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
|
||||
m.lines = strings.Split(s, "\n")
|
||||
@@ -126,6 +127,8 @@ func (m Model) visibleLines() (lines []string) {
|
||||
}
|
||||
|
||||
// scrollArea returns the scrollable boundaries for high performance rendering.
|
||||
//
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea.
|
||||
func (m Model) scrollArea() (top, bottom int) {
|
||||
top = max(0, m.YPosition)
|
||||
bottom = max(top, top+m.Height)
|
||||
@@ -165,7 +168,7 @@ func (m *Model) HalfViewDown() (lines []string) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.LineDown(m.Height / 2)
|
||||
return m.LineDown(m.Height / 2) //nolint:mnd
|
||||
}
|
||||
|
||||
// HalfViewUp moves the view up by half the height of the viewport.
|
||||
@@ -174,7 +177,7 @@ func (m *Model) HalfViewUp() (lines []string) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.LineUp(m.Height / 2)
|
||||
return m.LineUp(m.Height / 2) //nolint:mnd
|
||||
}
|
||||
|
||||
// LineDown moves the view down by the given number of lines.
|
||||
@@ -189,6 +192,8 @@ func (m *Model) LineDown(n int) (lines []string) {
|
||||
m.SetYOffset(m.YOffset + n)
|
||||
|
||||
// Gather lines to send off for performance scrolling.
|
||||
//
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea.
|
||||
bottom := clamp(m.YOffset+m.Height, 0, len(m.lines))
|
||||
top := clamp(m.YOffset+m.Height-n, 0, bottom)
|
||||
return m.lines[top:bottom]
|
||||
@@ -206,6 +211,8 @@ func (m *Model) LineUp(n int) (lines []string) {
|
||||
m.SetYOffset(m.YOffset - n)
|
||||
|
||||
// Gather lines to send off for performance scrolling.
|
||||
//
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea.
|
||||
top := max(0, m.YOffset)
|
||||
bottom := clamp(m.YOffset+n, 0, m.maxYOffset())
|
||||
return m.lines[top:bottom]
|
||||
@@ -242,6 +249,8 @@ func (m *Model) GotoBottom() (lines []string) {
|
||||
// first render and after a window resize.
|
||||
//
|
||||
// For high performance rendering only.
|
||||
//
|
||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
||||
func Sync(m Model) tea.Cmd {
|
||||
if len(m.lines) == 0 {
|
||||
return nil
|
||||
@@ -261,6 +270,9 @@ func ViewDown(m Model, lines []string) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
top, bottom := m.scrollArea()
|
||||
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
|
||||
// won't need to return a command here.
|
||||
return tea.ScrollDown(lines, top, bottom)
|
||||
}
|
||||
|
||||
@@ -272,6 +284,9 @@ func ViewUp(m Model, lines []string) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
top, bottom := m.scrollArea()
|
||||
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
|
||||
// won't need to return a command here.
|
||||
return tea.ScrollUp(lines, top, bottom)
|
||||
}
|
||||
|
||||
@@ -335,7 +350,7 @@ func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if !m.MouseWheelEnabled || msg.Action != tea.MouseActionPress {
|
||||
break
|
||||
}
|
||||
switch msg.Button {
|
||||
switch msg.Button { //nolint:exhaustive
|
||||
case tea.MouseButtonWheelUp:
|
||||
lines := m.LineUp(m.MouseWheelDelta)
|
||||
if m.HighPerformanceRendering {
|
||||
|
||||
Reference in New Issue
Block a user