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 			// check 256-colour support
212 			auto v = GetVersion();
213 
214 
215 		} else {
216 
217 			//TODO get terminal name and check 256-color support
218 
219 		}
220 
221 	}
222 
223 	public final pure nothrow @property @safe @nogc ref File file() {
224 		return _file;
225 	}
226 
227 	// ------
228 	// titles
229 	// ------
230 
231 	/**
232 	 * Gets the console's title.
233 	 * Only works on Windows.
234 	 */
235 	public @property string title() {
236 		version(Windows) {
237 			char[] title = new char[MAX_PATH];
238 			GetConsoleTitleA(title.ptr, MAX_PATH);
239 			return fromStringz(title.ptr).idup;
240 		} else {
241 			return "";
242 		}
243 	}
244 
245 	/**
246 	 * Sets the console's title.
247 	 * The original title is usually restored when the program's execution ends.
248 	 * Returns: The console's title. On Windows it may be cropped if its length exceeds MAX_PATH.
249 	 * Example:
250 	 * ---
251 	 * terminal.title = "Custom Title";
252 	 * ---
253 	 */
254 	public @property string title(string title) {
255 		version(Windows) {
256 			if(title.length > MAX_PATH) title = title[0..MAX_PATH];
257 			SetConsoleTitleA(toStringz(title));
258 		} else {
259 			_file.write("\033]0;" ~ title ~ "\007");
260 			_file.flush();
261 		}
262 		return title;
263 	}
264 
265 	// ----
266 	// size
267 	// ----
268 
269 	private static struct Size {
270 
271 		uint width;
272 		uint height;
273 
274 		alias columns = width;
275 		alias rows = height;
276 
277 	}
278 
279 	/**
280 	 * Gets the terminal's width (columns) and height (rows).
281 	 * Example:
282 	 * ---
283 	 * auto size = terminal.size;
284 	 * foreach(i ; 0..size.width)
285 	 *    write("*");
286 	 * ---
287 	 */
288 	public @property Size size() {
289 		version(Windows) {
290 			CONSOLE_SCREEN_BUFFER_INFO csbi;
291 			GetConsoleScreenBufferInfo(_file.windowsHandle, &csbi);
292 			with(csbi.srWindow) return Size(Right - Left + 1, Bottom - Top + 1);
293 		} else {
294 			winsize ws;
295 			ioctl(_file.fileno, TIOCGWINSZ, &ws);
296 			return Size(ws.ws_col, ws.ws_row);
297 		}
298 	}
299 
300 	public @property uint width() {
301 		return this.size.width;
302 	}
303 
304 	public @property uint height() {
305 		return this.size.height;
306 	}
307 
308 	// -------
309 	// colours
310 	// -------
311 
312 	public alias foreground = colorImpl!("foreground", 0);
313 
314 	public alias background = colorImpl!("background", 10);
315 
316 	private template colorImpl(string type, int add) {
317 
318 		public void colorImpl(color_t color) {
319 			version(Windows) {
320 				if(color == Color.reset) color = mixin("original." ~ type);
321 				mixin("current." ~ type) = color;
322 				this.update();
323 			} else {
324 				if(color == Color.reset) color = 39;
325 				this.update(color + add);
326 			}
327 		}
328 
329 		public void colorImpl(ubyte[3] rgb) {
330 			_file.writef("\033[%d;2;%d;%d;%dm", 38 + add, rgb[0], rgb[1], rgb[2]);
331 			version(Windows) this.uses256 = true;
332 		}
333 
334 		public void colorImpl(Ground!type ground) {
335 			if(ground.isRGB) mixin(type)(ground.rgb);
336 			else mixin(type)(ground.color);
337 		}
338 
339 	}
340 
341 	// ----------
342 	// formatting
343 	// ----------
344 
345 	public alias bold = formatImpl!(1, 22);
346 	
347 	public alias italic = formatImpl!(3, 23);
348 
349 	public alias strikethrough = formatImpl!(9, 29);
350 
351 	public alias underlined = formatImpl!(4, 24, "underscore");
352 	
353 	public alias overlined = formatImpl!(53, 55, "grid_horizontal");
354 
355 	public alias inversed = formatImpl!(7, 27, "reverse_video");
356 
357 	private template formatImpl(int start, int stop, string windowsAttr="") {
358 
359 		version(Windows) {
360 
361 			// save an alias to the attribute's value
362 			private enum hasAttribute = windowsAttr.length > 0;
363 			static if(hasAttribute) private enum attribute = mixin("COMMON_LVB_" ~ windowsAttr.toUpper());
364 
365 		}
366 
367 		version(Posix) {
368 
369 			// save the current state into a variable as it is not stored in an attribute
370 			private bool _active = false;
371 
372 		}
373 
374 		public @property bool formatImpl() {
375 			version(Windows) {
376 				static if(hasAttribute) return (current.attributes & attribute) != 0;
377 				else return false;
378 			} else {
379 				return _active;
380 			}
381 		}
382 		
383 		/// ditto
384 		public @property bool formatImpl(bool active) {
385 			version(Windows) {
386 				static if(hasAttribute) {
387 					if(active) current.attributes |= attribute;
388 					else current.attributes &= attribute ^ WORD.max;
389 					this.update();
390 					return active;
391 				} else {
392 					return false;
393 				}
394 			} else {
395 				this.update(active ? start : stop);
396 				return _active = active;
397 			}
398 		}
399 
400 	}
401 
402 	/**
403 	 * Resets the colour (foreground and background) and formatting.
404 	 */
405 	public void reset() {
406 		version(Windows) {
407 			current.attributes = original.attributes;
408 			this.update();
409 			if(this.uses256) {
410 				_file.write("\033[0m");
411 				this.uses256 = false;
412 			}
413 		} else {
414 			static foreach(format ; formats) {
415 				mixin(format) = false;
416 			}
417 			this.update(0); // reset colours
418 		}
419 	}
420 	
421 	version(Windows) private void update() {
422 		_file.flush();
423 		SetConsoleTextAttribute(_file.windowsHandle, current.attributes);
424 	}
425 	
426 	version(Posix) private void update(int ec) {
427 		_file.writef("\033[%dm", ec);
428 	}
429 
430 	// -------------
431 	// write methods
432 	// -------------
433 
434 	void write(E...)(E args) {
435 		foreach(arg ; args) {
436 			static if(is(typeof(arg) == Foreground)) {
437 				foreground = arg;
438 			} else static if(is(typeof(arg) == Background)) {
439 				background = arg;
440 			} else static if(is(typeof(arg) == Reset)) {
441 				reset();
442 			} else {
443 				mixin({
444 					string ret;
445 					foreach(format ; formats) {
446 						ret ~= "static if(is(typeof(arg) == " ~ capitalize(format) ~ ")){" ~ format ~ "=cast(bool)arg;}else ";
447 					}
448 					return ret ~ "_file.write(arg);";
449 				}());
450 			}
451 		}
452 	}
453 
454 	void writeln(E...)(E args) {
455 		write(args, "\n");
456 	}
457 
458 	void writelnr(E...)(E args) {
459 		writeln(args);
460 		reset();
461 	}
462 
463 	~this() {
464 		//this.reset();
465 	}
466 
467 }
468 
469 unittest {
470 
471 	import std.stdio;
472 
473 	auto terminal = new Terminal();
474 
475 	terminal.title = "terminal-color's unittest";
476 
477 	// test rgb palette
478 	ubyte conv(int num) {
479 		return cast(ubyte)(num * 16);
480 	}
481 	foreach(r ; 0..16) {
482 		foreach(g ; 0..16) {
483 			foreach(b ; 0..16) {
484 				terminal.write(Background([conv(r), conv(g), conv(b)]), "  ");
485 			}
486 			writeln();
487 		}
488 		writeln();
489 	}
490 	
491 	// test foreground
492 	foreach(color ; __traits(allMembers, Color)) {
493 		terminal.foreground = mixin("Color." ~ color);
494 		write("@");
495 	}
496 	writeln();
497 	
498 	// test foreground
499 	foreach(color ; __traits(allMembers, Color)) {
500 		terminal.background = mixin("Color." ~ color);
501 		write(" ");
502 	}
503 	writeln();
504 	
505 	writeln();
506 	
507 	// test formats
508 	static foreach(format ; formats) {
509 		mixin("terminal." ~ format) = true;
510 		terminal.writelnr(format);
511 	}
512 	
513 	terminal.writelnr();
514 
515 	// size
516 	writeln(terminal.size);
517 
518 	writeln();
519 
520 }