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}