Files
godeez/cmd/root.go
T

114 lines
3.0 KiB
Go

// Package cmd defines the godeez command line: the root command and its
// download, login, logout, update and version subcommands. It is a thin layer
// that parses flags and delegates to the internal packages.
package cmd
import (
"context"
"fmt"
"os"
"slices"
"github.com/mathismqn/godeez/internal/buildinfo"
"github.com/mathismqn/godeez/internal/update"
"github.com/spf13/cobra"
"golang.org/x/term"
)
// updateNoticeAnnotation marks the commands that may print an update notice.
// It is an annotation rather than a field because it is inherited: marking
// the download command opts in all of its subcommands.
const updateNoticeAnnotation = "godeez:update-notice"
// Execute runs the CLI.
//
// The update check is started before the command and collected after it, so
// the network round trip overlaps with work the user actually asked for
// instead of adding to the startup time.
func Execute(ctx context.Context) error {
root := newRootCmd()
var notice <-chan string
if wantsUpdateNotice(root) {
notice = update.StartCheck(ctx)
}
err := root.ExecuteContext(ctx)
printUpdateNotice(notice)
return err
}
func newRootCmd() *cobra.Command {
root := &cobra.Command{
Use: "godeez",
Short: "GoDeez is a tool to download music from Deezer",
SilenceUsage: true,
}
root.AddCommand(
newDownloadCmd(),
newLoginCmd(),
newLogoutCmd(),
newUpdateCmd(),
newVersionCmd(),
)
return root
}
// wantsUpdateNotice decides whether this invocation should check for updates.
//
// The aim is to nag only during real interactive use. Notices are suppressed
// when stderr is not a terminal, so they cannot corrupt piped or scripted
// output; on help output, where they are noise; on commands that only print
// their usage; and on anything not explicitly opted in via the annotation,
// which notably keeps `godeez version` and `godeez update` quiet.
func wantsUpdateNotice(root *cobra.Command) bool {
if !term.IsTerminal(int(os.Stderr.Fd())) {
return false
}
args := os.Args[1:]
if slices.Contains(args, "-h") || slices.Contains(args, "--help") {
return false
}
target, _, err := root.Find(args)
if err != nil || target == nil {
return false
}
if target.Run == nil && target.RunE == nil {
return false
}
for cmd := target; cmd != nil; cmd = cmd.Parent() {
if cmd.Annotations[updateNoticeAnnotation] == "true" {
return true
}
}
return false
}
// printUpdateNotice prints the notice only if the check has already finished.
//
// The non-blocking receive is the point: the command is done and the user
// should get their prompt back, so a check that is still in flight is
// dropped rather than waited on. A nil channel, meaning no check was started,
// takes the same path.
func printUpdateNotice(notice <-chan string) {
select {
case latest := <-notice:
if latest == "" {
return
}
fmt.Fprintf(os.Stderr, "\n ┌ Update available: %s → %s\n └ Run `godeez update` to install\n",
buildinfo.Version(), latest)
default:
}
}