// Package menu renders Crucible's interactive launcher: a numbered list of // commands for users who don't want to memorize subcommands. It holds no // builder logic — it returns the chosen dispatcher argument slice for the // caller to run. This is the seed of a future GUI front-end over the same core. package menu import ( "bufio" "fmt" "io" "strconv" "strings" ) // Item is one selectable command. type Item struct { Label string // e.g. "module build" Desc string // e.g. "build the configured module" Args []string // dispatcher args, e.g. {"module", "build"} Usage string Options []string } // ANSI styling; terminals that don't support it simply ignore the codes. const ( cReset = "\033[0m" cBold = "\033[1m" cCyan = "\033[36m" cDim = "\033[2m" ) // Select prints the numbered menu to out, reads a selection plus optional extra // arguments from in, and returns the full dispatcher argument slice. It returns // (nil, nil) when the user quits or enters an empty choice. func Select(out io.Writer, in io.Reader, items []Item) ([]string, error) { if len(items) == 0 { return nil, fmt.Errorf("no commands available") } fmt.Fprintf(out, "%s%sCrucible%s — pick a command:\n\n", cBold, cCyan, cReset) labelWidth := 0 for _, item := range items { if len(item.Label) > labelWidth { labelWidth = len(item.Label) } } for i, it := range items { fmt.Fprintf(out, " %s%2d%s %s%-*s%s %s%s%s\n", cCyan, i+1, cReset, cBold, labelWidth, it.Label, cReset, cDim, it.Desc, cReset) } fmt.Fprintf(out, " %sq%s quit\n\nselection: ", cCyan, cReset) r := bufio.NewReader(in) line, _ := r.ReadString('\n') switch choice := strings.TrimSpace(line); choice { case "", "q", "Q": return nil, nil default: n, err := strconv.Atoi(choice) if err != nil || n < 1 || n > len(items) { return nil, fmt.Errorf("invalid selection %q", choice) } it := items[n-1] if it.Usage != "" { fmt.Fprintf(out, "\nusage: %s\n", it.Usage) } if len(it.Options) > 0 { fmt.Fprintln(out, "options:") for _, option := range it.Options { fmt.Fprintf(out, " %s\n", option) } } fmt.Fprint(out, "\narguments (press Enter to use defaults): ") argLine, _ := r.ReadString('\n') extra := strings.Fields(strings.TrimSpace(argLine)) return append(append([]string{}, it.Args...), extra...), nil } }