Files
golang-cli/term_openbsd.go
pim d63ffd6a3a 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>
2026-06-05 21:48:48 +02:00

69 lines
2.2 KiB
Go

// 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)
}
}