1020304050607080901001101201301401501601701801902002102202302402502602702802903003103203303403503603703803904004104204304404504604704804905005105205305405505605705805906006106206306406506606706806907007107207307407507607707807908008108208308408508608708808909009109209309409509609709809901000101010201030104010501060107010801090110011101120113011401150116011701180119012001210122012301240125012601270128012901300131013201330134013501360137013811390140014101420143014401450146014701480149015001510152015301540155015601570158015911600161116211630164116501660167016801696060170017101720173017430301753030176017730301780179106318010631811063182018322118422118522118601872211882211892211900191419241934194019511961197119801995200520152020203151520415152050206020702080209021002110212021302140215021602170218021902200221022202230224022502260227022802290230023102320233023402350236023702380239024002410242024302440245024602470248024902500251151525202530254025502560257025802590260026102620263026402650266026702681525826902701515271027215152731515274151527515152760277027802790280028102820283028402850286028702880289029002910292029334229402950296029702980299030003010302030303040305030603070308030903100311031203130314031503160317031803190320032103220323032403250326032703280329033003310332033303340335033650337503380339034003410342034322344034503460347034803495311350491351035249135303542310355035666435703582383590360036187536203632113640365036666436703686643690370576371037203730374883750376664377037803794913804913814913820383038403850386038703880389039003910392039318039403950396039703980399040004010402040304040405040604070408040904100411041204130414041504160417141824190420042104220423042414251426242704280429043004310432143314341435243604370438043904400441144214431444244504460447044804490450145114521453145414552456045704580459046004611462146314641465146624670468046904700471047214731474147514761477147824790480048104820483048414851486148714881489149024910 /++ A module containing utilities for presenting information to the user Copyright: © 2017 Szabo Bogdan License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Szabo Bogdan +/ module trial.reporters.writer; import std.stdio; import std.algorithm; import std.string; /// The default writer is initialized at the test run initialization with the right /// class, depending on the hosts capabilities. ReportWriter defaultWriter; /// The writer interface is used to present information to the user. interface ReportWriter { /// The information type. /// Convey meaning through color with a handful of emphasis utility classes. enum Context { /// Some important information active, /// Less important information inactive, /// success, /// Something that the user should notice info, /// Something that the user should be aware of warning, /// Something that the user must notice danger, /// _default } /// Go back a few lines void goTo(int); /// Write a string void write(string, Context = Context.active); /// Write a string with reversed colors void writeReverse(string, Context = Context.active); /// Write a string and go to a new line void writeln(string, Context = Context.active); /// Show the cursor from user void showCursor(); /// Hide the cursor from user void hideCursor(); /// Get how many characters you can print on a line uint width(); } /// The console writer outputs data to the standard output. It does not /// support colors and cursor moving. /// This is the default writer if arsd.terminal is not present. class ConsoleWriter : ReportWriter { /// not supported void goTo(int) { } /// void write(string text, Context) { std.stdio.write(text); } /// void writeReverse(string text, Context) { std.stdio.write(text); } /// void writeln(string text, Context) { std.stdio.writeln(text); } /// not supported void showCursor() { } /// not supported void hideCursor() { } /// returns 80 uint width() { return 80; } } import trial.terminal; shared static this() { version (Windows) { import core.sys.windows.windows; SetConsoleCP(65001); SetConsoleOutputCP(65001); auto consoleType = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); if(consoleType == 2) { writeln("using the color console."); defaultWriter = new ColorConsoleWriter; } else { writeln("using the standard console."); defaultWriter = new ConsoleWriter; } std.stdio.stdout.flush; } else { defaultWriter = new ColorConsoleWriter; } } /// This writer uses arsd.terminal and it's used if you add this dependency to your project /// It supports all the features and you should use it if you want to get the best experience /// from this project class ColorConsoleWriter : ReportWriter { private { int[string] cues; Terminal terminal; int lines = 0; bool movedToBottom = false; Context currentContext = Context._default; bool isReversed = false; } this() { this.terminal = Terminal(ConsoleOutputType.linear); this.terminal._suppressDestruction = true; lines = this.terminal.cursorY; } void setColor(Context context) { if (!isReversed && context == currentContext) { return; } isReversed = false; currentContext = context; switch (context) { case Context.active: terminal.color(Color.white | Bright, Color.DEFAULT); break; case Context.inactive: terminal.color(Color.black | Bright, Color.DEFAULT); break; case Context.success: terminal.color(Color.green | Bright, Color.DEFAULT); break; case Context.info: terminal.color(Color.cyan, Color.DEFAULT); break; case Context.warning: terminal.color(Color.yellow, Color.DEFAULT); break; case Context.danger: terminal.color(Color.red, Color.DEFAULT); break; default: terminal.reset(); } } void setColorReverse(Context context) { if (!isReversed && context == currentContext) { return; } currentContext = context; isReversed = true; switch (context) { case Context.active: terminal.color(Color.DEFAULT, Color.white | Bright); break; case Context.inactive: terminal.color(Color.DEFAULT, Color.black | Bright); break; case Context.success: terminal.color(Color.DEFAULT, Color.green | Bright); break; case Context.info: terminal.color(Color.DEFAULT, Color.cyan); break; case Context.warning: terminal.color(Color.DEFAULT, Color.yellow); break; case Context.danger: terminal.color(Color.DEFAULT, Color.red); break; default: terminal.reset(); } } void resetColor() { setColor(Context._default); } /// Go up `y` lines void goTo(int y) { if (!movedToBottom) { movedToBottom = true; terminal.moveTo(0, terminal.height - 1); } terminal.moveTo(0, terminal.cursorY - y, ForceOption.alwaysSend); } /// writes a string void write(string text, Context context) { lines += text.count!(a => a == '\n'); setColor(context); terminal.write(text); terminal.flush; resetColor; terminal.flush; } /// writes a string with reversed colors void writeReverse(string text, Context context) { lines += text.count!(a => a == '\n'); setColorReverse(context); terminal.write(text); resetColor; terminal.flush; } /// writes a string and go to a new line void writeln(string text, Context context) { this.write(text ~ "\n", context); } /// show the terminal cursor void showCursor() { terminal.showCursor; } /// hide the terminal cursor void hideCursor() { terminal.hideCursor; } /// returns the terminal width uint width() { return terminal.width; } } /// You can use this writer if you don't want to keep the data in memmory /// It's useful for unit testing. It supports line navigation, with no color /// The context info might be added in the future, once a good format is found. class BufferedWriter : ReportWriter { /// The buffer used to write the data string buffer = ""; private { size_t line = 0; size_t charPos = 0; bool replace; string[] screen; } /// go uo y lines void goTo(int y) { line = line - y; charPos = 0; } /// returns 80 uint width() { return 80; } /// void write(string text, Context) { auto lines = text.count!(a => a == '\n'); auto pieces = buffer.split("\n"); auto newLines = text.split("\n"); for (auto i = line; i < line + newLines.length; i++) { if (i != line) { charPos = 0; } while (i >= screen.length) { screen ~= ""; } auto newLine = newLines[i - line]; if (charPos + newLine.length >= screen[i].length) { screen[i] = screen[i][0 .. charPos] ~ newLine; } else { screen[i] = screen[i][0 .. charPos] ~ newLine ~ screen[i][charPos + newLine.length .. $]; } charPos = charPos + newLine.length; } buffer = screen.join("\n"); screen = buffer.split("\n"); line += lines; } /// void writeReverse(string text, Context c) { write(text, c); } /// void writeln(string text, Context c) { write(text ~ '\n', c); } /// does nothing void showCursor() { } /// does nothing void hideCursor() { } } version (unittest) { version(Have_fluent_asserts) { import fluent.asserts; } } @("Buffered writer should return an empty buffer") unittest { auto writer = new BufferedWriter; writer.buffer.should.equal(""); } @("Buffered writer should print text") unittest { auto writer = new BufferedWriter; writer.write("1", ReportWriter.Context._default); writer.buffer.should.equal("1"); } @("Buffered writer should print text and add a new line") unittest { auto writer = new BufferedWriter; writer.write("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.buffer.should.equal("12\n"); } @("Buffered writer should print text and a new line") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.write("2", ReportWriter.Context._default); writer.buffer.should.equal("1\n2"); } @("Buffered writer should go back 1 line") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.writeln("3", ReportWriter.Context._default); writer.buffer.should.equal("3\n2\n"); } @("Buffered writer should not replace a line if the new text is shorter") unittest { auto writer = new BufferedWriter; writer.writeln("11", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.writeln("3", ReportWriter.Context._default); writer.buffer.should.equal("31\n2\n"); } @("Buffered writer should keep the old line number") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.writeln("", ReportWriter.Context._default); writer.writeln("3", ReportWriter.Context._default); writer.buffer.should.equal("1\n3\n"); } @("Buffered writer should keep the old line char position") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.write("3", ReportWriter.Context._default); writer.write("3", ReportWriter.Context._default); writer.buffer.should.equal("33\n2\n"); }
/++ A module containing utilities for presenting information to the user Copyright: © 2017 Szabo Bogdan License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Szabo Bogdan +/ module trial.reporters.writer; import std.stdio; import std.algorithm; import std.string; /// The default writer is initialized at the test run initialization with the right /// class, depending on the hosts capabilities. ReportWriter defaultWriter; /// The writer interface is used to present information to the user. interface ReportWriter { /// The information type. /// Convey meaning through color with a handful of emphasis utility classes. enum Context { /// Some important information active, /// Less important information inactive, /// success, /// Something that the user should notice info, /// Something that the user should be aware of warning, /// Something that the user must notice danger, /// _default } /// Go back a few lines void goTo(int); /// Write a string void write(string, Context = Context.active); /// Write a string with reversed colors void writeReverse(string, Context = Context.active); /// Write a string and go to a new line void writeln(string, Context = Context.active); /// Show the cursor from user void showCursor(); /// Hide the cursor from user void hideCursor(); /// Get how many characters you can print on a line uint width(); } /// The console writer outputs data to the standard output. It does not /// support colors and cursor moving. /// This is the default writer if arsd.terminal is not present. class ConsoleWriter : ReportWriter { /// not supported void goTo(int) { } /// void write(string text, Context) { std.stdio.write(text); } /// void writeReverse(string text, Context) { std.stdio.write(text); } /// void writeln(string text, Context) { std.stdio.writeln(text); } /// not supported void showCursor() { } /// not supported void hideCursor() { } /// returns 80 uint width() { return 80; } } import trial.terminal; shared static this() { version (Windows) { import core.sys.windows.windows; SetConsoleCP(65001); SetConsoleOutputCP(65001); auto consoleType = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); if(consoleType == 2) { writeln("using the color console."); defaultWriter = new ColorConsoleWriter; } else { writeln("using the standard console."); defaultWriter = new ConsoleWriter; } std.stdio.stdout.flush; } else { defaultWriter = new ColorConsoleWriter; } } /// This writer uses arsd.terminal and it's used if you add this dependency to your project /// It supports all the features and you should use it if you want to get the best experience /// from this project class ColorConsoleWriter : ReportWriter { private { int[string] cues; Terminal terminal; int lines = 0; bool movedToBottom = false; Context currentContext = Context._default; bool isReversed = false; } this() { this.terminal = Terminal(ConsoleOutputType.linear); this.terminal._suppressDestruction = true; lines = this.terminal.cursorY; } void setColor(Context context) { if (!isReversed && context == currentContext) { return; } isReversed = false; currentContext = context; switch (context) { case Context.active: terminal.color(Color.white | Bright, Color.DEFAULT); break; case Context.inactive: terminal.color(Color.black | Bright, Color.DEFAULT); break; case Context.success: terminal.color(Color.green | Bright, Color.DEFAULT); break; case Context.info: terminal.color(Color.cyan, Color.DEFAULT); break; case Context.warning: terminal.color(Color.yellow, Color.DEFAULT); break; case Context.danger: terminal.color(Color.red, Color.DEFAULT); break; default: terminal.reset(); } } void setColorReverse(Context context) { if (!isReversed && context == currentContext) { return; } currentContext = context; isReversed = true; switch (context) { case Context.active: terminal.color(Color.DEFAULT, Color.white | Bright); break; case Context.inactive: terminal.color(Color.DEFAULT, Color.black | Bright); break; case Context.success: terminal.color(Color.DEFAULT, Color.green | Bright); break; case Context.info: terminal.color(Color.DEFAULT, Color.cyan); break; case Context.warning: terminal.color(Color.DEFAULT, Color.yellow); break; case Context.danger: terminal.color(Color.DEFAULT, Color.red); break; default: terminal.reset(); } } void resetColor() { setColor(Context._default); } /// Go up `y` lines void goTo(int y) { if (!movedToBottom) { movedToBottom = true; terminal.moveTo(0, terminal.height - 1); } terminal.moveTo(0, terminal.cursorY - y, ForceOption.alwaysSend); } /// writes a string void write(string text, Context context) { lines += text.count!(a => a == '\n'); setColor(context); terminal.write(text); terminal.flush; resetColor; terminal.flush; } /// writes a string with reversed colors void writeReverse(string text, Context context) { lines += text.count!(a => a == '\n'); setColorReverse(context); terminal.write(text); resetColor; terminal.flush; } /// writes a string and go to a new line void writeln(string text, Context context) { this.write(text ~ "\n", context); } /// show the terminal cursor void showCursor() { terminal.showCursor; } /// hide the terminal cursor void hideCursor() { terminal.hideCursor; } /// returns the terminal width uint width() { return terminal.width; } } /// You can use this writer if you don't want to keep the data in memmory /// It's useful for unit testing. It supports line navigation, with no color /// The context info might be added in the future, once a good format is found. class BufferedWriter : ReportWriter { /// The buffer used to write the data string buffer = ""; private { size_t line = 0; size_t charPos = 0; bool replace; string[] screen; } /// go uo y lines void goTo(int y) { line = line - y; charPos = 0; } /// returns 80 uint width() { return 80; } /// void write(string text, Context) { auto lines = text.count!(a => a == '\n'); auto pieces = buffer.split("\n"); auto newLines = text.split("\n"); for (auto i = line; i < line + newLines.length; i++) { if (i != line) { charPos = 0; } while (i >= screen.length) { screen ~= ""; } auto newLine = newLines[i - line]; if (charPos + newLine.length >= screen[i].length) { screen[i] = screen[i][0 .. charPos] ~ newLine; } else { screen[i] = screen[i][0 .. charPos] ~ newLine ~ screen[i][charPos + newLine.length .. $]; } charPos = charPos + newLine.length; } buffer = screen.join("\n"); screen = buffer.split("\n"); line += lines; } /// void writeReverse(string text, Context c) { write(text, c); } /// void writeln(string text, Context c) { write(text ~ '\n', c); } /// does nothing void showCursor() { } /// does nothing void hideCursor() { } } version (unittest) { version(Have_fluent_asserts) { import fluent.asserts; } } @("Buffered writer should return an empty buffer") unittest { auto writer = new BufferedWriter; writer.buffer.should.equal(""); } @("Buffered writer should print text") unittest { auto writer = new BufferedWriter; writer.write("1", ReportWriter.Context._default); writer.buffer.should.equal("1"); } @("Buffered writer should print text and add a new line") unittest { auto writer = new BufferedWriter; writer.write("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.buffer.should.equal("12\n"); } @("Buffered writer should print text and a new line") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.write("2", ReportWriter.Context._default); writer.buffer.should.equal("1\n2"); } @("Buffered writer should go back 1 line") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.writeln("3", ReportWriter.Context._default); writer.buffer.should.equal("3\n2\n"); } @("Buffered writer should not replace a line if the new text is shorter") unittest { auto writer = new BufferedWriter; writer.writeln("11", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.writeln("3", ReportWriter.Context._default); writer.buffer.should.equal("31\n2\n"); } @("Buffered writer should keep the old line number") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.writeln("", ReportWriter.Context._default); writer.writeln("3", ReportWriter.Context._default); writer.buffer.should.equal("1\n3\n"); } @("Buffered writer should keep the old line char position") unittest { auto writer = new BufferedWriter; writer.writeln("1", ReportWriter.Context._default); writer.writeln("2", ReportWriter.Context._default); writer.goTo(2); writer.write("3", ReportWriter.Context._default); writer.write("3", ReportWriter.Context._default); writer.buffer.should.equal("33\n2\n"); }