summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-05 19:32:29 +0200
committerPaul Buetow <paul@buetow.org>2026-03-05 19:32:29 +0200
commit83ae6b0b19a1f5bb18069210700fc81d2f43278a (patch)
treec2d931e79c288df8ff46c52793d4ae35ad21082f
parentb547dc4372175bc54ca3bfdeb215b9734987c669 (diff)
migrate Bubble Tea and Lip Gloss to charm.land v2 APIs
-rw-r--r--go.mod33
-rw-r--r--go.sum73
-rw-r--r--internal/ascii/render.go2
-rw-r--r--internal/ascii/render_test.go2
-rw-r--r--internal/cli/timer.go2
-rw-r--r--internal/cli/tui.go2
-rw-r--r--internal/tui/entries.go29
-rw-r--r--internal/tui/entries_test.go118
-rw-r--r--internal/tui/keypress_test.go15
-rw-r--r--internal/tui/report.go4
-rw-r--r--internal/tui/report_test.go21
-rw-r--r--internal/tui/styles.go2
-rw-r--r--internal/tui/timer.go18
-rw-r--r--internal/tui/timer_test.go9
-rw-r--r--internal/tui/tui.go17
-rw-r--r--internal/tui/tui_test.go48
16 files changed, 203 insertions, 192 deletions
diff --git a/go.mod b/go.mod
index e572615..947ed67 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 5471ff1..fedd8a7 100644
--- a/go.sum
+++ b/go.sum
@@ -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)