1 /* 2 * hunt-console eases the creation of beautiful and testable command line interfaces. 3 * 4 * Copyright (C) 2018-2019, HuntLabs 5 * 6 * Website: https://www.huntlabs.net 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.console.helper.ProgressBar; 13 14 import hunt.console.error.LogicException; 15 import hunt.console.output.Output; 16 import hunt.console.output.Verbosity; 17 import hunt.console.util.StringUtils; 18 19 import hunt.Exceptions; 20 import hunt.collection.HashMap; 21 import hunt.collection.Map; 22 import hunt.console.helper.PlaceholderFormatter; 23 import hunt.util.DateTime; 24 import hunt.math.Helper; 25 import hunt.console.helper.AbstractHelper; 26 import hunt.console.output.Verbosity; 27 28 import std.regex; 29 import std.string; 30 import std.conv; 31 import hunt.util.StringBuilder; 32 33 class ProgressBar 34 { 35 // options 36 private int barWidth = 28; 37 private string barChar; 38 private string emptyBarChar = "-"; 39 private string progressChar = ">"; 40 private string format = null; 41 private int redrawFreq = 1; 42 43 private Output output; 44 private int step = 0; 45 private int max; 46 private long startTime; 47 private int stepWidth; 48 private float percent = 0f; 49 private int lastMessagesLength = 0; 50 private int formatLineCount; 51 private Map!(string, string) messages ; 52 private bool _overwrite = true; 53 54 private static Map!(string, PlaceholderFormatter) formatters; 55 private static Map!(string, string) formats; 56 57 public this(Output output) 58 { 59 messages = new HashMap!(string, string)(); 60 this(output, 0); 61 } 62 63 public this(Output output, int max) 64 { 65 this.output = output; 66 setMaxSteps(max); 67 68 if (!this.output.isDecorated()) { 69 // disable _overwrite when output does not support ANSI codes. 70 _overwrite = false; 71 72 if (this.max > 10) { 73 // set a reasonable redraw frequency so output isn't flooded 74 setRedrawFrequency(max / 10); 75 } 76 } 77 78 setFormat(determineBestFormat()); 79 80 startTime = DateTime.currentTimeMillis(); 81 } 82 83 public static void setPlaceholderFormatter(string name, PlaceholderFormatter formatter) 84 { 85 if (formatters is null) { 86 formatters = initPlaceholderFormatters(); 87 } 88 89 formatters.put(name, formatter); 90 } 91 92 public static PlaceholderFormatter getPlaceholderFormatter(string name) 93 { 94 if (formatters is null) { 95 formatters = initPlaceholderFormatters(); 96 } 97 98 return formatters.get(name); 99 } 100 101 public static void setFormatDefinition(string name, string format) 102 { 103 if (formats is null) { 104 formats = initFormats(); 105 } 106 107 formats.put(name, format); 108 } 109 110 public static string getFormatDefinition(string name) 111 { 112 if (formats is null) { 113 formats = initFormats(); 114 } 115 116 return formats.get(name); 117 } 118 119 public void setMessage(string message) 120 { 121 setMessage(message, "message"); 122 } 123 124 public void setMessage(string message, string name) 125 { 126 messages.put(name, message); 127 } 128 129 public string getMessage() 130 { 131 return getMessage("message"); 132 } 133 134 public string getMessage(string name) 135 { 136 return messages.get(name); 137 } 138 139 public long getStartTime() 140 { 141 return startTime; 142 } 143 144 public int getMaxSteps() 145 { 146 return max; 147 } 148 149 public int getProgress() 150 { 151 return step; 152 } 153 154 public int getStepWidth() 155 { 156 return stepWidth; 157 } 158 159 public float getProgressPercent() 160 { 161 return percent; 162 } 163 164 public void setBarWidth(int barWidth) 165 { 166 this.barWidth = barWidth; 167 } 168 169 public int getBarWidth() 170 { 171 return barWidth; 172 } 173 174 public void setBarCharacter(string c) 175 { 176 barChar = c; 177 } 178 179 public string getBarCharacter() 180 { 181 if (barChar is null) { 182 return (max == int.init || max == 0) ? emptyBarChar : "="; 183 } 184 185 return barChar; 186 } 187 188 public void setEmptyBarCharacter(string c) 189 { 190 this.emptyBarChar = c; 191 } 192 193 public string getEmptyBarCharacter() 194 { 195 return emptyBarChar; 196 } 197 198 public void setProgressCharacter(string c) 199 { 200 this.progressChar = c; 201 } 202 203 public string getProgressCharacter() 204 { 205 return progressChar; 206 } 207 208 public void setFormat(string format) 209 { 210 // try to use the _nomax variant if available 211 if (max == int.init || max == 0 && getFormatDefinition(format ~ "_nomax") !is null) { 212 this.format = getFormatDefinition(format ~ "_nomax"); 213 } else if (getFormatDefinition(format) !is null) { 214 this.format = getFormatDefinition(format); 215 } else { 216 this.format = format; 217 } 218 219 formatLineCount = StringUtils.count(this.format, '\n'); 220 } 221 222 public void setRedrawFrequency(int freq) 223 { 224 this.redrawFreq = freq; 225 } 226 227 public void start() 228 { 229 start(int.init); 230 } 231 232 public void start(int max) 233 { 234 startTime = DateTime.currentTimeMillis(); 235 step = 0; 236 percent = 0f; 237 238 if (max != int.init) { 239 setMaxSteps(max); 240 } 241 242 display(); 243 } 244 245 public void advance() 246 { 247 advance(1); 248 } 249 250 public void advance(int step) 251 { 252 setProgress(this.step + step); 253 } 254 255 public void setCurrent(int step) 256 { 257 setProgress(step); 258 } 259 260 public void setOverwrite(bool overwrite) 261 { 262 this._overwrite = overwrite; 263 } 264 265 public void setProgress(int step) 266 { 267 if (step < this.step) { 268 throw new LogicException("You can't regress the progress bar."); 269 } 270 271 if (max != int.init && max > 0 && step > max) { 272 max = step; 273 } 274 275 int prevPeriod = this.step / redrawFreq; 276 int currPeriod = step / redrawFreq; 277 this.step = step; 278 percent = (max != int.init && max > 0) ? (cast(float) step) / max : 0f; 279 if (prevPeriod != currPeriod || (max != int.init && max == step)) { 280 display(); 281 } 282 } 283 284 public void finish() 285 { 286 if (max == int.init || max == 0) { 287 max = step; 288 } 289 290 if (step == max && !_overwrite) { 291 // prevent double 100% output 292 return; 293 } 294 295 setProgress(max); 296 } 297 298 public void display() 299 { 300 if (output.getVerbosity() == Verbosity.QUIET) { 301 return; 302 } 303 304 string pattern = ("%([a-z\\-_]+)(?:\\:([^%]+))?%"/* , Pattern.CASE_INSENSITIVE */); 305 auto matchers = matchAll(this.format,regex(pattern,"i")); 306 307 int startPos = 0; 308 string text = ""; 309 StringBuilder sb = new StringBuilder(); 310 foreach(matcher ; matchers) { 311 string format = matcher.captures[1]; 312 PlaceholderFormatter formatter = getPlaceholderFormatter(format); 313 if (formatter !is null) { 314 text = formatter.format(this, output); 315 } else if (format !is null) { 316 text = messages.get(format); 317 } 318 319 if (matcher.captures[2] !is null) { 320 text = std..string.format("%" ~ matcher.captures[2], text); 321 } 322 323 // matcher.appendReplacement(sb, text); 324 auto pos = cast(int)(this.format.indexOf(format)); 325 sb.append(this.format[startPos..pos].replace(format,text)); 326 startPos = pos + cast(int)(format.length); 327 } 328 // matcher.appendTail(sb); 329 sb.append(this.format[startPos .. $]); 330 331 overwrite(sb.toString()); 332 } 333 334 public void clear() 335 { 336 if (!_overwrite) { 337 return; 338 } 339 340 char[] array = new char[formatLineCount]; 341 // Arrays.fill(array, '\n'); 342 for(int i = 0 ; i<formatLineCount; i++ ) 343 array[i] = '\n'; 344 345 overwrite(cast(string)(array)); 346 } 347 348 public void setMaxSteps(int max) 349 { 350 this.max = MathHelper.max(0, max); 351 stepWidth = this.max > 0 ? cast(int)(max.to!string.length) : 4; 352 } 353 354 public void overwrite(string message) 355 { 356 string[] lines = StringUtils.split(message, '\n'); 357 358 // append whitespace to match the line's length 359 if (lastMessagesLength != int.init) { 360 for (int i = 0; i < lines.length; i++) { 361 if (lastMessagesLength > AbstractHelper.strlenWithoutDecoration(output.getFormatter(), lines[i])) { 362 lines[i] = StringUtils.padRight(lines[i], lastMessagesLength, " "); 363 } 364 } 365 } 366 367 if (_overwrite) { 368 // move back to the beginning of the progress bar before redrawing it 369 output.write("\r"); 370 } else if (step > 0) { 371 // move to new line 372 output.writeln(""); 373 } 374 375 if (formatLineCount > 0) { 376 output.write(std..string.format("\033[%dA", formatLineCount)); 377 } 378 output.write(StringUtils.join(lines, "\n")); 379 380 lastMessagesLength = 0; 381 foreach (string line ; lines) { 382 int len = AbstractHelper.strlenWithoutDecoration(output.getFormatter(), line); 383 if (len > lastMessagesLength) { 384 lastMessagesLength = len; 385 } 386 } 387 } 388 389 public string determineBestFormat() 390 { 391 switch (output.getVerbosity()) with(Verbosity){ 392 case VERBOSE: 393 return max == int.init || max == 0 ? "verbose_nomax" : "verbose"; 394 case VERY_VERBOSE: 395 return max == int.init || max == 0 ? "very_verbose_nomax" : "very_verbose"; 396 case DEBUG: 397 return max == int.init || max == 0 ? "debug_nomax" : "debug"; 398 default: 399 return max == int.init || max == 0 ? "normal_nomax" : "normal"; 400 } 401 } 402 403 public static Map!(string, PlaceholderFormatter) initPlaceholderFormatters() 404 { 405 Map!(string, PlaceholderFormatter) formatters = new HashMap!(string, PlaceholderFormatter)(); 406 407 formatters.put("bar", new class PlaceholderFormatter 408 { 409 override public string format(ProgressBar bar, Output output) 410 { 411 int completeBars = bar.getMaxSteps() > 0 ? cast(int) (bar.getProgressPercent() * bar.getBarWidth()) : bar.getProgress() % bar.getBarWidth(); 412 string display = StringUtils.padRight("", completeBars, bar.getBarCharacter()); 413 if (completeBars < bar.getBarWidth()) { 414 int emptyBars = bar.getBarWidth() - completeBars - AbstractHelper.strlenWithoutDecoration(output.getFormatter(), to!string(bar.getProgressCharacter())); 415 display ~= bar.getProgressCharacter() ~ StringUtils.padRight("", emptyBars, bar.getEmptyBarCharacter()); 416 } 417 418 return display; 419 } 420 }); 421 422 formatters.put("elapsed", new class PlaceholderFormatter 423 { 424 override public string format(ProgressBar bar, Output output) 425 { 426 import std.math; 427 return AbstractHelper.formatTime(cast(long)/* MathHelper. */round(( DateTime.currentTimeMillis() / 1000) - (bar.getStartTime() / 1000))); 428 } 429 }); 430 431 formatters.put("remaining", new class PlaceholderFormatter 432 { 433 override public string format(ProgressBar bar, Output output) 434 { 435 if (bar.getMaxSteps() == 0) { 436 throw new LogicException("Unable to display the remaining time if the maximum number of steps is not set."); 437 } 438 439 long remaining; 440 if (bar.getProgress() == 0) { 441 remaining = 0; 442 } else { 443 import std.math; 444 remaining = /* MathHelper. */cast(long)round(cast(float) ( DateTime.currentTimeMillis() / 1000 - bar.getStartTime() / 1000) / cast(float) bar.getProgress() * (cast(float) bar.getMaxSteps() - cast(float) bar.getProgress())); 445 } 446 447 return AbstractHelper.formatTime(remaining); 448 } 449 }); 450 451 formatters.put("estimated", new class PlaceholderFormatter 452 { 453 override public string format(ProgressBar bar, Output output) 454 { 455 if (bar.getMaxSteps() == 0) { 456 throw new LogicException("Unable to display the estimated time if the maximum number of steps is not set."); 457 } 458 459 long estimated; 460 if (bar.getProgress() == 0) { 461 estimated = 0; 462 } else { 463 import std.math; 464 estimated =/* MathHelper. */cast(long)round(cast(float) ( DateTime.currentTimeMillis() / 1000 - bar.getStartTime() / 1000) / cast(float) bar.getProgress() * cast(float) bar.getMaxSteps()); 465 } 466 467 return AbstractHelper.formatTime(estimated); 468 } 469 }); 470 471 formatters.put("memory", new class PlaceholderFormatter 472 { 473 override public string format(ProgressBar bar, Output output) 474 { 475 implementationMissing(); 476 return null; 477 // return AbstractHelper.formatMemory(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()); 478 } 479 }); 480 481 formatters.put("current", new class PlaceholderFormatter 482 { 483 override public string format(ProgressBar bar, Output output) 484 { 485 return StringUtils.padLeft(to!string(bar.getProgress()), bar.getStepWidth(), " "); 486 } 487 }); 488 489 formatters.put("max", new class PlaceholderFormatter 490 { 491 override public string format(ProgressBar bar, Output output) 492 { 493 return to!string(bar.getMaxSteps()); 494 } 495 }); 496 497 formatters.put("percent", new class PlaceholderFormatter 498 { 499 override public string format(ProgressBar bar, Output output) 500 { 501 return to!string(cast(int) (bar.getProgressPercent() * 100)); 502 } 503 }); 504 505 return formatters; 506 } 507 508 private static Map!(string, string) initFormats() 509 { 510 Map!(string, string) formats = new HashMap!(string, string)(); 511 512 formats.put("normal", " %current%/%max% [%bar%] %percent:3s%%"); 513 formats.put("normal_nomax", " %current% [%bar%]"); 514 515 formats.put("verbose", " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%"); 516 formats.put("verbose_nomax", " %current% [%bar%] %elapsed:6s%"); 517 518 formats.put("very_verbose", " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%"); 519 formats.put("very_verbose_nomax", " %current% [%bar%] %elapsed:6s%"); 520 521 formats.put("debug", " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%"); 522 formats.put("debug_nomax", " %current% [%bar%] %elapsed:6s% %memory:6s%"); 523 524 return formats; 525 } 526 }