1 /** 2 A library for creating pleasant command-line output for repetitive tasks, 3 where a callback must iterate over an array of arguments. 4 5 This library assumes support for ANSI Escape Codes. If your terminal does 6 not support ANSI Escape Codes, then tough luck. 7 8 Authors: 9 $(UL 10 $(LI $(PERSON Jonathan M. Wilbur, jonathan@wilbur.space, http://jonathan.wilbur.space)) 11 ) 12 Copyright: Copyright (C) Jonathan M. Wilbur 13 License: $(LINK https://mit-license.org/, MIT License) 14 See_Also: 15 $(LINK2 https://en.wikipedia.org/wiki/ANSI_escape_code, ANSI Escape Codes) 16 */ 17 module idiot; 18 import core.thread : Thread; 19 import core.time : dur, Duration; 20 import std.array : appender, Appender, replace; 21 import std.conv : text; 22 import std.datetime.date : DateTime; 23 import std.datetime.stopwatch : AutoStart, StopWatch; 24 import std.datetime.systime : Clock, SysTime; 25 import std.format : formattedWrite; 26 import std.random : uniform; 27 import std.stdio : stdout, write, writeln, writefln; 28 29 immutable string ANSI_BLUE = "\x1B[34m"; 30 immutable string ANSI_CYAN = "\x1B[36m"; 31 immutable string ANSI_GREEN = "\x1B[32m"; 32 immutable string ANSI_YELLOW = "\x1B[33m"; 33 immutable string ANSI_RED = "\x1B[31m"; 34 immutable string ANSI_ERROR = "\x1B[33;41m"; 35 immutable string ANSI_RESET = "\x1B[0m"; 36 immutable string ANSI_DELETE_LINE = "\x1B[2K\r"; 37 38 // TODO: Ctrl-C signal reception: https://forum.dlang.org/post/hzzbypitcpvssxohrkmg@forum.dlang.org 39 40 /// 41 public 42 enum IdiotIterationStatus : ubyte 43 { 44 notStarted, 45 waiting, 46 working, 47 success, 48 failure, 49 errored 50 } 51 52 /// 53 public 54 struct IdiotIteration(T) 55 { 56 IdiotIterationStatus status; 57 SysTime startTime; 58 SysTime endTime; 59 Duration duration; 60 T argument; 61 } 62 63 /// 64 public 65 struct IdiotRun(T) 66 { 67 SysTime startTime; 68 SysTime endTime; 69 Duration duration; 70 size_t success; 71 size_t failure; 72 size_t errored; 73 IdiotIteration!(T)[] iterations; 74 } 75 76 /// 77 public 78 class Idiot(T) 79 { 80 // Output configuration 81 public bool showFraction = true; 82 public bool showPercent = true; 83 public bool showMarginalTime = true; 84 public bool showCumulativeTime = true; 85 86 // Execution configuration 87 public bool continueOnSuccess = true; // if false, only executes until success occurs 88 public bool continueOnFailure = true; 89 public bool continueOnException = true; 90 public bool delegate (size_t, T[]) callback; 91 public size_t millisecondsToPauseInBetweenIterations = 0u; 92 public size_t maximumMillisecondsOfAdditionalRandomPause = 0u; 93 94 // Execution history 95 IdiotRun!(T)[] runs; 96 97 /// Constructor that accepts a delegate 98 public nothrow @safe 99 this (in bool delegate (size_t, T[]) callback) 100 { 101 this.callback = callback; 102 } 103 104 /// Constructor that accepts a function 105 public nothrow @system 106 this (in bool function (size_t, T[]) callback) 107 { 108 import std.functional : toDelegate; 109 this.callback = toDelegate(callback); 110 } 111 112 /// Executes the callback over all elements in the supplied $(D arguments) array. 113 public 114 void execute(T[] arguments) 115 { 116 this.runs ~= IdiotRun!(T)(); 117 this.runs[$-1].startTime = Clock.currTime(); 118 StopWatch cumulativeStopWatch = StopWatch(AutoStart.yes); 119 for (size_t i = 0u; i < arguments.length; i++) 120 { 121 this.runs[$-1].iterations ~= IdiotIteration!(T)(); 122 this.runs[$-1].iterations[$-1].startTime = Clock.currTime(); 123 StopWatch iterationStopWatch = StopWatch(AutoStart.yes); 124 125 this.runs[$-1].iterations[$-1].status = IdiotIterationStatus.waiting; 126 write 127 ( 128 "[ " ~ ANSI_CYAN ~ "WAITING" ~ ANSI_RESET ~ " ]", 129 this.fraction(i+1, arguments.length), 130 this.percent(i+1, arguments.length), 131 this.marginalTime(), 132 this.cumulativeTime(cumulativeStopWatch.peek()), 133 "\t", 134 text(arguments[i]) 135 ); 136 137 /* NOTE: 138 Standard Output (stdout) is buffered on Windows terminals, and 139 flushed when a newline is encountered, so flush() is necessary 140 when you are trying to write less than a full line. 141 */ 142 version (Windows) stdout.flush(); // REVIEW: I believe this needs to change on Linux too... 143 144 size_t millisecondsToPause = 145 this.millisecondsToPauseInBetweenIterations + 146 uniform(0u, this.maximumMillisecondsOfAdditionalRandomPause); 147 Thread currentThread = Thread.getThis(); 148 currentThread.sleep(dur!("msecs")(millisecondsToPause)); 149 150 this.runs[$-1].iterations[$-1].status = IdiotIterationStatus.working; 151 write 152 ( 153 ANSI_DELETE_LINE, 154 "[ " ~ ANSI_YELLOW ~ "WORKING" ~ ANSI_RESET ~ " ]", 155 this.fraction(i+1, arguments.length), 156 this.percent(i+1, arguments.length), 157 this.marginalTime(), 158 this.cumulativeTime(cumulativeStopWatch.peek()), 159 "\t", 160 text(arguments[i]) 161 ); 162 163 /* NOTE: 164 Standard Output (stdout) is buffered on Windows terminals, and 165 flushed when a newline is encountered, so flush() is necessary 166 when you are trying to write less than a full line. 167 */ 168 version (Windows) stdout.flush(); // REVIEW: I believe this needs to change on Linux too... 169 170 bool result; 171 try 172 { 173 result = callback(i, arguments); 174 } 175 catch (Exception e) 176 { 177 if (this.continueOnException) 178 { 179 this.runs[$-1].iterations[$-1].status = IdiotIterationStatus.errored; 180 this.runs[$-1].errored++; 181 182 writeln 183 ( 184 ANSI_DELETE_LINE, 185 "[ " ~ ANSI_ERROR ~ "ERRORED" ~ ANSI_RESET ~ " ]", 186 this.fraction(i+1, arguments.length), 187 this.percent(i+1, arguments.length), 188 this.marginalTime(), 189 this.cumulativeTime(cumulativeStopWatch.peek()), 190 "\t", 191 text(arguments[i]) 192 ); 193 continue; 194 } 195 else throw e; 196 } 197 finally 198 { 199 iterationStopWatch.stop(); 200 this.runs[$-1].iterations[$-1].duration = iterationStopWatch.peek(); 201 } 202 203 if (result) // SUCCESS 204 { 205 this.runs[$-1].iterations[$-1].status = IdiotIterationStatus.success; 206 this.runs[$-1].success++; 207 208 writeln 209 ( 210 ANSI_DELETE_LINE, 211 "[ " ~ ANSI_GREEN ~ "SUCCESS" ~ ANSI_RESET ~ " ]", 212 this.fraction(i+1, arguments.length), 213 this.percent(i+1, arguments.length), 214 this.marginalTime(this.runs[$-1].iterations[$-1].duration), 215 this.cumulativeTime(cumulativeStopWatch.peek()), 216 "\t", 217 text(arguments[i]) 218 ); 219 if (!this.continueOnSuccess) break; 220 } 221 else // FAILURE 222 { 223 this.runs[$-1].iterations[$-1].status = IdiotIterationStatus.failure; 224 this.runs[$-1].failure++; 225 226 writeln 227 ( 228 ANSI_DELETE_LINE, 229 "[ " ~ ANSI_RED ~ "FAILURE" ~ ANSI_RESET ~ " ]", 230 this.fraction(i+1, arguments.length), 231 this.percent(i+1, arguments.length), 232 this.marginalTime(this.runs[$-1].iterations[$-1].duration), 233 this.cumulativeTime(cumulativeStopWatch.peek()), 234 "\t", 235 text(arguments[i]) 236 ); 237 if (!this.continueOnFailure) break; 238 } 239 } 240 cumulativeStopWatch.stop(); 241 242 // Save 243 this.runs[$-1].duration = cumulativeStopWatch.peek(); 244 this.runs[$-1].endTime = Clock.currTime(); 245 246 this.writeEndOfRunReport(); 247 } 248 249 private 250 string fraction (size_t numerator, size_t denominator) 251 { 252 if (!this.showFraction) return ""; 253 import std.math : floor, log10; 254 size_t digitsNeeded = cast(size_t) (cast(real) denominator).log10().floor()+1; 255 256 Appender!string writer = appender!string(); 257 string formatString = " [ %" ~ text(digitsNeeded) ~ "d / %d ]"; 258 formattedWrite(writer, formatString, numerator, denominator); 259 return writer.data; 260 } 261 262 private 263 string percent (size_t numerator, size_t denominator) 264 { 265 if (!this.showPercent) return ""; 266 float percent = ((cast(float) numerator / cast(float) denominator) * 100.0); 267 268 Appender!string writer = appender!string(); 269 formattedWrite(writer, " [ %8.4f%% ]", percent); 270 return writer.data; 271 } 272 273 private 274 string marginalTime (Duration duration = Duration()) 275 { 276 if (!this.showMarginalTime) return ""; 277 278 bool fasterThanLastIteration = 279 ( 280 (this.runs[$-1].iterations.length >= 2u) && 281 (duration > this.runs[$-1].iterations[$-2].duration) 282 ) ? true : false; 283 284 ubyte hours; 285 ubyte minutes; 286 ubyte seconds; 287 this.runs[$-1].duration.split!("hours", "minutes", "seconds")(hours, minutes, seconds); 288 Appender!string writer = appender!string(); 289 if (fasterThanLastIteration) 290 formattedWrite(writer, " [ MT %s%02d:%02d:%02d%s ]", ANSI_CYAN, hours, minutes, seconds, ANSI_RESET); 291 else 292 formattedWrite(writer, " [ MT %s%02d:%02d:%02d%s ]", ANSI_BLUE, hours, minutes, seconds, ANSI_RESET); 293 return writer.data; 294 } 295 296 private 297 string cumulativeTime (Duration duration = Duration()) 298 { 299 if (!this.showCumulativeTime) return ""; 300 301 ushort days; 302 ubyte hours; 303 ubyte minutes; 304 ubyte seconds; 305 duration.split!("days", "hours", "minutes", "seconds")(days, hours, minutes, seconds); 306 Appender!string writer = appender!string(); 307 formattedWrite(writer, " [ CT %04d:%02d:%02d:%02d ]", days, hours, minutes, seconds); 308 return writer.data; 309 } 310 311 private 312 void writeEndOfRunReport () 313 { 314 writeln(); 315 316 // Print start and end times 317 writeln("Started: ", (cast(DateTime) this.runs[$-1].startTime).toSimpleString()); 318 writeln("Ended: ", (cast(DateTime) this.runs[$-1].endTime).toSimpleString()); 319 320 // Print total time 321 ushort days; 322 ubyte hours; 323 ubyte minutes; 324 ubyte seconds; 325 ushort milliseconds; 326 this.runs[$-1].duration.split!("days", "hours", "minutes", "seconds", "msecs")(days, hours, minutes, seconds, milliseconds); 327 writefln("Total Time taken: %d Days, %d Hours, %d Minutes, %d Seconds, %d Milliseconds", days, hours, minutes, seconds, milliseconds); 328 329 // Average time 330 long averageMilliseconds = this.runs[$-1].duration.total!"msecs" / this.runs[$-1].iterations.length; 331 if (averageMilliseconds > 86_400_000u) 332 writeln("Average time per iteration: ", (averageMilliseconds / 3_600_000u), " Hours"); 333 else if (averageMilliseconds > 3_600_000u) // Display in minutes 334 writeln("Average time per iteration: ", (averageMilliseconds / 60_000u), " Minutes"); 335 else if (averageMilliseconds > 60_000u) // Display in seconds 336 writeln("Average time per iteration: ", (averageMilliseconds / 1_000u), " Seconds"); 337 else 338 writeln("Average time per iteration: ", (averageMilliseconds), " Milliseconds"); 339 340 // Pass / Fail 341 writeln("Successful iterations: ", this.runs[$-1].success); 342 writeln("Failed iterations: ", this.runs[$-1].failure); 343 writeln("Errored iterations: ", this.runs[$-1].errored); 344 writeln("Successful Rate: ", (cast(float) (this.runs[$-1].success) / cast(float) (this.runs[$-1].iterations.length)) * 100.0, "%"); 345 } 346 }