9e0a98ed07
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>
53 lines
1.7 KiB
Go
53 lines
1.7 KiB
Go
// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package keypress lets a streaming command stop on any keystroke. A
|
|
// watch-style command runs WaitForKey in a goroutine with a cancellable
|
|
// context; the first key pressed cancels the context, tearing down the stream.
|
|
//
|
|
// ctx, cancel := context.WithCancel(ctx)
|
|
// defer cancel()
|
|
// go keypress.WaitForKey(ctx, cancel)
|
|
// for { ev, err := stream.Recv(); ... } // returns when ctx is cancelled
|
|
//
|
|
// When standard input is not a terminal (piped, redirected, backgrounded) there
|
|
// is no keystroke to wait for, so WaitForKey just blocks until ctx ends and
|
|
// never cancels on its own.
|
|
package keypress
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
)
|
|
|
|
// WaitForKey blocks until a key is pressed on standard input or ctx is done,
|
|
// whichever comes first; on a keypress it calls cancel. If stdin is not a
|
|
// terminal it waits on ctx only (it neither reads input nor calls cancel). The
|
|
// terminal is placed in cbreak mode for the duration and restored on return.
|
|
//
|
|
// Run it in its own goroutine. If ctx ends first, WaitForKey returns and
|
|
// restores the terminal; a read already blocked on stdin is left to be reaped
|
|
// when the process exits (there is no portable way to interrupt it).
|
|
func WaitForKey(ctx context.Context, cancel context.CancelFunc) {
|
|
fd := int(os.Stdin.Fd())
|
|
old, err := cbreak(fd)
|
|
if err != nil {
|
|
<-ctx.Done() // stdin is not a tty: nothing to read, just honor ctx
|
|
return
|
|
}
|
|
defer func() { _ = restore(fd, old) }()
|
|
|
|
readDone := make(chan struct{})
|
|
go func() {
|
|
defer close(readDone)
|
|
buf := make([]byte, 1)
|
|
_, _ = os.Stdin.Read(buf)
|
|
}()
|
|
|
|
select {
|
|
case <-readDone:
|
|
cancel()
|
|
case <-ctx.Done():
|
|
}
|
|
}
|