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 }