1 /**
2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 Terminal colors. initColors must be called for the colors to be activated.
7 Colors are automatically toggled off if the output is not an interactive tty.
8 This can be bypassed by calling initColors with `true`.
9 */
10 module my.term_color;
11 
12 import std.stdio : writefln, stderr, stdout;
13 import logger = std.experimental.logger;
14 
15 @("shall print colors, backgrounds and modes")
16 unittest {
17     import std.stdio;
18     import std.traits;
19     import std.conv;
20     import std.string;
21 
22     initColors(true);
23 
24     foreach (c; EnumMembers!Color) {
25         write(c.to!string.color(c).toString.rightJustify(30));
26         foreach (m; EnumMembers!Mode) {
27             writef(" %s", m.to!string.color(c).mode(m));
28         }
29         writeln;
30     }
31     foreach (c; EnumMembers!Background) {
32         write(c.to!string.color.bg(c).toString.rightJustify(30));
33         foreach (m; EnumMembers!Mode) {
34             writef(" %s", m.to!string.color.bg(c).mode(m));
35         }
36         writeln;
37     }
38 }
39 
40 private template BaseColor(int n) {
41     enum BaseColor : int {
42         none = 39 + n,
43 
44         black = 30 + n,
45         red = 31 + n,
46         green = 32 + n,
47         yellow = 33 + n,
48         blue = 34 + n,
49         magenta = 35 + n,
50         cyan = 36 + n,
51         white = 37 + n,
52 
53         lightBlack = 90 + n,
54         lightRed = 91 + n,
55         lightGreen = 92 + n,
56         lightYellow = 93 + n,
57         lightBlue = 94 + n,
58         lightMagenta = 95 + n,
59         lightCyan = 96 + n,
60         lightWhite = 97 + n,
61     }
62 }
63 
64 alias Color = BaseColor!0;
65 alias Background = BaseColor!10;
66 
67 enum Mode {
68     none = 0,
69     bold = 1,
70     underline = 4,
71     blink = 5,
72     swap = 7,
73     hide = 8,
74 }
75 
76 struct ColorImpl {
77     import std.format : FormatSpec;
78 
79     private {
80         string text;
81         Color fg_;
82         Background bg_;
83         Mode mode_;
84     }
85 
86     this(string txt) @safe pure nothrow @nogc {
87         text = txt;
88     }
89 
90     this(string txt, Color c) @safe pure nothrow @nogc {
91         text = txt;
92         fg_ = c;
93     }
94 
95     ColorImpl opDispatch(string fn)() {
96         import std.conv : to;
97         import std.string : toLower;
98 
99         static if (fn.length >= 2 && (fn[0 .. 2] == "fg" || fn[0 .. 2] == "bg")) {
100             static if (fn[0 .. 2] == "fg") {
101                 fg_ = fn[2 .. $].toLower.to!Color;
102             } else static if (fn[0 .. 2] == "bg") {
103                 bg_ = fn[2 .. $].toLower.to!Background;
104             } else {
105                 static assert("unable to handle " ~ fn);
106             }
107         } else {
108             mode_ = fn.to!Mode;
109         }
110         return this;
111     }
112 
113     ColorImpl fg(Color c_) @safe pure nothrow @nogc {
114         this.fg_ = c_;
115         return this;
116     }
117 
118     ColorImpl bg(Background c_) @safe pure nothrow @nogc {
119         this.bg_ = c_;
120         return this;
121     }
122 
123     ColorImpl mode(Mode c_) @safe pure nothrow @nogc {
124         this.mode_ = c_;
125         return this;
126     }
127 
128     string toString() @safe const {
129         import std.exception : assumeUnique;
130         import std.format : FormatSpec;
131 
132         char[] buf;
133         buf.reserve(100);
134         auto fmt = FormatSpec!char("%s");
135         toString((const(char)[] s) @safe const{ buf ~= s; }, fmt);
136         auto trustedUnique(T)(T t) @trusted {
137             return assumeUnique(t);
138         }
139 
140         return trustedUnique(buf);
141     }
142 
143     void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const {
144         import std.format : formattedWrite;
145         import std.range.primitives : put;
146 
147         if (!_printColors || (fg_ == Color.none && bg_ == Background.none && mode_ == Mode.none))
148             put(w, text);
149         else
150             formattedWrite(w, "\033[%d;%d;%dm%s\033[0m", mode_, fg_, bg_, text);
151     }
152 }
153 
154 auto color(string s, Color c = Color.none) @safe pure nothrow @nogc {
155     return ColorImpl(s, c);
156 }
157 
158 @("shall be safe/pure/nothrow/nogc to color a string")
159 @safe pure nothrow @nogc unittest {
160     auto s = "foo".color(Color.red).bg(Background.black).mode(Mode.bold);
161 }
162 
163 @("shall be safe to color a string")
164 @safe unittest {
165     initColors(true);
166     auto s = "foo".color(Color.red).bg(Background.black).mode(Mode.bold).toString;
167 }
168 
169 @("shall use opDispatch for config")
170 @safe unittest {
171     import std.stdio;
172 
173     initColors(true);
174     writeln("opDispatch".color.fgred);
175     writeln("opDispatch".color.bgGreen);
176     writeln("opDispatch".color.bold);
177     writeln("opDispatch".color.fgred.bggreen.bold);
178 }
179 
180 /** It will detect whether or not stdout/stderr are a console/TTY and will
181  * consequently disable colored output if needed.
182  *
183  * Forgetting to call the function will result in ASCII escape sequences in the
184  * piped output, probably an undesiderable thing.
185  */
186 void initColors(bool forceOn = false) @trusted {
187     if (forceOn) {
188         _printColors = true;
189         return;
190     }
191 
192     if (_isColorsInitialized)
193         return;
194     scope (exit)
195         _isColorsInitialized = true;
196 
197     version (Windows) {
198         _printColors = false;
199     } else {
200         import my.tty;
201 
202         _printColors = isStdoutInteractive && isStderrInteractive;
203     }
204 }
205 
206 private:
207 
208 /** Whether to print text with colors or not
209  *
210  * Defaults to true but will be set to false in initColors() if stdout or
211  * stderr are not a TTY (which means the output is probably being piped and we
212  * don't want ASCII escape chars in it)
213 */
214 private shared bool _printColors = false;
215 private shared bool _isColorsInitialized = false;