From ce6e66f420438d00c4c3765a3650957384973c4b Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 21 Dec 2025 19:30:18 -0700 Subject: [PATCH] feat(habit): implement track command Uses go-dateparser for natural language date input (e.g., "yesterday", "2 days ago", "March 5"). Adds internal/dateutil package for reuse. Assisted-by: Claude Sonnet 4 via Crush --- AGENTS.md | 2 ++ cmd/habit/track.go | 65 ++++++++++++++++++++++++++++------- go.mod | 7 ++++ go.sum | 14 ++++++++ internal/dateutil/dateutil.go | 29 ++++++++++++++++ 5 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 internal/dateutil/dateutil.go diff --git a/AGENTS.md b/AGENTS.md index 82fa415c66d8bae92ad8cef8912e1e65010284c8..1cd7a55283d8c4d0317a0fb9fe1c8e0b28920fc0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,8 @@ Reference docs via context7 or doc-agent when unsure: parsing. - `github.com/spf13/cobra` (context7: `/websites/pkg_go_dev_github_com_spf13_cobra`): CLI framework. +- `github.com/markusmobius/go-dateparser` (context7: + `/markusmobius/go-dateparser`): Natural language date parsing. ## Gotchas diff --git a/cmd/habit/track.go b/cmd/habit/track.go index d603717b4474cb9f9c51e781acd6a1bf6e537fed..d6e050061a8e823fd740fc4bd0d36944a4f3f30a 100644 --- a/cmd/habit/track.go +++ b/cmd/habit/track.go @@ -5,12 +5,21 @@ package habit import ( + "errors" "fmt" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/completion" + "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/dateutil" + "git.secluded.site/lune/internal/ui" "github.com/spf13/cobra" ) +// ErrUnknownHabit indicates the specified habit key was not found in config. +var ErrUnknownHabit = errors.New("unknown habit key") + // TrackCmd tracks a habit activity. Exported for potential use by shortcuts. var TrackCmd = &cobra.Command{ Use: "track KEY", @@ -18,22 +27,54 @@ var TrackCmd = &cobra.Command{ Long: `Record that a habit was performed. KEY is the habit key from your config (not the raw Lunatask ID). -Tracks for today by default. Use --date to specify another date.`, +Tracks for today by default. Use --date to specify another date +using natural language (e.g., "yesterday", "2 days ago", "March 5").`, Args: cobra.ExactArgs(1), ValidArgsFunction: completion.Habits, - RunE: func(cmd *cobra.Command, args []string) error { - date, _ := cmd.Flags().GetString("date") - if date == "" { - date = "today" - } + RunE: runTrack, +} + +func init() { + TrackCmd.Flags().StringP("date", "d", "", "Date performed (default: today)") +} + +func runTrack(cmd *cobra.Command, args []string) error { + habitKey := args[0] + + cfg, err := config.Load() + if err != nil { + return err + } - // TODO: implement habit tracking - fmt.Fprintf(cmd.OutOrStdout(), "Tracking habit %s for %s (not yet implemented)\n", args[0], date) + habit := cfg.HabitByKey(habitKey) + if habit == nil { + return fmt.Errorf("%w: %s", ErrUnknownHabit, habitKey) + } - return nil - }, + date, err := resolveDate(cmd) + if err != nil { + return err + } + + apiClient, err := client.New() + if err != nil { + return err + } + + req := &lunatask.TrackHabitActivityRequest{PerformedOn: date} + + if _, err := apiClient.TrackHabitActivity(cmd.Context(), habit.ID, req); err != nil { + return err + } + + fmt.Fprintf(cmd.OutOrStdout(), "%s\n", + ui.Success.Render("Tracked "+habit.Name+" for "+date.String())) + + return nil } -func init() { - TrackCmd.Flags().StringP("date", "d", "", "Date performed (natural language, default: today)") +func resolveDate(cmd *cobra.Command) (lunatask.Date, error) { + dateStr, _ := cmd.Flags().GetString("date") + + return dateutil.Parse(dateStr) } diff --git a/go.mod b/go.mod index 9d59632f4271ace6d5a9589fa02cd31efb97a249..52d25313d77ab35d49741bc0740a8e19f9c07d04 100644 --- a/go.mod +++ b/go.mod @@ -39,8 +39,13 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/hablullah/go-hijri v1.0.2 // indirect + github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/magefile/mage v1.14.0 // indirect + github.com/markusmobius/go-dateparser v1.2.4 // 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 @@ -54,6 +59,8 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/tetratelabs/wazero v1.2.1 // indirect + github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect diff --git a/go.sum b/go.sum index 3d9e80eb5d49a4374737b40e462be67e0fd4f559..dca33022aa7f5cf756c6c57285f185938a18cd02 100644 --- a/go.sum +++ b/go.sum @@ -77,12 +77,22 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= +github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= +github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= +github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE= +github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE= github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70= github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk= 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.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/markusmobius/go-dateparser v1.2.4 h1:2e8XJozaERVxGwsRg72coi51L2aiYqE2gukkdLc85ck= +github.com/markusmobius/go-dateparser v1.2.4/go.mod h1:CBAUADJuMNhJpyM6IYaWAoFhtKaqnUcznY2cL7gNugY= 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= @@ -119,6 +129,10 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs= +github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= +github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw= +github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= 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= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= diff --git a/internal/dateutil/dateutil.go b/internal/dateutil/dateutil.go new file mode 100644 index 0000000000000000000000000000000000000000..da6aeaefeaf74c1de3a7a7e3bc8d148aa0e0b9b4 --- /dev/null +++ b/internal/dateutil/dateutil.go @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Package dateutil provides natural language date parsing for lune commands. +package dateutil + +import ( + "fmt" + + "git.secluded.site/go-lunatask" + dps "github.com/markusmobius/go-dateparser" +) + +// Parse parses a natural language date string into a lunatask.Date. +// Supports formats like "yesterday", "2 days ago", "March 5", "2024-01-15", etc. +// Returns today's date if the input is empty. +func Parse(input string) (lunatask.Date, error) { + if input == "" { + return lunatask.Today(), nil + } + + parsed, err := dps.Parse(nil, input) + if err != nil { + return lunatask.Date{}, fmt.Errorf("parsing date %q: %w", input, err) + } + + return lunatask.NewDate(parsed.Time), nil +}