1 module snck;
2 
3 import std.range.primitives : isInputRange;
4 import std.stdio : File, stderr;
5 
6 struct SnckConf {
7     double minSeconds = 0.1;
8     bool showPercent = true;
9     bool showCounter = true;
10     bool showProgressBar = true;
11     size_t barBlocks = 10;
12     bool showElapsedTime = true;
13     bool showETA = true;
14     bool showSpeed = true;
15     bool eraseLast = true;
16 
17     @property
18     bool showAnyTimeStats() {
19         return showElapsedTime || showETA || showSpeed;
20     }
21 }
22 
23 struct Snck(R) if (isInputRange!R) {
24     import std.range.primitives : ElementType, hasLength;
25     import std.array; // : empty, front, popFront;
26     import std.conv : to;
27     import std.datetime.stopwatch;
28 
29     R range;
30     StopWatch watch = StopWatch(AutoStart.no);
31     alias E = ElementType!R;
32     size_t count = 0;
33     Duration previous;
34     SnckConf conf;
35     File file;
36 
37     enum rewriteLine = "\r\033[K";
38 
39     this(R range) {
40         this.range = range;
41         this.watch.start();
42     }
43 
44     @property
45     ref output() {
46         if (!this.file.isOpen) {
47             this.file = stderr;
48         }
49         return this.file;
50     }
51 
52     @property
53     ref output(File f) {
54         this.file = f;
55         return this;
56     }
57 
58     @property empty() const {
59         return range.empty;
60     }
61 
62     auto front() {
63         return range.front();
64     }
65 
66     void popFront() {
67         range.popFront;
68         ++count;
69 
70         with (this.conf) {
71             // prevent too frequent message
72             auto now = watch.peek;
73             auto secs = (now - previous).total!"nsecs" * 1e-9;
74             if (!range.empty && secs < minSeconds) return;
75             output.write(this.rewriteLine);
76 
77             static if (hasLength!R) {
78                 auto total = count + range.length;
79             }
80 
81             if (showPercent) {
82                 static if (hasLength!R) {
83                     output.writef!"%3d%s: "(100 * count / total, "%");
84                 }
85                 output.writef!"%d"(this.count);
86                 static if (hasLength!R) {
87                     output.writef!"/%d"(total);
88                 }
89             }
90 
91             if (showProgressBar) {
92                 static if (hasLength!R) {
93                     output.write("|");
94                     auto passed = barBlocks * count / total;
95                     foreach (i; 0 .. barBlocks) {
96                         if (i <= passed) {
97                             output.write("█");
98                         } else {
99                             output.write(" ");
100                         }
101                     }
102                     output.write("|");
103                 }
104             }
105 
106             if (showAnyTimeStats) {
107                 output.writef!" [";
108                 if (showElapsedTime) this.printTime(now);
109 
110                 static if (hasLength!R) {
111                     auto fps = 1e9 * count / now.total!"nsecs";
112                     if (showETA) {
113                         auto remained = dur!"seconds"(to!long(range.length.to!double / fps));
114                         output.write("<");
115                         this.printTime(remained);
116                     }
117                     if (showSpeed) output.writef!", %.2fit/s"(fps);
118                 }
119                 output.writef!"]";
120             }
121 
122             if (this.range.empty) {
123                 output.writef(eraseLast ? this.rewriteLine : "\n");
124             }
125             this.previous = now;
126         }
127     }
128 
129     void printTime(Duration d) {
130         auto s = d.split!("hours", "minutes", "seconds");
131         if (s.hours > 0) {
132             this.output.writef!"%02d"(s.hours);
133         } else {
134             this.output.writef!"%02d:%02d"(s.minutes, s.seconds);
135         }
136     }
137 }
138 
139 auto snck(R)(R range) {
140     return Snck!R(range);
141 }
142 
143 auto snck(R)(R range, SnckConf conf) {
144     auto ret = Snck!R(range);
145     ret.conf = conf;
146     return ret;
147 }
148 
149 
150 
151 unittest
152 {
153     import core.thread;
154     import std.range;
155     import std.stdio;
156     foreach (i; [1, 2, 3].snck) {
157         Thread.sleep(dur!"msecs"(i * 300));
158     }
159 
160     foreach (i; iota(1000).snck) {
161         Thread.sleep(dur!"msecs"(1));
162     }
163 
164     SnckConf conf = {
165         barBlocks: 20,
166         minSeconds: 0.001,
167         eraseLast: false,
168     };
169     foreach (i; iota(2000).snck(conf).output(stdout)) {
170         Thread.sleep(dur!"msecs"(1));
171     }
172 }