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.
21type winsize struct {
22 Row uint16
23 Col uint16
24 Xpixel uint16
25 Ypixel uint16
26}
27
28// ioctl issues a SYS_IOCTL syscall against fd with the given request and
29// argument pointer, returning a Go error on failure. The pointer-to-uintptr
30// conversion must happen inside the Syscall argument list (unsafe.Pointer
31// rule 4), so arg stays a live pointer until the call.
32func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
33 _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
34 if errno != 0 {
35 return errno
36 }
37 return nil
38}
39
40// IsTerminal reports whether fd refers to a terminal device.
41func IsTerminal(fd int) bool {
42 var t syscall.Termios
43 return ioctl(fd, reqGetTermios, unsafe.Pointer(&t)) == nil
44}
45
46// MakeRaw puts the terminal referenced by fd into raw mode and returns its
47// pre-modification state, so a caller can later restore it. The flag
48// changes applied match golang.org/x/term's MakeRaw semantics exactly.
49func MakeRaw(fd int) (*State, error) {
50 var oldState State
51 if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&oldState.termios)); err != nil {
52 return nil, fmt.Errorf("rawterm: get termios: %w", err)
53 }
54
55 raw := oldState.termios
56 raw.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
57 raw.Oflag &^= syscall.OPOST
58 raw.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
59 raw.Cflag &^= syscall.CSIZE | syscall.PARENB
60 raw.Cflag |= syscall.CS8
61 raw.Cc[syscall.VMIN] = 1
62 raw.Cc[syscall.VTIME] = 0
63
64 if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&raw)); err != nil {
65 return nil, fmt.Errorf("rawterm: set termios: %w", err)
66 }
67
68 return &oldState, nil
69}
70
71// Restore applies a previously captured State back to the terminal
72// referenced by fd.
73func Restore(fd int, s *State) error {
74 if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&s.termios)); err != nil {
75 return fmt.Errorf("rawterm: set termios: %w", err)
76 }
77 return nil
78}
79
80// GetSize returns the terminal's current column and row count.
81func GetSize(fd int) (cols, rows int, err error) {
82 var ws winsize
83 if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil {
84 return 0, 0, fmt.Errorf("rawterm: get winsize: %w", err)
85 }
86 return int(ws.Col), int(ws.Row), nil
87}
88
89// SetWinsize sets the terminal's column and row count.
90func SetWinsize(fd int, cols, rows uint16) error {
91 ws := winsize{Row: rows, Col: cols}
92 if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil {
93 return fmt.Errorf("rawterm: set winsize: %w", err)
94 }
95 return nil
96}
97
98// ForegroundProcessGroup returns the process group id currently in the
99// foreground for the terminal referenced by fd (TIOCGPGRP).
100func ForegroundProcessGroup(fd int) (int, error) {
101 var pgid int32
102 if err := ioctl(fd, syscall.TIOCGPGRP, unsafe.Pointer(&pgid)); err != nil {
103 return 0, fmt.Errorf("rawterm: get foreground process group: %w", err)
104 }
105 return int(pgid), nil
106}