termenv_unix.go

  1//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
  2// +build darwin dragonfly freebsd linux netbsd openbsd solaris zos
  3
  4package termenv
  5
  6import (
  7	"fmt"
  8	"io"
  9	"strconv"
 10	"strings"
 11	"time"
 12
 13	"golang.org/x/sys/unix"
 14)
 15
 16const (
 17	// timeout for OSC queries.
 18	OSCTimeout = 5 * time.Second
 19)
 20
 21// ColorProfile returns the supported color profile:
 22// Ascii, ANSI, ANSI256, or TrueColor.
 23func (o *Output) ColorProfile() Profile {
 24	if !o.isTTY() {
 25		return Ascii
 26	}
 27
 28	if o.environ.Getenv("GOOGLE_CLOUD_SHELL") == "true" {
 29		return TrueColor
 30	}
 31
 32	term := o.environ.Getenv("TERM")
 33	colorTerm := o.environ.Getenv("COLORTERM")
 34
 35	switch strings.ToLower(colorTerm) {
 36	case "24bit":
 37		fallthrough
 38	case "truecolor":
 39		if strings.HasPrefix(term, "screen") {
 40			// tmux supports TrueColor, screen only ANSI256
 41			if o.environ.Getenv("TERM_PROGRAM") != "tmux" {
 42				return ANSI256
 43			}
 44		}
 45		return TrueColor
 46	case "yes":
 47		fallthrough
 48	case "true":
 49		return ANSI256
 50	}
 51
 52	switch term {
 53	case
 54		"alacritty",
 55		"contour",
 56		"rio",
 57		"wezterm",
 58		"xterm-ghostty",
 59		"xterm-kitty":
 60		return TrueColor
 61	case "linux", "xterm":
 62		return ANSI
 63	}
 64
 65	if strings.Contains(term, "256color") {
 66		return ANSI256
 67	}
 68	if strings.Contains(term, "color") {
 69		return ANSI
 70	}
 71	if strings.Contains(term, "ansi") {
 72		return ANSI
 73	}
 74
 75	return Ascii
 76}
 77
 78//nolint:mnd
 79func (o Output) foregroundColor() Color {
 80	s, err := o.termStatusReport(10)
 81	if err == nil {
 82		c, err := xTermColor(s)
 83		if err == nil {
 84			return c
 85		}
 86	}
 87
 88	colorFGBG := o.environ.Getenv("COLORFGBG")
 89	if strings.Contains(colorFGBG, ";") {
 90		c := strings.Split(colorFGBG, ";")
 91		i, err := strconv.Atoi(c[0])
 92		if err == nil {
 93			return ANSIColor(i)
 94		}
 95	}
 96
 97	// default gray
 98	return ANSIColor(7)
 99}
