1 /*
2  * Copyright (c) 2018 Kripth
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  *
22  */
23 module terminal;
24 
25 import std.stdio : File, stdout;
26 import std.string : fromStringz, toStringz, toUpper, capitalize;
27 import std.typecons : Flag;
28 import std.utf : toUTF8;
29 
30 version(Windows) {
31 
32 	import core.sys.windows.windows;
33 
34 	enum ubyte RED = FOREGROUND_RED;
35 	enum ubyte GREEN = FOREGROUND_GREEN;
36 	enum ubyte BLUE = FOREGROUND_BLUE;
37 	enum ubyte BRIGHT = FOREGROUND_INTENSITY;
38 
39 	alias color_t = ubyte;
40 
41 	enum Color : color_t {
42 
43 		black = 0,
44 		red = RED,
45 		green = GREEN,
46 		blue = BLUE,
47 		yellow = red | green,
48 		magenta = red | blue,
49 		cyan = green | blue,
50 		lightGray = red | green | blue,
51 		gray = black | BRIGHT,
52 		brightRed = red | BRIGHT,
53 		brightGreen = green | BRIGHT,
54 		brightBlue = blue | BRIGHT,
55 		brightYellow = yellow | BRIGHT,
56 		brightMagenta = magenta | BRIGHT,
57 		brightCyan = cyan | BRIGHT,
58 		white = lightGray | BRIGHT,
59 
60 		reset = 255
61 
62 	}
63 
64 } else version(Posix) {
65 
66 	version(linux) {
67 
68 		struct winsize {
69 
70 			ushort ws_row;
71 			ushort ws_col;
72 			ushort ws_xpixel;
73 			ushort ws_ypixel;
74 
75 		}
76 
77 		enum uint TIOCGWINSZ = 0x5413;
78 		extern(C) int ioctl(int, int, ...);
79 
80 	} else {
81 
82 		import core.sys.posix.sys.ioctl : winsize, TIOCGWINSZ, ioctl;
83 
84 	}
85 
86 	alias color_t = ubyte;
87 
88 	enum Color : ubyte {
89 
90 		black = 30,
91 		red = 31,
92 		green = 32,
93 		yellow = 33,
94 		blue = 34,
95 		magenta = 35,
96 		cyan = 36,
97 		lightGray = 37,
98 		gray = 90,
99 		brightRed = 91,
100 		brightGreen = 92,
101 		brightYellow = 93,
102 		brightBlue = 94,
103 		brightMagenta = 95,
104 		brightCyan = 96,
105 		white = 97,
106 
107 		reset = 255
108 
109 	}
110 
111 } else {
112 
113 	static assert(0);
114 
115 }
116 
117 private struct Ground(string _type) {
118 
119 	enum reset = typeof(this)(Color.reset);
120 
121 	union {
122 
123 		color_t color;
124 		ubyte[3] rgb;
125 
126 	}
127 
128 	bool isRGB;
129 
130 	this(color_t color) {
131 		this.color = color;
132 		isRGB = false;
133 	}
134 
135 	this(ubyte[3] rgb) {
136 		this.rgb = rgb;
137 		isRGB = true;
138 	}
139 
140 	this(ubyte r, ubyte g, ubyte b) {
141 		this([r, g, b]);
142 	}
143 
144 }
145 
146 alias Foreground = Ground!"foreground";
147 
148 alias Background = Ground!"background";
149 
150 private enum formats = ["bold", "italic", "strikethrough", "underlined", "overlined", "inversed"];
151 
152 mixin({
153 
154 	string ret;
155 	foreach(format ; formats) {
156 		ret ~= "alias " ~ capitalize(format) ~ "=Flag!`" ~ format ~ "`;";
157 	}
158 	return ret;
159 
160 }());
161 
162 alias Reset = Flag!"reset";
163 
164 enum RESET = Reset.yes;
165 
166 alias reset = RESET;
167 
168 /**
169  * Instance of a terminal.
170  */
171 class Terminal {
172 
173 	private File _file;
174 
175 	version(Windows) {
176 
177 		import std.bitmanip : bitfields;
178 
179 		private union Attribute {
180 			
181 			WORD attributes;
182 			mixin(bitfields!(
183 				ubyte, "foreground", 4,
184 				ubyte, "background", 4,
185 				// WORD is 16 bits, 8 bits are left out
186 			));
187 
188 			alias attributes this;
189 			
190 		}
191 
192 		private Attribute original, current;
193 
194 		private bool uses256 = false;
195 
196 	}
197 
198 	public this(File file=stdout) {
199 
200 		_file = file;
201 
202 		version(Windows) {
203 
204 			CONSOLE_SCREEN_BUFFER_INFO csbi;
205 			GetConsoleScreenBufferInfo(file.windowsHandle, &csbi);
206 
207 			// get default colours/formatting
208 			this.original = Attribute(csbi.wAttributes);
209 			this.current = Attribute(csbi.wAttributes);
210 
211 
212 		}
213 
214 	}
215 
216 	public final pure nothrow @property @safe @nogc ref File file() {
217 		return _file;
218 	}
219 
220 	// ------
221 	// titles
222 	// ------
223 
224 	/**
225 	 * Gets the console's title.
226 	 * Only works on Windows.
227 	 */
228 	public @property string title() {
229 		version(Windows) {
230 			char[] title = new char[MAX_PATH];
231 			GetConsoleTitleA(title.ptr, MAX_PATH);
232 			return fromStringz(title.ptr).idup;
233 		} else {
234 			return "";
235 		}
236 	}
237 
238 	/**
239 	 * Sets the console's title.
240 	 * The original title is usually restored when the program's execution ends.
241 	 * Returns: The console's title. On Windows it may be cropped if its length exceeds MAX_PATH.
242 	 * Example:
243 	 * ---
244 	 * terminal.title = "Custom Title";
245 	 * ---
246 	 */
247 	public @property string title(string title) {
248 		version(Windows) {
249 			if(title.length > MAX_PATH) title = title[0..MAX_PATH];
250 			SetConsoleTitleA(toStringz(title));
251 		} else {
252 			_file.write("\033]0;" ~ title ~ "\007");
253 			_file.flush();
254 		}
255 		return title;
256 	}
257 
258 	// ----
259 	// size
260 	// ----
261 
262 	private static struct Size {
263 
264 		uint width;
265 		uint height;
266 
267 		alias columns = width;
268 		alias rows = height;
269 
270 	}
271 
272 	/**
273 	 * Gets the terminal's width (columns) and height (rows).
274 	 * Example:
275 	 * ---
276 	 * auto size = terminal.size;
277 	 * foreach(i ; 0..size.width)
278 	 *    write("*");
279 	 * ---
280 	 */
281 	public @property Size size() {
282 		version(Windows) {
283 			CONSOLE_SCREEN_BUFFER_INFO csbi;
284 			GetConsoleScreenBufferInfo(_file.windowsHandle, &csbi);
285 			with(csbi.srWindow) return Size(Right - Left + 1, Bottom - Top + 1);
286 		} else {
287 			winsize ws;
288 			ioctl(_file.fileno, TIOCGWINSZ, &ws);
289 			return Size(ws.ws_col, ws.ws_row);
290 		}
291 	}
292 
293 	public @property uint width() {
294 		return this.size.width;
295 	}
296 
297 	public @property uint height() {
298 		return this.size.height;
299 	}
300 
301 	// -------
302 	// colours
303 	// -------
304 
305 	public alias foreground = colorImpl!("foreground", 0);
306 
307 	public alias background = colorImpl!("background", 10);
308 
309 	private template colorImpl(string type, int add) {
310 
311 		public void colorImpl(color_t color) {
312 			version(Windows) {
313 				if(color == Color.reset) color = mixin("original." ~ type);
314 				mixin("current." ~ type) = color;
315 				this.update();
316 			} else {
317 				if(color == Color.reset) color = 39;
318 				this.update(color + add);
319 			}
320 		}
321 
322 		public void colorImpl(ubyte[3] rgb) {
323 			_file.writef("\033[%d;2;%d;%d;%dm", 38 + add, rgb[0], rgb[1], rgb[2]);
324 			version(Windows) this.uses256 = true;
325 		}
326 
327 		public void colorImpl(Ground!type ground) {
328 			if(ground.isRGB) mixin(type)(ground.rgb);
329 			else mixin(type)(ground.color);
330 		}
331 
332 	}
333 
334 	// ----------
335 	// formatting
336 	// ----------
337 
338 	public alias bold = formatImpl!(1, 22);
339 	
340 	public alias italic = formatImpl!(3, 23);
341 
342 	public alias strikethrough = formatImpl!(9, 29);
343 
344 	public alias underlined = formatImpl!(4, 24, "underscore");
345 	
346 	public alias overlined = formatImpl!(53, 55, "grid_horizontal");
347 
348 	public alias inversed = formatImpl!(7, 27, "reverse_video");
349 
350 	private template formatImpl(int start, int stop, string windowsAttr="") {
351 
352 		version(Windows) {
353 
354 			// save an alias to the attribute's value
355 			private enum hasAttribute = windowsAttr.length > 0;
356 			static if(hasAttribute) private enum attribute = mixin("COMMON_LVB_" ~ windowsAttr.toUpper());
357 
358 		}
359 
360 		version(Posix) {
361 
362 			// save the current state into a variable as it is not stored in an attribute
363 			private bool _active = false;
364 
365 		}
366 
367 		public @property bool formatImpl() {
368 			version(Windows) {
369 				static if(hasAttribute) return (current.attributes & attribute) != 0;
370 				else return false;
371 			} else {
372 				return _active;
373 			}
374 		}
375 		
376 		/// ditto
377 		public @property bool formatImpl(bool active) {
378 			version(Windows) {
379 				static if(hasAttribute) {
380 					if(active) current.attributes |= attribute;
381 					else current.attributes &= attribute ^ WORD.max;
382 					this.update();
383 					return active;
384 				} else {
385 					return false;
386 				}
387 			} else {
388 				this.update(active ? start : stop);
389 				return _active = active;
390 			}
391 		}
392 
393 	}
394 
395 	/**
396 	 * Resets the colour (foreground and background) and formatting.
397 	 */
398 	public void reset() {
399 		version(Windows) {
400 			current.attributes = original.attributes;
401 			this.update();
402 			if(this.uses256) {
403 				_file.write("\033[0m");
404 				this.uses256 = false;
405 			}
406 		} else {
407 			static foreach(format ; formats) {
408 				mixin(format) = false;
409 			}
410 			this.update(0); // reset colours
411 		}
412 	}
413 	
414 	version(Windows) private void update() {
415 		_file.flush();
416 		SetConsoleTextAttribute(_file.windowsHandle, current.attributes);
417 	}
418 	
419 	version(Posix) private void update(int ec) {
420 		_file.writef("\033[%dm", ec);
421 	}
422 
423 	// -------------
424 	// write methods
425 	// -------------
426 
427 	void write(E...)(E args) {
428 		foreach(arg ; args) {
429 			static if(is(typeof(arg) == Foreground)) {
430 				foreground = arg;
431 			} else static if(is(typeof(arg) == Background)) {
432 				background = arg;
433 			} else static if(is(typeof(arg) == Reset)) {
434 				reset();
435 			} else {
436 				mixin({
437 					string ret;
438 					foreach(format ; formats) {
439 						ret ~= "static if(is(typeof(arg) == " ~ capitalize(format) ~ ")){" ~ format ~ "=cast(bool)arg;}else ";
440 					}
441 					return ret ~ "_file.write(arg);";
442 				}());
443 			}
444 		}
445 	}
446 
447 	void writeln(E...)(E args) {
448 		write(args, "\n");
449 	}
450 
451 	void writelnr(E...)(E args) {
452 		writeln(args);
453 		reset();
454 	}
455 
456 	~this() {
457 		//this.reset();
458 	}
459 
460 }
461 
462 unittest {
463 
464 	import std.stdio;
465 
466 	auto terminal = new Terminal();
467 
468 	terminal.title = "terminal-color's unittest";
469 
470 	// test rgb palette
471 	ubyte conv(int num) {
472 		return cast(ubyte)(num * 16);
473 	}
474 	foreach(r ; 0..16) {
475 		foreach(g ; 0..16) {
476 			foreach(b ; 0..16) {
477 				terminal.write(Background([conv(r), conv(g), conv(b)]), "  ");
478 			}
479 			writeln();
480 		}
481 		writeln();
482 	}
483 	
484 	// test foreground
485 	foreach(color ; __traits(allMembers, Color)) {
486 		terminal.foreground = mixin("Color." ~ color);
487 		write("@");
488 	}
489 	writeln();
490 	
491 	// test foreground
492 	foreach(color ; __traits(allMembers, Color)) {
493 		terminal.background = mixin("Color." ~ color);
494 		write(" ");
495 	}
496 	writeln();
497 	
498 	writeln();
499 	
500 	// test formats
501 	static foreach(format ; formats) {
502 		mixin("terminal." ~ format) = true;
503 		terminal.writelnr(format);
504 	}
505 	
506 	terminal.writelnr();
507 
508 	// size
509 	writeln(terminal.size);
510 
511 	writeln();
512 
513 }