1// +build darwin dragonfly freebsd linux netbsd openbsd solaris
2
3package termenv
4
5import (
6	"fmt"
7	"os"
8	"strconv"
9	"strings"
10
11	"golang.org/x/sys/unix"
12)
13
14func colorProfile() Profile {
15	term := os.Getenv("TERM")
16	colorTerm := os.Getenv("COLORTERM")
17
18	switch strings.ToLower(colorTerm) {
19	case "24bit":
20		fallthrough
21	case "truecolor":
22		if term == "screen" || !strings.HasPrefix(term, "screen") {
23			// enable TrueColor in tmux, but not for old-school screen
24			return TrueColor
25		}
26	case "yes":
27		fallthrough
28	case "true":
29		return ANSI256
30	}
31
32	if strings.Contains(term, "256color") {
33		return ANSI256
34	}
35	if strings.Contains(term, "color") {
36		return ANSI
37	}
38
39	return Ascii
40}
41
42func foregroundColor() Color {
43	s, err := termStatusReport(10)
44	if err == nil {
45		c, err := xTermColor(s)
46		if err == nil {
47			return c
48		}
49	}
50
51	colorFGBG := os.Getenv("COLORFGBG")
52	if strings.Contains(colorFGBG, ";") {
53		c := strings.Split(colorFGBG, ";")
54		i, err := strconv.Atoi(c[0])
55		if err == nil {
56			return ANSIColor(i)
57		}
58	}
59
60	// default gray
61	return ANSIColor(7)
62}
63
64func backgroundColor() Color {
65	s, err := termStatusReport(11)
66	if err == nil {
67		c, err := xTermColor(s)
68		if err == nil {
69			return c
70		}
71	}
72
73	colorFGBG := os.Getenv("COLORFGBG")
74	if strings.Contains(colorFGBG, ";") {
75		c := strings.Split(colorFGBG, ";")
76		i, err := strconv.Atoi(c[1])
77		if err == nil {
78			return ANSIColor(i)
79		}
80	}
81
82	// default black
83	return ANSIColor(0)
84}
85
86func readNextByte(f *os.File) (byte, error) {
87	var b [1]byte
88	n, err := f.Read(b[:])
89	if err != nil {
90		return 0, err
91	}
92
93	if n == 0 {
94		panic("read returned no data")
95	}
96
97	return b[0], nil
98}
99
100// readNextResponse reads either an OSC response or a cursor position response:
101//  * OSC response: "\x1b]11;rgb:1111/1111/1111\x1b\\"
102//  * cursor position response: "\x1b[42;1R"
103func readNextResponse(fd *os.File) (response string, isOSC bool, err error) {
104	start, err := readNextByte(fd)
105	if err != nil {
106		return "", false, err
107	}
108
109	// if we encounter a backslash, this is a left-over from the previous OSC
110	// response, which can be terminated by an optional backslash
111	if start == '\\' {
112		start, err = readNextByte(fd)
113		if err != nil {
114			return "", false, err
115		}
116	}
117
118	// first byte must be ESC
119	if start != '\033' {
120		return "", false, ErrStatusReport
121	}
122
123	response += string(start)
124
125	// next byte is either '[' (cursor position response) or ']' (OSC response)
126	tpe, err := readNextByte(fd)
127	if err != nil {
128		return "", false, err
129	}
130
131	response += string(tpe)
132
133	var oscResponse bool
134	switch tpe {
135	case '[':
136		oscResponse = false
137	case ']':
138		oscResponse = true
139	default:
140		return "", false, ErrStatusReport
141	}
142
143	for {
144		b, err := readNextByte(os.Stdout)
145		if err != nil {
146			return "", false, err
147		}
148
149		response += string(b)
150
151		if oscResponse {
152			// OSC can be terminated by BEL (\a) or ST (ESC)
153			if b == '\a' || strings.HasSuffix(response, "\033") {
154				return response, true, nil
155			}
156		} else {
157			// cursor position response is terminated by 'R'
158			if b == 'R' {
159				return response, false, nil
160			}
161		}
162
163		// both responses have less than 25 bytes, so if we read more, that's an error
164		if len(response) > 25 {
165			break
166		}
167	}
168
169	return "", false, ErrStatusReport
170}
171
172func termStatusReport(sequence int) (string, error) {
173	// screen/tmux can't support OSC, because they can be connected to multiple
174	// terminals concurrently.
175	term := os.Getenv("TERM")
176	if strings.HasPrefix(term, "screen") {
177		return "", ErrStatusReport
178	}
179
180	t, err := unix.IoctlGetTermios(unix.Stdout, tcgetattr)
181	if err != nil {
182		return "", ErrStatusReport
183	}
184	defer unix.IoctlSetTermios(unix.Stdout, tcsetattr, t)
185
186	noecho := *t
187	noecho.Lflag = noecho.Lflag &^ unix.ECHO
188	noecho.Lflag = noecho.Lflag &^ unix.ICANON
189	if err := unix.IoctlSetTermios(unix.Stdout, tcsetattr, &noecho); err != nil {
190		return "", ErrStatusReport
191	}
192
193	// first, send OSC query, which is ignored by terminal which do not support it
194	fmt.Printf("\033]%d;?\033\\", sequence)
195
196	// then, query cursor position, should be supported by all terminals
197	fmt.Printf("\033[6n")
198
199	// read the next response
200	res, isOSC, err := readNextResponse(os.Stdout)
201	if err != nil {
202		return "", err
203	}
204
205	// if this is not OSC response, then the terminal does not support it
206	if !isOSC {
207		return "", ErrStatusReport
208	}
209
210	// read the cursor query response next and discard the result
211	_, _, err = readNextResponse(os.Stdout)
212	if err != nil {
213		return "", err
214	}
215
216	// fmt.Println("Rcvd", res[1:])
217	return res, nil
218}
219