100
101//nolint:mnd
102func (o Output) backgroundColor() Color {
103	s, err := o.termStatusReport(11)
104	if err == nil {
105		c, err := xTermColor(s)
106		if err == nil {
107			return c
108		}
109	}
110
111	colorFGBG := o.environ.Getenv("COLORFGBG")
112	if strings.Contains(colorFGBG, ";") {
113		c := strings.Split(colorFGBG, ";")
114		i, err := strconv.Atoi(c[len(c)-1])
115		if err == nil {
116			return ANSIColor(i)
117		}
118	}
119
120	// default black
121	return ANSIColor(0)
122}
123
124func (o *Output) waitForData(timeout time.Duration) error {
125	fd := o.TTY().Fd()
126	tv := unix.NsecToTimeval(int64(timeout))
127	var readfds unix.FdSet
128	readfds.Set(int(fd)) //nolint:gosec
129
130	for {
131		n, err := unix.Select(int(fd)+1, &readfds, nil, nil, &tv) //nolint:gosec
132		if err == unix.EINTR {
133			continue
134		}
135		if err != nil {
136			return err //nolint:wrapcheck
137		}
138		if n == 0 {
139			return fmt.Errorf("timeout")
140		}
141
142		break
143	}
144
145	return nil
146}
147
148func (o *Output) readNextByte() (byte, error) {
149	if !o.unsafe {
150		if err := o.waitForData(OSCTimeout); err != nil {
151			return 0, err
152		}
153	}
154
155	var b [1]byte
156	n, err := o.TTY().Read(b[:])
157	if err != nil {
158		return 0, err //nolint:wrapcheck
159	}
160
161	if n == 0 {
162		panic("read returned no data")
163	}
164
165	return b[0], nil
166}
167
168// readNextResponse reads either an OSC response or a cursor position response:
169//   - OSC response: "\x1b]11;rgb:1111/1111/1111\x1b\\"
170//   - cursor position response: "\x1b[42;1R"
171func (o *Output) readNextResponse() (response string, isOSC bool, err error) {
172	start, err := o.readNextByte()
173	if err != nil {
174		return "", false, err
175	}
176
177	// first byte must be ESC
178	for start != ESC {
179		start, err = o.readNextByte()
180		if err != nil {
181			return "", false, err
182		}
183	}
184
185	response += string(start)
186
187	// next byte is either '[' (cursor position response) or ']' (OSC response)
188	tpe, err := o.readNextByte()
189	if err != nil {
190		return "", false, err
191	}
192
193	response += string(tpe)
194
195	var oscResponse bool
196	switch tpe {
197	case '[':
198		oscResponse = false
199	case ']':
200		oscResponse = true
201	default:
202		return "", false, ErrStatusReport
203	}
204
205	for {
206		b, err := o.readNextByte()
207		if err != nil {
208			return "", false, err
209		}
210
211		response += string(b)
212
213		if oscResponse {
214			// OSC can be terminated by BEL (\a) or ST (ESC)
215			if b == BEL || strings.HasSuffix(response, string(ESC)) {
216				return response, true, nil
217			}
218		} else {
219			// cursor position response is terminated by 'R'
220			if b == 'R' {
221				return response, false, nil
222			}
223		}
224
225		// both responses have less than 25 bytes, so if we read more, that's an error
226		if len(response) > 25 { //nolint:mnd
227			break
228		}
229	}
230
231	return "", false, ErrStatusReport
232}
233
234func (o Output) termStatusReport(sequence int) (string, error) {
235	// screen/tmux can't support OSC, because they can be connected to multiple
236	// terminals concurrently.
237	term := o.environ.Getenv("TERM")
238	if strings.HasPrefix(term, "screen") || strings.HasPrefix(term, "tmux") || strings.HasPrefix(term, "dumb") {
239		return "", ErrStatusReport
240	}
241
242	tty := o.TTY()
243	if tty == nil {
244		return "", ErrStatusReport
245	}
246
247	if !o.unsafe {
248		fd := int(tty.Fd()) //nolint:gosec
249		// if in background, we can't control the terminal
250		if !isForeground(fd) {
251			return "", ErrStatusReport
252		}
253
254		t, err := unix.IoctlGetTermios(fd, tcgetattr)
255		if err != nil {
256			return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
257		}
258		defer unix.IoctlSetTermios(fd, tcsetattr, t) //nolint:errcheck
259
260		noecho := *t
261		noecho.Lflag = noecho.Lflag &^ unix.ECHO
262		noecho.Lflag = noecho.Lflag &^ unix.ICANON
263		if err := unix.IoctlSetTermios(fd, tcsetattr, &noecho); err != nil {
264			return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
265		}
266	}
267
268	// first, send OSC query, which is ignored by terminal which do not support it
269	fmt.Fprintf(tty, OSC+"%d;?"+ST, sequence) //nolint:errcheck
270
271	// then, query cursor position, should be supported by all terminals
272	fmt.Fprintf(tty, CSI+"6n") //nolint:errcheck
273
274	// read the next response
275	res, isOSC, err := o.readNextResponse()
276	if err != nil {
277		return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
278	}
279
280	// if this is not OSC response, then the terminal does not support it
281	if !isOSC {
282		return "", ErrStatusReport
283	}
284
285	// read the cursor query response next and discard the result
286	_, _, err = o.readNextResponse()
287	if err != nil {
288		return "", err
289	}
290
291	// fmt.Println("Rcvd", res[1:])
292	return res, nil
293}
294
295// EnableVirtualTerminalProcessing enables virtual terminal processing on
296// Windows for w and returns a function that restores w to its previous state.
297// On non-Windows platforms, or if w does not refer to a terminal, then it
298// returns a non-nil no-op function and no error.
299func EnableVirtualTerminalProcessing(_ io.Writer) (func() error, error) {
300	return func() error { return nil }, nil
301}