diff --git a/go.mod b/go.mod index ced1074..39b45d8 100644 --- a/go.mod +++ b/go.mod @@ -3,43 +3,47 @@ module github.com/unfunco/t go 1.25 require ( + charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/fang v0.4.3 + github.com/charmbracelet/fang v0.4.4 github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea github.com/spf13/cobra v1.10.1 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.3.2 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/colorprofile v0.3.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20251114211333-9deacb990ee7 // indirect + github.com/charmbracelet/x/ansi v0.11.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.14 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20251114205511-64e30b5ee1c5 // indirect + github.com/charmbracelet/x/exp/color v0.0.0-20251006100439-2151805163c8 // 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.5.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.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.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/mango v0.1.0 // indirect - github.com/muesli/mango-cobra v1.2.0 // indirect - github.com/muesli/mango-pflag v0.1.0 // indirect + github.com/muesli/mango v0.2.0 // indirect + github.com/muesli/mango-cobra v1.3.0 // indirect + github.com/muesli/mango-pflag v0.2.0 // indirect github.com/muesli/roff v0.1.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index ed27544..8922194 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k= +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A= @@ -6,36 +8,42 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z 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/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= 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.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= -github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= -github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY= -github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg= +github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= +github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= +github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY= +github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= 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/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM= -github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M= -github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef/go.mod h1:AThRsQH1t+dfyOKIwXRoJBniYFQUkUpQq4paheHMc2o= -github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= -github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= -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/charmbracelet/ultraviolet v0.0.0-20251114211333-9deacb990ee7 h1:PeoXe8fG5yb6uaimw4Om9eiXL74MzFSwSj3coBtDPmY= +github.com/charmbracelet/ultraviolet v0.0.0-20251114211333-9deacb990ee7/go.mod h1:6lfcr3MNP+kZR25sF1nQwJFuQnNYBlFy3PGX5rvslXc= +github.com/charmbracelet/x/ansi v0.11.1 h1:iXAC8SyMQDJgtcz9Jnw+HU8WMEctHzoTAETIeA3JXMk= +github.com/charmbracelet/x/ansi v0.11.1/go.mod h1:M49wjzpIujwPceJ+t5w3qh2i87+HRtHohgb5iTyepL0= +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/charmtone v0.0.0-20251114205511-64e30b5ee1c5 h1:hB4cwUAX4XX7BFsk9OU/Kxhg7UFkvAIVI/6KKzK9o30= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20251114205511-64e30b5ee1c5/go.mod h1:URW1lCfdUqp9aey3HTV+jRvaGLuaj2d0jVk2NTRpklY= +github.com/charmbracelet/x/exp/color v0.0.0-20251006100439-2151805163c8 h1:X5wanVZ1RmBfIQFuIp4trGdL1F6eGYL7ekJaK47e6V4= +github.com/charmbracelet/x/exp/color v0.0.0-20251006100439-2151805163c8/go.mod h1:Fq7bG2T217JwA21s/gGh/uCMT5++zGDCt8l0MvbRxcA= +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.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I= +github.com/clipperhouse/displaywidth v0.5.0/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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= @@ -49,46 +57,46 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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/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/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= -github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= -github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= -github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= -github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= -github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= +github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec= +github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E= +github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k= +github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/t.go b/internal/cmd/t.go index c4a881c..9784e80 100644 --- a/internal/cmd/t.go +++ b/internal/cmd/t.go @@ -19,6 +19,7 @@ import ( "github.com/unfunco/t/internal/list" "github.com/unfunco/t/internal/model" "github.com/unfunco/t/internal/storage" + "github.com/unfunco/t/internal/theme" "github.com/unfunco/t/internal/tui" "github.com/unfunco/t/internal/version" ) @@ -30,15 +31,21 @@ var ( ErrEmptyTitle = errors.New("todo title cannot be blank") ) -// NewDefaultTCommand returns a new t command configured with the standard -// input, output, and error file descriptors. -func NewDefaultTCommand() *cobra.Command { - return NewTCommand(os.Stdin, os.Stdout, os.Stderr) +// NewDefaultTCommandWithTheme returns a new t command using the provided theme +// and configured with the standard IO file descriptors. +func NewDefaultTCommandWithTheme(th theme.Theme) *cobra.Command { + return NewTCommandWithTheme(os.Stdin, os.Stdout, os.Stderr, th) } // NewTCommand returns a new t command configured with the given input, output, // and error file descriptors. func NewTCommand(in io.Reader, out, errOut io.Writer) *cobra.Command { + return NewTCommandWithTheme(in, out, errOut, theme.Default()) +} + +// NewTCommandWithTheme returns a new t command configured with the provided +// input, output, error descriptors and theme. +func NewTCommandWithTheme(in io.Reader, out, errOut io.Writer, th theme.Theme) *cobra.Command { var ( today bool tomorrow bool @@ -88,6 +95,7 @@ func NewTCommand(in io.Reader, out, errOut io.Writer) *cobra.Command { } m := tui.New( + th, lists[list.TodayID], lists[list.TomorrowID], lists[list.TodosID], diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..40d30e2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2025 Daniel Morris +// SPDX-License-Identifier: MIT + +// Package config handles loading and saving application configuration. +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/unfunco/t/internal/paths" + "github.com/unfunco/t/internal/theme" +) + +const configFilename = "config.json" + +// Config captures the configurable application properties. +type Config struct { + Theme theme.Config `json:"theme"` +} + +// Load retrieves the configuration from the default data directory. +func Load() (Config, error) { + dataDir, err := paths.DefaultConfigDir() + if err != nil { + return Config{}, fmt.Errorf("determine config directory: %w", err) + } + + return LoadFromDir(dataDir) +} + +// LoadFromDir retrieves the configuration from the provided directory. +func LoadFromDir(configDir string) (Config, error) { + if configDir == "" { + return Config{}, fmt.Errorf("config directory cannot be empty") + } + + cfg := Config{ + Theme: theme.DefaultConfig(), + } + + configPath := filepath.Join(configDir, configFilename) + + data, err := os.ReadFile(configPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return cfg, nil + } + + return Config{}, fmt.Errorf("read config: %w", err) + } + + if len(data) == 0 { + return cfg, nil + } + + if err := json.Unmarshal(data, &cfg); err != nil { + return Config{}, fmt.Errorf("decode config: %w", err) + } + + return cfg, nil +} + +// Path returns the default configuration file path. +func Path() (string, error) { + configDir, err := paths.DefaultConfigDir() + if err != nil { + return "", fmt.Errorf("determine config directory: %w", err) + } + + return filepath.Join(configDir, configFilename), nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..b99d5af --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2025 Daniel Morris +// SPDX-License-Identifier: MIT + +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/unfunco/t/internal/theme" +) + +func TestLoadFromDirReturnsDefaultsWhenMissing(t *testing.T) { + dir := t.TempDir() + + cfg, err := LoadFromDir(dir) + if err != nil { + t.Fatalf("LoadFromDir() error = %v", err) + } + + if want := theme.DefaultConfig(); cfg.Theme != want { + t.Fatalf("theme mismatch, want %+v got %+v", want, cfg.Theme) + } +} + +func TestLoadFromDirReadsConfigFile(t *testing.T) { + dir := t.TempDir() + content := []byte(`{ + "theme": { + "text": "#010101", + "muted": "#020202", + "highlight": "#030303", + "success": "#040404", + "worry": "#050505" + } +}`) + + if err := os.WriteFile(filepath.Join(dir, "config.json"), content, 0o600); err != nil { + t.Fatalf("failed to write config file: %v", err) + } + + cfg, err := LoadFromDir(dir) + if err != nil { + t.Fatalf("LoadFromDir() error = %v", err) + } + + want := theme.Config{ + Text: "#010101", + Muted: "#020202", + Highlight: "#030303", + Success: "#040404", + Worry: "#050505", + } + + if cfg.Theme != want { + t.Fatalf("theme mismatch, want %+v got %+v", want, cfg.Theme) + } +} diff --git a/internal/paths/paths.go b/internal/paths/paths.go new file mode 100644 index 0000000..6545849 --- /dev/null +++ b/internal/paths/paths.go @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025 Daniel Morris +// SPDX-License-Identifier: MIT + +// Package paths centralises platform-specific filesystem locations. +package paths + +import ( + "fmt" + "os" + "path/filepath" +) + +const app = "t" + +// DefaultDataDir returns the standard directory used for mutable data. +func DefaultDataDir() (string, error) { + return resolveDir("XDG_DATA_HOME", func(home string) string { + return filepath.Join(home, ".local", "share") + }) +} + +// DefaultConfigDir returns the standard directory used for configuration. +func DefaultConfigDir() (string, error) { + return resolveDir("XDG_CONFIG_HOME", func(home string) string { + return filepath.Join(home, ".config") + }) +} + +func resolveDir(envVar string, fallback func(home string) string) (string, error) { + if dir := os.Getenv(envVar); dir != "" { + return filepath.Join(dir, app), nil + } + + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %w", err) + } + + return filepath.Join(fallback(home), app), nil +} diff --git a/internal/storage/file.go b/internal/storage/file.go index 6ed8e8d..4dac7bc 100644 --- a/internal/storage/file.go +++ b/internal/storage/file.go @@ -11,6 +11,7 @@ import ( "github.com/unfunco/t/internal/list" "github.com/unfunco/t/internal/model" + "github.com/unfunco/t/internal/paths" ) // File persists todos on disk. @@ -23,7 +24,7 @@ var _ Storage = (*File)(nil) // NewFileStorage creates file-backed storage rooted in the default data // directory, typically ~/.local/share/t. func NewFileStorage() (*File, error) { - dataDir, err := getDataDir() + dataDir, err := paths.DefaultDataDir() if err != nil { return nil, fmt.Errorf("failed to get data directory: %w", err) } @@ -40,24 +41,6 @@ func NewFileStorageWithDir(dataDir string) (*File, error) { return &File{dataDir}, nil } -// getDataDir returns the path to the data directory. -func getDataDir() (string, error) { - var configDir string - - if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" { - configDir = xdgDataHome - } else { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get user home directory: %w", err) - } - - configDir = filepath.Join(homeDir, ".local", "share") - } - - return filepath.Join(configDir, "t"), nil -} - // ensureDataDir creates the data directory if it doesn't exist. func (s *File) ensureDataDir() error { if err := os.MkdirAll(s.dataDir, 0o700); err != nil { diff --git a/internal/theme/theme.go b/internal/theme/theme.go index f0cb57a..e5c4f8d 100644 --- a/internal/theme/theme.go +++ b/internal/theme/theme.go @@ -14,11 +14,22 @@ import ( // Config represents the raw theme configuration values. type Config struct { - Text string - Muted string - Highlight string - Success string - Worry string + Text string `json:"text"` + Muted string `json:"muted"` + Highlight string `json:"highlight"` + Success string `json:"success"` + Worry string `json:"worry"` +} + +// DefaultConfig returns the built-in theme configuration. +func DefaultConfig() Config { + return Config{ + Text: "#FFFFFF", + Muted: "#696969", + Highlight: "#58C5C7", + Success: "#99CC00", + Worry: "#FF7676", + } } // Theme defines the theme for the TUI and CLI help docs. @@ -29,11 +40,11 @@ type Theme struct { Success PaletteColor Worry PaletteColor - // UI characters - CursorChar string // Character shown next to selected todo item + CursorChar string // Character shown next to selected todo item. } -// PaletteColor keeps a hex colour string for LipGloss and an RGBA version for Fang. +// PaletteColor keeps a hex colour string for LipGloss and an RGBA version +// for Fang. type PaletteColor struct { raw string rgba color.RGBA @@ -51,13 +62,7 @@ func (c PaletteColor) RGBA() color.RGBA { // Default returns the default theme. func Default() Theme { - return MustFromConfig(Config{ - Text: "#FFFFFF", - Muted: "#696969", - Highlight: "#58C5C7", - Success: "#99CC00", - Worry: "#FF7676", - }) + return MustFromConfig(DefaultConfig()) } // MustFromConfig creates a Theme from a Config and panics if parsing fails. @@ -66,6 +71,7 @@ func MustFromConfig(cfg Config) Theme { if err != nil { panic(fmt.Sprintf("invalid theme configuration: %v", err)) } + return t } @@ -133,7 +139,7 @@ func normalizeHex(input string) (string, error) { case 3: s = fmt.Sprintf("%c%c%c%c%c%c", s[0], s[0], s[1], s[1], s[2], s[2]) case 6: - // Sound. + // All good in the hood. default: return "", fmt.Errorf("hex colour must be 3 or 6 characters, got %d", len(s)) } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 2751b73..966fab7 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -154,8 +154,8 @@ type Model struct { editingIndex int } -// New creates a new TUI model with the provided todo lists. -func New(todayList, tomorrowList, todoList *model.TodoList) Model { +// New creates a new TUI model with the provided todo lists and theme. +func New(th theme.Theme, todayList, tomorrowList, todoList *model.TodoList) Model { ti := textinput.New() ti.Placeholder = "Todo title" ti.CharLimit = 100 @@ -171,7 +171,7 @@ func New(todayList, tomorrowList, todoList *model.TodoList) Model { keys: DefaultKeyMap(), activeTab: TabToday, cursor: 0, - theme: theme.Default(), + theme: th, todayList: todayList, tomorrowList: tomorrowList, todoList: todoList, diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index b35291c..4ed70de 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -11,6 +11,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/unfunco/t/internal/model" + "github.com/unfunco/t/internal/theme" ) var todoCounter int @@ -43,7 +44,7 @@ func newTestModel() Model { Name: "Todos", Todos: []model.Todo{newTestTodo("General task", "")}, } - return New(todayList, tomorrowList, todoList) + return New(theme.Default(), todayList, tomorrowList, todoList) } func TestNew(t *testing.T) { diff --git a/main.go b/main.go index 27ad496..0c4bd4f 100644 --- a/main.go +++ b/main.go @@ -2,29 +2,71 @@ package main import ( "context" + "fmt" "image/color" "os" + "charm.land/lipgloss/v2" "github.com/charmbracelet/fang" - "github.com/charmbracelet/lipgloss/v2" "github.com/unfunco/t/internal/cmd" + "github.com/unfunco/t/internal/config" "github.com/unfunco/t/internal/theme" ) func main() { + configPath := "" + if path, err := config.Path(); err == nil { + configPath = path + } + + cfg := config.Config{ + Theme: theme.DefaultConfig(), + } + if loadedCfg, err := config.Load(); err != nil { + logConfigWarning(configPath, err) + } else { + cfg = loadedCfg + } + + th := theme.Default() + if parsedTheme, err := theme.FromConfig(cfg.Theme); err != nil { + logThemeWarning(configPath, err) + } else { + th = parsedTheme + } + if err := fang.Execute( context.Background(), - cmd.NewDefaultTCommand(), - fang.WithColorSchemeFunc(customColorScheme), + cmd.NewDefaultTCommandWithTheme(th), + fang.WithColorSchemeFunc(func(c lipgloss.LightDarkFunc) fang.ColorScheme { + return customColorScheme(c, th) + }), fang.WithNotifySignal(os.Interrupt, os.Kill), ); err != nil { os.Exit(1) } } -func customColorScheme(c lipgloss.LightDarkFunc) fang.ColorScheme { +func logConfigWarning(configPath string, err error) { + if configPath == "" { + _, _ = fmt.Fprintf(os.Stderr, "warning: failed to load config: %v; using defaults\n", err) + return + } + + _, _ = fmt.Fprintf(os.Stderr, "warning: failed to load config at %s: %v; using defaults\n", configPath, err) +} + +func logThemeWarning(configPath string, err error) { + if configPath == "" { + _, _ = fmt.Fprintf(os.Stderr, "warning: invalid theme configuration: %v; using default theme\n", err) + return + } + + _, _ = fmt.Fprintf(os.Stderr, "warning: invalid theme configuration in %s: %v; using default theme\n", configPath, err) +} + +func customColorScheme(c lipgloss.LightDarkFunc, th theme.Theme) fang.ColorScheme { scheme := fang.AnsiColorScheme(c) - th := theme.Default() scheme.Base = th.Text.RGBA() scheme.Description = th.Muted.RGBA()