1 /++ 2 A module containing utilities for presenting information to the user 3 4 Copyright: © 2017 Szabo Bogdan 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Szabo Bogdan 7 +/ 8 module trial.reporters.writer; 9 10 import std.stdio; 11 import std.algorithm; 12 import std..string; 13 14 /// The default writer is initialized at the test run initialization with the right 15 /// class, depending on the hosts capabilities. 16 ReportWriter defaultWriter; 17 18 /// The writer interface is used to present information to the user. 19 interface ReportWriter 20 { 21 22 /// The information type. 23 /// Convey meaning through color with a handful of emphasis utility classes. 24 enum Context 25 { 26 /// Some important information 27 active, 28 29 /// Less important information 30 inactive, 31 32 /// 33 success, 34 35 /// Something that the user should notice 36 info, 37 38 /// Something that the user should be aware of 39 warning, 40 41 /// Something that the user must notice 42 danger, 43 44 /// 45 _default 46 } 47 48 /// Go back a few lines 49 void goTo(int); 50 51 /// Write a string 52 void write(string, Context = Context.active); 53 54 /// Write a string with reversed colors 55 void writeReverse(string, Context = Context.active); 56 57 /// Write a string and go to a new line 58 void writeln(string, Context = Context.active); 59 60 /// Show the cursor from user 61 void showCursor(); 62 63 /// Hide the cursor from user 64 void hideCursor(); 65 66 /// Get how many characters you can print on a line 67 uint width(); 68 } 69 70 /// The console writer outputs data to the standard output. It does not 71 /// support colors and cursor moving. 72 /// This is the default writer if arsd.terminal is not present. 73 class ConsoleWriter : ReportWriter 74 { 75 76 /// not supported 77 void goTo(int) 78 { 79 } 80 81 /// 82 void write(string text, Context) 83 { 84 std.stdio.write(text); 85 } 86 87 /// 88 void writeReverse(string text, Context) 89 { 90 std.stdio.write(text); 91 } 92 93 /// 94 void writeln(string text, Context) 95 { 96 std.stdio.writeln(text); 97 } 98 99 /// not supported 100 void showCursor() 101 { 102 } 103 104 /// not supported 105 void hideCursor() 106 { 107 } 108 109 /// returns 80 110 uint width() 111 { 112 return 80; 113 } 114 } 115 116 import trial.terminal; 117 118 shared static this() 119 { 120 version (Windows) 121 { 122 import core.sys.windows.windows; 123 124 SetConsoleCP(65001); 125 SetConsoleOutputCP(65001); 126 127 auto consoleType = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); 128 129 if(consoleType == 2) { 130 writeln("using the color console."); 131 defaultWriter = new ColorConsoleWriter; 132 } else { 133 writeln("using the standard console."); 134 defaultWriter = new ConsoleWriter; 135 } 136 std.stdio.stdout.flush; 137 } else { 138 defaultWriter = new ColorConsoleWriter; 139 } 140 } 141 142 143 /// This writer uses arsd.terminal and it's used if you add this dependency to your project 144 /// It supports all the features and you should use it if you want to get the best experience 145 /// from this project 146 class ColorConsoleWriter : ReportWriter 147 { 148 private 149 { 150 int[string] cues; 151 Terminal terminal; 152 153 int lines = 0; 154 bool movedToBottom = false; 155 Context currentContext = Context._default; 156 bool isReversed = false; 157 } 158 159 this() 160 { 161 this.terminal = Terminal(ConsoleOutputType.linear); 162 this.terminal._suppressDestruction = true; 163 164 lines = this.terminal.cursorY; 165 } 166 167 void setColor(Context context) 168 { 169 if (!isReversed && context == currentContext) 170 { 171 return; 172 } 173 174 isReversed = false; 175 currentContext = context; 176 177 switch (context) 178 { 179 case Context.active: 180 terminal.color(Color.white | Bright, Color.DEFAULT); 181 break; 182 183 case Context.inactive: 184 terminal.color(Color.black | Bright, Color.DEFAULT); 185 break; 186 187 case Context.success: 188 terminal.color(Color.green | Bright, Color.DEFAULT); 189 break; 190 191 case Context.info: 192 terminal.color(Color.cyan, Color.DEFAULT); 193 break; 194 195 case Context.warning: 196 terminal.color(Color.yellow, Color.DEFAULT); 197 break; 198 199 case Context.danger: 200 terminal.color(Color.red, Color.DEFAULT); 201 break; 202 203 default: 204 terminal.reset(); 205 } 206 } 207 208 void setColorReverse(Context context) 209 { 210 if (!isReversed && context == currentContext) 211 { 212 return; 213 } 214 215 currentContext = context; 216 isReversed = true; 217 218 switch (context) 219 { 220 case Context.active: 221 terminal.color(Color.DEFAULT, Color.white | Bright); 222 break; 223 224 case Context.inactive: 225 terminal.color(Color.DEFAULT, Color.black | Bright); 226 break; 227 228 case Context.success: 229 terminal.color(Color.DEFAULT, Color.green | Bright); 230 break; 231 232 case Context.info: 233 terminal.color(Color.DEFAULT, Color.cyan); 234 break; 235 236 case Context.warning: 237 terminal.color(Color.DEFAULT, Color.yellow); 238 break; 239 240 case Context.danger: 241 terminal.color(Color.DEFAULT, Color.red); 242 break; 243 244 default: 245 terminal.reset(); 246 } 247 } 248 249 void resetColor() 250 { 251 setColor(Context._default); 252 } 253 254 /// Go up `y` lines 255 void goTo(int y) 256 { 257 if (!movedToBottom) 258 { 259 movedToBottom = true; 260 terminal.moveTo(0, terminal.height - 1); 261 } 262 terminal.moveTo(0, terminal.cursorY - y, ForceOption.alwaysSend); 263 } 264 265 /// writes a string 266 void write(string text, Context context) 267 { 268 lines += text.count!(a => a == '\n'); 269 270 setColor(context); 271 272 terminal.write(text); 273 terminal.flush; 274 resetColor; 275 terminal.flush; 276 } 277 278 /// writes a string with reversed colors 279 void writeReverse(string text, Context context) 280 { 281 lines += text.count!(a => a == '\n'); 282 283 setColorReverse(context); 284 285 terminal.write(text); 286 resetColor; 287 terminal.flush; 288 } 289 290 /// writes a string and go to a new line 291 void writeln(string text, Context context) 292 { 293 this.write(text ~ "\n", context); 294 } 295 296 /// show the terminal cursor 297 void showCursor() 298 { 299 terminal.showCursor; 300 } 301 302 /// hide the terminal cursor 303 void hideCursor() 304 { 305 terminal.hideCursor; 306 } 307 308 /// returns the terminal width 309 uint width() 310 { 311 return terminal.width; 312 } 313 } 314 315 /// You can use this writer if you don't want to keep the data in memmory 316 /// It's useful for unit testing. It supports line navigation, with no color 317 /// The context info might be added in the future, once a good format is found. 318 class BufferedWriter : ReportWriter 319 { 320 321 /// The buffer used to write the data 322 string buffer = ""; 323 324 private 325 { 326 size_t line = 0; 327 size_t charPos = 0; 328 bool replace; 329 330 string[] screen; 331 } 332 333 /// go uo y lines 334 void goTo(int y) 335 { 336 line = line - y; 337 charPos = 0; 338 } 339 340 /// returns 80 341 uint width() 342 { 343 return 80; 344 } 345 346 /// 347 void write(string text, Context) 348 { 349 auto lines = text.count!(a => a == '\n'); 350 auto pieces = buffer.split("\n"); 351 352 auto newLines = text.split("\n"); 353 354 for (auto i = line; i < line + newLines.length; i++) 355 { 356 if (i != line) 357 { 358 charPos = 0; 359 } 360 361 while (i >= screen.length) 362 { 363 screen ~= ""; 364 } 365 366 auto newLine = newLines[i - line]; 367 368 if (charPos + newLine.length >= screen[i].length) 369 { 370 screen[i] = screen[i][0 .. charPos] ~ newLine; 371 } 372 else 373 { 374 screen[i] = screen[i][0 .. charPos] ~ newLine ~ screen[i][charPos + newLine.length .. $]; 375 } 376 charPos = charPos + newLine.length; 377 } 378 379 buffer = screen.join("\n"); 380 screen = buffer.split("\n"); 381 line += lines; 382 } 383 384 /// 385 void writeReverse(string text, Context c) 386 { 387 write(text, c); 388 } 389 390 /// 391 void writeln(string text, Context c) 392 { 393 write(text ~ '\n', c); 394 } 395 396 /// does nothing 397 void showCursor() 398 { 399 } 400 401 /// does nothing 402 void hideCursor() 403 { 404 } 405 } 406 407 version (unittest) 408 { 409 version(Have_fluent_asserts) { 410 import fluent.asserts; 411 } 412 } 413 414 @("Buffered writer should return an empty buffer") 415 unittest 416 { 417 auto writer = new BufferedWriter; 418 writer.buffer.should.equal(""); 419 } 420 421 @("Buffered writer should print text") 422 unittest 423 { 424 auto writer = new BufferedWriter; 425 writer.write("1", ReportWriter.Context._default); 426 writer.buffer.should.equal("1"); 427 } 428 429 @("Buffered writer should print text and add a new line") 430 unittest 431 { 432 auto writer = new BufferedWriter; 433 writer.write("1", ReportWriter.Context._default); 434 writer.writeln("2", ReportWriter.Context._default); 435 writer.buffer.should.equal("12\n"); 436 } 437 438 @("Buffered writer should print text and a new line") 439 unittest 440 { 441 auto writer = new BufferedWriter; 442 writer.writeln("1", ReportWriter.Context._default); 443 writer.write("2", ReportWriter.Context._default); 444 writer.buffer.should.equal("1\n2"); 445 } 446 447 @("Buffered writer should go back 1 line") 448 unittest 449 { 450 auto writer = new BufferedWriter; 451 writer.writeln("1", ReportWriter.Context._default); 452 writer.writeln("2", ReportWriter.Context._default); 453 writer.goTo(2); 454 writer.writeln("3", ReportWriter.Context._default); 455 writer.buffer.should.equal("3\n2\n"); 456 } 457 458 @("Buffered writer should not replace a line if the new text is shorter") 459 unittest 460 { 461 auto writer = new BufferedWriter; 462 writer.writeln("11", ReportWriter.Context._default); 463 writer.writeln("2", ReportWriter.Context._default); 464 writer.goTo(2); 465 writer.writeln("3", ReportWriter.Context._default); 466 writer.buffer.should.equal("31\n2\n"); 467 } 468 469 @("Buffered writer should keep the old line number") 470 unittest 471 { 472 auto writer = new BufferedWriter; 473 writer.writeln("1", ReportWriter.Context._default); 474 writer.writeln("2", ReportWriter.Context._default); 475 writer.goTo(2); 476 writer.writeln("", ReportWriter.Context._default); 477 writer.writeln("3", ReportWriter.Context._default); 478 writer.buffer.should.equal("1\n3\n"); 479 } 480 481 @("Buffered writer should keep the old line char position") 482 unittest 483 { 484 auto writer = new BufferedWriter; 485 writer.writeln("1", ReportWriter.Context._default); 486 writer.writeln("2", ReportWriter.Context._default); 487 writer.goTo(2); 488 writer.write("3", ReportWriter.Context._default); 489 writer.write("3", ReportWriter.Context._default); 490 writer.buffer.should.equal("33\n2\n"); 491 }