• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

muesli / termenv / 4378269639

09 Mar 2023 07:57PM UTC coverage: 57.484%. First build
4378269639

push

github

Ayman Bagabas
fix(output): status report ignored when assumeTTY or unsafe is true

27 of 27 new or added lines in 3 files covered. (100.0%)

530 of 922 relevant lines covered (57.48%)

8.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.61
/termenv_unix.go
1
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
2
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
3

4
package termenv
5

6
import (
7
        "fmt"
8
        "io"
9
        "strconv"
10
        "strings"
11
        "time"
12

13
        "golang.org/x/sys/unix"
14
)
15

16
const (
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.
23
func (o *Output) ColorProfile() Profile {
14✔
24
        if !o.isTTY() {
27✔
25
                return Ascii
13✔
26
        }
13✔
27

28
        term := o.environ.Getenv("TERM")
1✔
29
        colorTerm := o.environ.Getenv("COLORTERM")
1✔
30

1✔
31
        switch strings.ToLower(colorTerm) {
1✔
32
        case "24bit":
×
33
                fallthrough
×
34
        case "truecolor":
×
35
                if strings.HasPrefix(term, "screen") {
×
36
                        // tmux supports TrueColor, screen only ANSI256
×
37
                        if o.environ.Getenv("TERM_PROGRAM") != "tmux" {
×
38
                                return ANSI256
×
39
                        }
×
40
                }
41
                return TrueColor
×
42
        case "yes":
×
43
                fallthrough
×
44
        case "true":
×
45
                return ANSI256
×
46
        }
47

48
        switch term {
1✔
49
        case "xterm-kitty":
×
50
                return TrueColor
×
51
        case "linux":
×
52
                return ANSI
×
53
        }
54

55
        if strings.Contains(term, "256color") {
1✔
56
                return ANSI256
×
57
        }
×
58
        if strings.Contains(term, "color") {
1✔
59
                return ANSI
×
60
        }
×
61
        if strings.Contains(term, "ansi") {
1✔
62
                return ANSI
×
63
        }
×
64

65
        return Ascii
1✔
66
}
67

68
func (o Output) foregroundColor() Color {
×
69
        s, err := o.termStatusReport(10)
×
70
        if err == nil {
×
71
                c, err := xTermColor(s)
×
72
                if err == nil {
×
73
                        return c
×
74
                }
×
75
        }
76

77
        colorFGBG := o.environ.Getenv("COLORFGBG")
×
78
        if strings.Contains(colorFGBG, ";") {
×
79
                c := strings.Split(colorFGBG, ";")
×
80
                i, err := strconv.Atoi(c[0])
×
81
                if err == nil {
×
82
                        return ANSIColor(i)
×
83
                }
×
84
        }
85

86
        // default gray
87
        return ANSIColor(7)
×
88
}
89

90
func (o Output) backgroundColor() Color {
×
91
        s, err := o.termStatusReport(11)
×
92
        if err == nil {
×
93
                c, err := xTermColor(s)
×
94
                if err == nil {
×
95
                        return c
×
96
                }
×
97
        }
98

99
        colorFGBG := o.environ.Getenv("COLORFGBG")
×
100
        if strings.Contains(colorFGBG, ";") {
×
101
                c := strings.Split(colorFGBG, ";")
×
102
                i, err := strconv.Atoi(c[len(c)-1])
×
103
                if err == nil {
×
104
                        return ANSIColor(i)
×
105
                }
×
106
        }
107

108
        // default black
109
        return ANSIColor(0)
×
110
}
111

112
func (o *Output) waitForData(tty File, timeout time.Duration) error {
×
113
        fd := tty.Fd()
×
114
        tv := unix.NsecToTimeval(int64(timeout))
×
115
        var readfds unix.FdSet
×
116
        readfds.Set(int(fd))
×
117

×
118
        for {
×
119
                n, err := unix.Select(int(fd)+1, &readfds, nil, nil, &tv)
×
120
                if err == unix.EINTR {
×
121
                        continue
×
122
                }
123
                if err != nil {
×
124
                        return err
×
125
                }
×
126
                if n == 0 {
×
127
                        return fmt.Errorf("timeout")
×
128
                }
×
129

130
                break
×
131
        }
132

133
        return nil
×
134
}
135

136
func (o *Output) readNextByte(tty io.ReadWriter) (byte, error) {
×
137
        if f, ok := tty.(File); ok && !o.unsafe {
×
138
                if err := o.waitForData(f, OSCTimeout); err != nil {
×
139
                        return 0, err
×
140
                }
×
141
        }
142

143
        var b [1]byte
×
144
        n, err := tty.Read(b[:])
×
145
        if err != nil {
×
146
                return 0, err
×
147
        }
×
148

149
        if n == 0 {
×
150
                panic("read returned no data")
×
151
        }
152

153
        return b[0], nil
×
154
}
155

156
// readNextResponse reads either an OSC response or a cursor position response:
157
//   - OSC response: "\x1b]11;rgb:1111/1111/1111\x1b\\"
158
//   - cursor position response: "\x1b[42;1R"
159
func (o *Output) readNextResponse(tty io.ReadWriter) (response string, isOSC bool, err error) {
×
160
        start, err := o.readNextByte(tty)
×
161
        if err != nil {
×
162
                return "", false, err
×
163
        }
×
164

165
        // first byte must be ESC
166
        for start != ESC {
×
167
                start, err = o.readNextByte(tty)
×
168
                if err != nil {
×
169
                        return "", false, err
×
170
                }
×
171
        }
172

173
        response += string(start)
×
174

×
175
        // next byte is either '[' (cursor position response) or ']' (OSC response)
×
176
        tpe, err := o.readNextByte(tty)
×
177
        if err != nil {
×
178
                return "", false, err
×
179
        }
×
180

181
        response += string(tpe)
×
182

×
183
        var oscResponse bool
×
184
        switch tpe {
×
185
        case '[':
×
186
                oscResponse = false
×
187
        case ']':
×
188
                oscResponse = true
×
189
        default:
×
190
                return "", false, ErrStatusReport
×
191
        }
192

193
        for {
×
194
                b, err := o.readNextByte(tty)
×
195
                if err != nil {
×
196
                        return "", false, err
×
197
                }
×
198

199
                response += string(b)
×
200

×
201
                if oscResponse {
×
202
                        // OSC can be terminated by BEL (\a) or ST (ESC)
×
203
                        if b == BEL || strings.HasSuffix(response, string(ESC)) {
×
204
                                return response, true, nil
×
205
                        }
×
206
                } else {
×
207
                        // cursor position response is terminated by 'R'
×
208
                        if b == 'R' {
×
209
                                return response, false, nil
×
210
                        }
×
211
                }
212

213
                // both responses have less than 25 bytes, so if we read more, that's an error
214
                if len(response) > 25 {
×
215
                        break
×
216
                }
217
        }
218

219
        return "", false, ErrStatusReport
×
220
}
221

222
func (o Output) termStatusReport(sequence int) (string, error) {
×
223
        // screen/tmux can't support OSC, because they can be connected to multiple
×
224
        // terminals concurrently.
×
225
        term := o.environ.Getenv("TERM")
×
226
        if strings.HasPrefix(term, "screen") || strings.HasPrefix(term, "tmux") {
×
227
                return "", ErrStatusReport
×
228
        }
×
229

230
        tty, ok := o.TTY().(io.ReadWriter)
×
231
        if tty == nil || !ok {
×
232
                return "", ErrStatusReport
×
233
        }
×
234

235
        if !o.unsafe {
×
236
                f, ok := tty.(File)
×
237
                if !ok {
×
238
                        return "", ErrStatusReport
×
239
                }
×
240
                fd := int(f.Fd())
×
241
                // if in background, we can't control the terminal
×
242
                if !isForeground(fd) {
×
243
                        return "", ErrStatusReport
×
244
                }
×
245

246
                t, err := unix.IoctlGetTermios(fd, tcgetattr)
×
247
                if err != nil {
×
248
                        return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
×
249
                }
×
250
                defer unix.IoctlSetTermios(fd, tcsetattr, t) //nolint:errcheck
×
251

×
252
                noecho := *t
×
253
                noecho.Lflag = noecho.Lflag &^ unix.ECHO
×
254
                noecho.Lflag = noecho.Lflag &^ unix.ICANON
×
255
                if err := unix.IoctlSetTermios(fd, tcsetattr, &noecho); err != nil {
×
256
                        return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
×
257
                }
×
258
        }
259

260
        // first, send OSC query, which is ignored by terminal which do not support it
261
        fmt.Fprintf(tty, OSC+"%d;?"+ST, sequence)
×
262

×
263
        // then, query cursor position, should be supported by all terminals
×
264
        fmt.Fprintf(tty, CSI+"6n")
×
265

×
266
        // read the next response
×
267
        res, isOSC, err := o.readNextResponse(tty)
×
268
        if err != nil {
×
269
                return "", fmt.Errorf("%s: %s", ErrStatusReport, err)
×
270
        }
×
271

272
        // if this is not OSC response, then the terminal does not support it
273
        if !isOSC {
×
274
                return "", ErrStatusReport
×
275
        }
×
276

277
        // read the cursor query response next and discard the result
278
        _, _, err = o.readNextResponse(tty)
×
279
        if err != nil {
×
280
                return "", err
×
281
        }
×
282

283
        // fmt.Println("Rcvd", res[1:])
284
        return res, nil
×
285
}
286

287
// EnableVirtualTerminalProcessing enables virtual terminal processing on
288
// Windows for w and returns a function that restores w to its previous state.
289
// On non-Windows platforms, or if w does not refer to a terminal, then it
290
// returns a non-nil no-op function and no error.
291
func EnableVirtualTerminalProcessing(w io.Writer) (func() error, error) {
1✔
292
        return func() error { return nil }, nil
2✔
293
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc