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 }