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