diff --git a/.golangci.yaml b/.golangci.yaml index 14cb3d2a45d8cf7f2dd399b14ab486a94ac13c6f..55ca3552f4099bb0a4dc9ac24d0af7ed2ae75968 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -151,5 +151,6 @@ linters: - path: internal/ui/ linters: - gochecknoglobals # Style constants are package-level vars + - ireturn # Generic spinner helper uses type params - path: internal/config/ text: "0o700" # Config directory permissions are intentional diff --git a/cmd/habit/track.go b/cmd/habit/track.go index d6e050061a8e823fd740fc4bd0d36944a4f3f30a..c63e192f600b42946805a5ebce737f8f9ce65114 100644 --- a/cmd/habit/track.go +++ b/cmd/habit/track.go @@ -63,7 +63,11 @@ func runTrack(cmd *cobra.Command, args []string) error { req := &lunatask.TrackHabitActivityRequest{PerformedOn: date} - if _, err := apiClient.TrackHabitActivity(cmd.Context(), habit.ID, req); err != nil { + if err := ui.SpinVoid("Tracking habit…", func() error { + _, err := apiClient.TrackHabitActivity(cmd.Context(), habit.ID, req) + + return err + }); err != nil { return err } diff --git a/cmd/init/apikey.go b/cmd/init/apikey.go index 2ce799c8930ac9e1867ecafa792b42d97854c8ab..bb5b4554d064bf244188bd32dd8ae1889aeaa5f3 100644 --- a/cmd/init/apikey.go +++ b/cmd/init/apikey.go @@ -13,7 +13,6 @@ import ( "git.secluded.site/go-lunatask" "github.com/charmbracelet/huh" - "github.com/charmbracelet/huh/spinner" "github.com/spf13/cobra" "git.secluded.site/lune/internal/client" @@ -205,19 +204,9 @@ func promptForToken(out io.Writer) error { } func validateWithSpinner(token string) error { - var validationErr error - - err := spinner.New(). - Title("Verifying access token..."). - Action(func() { - validationErr = validateTokenWithPing(token) - }). - Run() - if err != nil { - return err - } - - return validationErr + return ui.SpinVoid("Verifying access token…", func() error { + return validateTokenWithPing(token) + }) } func validateTokenWithPing(token string) error { diff --git a/cmd/journal/add.go b/cmd/journal/add.go index 6f42c97aab8d3ec4f2d025b77a303013cc838009..d84e23e7fe0929e7b6b5b104e61fdfe8e15d57c1 100644 --- a/cmd/journal/add.go +++ b/cmd/journal/add.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "git.secluded.site/go-lunatask" "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/dateutil" "git.secluded.site/lune/internal/ui" @@ -60,7 +61,9 @@ func runAdd(cmd *cobra.Command, args []string) error { builder.WithName(name) } - entry, err := builder.Create(cmd.Context()) + entry, err := ui.Spin("Creating journal entry…", func() (*lunatask.JournalEntry, error) { + return builder.Create(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/note/add.go b/cmd/note/add.go index 0f3a5a2f922be3cc3fb172212924c9b54868c4dd..563ea3222b6dc3dcf25d387a167e046ec560fcf3 100644 --- a/cmd/note/add.go +++ b/cmd/note/add.go @@ -72,7 +72,9 @@ func runAdd(cmd *cobra.Command, args []string) error { applySource(cmd, builder) - note, err := builder.Create(cmd.Context()) + note, err := ui.Spin("Creating note…", func() (*lunatask.Note, error) { + return builder.Create(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/note/delete.go b/cmd/note/delete.go index b7f52a1cd73ce92ce7fe40f268a9ad7dd370801c..6f22b0eda88a6cd3c51ba6db0e135ffbddabde12 100644 --- a/cmd/note/delete.go +++ b/cmd/note/delete.go @@ -7,6 +7,7 @@ package note import ( "fmt" + "git.secluded.site/go-lunatask" "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" @@ -48,7 +49,9 @@ func runDelete(cmd *cobra.Command, args []string) error { return err } - note, err := apiClient.DeleteNote(cmd.Context(), id) + note, err := ui.Spin("Deleting note…", func() (*lunatask.Note, error) { + return apiClient.DeleteNote(cmd.Context(), id) + }) if err != nil { return err } diff --git a/cmd/note/list.go b/cmd/note/list.go index 75f4853914e7b5c6ce2cdff37649879b7c6c884b..8d53896a48811bf1986a75a0163c2361cc96379e 100644 --- a/cmd/note/list.go +++ b/cmd/note/list.go @@ -47,7 +47,9 @@ func runList(cmd *cobra.Command, _ []string) error { opts := buildListOptions(cmd) - notes, err := apiClient.ListNotes(cmd.Context(), opts) + notes, err := ui.Spin("Fetching notes…", func() ([]lunatask.Note, error) { + return apiClient.ListNotes(cmd.Context(), opts) + }) if err != nil { return err } diff --git a/cmd/note/show.go b/cmd/note/show.go index 71011cb44b53d65b71c32036dc22362d95ff0a28..438ed18b80e6d901665d96e8c0d1a3df45b35c54 100644 --- a/cmd/note/show.go +++ b/cmd/note/show.go @@ -45,7 +45,9 @@ func runShow(cmd *cobra.Command, args []string) error { return err } - note, err := apiClient.GetNote(cmd.Context(), id) + note, err := ui.Spin("Fetching note…", func() (*lunatask.Note, error) { + return apiClient.GetNote(cmd.Context(), id) + }) if err != nil { return err } diff --git a/cmd/note/update.go b/cmd/note/update.go index c57eb5bc98c69a2c20b5b4f2de6f9672977b8d12..f3ff9e3680c4cee05bde589ff9e1fb9d9e5c4bff 100644 --- a/cmd/note/update.go +++ b/cmd/note/update.go @@ -67,7 +67,9 @@ func runUpdate(cmd *cobra.Command, args []string) error { return err } - note, err := builder.Update(cmd.Context()) + note, err := ui.Spin("Updating note…", func() (*lunatask.Note, error) { + return builder.Update(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/person/add.go b/cmd/person/add.go index f25edff380ddc7ca4301cbd1045945ba466aec24..a1c1597b9b127d4edfc2544ec2274be9b0485c03 100644 --- a/cmd/person/add.go +++ b/cmd/person/add.go @@ -51,7 +51,9 @@ func runAdd(cmd *cobra.Command, args []string) error { applySource(cmd, builder) - person, err := builder.Create(cmd.Context()) + person, err := ui.Spin("Creating person…", func() (*lunatask.Person, error) { + return builder.Create(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/person/delete.go b/cmd/person/delete.go index b57bb858a8e09530316a75f9e76b58fd6fa5be8c..6e3902a55321d46ca8fd4553213bc0fa57af2265 100644 --- a/cmd/person/delete.go +++ b/cmd/person/delete.go @@ -7,6 +7,7 @@ package person import ( "fmt" + "git.secluded.site/go-lunatask" "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" @@ -48,7 +49,9 @@ func runDelete(cmd *cobra.Command, args []string) error { return err } - person, err := apiClient.DeletePerson(cmd.Context(), id) + person, err := ui.Spin("Deleting person…", func() (*lunatask.Person, error) { + return apiClient.DeletePerson(cmd.Context(), id) + }) if err != nil { return err } diff --git a/cmd/person/list.go b/cmd/person/list.go index 74d8246e974c9902ea2b2271b4194479e87bd56b..a5be1937f92d0691b0dab3a7ac862b3f85c3f60b 100644 --- a/cmd/person/list.go +++ b/cmd/person/list.go @@ -45,7 +45,9 @@ func runList(cmd *cobra.Command, _ []string) error { opts := buildListOptions(cmd) - people, err := apiClient.ListPeople(cmd.Context(), opts) + people, err := ui.Spin("Fetching people…", func() ([]lunatask.Person, error) { + return apiClient.ListPeople(cmd.Context(), opts) + }) if err != nil { return err } diff --git a/cmd/person/show.go b/cmd/person/show.go index adaa8b51fd56639891ffa51f6acb246c65087a70..5929d22d59ba4baa24dc1e61511a05169510910f 100644 --- a/cmd/person/show.go +++ b/cmd/person/show.go @@ -44,7 +44,9 @@ func runShow(cmd *cobra.Command, args []string) error { return err } - person, err := apiClient.GetPerson(cmd.Context(), id) + person, err := ui.Spin("Fetching person…", func() (*lunatask.Person, error) { + return apiClient.GetPerson(cmd.Context(), id) + }) if err != nil { return err } diff --git a/cmd/person/timeline.go b/cmd/person/timeline.go index 60c46934e6605aee894c4f25ac60deeb413c64f0..9d6106c2570abd69ab9aa3265361232ee33eb259 100644 --- a/cmd/person/timeline.go +++ b/cmd/person/timeline.go @@ -54,7 +54,9 @@ func runTimeline(cmd *cobra.Command, args []string) error { return err } - note, err := builder.Create(cmd.Context()) + note, err := ui.Spin("Creating timeline note…", func() (*lunatask.PersonTimelineNote, error) { + return builder.Create(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/person/update.go b/cmd/person/update.go index e1a6418c378e1db331be02ade15c29c887c514f8..0e4c8233e919281c00427b719d5b94d261a8078a 100644 --- a/cmd/person/update.go +++ b/cmd/person/update.go @@ -54,7 +54,9 @@ func runUpdate(cmd *cobra.Command, args []string) error { return err } - person, err := builder.Update(cmd.Context()) + person, err := ui.Spin("Updating person…", func() (*lunatask.Person, error) { + return builder.Update(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/ping.go b/cmd/ping.go index a96bf05e7004b7dada3a9055e45fe1d61dad879b..31ceeaed320c01646b5db7a2fc49f1e04b2ec08d 100644 --- a/cmd/ping.go +++ b/cmd/ping.go @@ -7,6 +7,7 @@ package cmd import ( "fmt" + "git.secluded.site/go-lunatask" "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/ui" "github.com/spf13/cobra" @@ -21,7 +22,9 @@ var pingCmd = &cobra.Command{ return err } - resp, err := c.Ping(cmd.Context()) + resp, err := ui.Spin("Verifying token…", func() (*lunatask.PingResponse, error) { + return c.Ping(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/task/add.go b/cmd/task/add.go index 7c26c5c0c54f485f1cafb201f09a29e77f1fb4ca..7075e75bee004693ef7e533f305355888bf5b759 100644 --- a/cmd/task/add.go +++ b/cmd/task/add.go @@ -84,7 +84,9 @@ func runAdd(cmd *cobra.Command, args []string) error { return err } - task, err := builder.Create(cmd.Context()) + task, err := ui.Spin("Creating task…", func() (*lunatask.Task, error) { + return builder.Create(cmd.Context()) + }) if err != nil { return err } diff --git a/cmd/task/delete.go b/cmd/task/delete.go index 498e26e25ed1943439ed77377d23bf6de7ab04d7..68ff31e37d8d954a9715d1b578aff989afa25293 100644 --- a/cmd/task/delete.go +++ b/cmd/task/delete.go @@ -49,7 +49,11 @@ func runDelete(cmd *cobra.Command, args []string) error { return err } - if _, err := apiClient.DeleteTask(cmd.Context(), id); err != nil { + if err := ui.SpinVoid("Deleting task…", func() error { + _, err := apiClient.DeleteTask(cmd.Context(), id) + + return err + }); err != nil { return err } diff --git a/cmd/task/list.go b/cmd/task/list.go index 65e14a2791c1b1ae21acde0a2a09a236c0d31998..c387d0fd6416be59b0d9f83d5e97b5ec16dcdf86 100644 --- a/cmd/task/list.go +++ b/cmd/task/list.go @@ -55,7 +55,9 @@ func runList(cmd *cobra.Command, _ []string) error { return err } - tasks, err := apiClient.ListTasks(cmd.Context(), nil) + tasks, err := ui.Spin("Fetching tasks…", func() ([]lunatask.Task, error) { + return apiClient.ListTasks(cmd.Context(), nil) + }) if err != nil { return err } diff --git a/cmd/task/show.go b/cmd/task/show.go index 4737123ed1a63d0f61830ab98209e7ba83e88bea..34568c19ca37619bfae1ee8b7200d6e775d09f77 100644 --- a/cmd/task/show.go +++ b/cmd/task/show.go @@ -45,7 +45,9 @@ func runShow(cmd *cobra.Command, args []string) error { return err } - task, err := apiClient.GetTask(cmd.Context(), id) + task, err := ui.Spin("Fetching task…", func() (*lunatask.Task, error) { + return apiClient.GetTask(cmd.Context(), id) + }) if err != nil { return err } diff --git a/cmd/task/update.go b/cmd/task/update.go index 7ef34a92bd7444155a21a10d0172bc31e474ca67..43d935a397c6c502e72f6a9839378fc91cd61815 100644 --- a/cmd/task/update.go +++ b/cmd/task/update.go @@ -79,7 +79,9 @@ func runUpdate(cmd *cobra.Command, args []string) error { return err } - task, err := builder.Update(cmd.Context()) + task, err := ui.Spin("Updating task…", func() (*lunatask.Task, error) { + return builder.Update(cmd.Context()) + }) if err != nil { return err } diff --git a/internal/stats/stats.go b/internal/stats/stats.go index 6cd4ebb6850a11ceba53f2a5f781e0dc4ff03d41..11ae2a036df90de30bd753274f3a7fbaa10d1e69 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -10,6 +10,7 @@ import ( "fmt" "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/ui" ) // TaskCounter provides methods to count tasks by various criteria. @@ -71,7 +72,9 @@ func (tc *TaskCounter) ensureTasks(ctx context.Context) error { return nil } - tasks, err := tc.client.ListTasks(ctx, nil) + tasks, err := ui.Spin("Fetching tasks…", func() ([]lunatask.Task, error) { + return tc.client.ListTasks(ctx, nil) + }) if err != nil { return fmt.Errorf("listing tasks: %w", err) } diff --git a/internal/ui/spinner.go b/internal/ui/spinner.go new file mode 100644 index 0000000000000000000000000000000000000000..18e8edb61cec10b7367fcdf8ecaea4e8b6707828 --- /dev/null +++ b/internal/ui/spinner.go @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package ui + +import ( + "fmt" + + "github.com/charmbracelet/huh/spinner" +) + +// Spin executes fn while displaying a spinner with the given title. +// Uses generics to preserve the return type of the wrapped function. +func Spin[T any](title string, fn func() (T, error)) (T, error) { + var result T + + var fnErr error + + spinErr := spinner.New(). + Title(title). + Action(func() { + result, fnErr = fn() + }). + Run() + if spinErr != nil { + return result, fmt.Errorf("spinner: %w", spinErr) + } + + return result, fnErr +} + +// SpinVoid executes fn while displaying a spinner with the given title. +// Use for functions that only return an error. +func SpinVoid(title string, fn func() error) error { + var fnErr error + + spinErr := spinner.New(). + Title(title). + Action(func() { + fnErr = fn() + }). + Run() + if spinErr != nil { + return fmt.Errorf("spinner: %w", spinErr) + } + + return fnErr +}