diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5dd59b2..ef15824 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 ./... diff --git a/go.mod b/go.mod index baabb63..7cf7882 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index deedf30..e5d8542 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/app/app.go b/internal/app/app.go index 95af2b8..ebb7e8f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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 { diff --git a/internal/app/app_test.go b/internal/app/app_test.go new file mode 100644 index 0000000..fcbdec3 --- /dev/null +++ b/internal/app/app_test.go @@ -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)) + } +} diff --git a/internal/app/edit_cell.go b/internal/app/edit_cell.go index 2260c6c..074f6fb 100644 --- a/internal/app/edit_cell.go +++ b/internal/app/edit_cell.go @@ -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 -} \ No newline at end of file +} diff --git a/internal/app/edit_cell_keys.go b/internal/app/edit_cell_keys.go index 6789722..43ca178 100644 --- a/internal/app/edit_cell_keys.go +++ b/internal/app/edit_cell_keys.go @@ -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}, } -} \ No newline at end of file +} diff --git a/internal/app/query.go b/internal/app/query.go index 398c42a..aa48759 100644 --- a/internal/app/query.go +++ b/internal/app/query.go @@ -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() } - diff --git a/internal/app/query_keys.go b/internal/app/query_keys.go index 5ef7ee4..320db18 100644 --- a/internal/app/query_keys.go +++ b/internal/app/query_keys.go @@ -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}, } -} \ No newline at end of file +} diff --git a/internal/app/row_detail.go b/internal/app/row_detail.go index 92a5292..c0fe1b3 100644 --- a/internal/app/row_detail.go +++ b/internal/app/row_detail.go @@ -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() -} \ No newline at end of file +} diff --git a/internal/app/row_detail_keys.go b/internal/app/row_detail_keys.go index 361d8c0..f7ae202 100644 --- a/internal/app/row_detail_keys.go +++ b/internal/app/row_detail_keys.go @@ -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}, } -} \ No newline at end of file +} diff --git a/internal/app/table_data.go b/internal/app/table_data.go index 19018ff..808649b 100644 --- a/internal/app/table_data.go +++ b/internal/app/table_data.go @@ -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() -} \ No newline at end of file +} diff --git a/internal/app/table_data_keys.go b/internal/app/table_data_keys.go index 574bd38..e2b7b39 100644 --- a/internal/app/table_data_keys.go +++ b/internal/app/table_data_keys.go @@ -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}, } -} \ No newline at end of file +} diff --git a/internal/app/table_list.go b/internal/app/table_list.go index 42ecda5..a0b9e86 100644 --- a/internal/app/table_list.go +++ b/internal/app/table_list.go @@ -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() -} \ No newline at end of file +} diff --git a/internal/app/table_list_keys.go b/internal/app/table_list_keys.go index a5a0667..62315a3 100644 --- a/internal/app/table_list_keys.go +++ b/internal/app/table_list_keys.go @@ -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}, } -} \ No newline at end of file +}