From 324b30b8d1af0017c9ab216ecb03c601f82a5b76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 00:25:09 +0000 Subject: [PATCH 1/3] Initial plan From 3361e818536cb1d35f86a82c3676e1e97c1745ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 00:48:20 +0000 Subject: [PATCH 2/3] feat: upgrade to bubbletea v2, lipgloss v2, and bubbles v2 Co-authored-by: topfunky <26+topfunky@users.noreply.github.com> --- database.go | 12 +++++- database_test.go | 11 +++--- go.mod | 32 +++++++--------- go.sum | 67 ++++++++++++++------------------ handlers.go | 56 ++++++++++++++------------- handlers_test.go | 99 ++++++++++++++++++++++++------------------------ main.go | 9 +++-- model.go | 10 ++--- status/status.go | 35 ++++++++--------- test_helpers.go | 7 ++-- utils.go | 4 +- view.go | 17 +++++---- view_test.go | 26 +++++++------ 13 files changed, 195 insertions(+), 190 deletions(-) diff --git a/database.go b/database.go index 6355a3c..5decb5d 100644 --- a/database.go +++ b/database.go @@ -7,8 +7,8 @@ import ( "path/filepath" "time" - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/table" + "charm.land/lipgloss/v2" ) // getDBPath returns the full path to the SQLite database file @@ -149,6 +149,12 @@ func buildTableView(limit int) (table.Model, error) { log.Printf("buildTableView: Created %d row(s) for table", len(rows)) + // Calculate total table width from column widths + tableWidth := 0 + for _, col := range columns { + tableWidth += col.Width + } + // Calculate table height: header (1) + data rows + extra padding // Ensure minimum height of 3 to display header + at least 1 row properly tableHeight := max(len(rows)+2, 3) @@ -157,6 +163,7 @@ func buildTableView(limit int) (table.Model, error) { table.WithRows(rows), table.WithFocused(false), table.WithHeight(tableHeight), + table.WithWidth(tableWidth), ) s := table.DefaultStyles() @@ -170,6 +177,7 @@ func buildTableView(limit int) (table.Model, error) { s.Cell = s.Cell. Padding(0, 0). Foreground(lipgloss.Color(colorLightGrey)) + s.Selected = s.Cell // No cursor highlighting for unfocused table t.SetStyles(s) log.Printf("buildTableView: Table created with height=%d, rows=%d", t.Height(), len(t.Rows())) diff --git a/database_test.go b/database_test.go index 0e0afee..aaab879 100644 --- a/database_test.go +++ b/database_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/progress" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/progress" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/stretchr/testify/assert" ) @@ -280,7 +281,7 @@ func TestFirstSaveThenImmediateHistoryRead(t *testing.T) { startTime := time.Now().Unix() - 120 m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: startTime, targetDuration: 3600, countUpMode: true, @@ -289,7 +290,7 @@ func TestFirstSaveThenImmediateHistoryRead(t *testing.T) { } // Press 'd' to mark done and activate prompt - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}} + keyMsg := tea.KeyPressMsg{Code: 'd', Text: "d"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -301,7 +302,7 @@ func TestFirstSaveThenImmediateHistoryRead(t *testing.T) { modelTyped = newModel.(model) // Immediately press 'h' to show history (this is the bug scenario) - keyMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'h'}} + keyMsg = tea.KeyPressMsg{Code: 'h', Text: "h"} newModel, _ = modelTyped.Update(keyMsg) modelTyped = newModel.(model) diff --git a/go.mod b/go.mod index 77c5ea2..fceb2a3 100644 --- a/go.mod +++ b/go.mod @@ -1,44 +1,40 @@ module tiny-timer -go 1.24.2 +go 1.25.0 require ( - github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 + charm.land/bubbles/v2 v2.0.0 + charm.land/bubbletea/v2 v2.0.2 + charm.land/lipgloss/v2 v2.0.2 + github.com/charmbracelet/x/ansi v0.11.6 github.com/stretchr/testify v1.10.0 modernc.org/sqlite v1.43.0 ) require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.11.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.14 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.6.2 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // 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/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/google/uuid v1.6.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // 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/ncruces/go-strftime v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.42.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.67.4 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index da209a0..46befcc 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,35 @@ -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.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +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.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= +charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= +charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -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.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI= -github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI= -github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= -github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= -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/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/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= -github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -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/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -42,16 +40,10 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -70,12 +62,9 @@ golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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.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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/handlers.go b/handlers.go index c824847..cee1ccc 100644 --- a/handlers.go +++ b/handlers.go @@ -6,15 +6,15 @@ import ( "strconv" "time" - "github.com/charmbracelet/bubbles/progress" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/progress" + tea "charm.land/bubbletea/v2" "tiny-timer/status" ) // Top level event handler that is called each time the screen is updated func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: return updateKey(m, msg) case tea.WindowSizeMsg: @@ -29,7 +29,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // FrameMsg is sent when the progress bar wants to animate itself case progress.FrameMsg: progressModel, cmd := m.progress.Update(msg) - m.progress = progressModel.(progress.Model) + m.progress = progressModel return m, cmd case status.InfoMsg, status.ClearStatusMsg: @@ -84,9 +84,12 @@ func updatePercent(m model) (tea.Model, tea.Cmd) { } func updateWindowSize(m model, msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { - m.progress.Width = msg.Width - padding*2 - 4 - m.progress.Width = min(m.progress.Width, maxWidth) - m.help.Width = msg.Width + width := msg.Width - padding*2 - 4 + if width > maxWidth { + width = maxWidth + } + m.progress.SetWidth(width) + m.help.SetWidth(msg.Width) // Update status component with window size s, cmd := m.status.Update(msg) @@ -159,49 +162,50 @@ func handlePromptInput(m model, msg promptMsg) (tea.Model, tea.Cmd) { } // handlePromptKeyInput handles key input when prompt is active -func handlePromptKeyInput(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func handlePromptKeyInput(m model, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": return handlePromptInput(m, promptMsg{title: m.inputBuffer, logDB: m.promptType == promptLogAndReset}) - case tea.KeyEsc: + case "esc": m.promptActive = false return m, nil - case tea.KeyBackspace: + case "backspace": if len(m.inputBuffer) > 0 { m.inputBuffer = m.inputBuffer[:len(m.inputBuffer)-1] } return m, nil - case tea.KeySpace: + case "space": // Only allow space for title prompts, not duration if m.promptType != promptSetDuration { m.inputBuffer += " " } return m, nil - case tea.KeyRunes: - for _, r := range msg.Runes { - // For duration prompts, only allow numeric characters - if m.promptType == promptSetDuration { - if r >= '0' && r <= '9' { + default: + if len(msg.Text) > 0 { + for _, r := range msg.Text { + // For duration prompts, only allow numeric characters + if m.promptType == promptSetDuration { + if r >= '0' && r <= '9' { + m.inputBuffer += string(r) + } + } else { m.inputBuffer += string(r) } - } else { - m.inputBuffer += string(r) } } return m, nil } - return m, nil } // handleTableViewKey handles key input when in table view mode -func handleTableViewKey(m model, _ tea.KeyMsg) (tea.Model, tea.Cmd) { +func handleTableViewKey(m model, _ tea.KeyPressMsg) (tea.Model, tea.Cmd) { // Any key exits table view m.mode = timerView return m, nil } // handleCountUpModeKey handles key input in count-up mode -func handleCountUpModeKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func handleCountUpModeKey(m model, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { key := msg.String() switch key { case "d": @@ -249,7 +253,7 @@ func handleCountUpModeKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleTimerModeKey handles key input in timer mode (non count-up) -func handleTimerModeKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func handleTimerModeKey(m model, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { key := msg.String() switch key { case "d": @@ -296,9 +300,9 @@ func handleTimerModeKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } -func updateKey(m model, msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func updateKey(m model, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // Handle Ctrl-Z to suspend in all modes - if msg.Type == tea.KeyCtrlZ { + if msg.String() == "ctrl+z" { return m, tea.Suspend } diff --git a/handlers_test.go b/handlers_test.go index 53fa239..3c8675e 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -5,15 +5,16 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/progress" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/progress" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/stretchr/testify/assert" ) func TestTimerContinuesAfterPause(t *testing.T) { // Test that timer calculates elapsed time correctly even after a simulated pause m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix() - 30, // Started 30 seconds ago targetDuration: 120, // 2 minute timer title: "Test Task", @@ -43,7 +44,7 @@ func TestResumeAfterCompletion(t *testing.T) { // Create a timer that started 70 seconds ago (past the 60 second target) m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix() - 70, targetDuration: 60, title: "Completed Task", @@ -81,7 +82,7 @@ func TestTickAfterCompletion(t *testing.T) { // Create a timer that started 70 seconds ago (past the 60 second target) m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix() - 70, targetDuration: 60, title: "Tick After Complete", @@ -111,7 +112,7 @@ func TestTickAfterCompletion(t *testing.T) { func TestCtrlZSuspendsInTimerView(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 60, title: "Test Task", @@ -119,7 +120,7 @@ func TestCtrlZSuspendsInTimerView(t *testing.T) { } // Create a Ctrl-Z key message - keyMsg := tea.KeyMsg{Type: tea.KeyCtrlZ} + keyMsg := tea.KeyPressMsg{Code: 'z', Mod: tea.ModCtrl} // Process the Ctrl-Z key newModel, cmd := m.Update(keyMsg) @@ -133,7 +134,7 @@ func TestCtrlZSuspendsInTimerView(t *testing.T) { func TestCtrlZSuspendsInTableView(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 60, title: "Test Task", @@ -141,7 +142,7 @@ func TestCtrlZSuspendsInTableView(t *testing.T) { } // Create a Ctrl-Z key message - keyMsg := tea.KeyMsg{Type: tea.KeyCtrlZ} + keyMsg := tea.KeyPressMsg{Code: 'z', Mod: tea.ModCtrl} // Process the Ctrl-Z key newModel, cmd := m.Update(keyMsg) @@ -156,7 +157,7 @@ func TestCtrlZSuspendsInTableView(t *testing.T) { func TestOtherKeysStillQuitTimerView(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 60, title: "Test Task", @@ -164,7 +165,7 @@ func TestOtherKeysStillQuitTimerView(t *testing.T) { } // Test that pressing 'q' still quits - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}} + keyMsg := tea.KeyPressMsg{Code: 'q', Text: "q"} // Process the key _, cmd := m.Update(keyMsg) @@ -175,7 +176,7 @@ func TestOtherKeysStillQuitTimerView(t *testing.T) { func TestCountUpModeInitialization(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 3600, countUpMode: true, @@ -189,7 +190,7 @@ func TestCountUpModeInitialization(t *testing.T) { func TestCountUpModeElapsedTime(t *testing.T) { startTime := time.Now().Unix() - 30 m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: startTime, targetDuration: 3600, countUpMode: true, @@ -202,7 +203,7 @@ func TestCountUpModeElapsedTime(t *testing.T) { func TestCountUpModeKeysActivatePrompt(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 3600, countUpMode: true, @@ -210,7 +211,7 @@ func TestCountUpModeKeysActivatePrompt(t *testing.T) { } // Test 'd' key activates prompt for logging - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}} + keyMsg := tea.KeyPressMsg{Code: 'd', Text: "d"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) assert.True(t, modelTyped.promptActive, "Expected prompt to be active after pressing 'd'") @@ -219,7 +220,7 @@ func TestCountUpModeKeysActivatePrompt(t *testing.T) { // Test 't' key activates prompt for title only m.promptActive = false m.title = "Test Title" - keyMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t'}} + keyMsg = tea.KeyPressMsg{Code: 't', Text: "t"} newModel, _ = m.Update(keyMsg) modelTyped = newModel.(model) assert.True(t, modelTyped.promptActive, "Expected prompt to be active after pressing 't'") @@ -228,7 +229,7 @@ func TestCountUpModeKeysActivatePrompt(t *testing.T) { func TestCountUpModePromptInput(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 3600, countUpMode: true, @@ -239,13 +240,13 @@ func TestCountUpModePromptInput(t *testing.T) { } // Test typing characters - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'T', 'e', 's', 't'}} + keyMsg := tea.KeyPressMsg{Text: "Test"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) assert.Equal(t, "Test", modelTyped.inputBuffer, "Expected input buffer to accumulate typed characters") // Test backspace - keyMsg = tea.KeyMsg{Type: tea.KeyBackspace} + keyMsg = tea.KeyPressMsg{Code: tea.KeyBackspace} newModel, _ = modelTyped.Update(keyMsg) modelTyped = newModel.(model) assert.Equal(t, "Tes", modelTyped.inputBuffer, "Expected backspace to remove last character") @@ -257,7 +258,7 @@ func TestCountUpModePromptLogAndReset(t *testing.T) { startTime := time.Now().Unix() - 120 m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: startTime, targetDuration: 3600, countUpMode: true, @@ -286,7 +287,7 @@ func TestCountUpModePromptLogAndReset(t *testing.T) { func TestCountUpModePromptTitleOnly(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix() - 50, targetDuration: 3600, countUpMode: true, @@ -295,7 +296,7 @@ func TestCountUpModePromptTitleOnly(t *testing.T) { } // Test that pressing 't' activates title-only prompt - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t'}} + keyMsg := tea.KeyPressMsg{Code: 't', Text: "t"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -305,7 +306,7 @@ func TestCountUpModePromptTitleOnly(t *testing.T) { // Now test that input works in prompt mode (appending to pre-filled text) m = modelTyped - keyMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'N', 'e', 'w'}} + keyMsg = tea.KeyPressMsg{Text: "New"} newModel, _ = m.Update(keyMsg) modelTyped = newModel.(model) assert.Equal(t, "Old TitleNew", modelTyped.inputBuffer, "Expected input buffer to contain pre-filled text plus typed text") @@ -313,7 +314,7 @@ func TestCountUpModePromptTitleOnly(t *testing.T) { func TestCountUpModePromptWithSpaces(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix() - 50, targetDuration: 3600, countUpMode: true, @@ -322,20 +323,20 @@ func TestCountUpModePromptWithSpaces(t *testing.T) { } // Activate prompt with 'd' key - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}} + keyMsg := tea.KeyPressMsg{Code: 'd', Text: "d"} newModel, _ := m.Update(keyMsg) m = newModel.(model) assert.True(t, m.promptActive, "Expected prompt to be active") // Type "Work on " with space - keyMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o', 'n', ' ', 't', 'a', 's', 'k'}} + keyMsg = tea.KeyPressMsg{Text: "on task"} newModel, _ = m.Update(keyMsg) m = newModel.(model) assert.Equal(t, "Workon task", m.inputBuffer, "Expected input to include space from runes") // Also test explicit space key m.inputBuffer = "Test" - keyMsg = tea.KeyMsg{Type: tea.KeySpace} + keyMsg = tea.KeyPressMsg{Code: tea.KeySpace} newModel, _ = m.Update(keyMsg) m = newModel.(model) assert.Equal(t, "Test ", m.inputBuffer, "Expected KeySpace to add space to input buffer") @@ -343,7 +344,7 @@ func TestCountUpModePromptWithSpaces(t *testing.T) { func TestNormalModeSetTitle(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 1500, countUpMode: false, @@ -352,7 +353,7 @@ func TestNormalModeSetTitle(t *testing.T) { } // Press 't' to activate prompt - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t'}} + keyMsg := tea.KeyPressMsg{Code: 't', Text: "t"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -362,7 +363,7 @@ func TestNormalModeSetTitle(t *testing.T) { // Simulate typing new title modelTyped.inputBuffer = "New Title" - newModel, _ = modelTyped.Update(tea.KeyMsg{Type: tea.KeyEnter}) + newModel, _ = modelTyped.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) modelTyped = newModel.(model) assert.Equal(t, "New Title", modelTyped.title, "Expected title to be updated") @@ -379,7 +380,7 @@ func TestHKeyShowsHistoryInCountdownMode(t *testing.T) { assert.NoError(t, err) m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 1500, countUpMode: false, @@ -387,7 +388,7 @@ func TestHKeyShowsHistoryInCountdownMode(t *testing.T) { } // Press 'h' to show history - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'h'}} + keyMsg := tea.KeyPressMsg{Code: 'h', Text: "h"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -404,7 +405,7 @@ func TestHKeyShowsHistoryInCountUpMode(t *testing.T) { assert.NoError(t, err) m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 3600, countUpMode: true, @@ -412,7 +413,7 @@ func TestHKeyShowsHistoryInCountUpMode(t *testing.T) { } // Press 'h' to show history - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'h'}} + keyMsg := tea.KeyPressMsg{Code: 'h', Text: "h"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -425,7 +426,7 @@ func TestDKeyMarksDoneInCountdownMode(t *testing.T) { startTime := time.Now().Unix() - 120 m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: startTime, targetDuration: 1500, countUpMode: false, @@ -434,7 +435,7 @@ func TestDKeyMarksDoneInCountdownMode(t *testing.T) { } // Press 'd' to mark done - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}} + keyMsg := tea.KeyPressMsg{Code: 'd', Text: "d"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -462,7 +463,7 @@ func TestDKeyMarksDoneInCountUpMode(t *testing.T) { startTime := time.Now().Unix() - 120 m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: startTime, targetDuration: 3600, countUpMode: true, @@ -471,7 +472,7 @@ func TestDKeyMarksDoneInCountUpMode(t *testing.T) { } // Press 'd' to mark done - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}} + keyMsg := tea.KeyPressMsg{Code: 'd', Text: "d"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -495,7 +496,7 @@ func TestDKeyMarksDoneInCountUpMode(t *testing.T) { func TestTKeyEditsTitleInCountdownMode(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 1500, countUpMode: false, @@ -504,7 +505,7 @@ func TestTKeyEditsTitleInCountdownMode(t *testing.T) { } // Press 't' to edit title - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t'}} + keyMsg := tea.KeyPressMsg{Code: 't', Text: "t"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -522,7 +523,7 @@ func TestTKeyEditsTitleInCountdownMode(t *testing.T) { func TestTKeyEditsTitleInCountUpMode(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 3600, countUpMode: true, @@ -531,7 +532,7 @@ func TestTKeyEditsTitleInCountUpMode(t *testing.T) { } // Press 't' to edit title - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t'}} + keyMsg := tea.KeyPressMsg{Code: 't', Text: "t"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -542,7 +543,7 @@ func TestTKeyEditsTitleInCountUpMode(t *testing.T) { func TestMKeySetsDurationInCountdownMode(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 1500, // 25 minutes countUpMode: false, @@ -550,7 +551,7 @@ func TestMKeySetsDurationInCountdownMode(t *testing.T) { } // Press 'm' to set duration - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} + keyMsg := tea.KeyPressMsg{Code: 'm', Text: "m"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -567,7 +568,7 @@ func TestMKeySetsDurationInCountdownMode(t *testing.T) { func TestMKeySetsDurationInCountUpMode(t *testing.T) { m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 3600, // 1 hour countUpMode: true, @@ -575,7 +576,7 @@ func TestMKeySetsDurationInCountUpMode(t *testing.T) { } // Press 'm' to set duration - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} + keyMsg := tea.KeyPressMsg{Code: 'm', Text: "m"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -593,7 +594,7 @@ func TestMKeySetsDurationInCountUpMode(t *testing.T) { func TestMKeyResetsTimerAfterSettingDuration(t *testing.T) { startTime := time.Now().Unix() - 60 m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: startTime, targetDuration: 1500, countUpMode: false, @@ -601,7 +602,7 @@ func TestMKeyResetsTimerAfterSettingDuration(t *testing.T) { } // Press 'm' to set duration - keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}} + keyMsg := tea.KeyPressMsg{Code: 'm', Text: "m"} newModel, _ := m.Update(keyMsg) modelTyped := newModel.(model) @@ -622,7 +623,7 @@ func TestTimerEndPromptsForTitle(t *testing.T) { // Create a timer that is just about to finish m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix() - 61, targetDuration: 60, title: "Original Title", diff --git a/main.go b/main.go index 259c5c4..45bd47f 100644 --- a/main.go +++ b/main.go @@ -13,9 +13,10 @@ import ( "strconv" "time" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/progress" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/progress" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "tiny-timer/status" ) @@ -177,7 +178,7 @@ func createKeyBindings() keyMap { } func createModel(title string, countUp bool, targetDuration int64, keys keyMap) model { - prog := progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()) + prog := progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()) if countUp { prog.SetPercent(0) } else { diff --git a/model.go b/model.go index fce6a00..e65c52e 100644 --- a/model.go +++ b/model.go @@ -3,11 +3,11 @@ package main import ( "time" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/progress" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/help" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/progress" + "charm.land/bubbles/v2/table" + tea "charm.land/bubbletea/v2" "tiny-timer/status" ) diff --git a/status/status.go b/status/status.go index 869f8be..a309e1d 100644 --- a/status/status.go +++ b/status/status.go @@ -4,11 +4,12 @@ package status import ( + "image/color" "time" - "github.com/charmbracelet/bubbles/help" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/help" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" ) @@ -32,17 +33,17 @@ type ClearStatusMsg struct{} type Theme struct { Name string - Green lipgloss.Color - GreenDark lipgloss.Color - BgSubtle lipgloss.Color - Red lipgloss.Color - Error lipgloss.Color - White lipgloss.Color - Yellow lipgloss.Color - Warning lipgloss.Color - BgOverlay lipgloss.Color - Primary lipgloss.Color - Border lipgloss.Color + Green color.Color + GreenDark color.Color + BgSubtle color.Color + Red color.Color + Error color.Color + White color.Color + Yellow color.Color + Warning color.Color + BgOverlay color.Color + Primary color.Color + Border color.Color } func NewTheme() *Theme { @@ -111,7 +112,7 @@ func (m *StatusCmp) Update(msg tea.Msg) (*StatusCmp, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width - m.help.Width = msg.Width - 2 + m.help.SetWidth(msg.Width - 2) return m, nil case InfoMsg: @@ -143,7 +144,7 @@ func (m *StatusCmp) infoMsg() string { var infoTypeLabel string var infoTypeStyle lipgloss.Style var messageStyle lipgloss.Style - var messageFg lipgloss.Color + var messageFg color.Color switch m.info.Type { case InfoTypeError: @@ -174,7 +175,7 @@ func (m *StatusCmp) infoMsg() string { info := ansi.Truncate(m.info.Msg, widthLeft, "…") // Render message with calculated width - if messageFg != "" { + if messageFg != nil { messageStyle = messageStyle.Foreground(messageFg) } message := messageStyle.Width(widthLeft + 2).Render(info) diff --git a/test_helpers.go b/test_helpers.go index 47ed258..b3b73c0 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -6,8 +6,9 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/progress" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/progress" + "charm.land/lipgloss/v2" "tiny-timer/status" ) @@ -54,7 +55,7 @@ func newTestModel() model { statusCmp := status.NewStatusCmp() statusCmp.SetKeyMap(keys) return model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 60, help: newHelpModel(), diff --git a/utils.go b/utils.go index 8120d66..8dd6583 100644 --- a/utils.go +++ b/utils.go @@ -6,8 +6,8 @@ import ( "runtime" "testing" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/help" + "charm.land/lipgloss/v2" ) // A display helper for formatting the time remaining in the timer diff --git a/view.go b/view.go index b3e724e..d6f98cb 100644 --- a/view.go +++ b/view.go @@ -5,7 +5,8 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/key" + "charm.land/bubbles/v2/key" + tea "charm.land/bubbletea/v2" ) // promptKeyMap defines keybindings for prompt mode @@ -40,7 +41,7 @@ func (k tableKeyMap) FullHelp() [][]key.Binding { } // Handler that draws the UI of the application -func (m model) View() string { +func (m model) View() tea.View { if m.promptActive { pad := strings.Repeat(" ", padding) var promptText string @@ -53,7 +54,7 @@ func (m model) View() string { Confirm: m.keys.Confirm, Cancel: m.keys.Cancel, } - return "\n" + pad + promptText + "\n\n" + pad + m.help.View(promptKeys) + return tea.NewView("\n" + pad + promptText + "\n\n" + pad + m.help.View(promptKeys)) } if m.mode == tableView { @@ -69,9 +70,9 @@ func (m model) View() string { } m.status.SetKeyMap(tableKeys) statusView := m.status.View() - return "\n" + + return tea.NewView("\n" + strings.Join(paddedTable, "\n") + "\n\n" + - statusView + statusView) } elapsed := time.Now().Unix() - m.startTime @@ -96,12 +97,12 @@ func (m model) View() string { // Ensure status component has the full keymap for timer view m.status.SetKeyMap(m.keys) - + // status.View() handles displaying status messages OR help text statusView := m.status.View() - return "\n" + + return tea.NewView("\n" + titleLine + pad + m.progress.View() + fmt.Sprintf(" %s \n\n", formatDurationAsMMSS(remaining)) + - statusView + statusView) } diff --git a/view_test.go b/view_test.go index f4f9b99..0b9e717 100644 --- a/view_test.go +++ b/view_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/progress" - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/progress" + "charm.land/bubbles/v2/table" + "charm.land/lipgloss/v2" "github.com/stretchr/testify/assert" "tiny-timer/status" ) @@ -17,7 +17,7 @@ func TestViewWithTitle(t *testing.T) { statusCmp := status.NewStatusCmp() statusCmp.SetKeyMap(newTestModel().keys) m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 60, title: "Test Task", @@ -26,7 +26,7 @@ func TestViewWithTitle(t *testing.T) { keys: newTestModel().keys, } - view := m.View() + view := m.View().Content // Verify that the title is displayed in the view assert.Contains(t, view, "Test Task", "Expected view to contain the title") @@ -37,7 +37,7 @@ func TestViewWithoutTitle(t *testing.T) { statsuCmp := status.NewStatusCmp() statsuCmp.SetKeyMap(newTestModel().keys) m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 60, title: "", @@ -46,7 +46,7 @@ func TestViewWithoutTitle(t *testing.T) { keys: newTestModel().keys, } - view := m.View() + view := m.View().Content // Count the number of lines - should have one fewer line when no title is present lines := strings.Split(view, "\n") @@ -72,7 +72,7 @@ func TestTableViewMode(t *testing.T) { assert.Equal(t, timerView, m.mode) // Verify timer view is displayed with help text - view := m.View() + view := m.View().Content assert.Contains(t, view, "title") assert.Contains(t, view, "history") } @@ -90,7 +90,7 @@ func TestTableHeadersAreLeftAligned(t *testing.T) { statsuCmp := status.NewStatusCmp() statsuCmp.SetKeyMap(newTestModel().keys) m := model{ - progress: progress.New(progress.WithGradient(colorMontezumaGold, colorCream), progress.WithoutPercentage()), + progress: progress.New(progress.WithColors(lipgloss.Color(colorMontezumaGold), lipgloss.Color(colorCream)), progress.WithoutPercentage()), startTime: time.Now().Unix(), targetDuration: 60, title: "Test Task", @@ -137,6 +137,7 @@ func TestTableHeadersAreLeftAligned(t *testing.T) { table.WithRows(rows), table.WithFocused(false), table.WithHeight(len(rows)+2), // Add extra height for visibility + table.WithWidth(40+10+20), // Sum of column widths ) s := table.DefaultStyles() @@ -148,13 +149,14 @@ func TestTableHeadersAreLeftAligned(t *testing.T) { Padding(0, 0) s.Cell = s.Cell. Padding(0, 0) + s.Selected = s.Cell // No cursor highlighting for unfocused table tbl.SetStyles(s) m.table = tbl m.mode = tableView // Get the rendered view - view := m.View() + view := m.View().Content // Check that headers appear in the output assert.Contains(t, view, "Title", "Table should contain 'Title' header") @@ -203,7 +205,7 @@ func TestCountUpViewDisplay(t *testing.T) { m.mode = timerView m.title = "My Task" - view := m.View() + view := m.View().Content assert.Contains(t, view, "My Task", "Expected title to be displayed") assert.Contains(t, view, "done", "Expected count-up mode help text") assert.Contains(t, view, "title", "Expected title change help text") @@ -212,7 +214,7 @@ func TestCountUpViewDisplay(t *testing.T) { func TestHelpTextUsesTwoToneGrey(t *testing.T) { // Test that help text renders keys in light grey (#a0a0a0) and descriptions in dark grey (#626262) m := newTestModel() - m.help.Width = 80 + m.help.SetWidth(80) // Render test strings with the key and description styles keyStyle := m.help.Styles.ShortKey From 7be26b4764b21ca4e2a115da8e65f649248b1a22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 01:42:10 +0000 Subject: [PATCH 3/3] ci: update Go version to 1.25 to match go.mod requirement Co-authored-by: topfunky <26+topfunky@users.noreply.github.com> --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d11e972..7ec0a69 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.24' + go-version: '1.25' - name: Install dependencies run: |