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:
2026-06-05 22:11:13 +02:00
parent e030cd28e9
commit 9e0a98ed07
12 changed files with 530 additions and 260 deletions
+29
View File
@@ -113,6 +113,35 @@ Both are additive conveniences: every builder method returns a plain
`*cli.Node[C]`, so builder and struct-literal construction interoperate, and you
can still drive the lifecycle yourself with `Shell`/`Dispatch` instead of `App`.
`cli.Validate(root)` reports common authoring faults (more than one slot child
under a node, an empty word, duplicate sibling words, a dead-end node). It is
optional; the idiomatic use is a one-line unit test so a malformed tree fails the
build:
```go
func TestTreeValid(t *testing.T) {
if err := cli.Validate(buildTree()); err != nil { t.Fatal(err) }
}
```
## Streaming commands
For a `watch`-style command that streams until interrupted, the
[`keypress`](keypress) subpackage stops it on any keystroke:
```go
func runWatch(ctx context.Context, c Client, _ []string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go keypress.WaitForKey(ctx, cancel) // any key cancels ctx
stream, _ := c.Watch(ctx, req)
for { ev, err := stream.Recv(); /* returns when ctx is cancelled */ }
}
```
When stdin is not a terminal it simply waits on the context, so piped/one-shot
use never blocks on a keypress.
## Output: color and JSON
A command describes its result **once** and the framework renders it: