114 lines
3.0 KiB
Go
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:
|
|
}
|
|
}
|