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 }