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 }