// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt // 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) } }