1 /**
2  * Basic cross-platform color output
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.sys.term;
15 
16 import std.stdio;
17 
18 /// Base class for a terminal implementation.
19 /// The interface only contains the common subset of features
20 /// available on both platforms (Windows API and ANSI).
21 abstract class Term
22 {
23 	/// The 16 standard VGA/ANSI colors plus a "none" pseudo-color.
24 	enum Color : byte
25 	{
26 		none          = -1                , /// transparent or default
27 
28 		black         = 0                 , ///
29 
30 		red           = 1 << 0            , ///
31 		green         = 1 << 1            , ///
32 		blue          = 1 << 2            , ///
33 
34 		yellow        = red | green       , ///
35 		magenta       = red |         blue, ///
36 		cyan          =       green | blue, ///
37 		gray          = red | green | blue, ///
38 
39 		darkGray      = bright | black    , ///
40 		brightRed     = bright | red      , ///
41 		brightGreen   = bright | green    , ///
42 		brightBlue    = bright | blue     , ///
43 		brightYellow  = bright | yellow   , ///
44 		brightMagenta = bright | magenta  , ///
45 		brightCyan    = bright | cyan     , ///
46 		white         = bright | gray     , ///
47 
48 		// Place this definition after the colors so that e.g.
49 		// std.conv prefers the color name over this flag name.
50 		bright        = 1 << 3, /// (mask)
51 	}
52 
53 	private enum mixColorAliases(T) = (){
54 		import std.traits : EnumMembers;
55 		string s;
56 		foreach (i, m; EnumMembers!Color)
57 		{
58 			enum name = __traits(identifier, EnumMembers!Color[i]);
59 			s ~= `enum ` ~ name ~ ` = ` ~ T.stringof ~ `(Color.` ~ name ~ `);`;
60 		}
61 
62 		return s;
63 	}();
64 
65 	/// Color wrapping formatting operations for put.
66 	struct ForegroundColor { Color c; /***/ mixin(mixColorAliases!ForegroundColor); }
67 	struct BackgroundColor { Color c; /***/ mixin(mixColorAliases!BackgroundColor); } /// ditto
68 
69 	/// Shorthands
70 	alias fg = ForegroundColor;
71 	alias bg = BackgroundColor; /// ditto
72 	mixin(mixColorAliases!ForegroundColor); /// ditto
73 
74 	/// Puts some combination of colors and stringifiable objects in order.
75 	void put(Args...)(auto ref Args args)
76 	{
77 		putImpl(InitState.init, args);
78 	}
79 
80 private:
81 	void putImpl(State, Args...)(State state, ref Args args)
82 	{
83 		// Use a tail recursion and maintain state using a type to
84 		// allow congesting consecutive formatting operations into a
85 		// single terminal operation.
86 		// E.g.: `fg.a, bg.b` (or swapped) gets encoded as `\e[3a;4bm`
87 		// instead of `\e[3am\e[4bm`
88 		// E.g.: `fg.a, fg.b` gets encoded as `\e[3bm` instead of
89 		// `\e[3am\e[3bm`
90 		static if (Args.length == 0)
91 			flush(state);
92 		else
93 		{
94 			alias A = Args[0];
95 			static if (is(A == ForegroundColor))
96 			{
97 				static struct NextState
98 				{
99 					ForegroundColor fg;
100 					typeof(State.bg) bg;
101 				}
102 				return putImpl(NextState(args[0], state.bg), args[1..$]);
103 			}
104 			else
105 			static if (is(A == BackgroundColor))
106 			{
107 				static struct NextState
108 				{
109 					typeof(State.fg) fg;
110 					BackgroundColor bg;
111 				}
112 				return putImpl(NextState(state.fg, args[0]), args[1..$]);
113 			}
114 			else
115 			{
116 				flush(state);
117 				static if (is(A : const(char)[]))
118 					putText(args[0]);
119 				else
120 				{
121 					import std.format : formattedWrite;
122 					formattedWrite!"%s"(&putText, args[0]);
123 				}
124 				return putImpl(initState, args[1..$]);
125 			}
126 		}
127 	}
128 
129 	alias NoColor = void[0];
130 	static struct InitState
131 	{
132 		NoColor fg, bg;
133 	}
134 	enum initState = InitState.init;
135 
136 	void flush(State)(State state)
137 	{
138 		static if (!is(typeof(state.bg) == NoColor))
139 			static if (!is(typeof(state.fg) == NoColor))
140 				setColor(state.bg.c, state.fg.c);
141 			else
142 				setBackgroundColor(state.bg.c);
143 		else
144 			static if (!is(typeof(state.fg) == NoColor))
145 				setTextColor(state.fg.c);
146 	}
147 
148 protected:
149 	void putText(in char[] s);
150 	void setTextColor(Color c);
151 	void setBackgroundColor(Color c);
152 	void setColor(Color fg, Color bg);
153 }
154 
155 /// No output (including text).
156 /// (See DumbTerm for text-only output).
157 class NullTerm : Term
158 {
159 protected:
160 	override void putText(in char[] s) {}
161 	override void setTextColor(Color c) {}
162 	override void setBackgroundColor(Color c) {}
163 	override void setColor(Color fg, Color bg) {}
164 }
165 
166 /// Base class for File-based terminal implementations.
167 abstract class FileTerm : Term
168 {
169 	File f; ///
170 
171 protected:
172 	override void putText(in char[] s) { f.rawWrite(s); }
173 }
174 
175 /// No color, only text output.
176 class DumbTerm : FileTerm
177 {
178 	this(File f)
179 	{
180 		this.f = f;
181 	} ///
182 
183 protected:
184 	override void setTextColor(Color c) {}
185 	override void setBackgroundColor(Color c) {}
186 	override void setColor(Color fg, Color bg) {}
187 }
188 
189 /// ANSI escape sequence based terminal.
190 class ANSITerm : FileTerm
191 {
192 	/// Returns true if `f` is a TTY and `TERM`
193 	/// is set to anything other than "dumb".
194 	static bool isTerm(File f)
195 	{
196 		version (Posix)
197 		{
198 			import core.sys.posix.unistd : isatty;
199 			if (!isatty(f.fileno))
200 				return false;
201 		}
202 
203 		import std.process : environment;
204 		auto term = environment.get("TERM");
205 		if (!term || term == "dumb")
206 			return false;
207 		return true;
208 	}
209 
210 	/// Convert a `Color` to an ANSI color number.
211 	static int ansiColor(Color c, bool background)
212 	{
213 		if (c < -1 || c > Color.white)
214 			assert(false);
215 
216 		int result;
217 		if (c == Color.none)
218 			result = 39;
219 		else
220 		if (c & Color.bright)
221 			result = 90 + (c & ~uint(Color.bright));
222 		else
223 			result = 30 + c;
224 		if (background)
225 			result += 10;
226 		return result;
227 	}
228 
229 	this(File f)
230 	{
231 		this.f = f;
232 	} ///
233 
234 protected:
235 	override void setTextColor(Color c)
236 	{
237 		f.writef("\x1b[%dm", ansiColor(c, false));
238 	}
239 
240 	override void setBackgroundColor(Color c)
241 	{
242 		f.writef("\x1b[%dm", ansiColor(c, true));
243 	}
244 
245 	override void setColor(Color fg, Color bg)
246 	{
247 		f.writef("\x1b[%d;%dm", ansiColor(fg, false), ansiColor(bg, true));
248 	}
249 }
250 
251 /// Windows API based terminal.
252 version (Windows)
253 class WindowsTerm : FileTerm
254 {
255 	import core.sys.windows.basetsd : HANDLE;
256 	import core.sys.windows.windef : WORD;
257 	import core.sys.windows.wincon : CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, SetConsoleTextAttribute;
258 	import ae.sys.windows.exception : wenforce;
259 
260 	/// Returns true if `f` is a Windows console.
261 	static bool isTerm(File f)
262 	{
263 		CONSOLE_SCREEN_BUFFER_INFO info;
264 		return !!GetConsoleScreenBufferInfo(f.windowsHandle, &info);
265 	}
266 
267 	this(File f)
268 	{
269 		this.f = f;
270 		handle = f.windowsHandle;
271 
272 		CONSOLE_SCREEN_BUFFER_INFO info;
273 		GetConsoleScreenBufferInfo(handle, &info)
274 			.wenforce("GetConsoleScreenBufferInfo");
275 		attributes = origAttributes = info.wAttributes;
276 	} ///
277 
278 protected:
279 	HANDLE handle;
280 	WORD attributes, origAttributes;
281 
282 	final void setAttribute(bool background)(Color c)
283 	{
284 		enum shift = background ? 4 : 0;
285 		enum mask = 0xF << shift;
286 		if (c == Color.none)
287 			attributes = (attributes & ~mask) | (origAttributes & mask);
288 		else
289 		{
290 			WORD value = c;
291 			value <<= shift;
292 			attributes = (attributes & ~mask) | value;
293 		}
294 	}
295 
296 	final void applyAttribute()
297 	{
298 		f.flush();
299 		SetConsoleTextAttribute(handle, attributes)
300 			.wenforce("SetConsoleTextAttribute");
301 	}
302 
303 	override void setTextColor(Color c)
304 	{
305 		setAttribute!false(c);
306 		applyAttribute();
307 	}
308 
309 	override void setBackgroundColor(Color c)
310 	{
311 		setAttribute!true(c);
312 		applyAttribute();
313 	}
314 
315 	override void setColor(Color fg, Color bg)
316 	{
317 		setAttribute!false(fg);
318 		setAttribute!true(bg);
319 		applyAttribute();
320 	}
321 }
322 
323 /// Returns whether the given file is likely to be attached to a terminal.
324 bool isTerm(File f)
325 {
326 	version (Windows)
327 	{
328 		if (WindowsTerm.isTerm(f))
329 			return true;
330 		// fall through - we might be on a POSIX environment on Windows
331 	}
332 	return ANSITerm.isTerm(f);
333 }
334 
335 /// Choose a suitable terminal implementation for the given file, create and return it.
336 Term makeTerm(File f = stderr)
337 {
338 	if (isTerm(f))
339 		version (Windows)
340 			return new WindowsTerm(f);
341 		else
342 			return new ANSITerm(f);
343 	else
344 		return new DumbTerm(f);
345 }
346 
347 private Term globalTerm;
348 /// Get or set a `Term` implementation for the current thread (creating one if necessary).
349 @property Term term()
350 {
351 	if (!globalTerm)
352 		globalTerm = makeTerm();
353 	return globalTerm;
354 } /// ditto
355 @property Term term(Term newTerm)
356 {
357 	return globalTerm = newTerm;
358 } /// ditto
359 
360 version (ae_sys_term_demo)
361 private void main()
362 {
363 	auto t = term;
364 	foreach (bg; -1 .. 16)
365 	{
366 		t.put(t.bg(cast(Term.Color)bg));
367 		foreach (fg; -1 .. 16)
368 			t.put(t.fg(cast(Term.Color)fg), "XX");
369 		t.put(t.none, t.bg.none, "\n");
370 	}
371 	readln();
372 }