feat(dispatch): launch interactive menu on no-arg TTY invocation
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/app"
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/app"
|
||||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo"
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo"
|
||||||
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/menu"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Exit codes follow the sysexits(3) convention so CI can distinguish
|
// Exit codes follow the sysexits(3) convention so CI can distinguish
|
||||||
@@ -103,8 +104,52 @@ func find(name string) (Builder, bool) {
|
|||||||
return Builder{}, false
|
return Builder{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main is the `crucible` dispatcher entrypoint.
|
// Main is the `crucible` dispatcher entrypoint. With no arguments on an
|
||||||
func Main(args []string) int { return run(args, os.Stdout, os.Stderr) }
|
// interactive terminal it launches the menu; otherwise (pipes, CI) it prints
|
||||||
|
// usage, preserving scriptable behavior.
|
||||||
|
func Main(args []string) int {
|
||||||
|
if len(args) == 0 && interactive(os.Stdout) {
|
||||||
|
return runMenu(os.Stdout, os.Stderr, os.Stdin)
|
||||||
|
}
|
||||||
|
return run(args, os.Stdout, os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func interactive(f *os.File) bool {
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fi.Mode()&os.ModeCharDevice != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func menuItems() []menu.Item {
|
||||||
|
var items []menu.Item
|
||||||
|
for _, b := range Registry {
|
||||||
|
if !b.Wired {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, sub := range b.subcommands() {
|
||||||
|
items = append(items, menu.Item{
|
||||||
|
Label: b.Name + " " + sub,
|
||||||
|
Desc: b.Summary,
|
||||||
|
Args: []string{b.Name, sub},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMenu(out, errw io.Writer, in io.Reader) int {
|
||||||
|
chosen, err := menu.Select(out, in, menuItems())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(errw, "crucible:", err)
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
if chosen == nil {
|
||||||
|
return exitOK
|
||||||
|
}
|
||||||
|
return run(chosen, out, errw)
|
||||||
|
}
|
||||||
|
|
||||||
// RunBuilder is the standalone `crucible-<name>` entrypoint.
|
// RunBuilder is the standalone `crucible-<name>` entrypoint.
|
||||||
func RunBuilder(name string, args []string) int {
|
func RunBuilder(name string, args []string) int {
|
||||||
|
|||||||
@@ -135,3 +135,22 @@ func TestLegacyCommandsHaveExactlyOneHome(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMenuItemsSkipsUnwiredIncludesWired(t *testing.T) {
|
||||||
|
items := menuItems()
|
||||||
|
if len(items) == 0 {
|
||||||
|
t.Fatal("expected menu items")
|
||||||
|
}
|
||||||
|
sawModuleBuild := false
|
||||||
|
for _, it := range items {
|
||||||
|
if it.Args[0] == "depot" {
|
||||||
|
t.Errorf("unwired builder %q must not appear in the menu", it.Args[0])
|
||||||
|
}
|
||||||
|
if len(it.Args) == 2 && it.Args[0] == "module" && it.Args[1] == "build" {
|
||||||
|
sawModuleBuild = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sawModuleBuild {
|
||||||
|
t.Error("expected 'module build' in the menu")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user