feat: Validate + keypress subpackage; RFC-style design.md (v1.2.0)
Validate(root): optional startup/test check for tree authoring faults — >1 slot child per node, empty word, duplicate sibling words, dead-end node — traversing circular slots without looping (#3). keypress subpackage: WaitForKey(ctx, cancel) cancels a context on any keystroke for watch-style streaming commands, with per-GOOS cbreak (linux TCGETS/TCSETS, BSD TIOCGETA/TIOCSETA) and a non-tty/unsupported fallback that just waits on ctx. Lifts the last OpenBSD-specific bit out of evpnc/maglevc's watch.go (#6). docs: replace PROPOSAL.md with an RFC-2119 design.md (FR/NFR for the library). Example now dogfoods Validate (a unit test) and keypress (a bounded `watch` command). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import (
|
||||
"strings"
|
||||
|
||||
cli "git.ipng.ch/ipng/golang-cli"
|
||||
"git.ipng.ch/ipng/golang-cli/keypress"
|
||||
)
|
||||
|
||||
// inventory is the "client" C that the tree is generic over. In a real app this
|
||||
@@ -154,6 +155,28 @@ func runColors(context.Context, inventory, []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// runWatch streams a few simulated events, stopping early if a key is pressed.
|
||||
// In a real CLI this would be a server-side stream; here it is a bounded loop so
|
||||
// the demo never hangs when stdin is not a terminal. Press any key (in a TTY) to
|
||||
// stop it before it finishes.
|
||||
func runWatch(ctx context.Context, _ inventory, _ []string) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
go keypress.WaitForKey(ctx, cancel)
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil // key pressed
|
||||
default:
|
||||
}
|
||||
if err := cli.Emit(cli.KV("event", fmt.Sprintf("%d", i)), map[string]any{"event": i}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runQuit(context.Context, inventory, []string) error { return cli.ErrQuit }
|
||||
|
||||
// buildTree is the single source of truth for the command set, built with the
|
||||
@@ -168,6 +191,7 @@ func buildTree() *cli.Node[inventory] {
|
||||
b.Slot("<svc>", "show one service", dynServices, runShowServerService))))),
|
||||
b.Dir("ping", "ping a server",
|
||||
b.Slot("<name>", "ping this server", dynServers, runPing)),
|
||||
b.Cmd("watch", "stream a few events (any key stops it)", runWatch),
|
||||
b.Cmd("colors", "show the ANSI color palette", runColors),
|
||||
b.Cmd("quit", "exit the shell", runQuit),
|
||||
b.Cmd("exit", "exit the shell", runQuit),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
cli "git.ipng.ch/ipng/golang-cli"
|
||||
)
|
||||
|
||||
// TestTreeValid keeps the example's command tree free of the authoring faults
|
||||
// Validate checks for. It doubles as the recommended way to use Validate: from
|
||||
// a unit test, so a malformed tree fails the build rather than misdispatching
|
||||
// at runtime.
|
||||
func TestTreeValid(t *testing.T) {
|
||||
if err := cli.Validate(buildTree()); err != nil {
|
||||
t.Fatalf("buildTree() has authoring faults:\n%v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user