Files
sow-tools/internal/menu/menu.go
archvillainette f257672427
sync-wrappers / sync (push) Successful in 16s
test / test (push) Successful in 1m22s
build-binaries / build-binaries (push) Successful in 2m7s
build-image / publish (push) Successful in 38s
command and help ux pass (#21)
Reviewed-on: #21
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
2026-06-25 09:29:39 +00:00

78 lines
2.3 KiB
Go

// 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
}
}