1//go:build linux || darwin
2
3// Package rawterm provides the small set of terminal-control primitives vev
4// needs directly on top of the frozen stdlib syscall package. The common raw
5// mode and terminal-size operations use platform-specific termios requests.
6package rawterm
7
8import (
9 "fmt"
10 "syscall"
11 "unsafe"
12)
13
14// State is an opaque snapshot of a terminal's termios settings, captured by
15// MakeRaw and consumed by Restore.
16type State struct {
17 termios syscall.Termios
18}
19
20// Winsize mirrors the kernel's struct winsize for TIOCGWINSZ/TIOCSWINSZ.
21// Zero pixel dimensions mean the controlling terminal did not report them.
22type Winsize struct {
23 Row uint16
24 Col uint16
25 Xpixel uint16
26 Ypixel uint16
27}
28
29// ioctl issues a SYS_IOCTL syscall against fd with the given request and
30// argument pointer, returning a Go error on failure. The pointer-to-uintptr
31// conversion must happen inside the Syscall argument list (unsafe.Pointer
32// rule 4), so arg stays a live pointer until the call.
33func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
34 _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
35 if errno != 0 {
36 return errno
37 }
38 return nil
39}
40
41// IsTerminal reports whether fd refers to a terminal device.
42func IsTerminal(fd int) bool {
43 var t syscall.Termios
44 return ioctl(fd, reqGetTermios, unsafe.Pointer(&t)) == nil
45}
46
47// MakeRaw puts the terminal referenced by fd into raw mode and returns its
48// pre-modification state, so a caller can later restore it. The flag
49// changes applied match golang.org/x/term's MakeRaw semantics exactly.
50func MakeRaw(fd int) (*State, error) {
51 var oldState State
52 if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&oldState.termios)); err != nil {
53 return nil, fmt.Errorf("rawterm: get termios: %w", err)
54 }
55
56 raw := oldState.termios
57 raw.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
58 raw.Oflag &^= syscall.OPOST
59 raw.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
60 raw.Cflag &^= syscall.CSIZE | syscall.PARENB
61 raw.Cflag |= syscall.CS8
62 raw.Cc[syscall.VMIN] = 1
63 raw.Cc[syscall.VTIME] = 0
64
65 if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&raw)); err != nil {
66 return nil, fmt.Errorf("rawterm: set termios: %w", err)
67 }
68
69 return &oldState, nil
70}
71
72// Restore applies a previously captured State back to the terminal
73// referenced by fd.
74func Restore(fd int, s *State) error {
75 if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&s.termios)); err != nil {
76 return fmt.Errorf("rawterm: set termios: %w", err)
77 }
78 return nil
79}
80
81// GetSize returns the terminal's current column and row count.
82func GetSize(fd int) (cols, rows int, err error) {
83 ws, err := GetWinsize(fd)
84 if err != nil {
85 return 0, 0, err
86 }
87 return int(ws.Col), int(ws.Row), nil
88}
89
90// GetWinsize returns the terminal's cell and optional pixel dimensions.
91func GetWinsize(fd int) (Winsize, error) {
92 var ws Winsize
93 if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil {
94 return Winsize{}, fmt.Errorf("rawterm: get winsize: %w", err)
95 }
96 return ws, nil
97}
98
99// SetWinsize sets the terminal's column and row count.
100func SetWinsize(fd int, cols, rows uint16) error {
101 return SetWinsizeFull(fd, Winsize{Row: rows, Col: cols})
102}
103
104// SetWinsizeFull sets the terminal's cell and optional pixel dimensions.
105func SetWinsizeFull(fd int, ws Winsize) error {
106 if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil {
107 return fmt.Errorf("rawterm: set winsize: %w", err)
108 }
109 return nil
110}
111
112// ForegroundProcessGroup returns the process group id currently in the
113// foreground for the terminal referenced by fd (TIOCGPGRP).
114func ForegroundProcessGroup(fd int) (int, error) {
115 var pgid int32
116 if err := ioctl(fd, syscall.TIOCGPGRP, unsafe.Pointer(&pgid)); err != nil {
117 return 0, fmt.Errorf("rawterm: get foreground process group: %w", err)
118 }
119 return int(pgid), nil
120}