From 3e45bfd0c78dfa8aadb8b73bfd4b19e87fb0c2d4 Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Tue, 23 Jun 2026 11:49:31 +0530 Subject: [PATCH] feat(cli): architecture picker + post-fetch O/C/Enter menu When multiple download links are returned (e.g. x32+x64 for consumer ISOs), an architecture picker is shown before printing the URL. After the URL is printed, an interactive menu lets the user open the link in the default browser (O), copy it to the clipboard (C), or exit (Enter). Menu is suppressed when stdout is not a terminal (piping). Co-Authored-By: Claude Sonnet 4.6 --- cli/main.go | 43 ++++++++++++++++++++++++---- cli/picker.go | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/cli/main.go b/cli/main.go index 5dc447f..42867e0 100644 --- a/cli/main.go +++ b/cli/main.go @@ -308,18 +308,42 @@ func runConsumer(productID, query, langName string, noContribute bool) error { } fmt.Fprintln(os.Stderr, "") - for _, link := range links { + + var selectedURI string + if isTerminal() && len(links) > 1 { + link, err := pickArchitecture(links) + if err != nil { + return err + } name := urlFilename(link.URI) - fmt.Fprintf(os.Stderr, " %s\n", name) - fmt.Fprintf(os.Stderr, " %s\n", strings.Repeat("─", len(name))) - fmt.Println(link.URI) // stdout — stays clean for piping - fmt.Fprintln(os.Stderr, "") + fmt.Fprintf(os.Stderr, "\n %s\n", name) + fmt.Fprintf(os.Stderr, " %s\n\n", strings.Repeat("─", len(name))) + fmt.Println(link.URI) + selectedURI = link.URI + } else { + for _, link := range links { + name := urlFilename(link.URI) + fmt.Fprintf(os.Stderr, " %s\n", name) + fmt.Fprintf(os.Stderr, " %s\n", strings.Repeat("─", len(name))) + fmt.Println(link.URI) // stdout — stays clean for piping + fmt.Fprintln(os.Stderr, "") + } + if len(links) == 1 { + selectedURI = links[0].URI + } } if !noContribute { fmt.Fprintln(os.Stderr, " ✓ Shared with MSDL Web App (https://msdl.tech-latest.com/)") fmt.Fprintln(os.Stderr, " Opt out: --no-contribute") fmt.Fprintln(os.Stderr, "") + } + + if isTerminal() && selectedURI != "" { + postFetchMenu(selectedURI) + } + + if !noContribute { // Wait up to 5s for the contribution to complete before process exit. done := make(chan struct{}) go func() { wg.Wait(); close(done) }() @@ -359,6 +383,9 @@ func runEval(slug string) error { if len(links) == 1 { fmt.Println(links[0].URL) + if isTerminal() { + postFetchMenu(links[0].URL) + } return nil } @@ -377,6 +404,10 @@ func runEval(slug string) error { if err != nil { return err } - fmt.Println(links[n-1].URL) + selected := links[n-1].URL + fmt.Println(selected) + if isTerminal() { + postFetchMenu(selected) + } return nil } diff --git a/cli/picker.go b/cli/picker.go index baeb830..c1cd10b 100644 --- a/cli/picker.go +++ b/cli/picker.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "os" + "os/exec" + "runtime" "strconv" "strings" ) @@ -76,3 +78,79 @@ func pickEvalProduct(products []EvalProduct) (EvalProduct, error) { } return products[n-1], nil } + +func isTerminal() bool { + fi, err := os.Stdin.Stat() + if err != nil { + return false + } + return fi.Mode()&os.ModeCharDevice != 0 +} + +func pickArchitecture(links []DownloadLink) (DownloadLink, error) { + fmt.Fprintln(os.Stderr, "Select architecture:") + for i, l := range links { + label := l.Architecture + if label == "" { + label = fmt.Sprintf("link %d", i+1) + } + fmt.Fprintf(os.Stderr, " %2d. %s\n", i+1, label) + } + fmt.Fprint(os.Stderr, "\nChoice: ") + n, err := parseChoice(os.Stdin, len(links)) + if err != nil { + return DownloadLink{}, err + } + return links[n-1], nil +} + +func openInBrowser(rawURL string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("cmd", "/c", "start", "", rawURL) + case "darwin": + cmd = exec.Command("open", rawURL) + default: + cmd = exec.Command("xdg-open", rawURL) + } + return cmd.Run() +} + +func copyToClipboard(text string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("cmd", "/c", "clip") + case "darwin": + cmd = exec.Command("pbcopy") + default: + if _, err := exec.LookPath("xclip"); err == nil { + cmd = exec.Command("xclip", "-selection", "clipboard") + } else { + cmd = exec.Command("xsel", "--clipboard", "--input") + } + } + cmd.Stdin = strings.NewReader(text) + return cmd.Run() +} + +func postFetchMenu(uri string) { + fmt.Fprint(os.Stderr, "\n [O] Open in browser [C] Copy to clipboard [Enter] Exit\n > ") + scanner := bufio.NewScanner(os.Stdin) + if !scanner.Scan() { + return + } + switch strings.ToLower(strings.TrimSpace(scanner.Text())) { + case "o": + if err := openInBrowser(uri); err != nil { + fmt.Fprintln(os.Stderr, " Could not open browser:", err) + } + case "c": + if err := copyToClipboard(uri); err != nil { + fmt.Fprintln(os.Stderr, " Could not copy to clipboard:", err) + } else { + fmt.Fprintln(os.Stderr, " ✓ Copied to clipboard") + } + } +}