* along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
*/
-package uk.org.distorted.tripe; package object progress;
+package uk.org.distorted.tripe; package object progress {
/*----- Imports -----------------------------------------------------------*/
-import Math.ceil;
-import System.currentTimeMillis;
-import System.{err => stderr}; // FIXME: split out terminal progress
+import scala.collection.mutable.{Publisher, Subscriber};
+
+import java.lang.Math.ceil;
+import java.lang.System.currentTimeMillis;
/*----- Main code ---------------------------------------------------------*/
else f"$m%02d:$s%02d"
}
-private val UDATA = Seq("kB", "MB", "GB", "PB", "EB");
+private val UDATA = Seq("kB", "MB", "GB", "TB", "PB", "EB");
def formatBytes(n: Long): String = {
- val (x, u) = (n.toDouble, "B ") /: UDATA {
+ val (x, u) = ((n.toDouble, "B ") /: UDATA) { (xu, n) => (xu, n) match {
case ((x, u), name) if x >= 1024.0 => (x/1024.0, name)
case (xu, _) => xu
- }
+ } }
f"$x%6.1f$u%s"
}
def clear();
def commit();
def commit(line: String) { commit(); set(line); commit(); }
-
def begin(job: Job);
}
+abstract class Event; // other subclasses can be added!
+abstract class Progress extends Event { def cur: Long; } // it changed
+object Progress {
+ def unapply(p: Progress) =
+ if (p == null) None
+ else Some(p.cur);
+}
+case class Update(override val cur: Long) extends Progress; // progress has been made
+case class Changed(override val cur: Long) extends Progress; // what or max changed
+abstract class Stopped extends Event; // job has stopped
+case object Done extends Stopped; // job completed successfuly
+final case class Failed(why: String) extends Stopped; // job failed
+case object Cancelled extends Stopped; // job was cancelled
-trait Job with Publisher[ {
+trait Job extends Publisher[Event] {
def what: String; // imperative for what we're doing
def cur: Long; // current position in work
def max: Long; // maximum work to do
- def format: String; // describe progress in useful terms
+ def format: String = { // describe progress in useful terms
+ val c = cur;
+ val m = max;
+ if (m >= 0) {
+ val fm = m.formatted("%d");
+ s"%${fm.length}d/%s".format(c, fm) // ugh!
+ } else if (c > 0) s"$c"
+ else ""
+ }
+ def cancel();
private[this] val t0 = currentTimeMillis;
+ type Pub = Job;
- def eta: Int =
+ def taken: Double = (currentTimeMillis - t0)/1000.0;
+ def eta: Double =
/* Report the estimated time remaining in seconds, or -1 if no idea.
*
* The model here is very stupid. Weird jobs should override this and do
*/
if (max < 0 || cur <= 0) -1
- else ceil((currentTimeMillis - t0)/1000.0 *
- (max - cur)/cur.toDouble).toInt;
+ else taken*(max - cur)/cur.toDouble;
}
-object TerminalEyecandy extends Eyecandy {
+/*----- Terminal eyecandy (FIXME: split this out) -------------------------*/
+
+import java.io.FileDescriptor;
+import java.lang.System.{out => stdout};
+import sys.isatty;
+
+object TerminalEyecandy extends Eyecandy with Subscriber[Event, Job] {
private var last = "";
- var eyecandyp =
+ var eyecandyp = isatty(FileDescriptor.out);
+
+ /* Assume that characters take up one cell each. This is going to fail
+ * badly for combining characters, zero-width characters, wide Asian
+ * characters, and lots of other Unicode characters. The problem is that
+ * Java doesn't have any way to query the display width of a character,
+ * and, honestly, I don't care enough to do the (substantial) work required
+ * to do this properly.
+ */
+
+ def set(line: String) {
+ if (eyecandyp) {
+
+ /* If the old line is longer than the new one, then we must overprint
+ * the end part.
+ */
+ if (line.length < last.length) {
+ val n = last.length - line.length;
+ for (_ <- 0 until n) stdout.write('\b');
+ for (_ <- 0 until n) stdout.write(' ');
+ }
+
+ /* Figure out the length of the common prefix between what we had
+ * before and what we have now.
+ */
+ val m = (0 until (last.length min line.length)) prefixLength
+ { i => last(i) == line(i) };
+
+ /* Delete the tail from the old line and print the new version. */
+ for (_ <- m until last.length) stdout.write('\b');
+ stdout.print(line.substring(m));
+ stdout.flush();
+ }
+
+ /* Update the state. */
+ last = line;
+ }
+
+ def clear() { set(""); }
+
+ def commit() {
+ if (last != "") {
+ if (eyecandyp) stdout.write('\n');
+ else stdout.println(last);
+ last = "";
+ }
+ }
+
+ private final val spinner = """/-\|""";
+ private var step: Int = 0;
+ private final val width = 40;
+
+ def begin(job: Job) { job.subscribe(this); }
+
+ def notify(job: Job, ev: Event) {
+ ev match {
+ case Progress(cur) =>
+ /* Redraw the status line. */
+
+ val max = job.max;
+
+ val sb = new StringBuilder;
+ sb ++= job.what; sb += ' ';
+
+ /* Step the spinner. */
+ step += 1; if (step >= spinner.length) step = 0;
+ sb += spinner(step); sb += ' ';
+
+ /* Progress bar. */
+ if (max < 0)
+ sb ++= "[unknown progress]";
+ else {
+ val n = (width*cur/max).toInt;
+ sb += '[';
+ for (_ <- 0 until n) sb += '=';
+ for (_ <- n until 40) sb += ' ';
+ sb += ']';
+
+ val f = job.format;
+ if (f != "") { sb += ' '; sb ++= f; }
+ sb ++= (100*cur/max).formatted(" %3d%%");
+
+ val eta = job.eta;
+ if (eta >= 0) {
+ sb += ' '; sb += '(';
+ sb ++= formatTime(ceil(eta).toInt);
+ sb += ')';
+ }
+ }
+
+ /* Done. */
+ set(sb.result);
+
+ case Done =>
+ val t = formatTime(ceil(job.taken).toInt);
+ set(s"${job.what} done ($t)"); commit();
+
+ case Cancelled =>
+ set(s"${job.what} CANCELLED"); commit();
+
+ case Failed(msg) =>
+ set(s"${job.what} FAILED: $msg"); commit();
+
+ case _ => ok;
+ }
+ }
+}
+
+/*----- Testing cruft -----------------------------------------------------*/
+
+trait AsyncJob extends Job {
+ protected def run();
+ private var _cur: Long = 0; override def cur = _cur;
+
}
+
+
+
+import Thread.sleep;
+
+class ToyJob(val max: Long) extends Job {
+ val what = "Dummy job";
+ private var _i: Long = 0; def cur = _i;
+
+ def cancel() { ??? }
+ def run() {
+ for (i <- 1l until max) { _i = i; publish(Update(i)); sleep(100); }
+ publish(Done);
+ }
+}
+
+def testjob(n: Long) {
+ val j = new ToyJob(n);
+ TerminalEyecandy.begin(j);
+ j.run();
+}
+
/*----- That's all, folks -------------------------------------------------*/
+
+}