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 }