From 08bd405ff858cde38a5ba408977f1060887caa5e Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 21 Dec 2025 10:53:23 -0700 Subject: [PATCH] refactor: centralize completions and ui helpers - Move completion functions to internal/completion package - Add Static() helper for inline static value completions - Extract ui.Confirm() using charmbracelet/huh - Implement done command via task.UpdateCmd delegation Assisted-by: Claude Opus 4.5 via Crush --- cmd/done.go | 12 +++-- cmd/habit/track.go | 19 +------ cmd/note/add.go | 19 +------ cmd/note/delete.go | 12 +---- cmd/note/list.go | 3 +- cmd/note/update.go | 3 +- cmd/person/add.go | 16 +----- cmd/person/delete.go | 12 +---- cmd/person/update.go | 3 +- cmd/task/add.go | 53 +++---------------- cmd/task/delete.go | 12 +---- cmd/task/list.go | 7 ++- cmd/task/update.go | 15 +++--- go.mod | 11 ++++ go.sum | 23 ++++++++ internal/completion/completion.go | 87 +++++++++++++++++++++++++++++++ internal/completion/static.go | 16 ++++++ internal/ui/confirm.go | 26 +++++++++ 18 files changed, 201 insertions(+), 148 deletions(-) create mode 100644 internal/completion/completion.go create mode 100644 internal/completion/static.go create mode 100644 internal/ui/confirm.go diff --git a/cmd/done.go b/cmd/done.go index ab147eab96789df2d2731db53a234d7a9ab78e4f..4fdfa5e9bada43bbcda1d831c59c321c86e74d9c 100644 --- a/cmd/done.go +++ b/cmd/done.go @@ -5,8 +5,7 @@ package cmd import ( - "fmt" - + "git.secluded.site/lune/cmd/task" "github.com/spf13/cobra" ) @@ -16,9 +15,12 @@ var doneCmd = &cobra.Command{ GroupID: "shortcuts", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - // TODO: implement as task update --status completed - fmt.Fprintf(cmd.OutOrStdout(), "Marking task %s as done (not yet implemented)\n", args[0]) + _ = task.UpdateCmd.Flags().Set("status", "completed") + + task.UpdateCmd.SetIn(cmd.InOrStdin()) + task.UpdateCmd.SetOut(cmd.OutOrStdout()) + task.UpdateCmd.SetErr(cmd.ErrOrStderr()) - return nil + return task.UpdateCmd.RunE(task.UpdateCmd, args) }, } diff --git a/cmd/habit/track.go b/cmd/habit/track.go index b193937497bb9ad4d5f10191f9e2c61d1d4cbe52..0433f8c6d53da6dd237b6c0bcdff8d781f451931 100644 --- a/cmd/habit/track.go +++ b/cmd/habit/track.go @@ -7,7 +7,7 @@ package habit import ( "fmt" - "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/completion" "github.com/spf13/cobra" ) @@ -20,7 +20,7 @@ var TrackCmd = &cobra.Command{ KEY is the habit key from your config (not the raw Lunatask ID). Tracks for today by default. Use --date to specify another date.`, Args: cobra.ExactArgs(1), - ValidArgsFunction: completeHabits, + ValidArgsFunction: completion.Habits, RunE: func(cmd *cobra.Command, args []string) error { date, _ := cmd.Flags().GetString("date") if date == "" { @@ -37,18 +37,3 @@ Tracks for today by default. Use --date to specify another date.`, func init() { TrackCmd.Flags().StringP("date", "d", "", "Date performed (natural language, default: today)") } - -// completeHabits returns habit keys from config for shell completion. -func completeHabits(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - cfg, err := config.Load() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - keys := make([]string, len(cfg.Habits)) - for i, h := range cfg.Habits { - keys[i] = h.Key - } - - return keys, cobra.ShellCompDirectiveNoFileComp -} diff --git a/cmd/note/add.go b/cmd/note/add.go index d714491fa13eaea45b675b999e63ec49a1e416c6..184e1b4f8afd0d8883bb04f9a5930223ed590337 100644 --- a/cmd/note/add.go +++ b/cmd/note/add.go @@ -7,7 +7,7 @@ package note import ( "fmt" - "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/completion" "github.com/spf13/cobra" ) @@ -34,20 +34,5 @@ func init() { AddCmd.Flags().StringP("notebook", "b", "", "Notebook key (from config)") AddCmd.Flags().StringP("content", "c", "", "Note content (use - for stdin)") - _ = AddCmd.RegisterFlagCompletionFunc("notebook", completeNotebooks) -} - -// completeNotebooks returns notebook keys from config for shell completion. -func completeNotebooks(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - cfg, err := config.Load() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - keys := make([]string, len(cfg.Notebooks)) - for i, n := range cfg.Notebooks { - keys[i] = n.Key - } - - return keys, cobra.ShellCompDirectiveNoFileComp + _ = AddCmd.RegisterFlagCompletionFunc("notebook", completion.Notebooks) } diff --git a/cmd/note/delete.go b/cmd/note/delete.go index e6594d2de124e3bae5f0527dc43a574a3e637385..cc7bec485bfbd859d08301a35e04009d56e3b479 100644 --- a/cmd/note/delete.go +++ b/cmd/note/delete.go @@ -5,10 +5,7 @@ package note import ( - "bufio" "fmt" - "os" - "strings" "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" @@ -27,14 +24,7 @@ var DeleteCmd = &cobra.Command{ force, _ := cmd.Flags().GetBool("force") if !force { - fmt.Fprintf(cmd.OutOrStderr(), "%s Delete note %s? [y/N] ", - ui.Warning.Render("Warning:"), args[0]) - - reader := bufio.NewReader(os.Stdin) - response, _ := reader.ReadString('\n') - response = strings.TrimSpace(strings.ToLower(response)) - - if response != "y" && response != "yes" { + if !ui.Confirm(fmt.Sprintf("Delete note %s?", args[0])) { fmt.Fprintln(cmd.OutOrStdout(), "Cancelled") return nil diff --git a/cmd/note/list.go b/cmd/note/list.go index a3abc0738503ba5c0a4f355b1dc964730048741b..7a3213ae82088a0729dd80480ac44aedd7e59966 100644 --- a/cmd/note/list.go +++ b/cmd/note/list.go @@ -7,6 +7,7 @@ package note import ( "fmt" + "git.secluded.site/lune/internal/completion" "github.com/spf13/cobra" ) @@ -30,5 +31,5 @@ func init() { ListCmd.Flags().StringP("notebook", "b", "", "Filter by notebook key") ListCmd.Flags().Bool("json", false, "Output as JSON") - _ = ListCmd.RegisterFlagCompletionFunc("notebook", completeNotebooks) + _ = ListCmd.RegisterFlagCompletionFunc("notebook", completion.Notebooks) } diff --git a/cmd/note/update.go b/cmd/note/update.go index e42da14aaa1f9126ba68a8c23d104068d74f2803..01b1bf33fadd22f74b38b2b3514bed46602da3f1 100644 --- a/cmd/note/update.go +++ b/cmd/note/update.go @@ -7,6 +7,7 @@ package note import ( "fmt" + "git.secluded.site/lune/internal/completion" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -34,5 +35,5 @@ func init() { UpdateCmd.Flags().StringP("content", "c", "", "Note content (use - for stdin)") UpdateCmd.Flags().StringP("date", "d", "", "Note date (natural language)") - _ = UpdateCmd.RegisterFlagCompletionFunc("notebook", completeNotebooks) + _ = UpdateCmd.RegisterFlagCompletionFunc("notebook", completion.Notebooks) } diff --git a/cmd/person/add.go b/cmd/person/add.go index b8aa8086787c5a48dfd5bcb0ca5c8b419c36a7bf..a6e02fc19f7aa243b7d633f05a83b767b85a90d2 100644 --- a/cmd/person/add.go +++ b/cmd/person/add.go @@ -7,6 +7,7 @@ package person import ( "fmt" + "git.secluded.site/lune/internal/completion" "github.com/spf13/cobra" ) @@ -26,18 +27,5 @@ var AddCmd = &cobra.Command{ func init() { AddCmd.Flags().StringP("relationship", "r", "", "Relationship strength") - _ = AddCmd.RegisterFlagCompletionFunc("relationship", completeRelationships) -} - -// completeRelationships returns relationship strength options for shell completion. -func completeRelationships(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{ - "family", - "intimate-friends", - "close-friends", - "casual-friends", - "acquaintances", - "business-contacts", - "almost-strangers", - }, cobra.ShellCompDirectiveNoFileComp + _ = AddCmd.RegisterFlagCompletionFunc("relationship", completion.Relationships) } diff --git a/cmd/person/delete.go b/cmd/person/delete.go index 103602e0228614071c6ab7b201081d47883a04eb..1fcd6a49fdfe8b18026cb5668d3383b7c770ca53 100644 --- a/cmd/person/delete.go +++ b/cmd/person/delete.go @@ -5,10 +5,7 @@ package person import ( - "bufio" "fmt" - "os" - "strings" "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" @@ -27,14 +24,7 @@ var DeleteCmd = &cobra.Command{ force, _ := cmd.Flags().GetBool("force") if !force { - fmt.Fprintf(cmd.OutOrStderr(), "%s Delete person %s? [y/N] ", - ui.Warning.Render("Warning:"), args[0]) - - reader := bufio.NewReader(os.Stdin) - response, _ := reader.ReadString('\n') - response = strings.TrimSpace(strings.ToLower(response)) - - if response != "y" && response != "yes" { + if !ui.Confirm(fmt.Sprintf("Delete person %s?", args[0])) { fmt.Fprintln(cmd.OutOrStdout(), "Cancelled") return nil diff --git a/cmd/person/update.go b/cmd/person/update.go index bd3ed4672a9ada18b157cc0f62ceb9bc3dff9e9a..2bdb031cd4c4ee0413b0e9fdacf65ab6fe7758b8 100644 --- a/cmd/person/update.go +++ b/cmd/person/update.go @@ -7,6 +7,7 @@ package person import ( "fmt" + "git.secluded.site/lune/internal/completion" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -33,5 +34,5 @@ func init() { UpdateCmd.Flags().String("last", "", "Last name") UpdateCmd.Flags().StringP("relationship", "r", "", "Relationship strength") - _ = UpdateCmd.RegisterFlagCompletionFunc("relationship", completeRelationships) + _ = UpdateCmd.RegisterFlagCompletionFunc("relationship", completion.Relationships) } diff --git a/cmd/task/add.go b/cmd/task/add.go index d0ed8c89b400d2946c64d6c462b5e59ed5a12ccb..f7cbe277a7a7bade8234a7bc71fada66e562f656 100644 --- a/cmd/task/add.go +++ b/cmd/task/add.go @@ -7,7 +7,7 @@ package task import ( "fmt" - "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/completion" "github.com/spf13/cobra" ) @@ -40,53 +40,12 @@ func init() { AddCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4") AddCmd.Flags().String("schedule", "", "Schedule date (natural language)") - _ = AddCmd.RegisterFlagCompletionFunc("area", completeAreas) - _ = AddCmd.RegisterFlagCompletionFunc("goal", completeGoals) + _ = AddCmd.RegisterFlagCompletionFunc("area", completion.Areas) + _ = AddCmd.RegisterFlagCompletionFunc("goal", completion.Goals) _ = AddCmd.RegisterFlagCompletionFunc("status", - func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"later", "next", "started", "waiting"}, cobra.ShellCompDirectiveNoFileComp - }) + completion.Static("later", "next", "started", "waiting")) _ = AddCmd.RegisterFlagCompletionFunc("motivation", - func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"must", "should", "want"}, cobra.ShellCompDirectiveNoFileComp - }) + completion.Static("must", "should", "want")) _ = AddCmd.RegisterFlagCompletionFunc("eisenhower", - func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"1", "2", "3", "4"}, cobra.ShellCompDirectiveNoFileComp - }) -} - -// completeAreas returns area keys from config for shell completion. -func completeAreas(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - cfg, err := config.Load() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - keys := make([]string, len(cfg.Areas)) - for i, a := range cfg.Areas { - keys[i] = a.Key - } - - return keys, cobra.ShellCompDirectiveNoFileComp -} - -// completeGoals returns goal keys from config for shell completion. -// Note: This returns all goals across all areas. A smarter completion -// would filter based on the --area flag value if set. -func completeGoals(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - cfg, err := config.Load() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - var keys []string - - for _, a := range cfg.Areas { - for _, g := range a.Goals { - keys = append(keys, g.Key) - } - } - - return keys, cobra.ShellCompDirectiveNoFileComp + completion.Static("1", "2", "3", "4")) } diff --git a/cmd/task/delete.go b/cmd/task/delete.go index 9bce05f3336cd93c446f1cfc4e7cc418493372fb..0cd697c802b178a8c110cab3a9dc4ae711e67740 100644 --- a/cmd/task/delete.go +++ b/cmd/task/delete.go @@ -5,10 +5,7 @@ package task import ( - "bufio" "fmt" - "os" - "strings" "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" @@ -27,14 +24,7 @@ var DeleteCmd = &cobra.Command{ force, _ := cmd.Flags().GetBool("force") if !force { - fmt.Fprintf(cmd.OutOrStderr(), "%s Delete task %s? [y/N] ", - ui.Warning.Render("Warning:"), args[0]) - - reader := bufio.NewReader(os.Stdin) - response, _ := reader.ReadString('\n') - response = strings.TrimSpace(strings.ToLower(response)) - - if response != "y" && response != "yes" { + if !ui.Confirm(fmt.Sprintf("Delete task %s?", args[0])) { fmt.Fprintln(cmd.OutOrStdout(), "Cancelled") return nil diff --git a/cmd/task/list.go b/cmd/task/list.go index b1fee5b82c3d88078451cf139d6b3f17288dfe1b..9523a7d2a5a3ec2d97fe739d0d91e687df1c67ed 100644 --- a/cmd/task/list.go +++ b/cmd/task/list.go @@ -7,6 +7,7 @@ package task import ( "fmt" + "git.secluded.site/lune/internal/completion" "github.com/spf13/cobra" ) @@ -31,9 +32,7 @@ func init() { ListCmd.Flags().StringP("status", "s", "", "Filter by status") ListCmd.Flags().Bool("json", false, "Output as JSON") - _ = ListCmd.RegisterFlagCompletionFunc("area", completeAreas) + _ = ListCmd.RegisterFlagCompletionFunc("area", completion.Areas) _ = ListCmd.RegisterFlagCompletionFunc("status", - func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"later", "next", "started", "waiting", "completed"}, cobra.ShellCompDirectiveNoFileComp - }) + completion.Static("later", "next", "started", "waiting", "completed")) } diff --git a/cmd/task/update.go b/cmd/task/update.go index 36862d017d33dd7e9d14cc02562d758fca2cf236..f658b0fca1a615aa7a20cd7af4e68c9395e03bb1 100644 --- a/cmd/task/update.go +++ b/cmd/task/update.go @@ -7,6 +7,7 @@ package task import ( "fmt" + "git.secluded.site/lune/internal/completion" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -40,14 +41,12 @@ func init() { UpdateCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4") UpdateCmd.Flags().String("schedule", "", "Schedule date (natural language)") - _ = UpdateCmd.RegisterFlagCompletionFunc("area", completeAreas) - _ = UpdateCmd.RegisterFlagCompletionFunc("goal", completeGoals) + _ = UpdateCmd.RegisterFlagCompletionFunc("area", completion.Areas) + _ = UpdateCmd.RegisterFlagCompletionFunc("goal", completion.Goals) _ = UpdateCmd.RegisterFlagCompletionFunc("status", - func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"later", "next", "started", "waiting", "completed"}, cobra.ShellCompDirectiveNoFileComp - }) + completion.Static("later", "next", "started", "waiting", "completed")) _ = UpdateCmd.RegisterFlagCompletionFunc("motivation", - func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"must", "should", "want"}, cobra.ShellCompDirectiveNoFileComp - }) + completion.Static("must", "should", "want")) + _ = UpdateCmd.RegisterFlagCompletionFunc("eisenhower", + completion.Static("1", "2", "3", "4")) } diff --git a/go.mod b/go.mod index cc5706879ef5a38f004b561d7dd467c431284499..58dcdde416ee873662662fe3307cc6cf91d99d17 100644 --- a/go.mod +++ b/go.mod @@ -13,22 +13,33 @@ require ( require ( charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/huh v0.8.0 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20251217160852-6b0c0e26fad9 // indirect github.com/charmbracelet/x/ansi v0.11.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/exp/charmtone v0.0.0-20251215102626-e0db08df7383 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // 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.6.2 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // 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.19 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.2.0 // indirect github.com/muesli/mango-cobra v1.3.0 // indirect diff --git a/go.sum b/go.sum index 3832e570abec44607885cf375ea6933b56094e5e..3da14adcd1c64cea01c7d18fb87870630e3dbe55 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,24 @@ git.secluded.site/go-lunatask v0.1.0-rc7 h1:kzwAN9h4zVTo0OBs4B23ba5mAqxus6nYU62L git.secluded.site/go-lunatask v0.1.0-rc7/go.mod h1:sWUQxme1z7qfsfS59nU5hqPvsRCt+HBmT/yBeIn6Fmc= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +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.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= 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/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= 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/ultraviolet v0.0.0-20251217160852-6b0c0e26fad9 h1:dsDBRP9Iyco0EjVpCsAzl8VGbxk04fP3sa80ySJSAZw= @@ -24,6 +34,8 @@ github.com/charmbracelet/x/exp/charmtone v0.0.0-20251215102626-e0db08df7383 h1:x github.com/charmbracelet/x/exp/charmtone v0.0.0-20251215102626-e0db08df7383/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= 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/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= 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= @@ -39,6 +51,10 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV 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= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -47,8 +63,14 @@ 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/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +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.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= @@ -80,6 +102,7 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/internal/completion/completion.go b/internal/completion/completion.go new file mode 100644 index 0000000000000000000000000000000000000000..7f03746a00c4bc85295c760c7f1dd24f6b98be6f --- /dev/null +++ b/internal/completion/completion.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Package completion provides shell completion helpers for CLI commands. +package completion + +import ( + "git.secluded.site/lune/internal/config" + "github.com/spf13/cobra" +) + +// Areas returns area keys from config for shell completion. +func Areas(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfg, err := config.Load() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + keys := make([]string, len(cfg.Areas)) + for i, a := range cfg.Areas { + keys[i] = a.Key + } + + return keys, cobra.ShellCompDirectiveNoFileComp +} + +// Goals returns all goal keys across all areas for shell completion. +func Goals(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfg, err := config.Load() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var keys []string + + for _, a := range cfg.Areas { + for _, g := range a.Goals { + keys = append(keys, g.Key) + } + } + + return keys, cobra.ShellCompDirectiveNoFileComp +} + +// Notebooks returns notebook keys from config for shell completion. +func Notebooks(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfg, err := config.Load() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + keys := make([]string, len(cfg.Notebooks)) + for i, n := range cfg.Notebooks { + keys[i] = n.Key + } + + return keys, cobra.ShellCompDirectiveNoFileComp +} + +// Habits returns habit keys from config for shell completion. +func Habits(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfg, err := config.Load() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + keys := make([]string, len(cfg.Habits)) + for i, h := range cfg.Habits { + keys[i] = h.Key + } + + return keys, cobra.ShellCompDirectiveNoFileComp +} + +// Relationships returns relationship strength options for shell completion. +func Relationships(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{ + "family", + "intimate-friends", + "close-friends", + "casual-friends", + "acquaintances", + "business-contacts", + "almost-strangers", + }, cobra.ShellCompDirectiveNoFileComp +} diff --git a/internal/completion/static.go b/internal/completion/static.go new file mode 100644 index 0000000000000000000000000000000000000000..55afbe8c9ffe272b5280d30c2bdf5505a4e86662 --- /dev/null +++ b/internal/completion/static.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package completion + +import "github.com/spf13/cobra" + +// Static returns a completion function that provides fixed values. +func Static(values ...string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + vals := append([]string(nil), values...) + + return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return vals, cobra.ShellCompDirectiveNoFileComp + } +} diff --git a/internal/ui/confirm.go b/internal/ui/confirm.go new file mode 100644 index 0000000000000000000000000000000000000000..9396c7e0a53d09cfb76690c17d39aa6255288d79 --- /dev/null +++ b/internal/ui/confirm.go @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package ui + +import "github.com/charmbracelet/huh" + +// Confirm asks the user to confirm an action using an interactive prompt. +// Returns false on cancellation or error (fail closed). +func Confirm(title string) bool { + var confirmed bool + + err := huh.NewConfirm(). + Title(title). + Affirmative("Yes"). + Negative("No"). + Inline(true). + Value(&confirmed). + Run() + if err != nil { + return false + } + + return confirmed +}