diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-05 19:24:09 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-05 19:24:09 +0200 |
| commit | 30c955ef113e5e0c99c147ee4e7c8c20b0a7f273 (patch) | |
| tree | f872b7c44e62609a455dbed356fcc28ab3b53531 | |
| parent | 33064821d637aeef94fe6ee96edbac7a503c0692 (diff) | |
Migrate UI stack to Bubble Tea v2
| -rw-r--r-- | cmd/tasksamurai/main.go | 4 | ||||
| -rw-r--r-- | go.mod | 37 | ||||
| -rw-r--r-- | go.sum | 81 | ||||
| -rw-r--r-- | internal/atable/table.go | 70 | ||||
| -rw-r--r-- | internal/ui/handlers.go | 146 | ||||
| -rw-r--r-- | internal/ui/keyhandlers.go | 118 | ||||
| -rw-r--r-- | internal/ui/table.go | 172 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 104 | ||||
| -rw-r--r-- | internal/ui/taskdetail.go | 14 |
9 files changed, 383 insertions, 363 deletions
diff --git a/cmd/tasksamurai/main.go b/cmd/tasksamurai/main.go index 96c8be8..15ee037 100644 --- a/cmd/tasksamurai/main.go +++ b/cmd/tasksamurai/main.go @@ -11,7 +11,7 @@ import ( "codeberg.org/snonux/tasksamurai/internal/task" "codeberg.org/snonux/tasksamurai/internal/ui" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" ) func main() { @@ -48,7 +48,7 @@ func main() { // previous command line artefacts behind. fmt.Print("\033[H\033[2J") - p := tea.NewProgram(&m, tea.WithAltScreen()) + p := tea.NewProgram(&m) if _, err := p.Run(); err != nil { fmt.Fprintln(os.Stderr, "error running ui:", err) os.Exit(1) @@ -1,33 +1,30 @@ module codeberg.org/snonux/tasksamurai -go 1.23.8 +go 1.24.2 require ( - github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.5 - github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/x/ansi v0.8.0 + charm.land/bubbles/v2 v2.0.0 + charm.land/bubbletea/v2 v2.0.1 + charm.land/lipgloss/v2 v2.0.0 + github.com/charmbracelet/x/ansi v0.11.6 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/magefile/mage v1.15.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magefile/mage v1.15.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.16 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // 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/lucasb-eyer/go-colorful v1.3.0 // 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/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect ) @@ -1,57 +1,48 @@ +charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= +charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= +charm.land/bubbletea/v2 v2.0.1 h1:B8e9zzK7x9JJ+XvHGF4xnYu9Xa0E0y0MyggY6dbaCfQ= +charm.land/bubbletea/v2 v2.0.1/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/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= -github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/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.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -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/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-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= +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/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/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/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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= -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/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +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= diff --git a/internal/atable/table.go b/internal/atable/table.go index 44f1c9f..d31088e 100644 --- a/internal/atable/table.go +++ b/internal/atable/table.go @@ -4,11 +4,11 @@ package table import ( "strings" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/help" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" ) @@ -150,7 +150,7 @@ func New(opts ...Option) Model { m := Model{ cursor: 0, colCursor: 0, - viewport: viewport.New(0, 20), //nolint:mnd + viewport: viewport.New(viewport.WithWidth(0), viewport.WithHeight(20)), //nolint:mnd KeyMap: DefaultKeyMap(), Help: help.New(), @@ -184,14 +184,14 @@ func WithRows(rows []Row) Option { // WithHeight sets the height of the table. func WithHeight(h int) Option { return func(m *Model) { - m.viewport.Height = h - lipgloss.Height(m.headersView()) + m.viewport.SetHeight(h - lipgloss.Height(m.headersView())) } } // WithWidth sets the width of the table. func WithWidth(w int) Option { return func(m *Model) { - m.viewport.Width = w + m.viewport.SetWidth(w) } } @@ -230,20 +230,21 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: + height := m.viewport.Height() switch { case key.Matches(msg, m.KeyMap.LineUp): m.MoveUp(1) case key.Matches(msg, m.KeyMap.LineDown): m.MoveDown(1) case key.Matches(msg, m.KeyMap.PageUp): - m.MoveUp(m.viewport.Height) + m.MoveUp(height) case key.Matches(msg, m.KeyMap.PageDown): - m.MoveDown(m.viewport.Height) + m.MoveDown(height) case key.Matches(msg, m.KeyMap.HalfPageUp): - m.MoveUp(m.viewport.Height / 2) //nolint:mnd + m.MoveUp(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.HalfPageDown): - m.MoveDown(m.viewport.Height / 2) //nolint:mnd + m.MoveDown(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.GotoTop): m.GotoTop() case key.Matches(msg, m.KeyMap.GotoBottom): @@ -296,16 +297,17 @@ func (m Model) HelpView() string { // columns and rows. func (m *Model) UpdateViewport() { renderedRows := make([]string, 0, len(m.rows)) + height := m.viewport.Height() // Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height // Constant runtime, independent of number of rows in a table. // Limits the number of renderedRows to a maximum of 2*m.viewport.Height if m.cursor >= 0 { - m.start = clamp(m.cursor-m.viewport.Height, 0, m.cursor) + m.start = clamp(m.cursor-height, 0, m.cursor) } else { m.start = 0 } - m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.rows)) + m.end = clamp(m.cursor+height, m.cursor, len(m.rows)) for i := m.start; i < m.end; i++ { renderedRows = append(renderedRows, m.renderRow(i)) } @@ -349,24 +351,24 @@ func (m *Model) SetColumns(c []Column) { // SetWidth sets the width of the viewport of the table. func (m *Model) SetWidth(w int) { - m.viewport.Width = w + m.viewport.SetWidth(w) m.UpdateViewport() } // SetHeight sets the height of the viewport of the table. func (m *Model) SetHeight(h int) { - m.viewport.Height = h - lipgloss.Height(m.headersView()) + m.viewport.SetHeight(h - lipgloss.Height(m.headersView())) m.UpdateViewport() } // Height returns the viewport height of the table. func (m Model) Height() int { - return m.viewport.Height + return m.viewport.Height() } // Width returns the viewport width of the table. func (m Model) Width() int { - return m.viewport.Width + return m.viewport.Width() } // Cursor returns the index of the selected row. @@ -395,13 +397,15 @@ func (m *Model) SetColumnCursor(n int) { // It can not go above the first row. func (m *Model) MoveUp(n int) { m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1) + yOffset := m.viewport.YOffset() + height := m.viewport.Height() switch { case m.start == 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) - case m.start < m.viewport.Height: - m.viewport.YOffset = (clamp(clamp(m.viewport.YOffset+n, 0, m.cursor), 0, m.viewport.Height)) - case m.viewport.YOffset >= 1: - m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height) + m.viewport.SetYOffset(clamp(yOffset, 0, m.cursor)) + case m.start < height: + m.viewport.SetYOffset(clamp(clamp(yOffset+n, 0, m.cursor), 0, height)) + case yOffset >= 1: + m.viewport.SetYOffset(clamp(yOffset+n, 1, height)) } m.UpdateViewport() } @@ -411,15 +415,17 @@ func (m *Model) MoveUp(n int) { func (m *Model) MoveDown(n int) { m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1) m.UpdateViewport() + yOffset := m.viewport.YOffset() + height := m.viewport.Height() switch { - case m.end == len(m.rows) && m.viewport.YOffset > 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) - case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor)) - case m.viewport.YOffset > 1: - case m.cursor > m.viewport.YOffset+m.viewport.Height-1: - m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1)) + case m.end == len(m.rows) && yOffset > 0: + m.viewport.SetYOffset(clamp(yOffset-n, 1, height)) + case m.cursor > (m.end-m.start)/2 && yOffset > 0: + m.viewport.SetYOffset(clamp(yOffset-n, 1, m.cursor)) + case yOffset > 1: + case m.cursor > yOffset+height-1: + m.viewport.SetYOffset(clamp(yOffset+1, 0, 1)) } } @@ -529,7 +535,7 @@ func addSpacingStyled(cells []string, style lipgloss.Style) []string { spaced := make([]string, 0, len(cells)*2-1) for i, cell := range cells { if i > 0 { - spaced = append(spaced, style.Copy().Padding(0, 0).Render(" ")) + spaced = append(spaced, style.Padding(0, 0).Render(" ")) } spaced = append(spaced, cell) } diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 11a78cd..3122a34 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -6,17 +6,17 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" "github.com/charmbracelet/x/ansi" "codeberg.org/snonux/tasksamurai/internal/task" ) // handleTextInput provides generic text input handling for all input modes -func (m *Model) handleTextInput(msg tea.KeyMsg, input *textinput.Model, onEnter func(string) error, onExit func()) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleTextInput(msg tea.KeyPressMsg, input *textinput.Model, onEnter func(string) error, onExit func()) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": value := input.Value() if err := onEnter(value); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) @@ -29,7 +29,7 @@ func (m *Model) handleTextInput(msg tea.KeyMsg, input *textinput.Model, onEnter onExit() m.updateTableHeight() return m, nil - case tea.KeyEsc: + case "esc": input.Blur() onExit() m.updateTableHeight() @@ -41,13 +41,13 @@ func (m *Model) handleTextInput(msg tea.KeyMsg, input *textinput.Model, onEnter } // handleAnnotationMode handles keyboard input when in annotation mode -func (m *Model) handleAnnotationMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleAnnotationMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { // Annotation can be empty when replacing (to remove all) if !m.replaceAnnotations && strings.TrimSpace(value) == "" { return fmt.Errorf("annotation cannot be empty") } - + if m.replaceAnnotations { if err := task.ReplaceAnnotations(m.annotateID, value); err != nil { return err @@ -61,14 +61,14 @@ func (m *Model) handleAnnotationMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.annotating = false m.replaceAnnotations = false } - + model, cmd := m.handleTextInput(msg, &m.annotateInput, onEnter, onExit) - if msg.Type == tea.KeyEnter && m.annotateInput.Value() != "" { + if msg.String() == "enter" && m.annotateInput.Value() != "" { // Start blink after successful annotation return model, m.startBlink(m.annotateID, false) } @@ -76,7 +76,7 @@ func (m *Model) handleAnnotationMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleDescriptionMode handles keyboard input when editing description -func (m *Model) handleDescriptionMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleDescriptionMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { if err := validateDescription(value); err != nil { return err @@ -87,20 +87,20 @@ func (m *Model) handleDescriptionMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.descEditing = false } - + model, cmd := m.handleTextInput(msg, &m.descInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { return model, m.startBlink(m.descID, false) } return model, cmd } // handleTagsMode handles keyboard input when editing tags -func (m *Model) handleTagsMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleTagsMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { words := strings.Fields(value) var adds, removes []string @@ -136,13 +136,13 @@ func (m *Model) handleTagsMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.tagsEditing = false } - + model, cmd := m.handleTextInput(msg, &m.tagsInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { if m.showTaskDetail { // In detail view, blink the tags field return model, m.startDetailBlink(4) // Tags is field index 4 @@ -153,9 +153,9 @@ func (m *Model) handleTagsMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleDueEditMode handles due date editing -func (m *Model) handleDueEditMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleDueEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": if err := task.SetDueDate(m.dueID, m.dueDate.Format("2006-01-02")); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { @@ -174,12 +174,12 @@ func (m *Model) handleDueEditMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.updateTableHeight() return m, cmd - case tea.KeyEsc: + case "esc": m.dueEditing = false m.updateTableHeight() return m, nil } - + switch msg.String() { case "h", "left": m.dueDate = m.dueDate.AddDate(0, 0, -1) @@ -194,7 +194,7 @@ func (m *Model) handleDueEditMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleRecurrenceMode handles recurrence editing -func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleRecurrenceMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { if err := validateRecurrence(value); err != nil { return err @@ -205,13 +205,13 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.recurEditing = false } - + model, cmd := m.handleTextInput(msg, &m.recurInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { if m.showTaskDetail { // In detail view, blink the recurrence field (dynamic index) // Need to calculate the index based on whether recurrence field exists @@ -226,18 +226,18 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleProjectMode handles project editing -func (m *Model) handleProjectMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleProjectMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { return task.SetProject(m.projID, value) } - + onExit := func() { m.projEditing = false m.reload() } - + model, cmd := m.handleTextInput(msg, &m.projInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { if m.showTaskDetail { // In detail view, blink the project field return model, m.startDetailBlink(fieldProject) // Project field index in detail view @@ -248,9 +248,9 @@ func (m *Model) handleProjectMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handlePriorityMode handles priority selection -func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handlePriorityMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": priority := priorityOptions[m.priorityIndex] if err := validatePriority(priority); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) @@ -277,12 +277,12 @@ func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.updateTableHeight() return m, cmd - case tea.KeyEsc: + case "esc": m.prioritySelecting = false m.updateTableHeight() return m, nil } - + switch msg.String() { case "h", "left": m.priorityIndex = (m.priorityIndex + len(priorityOptions) - 1) % len(priorityOptions) @@ -293,29 +293,29 @@ func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleFilterMode handles filter editing -func (m *Model) handleFilterMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleFilterMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { m.filters = strings.Fields(value) _ = m.reload() return nil } - + onExit := func() { m.filterEditing = false } - + return m.handleTextInput(msg, &m.filterInput, onEnter, onExit) } // handleAddTaskMode handles adding a new task -func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleAddTaskMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": oldIDs := make(map[int]struct{}, len(m.tasks)) for _, tsk := range m.tasks { oldIDs[tsk.ID] = struct{}{} } - + if err := task.AddLine(m.addInput.Value()); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { @@ -323,11 +323,11 @@ func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { }) return m, cmd } - + m.addingTask = false m.addInput.Blur() m.reload() - + // Find the newly added task var newID int row := -1 @@ -338,7 +338,7 @@ func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { break } } - + m.updateTableHeight() if row >= 0 { prevRow := m.tbl.Cursor() @@ -349,23 +349,23 @@ func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, m.startBlink(newID, false) } return m, nil - - case tea.KeyEsc: + + case "esc": m.addingTask = false m.addInput.Blur() m.updateTableHeight() return m, nil } - + var cmd tea.Cmd m.addInput, cmd = m.addInput.Update(msg) return m, cmd } // handleSearchMode handles search input -func (m *Model) handleSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleSearchMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": pattern := m.searchInput.Value() if pattern != "" { // Check cache first @@ -388,7 +388,7 @@ func (m *Model) handleSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.searchInput.Blur() m.reload() m.updateTableHeight() - + if len(m.searchMatches) > 0 { match := m.searchMatches[m.searchIndex] prevRow := m.tbl.Cursor() @@ -398,23 +398,23 @@ func (m *Model) handleSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.updateSelectionHighlight(prevRow, m.tbl.Cursor(), prevCol, m.tbl.ColumnCursor()) } return m, nil - - case tea.KeyEsc: + + case "esc": m.searching = false m.searchInput.Blur() m.updateTableHeight() return m, nil } - + var cmd tea.Cmd m.searchInput, cmd = m.searchInput.Update(msg) return m, cmd } // handleHelpSearchMode handles search input in help mode -func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleHelpSearchMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": pattern := m.helpSearchInput.Value() if pattern != "" { // Check cache first @@ -435,7 +435,7 @@ func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.helpSearching = false m.helpSearchInput.Blur() - + // Find matching help lines m.helpSearchMatches = nil if m.helpSearchRegex != nil { @@ -451,13 +451,13 @@ func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } return m, nil - - case tea.KeyEsc: + + case "esc": m.helpSearching = false m.helpSearchInput.Blur() return m, nil } - + var cmd tea.Cmd m.helpSearchInput, cmd = m.helpSearchInput.Update(msg) return m, cmd @@ -465,7 +465,7 @@ func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // handleBlinkingState handles input when a task is blinking func (m *Model) handleBlinkingState(msg tea.Msg) (tea.Model, tea.Cmd) { - if _, ok := msg.(tea.KeyMsg); ok { + if _, ok := msg.(tea.KeyPressMsg); ok { // Only allow navigation while blinking prevRow := m.tbl.Cursor() prevCol := m.tbl.ColumnCursor() @@ -480,7 +480,7 @@ func (m *Model) handleBlinkingState(msg tea.Msg) (tea.Model, tea.Cmd) { } // handleEditingModes checks if we're in any editing mode and handles it -func (m *Model) handleEditingModes(msg tea.KeyMsg) (handled bool, model tea.Model, cmd tea.Cmd) { +func (m *Model) handleEditingModes(msg tea.KeyPressMsg) (handled bool, model tea.Model, cmd tea.Cmd) { switch { case m.annotating: model, cmd = m.handleAnnotationMode(msg) @@ -539,11 +539,11 @@ func (m *Model) getTaskAtCursor() *task.Task { } // handleTaskDetailMode handles keyboard input in task detail view -func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleTaskDetailMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if m.detailSearching { var cmd tea.Cmd - switch msg.Type { - case tea.KeyEnter: + switch msg.String() { + case "enter": pattern := m.detailSearchInput.Value() if pattern != "" { re, err := compileAndCacheRegex(pattern) @@ -559,7 +559,7 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.detailSearching = false m.detailSearchInput.Blur() return m, nil - case tea.KeyEsc, tea.KeyCtrlC: + case "esc", "ctrl+c": m.detailSearching = false m.detailSearchInput.Blur() return m, nil @@ -568,7 +568,7 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, cmd } } - + // Normal task detail view mode switch msg.String() { case "q", "esc": @@ -605,7 +605,7 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // Check if current field is editable return m.handleDetailFieldEdit() } - + return m, nil } @@ -765,4 +765,4 @@ func (m *Model) activateDescriptionEdit(id int, tsk *task.Task) (tea.Model, tea. m.descInput.Focus() m.updateTableHeight() return m, nil -}
\ No newline at end of file +} diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go index 0a2bc62..9eedafa 100644 --- a/internal/ui/keyhandlers.go +++ b/internal/ui/keyhandlers.go @@ -7,15 +7,15 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" "codeberg.org/snonux/tasksamurai/internal/task" ) // handleNormalMode handles keyboard input in normal mode (not editing) -func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // If help is shown, handle special cases if m.showHelp { switch msg.String() { @@ -36,7 +36,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "pgup", "b": m.helpViewport.PageUp() return m, nil - case "pgdown", " ": + case "pgdown", "space": m.helpViewport.PageDown() return m, nil case "g", "home": @@ -50,7 +50,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } } - + switch msg.String() { case "H": return m.handleToggleHelp() @@ -98,7 +98,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.handleToggleDisco() case "B": return m.handleToggleBlink() - case " ": + case "space": return m.handleRefresh() case "/", "?": return m.handleSearch() @@ -123,7 +123,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *Model) handleToggleHelp() (tea.Model, tea.Cmd) { m.showHelp = true // Initialize help viewport with proper dimensions - width := m.tbl.Width() - 4 // Account for padding + width := m.tbl.Width() - 4 // Account for padding height := m.windowHeight - 6 // Leave room for status bars and search input if width <= 0 { width = 80 // Default width @@ -131,7 +131,7 @@ func (m *Model) handleToggleHelp() (tea.Model, tea.Cmd) { if height <= 0 { height = 20 // Default height } - m.helpViewport = viewport.New(width, height) + m.helpViewport = viewport.New(viewport.WithWidth(width), viewport.WithHeight(height)) // Set the content immediately content := m.buildHelpContent() m.helpViewport.SetContent(content) @@ -187,7 +187,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + // Check if task is started started := false for _, tsk := range m.tasks { @@ -196,7 +196,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) { break } } - + if started { if err := task.Stop(id); err != nil { m.showError(err) @@ -208,7 +208,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) { return m, nil } } - + m.reload() return m, m.startBlink(id, false) } @@ -226,17 +226,17 @@ func (m *Model) handleOpenURL() (tea.Model, tea.Cmd) { if task == nil { return m, nil } - + url := urlRegex.FindString(task.Description) if url == "" { return m, nil } - + if err := exec.Command(m.browserCmd, url).Run(); err != nil { m.showError(fmt.Errorf("opening browser: %w", err)) return m, nil } - + |
