summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-05 19:24:09 +0200
committerPaul Buetow <paul@buetow.org>2026-03-05 19:24:09 +0200
commit30c955ef113e5e0c99c147ee4e7c8c20b0a7f273 (patch)
treef872b7c44e62609a455dbed356fcc28ab3b53531
parent33064821d637aeef94fe6ee96edbac7a503c0692 (diff)
Migrate UI stack to Bubble Tea v2
-rw-r--r--cmd/tasksamurai/main.go4
-rw-r--r--go.mod37
-rw-r--r--go.sum81
-rw-r--r--internal/atable/table.go70
-rw-r--r--internal/ui/handlers.go146
-rw-r--r--internal/ui/keyhandlers.go118
-rw-r--r--internal/ui/table.go172
-rw-r--r--internal/ui/table_test.go104
-rw-r--r--internal/ui/taskdetail.go14
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)
diff --git a/go.mod b/go.mod
index 49cbc96..7a06299 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 29a5ba6..245c915 100644
--- a/go.sum
+++ b/go.sum
@@ -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
}
-
+