feat: golang-cli v1.0.0 — generic command-tree CLI library
Reusable, generics-based CLI extracted from vpp-evpn's cmd/evpnc: a declarative command tree (Node[C]) from which dispatch, '?'-help and TAB-completion are derived, an interactive Shell[C], dynamic slot resolvers (context-dependent via captured args), text-or-JSON output (Emit), and color helpers (Paint/Label/KV). Builds on Linux and OpenBSD (readline termios override). Includes a self-contained example and a design proposal under docs/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build openbsd
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// applyTermFuncs makes readline drive the terminal through golang.org/x/sys/unix.
|
||||
//
|
||||
// chzyer/readline's own termios handling (term_bsd.go) issues raw
|
||||
// syscall.Syscall6(SYS_IOCTL, ...) calls. OpenBSD forbids direct syscalls from
|
||||
// outside libc, so those return ENOSYS: readline's IsTerminal() then reports
|
||||
// false and the REPL silently degrades to a dumb line reader -- no prompt, no
|
||||
// tab completion. x/sys/unix performs the same ioctls through libc.
|
||||
//
|
||||
// The raw-mode flag set below is copied verbatim from readline's own MakeRaw
|
||||
// (term.go), and that match matters: it deliberately leaves OPOST enabled, so a
|
||||
// command's "\n" output is still translated to "\r\n". A full cfmakeraw (e.g.
|
||||
// golang.org/x/term.MakeRaw) clears OPOST and staircases multi-line output.
|
||||
// Only OpenBSD needs this; every other platform keeps readline's native path.
|
||||
func applyTermFuncs(cfg *readline.Config) {
|
||||
stdin := int(os.Stdin.Fd())
|
||||
stdout := int(os.Stdout.Fd())
|
||||
var saved *unix.Termios
|
||||
|
||||
cfg.FuncIsTerminal = func() bool {
|
||||
_, err := unix.IoctlGetTermios(stdin, unix.TIOCGETA)
|
||||
return err == nil
|
||||
}
|
||||
cfg.FuncMakeRaw = func() error {
|
||||
old, err := unix.IoctlGetTermios(stdin, unix.TIOCGETA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
saved = old
|
||||
raw := *old
|
||||
raw.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
|
||||
// OPOST is intentionally left set (see the doc comment above).
|
||||
raw.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
|
||||
raw.Cflag &^= unix.CSIZE | unix.PARENB
|
||||
raw.Cflag |= unix.CS8
|
||||
raw.Cc[unix.VMIN] = 1
|
||||
raw.Cc[unix.VTIME] = 0
|
||||
return unix.IoctlSetTermios(stdin, unix.TIOCSETA, &raw)
|
||||
}
|
||||
cfg.FuncExitRaw = func() error {
|
||||
if saved == nil {
|
||||
return nil
|
||||
}
|
||||
err := unix.IoctlSetTermios(stdin, unix.TIOCSETA, saved)
|
||||
saved = nil
|
||||
return err
|
||||
}
|
||||
cfg.FuncGetWidth = func() int {
|
||||
ws, err := unix.IoctlGetWinsize(stdout, unix.TIOCGWINSZ)
|
||||
if err != nil || ws.Col == 0 {
|
||||
return 80 // sane default when the width can't be read
|
||||
}
|
||||
return int(ws.Col)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user