@@ -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
@@ -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)
}
@@ -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
@@ -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=
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// 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
+}