diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9fea58..72bed38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,14 @@ name: build + on: [push, pull_request] + jobs: test: strategy: matrix: - go-version: [~1.22, ^1] + go-version: [stable, oldstable] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} - env: - GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v5 @@ -26,3 +26,22 @@ jobs: - name: Test run: go test ./... + + dependabot: + needs: [test] + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} + steps: + - id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + - run: | + gh pr review --approve "$PR_URL" + gh pr merge --squash --auto "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.golangci-soft.yml b/.golangci-soft.yml deleted file mode 100644 index d325d4f..0000000 --- a/.golangci-soft.yml +++ /dev/null @@ -1,40 +0,0 @@ -run: - tests: false - issues-exit-code: 0 - -issues: - include: - - EXC0001 - - EXC0005 - - EXC0011 - - EXC0012 - - EXC0013 - - max-issues-per-linter: 0 - max-same-issues: 0 - -linters: - enable: - - exhaustive - - goconst - - godot - - godox - - mnd - - gomoddirectives - - goprintffuncname - - misspell - - nakedret - - nestif - - noctx - - nolintlint - - prealloc - - wrapcheck - - # disable default linters, they are already enabled in .golangci.yml - disable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused diff --git a/.golangci.yml b/.golangci.yml index d6789e0..90c5c08 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,10 +15,22 @@ issues: linters: enable: - bodyclose + - exhaustive + - goconst + - godot + - godox - gofumpt - goimports + - gomoddirectives + - goprintffuncname - gosec + - misspell + - nakedret + - nestif - nilerr + - noctx + - nolintlint + - prealloc - revive - rowserrcheck - sqlclosecheck @@ -26,3 +38,4 @@ linters: - unconvert - unparam - whitespace + - wrapcheck diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..3507203 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,14 @@ +# https://taskfile.dev + +version: '3' + +tasks: + lint: + desc: Run lint + cmds: + - golangci-lint run + + test: + desc: Run tests + cmds: + - go test ./... {{.CLI_ARGS}} diff --git a/filepicker/filepicker.go b/filepicker/filepicker.go index d370740..da6af56 100644 --- a/filepicker/filepicker.go +++ b/filepicker/filepicker.go @@ -182,10 +182,10 @@ func newStack() stack { } } -func (m *Model) pushView(selected, minIdx, maxIdx int) { +func (m *Model) pushView(selected, minimum, maximum int) { m.selectedStack.Push(selected) - m.minStack.Push(minIdx) - m.maxStack.Push(maxIdx) + m.minStack.Push(minimum) + m.maxStack.Push(maximum) } func (m *Model) popView() (int, int, int) { @@ -225,6 +225,9 @@ func (m Model) readDir(path string, showHidden bool) tea.Cmd { // SetHeight sets the height of the file picker. func (m *Model) SetHeight(h int) { m.height = h + if m.maxIdx > m.height-1 { + m.maxIdx = m.minIdx + m.height - 1 + } } // Height returns the height of the file picker. diff --git a/go.mod b/go.mod index 76f45db..205520f 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,15 @@ module github.com/charmbracelet/bubbles/v2 -go 1.22 +go 1.23.0 + +toolchain go1.23.7 require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/atotto/clipboard v0.1.4 - github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250204175057-2a96bc438da6 + github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250318133619-2ce107c85fed github.com/charmbracelet/harmonica v0.2.0 - github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250204145343-96725424379d + github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250319221657-e0b75f7d5b68 github.com/charmbracelet/x/ansi v0.8.0 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a github.com/dustin/go-humanize v1.0.1 @@ -19,14 +21,14 @@ require ( require ( github.com/aymanbagabas/go-udiff v0.2.0 // indirect - github.com/charmbracelet/colorprofile v0.2.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.9-0.20250203222631-bea22a7f0a07 // indirect - github.com/charmbracelet/x/input v0.3.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/input v0.3.4 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/windows v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index 7746f25..6cbf30d 100644 --- a/go.sum +++ b/go.sum @@ -2,26 +2,24 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -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/v2 v2.0.0-alpha.2.0.20250204175057-2a96bc438da6 h1:KbLqbYgHC5JZWks50WjBJr92Hs+GpiauwZaDObw9vxE= -github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250204175057-2a96bc438da6/go.mod h1:zaWGYfO6cBtu4dRgGbVN7yMYUz3j9sVIzHy2gn0jdo8= -github.com/charmbracelet/colorprofile v0.2.0 h1:iiIQlp3LSvoJPtR11KoDfIf9wqWm2mn/iU420rHOZ/A= -github.com/charmbracelet/colorprofile v0.2.0/go.mod h1:6wPrSSR4QtwYtOY3h0bLRw5YOUAIKWlZIJ02CTAsZsk= +github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250318133619-2ce107c85fed h1:Fsn31oc6fmX7YgfZ1Am+19HVorJ8Bf6wJTebVNLcQP0= +github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250318133619-2ce107c85fed/go.mod h1:ks5r0VCpADtYFlNU15krnFIbMn545yV3U6ee6UJrlto= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 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/v2 v2.0.0-alpha.2.0.20250204145343-96725424379d h1:wW4446FqrhqEHT96r2OVGNU0izi8siEybQVZ+qBRpJs= -github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250204145343-96725424379d/go.mod h1:ZWl23X8o1vsQu8dpju10HKXepcMMlsHO8SwLl2OhmEU= +github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250319221657-e0b75f7d5b68 h1:4Uhi39pfXraqtHyB8ejdU+PqmVyjhd6hhEKA+7sKflE= +github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250319221657-e0b75f7d5b68/go.mod h1:XhU7tcZRWVGzkjWQ6XYRH7tIVqYuWLx6XLjVqAz+7FU= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.9-0.20250203222631-bea22a7f0a07 h1:RFHEvURPMgGGd8epjjhi2UpXSKyFs39iRF4JTYCEdLg= -github.com/charmbracelet/x/cellbuf v0.0.9-0.20250203222631-bea22a7f0a07/go.mod h1:dKfNBxLovpvzzxAP6/GZfs5eb7vNxHlUDnwGhRmvIdY= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw= github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/input v0.3.1 h1:TE4s3fTRj+OUpJ86dKphrN99+NgBnto//EkWncMJQIg= -github.com/charmbracelet/x/input v0.3.1/go.mod h1:4w9jS/NW62WrHSdmjbpzydvnbqkd+mtyK8WOWbHCdvs= +github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0= +github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= @@ -32,14 +30,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -49,7 +43,7 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/textarea/textarea.go b/textarea/textarea.go index 790d2fa..8d29154 100644 --- a/textarea/textarea.go +++ b/textarea/textarea.go @@ -943,13 +943,12 @@ func (m Model) LineInfo() LineInfo { // repositionView repositions the view of the viewport based on the defined // scrolling behavior. func (m *Model) repositionView() { - minOffset := m.viewport.YOffset - maxOffset := minOffset + m.viewport.Height() - 1 - - if row := m.cursorLineNumber(); row < minOffset { - m.viewport.LineUp(minOffset - row) - } else if row > maxOffset { - m.viewport.LineDown(row - maxOffset) + minimum := m.viewport.YOffset + maximum := minimum + m.viewport.Height() - 1 + if row := m.cursorLineNumber(); row < minimum { + m.viewport.LineUp(minimum - row) + } else if row > maximum { + m.viewport.LineDown(row - maximum) } } diff --git a/textinput/textinput_test.go b/textinput/textinput_test.go index bac660d..3bfb958 100644 --- a/textinput/textinput_test.go +++ b/textinput/textinput_test.go @@ -1,6 +1,8 @@ package textinput import ( + "fmt" + "strconv" "strings" "testing" ) @@ -43,3 +45,36 @@ func Test_SlicingOutsideCap(t *testing.T) { textinput.SetWidth(32) textinput.View() } + +func ExampleValidateFunc() { + creditCardNumber := New() + creditCardNumber.Placeholder = "4505 **** **** 1234" + creditCardNumber.Focus() + creditCardNumber.CharLimit = 20 + creditCardNumber.SetWidth(30) + creditCardNumber.Prompt = "" + // This anonymous function is a valid function for ValidateFunc. + creditCardNumber.Validate = func(s string) error { + // Credit Card Number should a string less than 20 digits + // It should include 16 integers and 3 spaces + if len(s) > 16+3 { + return fmt.Errorf("CCN is too long") + } + + if len(s) == 0 || len(s)%5 != 0 && (s[len(s)-1] < '0' || s[len(s)-1] > '9') { + return fmt.Errorf("CCN is invalid") + } + + // The last digit should be a number unless it is a multiple of 4 in which + // case it should be a space + if len(s)%5 == 0 && s[len(s)-1] != ' ' { + return fmt.Errorf("CCN must separate groups with spaces") + } + + // The remaining digits should be integers + c := strings.ReplaceAll(s, " ", "") + _, err := strconv.ParseInt(c, 10, 64) + + return err + } +}