-
Notifications
You must be signed in to change notification settings - Fork 7
/
gchalk.go
400 lines (351 loc) · 12.3 KB
/
gchalk.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// Package gchalk is terminal string styling for go done right, with full Linux, MacOS, and painless Windows 10 support.
//
// GChalk is a library heavily inspired by https://github.com/chalk/chalk, the
// popular Node.js terminal color library, and using golang ports of supports-color
// (https://github.com/jwalton/go-supportscolor) and ansi-styles
// (https://github.com/jwalton/gchalk/pkg/ansistyles).
//
// A very simple usage example would be:
//
// fmt.Println(gchalk.Blue("This line is blue"))
//
// Note that this works on all platforms - there's no need to write to a special
// stream or use a special print function to get color on Windows 10.
//
// Some examples:
//
// // Combine styled and normal strings
// fmt.Println(gchalk.Blue("Hello") + " World" + gchalk.Red("!"))
//
// // Compose multiple styles using the chainable API
// fmt.Println(gchalk.WithBlue().WithBgRed().Bold("Hello world!"))
//
// // Pass in multiple arguments
// fmt.Println(gchalk.Blue("Hello", "World!", "Foo", "bar", "biz", "baz"))
//
// // Nest styles
// fmt.Println(gchalk.Green(
// "I am a green line " +
// gchalk.WithBlue().WithUnderline().Bold("with a blue substring") +
// " that becomes green again!"
// ))
//
// // Use RGB colors in terminal emulators that support it.
// fmt.Println(gchalk.WithRGB(123, 45, 67).Underline("Underlined reddish color"))
// fmt.Println(gchalk.WithHex("#DEADED").Bold("Bold gray!"))
//
// // Write to stderr:
// os.Stderr.WriteString(gchalk.Stderr.Red("Ohs noes!\n"))
//
// See the README.md for more details.
//
package gchalk
import (
"fmt"
"strings"
"sync"
"github.com/jwalton/go-supportscolor"
)
// ColorLevel represents the ANSI color level supported by the terminal.
type ColorLevel = supportscolor.ColorLevel
// ColorFn is a convenience type for a function that takes in a string and returns a colored string.
type ColorFn func(str ...string) string
const (
// LevelNone represents a terminal that does not support color at all.
LevelNone ColorLevel = supportscolor.None
// LevelBasic represents a terminal with basic 16 color support.
LevelBasic ColorLevel = supportscolor.Basic
// LevelAnsi256 represents a terminal with 256 color support.
LevelAnsi256 ColorLevel = supportscolor.Ansi256
// LevelAnsi16m represents a terminal with full true color support.
LevelAnsi16m ColorLevel = supportscolor.Ansi16m
)
type stylerData struct {
open string
close string
openAll string
closeAll string
parent *stylerData
}
// builderShared contains data shared between all builders.
type builderShared struct {
Level ColorLevel
mutex sync.Mutex
}
// A Builder is used to define and chain together styles.
//
// Instances of Builder cannot be constructed directly - you can build a new
// instance via the New() function, which will give you an instance you can
// configure without modifying the "default" Builder.
//
type Builder struct {
styler *stylerData
shared *builderShared
bgBlack *Builder
bgBrightBlack *Builder
bgBlue *Builder
bgBrightBlue *Builder
bgCyan *Builder
bgBrightCyan *Builder
bgGray *Builder
bgGreen *Builder
bgBrightGreen *Builder
bgGrey *Builder
bgMagenta *Builder
bgBrightMagenta *Builder
bgRed *Builder
bgBrightRed *Builder
bgWhite *Builder
bgBrightWhite *Builder
bgYellow *Builder
bgBrightYellow *Builder
black *Builder
brightBlack *Builder
blue *Builder
brightBlue *Builder
cyan *Builder
brightCyan *Builder
gray *Builder
green *Builder
brightGreen *Builder
grey *Builder
magenta *Builder
brightMagenta *Builder
red *Builder
brightRed *Builder
white *Builder
brightWhite *Builder
yellow *Builder
brightYellow *Builder
bold *Builder
dim *Builder
hidden *Builder
inverse *Builder
italic *Builder
overline *Builder
strikethrough *Builder
underline *Builder
reset *Builder
}
// An Option which can be passed to `New()`.
type Option func(*Builder)
// ForceLevel is an option that can be passed to `New` to force the color level
// used.
func ForceLevel(level ColorLevel) Option {
return func(builder *Builder) {
builder.shared.Level = level
}
}
// New creates a new instance of GChalk.
func New(options ...Option) *Builder {
builder := &Builder{styler: nil}
builder.shared = &builderShared{
Level: supportscolor.Stdout().Level,
}
for index := range options {
options[index](builder)
}
return builder
}
// rootBuilder is the default GChalk instance, pre-configured for stdout.
var rootBuilder = New()
// Stderr is an instance of GChalk pre-configured for stderr. Use this when coloring
// strings you intend to write the stderr.
var Stderr = New(
ForceLevel(ColorLevel(supportscolor.Stderr().Level)),
)
func createBuilder(parentBuilder *Builder, open string, close string) *Builder {
var parent *stylerData
if parentBuilder.styler != nil {
parent = parentBuilder.styler
}
openAll := open
closeAll := close
if parent != nil {
openAll = parent.openAll + open
closeAll = close + parent.closeAll
}
return &Builder{
shared: parentBuilder.shared,
styler: &stylerData{
open: open,
close: close,
openAll: openAll,
closeAll: closeAll,
parent: parent,
},
}
}
func (builder *Builder) applyStyle(strs ...string) string {
if len(strs) == 0 {
return ""
}
var str string
if len(strs) == 1 {
str = strs[0]
} else {
str = strings.Join(strs, " ")
}
if (builder.shared.Level <= LevelNone) || str == "" {
return str
}
styler := builder.styler
if styler == nil {
return str
}
openAll := styler.openAll
closeAll := styler.closeAll
if strings.Contains(str, "\u001B") {
for styler != nil {
// Replace any instances already present with a re-opening code
// otherwise only the part of the string until said closing code
// will be colored, and the rest will simply be 'plain'.
if styler.close == "\u001b[22m" {
// This is kind of a weird corner case - both "bold" and "dim"
// close with "22", but these are actually not mutually exclusive
// styles - you can have something both bold and dim at the same
// time (iTerm 2, for example, will render it as a dimmer color,
// with a bold font face). So when we nest "dim" inside "bold",
// if we just replace the dim's close with bold's open, we'll
// end up with something that is dim and bold at the same time.
// The fix here is to keep the close tag. This can lead to
// a big chain of close tags followed immediately by open tags
// in cases where we do a lot of nesting, and in any other
// case this is pointless (as a string can't be both red and
// blue at the same time, for example), so we treat this as a
// special case.
str = strings.ReplaceAll(str, styler.close, styler.close+styler.open)
} else {
str = strings.ReplaceAll(str, styler.close, styler.open)
}
styler = styler.parent
}
}
// We can move both next actions out of loop, because remaining actions in loop won't have
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
if strings.Contains(str, "\n") {
str = stringEncaseCRLF(str, closeAll, openAll)
}
// Concat using "+" instead of fmt.Sprintf, because it's about four times faster.
return openAll + str + closeAll
}
// SetLevel is used to override the auto-detected color level.
func SetLevel(level ColorLevel) {
rootBuilder.SetLevel(level)
}
// GetLevel returns the currently configured color level.
func GetLevel() ColorLevel {
return rootBuilder.GetLevel()
}
// SetLevel is used to override the auto-detected color level for a builder. Calling
// this at any level of the builder will affect the entire instance of the builder.
func (builder *Builder) SetLevel(level ColorLevel) {
builder.shared.Level = level
}
// GetLevel returns the currently configured level for this builder.
func (builder *Builder) GetLevel() ColorLevel {
return builder.shared.Level
}
// StyleMust will return a function which colors a string with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will panic.
func StyleMust(styles ...string) ColorFn {
return rootBuilder.WithStyleMust(styles...).applyStyle
}
// WithStyleMust will construct a Builder that generates strings with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will panic.
func WithStyleMust(styles ...string) *Builder {
return rootBuilder.WithStyleMust(styles...)
}
// StyleMust will return a function which colors a string with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will panic.
func (builder *Builder) StyleMust(styles ...string) ColorFn {
return builder.WithStyleMust(styles...).applyStyle
}
// WithStyleMust will construct a Builder that generates strings with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will panic.
func (builder *Builder) WithStyleMust(styles ...string) *Builder {
var result, err = builder.WithStyle(styles...)
if err != nil {
panic(err)
}
return result
}
// Style will return a function which colors a string with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will return an error.
func Style(styles ...string) (ColorFn, error) {
newBuilder, err := rootBuilder.WithStyle(styles...)
if err != nil {
return rootBuilder.applyStyle, err
}
return newBuilder.applyStyle, nil
}
// WithStyle will construct a Builder that generates strings with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will return an error.
func WithStyle(styles ...string) (*Builder, error) {
return rootBuilder.WithStyle(styles...)
}
// Style will return a function which colors a string with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will return an error.
func (builder *Builder) Style(styles ...string) (ColorFn, error) {
newBuilder, err := rootBuilder.WithStyle(styles...)
if err != nil {
return rootBuilder.applyStyle, err
}
return newBuilder.applyStyle, nil
}
// WithStyle will construct a Builder that generates strings with the specified
// styles. Styles can be specified as a named style (e.g. "red", "bgRed", "bgred"),
// or as a hex color ("#ff00ff" or "bg#ff00ff"). If the style cannot
// be parsed, this will return an error.
func (builder *Builder) WithStyle(styles ...string) (*Builder, error) {
var err error = nil
var result *Builder = builder
for _, style := range styles {
newBuilder := result.getBuilderForStyle(style)
if newBuilder == nil {
// Handle hex codes.
if style[0] == '#' {
newBuilder = builder.WithHex(style)
} else if strings.HasPrefix(style, "bg#") {
newBuilder = builder.WithBgHex(style[2:])
}
}
if newBuilder != nil {
result = newBuilder
} else {
err = fmt.Errorf("No such style: %s", style)
result = builder
}
}
return result, err
}
// Paint will apply a style to a string.
// This is similar to the `paint()` function from Rust's `ansi_term` crate.
//
// gchalk.WithRed().Paint("Hello World!")
//
func (builder *Builder) Paint(strs ...string) string {
return builder.applyStyle(strs...)
}
// Sprintf is a convenience function for coloring formatted strings.
//
// gchalk.WithRed().Sprtinf("Hello %s", "World!")
//
func (builder *Builder) Sprintf(format string, a ...interface{}) string {
return builder.applyStyle(fmt.Sprintf(format, a...))
}