feat(tests): add comprehensive test suite, update deps and Go 1.26.1

- Add 20+ tests for utility functions, SharedData, and Model
- Tests cover: LoadTables, LoadTableData, UpdateCell, pagination,
  table inference, focus/blur, empty database, invalid indices
- Update Go to 1.26.1, upgrade all dependencies
- Replace custom Min/Max with Go builtin min/max
- Format all files with goimports
- Add staticcheck to CI workflow
This commit is contained in:
2026-03-13 02:46:03 +00:00
parent 7913b17d74
commit aa4c97c553
15 changed files with 581 additions and 169 deletions

View File

@@ -14,3 +14,6 @@ jobs:
- run: go test -race ./...
- run: go vet ./...
- run: go build ./...
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: staticcheck ./...

37
go.mod
View File

@@ -1,32 +1,30 @@
module github.com/taigrr/teaqlite
go 1.26.0
go 1.26.1
require (
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/fang v0.4.4
github.com/charmbracelet/fang v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/spf13/cobra v1.10.2
modernc.org/sqlite v1.46.1
)
require (
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect
charm.land/lipgloss/v2 v2.0.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250711012602-b1f986320f7e // indirect
github.com/charmbracelet/x/exp/color v0.0.0-20250711012602-b1f986320f7e // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260311145557-c83711a11ffa // 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.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -34,24 +32,25 @@ require (
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/mattn/go-runewidth v0.0.21 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-cobra v1.2.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/mango-cobra v1.3.0 // indirect
github.com/muesli/mango-pflag v0.2.0 // indirect
github.com/muesli/roff v0.1.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.27.0 // indirect
modernc.org/libc v1.67.6 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

91
go.sum
View File

@@ -1,33 +1,31 @@
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
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.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
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.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ=
github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
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/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ=
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0=
github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 h1:J8v4kWJYCaxv1SLhLunN74S+jMteZ1f7Dae99ioq4Bo=
github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188/go.mod h1:FzWNAbe1jEmI+GZljSnlaSA8wJjnNIZhWBLkTsAl6eg=
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/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250711012602-b1f986320f7e h1:sc41kBOnun1OX15Lg05ZB6Ly6AFWnntCYb8jsEDBAPs=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250711012602-b1f986320f7e/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
github.com/charmbracelet/x/exp/color v0.0.0-20250711012602-b1f986320f7e h1:D0tltuLCSvxMznOpQg7f3MArp8ImU0zALbakI47ffkw=
github.com/charmbracelet/x/exp/color v0.0.0-20250711012602-b1f986320f7e/go.mod h1:hk/GyTELmEgX54pBAOHcFvH8Xed53JWo/g8kJXFo/PI=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260311145557-c83711a11ffa h1:/hY9CTFQJJ7G5Hu0MFAZTUXV/JO8H8FOIdWKvRA+tTw=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260311145557-c83711a11ffa/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY=
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=
@@ -36,12 +34,10 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
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.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
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=
@@ -63,18 +59,18 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/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/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k=
github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
@@ -90,44 +86,45 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=

View File

@@ -522,20 +522,6 @@ func WrapText(text string, width int) []string {
return lines
}
func Min(a, b int) int {
if a < b {
return a
}
return b
}
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func InitialModel(db *sql.DB, opts ...Option) *Model {
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {

428
internal/app/app_test.go Normal file
View File

@@ -0,0 +1,428 @@
package app
import (
"database/sql"
"testing"
_ "modernc.org/sqlite"
)
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
want string
}{
{"short string", "hello", 10, "hello"},
{"exact length", "hello", 5, "hello"},
{"needs truncation", "hello world", 8, "hello..."},
{"empty string", "", 5, ""},
{"min truncation", "abcdef", 4, "a..."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := TruncateString(tt.input, tt.maxLen)
if got != tt.want {
t.Errorf("TruncateString(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
}
})
}
}
func TestWrapText(t *testing.T) {
tests := []struct {
name string
text string
width int
want []string
}{
{"short text", "hello", 20, []string{"hello"}},
{"wrap at word boundary", "hello world foo", 12, []string{"hello world", "foo"}},
{"zero width", "hello", 0, []string{"hello"}},
{"empty text", "", 10, []string{""}},
{"single long word", "abcdefghij", 5, []string{"abcde", "fghij"}},
{"multiple words wrapping", "one two three four", 10, []string{"one two", "three four"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := WrapText(tt.text, tt.width)
if len(got) != len(tt.want) {
t.Errorf("WrapText(%q, %d) returned %d lines, want %d: %v", tt.text, tt.width, len(got), len(tt.want), got)
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("WrapText(%q, %d)[%d] = %q, want %q", tt.text, tt.width, i, got[i], tt.want[i])
}
}
})
}
}
func TestNextID(t *testing.T) {
// nextID should return monotonically increasing values
id1 := nextID()
id2 := nextID()
id3 := nextID()
if id2 <= id1 {
t.Errorf("nextID() not monotonically increasing: %d, %d", id1, id2)
}
if id3 <= id2 {
t.Errorf("nextID() not monotonically increasing: %d, %d", id2, id3)
}
}
func TestDefaultAppKeyMap(t *testing.T) {
km := DefaultAppKeyMap()
if len(km.Quit.Keys()) == 0 {
t.Error("Quit keybinding has no keys")
}
if len(km.Suspend.Keys()) == 0 {
t.Error("Suspend keybinding has no keys")
}
if len(km.ToggleHelp.Keys()) == 0 {
t.Error("ToggleHelp keybinding has no keys")
}
}
// createTestDB creates an in-memory SQLite database with test data
func createTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("failed to open test database: %v", err)
}
_, err = db.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
);
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
CREATE TABLE products (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
price REAL
);
INSERT INTO products (title, price) VALUES ('Widget', 9.99);
INSERT INTO products (title, price) VALUES ('Gadget', 19.99);
`)
if err != nil {
t.Fatalf("failed to create test data: %v", err)
}
return db
}
func TestNewSharedData(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if shared.DB != db {
t.Error("NewSharedData should store the database reference")
}
if shared.Width != 80 || shared.Height != 24 {
t.Errorf("default dimensions should be 80x24, got %dx%d", shared.Width, shared.Height)
}
}
func TestSharedDataLoadTables(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
if len(shared.Tables) != 2 {
t.Fatalf("expected 2 tables, got %d: %v", len(shared.Tables), shared.Tables)
}
// Tables should be sorted alphabetically
if shared.Tables[0] != "products" || shared.Tables[1] != "users" {
t.Errorf("expected [products, users], got %v", shared.Tables)
}
// FilteredTables should be a copy of Tables
if len(shared.FilteredTables) != len(shared.Tables) {
t.Errorf("FilteredTables length mismatch: %d vs %d", len(shared.FilteredTables), len(shared.Tables))
}
}
func TestSharedDataLoadTableData(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
// Load "users" table (index 1 since sorted alphabetically)
shared.SelectedTable = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() failed: %v", err)
}
if len(shared.Columns) != 3 {
t.Fatalf("expected 3 columns, got %d: %v", len(shared.Columns), shared.Columns)
}
if shared.Columns[0] != "id" || shared.Columns[1] != "name" || shared.Columns[2] != "email" {
t.Errorf("unexpected columns: %v", shared.Columns)
}
if shared.TotalRows != 3 {
t.Errorf("expected 3 total rows, got %d", shared.TotalRows)
}
if len(shared.TableData) != 3 {
t.Fatalf("expected 3 data rows, got %d", len(shared.TableData))
}
// Check primary keys detected
if len(shared.PrimaryKeys) != 1 || shared.PrimaryKeys[0] != "id" {
t.Errorf("expected primary key [id], got %v", shared.PrimaryKeys)
}
// Should not be marked as query result
if shared.IsQueryResult {
t.Error("regular table load should not be marked as query result")
}
}
func TestSharedDataLoadTableDataInvalidIndex(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
shared.SelectedTable = 99
if err := shared.LoadTableData(); err == nil {
t.Error("LoadTableData() should fail with invalid table index")
}
}
func TestSharedDataUpdateCell(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
// Load users table
shared.SelectedTable = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() failed: %v", err)
}
// Update name of first user (row 0, col 1 = name)
if err := shared.UpdateCell(0, 1, "Alicia"); err != nil {
t.Fatalf("UpdateCell() failed: %v", err)
}
// Verify local data updated
if shared.FilteredData[0][1] != "Alicia" {
t.Errorf("FilteredData not updated, got %q", shared.FilteredData[0][1])
}
// Verify database updated
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = 1").Scan(&name)
if err != nil {
t.Fatalf("failed to verify update: %v", err)
}
if name != "Alicia" {
t.Errorf("database not updated, got %q", name)
}
}
func TestSharedDataUpdateCellInvalidIndex(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
shared.SelectedTable = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() failed: %v", err)
}
if err := shared.UpdateCell(99, 0, "value"); err == nil {
t.Error("UpdateCell() should fail with invalid row index")
}
if err := shared.UpdateCell(0, 99, "value"); err == nil {
t.Error("UpdateCell() should fail with invalid column index")
}
}
func TestSharedDataPagination(t *testing.T) {
db := createTestDB(t)
defer db.Close()
// Insert enough rows to test pagination
for i := 0; i < 25; i++ {
_, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)",
"User"+string(rune('A'+i)), "user@example.com")
if err != nil {
t.Fatalf("failed to insert test data: %v", err)
}
}
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
shared.SelectedTable = 1
shared.CurrentPage = 0
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() page 0 failed: %v", err)
}
if shared.TotalRows != 28 { // 3 original + 25 new
t.Errorf("expected 28 total rows, got %d", shared.TotalRows)
}
if len(shared.TableData) != PageSize {
t.Errorf("page 0 should have %d rows, got %d", PageSize, len(shared.TableData))
}
// Load page 2
shared.CurrentPage = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() page 1 failed: %v", err)
}
if len(shared.TableData) != 8 { // 28 - 20 = 8
t.Errorf("page 1 should have 8 rows, got %d", len(shared.TableData))
}
}
func TestInitialModel(t *testing.T) {
db := createTestDB(t)
defer db.Close()
m := InitialModel(db)
if m.Err() != nil {
t.Fatalf("InitialModel() returned error: %v", m.Err())
}
if m.width != 80 || m.height != 24 {
t.Errorf("default dimensions should be 80x24, got %dx%d", m.width, m.height)
}
if !m.Focused() {
t.Error("model should be focused by default")
}
}
func TestInitialModelWithOptions(t *testing.T) {
db := createTestDB(t)
defer db.Close()
km := DefaultAppKeyMap()
m := InitialModel(db, WithKeyMap(km), WithDimensions(120, 40))
if m.Err() != nil {
t.Fatalf("InitialModel() returned error: %v", m.Err())
}
if m.width != 120 || m.height != 40 {
t.Errorf("custom dimensions should be 120x40, got %dx%d", m.width, m.height)
}
}
func TestModelFocusBlur(t *testing.T) {
db := createTestDB(t)
defer db.Close()
m := InitialModel(db)
if !m.Focused() {
t.Error("model should be focused initially")
}
m.Blur()
if m.Focused() {
t.Error("model should not be focused after Blur()")
}
m.Focus()
if !m.Focused() {
t.Error("model should be focused after Focus()")
}
}
func TestInferTableFromQueryResult(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
// Set up columns matching the users table
shared.Columns = []string{"id", "name", "email"}
shared.IsQueryResult = true
shared.FilteredData = [][]string{{"1", "Alice", "alice@example.com"}}
tableName, err := shared.inferTableFromQueryResult(0, 0)
if err != nil {
t.Fatalf("inferTableFromQueryResult() failed: %v", err)
}
if tableName != "users" {
t.Errorf("expected 'users', got %q", tableName)
}
// Verify it was cached
if shared.QueryTableName != "users" {
t.Errorf("QueryTableName should be cached as 'users', got %q", shared.QueryTableName)
}
}
func TestGetTableInfo(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
cols, pks, err := shared.getTableInfo("users")
if err != nil {
t.Fatalf("getTableInfo() failed: %v", err)
}
if len(cols) != 3 {
t.Errorf("expected 3 columns, got %d", len(cols))
}
if len(pks) != 1 || pks[0] != "id" {
t.Errorf("expected primary key [id], got %v", pks)
}
}
func TestSharedDataEmptyDatabase(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("failed to open test database: %v", err)
}
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() on empty db failed: %v", err)
}
if len(shared.Tables) != 0 {
t.Errorf("expected 0 tables in empty db, got %d", len(shared.Tables))
}
}

View File

@@ -4,10 +4,10 @@ import (
"fmt"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
type EditCellModel struct {
@@ -151,7 +151,7 @@ func (m *EditCellModel) View() string {
content := fmt.Sprintf("%s\n\n", TitleStyle.Render(fmt.Sprintf("Edit Cell: %s", columnName)))
content += fmt.Sprintf("Value: %s\n\n", m.input.View())
if m.showFullHelp {
content += m.help.FullHelpView(m.keyMap.FullHelp())
} else {
@@ -159,4 +159,4 @@ func (m *EditCellModel) View() string {
}
return content
}
}

View File

@@ -4,17 +4,17 @@ import "github.com/charmbracelet/bubbles/key"
// EditCellKeyMap defines keybindings for the edit cell view
type EditCellKeyMap struct {
Save key.Binding
Cancel key.Binding
CursorLeft key.Binding
CursorRight key.Binding
WordLeft key.Binding
WordRight key.Binding
LineStart key.Binding
LineEnd key.Binding
DeleteWord key.Binding
DeleteChar key.Binding
ToggleHelp key.Binding
Save key.Binding
Cancel key.Binding
CursorLeft key.Binding
CursorRight key.Binding
WordLeft key.Binding
WordRight key.Binding
LineStart key.Binding
LineEnd key.Binding
DeleteWord key.Binding
DeleteChar key.Binding
ToggleHelp key.Binding
}
// DefaultEditCellKeyMap returns the default keybindings for edit cell
@@ -79,4 +79,4 @@ func (k EditCellKeyMap) FullHelp() [][]key.Binding {
{k.CursorLeft, k.CursorRight, k.WordLeft, k.WordRight},
{k.LineStart, k.LineEnd, k.DeleteWord, k.DeleteChar, k.ToggleHelp},
}
}
}

View File

@@ -418,7 +418,7 @@ func (m *QueryModel) View() string {
content.WriteString("\n")
// Data rows with scrolling
visibleCount := Max(1, m.Shared.Height-10)
visibleCount := max(1, m.Shared.Height-10)
startIdx := 0
// Adjust start index if selected row is out of view
@@ -426,7 +426,7 @@ func (m *QueryModel) View() string {
startIdx = m.selectedRow - visibleCount + 1
}
endIdx := Min(len(m.results), startIdx+visibleCount)
endIdx := min(len(m.results), startIdx+visibleCount)
for i := range endIdx {
if i < startIdx {
@@ -465,4 +465,3 @@ func (m *QueryModel) View() string {
return content.String()
}

View File

@@ -8,25 +8,25 @@ import "github.com/charmbracelet/bubbles/key"
// - G: go to end (single 'G' press)
type QueryKeyMap struct {
// Input mode keys
Execute key.Binding
Escape key.Binding
CursorLeft key.Binding
CursorRight key.Binding
WordLeft key.Binding
WordRight key.Binding
LineStart key.Binding
LineEnd key.Binding
DeleteWord key.Binding
Execute key.Binding
Escape key.Binding
CursorLeft key.Binding
CursorRight key.Binding
WordLeft key.Binding
WordRight key.Binding
LineStart key.Binding
LineEnd key.Binding
DeleteWord key.Binding
// Results mode keys
Up key.Binding
Down key.Binding
Enter key.Binding
EditQuery key.Binding
GoToStart key.Binding
GoToEnd key.Binding
Back key.Binding
ToggleHelp key.Binding
Up key.Binding
Down key.Binding
Enter key.Binding
EditQuery key.Binding
GoToStart key.Binding
GoToEnd key.Binding
Back key.Binding
ToggleHelp key.Binding
}
// DefaultQueryKeyMap returns the default keybindings for query view
@@ -69,7 +69,7 @@ func DefaultQueryKeyMap() QueryKeyMap {
key.WithKeys("ctrl+w"),
key.WithHelp("ctrl+w", "delete word"),
),
// Results mode
Up: key.NewBinding(
key.WithKeys("up", "k"),
@@ -119,4 +119,4 @@ func (k QueryKeyMap) FullHelp() [][]key.Binding {
{k.CursorLeft, k.CursorRight, k.WordLeft, k.WordRight},
{k.LineStart, k.LineEnd, k.DeleteWord, k.ToggleHelp},
}
}
}

View File

@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)
type RowDetailModel struct {
@@ -171,7 +171,7 @@ func (m *RowDetailModel) View() string {
if availableWidth < 20 {
availableWidth = 20 // Minimum width
}
if len(value) > availableWidth {
// Wrap long values
lines := WrapText(value, availableWidth)
@@ -195,4 +195,4 @@ func (m *RowDetailModel) View() string {
}
return content.String()
}
}

View File

@@ -66,4 +66,4 @@ func (k RowDetailKeyMap) FullHelp() [][]key.Binding {
{k.Up, k.Down, k.Enter},
{k.Escape, k.Back, k.GoToStart, k.GoToEnd, k.ToggleHelp},
}
}
}

View File

@@ -5,10 +5,10 @@ import (
"sort"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
type TableDataModel struct {
@@ -265,10 +265,10 @@ func (m *TableDataModel) filterData() {
row []string
score int
}
var matches []rowMatch
searchLower := strings.ToLower(searchValue)
for _, row := range m.Shared.TableData {
bestScore := 0
// Check each cell in the row and take the best score
@@ -278,17 +278,17 @@ func (m *TableDataModel) filterData() {
bestScore = score
}
}
if bestScore > 0 {
matches = append(matches, rowMatch{row: row, score: bestScore})
}
}
// Sort by score (highest first)
sort.Slice(matches, func(i, j int) bool {
return matches[i].score > matches[j].score
})
// Extract sorted rows
m.Shared.FilteredData = make([][]string, len(matches))
for i, match := range matches {
@@ -307,65 +307,65 @@ func (m *TableDataModel) fuzzyScore(text, pattern string) int {
if pattern == "" {
return 1
}
textLen := len(text)
patternLen := len(pattern)
if patternLen > textLen {
return 0
}
// Exact match gets highest score
if text == pattern {
return 1000
}
// Prefix match gets high score
if strings.HasPrefix(text, pattern) {
return 900
}
// Contains match gets medium score
if strings.Contains(text, pattern) {
return 800
}
// Fuzzy character sequence matching
score := 0
textIdx := 0
patternIdx := 0
consecutiveMatches := 0
for textIdx < textLen && patternIdx < patternLen {
if text[textIdx] == pattern[patternIdx] {
score += 10
consecutiveMatches++
// Bonus for consecutive matches
if consecutiveMatches > 1 {
score += consecutiveMatches * 5
}
// Bonus for matches at word boundaries
if textIdx == 0 || text[textIdx-1] == '_' || text[textIdx-1] == '-' || text[textIdx-1] == ' ' {
score += 20
}
patternIdx++
} else {
consecutiveMatches = 0
}
textIdx++
}
// Must match all pattern characters
if patternIdx < patternLen {
return 0
}
// Bonus for shorter text (more precise match)
score += (100 - textLen)
return score
}
@@ -409,18 +409,18 @@ func (m *TableDataModel) View() string {
content.WriteString("\n")
// Show data rows with scrolling within current page
visibleCount := Max(1, m.Shared.Height-10)
visibleCount := max(1, m.Shared.Height-10)
totalRows := len(m.Shared.FilteredData)
startIdx := 0
// If there are more rows than can fit on screen, scroll the view
if totalRows > visibleCount && m.selectedRow >= visibleCount {
startIdx = m.selectedRow - visibleCount + 1
// Ensure we don't scroll past the end
startIdx = min(startIdx, totalRows-visibleCount)
}
endIdx := Min(totalRows, startIdx+visibleCount)
endIdx := min(totalRows, startIdx+visibleCount)
for i := startIdx; i < endIdx; i++ {
row := m.Shared.FilteredData[i]
@@ -453,4 +453,4 @@ func (m *TableDataModel) View() string {
}
return content.String()
}
}

View File

@@ -92,4 +92,4 @@ func (k TableDataKeyMap) FullHelp() [][]key.Binding {
{k.Enter, k.Search, k.Escape, k.Back},
{k.GoToStart, k.GoToEnd, k.Refresh, k.SQLMode, k.ToggleHelp},
}
}
}

View File

@@ -5,10 +5,10 @@ import (
"sort"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
type TableListModel struct {
@@ -254,22 +254,22 @@ func (m *TableListModel) filterTables() {
name string
score int
}
var matches []tableMatch
searchLower := strings.ToLower(searchValue)
for _, table := range m.Shared.Tables {
score := m.fuzzyScore(strings.ToLower(table), searchLower)
if score > 0 {
matches = append(matches, tableMatch{name: table, score: score})
}
}
// Sort by score (highest first)
sort.Slice(matches, func(i, j int) bool {
return matches[i].score > matches[j].score
})
// Extract sorted table names
m.Shared.FilteredTables = make([]string, len(matches))
for i, match := range matches {
@@ -289,65 +289,65 @@ func (m *TableListModel) fuzzyScore(text, pattern string) int {
if pattern == "" {
return 1
}
textLen := len(text)
patternLen := len(pattern)
if patternLen > textLen {
return 0
}
// Exact match gets highest score
if text == pattern {
return 1000
}
// Prefix match gets high score
if strings.HasPrefix(text, pattern) {
return 900
}
// Contains match gets medium score
if strings.Contains(text, pattern) {
return 800
}
// Fuzzy character sequence matching
score := 0
textIdx := 0
patternIdx := 0
consecutiveMatches := 0
for textIdx < textLen && patternIdx < patternLen {
if text[textIdx] == pattern[patternIdx] {
score += 10
consecutiveMatches++
// Bonus for consecutive matches
if consecutiveMatches > 1 {
score += consecutiveMatches * 5
}
// Bonus for matches at word boundaries
if textIdx == 0 || text[textIdx-1] == '_' || text[textIdx-1] == '-' {
score += 20
}
patternIdx++
} else {
consecutiveMatches = 0
}
textIdx++
}
// Must match all pattern characters
if patternIdx < patternLen {
return 0
}
// Bonus for shorter text (more precise match)
score += (100 - textLen)
return score
}
@@ -356,7 +356,7 @@ func (m *TableListModel) getVisibleCount() int {
if m.searching {
reservedLines += 2
}
return Max(1, m.Shared.Height-reservedLines)
return max(1, m.Shared.Height-reservedLines)
}
func (m *TableListModel) adjustPage() {
@@ -389,7 +389,7 @@ func (m *TableListModel) View() string {
} else {
visibleCount := m.getVisibleCount()
startIdx := m.currentPage * visibleCount
endIdx := Min(startIdx+visibleCount, len(m.Shared.FilteredTables))
endIdx := min(startIdx+visibleCount, len(m.Shared.FilteredTables))
for i := startIdx; i < endIdx; i++ {
table := m.Shared.FilteredTables[i]
@@ -419,4 +419,4 @@ func (m *TableListModel) View() string {
}
return content.String()
}
}

View File

@@ -87,4 +87,4 @@ func (k TableListKeyMap) FullHelp() [][]key.Binding {
{k.Enter, k.Search, k.Escape, k.Refresh},
{k.GoToStart, k.GoToEnd, k.SQLMode, k.ToggleHelp},
}
}
}