diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e685121..33b0bd9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-go@def8c394e3ad351a79bc93815e4a585520fe993b with: check-latest: true - go-version: 1.26.x + go-version-file: "go.mod" - name: Import GPG key uses: crazy-max/ghaction-import-gpg@5a30dd90ba97bcb89e03060b1d36736828e222e4 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 8be6937..d4bc1a7 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -6,9 +6,6 @@ on: permissions: contents: read -env: - GO_VERSION: 1.25.x - jobs: lint: name: Run Linter @@ -23,7 +20,7 @@ jobs: uses: actions/setup-go@def8c394e3ad351a79bc93815e4a585520fe993b with: check-latest: true - go-version: ${{ env.GO_VERSION }} + go-version-file: "go.mod" - name: Install dependencies run: go mod download diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 322e6ad..c8127c7 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -19,7 +19,6 @@ permissions: security-events: write env: - GO_VERSION: 1.25.x OUTPUT_FILE: results.sarif jobs: @@ -48,7 +47,7 @@ jobs: with: output-format: sarif output-file: ${{ env.OUTPUT_FILE }} - go-version-input: ${{ env.GO_VERSION }} + go-version-file: "go.mod" - name: Upload SARIF file uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d2ed050..cc70aef 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,8 +13,6 @@ jobs: strategy: fail-fast: false matrix: - go-version: - - 1.25.x platform: - macos-latest - windows-latest @@ -29,7 +27,7 @@ jobs: uses: actions/setup-go@def8c394e3ad351a79bc93815e4a585520fe993b with: check-latest: true - go-version: ${{ matrix.go-version }} + go-version-file: "go.mod" cache: true cache-dependency-path: "**/go.sum" diff --git a/.gitignore b/.gitignore index 9766e11..2b98e20 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins +/bin/ *.exe *.exe~ *.dll @@ -21,4 +22,4 @@ go.work # Ignore VSCode settings.json -.vscode/settings.json \ No newline at end of file +.vscode/settings.json diff --git a/.golangci.yaml b/.golangci.yaml index c778be5..abcbe34 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -131,6 +131,26 @@ linters: # Linter Settings Configuration ###################################################################################################### settings: + gocritic: + enable-all: true + disabled-checks: + - unnamedResult + revive: + rules: + - name: var-naming + severity: warning + disabled: false + exclude: [""] + arguments: + - [""] # AllowList + - [""] # DenyList + - - skip-initialism-name-checks: false + upper-case-const: true + skip-package-name-checks: true + skip-package-name-collision-with-go-std: true + extra-bad-package-names: + - helpers + - models varnamelen: max-distance: 5 min-name-length: 3 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index dc97721..c691356 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -16,6 +16,10 @@ builds: - arm - arm64 + ignore: + - goos: windows + goarch: arm + archives: - name_template: >- {{- .ProjectName }}_ diff --git a/.mockery.yaml b/.mockery.yaml index b10d426..56cf410 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -5,7 +5,6 @@ # go-remove: https://github.com/nicholas-fedor/go-remove/ # # Mockery: https://vektra.github.io/mockery/latest/ # # Configuration: https://vektra.github.io/mockery/latest/configuration # -# Mockery Version: 3.5.2 # # # ###################################################################################################### @@ -13,7 +12,7 @@ # Mockery Usage # # # # Install Mockery: # -# go install github.com/vektra/mockery/v3@v3.5.2 # +# go install github.com/vektra/mockery/v3@latest # # # # Run CLI command from root: # # mockery # diff --git a/README.md b/README.md index 9fd5945..6c8ade9 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ A command-line tool to remove Go binaries with ease -`go-remove` is a command-line tool for removing Go binaries from standard binary directories (`GOROOT/bin`, `GOBIN`, or `GOPATH/bin`). -It supports both direct removal of a specified binary and an interactive TUI (Terminal User Interface) mode for selecting binaries to delete. +`go-remove` is a simple CLI/TUI for removing Go binaries. ## Features diff --git a/cmd/root.go b/cmd/root.go index d9f6c05..9c41f67 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -74,6 +74,7 @@ var rootCmd = &cobra.Command{ log, ) } + switch logLevel { case "debug": zapLogger.Logger = zapLogger.WithOptions( @@ -92,6 +93,7 @@ var rootCmd = &cobra.Command{ zap.IncreaseLevel(zapcore.InfoLevel), ) } + log = zapLogger // Update the logger in deps deps.Logger = log } diff --git a/go.mod b/go.mod index 6c770fb..ef1bd22 100644 --- a/go.mod +++ b/go.mod @@ -3,40 +3,34 @@ module github.com/nicholas-fedor/go-remove go 1.26.0 require ( - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 + charm.land/bubbletea/v2 v2.0.0 + charm.land/lipgloss/v2 v2.0.0 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.1 ) require ( - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect -) - -require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.3.2 // indirect - github.com/charmbracelet/x/ansi v0.10.2 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7ef2007..667e38c 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,38 @@ -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/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= -github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= -github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= -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/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= -github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ= +charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= +charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= +github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= +github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 h1:Af/L28Xh+pddhouT/6lJ7IAIYfu5tWJOB0iqt+mXsYM= +github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73/go.mod h1:E6/0abq9uG2SnM8IbLB9Y5SW09uIgfaFETk8aRzgXUQ= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 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.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -58,14 +56,12 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 1c0e7c4..8a6b60e 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -28,7 +28,7 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" fsmocks "github.com/nicholas-fedor/go-remove/internal/fs/mocks" "github.com/nicholas-fedor/go-remove/internal/logger" diff --git a/internal/cli/mocks/ProgramRunner.go b/internal/cli/mocks/ProgramRunner.go index 609ef70..dec7538 100644 --- a/internal/cli/mocks/ProgramRunner.go +++ b/internal/cli/mocks/ProgramRunner.go @@ -5,7 +5,7 @@ package mocks import ( - "github.com/charmbracelet/bubbletea" + "charm.land/bubbletea/v2" mock "github.com/stretchr/testify/mock" ) diff --git a/internal/cli/tui.go b/internal/cli/tui.go index 9d3c2ef..02bb09b 100644 --- a/internal/cli/tui.go +++ b/internal/cli/tui.go @@ -24,9 +24,9 @@ import ( "sort" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/nicholas-fedor/go-remove/internal/fs" "github.com/nicholas-fedor/go-remove/internal/logger" @@ -81,9 +81,15 @@ type model struct { type DefaultRunner struct{} // RunTUI launches the interactive TUI mode for binary selection and removal. -func RunTUI(dir string, config Config, logger logger.Logger, fs fs.FS, runner ProgramRunner) error { +func RunTUI( + dir string, + config Config, + log logger.Logger, + filesystem fs.FS, + runner ProgramRunner, +) error { // Fetch available binaries from the specified directory. - choices := fs.ListBinaries(dir) + choices := filesystem.ListBinaries(dir) if len(choices) == 0 { return fmt.Errorf("%w: %s", ErrNoBinariesFound, dir) } @@ -93,13 +99,13 @@ func RunTUI(dir string, config Config, logger logger.Logger, fs fs.FS, runner Pr choices: choices, dir: dir, config: config, - logger: logger, - fs: fs, + logger: log, + fs: filesystem, cursorX: 0, cursorY: 0, sortAscending: true, styles: defaultStyleConfig(), - }, tea.WithAltScreen()) + }) if err != nil { return fmt.Errorf("failed to start TUI program: %w", err) } @@ -140,7 +146,7 @@ func (r DefaultRunner) RunProgram(m tea.Model, opts ...tea.ProgramOption) (*tea. func (m *model) Init() tea.Cmd { m.sortChoices() - return tea.EnterAltScreen // Switch to alternate screen buffer + return nil } // Update processes TUI events and updates the model state. @@ -300,10 +306,13 @@ func (m *model) updateGrid() { } } -// View renders the TUI interface as a string. -func (m *model) View() string { +// View renders the TUI interface as a tea.View. +func (m *model) View() tea.View { if len(m.choices) == 0 { - return "No binaries found.\n" + view := tea.NewView("No binaries found.\n") + view.AltScreen = true + + return view } // Apply configured styles for UI elements. @@ -378,10 +387,15 @@ func (m *model) View() string { s.WriteString(footer) - return lipgloss.NewStyle(). + content := lipgloss.NewStyle(). PaddingLeft(leftPadding). Width(m.width - leftPadding). Render(s.String()) + + view := tea.NewView(content) + view.AltScreen = true + + return view } // maximum returns the larger of two integers. diff --git a/internal/cli/tui_test.go b/internal/cli/tui_test.go index 6c61bbc..d8edf4e 100644 --- a/internal/cli/tui_test.go +++ b/internal/cli/tui_test.go @@ -27,7 +27,7 @@ import ( "github.com/stretchr/testify/mock" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" fsmocks "github.com/nicholas-fedor/go-remove/internal/fs/mocks" logmocks "github.com/nicholas-fedor/go-remove/internal/logger/mocks" @@ -38,7 +38,7 @@ type tuiMockRunner struct { runProgram func(tea.Model, ...tea.ProgramOption) (*tea.Program, error) } -// RunProgram executes the mock runner’s program function. +// RunProgram executes the mock runner's program function. func (m *tuiMockRunner) RunProgram( model tea.Model, opts ...tea.ProgramOption, @@ -46,7 +46,7 @@ func (m *tuiMockRunner) RunProgram( return m.runProgram(model, opts...) } -// TestRunTUI verifies the RunTUI function’s behavior under various conditions. +// TestRunTUI verifies the RunTUI function's behavior under various conditions. func TestRunTUI(t *testing.T) { type args struct { dir string @@ -128,7 +128,7 @@ func TestRunTUI(t *testing.T) { } } -// Test_model_Init verifies the Init method’s command output. +// Test_model_Init verifies the Init method's command output. func Test_model_Init(t *testing.T) { tests := []struct { name string @@ -138,7 +138,7 @@ func Test_model_Init(t *testing.T) { { name: "basic init", m: model{}, - want: tea.EnterAltScreen, + want: nil, }, } @@ -152,7 +152,36 @@ func Test_model_Init(t *testing.T) { } } -// Test_model_Update verifies the Update method’s state changes and commands. +// keyPress creates a KeyPressMsg with the given rune. +func keyPress(r rune) tea.KeyPressMsg { + return tea.KeyPressMsg{Text: string(r), Code: r, ShiftedCode: r} +} + +// keyPressString creates a KeyPressMsg from a string representation. +func keyPressString(s string) tea.KeyPressMsg { + switch s { + case "enter": + return tea.KeyPressMsg{Code: tea.KeyEnter} + case "up": + return tea.KeyPressMsg{Code: tea.KeyUp} + case "down": + return tea.KeyPressMsg{Code: tea.KeyDown} + case "left": + return tea.KeyPressMsg{Code: tea.KeyLeft} + case "right": + return tea.KeyPressMsg{Code: tea.KeyRight} + default: + if len(s) == 1 { + r := rune(s[0]) + + return tea.KeyPressMsg{Text: s, Code: r, ShiftedCode: r} + } + + return tea.KeyPressMsg{} + } +} + +// Test_model_Update verifies the Update method's state changes and commands. func Test_model_Update(t *testing.T) { type args struct { msg tea.Msg @@ -168,21 +197,21 @@ func Test_model_Update(t *testing.T) { { name: "quit with q", m: &model{}, - args: args{msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}}, + args: args{msg: keyPress('q')}, want: model{}, wantCmd: tea.Quit, }, { name: "move up", m: &model{cursorY: 1, rows: 2}, - args: args{msg: tea.KeyMsg{Type: tea.KeyUp}}, + args: args{msg: keyPressString("up")}, want: model{cursorY: 0, rows: 2}, wantCmd: nil, }, { name: "move down within bounds", m: &model{cursorY: 0, rows: 2, cols: 2, choices: []string{"a", "b", "c", "d"}}, - args: args{msg: tea.KeyMsg{Type: tea.KeyDown}}, + args: args{msg: keyPressString("down")}, want: model{cursorY: 1, rows: 2, cols: 2, choices: []string{"a", "b", "c", "d"}}, wantCmd: nil, }, @@ -195,7 +224,7 @@ func Test_model_Update(t *testing.T) { cols: 2, choices: []string{"a", "b", "c", "d"}, }, - args: args{msg: tea.KeyMsg{Type: tea.KeyDown}}, + args: args{msg: keyPressString("down")}, want: model{ cursorY: 1, cursorX: 1, @@ -208,14 +237,14 @@ func Test_model_Update(t *testing.T) { { name: "move left", m: &model{cursorX: 1, cols: 2}, - args: args{msg: tea.KeyMsg{Type: tea.KeyLeft}}, + args: args{msg: keyPressString("left")}, want: model{cursorX: 0, cols: 2}, wantCmd: nil, }, { name: "move right within bounds", m: &model{cursorX: 0, cols: 2, choices: []string{"a", "b"}}, - args: args{msg: tea.KeyMsg{Type: tea.KeyRight}}, + args: args{msg: keyPressString("right")}, want: model{cursorX: 1, cols: 2, choices: []string{"a", "b"}}, wantCmd: nil, }, @@ -229,7 +258,7 @@ func Test_model_Update(t *testing.T) { width: 80, height: 24, }, - args: args{msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}}}, + args: args{msg: keyPress('s')}, want: model{ choices: []string{"vhs", "age"}, sortAscending: false, @@ -250,7 +279,7 @@ func Test_model_Update(t *testing.T) { width: 80, height: 24, }, - args: args{msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}}}, + args: args{msg: keyPress('s')}, want: model{ choices: []string{"age", "vhs"}, sortAscending: true, @@ -286,7 +315,7 @@ func Test_model_Update(t *testing.T) { return m }(), }, - args: args{msg: tea.KeyMsg{Type: tea.KeyEnter}}, + args: args{msg: keyPressString("enter")}, want: model{ choices: []string{"vhs"}, cols: 1, @@ -324,7 +353,7 @@ func Test_model_Update(t *testing.T) { return m }(), }, - args: args{msg: tea.KeyMsg{Type: tea.KeyEnter}}, + args: args{msg: keyPressString("enter")}, want: model{ choices: []string{"age"}, cols: 1, @@ -390,7 +419,7 @@ func Test_model_Update(t *testing.T) { } } -// Test_model_updateGrid verifies the updateGrid method’s layout calculations. +// Test_model_updateGrid verifies the updateGrid method's layout calculations. func Test_model_updateGrid(t *testing.T) { tests := []struct { name string @@ -467,7 +496,7 @@ func stripANSI(str string) string { return ansiRegex.ReplaceAllString(str, "") } -// Test_model_View verifies the View method’s rendered output. +// Test_model_View verifies the View method's rendered output. func Test_model_View(t *testing.T) { const contentWidth = 78 // Max visible width for content (excluding leftPadding) @@ -513,14 +542,14 @@ func Test_model_View(t *testing.T) { }, want: func() string { lines := make([]string, 0, 25) + lines = append( lines, leftPaddingStr+pad("Select a binary to remove:", effectiveWidth), + leftPaddingStr+pad("", effectiveWidth), + leftPaddingStr+pad("❯ vhs", effectiveWidth), + leftPaddingStr+pad("", effectiveWidth), ) - lines = append(lines, leftPaddingStr+pad("", effectiveWidth)) - lines = append(lines, leftPaddingStr+pad("❯ vhs", effectiveWidth)) - - lines = append(lines, leftPaddingStr+pad("", effectiveWidth)) for range 19 { lines = append(lines, leftPaddingStr+pad("", effectiveWidth)) } @@ -528,8 +557,11 @@ func Test_model_View(t *testing.T) { footerPart1 := "↑/k: up ↓/j: down ←/h: left →/l: right Enter: remove s: toggle sort q:" footerPart2 := "quit" - lines = append(lines, leftPaddingStr+pad(footerPart1, effectiveWidth)) - lines = append(lines, leftPaddingStr+pad(footerPart2, effectiveWidth)) + lines = append( + lines, + leftPaddingStr+pad(footerPart1, effectiveWidth), + leftPaddingStr+pad(footerPart2, effectiveWidth), + ) return strings.Join(lines, "\n") }(), @@ -550,19 +582,16 @@ func Test_model_View(t *testing.T) { }, want: func() string { lines := make([]string, 0, 25) + lines = append( lines, leftPaddingStr+pad("Select a binary to remove:", effectiveWidth), + leftPaddingStr+pad("", effectiveWidth), + leftPaddingStr+pad("❯ age", effectiveWidth), + leftPaddingStr+pad(" vhs", effectiveWidth), // Adjusted to match actual padding + leftPaddingStr+pad("", effectiveWidth), + leftPaddingStr+pad("Removed tool", effectiveWidth), ) - lines = append(lines, leftPaddingStr+pad("", effectiveWidth)) - lines = append(lines, leftPaddingStr+pad("❯ age", effectiveWidth)) - lines = append(lines, leftPaddingStr+pad( - " vhs", - effectiveWidth, - )) // Adjusted to match actual padding - lines = append(lines, leftPaddingStr+pad("", effectiveWidth)) - - lines = append(lines, leftPaddingStr+pad("Removed tool", effectiveWidth)) for range 17 { // Adjusted for rows=2 lines = append(lines, leftPaddingStr+pad("", effectiveWidth)) } @@ -570,8 +599,11 @@ func Test_model_View(t *testing.T) { footerPart1 := "↑/k: up ↓/j: down ←/h: left →/l: right Enter: remove s: toggle sort q:" footerPart2 := "quit" - lines = append(lines, leftPaddingStr+pad(footerPart1, effectiveWidth)) - lines = append(lines, leftPaddingStr+pad(footerPart2, effectiveWidth)) + lines = append( + lines, + leftPaddingStr+pad(footerPart1, effectiveWidth), + leftPaddingStr+pad(footerPart2, effectiveWidth), + ) return strings.Join(lines, "\n") }(), @@ -583,7 +615,7 @@ func Test_model_View(t *testing.T) { tt.m.sortChoices() tt.m.updateGrid() - got := stripANSI(tt.m.View()) + got := stripANSI(tt.m.View().Content) if got != tt.want { t.Errorf("model.View() got = %q, want %q", got, tt.want) } @@ -591,7 +623,7 @@ func Test_model_View(t *testing.T) { } } -// Test_max verifies the max function’s comparison logic. +// Test_max verifies the max function's comparison logic. func Test_max(t *testing.T) { tests := []struct { name string @@ -613,7 +645,7 @@ func Test_max(t *testing.T) { } } -// Test_min verifies the min function’s comparison logic. +// Test_min verifies the min function's comparison logic. func Test_min(t *testing.T) { tests := []struct { name string diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 96701c2..82274b3 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -102,14 +102,14 @@ func (r *RealFS) AdjustBinaryPath(dir, binary string) string { } // RemoveBinary deletes a binary file from the filesystem. -func (r *RealFS) RemoveBinary(binaryPath, name string, verbose bool, logger logger.Logger) error { +func (r *RealFS) RemoveBinary(binaryPath, name string, verbose bool, log logger.Logger) error { // Verify the binary exists before attempting removal. if _, err := os.Stat(binaryPath); os.IsNotExist(err) { return fmt.Errorf("%w: %s at %s", ErrBinaryNotFound, name, binaryPath) } // Log debug and info messages if verbose mode is enabled. - sugar := logger.Sugar() + sugar := log.Sugar() if verbose { sugar.Debugf("Constructed binary path: %s", binaryPath) sugar.Infof("Removing binary: %s", binaryPath) diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go index b1ce7d6..c262d4d 100644 --- a/internal/fs/fs_test.go +++ b/internal/fs/fs_test.go @@ -70,8 +70,8 @@ func TestRealFS_DetermineBinDir(t *testing.T) { name: "useGoroot with GOROOT set", r: &RealFS{}, args: args{useGoroot: true}, - env: map[string]string{"GOROOT": "/go"}, - want: filepath.Join("/go", "bin"), + env: map[string]string{"GOROOT": filepath.FromSlash("/go")}, + want: filepath.FromSlash("/go/bin"), wantErr: false, }, { @@ -86,16 +86,16 @@ func TestRealFS_DetermineBinDir(t *testing.T) { name: "use GOBIN", r: &RealFS{}, args: args{useGoroot: false}, - env: map[string]string{"GOBIN": "/custom/bin"}, - want: "/custom/bin", + env: map[string]string{"GOBIN": filepath.FromSlash("/custom/bin")}, + want: filepath.FromSlash("/custom/bin"), wantErr: false, }, { name: "use GOPATH/bin when GOBIN unset", r: &RealFS{}, args: args{useGoroot: false}, - env: map[string]string{"GOPATH": "/gopath", "GOBIN": ""}, - want: filepath.Join("/gopath", "bin"), + env: map[string]string{"GOPATH": filepath.FromSlash("/gopath"), "GOBIN": ""}, + want: filepath.FromSlash("/gopath/bin"), wantErr: false, }, { @@ -160,8 +160,8 @@ func TestRealFS_AdjustBinaryPath(t *testing.T) { { name: "basic path", r: &RealFS{}, - args: args{dir: "/bin", binary: "tool"}, - want: filepath.Join("/bin", "tool") + func() string { + args: args{dir: filepath.FromSlash("/bin"), binary: "tool"}, + want: filepath.FromSlash("/bin/tool") + func() string { if runtime.GOOS == windowsOS { return windowsExt } @@ -172,8 +172,8 @@ func TestRealFS_AdjustBinaryPath(t *testing.T) { { name: "empty binary", r: &RealFS{}, - args: args{dir: "/bin", binary: ""}, - want: filepath.Join("/bin"), //nolint:gocritic // Single argument intentional + args: args{dir: filepath.FromSlash("/bin"), binary: ""}, + want: filepath.FromSlash("/bin"), }, } if runtime.GOOS == windowsOS { @@ -185,8 +185,8 @@ func TestRealFS_AdjustBinaryPath(t *testing.T) { }{ name: "windows adds .exe", r: &RealFS{}, - args: args{dir: "/bin", binary: "tool"}, - want: filepath.Join("/bin", "tool.exe"), + args: args{dir: filepath.FromSlash("/bin"), binary: "tool"}, + want: filepath.FromSlash("/bin/tool.exe"), }) }