command and help ux pass
This commit is contained in:
+23
-6
@@ -14,9 +14,11 @@ import (
|
||||
|
||||
// Item is one selectable command.
|
||||
type Item struct {
|
||||
Label string // e.g. "module build"
|
||||
Desc string // e.g. "build/extract/validate/compare the .mod"
|
||||
Args []string // dispatcher args, e.g. {"module", "build"}
|
||||
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.
|
||||
@@ -35,9 +37,15 @@ func Select(out io.Writer, in io.Reader, items []Item) ([]string, error) {
|
||||
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%-24s%s %s%s%s\n",
|
||||
cCyan, i+1, cReset, cBold, it.Label, cReset, cDim, it.Desc, cReset)
|
||||
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)
|
||||
|
||||
@@ -52,7 +60,16 @@ func Select(out io.Writer, in io.Reader, items []Item) ([]string, error) {
|
||||
return nil, fmt.Errorf("invalid selection %q", choice)
|
||||
}
|
||||
it := items[n-1]
|
||||
fmt.Fprintf(out, "extra args for %q (optional): ", it.Label)
|
||||
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
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -45,3 +47,58 @@ func TestSelectInvalidErrors(t *testing.T) {
|
||||
t.Fatal("out-of-range selection should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectAlignsDescriptionsAfterLongestLabel(t *testing.T) {
|
||||
items := []Item{
|
||||
{Label: "hak build", Desc: "build haks", Args: []string{"hak", "build"}},
|
||||
{Label: "topdata exceptionally-long-command", Desc: "long command", Args: []string{"topdata", "long"}},
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if _, err := Select(&out, strings.NewReader("q\n"), items); err != nil {
|
||||
t.Fatalf("select: %v", err)
|
||||
}
|
||||
text := stripANSI(out.String())
|
||||
var positions []int
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
for _, desc := range []string{"build haks", "long command"} {
|
||||
if index := strings.Index(line, desc); index >= 0 {
|
||||
positions = append(positions, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(positions) != 2 {
|
||||
t.Fatalf("expected two menu descriptions, got %d:\n%s", len(positions), text)
|
||||
}
|
||||
if positions[0] != positions[1] {
|
||||
t.Fatalf("descriptions are not aligned: columns %v\n%s", positions, text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectShowsCommandGuidanceBeforeArgumentsPrompt(t *testing.T) {
|
||||
items := []Item{{
|
||||
Label: "topdata build",
|
||||
Desc: "compile topdata",
|
||||
Args: []string{"topdata", "build"},
|
||||
Usage: "crucible topdata build [--force]",
|
||||
Options: []string{"--force rebuild outputs"},
|
||||
}}
|
||||
var out bytes.Buffer
|
||||
if _, err := Select(&out, strings.NewReader("1\n\n"), items); err != nil {
|
||||
t.Fatalf("select: %v", err)
|
||||
}
|
||||
text := stripANSI(out.String())
|
||||
for _, want := range []string{
|
||||
"usage: crucible topdata build [--force]",
|
||||
"options:",
|
||||
"--force rebuild outputs",
|
||||
"arguments (press Enter to use defaults):",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Errorf("selected command guidance missing %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stripANSI(value string) string {
|
||||
return regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(value, "")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user