windows-iso-downloader/cli/picker.go
Shekhar Vaidya a9c713cd6a feat(cli): combined product picker with eval products + clipboard fix
- Interactive `msdl` (no flags) now lists consumer + eval products in one
  picker with an Evaluation / Enterprise separator; selecting a server
  product routes to the eval flow automatically
- copyToClipboard calls clip.exe by full SystemRoot path, bypassing
  cmd.exe PATH lookup that failed in some terminal environments
- CliHandoff card: winget primary install + msdl (no .exe) in run command

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:25:03 +05:30

198 lines
5.2 KiB
Go

package main
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
)
// parseChoice reads a 1-based integer from r, validated in [1, max].
func parseChoice(r io.Reader, max int) (int, error) {
scanner := bufio.NewScanner(r)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return 0, fmt.Errorf("reading input: %w", err)
}
return 0, fmt.Errorf("no input")
}
text := strings.TrimSpace(scanner.Text())
if text == "" {
return 0, fmt.Errorf("enter a number between 1 and %d", max)
}
n, err := strconv.Atoi(text)
if err != nil || n < 1 || n > max {
return 0, fmt.Errorf("enter a number between 1 and %d", max)
}
return n, nil
}
func pickProduct(products []Product) (Product, error) {
if len(products) == 0 {
return Product{}, fmt.Errorf("no products available")
}
fmt.Fprintln(os.Stderr, "\nSelect a product:")
for i, p := range products {
fmt.Fprintf(os.Stderr, " %2d. %s\n", i+1, p.Name)
}
fmt.Fprint(os.Stderr, "\nChoice: ")
n, err := parseChoice(os.Stdin, len(products))
if err != nil {
return Product{}, err
}
return products[n-1], nil
}
func pickLanguage(langs []Language) (Language, error) {
if len(langs) == 0 {
return Language{}, fmt.Errorf("no languages available")
}
fmt.Fprintln(os.Stderr, "\nSelect a language:")
for i, l := range langs {
fmt.Fprintf(os.Stderr, " %2d. %s\n", i+1, l.Language)
}
fmt.Fprint(os.Stderr, "\nChoice: ")
n, err := parseChoice(os.Stdin, len(langs))
if err != nil {
return Language{}, err
}
return langs[n-1], nil
}
func pickEvalProduct(products []EvalProduct) (EvalProduct, error) {
if len(products) == 0 {
return EvalProduct{}, fmt.Errorf("no eval products available")
}
fmt.Fprintln(os.Stderr, "\nSelect an evaluation product:")
for i, p := range products {
fmt.Fprintf(os.Stderr, " %2d. %s\n", i+1, p.Name)
}
fmt.Fprint(os.Stderr, "\nChoice: ")
n, err := parseChoice(os.Stdin, len(products))
if err != nil {
return EvalProduct{}, err
}
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 pickCombined() (isEval bool, p Product, ep EvalProduct, err error) {
total := len(consumerProducts) + len(evalProducts)
fmt.Fprintln(os.Stderr, "\nSelect a product:")
for i, prod := range consumerProducts {
fmt.Fprintf(os.Stderr, " %2d. %s\n", i+1, prod.Name)
}
fmt.Fprintln(os.Stderr, "\n ── Evaluation / Enterprise ──")
offset := len(consumerProducts)
for i, e := range evalProducts {
fmt.Fprintf(os.Stderr, " %2d. %s\n", offset+i+1, e.Name)
}
fmt.Fprint(os.Stderr, "\nChoice: ")
n, err := parseChoice(os.Stdin, total)
if err != nil {
return false, Product{}, EvalProduct{}, err
}
if n <= offset {
return false, consumerProducts[n-1], EvalProduct{}, nil
}
return true, Product{}, evalProducts[n-offset-1], nil
}
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 {
switch runtime.GOOS {
case "windows":
// Write URL to a temp .url file (Windows Internet Shortcut format).
// The URL never touches cmd.exe — only the temp filename does, which
// contains no special characters.
f, err := os.CreateTemp("", "msdl-*.url")
if err != nil {
return err
}
name := f.Name()
defer os.Remove(name)
if _, err := fmt.Fprintf(f, "[InternetShortcut]\r\nURL=%s\r\n", rawURL); err != nil {
f.Close()
return err
}
f.Close()
cmdExe := os.Getenv("COMSPEC")
if cmdExe == "" {
cmdExe = `C:\Windows\System32\cmd.exe`
}
return exec.Command(cmdExe, "/c", "start", "", name).Run()
case "darwin":
return exec.Command("open", rawURL).Run()
default:
return exec.Command("xdg-open", rawURL).Run()
}
}
func copyToClipboard(text string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
clipExe := `C:\Windows\System32\clip.exe`
if sr := os.Getenv("SystemRoot"); sr != "" {
clipExe = sr + `\System32\clip.exe`
}
cmd = exec.Command(clipExe)
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")
}
}
}