...
1
2
3 package term
4
5 import (
6 "io"
7 "os"
8 "strconv"
9 "strings"
10
11 "github.com/muesli/termenv"
12 "golang.org/x/term"
13 )
14
15
16 type Term struct {
17 in *os.File
18 out *os.File
19 errOut *os.File
20 isTTY bool
21 colorEnabled bool
22 is256enabled bool
23 hasTrueColor bool
24 width int
25 widthPercent int
26 }
27
28
29
30
31
32
33
34
35 func FromEnv() Term {
36 var stdoutIsTTY bool
37 var isColorEnabled bool
38 var termWidthOverride int
39 var termWidthPercentage int
40
41 spec := os.Getenv("GH_FORCE_TTY")
42 if spec != "" {
43 stdoutIsTTY = true
44 isColorEnabled = !IsColorDisabled()
45
46 if w, err := strconv.Atoi(spec); err == nil {
47 termWidthOverride = w
48 } else if strings.HasSuffix(spec, "%") {
49 if p, err := strconv.Atoi(spec[:len(spec)-1]); err == nil {
50 termWidthPercentage = p
51 }
52 }
53 } else {
54 stdoutIsTTY = IsTerminal(os.Stdout)
55 isColorEnabled = IsColorForced() || (!IsColorDisabled() && stdoutIsTTY)
56 }
57
58 isVirtualTerminal := false
59 if stdoutIsTTY {
60 if err := enableVirtualTerminalProcessing(os.Stdout); err == nil {
61 isVirtualTerminal = true
62 }
63 }
64
65 return Term{
66 in: os.Stdin,
67 out: os.Stdout,
68 errOut: os.Stderr,
69 isTTY: stdoutIsTTY,
70 colorEnabled: isColorEnabled,
71 is256enabled: isVirtualTerminal || is256ColorSupported(),
72 hasTrueColor: isVirtualTerminal || isTrueColorSupported(),
73 width: termWidthOverride,
74 widthPercent: termWidthPercentage,
75 }
76 }
77
78
79 func (t Term) In() io.Reader {
80 return t.in
81 }
82
83
84 func (t Term) Out() io.Writer {
85 return t.out
86 }
87
88
89 func (t Term) ErrOut() io.Writer {
90 return t.errOut
91 }
92
93
94 func (t Term) IsTerminalOutput() bool {
95 return t.isTTY
96 }
97
98
99
100 func (t Term) IsColorEnabled() bool {
101 return t.colorEnabled
102 }
103
104
105 func (t Term) Is256ColorSupported() bool {
106 return t.is256enabled
107 }
108
109
110 func (t Term) IsTrueColorSupported() bool {
111 return t.hasTrueColor
112 }
113
114
115
116 func (t Term) Size() (int, int, error) {
117 if t.width > 0 {
118 return t.width, -1, nil
119 }
120
121 ttyOut := t.out
122 if ttyOut == nil || !IsTerminal(ttyOut) {
123 if f, err := openTTY(); err == nil {
124 defer f.Close()
125 ttyOut = f
126 } else {
127 return -1, -1, err
128 }
129 }
130
131 width, height, err := terminalSize(ttyOut)
132 if err == nil && t.widthPercent > 0 {
133 return int(float64(width) * float64(t.widthPercent) / 100), height, nil
134 }
135
136 return width, height, err
137 }
138
139
140 func (t Term) Theme() string {
141 if !t.IsColorEnabled() {
142 return "none"
143 }
144 if termenv.HasDarkBackground() {
145 return "dark"
146 }
147 return "light"
148 }
149
150
151 func IsTerminal(f *os.File) bool {
152 return term.IsTerminal(int(f.Fd()))
153 }
154
155 func terminalSize(f *os.File) (int, int, error) {
156 return term.GetSize(int(f.Fd()))
157 }
158
159
160
161 func IsColorDisabled() bool {
162 return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0"
163 }
164
165
166 func IsColorForced() bool {
167 return os.Getenv("CLICOLOR_FORCE") != "" && os.Getenv("CLICOLOR_FORCE") != "0"
168 }
169
170 func is256ColorSupported() bool {
171 return isTrueColorSupported() ||
172 strings.Contains(os.Getenv("TERM"), "256") ||
173 strings.Contains(os.Getenv("COLORTERM"), "256")
174 }
175
176 func isTrueColorSupported() bool {
177 term := os.Getenv("TERM")
178 colorterm := os.Getenv("COLORTERM")
179
180 return strings.Contains(term, "24bit") ||
181 strings.Contains(term, "truecolor") ||
182 strings.Contains(colorterm, "24bit") ||
183 strings.Contains(colorterm, "truecolor")
184 }
185
View as plain text