refactor(maglevc): build the CLI on the golang-cli library
Replace maglevc's hand-rolled command-tree CLI with git.ipng.ch/ipng/golang-cli v1.3.0, mirroring the evpnc refactor. The tree (commands.go) and the gRPC-status error unwrap (color.go) stay app-specific; the generic parts — parse tree, completion, '?'-help, the readline shell, one-shot dispatch, color helpers, and the watch keypress handler — now come from the library. - main.go: a single cli.App[grpcapi.MaglevClient] with a Connect callback; drops the flag/color-default/dispatch boilerplate. - commands.go: `type node = cli.Node[grpcapi.MaglevClient]`; label() -> cli.Label(); dyn* gain the captured-args parameter the library's Dynamic signature carries; runQuit returns cli.ErrQuit. - watch.go: keypress.WaitForKey replaces the inline cbreak helper. - color.go: only formatError remains, reading cli.ColorEnabled(). - delete tree.go, complete.go, shell.go. - tests use the library API; add TestTreeValid (cli.Validate). Behavior is unchanged except labels/errors now use the library's bright ANSI palette (was dark); escape lengths are identical so tabwriter alignment is unaffected. maglevc additionally gains the OpenBSD readline fix and BSD-correct watch keypress it previously lacked. Builds on linux and openbsd. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-48
@@ -10,15 +10,15 @@ import (
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"git.ipng.ch/ipng/golang-cli/keypress"
|
||||
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
||||
)
|
||||
|
||||
func dynWatchEventOpts(_ context.Context, _ grpcapi.MaglevClient) []string {
|
||||
func dynWatchEventOpts(_ context.Context, _ grpcapi.MaglevClient, _ []string) []string {
|
||||
return []string{"num", "log", "backend", "frontend"}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func runWatchEvents(ctx context.Context, client grpcapi.MaglevClient, args []str
|
||||
watchCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
go watchStopOnKeypress(watchCtx, cancel)
|
||||
go keypress.WaitForKey(watchCtx, cancel)
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
@@ -127,48 +127,5 @@ func runWatchEvents(ctx context.Context, client grpcapi.MaglevClient, args []str
|
||||
}
|
||||
}
|
||||
|
||||
// watchStopOnKeypress puts stdin into cbreak mode (when it is a terminal) and
|
||||
// calls cancel when any byte arrives. Cbreak mode disables canonical (line)
|
||||
// input so a single keypress is sufficient, while preserving output
|
||||
// post-processing (OPOST/ONLCR) so that fmt.Printf("\n") still produces the
|
||||
// expected carriage-return+newline on screen. Falls back gracefully when stdin
|
||||
// is not a tty. The goroutine exits when ctx is cancelled.
|
||||
func watchStopOnKeypress(ctx context.Context, cancel context.CancelFunc) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
if old, err := stdinCbreak(fd); err == nil {
|
||||
defer unix.IoctlSetTermios(fd, unix.TCSETSF, old) //nolint:errcheck
|
||||
}
|
||||
|
||||
readDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(readDone)
|
||||
buf := make([]byte, 1)
|
||||
os.Stdin.Read(buf) //nolint:errcheck
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-readDone:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
// stdinCbreak sets the terminal referred to by fd into cbreak mode: canonical
|
||||
// input and echo are disabled (so single keystrokes are immediately available)
|
||||
// but output post-processing is left untouched (so \n still maps to \r\n).
|
||||
// Returns the previous termios so the caller can restore it, or an error if fd
|
||||
// is not a terminal.
|
||||
func stdinCbreak(fd int) (*unix.Termios, error) {
|
||||
old, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||
if err != nil {
|
||||
return nil, err // not a terminal
|
||||
}
|
||||
t := *old
|
||||
t.Lflag &^= unix.ICANON | unix.ECHO | unix.ECHOE | unix.ECHOK | unix.ECHONL
|
||||
t.Cc[unix.VMIN] = 1
|
||||
t.Cc[unix.VTIME] = 0
|
||||
if err := unix.IoctlSetTermios(fd, unix.TCSETS, &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return old, nil
|
||||
}
|
||||
// Stopping the watch on a keypress now uses keypress.WaitForKey from the
|
||||
// golang-cli library (per-platform cbreak handling, with a non-tty fallback).
|
||||
|
||||
Reference in New Issue
Block a user