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 <vladimir@thecybershadow.net>
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,
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 	static bool isTerm(File f)
193 	{
194 		version (Posix)
195 		{
196 			import core.sys.posix.unistd : isatty;
197 			if (!isatty(f.fileno))
198 				return false;
199 		}
200 
201 		import std.process : environment;
202 		auto term = environment.get("TERM");
203 		if (!term || term == "dumb")
204 			return false;
205 		return true;
206 	}
207 
208 	static int ansiColor(Color c, bool background)
209 	{
210 		if (c < -1 || c > Color.white)
211 			assert(false);
212 
213 		int result;
214 		if (c == Color.none)
215 			result = 39;
216 		else
217 		if (c & Color.bright)
218 			result = 90 + (c & ~uint(Color.bright));
219 		else
220 			result = 30 + c;
221 		if (background)
222 			result += 10;
223 		return result;
224 	}
225 
226 	this(File f)
227 	{
228 		this.f = f;
229 	}
230 
231 protected:
232 	override void setTextColor(Color c)
233 	{
234 		f.writef("\x1b[%dm", ansiColor(c, false));
235 	}
236 
237 	override void setBackgroundColor(Color c)
238 	{
239 		f.writef("\x1b[%dm", ansiColor(c, true));
240 	}
241 
242 	override void setColor(Color fg, Color bg)
243 	{
244 		f.writef("\x1b[%d;%dm", ansiColor(fg, false), ansiColor(bg, true));
245 	}
246 }
247 
248 /// Windows API based terminal.
249 version (Windows)
250 class WindowsTerm : FileTerm
251 {
252 	import core.sys.windows.basetsd : HANDLE;
253 	import core.sys.windows.windef : WORD;
254 	import core.sys.windows.wincon : CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, SetConsoleTextAttribute;
255 	import ae.sys.windows.exception : wenforce;
256 
257 	static bool isTerm(File f)
258 	{
259 		CONSOLE_SCREEN_BUFFER_INFO info;
260 		return !!GetConsoleScreenBufferInfo(f.windowsHandle, &info);
261 	}
262 
263 	HANDLE handle;
264 	WORD attributes, origAttributes;
265 
266 	this(File f)
267 	{
268 		this.f = f;
269 		handle = f.windowsHandle;
270 
271 		CONSOLE_SCREEN_BUFFER_INFO info;
272 		GetConsoleScreenBufferInfo(handle, &info)
273 			.wenforce("GetConsoleScreenBufferInfo");
274 		attributes = origAttributes = info.wAttributes;
275 	}
276 
277 protected:
278 	final void setAttribute(bool background)(Color c)
279 	{
280 		enum shift = background ? 4 : 0;
281 		enum mask = 0xF << shift;
282 		if (c == Color.none)
283 			attributes = (attributes & ~mask) | (origAttributes & mask);
284 		else
285 		{
286 			WORD value = c;
287 			value <<= shift;
288 			attributes = (attributes & ~mask) | value;
289 		}
290 	}
291 
292 	final void applyAttribute()
293 	{
294 		f.flush();
295 		SetConsoleTextAttribute(handle, attributes)
296 			.wenforce("SetConsoleTextAttribute");
297 	}
298 
299 	override void setTextColor(Color c)
300 	{
301 		setAttribute!false(c);
302 		applyAttribute();
303 	}
304 
305 	override void setBackgroundColor(Color c)
306 	{
307 		setAttribute!true(c);
308 		applyAttribute();
309 	}
310 
311 	override void setColor(Color fg, Color bg)
312 	{
313 		setAttribute!false(fg);
314 		setAttribute!true(bg);
315 		applyAttribute();
316 	}
317 }
318 
319 /// Returns whether the given file is likely to be attached to a terminal.
320 bool isTerm(File f)
321 {
322 	version (Windows)
323 	{
324 		if (WindowsTerm.isTerm(f))
325 			return true;
326 		// fall through - we might be on a POSIX environment on Windows
327 	}
328 	return ANSITerm.isTerm(f);
329 }
330 
331 /// Choose a suitable terminal implementation for the given file, create and return it.
332 Term makeTerm(File f = stderr)
333 {
334 	if (isTerm(f))
335 		version (Windows)
336 			return new WindowsTerm(f);
337 		else
338 			return new ANSITerm(f);
339 	else
340 		return new DumbTerm(f);
341 }
342 
343 /// Get or set a Term implementation for the current thread (creating one if necessary).
344 private Term globalTerm;
345 @property Term term()
346 {
347 	if (!globalTerm)
348 		globalTerm = makeTerm();
349 	return globalTerm;
350 }
351 @property Term term(Term newTerm) /// ditto
352 {
353 	return globalTerm = newTerm;
354 }
355 
356 version (ae_sys_term_demo)
357 void main()
358 {
359 	auto t = term;
360 	foreach (bg; -1 .. 16)
361 	{
362 		t.put(t.bg(cast(Term.Color)bg));
363 		foreach (fg; -1 .. 16)
364 			t.put(t.fg(cast(Term.Color)fg), "XX");
365 		t.put(t.none, t.bg.none, "\n");
366 	}
367 	readln();
368 }