diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-05 19:32:29 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-05 19:32:29 +0200 |
| commit | 83ae6b0b19a1f5bb18069210700fc81d2f43278a (patch) | |
| tree | c2d931e79c288df8ff46c52793d4ae35ad21082f | |
| parent | b547dc4372175bc54ca3bfdeb215b9734987c669 (diff) | |
migrate Bubble Tea and Lip Gloss to charm.land v2 APIs
| -rw-r--r-- | go.mod | 33 | ||||
| -rw-r--r-- | go.sum | 73 | ||||
| -rw-r--r-- | internal/ascii/render.go | 2 | ||||
| -rw-r--r-- | internal/ascii/render_test.go | 2 | ||||
| -rw-r--r-- | internal/cli/timer.go | 2 | ||||
| -rw-r--r-- | internal/cli/tui.go | 2 | ||||
| -rw-r--r-- | internal/tui/entries.go | 29 | ||||
| -rw-r--r-- | internal/tui/entries_test.go | 118 | ||||
| -rw-r--r-- | internal/tui/keypress_test.go | 15 | ||||
| -rw-r--r-- | internal/tui/report.go | 4 | ||||
| -rw-r--r-- | internal/tui/report_test.go | 21 | ||||
| -rw-r--r-- | internal/tui/styles.go | 2 | ||||
| -rw-r--r-- | internal/tui/timer.go | 18 | ||||
| -rw-r--r-- | internal/tui/timer_test.go | 9 | ||||
| -rw-r--r-- | internal/tui/tui.go | 17 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 48 |
16 files changed, 203 insertions, 192 deletions
@@ -3,32 +3,29 @@ module codeberg.org/snonux/timesamurai go 1.24.3 require ( - github.com/charmbracelet/bubbletea v1.3.5 - github.com/charmbracelet/lipgloss v1.1.0 + charm.land/bubbletea/v2 v2.0.1 + charm.land/lipgloss/v2 v2.0.0 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/magefile/mage v1.15.0 + github.com/spf13/cobra v1.10.2 ) require ( - 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/ansi v0.8.0 // 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/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.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/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-runewidth v0.0.19 // 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/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.9 // 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,41 +1,40 @@ -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbletea v1.3.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/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +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/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/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/lucasb-eyer/go-colorful v1.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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -46,14 +45,10 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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-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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/ascii/render.go b/internal/ascii/render.go index eb41dd1..45d4c32 100644 --- a/internal/ascii/render.go +++ b/internal/ascii/render.go @@ -5,7 +5,7 @@ package ascii import ( - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" ) // RenderNumber renders a string of digits as ASCII art using the specified font. diff --git a/internal/ascii/render_test.go b/internal/ascii/render_test.go index 1ba4f90..60b20c4 100644 --- a/internal/ascii/render_test.go +++ b/internal/ascii/render_test.go @@ -3,7 +3,7 @@ package ascii import ( "testing" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" ) func TestGetFont(t *testing.T) { diff --git a/internal/cli/timer.go b/internal/cli/timer.go index 222b7df..25a6b2c 100644 --- a/internal/cli/timer.go +++ b/internal/cli/timer.go @@ -12,7 +12,7 @@ import ( timesamuraiTimer "codeberg.org/snonux/timesamurai/internal/timer" tuiapp "codeberg.org/snonux/timesamurai/internal/tui" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/spf13/cobra" ) diff --git a/internal/cli/tui.go b/internal/cli/tui.go index e3b6426..1cc1b6b 100644 --- a/internal/cli/tui.go +++ b/internal/cli/tui.go @@ -2,7 +2,7 @@ package cli import ( tuiapp "codeberg.org/snonux/timesamurai/internal/tui" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/spf13/cobra" ) diff --git a/internal/tui/entries.go b/internal/tui/entries.go index e2f7e54..57255b1 100644 --- a/internal/tui/entries.go +++ b/internal/tui/entries.go @@ -10,11 +10,11 @@ import ( "time" "unicode/utf8" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "codeberg.org/snonux/timesamurai/internal/duration" "codeberg.org/snonux/timesamurai/internal/timefmt" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) type entryEditField int @@ -125,7 +125,7 @@ func (m *EntriesModel) SetEntries(entries []worktime.Entry) { // Update handles keyboard navigation and search/filter/edit interaction. func (m *EntriesModel) Update(msg tea.Msg) (EntriesModel, tea.Cmd) { - keyMsg, ok := msg.(tea.KeyMsg) + keyMsg, ok := msg.(tea.KeyPressMsg) if !ok { return *m, nil } @@ -174,7 +174,7 @@ func (m *EntriesModel) updateConfirmDelete(key string) bool { return true } -func (m *EntriesModel) updateEditMode(keyMsg tea.KeyMsg) bool { +func (m *EntriesModel) updateEditMode(keyMsg tea.KeyPressMsg) bool { if !m.editMode { return false } @@ -200,7 +200,7 @@ func (m *EntriesModel) updateEditMode(keyMsg tea.KeyMsg) bool { return true } -func (m *EntriesModel) updateDayOffMode(keyMsg tea.KeyMsg) bool { +func (m *EntriesModel) updateDayOffMode(keyMsg tea.KeyPressMsg) bool { if !m.dayOffMode { return false } @@ -239,7 +239,7 @@ func (m *EntriesModel) updateDayOffMode(keyMsg tea.KeyMsg) bool { return true } -func (m *EntriesModel) updateSearchFilterMode(keyMsg tea.KeyMsg) bool { +func (m *EntriesModel) updateSearchFilterMode(keyMsg tea.KeyPressMsg) bool { if !m.searchMode && !m.filterMode { return false } @@ -268,7 +268,7 @@ func (m *EntriesModel) updateSearchFilterMode(keyMsg tea.KeyMsg) bool { return true } -func (m *EntriesModel) updateNormalMode(keyMsg tea.KeyMsg) { +func (m *EntriesModel) updateNormalMode(keyMsg tea.KeyPressMsg) { switch keyMsg.String() { case "/": m.searchMode = true @@ -1084,15 +1084,16 @@ func trimLastRune(value string) string { return value[:len(value)-size] } -func appendInputKey(input string, keyMsg tea.KeyMsg) string { - switch keyMsg.Type { - case tea.KeyRunes: - return input + string(keyMsg.Runes) - case tea.KeySpace: +func appendInputKey(input string, keyMsg tea.KeyPressMsg) string { + if keyMsg.String() == "space" { return input + " " - default: - return input } + + if keyMsg.Text != "" { + return input + keyMsg.Text + } + + return input } func entryMatchesSearch(entry worktime.Entry, search string) bool { diff --git a/internal/tui/entries_test.go b/internal/tui/entries_test.go index e1541a0..8d6bf1f 100644 --- a/internal/tui/entries_test.go +++ b/internal/tui/entries_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" + tea "charm.land/bubbletea/v2" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" ) func TestEntriesModelSortsChronologically(t *testing.T) { @@ -44,17 +44,17 @@ func TestEntriesColumnNavigationKeys(t *testing.T) { t.Fatalf("selectedColumn = %d, want %d", model.selectedColumn, entriesColumnDescription) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyLeft}) + model, _ = model.Update(keyCode(tea.KeyLeft)) if model.selectedColumn != entriesColumnSource { t.Fatalf("selectedColumn after left = %d, want %d", model.selectedColumn, entriesColumnSource) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'h'}}) + model, _ = model.Update(keyRune('h')) if model.selectedColumn != entriesColumnValue { t.Fatalf("selectedColumn after h = %d, want %d", model.selectedColumn, entriesColumnValue) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'l'}}) + model, _ = model.Update(keyRune('l')) if model.selectedColumn != entriesColumnSource { t.Fatalf("selectedColumn after l = %d, want %d", model.selectedColumn, entriesColumnSource) } @@ -65,7 +65,7 @@ func TestEntriesEnterEditsSelectedColumnValue(t *testing.T) { model.SetSize(120, 12) model.selectedColumn = entriesColumnValue - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyCode(tea.KeyEnter)) if !model.editMode { t.Fatal("editMode = false, want true after Enter on value column") } @@ -81,23 +81,23 @@ func TestEntriesEnterEditsSelectedColumnDateAndTime(t *testing.T) { original := model.visible[0].Epoch model.selectedColumn = entriesColumnDate - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyCode(tea.KeyEnter)) if !model.editMode || model.editField != entryEditFieldDate { t.Fatalf("date edit not entered: editMode=%t editField=%d", model.editMode, model.editField) } model.input = "2026-02-02" - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyCode(tea.KeyEnter)) if model.visible[0].Epoch == original { t.Fatal("epoch did not change after date edit") } model.selectedColumn = entriesColumnTime - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyCode(tea.KeyEnter)) if !model.editMode || model.editField != entryEditFieldTime { t.Fatalf("time edit not entered: editMode=%t editField=%d", model.editMode, model.editField) } model.input = "13:45" - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyCode(tea.KeyEnter)) updatedTime := time.Unix(model.visible[0].Epoch, 0) if updatedTime.Hour() != 13 || updatedTime.Minute() != 45 { @@ -109,34 +109,34 @@ func TestEntriesNavigationKeys(t *testing.T) { model := NewEntriesModel(sampleEntries(20)) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}}) + model, _ = model.Update(keyRune('j')) if model.cursor != 1 { t.Fatalf("cursor after j = %d, want 1", model.cursor) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}) + model, _ = model.Update(keyRune('k')) if model.cursor != 0 { t.Fatalf("cursor after k = %d, want 0", model.cursor) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'G'}}) + model, _ = model.Update(keyRune('G')) if model.cursor != len(model.visible)-1 { t.Fatalf("cursor after G = %d, want %d", model.cursor, len(model.visible)-1) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'g'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'g'}}) + model, _ = model.Update(keyRune('g')) + model, _ = model.Update(keyRune('g')) if model.cursor != 0 { t.Fatalf("cursor after gg = %d, want 0", model.cursor) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyCtrlD}) + model, _ = model.Update(keyCtrl('d')) if model.cursor == 0 { t.Fatal("cursor did not move after ctrl+d") } before := model.cursor - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyCtrlU}) + model, _ = model.Update(keyCtrl('u')) if model.cursor >= before { t.Fatalf("cursor after ctrl+u = %d, want less than %d", model.cursor, before) } @@ -153,27 +153,27 @@ func TestEntriesSearchAndFilter(t *testing.T) { model.SetSize(120, 12) // Search for "meeting". - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'i'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'n'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'g'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyRune('/')) + model, _ = model.Update(keyRune('m')) + model, _ = model.Update(keyRune('e')) + model, _ = model.Update(keyRune('e')) + model, _ = model.Update(keyRune('t')) + model, _ = model.Update(keyRune('i')) + model, _ = model.Update(keyRune('n')) + model, _ = model.Update(keyRune('g')) + model, _ = model.Update(keyCode(tea.KeyEnter)) if len(model.visible) != 1 || model.visible[0].Descr != "meeting" { t.Fatalf("search results mismatch: %+v", model.visible) } // Apply filter by category "work" on top of search. - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'f'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'w'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyRune('f')) + model, _ = model.Update(keyRune('w')) + model, _ = model.Update(keyRune('o')) + model, _ = model.Update(keyRune('r')) + model, _ = model.Update(keyRune('k')) + model, _ = model.Update(keyCode(tea.KeyEnter)) if len(model.visible) != 1 || model.visible[0].What != "work" { t.Fatalf("filter results mismatch: %+v", model.visible) @@ -186,13 +186,13 @@ func TestEntriesEditFlow(t *testing.T) { original := model.visible[0].Descr - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) + model, _ = model.Update(keyRune('e')) if !model.editMode { t.Fatal("editMode = false, want true after e") } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'!'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyRune('!')) + model, _ = model.Update(keyCode(tea.KeyEnter)) if model.editMode { t.Fatal("editMode = true, want false after Enter") @@ -207,7 +207,7 @@ func TestEntriesEditModeAcceptsSpaceKey(t *testing.T) { model.editMode = true model.input = "hello" - model, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace}) + model, _ = model.Update(keyRune(' ')) if model.input != "hello " { t.Fatalf("input after space = %q, want %q", model.input, "hello ") } @@ -244,13 +244,13 @@ func TestEntriesValueEditFlow(t *testing.T) { model := NewEntriesModel(sampleEntries(3)) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'v'}}) + model, _ = model.Update(keyRune('v')) if !model.editMode { t.Fatal("editMode = false, want true after v") } model.input = "120" - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyCode(tea.KeyEnter)) if model.editMode { t.Fatal("editMode = true, want false after Enter") } @@ -264,7 +264,7 @@ func TestEntriesBackspaceIsRuneSafeInEditMode(t *testing.T) { model.editMode = true model.input = "aй" - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyBackspace}) + model, _ = model.Update(keyCode(tea.KeyBackspace)) if model.input != "a" { t.Fatalf("input after backspace = %q, want %q", model.input, "a") } @@ -275,7 +275,7 @@ func TestEntriesBackspaceIsRuneSafeInSearchMode(t *testing.T) { model.searchMode = true model.input = "тест" - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyBackspace}) + model, _ = model.Update(keyCode(tea.KeyBackspace)) if model.input != "тес" { t.Fatalf("input after backspace = %q, want %q", model.input, "тес") } @@ -285,13 +285,13 @@ func TestEntriesDeleteWithConfirmation(t *testing.T) { model := NewEntriesModel(sampleEntries(3)) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) + model, _ = model.Update(keyRune('d')) + model, _ = model.Update(keyRune('d')) if !model.confirmDelete { t.Fatal("confirmDelete = false, want true after dd") } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'n'}}) + model, _ = model.Update(keyRune('n')) if model.confirmDelete { t.Fatal("confirmDelete = true after cancel") } @@ -299,9 +299,9 @@ func TestEntriesDeleteWithConfirmation(t *testing.T) { t.Fatalf("entries len = %d, want 3 after cancel", len(model.visible)) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'y'}}) + model, _ = model.Update(keyRune('d')) + model, _ = model.Update(keyRune('d')) + model, _ = model.Update(keyRune('y')) if len(model.visible) != 2 { t.Fatalf("entries len = %d, want 2 after delete confirmation", len(model.visible)) @@ -312,16 +312,16 @@ func TestEntriesInsertWithOAndShiftO(t *testing.T) { model := NewEntriesModel(sampleEntries(2)) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}}) + model, _ = model.Update(keyRune('o')) if len(model.visible) != 3 { t.Fatalf("entries len = %d, want 3 after o", len(model.visible)) } if !model.editMode { t.Fatal("editMode = false after o insertion") } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEsc}) + model, _ = model.Update(keyCode(tea.KeyEscape)) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'O'}}) + model, _ = model.Update(keyRune('O')) if len(model.visible) != 4 { t.Fatalf("entries len = %d, want 4 after O", len(model.visible)) } @@ -349,9 +349,9 @@ func TestEntriesDeletePersistsToDB(t *testing.T) { model.SetPersistence(dbDir, host) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'y'}}) + model, _ = model.Update(keyRune('d')) + model, _ = model.Update(keyRune('d')) + model, _ = model.Update(keyRune('y')) if len(model.visible) != 2 { t.Fatalf("entries len = %d, want 2 after persisted delete", len(model.visible)) @@ -368,7 +368,7 @@ func TestEntriesDeletePersistsToDB(t *testing.T) { t.Fatalf("host entries len before save = %d, want 3", len(reloaded.Entries[host])) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}}) + model, _ = model.Update(keyRune('s')) if model.hasUnsavedChanges() { t.Fatal("hasUnsavedChanges = true, want false after save") } @@ -399,13 +399,13 @@ func TestEntriesDayOffPromptPersistsToDB(t *testing.T) { model.SetPersistence(dbDir, host) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'D'}}) + model, _ = model.Update(keyRune('D')) if !model.dayOffMode { t.Fatal("dayOffMode = false, want true after D") } model.dayOffDate = time.Date(2026, 1, 22, 12, 34, 0, 0, time.Local) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = model.Update(keyCode(tea.KeyEnter)) if model.dayOffMode { t.Fatal("dayOffMode = true, want false after Enter") } @@ -423,7 +423,7 @@ func TestEntriesDayOffPromptPersistsToDB(t *testing.T) { t.Fatalf("entries len before save = %d, want 0", len(entries)) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}}) + model, _ = model.Update(keyRune('s')) if model.hasUnsavedChanges() { t.Fatal("hasUnsavedChanges = true, want false after save") } @@ -455,23 +455,23 @@ func TestEntriesDayOffDatepickerNavigation(t *testing.T) { model := NewEntriesModel(sampleEntries(1)) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'D'}}) + model, _ = model.Update(keyRune('D')) if !model.dayOffMode { t.Fatal("dayOffMode = false, want true after D") } model.dayOffDate = time.Date(2026, 2, 12, 0, 0, 0, 0, time.Local) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRight}) + model, _ = model.Update(keyCode(tea.KeyRight)) if !sameDay(model.dayOffDate, time.Date(2026, 2, 13, 0, 0, 0, 0, time.Local)) { t.Fatalf("dayOffDate after right = %v, want 2026-02-13", model.dayOffDate) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown}) + model, _ = model.Update(keyCode(tea.KeyDown)) if !sameDay(model.dayOffDate, time.Date(2026, 2, 20, 0, 0, 0, 0, time.Local)) { t.Fatalf("dayOffDate after down = %v, want 2026-02-20", model.dayOffDate) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyPgUp}) + model, _ = model.Update(keyCode(tea.KeyPgUp)) if !sameDay(model.dayOffDate, time.Date(2026, 1, 20, 0, 0, 0, 0, time.Local)) { t.Fatalf("dayOffDate after pgup = %v, want 2026-01-20", model.dayOffDate) } diff --git a/internal/tui/keypress_test.go b/internal/tui/keypress_test.go new file mode 100644 index 0000000..3ca2843 --- /dev/null +++ b/internal/tui/keypress_test.go @@ -0,0 +1,15 @@ +package tui + +import tea "charm.land/bubbletea/v2" + +func keyRune(r rune) tea.KeyPressMsg { + return tea.KeyPressMsg{Code: r, Text: string(r)} +} + +func keyCode(code rune) tea.KeyPressMsg { + return tea.KeyPressMsg{Code: code} +} + +func keyCtrl(code rune) tea.KeyPressMsg { + return tea.KeyPressMsg{Code: code, Mod: tea.ModCtrl} +} diff --git a/internal/tui/report.go b/internal/tui/report.go index 3db4ab0..28b60f7 100644 --- a/internal/tui/report.go +++ b/internal/tui/report.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + tea "charm.land/bubbletea/v2" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" ) // ReportModel is a weekly report browser screen. @@ -66,7 +66,7 @@ func (m *ReportModel) SetWarning(warning string) { // Update handles keyboard interaction. func (m *ReportModel) Update(msg tea.Msg) (ReportModel, tea.Cmd) { - keyMsg, ok := msg.(tea.KeyMsg) + keyMsg, ok := msg.(tea.KeyPressMsg) if !ok { return *m, nil } diff --git a/internal/tui/report_test.go b/internal/tui/report_test.go index 2bde9ff..e37a209 100644 --- a/internal/tui/report_test.go +++ b/internal/tui/report_test.go @@ -5,21 +5,20 @@ import ( "testing" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" ) func TestReportWeekNavigation(t *testing.T) { model := NewReportModel(sampleWeeks()) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{']'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'w'}}) + model, _ = model.Update(keyRune(']')) + model, _ = model.Update(keyRune('w')) if model.weekIndex != 1 { t.Fatalf("weekIndex after ]w = %d, want 1", model.weekIndex) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'['}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'w'}}) + model, _ = model.Update(keyRune('[')) + model, _ = model.Update(keyRune('w')) if model.weekIndex != 0 { t.Fatalf("weekIndex after [w = %d, want 0", model.weekIndex) } @@ -29,23 +28,23 @@ func TestReportScrollingAndTopBottom(t *testing.T) { model := NewReportModel(sampleWeeks()) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}}) + model, _ = model.Update(keyRune('j')) if model.cursor != 1 { t.Fatalf("cursor after j = %d, want 1", model.cursor) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}) + model, _ = model.Update(keyRune('k')) if model.cursor != 0 { t.Fatalf("cursor after k = %d, want 0", model.cursor) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'G'}}) + model, _ = model.Update(keyRune('G')) if model.cursor != model.rowCount()-1 { t.Fatalf("cursor after G = %d, want %d", model.cursor, model.rowCount()-1) } - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'g'}}) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'g'}}) + model, _ = model.Update(keyRune('g')) + model, _ = model.Update(keyRune('g')) if model.cursor != 0 { t.Fatalf("cursor after gg = %d, want 0", model.cursor) } @@ -55,7 +54,7 @@ func TestReportVerboseToggle(t *testing.T) { model := NewReportModel(sampleWeeks()) model.SetSize(120, 12) - model, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'v'}}) + model, _ = model.Update(keyRune('v')) if !model.verbose { t.Fatal("verbose = false, want true after v") } diff --git a/internal/tui/styles.go b/internal/tui/styles.go index c131c1f..3420dbf 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -1,6 +1,6 @@ package tui -import "github.com/charmbracelet/lipgloss" +import "charm.land/lipgloss/v2" // Styles groups visual styles for the root TUI scaffold. type Styles struct { diff --git a/internal/tui/timer.go b/internal/tui/timer.go index 980390a..04098fe 100644 --- a/internal/tui/timer.go +++ b/internal/tui/timer.go @@ -5,12 +5,12 @@ import ( "strings" "time" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "codeberg.org/snonux/timesamurai/internal/ascii" "codeberg.org/snonux/timesamurai/internal/config" timesamuraiTimer "codeberg.org/snonux/timesamurai/internal/timer" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/common-nighthawk/go-figure" ) @@ -121,14 +121,14 @@ func (m TimerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, timerTick() - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "q", "ctrl+c": m.quitting = true _ = m.state.Save() return m, tea.Quit - case "s", " ": + case "s", "space": if m.state.Running { m.state.ElapsedTime += time.Since(m.state.StartTime) m.state.Running = false @@ -167,9 +167,9 @@ func (m TimerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the timer screen. -func (m TimerModel) View() string { +func (m TimerModel) View() tea.View { if m.quitting { - return "" + return tea.NewView("") } elapsed := m.state.ElapsedTime @@ -194,13 +194,15 @@ func (m TimerModel) View() string { m.helpStyle.Render("s/Space: start-stop, r: reset, f: change font, l: work login/logout, q: quit"), } - return lipgloss.Place( + view := tea.NewView(lipgloss.Place( m.width, m.height, lipgloss.Center, lipgloss.Center, lipgloss.JoinVertical(lipgloss.Center, lines...), - ) + )) + view.AltScreen = true + return view } func (m TimerModel) renderTimer(elapsed time.Duration) string { diff --git a/internal/tui/timer_test.go b/internal/tui/timer_test.go index 8bc9432..aeeda9e 100644 --- a/internal/tui/timer_test.go +++ b/internal/tui/timer_test.go @@ -5,7 +5,6 @@ import ( "codeberg.org/snonux/timesamurai/internal/config" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" ) func TestTimerModelToggleWorkLogin(t *testing.T) { @@ -25,13 +24,13 @@ func TestTimerModelToggleWorkLogin(t *testing.T) { t.Fatal("work integration should be enabled") } - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'l'}}) + modelAny, _ := model.Update(keyRune('l')) model = modelAny.(TimerModel) if !model.work.loggedIn { t.Fatal("work should be logged in after first l") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'l'}}) + modelAny, _ = model.Update(keyRune('l')) model = modelAny.(TimerModel) if model.work.loggedIn { t.Fatal("work should be logged out after second l") @@ -55,7 +54,7 @@ func TestTimerModelFontCycling(t *testing.T) { } originalFont := model.font - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'f'}}) + modelAny, _ := model.Update(keyRune('f')) model = modelAny.(TimerModel) if model.font == originalFont { t.Fatalf("font did not change after f: %q", model.font) @@ -73,7 +72,7 @@ func TestTimerModelWorkToggleWhenDisabled(t *testing.T) { t.Fatalf("NewTimerModel() error = %v", err) } - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'l'}}) + modelAny, _ := model.Update(keyRune('l')) model = modelAny.(TimerModel) if model.work.status != "work integration disabled" { t.Fatalf("workStatus = %q, want work integration disabled", model.work.status) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 3c4feb6..efdbbdb 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -5,11 +5,11 @@ import ( "strings" "time" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" timesamurai "codeberg.org/snonux/timesamurai/internal" "codeberg.org/snonux/timesamurai/internal/config" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) type tab int @@ -135,7 +135,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.timer.SetSize(bodyWidth, bodyHeight) return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: key := msg.String() entriesCapturingText := m.activeTab == tabEntries && m.entries.capturesTextInput() @@ -228,7 +228,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View implements tea.Model. -func (m *Model) View() string { +func (m *Model) View() tea.View { header := m.renderTabs() body := m.renderBody() status := m.renderStatusLine() @@ -267,9 +267,12 @@ func (m *Model) View() string { rendered := m.styles.App.Render(content) if m.width > 0 && m.height > 0 { - return lipgloss.Place(m.width, m.height, lipgloss.Left, lipgloss.Top, rendered) + rendered = lipgloss.Place(m.width, m.height, lipgloss.Left, lipgloss.Top, rendered) } - return rendered + + view := tea.NewView(rendered) + view.AltScreen = true + return view } func (m *Model) nextTab() tea.Cmd { @@ -320,7 +323,7 @@ func (m *Model) renderBody() string { } return m.report.View(m.styles) case tabTimer: - return m.timer.View() + return m.timer.View().Content default: return "" } diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 16d3ca2..b2e6efc 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -5,29 +5,29 @@ import ( "testing" "time" + tea "charm.land/bubbletea/v2" "codeberg.org/snonux/timesamurai/internal/config" "codeberg.org/snonux/timesamurai/internal/worktime" - tea "github.com/charmbracelet/bubbletea" ) func TestTabNavigation(t *testing.T) { model := newRootModelForTest(t) - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyTab}) + modelAny, _ := model.Update(keyCode(tea.KeyTab)) model = modelAny.(*Model) if model.activeTab != tabReport { t.Fatalf("active tab after Tab = %v, want %v", model.activeTab, tabReport) } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'g'}}) + modelAny, _ = model.Update(keyRune('g')) model = modelAny.(*Model) - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'T'}}) + modelAny, _ = model.Update(keyRune('T')) model = modelAny.(*Model) if model.activeTab != tabEntries { t.Fatalf("active tab after gT = %v, want %v", model.activeTab, tabEntries) } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'3'}}) + modelAny, _ = model.Update(keyRune('3')) model = modelAny.(*Model) if model.activeTab != tabTimer { t.Fatalf("active tab after key 3 = %v, want %v", model.activeTab, tabTimer) @@ -37,15 +37,15 @@ func TestTabNavigation(t *testing.T) { func TestEntriesTextEditingIgnoresRootGlobalShortcuts(t *testing.T) { model := newRootModelForTest(t) - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}}) + modelAny, _ := model.Update(keyRune('o')) model = modelAny.(*Model) if !model.entries.editMode { t.Fatal("entries.editMode = false, want true after o") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'g'}}) + modelAny, _ = model.Update(keyRune('g')) model = modelAny.(*Model) - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace}) + modelAny, _ = model.Update(keyRune(' ')) model = modelAny.(*Model) if model.entries.input != "g " { @@ -59,25 +59,25 @@ func TestEntriesTextEditingIgnoresRootGlobalShortcuts(t *testing.T) { func TestHelpToggle(t *testing.T) { model := newRootModelForTest(t) - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}) + modelAny, _ := model.Update(keyRune('?')) model = modelAny.(*Model) if !model.showHelp { t.Fatal("showHelp = false, want true after ?") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}) + modelAny, _ = model.Update(keyRune('?')) model = modelAny.(*Model) if model.showHelp { t.Fatal("showHelp = true, want false after second ?") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'H'}}) + modelAny, _ = model.Update(keyRune('H')) model = modelAny.(*Model) if !model.showHelp { t.Fatal("showHelp = false, want true after H") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyEsc}) + modelAny, _ = model.Update(keyCode(tea.KeyEscape)) model = modelAny.(*Model) if model.showHelp { t.Fatal("showHelp = true, want false after Esc") @@ -87,7 +87,7 @@ func TestHelpToggle(t *testing.T) { func TestQuitKeys(t *testing.T) { model := newRootModelForTest(t) - modelAny, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}) + modelAny, cmd := model.Update(keyRune('q')) model = modelAny.(*Model) if cmd == nil { t.Fatal("quit cmd is nil for q") @@ -96,9 +96,9 @@ func TestQuitKeys(t *testing.T) { t.Fatal("q key did not return tea.Quit command") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'Z'}}) + modelAny, _ = model.Update(keyRune('Z')) model = modelAny.(*Model) - _, cmd = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'Q'}}) + _, cmd = model.Update(keyRune('Q')) if cmd == nil { t.Fatal("quit cmd is nil for ZQ") } @@ -110,13 +110,13 @@ func TestQuitKeys(t *testing.T) { func TestQuitWithUnsavedChangesPromptsConfirmation(t *testing.T) { model := newRootModelForTest(t) - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}}) + modelAny, _ := model.Update(keyRune('o')) model = modelAny.(*Model) if !model.entries.hasUnsavedChanges() { t.Fatal("entries.hasUnsavedChanges() = false, want true after insertion") } - modelAny, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}) + modelAny, cmd := model.Update(keyRune('q')) model = modelAny.(*Model) if cmd != nil { t.Fatal("quit command should be deferred until quit confirmation") @@ -125,7 +125,7 @@ func TestQuitWithUnsavedChangesPromptsConfirmation(t *testing.T) { t.Fatal("confirmQuit = false, want true after q with unsaved changes") } - modelAny, cmd = model.Update(tea.KeyMsg{Type: tea.KeyEsc}) + modelAny, cmd = model.Update(keyCode(tea.KeyEscape)) model = modelAny.(*Model) if cmd != nil { t.Fatal("Esc in quit confirmation should not quit") @@ -138,19 +138,19 @@ func TestQuitWithUnsavedChangesPromptsConfirmation(t *testing.T) { func TestQuitConfirmationSaveAndQuitPersistsEntries(t *testing.T) { model := newRootModelForTest(t) - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}}) + modelAny, _ := model.Update(keyRune('o')) model = modelAny.(*Model) if !model.entries.hasUnsavedChanges() { t.Fatal("entries.hasUnsavedChanges() = false, want true after insertion") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}}) + modelAny, _ = model.Update(keyRune('q')) model = modelAny.(*Model) if !model.confirmQuit { t.Fatal("confirmQuit = false, want true after q with unsaved changes") } - modelAny, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'s'}}) + modelAny, cmd := model.Update(keyRune('s')) model = modelAny.(*Model) if cmd == nil { t.Fatal("quit cmd is nil for save-and-quit") @@ -174,7 +174,7 @@ func TestQuitConfirmationSaveAndQuitPersistsEntries(t *testing.T) { func TestViewContainsTabLabels(t *testing.T) { model := newRootModelForTest(t) view := model.View() - if view == "" { + if view.Content == "" { t.Fatal("View() returned empty output") } } @@ -190,13 +190,13 @@ func TestEntriesTabUsesEntriesModelView(t *testing.T) { func TestDiscoToggleAndThemeResetKeys(t *testing.T) { model := newRootModelForTest(t) - modelAny, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}) + modelAny, _ := model.Update(keyRune('x')) model = modelAny.(*Model) if !model.disco { t.Fatal("disco = false, want true after x") } - modelAny, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'C'}}) + modelAny, _ = model.Update(keyRune('C')) model = modelAny.(*Model) if model.theme != DefaultTheme() { t.Fatalf("theme after C = %+v, want default theme", model.theme) |
