From 6ab605d04ad6f6363476a4e2528d8230f4a6de53 Mon Sep 17 00:00:00 2001 From: nobe4 Date: Sat, 20 Jul 2024 12:52:04 +0200 Subject: [PATCH] refactor!(cmd): reorganize the commands (#102) --- internal/cmd/deprecated.go | 17 +++++ internal/cmd/list.go | 66 ----------------- internal/cmd/repl.go | 50 ------------- internal/cmd/root.go | 106 +++++++++++++++++++++------- internal/cmd/sync.go | 25 ++++++- internal/manager/manager.go | 34 ++++----- internal/manager/refreshStrategy.go | 47 ++++++++++++ 7 files changed, 185 insertions(+), 160 deletions(-) create mode 100644 internal/cmd/deprecated.go delete mode 100644 internal/cmd/list.go delete mode 100644 internal/cmd/repl.go create mode 100644 internal/manager/refreshStrategy.go diff --git a/internal/cmd/deprecated.go b/internal/cmd/deprecated.go new file mode 100644 index 0000000..4d1b32d --- /dev/null +++ b/internal/cmd/deprecated.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(&cobra.Command{ + Use: "repl", + Short: "deprecated: use `gh-not --repl` instead", + }) + + rootCmd.AddCommand(&cobra.Command{ + Use: "list", + Short: "deprecated: use 'gh-not' instead", + }) +} diff --git a/internal/cmd/list.go b/internal/cmd/list.go deleted file mode 100644 index b70d233..0000000 --- a/internal/cmd/list.go +++ /dev/null @@ -1,66 +0,0 @@ -package cmd - -import ( - "fmt" - "log/slog" - - "github.com/nobe4/gh-not/internal/jq" - "github.com/spf13/cobra" -) - -var ( - filterFlag = "" - jqFlag = "" - - listCmd = &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List notifications", - Example: ` - gh-not list - gh-not list --filter '.author.login | contains("4")' -`, - RunE: runList, - } -) - -func init() { - rootCmd.AddCommand(listCmd) - - listCmd.Flags().StringVarP(&filterFlag, "filter", "f", "", "Filter with a jq expression passed into a select(...) call") - listCmd.Flags().StringVarP(&jqFlag, "jq", "q", "", "jq expression to run on the notification list") - listCmd.MarkFlagsMutuallyExclusive("filter", "jq") -} - -func runList(cmd *cobra.Command, args []string) error { - if err := manager.Load(); err != nil { - slog.Error("Failed to load the notifications", "err", err) - return err - } - - notifications := manager.Notifications.Visible() - - if filterFlag != "" { - notificationsList, err := jq.Filter(filterFlag, notifications) - if err != nil { - return err - } - notifications = notificationsList - } - - if jqFlag != "" { - return fmt.Errorf("`gh-not list --jq` implementation needed") - } - - out, err := notifications.Table() - if err != nil { - slog.Warn("Failed to generate a table, using toString", "err", err) - out = notifications.String() - } - - out += fmt.Sprintf("\nFound %d notifications", len(notifications)) - - fmt.Println(out) - - return nil -} diff --git a/internal/cmd/repl.go b/internal/cmd/repl.go deleted file mode 100644 index a84b966..0000000 --- a/internal/cmd/repl.go +++ /dev/null @@ -1,50 +0,0 @@ -package cmd - -import ( - "log/slog" - - tea "github.com/charmbracelet/bubbletea" - "github.com/nobe4/gh-not/internal/views/normal" - "github.com/spf13/cobra" -) - -var ( - replCmd = &cobra.Command{ - Use: "repl", - Aliases: []string{"r"}, - Short: "Launch a REPL with notifications", - RunE: runRepl, - } -) - -func init() { - rootCmd.AddCommand(replCmd) -} - -func runRepl(cmd *cobra.Command, args []string) error { - if err := manager.Load(); err != nil { - slog.Error("Failed to load the notifications", "err", err) - return err - } - - notifications := manager.Notifications.Visible() - - renderCache, err := notifications.Table() - if err != nil { - return err - } - - model := normal.New(manager.Actors, notifications, renderCache, config.Data.Keymap, config.Data.View) - - p := tea.NewProgram(model) - if _, err := p.Run(); err != nil { - return err - } - - if err := manager.Save(); err != nil { - slog.Error("Failed to save the notifications", "err", err) - return err - } - - return nil -} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 131a210..fb0602c 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -1,40 +1,44 @@ package cmd import ( + "fmt" "log/slog" - "github.com/nobe4/gh-not/internal/api" - "github.com/nobe4/gh-not/internal/api/file" + tea "github.com/charmbracelet/bubbletea" "github.com/nobe4/gh-not/internal/api/github" configPkg "github.com/nobe4/gh-not/internal/config" + "github.com/nobe4/gh-not/internal/jq" "github.com/nobe4/gh-not/internal/logger" managerPkg "github.com/nobe4/gh-not/internal/manager" + "github.com/nobe4/gh-not/internal/notifications" "github.com/nobe4/gh-not/internal/version" + "github.com/nobe4/gh-not/internal/views/normal" "github.com/spf13/cobra" ) var ( - verbosityFlag int - configPathFlag string - notificationDumpPath string - refreshFlag bool - noRefreshFlag bool + verbosityFlag int + configPathFlag string + filterFlag string + jqFlag string + repl bool config *configPkg.Config manager *managerPkg.Manager rootCmd = &cobra.Command{ Use: "gh-not", - Short: "Manage your GitHub notifications", + Short: "Lists your GitHub notifications", Version: version.String(), Example: ` - gh-not --config list - gh-not --no-refresh list - gh-not --from-file notifications.json list - gh-not sync --refresh --verbosity 4 + gh-not --verbosity 2 + gh-not --config /path/to/config.yaml + gh-not --filter '.repository.full_name | contains("nobe4")' + gh-not --repl `, PersistentPreRunE: setupGlobals, SilenceErrors: true, + RunE: runRoot, } ) @@ -48,11 +52,11 @@ func init() { rootCmd.PersistentFlags().IntVarP(&verbosityFlag, "verbosity", "v", 1, "Change logger verbosity") rootCmd.PersistentFlags().StringVarP(&configPathFlag, "config", "c", "", "Path to the YAML config file") - rootCmd.PersistentFlags().StringVarP(¬ificationDumpPath, "from-file", "", "", "Path to notification dump in JSON (generate with 'gh api /notifications')") + rootCmd.Flags().StringVarP(&filterFlag, "filter", "f", "", "Filter with a jq expression passed into a select(...) call") + rootCmd.Flags().StringVarP(&jqFlag, "jq", "q", "", "jq expression to run on the notification list") + rootCmd.MarkFlagsMutuallyExclusive("filter", "jq") - rootCmd.PersistentFlags().BoolVarP(&refreshFlag, "refresh", "r", false, "Force a refresh") - rootCmd.PersistentFlags().BoolVarP(&noRefreshFlag, "no-refresh", "R", false, "Prevent a refresh") - rootCmd.MarkFlagsMutuallyExclusive("refresh", "no-refresh") + rootCmd.Flags().BoolVarP(&repl, "repl", "", false, "Start a REPL with the notifications list") } func setupGlobals(cmd *cobra.Command, args []string) error { @@ -62,25 +66,79 @@ func setupGlobals(cmd *cobra.Command, args []string) error { } var err error - config, err = configPkg.New(configPathFlag) if err != nil { slog.Error("Failed to load the config", "path", configPathFlag, "err", err) return err } - var caller api.Caller - if notificationDumpPath != "" { - caller = file.New(notificationDumpPath) - } else { - caller, err = github.New() + manager = managerPkg.New(config.Data) + + return nil +} + +func runRoot(cmd *cobra.Command, args []string) error { + if err := manager.Load(); err != nil { + slog.Error("Failed to load the notifications", "err", err) + return err + } + + notifications := manager.Notifications.Visible() + + if filterFlag != "" { + notificationsList, err := jq.Filter(filterFlag, notifications) if err != nil { - slog.Error("Failed to create an API REST client", "err", err) return err } + notifications = notificationsList } - manager = managerPkg.New(config.Data, caller, refreshFlag, noRefreshFlag) + if jqFlag != "" { + return fmt.Errorf("`gh-not list --jq` implementation needed") + } + + table, err := notifications.Table() + if err != nil { + slog.Warn("Failed to generate a table, using toString", "err", err) + table = notifications.String() + } + + if repl { + return displayRepl(table, notifications) + } + + displayTable(table, notifications) + + return nil +} + +func displayTable(table string, notifications notifications.Notifications) { + out := table + out += fmt.Sprintf("\nFound %d notifications", len(notifications)) + // TODO: add a notice if the notifications could be refreshed + + fmt.Println(out) +} + +func displayRepl(renderCache string, n notifications.Notifications) error { + caller, err := github.New() + if err != nil { + slog.Error("Failed to create an API REST client", "err", err) + return err + } + manager.WithCaller(caller) + + model := normal.New(manager.Actors, n, renderCache, config.Data.Keymap, config.Data.View) + + p := tea.NewProgram(model) + if _, err := p.Run(); err != nil { + return err + } + + if err := manager.Save(); err != nil { + slog.Error("Failed to save the notifications", "err", err) + return err + } return nil } diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go index ca4f695..1fd06ab 100644 --- a/internal/cmd/sync.go +++ b/internal/cmd/sync.go @@ -1,13 +1,20 @@ package cmd import ( + "fmt" "log/slog" + "github.com/nobe4/gh-not/internal/api" + "github.com/nobe4/gh-not/internal/api/file" + "github.com/nobe4/gh-not/internal/api/github" + managerPkg "github.com/nobe4/gh-not/internal/manager" "github.com/spf13/cobra" ) var ( - noop bool + noop bool + notificationDumpPath string + refreshStrategy managerPkg.RefreshStrategy syncCmd = &cobra.Command{ Use: "sync", @@ -26,9 +33,25 @@ func init() { rootCmd.AddCommand(syncCmd) syncCmd.Flags().BoolVarP(&noop, "noop", "n", false, "Doesn't execute any action") + syncCmd.Flags().VarP(&refreshStrategy, "refresh-strategy", "r", fmt.Sprintf("Refresh strategy: %s", refreshStrategy.Allowed())) + syncCmd.Flags().StringVarP(¬ificationDumpPath, "from-file", "", "", "Path to notification dump in JSON (generate with 'gh api /notifications')") } func runSync(cmd *cobra.Command, args []string) error { + var caller api.Caller + var err error + + if notificationDumpPath != "" { + caller = file.New(notificationDumpPath) + } else { + caller, err = github.New() + if err != nil { + slog.Error("Failed to create an API REST client", "err", err) + return err + } + } + manager.WithRefresh(refreshStrategy).WithCaller(caller) + if err := manager.Load(); err != nil { slog.Error("Failed to load the notifications", "err", err) return err diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 681b3e9..108cc98 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -13,14 +13,6 @@ import ( "github.com/nobe4/gh-not/internal/notifications" ) -type RefreshStrategy int - -const ( - DefaultRefresh RefreshStrategy = iota - ForceRefresh - ForceNoRefresh -) - type Manager struct { Notifications notifications.Notifications cache cache.ExpiringReadWriter @@ -30,26 +22,26 @@ type Manager struct { refresh RefreshStrategy } -func New(config *config.Data, caller api.Caller, refresh, noRefresh bool) *Manager { +func New(config *config.Data) *Manager { m := &Manager{} m.config = config m.cache = cache.NewFileCache(m.config.Cache.TTLInHours, m.config.Cache.Path) + + return m +} + +func (m *Manager) WithCaller(caller api.Caller) *Manager { m.client = gh.NewClient(caller, m.cache, m.config.Endpoint) m.Actors = actors.Map(m.client) - m.setRefresh(refresh, noRefresh) - return m } -func (m *Manager) setRefresh(refresh, noRefresh bool) { - m.refresh = DefaultRefresh - if refresh { - m.refresh = ForceRefresh - } else if noRefresh { - m.refresh = ForceNoRefresh - } +func (m *Manager) WithRefresh(refresh RefreshStrategy) *Manager { + m.refresh = refresh + + return m } func (m *Manager) Load() error { @@ -75,7 +67,7 @@ func (m *Manager) shouldRefresh(expired bool) bool { return true } - if expired && m.refresh == ForceNoRefresh { + if expired && m.refresh == PreventRefresh { slog.Info("preventing a refresh") return false } @@ -85,6 +77,10 @@ func (m *Manager) shouldRefresh(expired bool) bool { } func (m *Manager) refreshNotifications() error { + if m.client == nil { + return fmt.Errorf("manager has no client, cannot refresh notifications") + } + fmt.Printf("Refreshing the cache...\n") remoteNotifications, err := m.client.Notifications() diff --git a/internal/manager/refreshStrategy.go b/internal/manager/refreshStrategy.go new file mode 100644 index 0000000..0b338dc --- /dev/null +++ b/internal/manager/refreshStrategy.go @@ -0,0 +1,47 @@ +package manager + +import "fmt" + +// RefreshStrategy is an enum for the refresh strategy. +// It implements https://pkg.go.dev/github.com/spf13/pflag#Value. +type RefreshStrategy int + +const ( + AutoRefresh RefreshStrategy = iota + ForceRefresh + PreventRefresh +) + +func (r RefreshStrategy) String() string { + switch r { + case AutoRefresh: + return "auto" + case ForceRefresh: + return "force" + case PreventRefresh: + return "prevent" + } + return "unknown" +} + +func (r *RefreshStrategy) Allowed() string { + return "auto, force, prevent" +} + +func (r *RefreshStrategy) Set(value string) error { + switch value { + case "auto": + *r = ForceRefresh + case "force": + *r = ForceRefresh + case "prevent": + *r = PreventRefresh + default: + return fmt.Errorf(`must be one of %s`, r.Allowed()) + } + return nil +} + +func (r RefreshStrategy) Type() string { + return "RefreshStrategy" +}