import scala.collection.mutable.{Publisher, Subscriber};
-import java.lang.Math.ceil;
import java.lang.System.currentTimeMillis;
-/*----- Main code ---------------------------------------------------------*/
+/*----- Progress displays -------------------------------------------------*/
-def formatTime(t: Int): String =
- if (t < -1) "???"
- else {
- val (s, t1) = (t%60, t/60);
- val (m, h) = (t1%60, t1/60);
- if (h > 0) f"$h%d:$m%02d:$s%02d"
- else f"$m%02d:$s%02d"
- }
-
-private val UDATA = Seq("kB", "MB", "GB", "TB", "PB", "EB");
-def formatBytes(n: Long): String = {
- 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"
-}
-
-trait Eyecandy {
- def set(line: String);
- 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 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
- 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();
+trait Model {
+ protected val t0 = currentTimeMillis;
- private[this] val t0 = currentTimeMillis;
- type Pub = Job;
+ def what: String;
+ def max: Long;
- def taken: Double = (currentTimeMillis - t0)/1000.0;
- def eta: Double =
+ def eta(cur: Long): 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
- * something more sensible.
+ * The model here is very stupid. Weird jobs should override this and
+ * do something more sensible.
*/
- if (max < 0 || cur <= 0) -1
- else taken*(max - cur)/cur.toDouble;
-}
-
-/*----- 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 = 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;
+ val max = this.max;
+ val delta = currentTimeMillis - t0
+ if (max < 0 || cur <= 0) -1 else delta*(max - cur)/cur.toDouble
}
- def clear() { set(""); }
+ protected def fmt1(n: Long): String = n.toString;
- def commit() {
- if (last != "") {
- if (eyecandyp) stdout.write('\n');
- else stdout.println(last);
- last = "";
- }
+ def format(cur: Long): String = {
+ val max = this.max;
+ val fc = fmt1(cur);
+ if (max >= 0) { val fm = fmt1(max); s"%${fm.length}s/%s".format(fc, fm) }
+ else if (cur > 0) fc
+ else ""
}
+}
- 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();
+class SimpleModel(val what: String, val max: Long) extends Model;
- case Failed(msg) =>
- set(s"${job.what} FAILED: $msg"); commit();
+private val UDATA = Seq("kB", "MB", "GB", "TB", "PB", "EB");
- case _ => ok;
- }
+trait DataModel extends Model {
+ override def fmt1(n: Long): String = {
+ 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"
}
}
-/*----- Testing cruft -----------------------------------------------------*/
-
-trait AsyncJob extends Job {
- protected def run();
- private var _cur: Long = 0; override def cur = _cur;
+trait BaseReporter {
+ def done();
+ def failed(e: Exception);
+}
-
+trait JobReporter extends BaseReporter {
+ def step(cur: Long);
+ def change(model: Model, cur: Long);
}
+trait OperationReporter extends BaseReporter {
+ def step(detail: String);
+}
+def withReporter[T, R <: BaseReporter]
+ (rep: R, body: R => T): T = {
+ val ret = try { body(rep) }
+ catch { case e: Exception => rep.failed(e); throw e; }
+ rep.done();
+ ret
+}
+trait Eyecandy {
+ def note(msg: String);
+ def clear();
+ def commit();
+ def record(msg: String) { note(msg); commit(); }
+ def done();
+ def cancelled() { failed("cancelled"); }
+ def failed(msg: String);
-import Thread.sleep;
+ def beginJob(model: Model): JobReporter
+ // = new JobReporter(model);
-class ToyJob(val max: Long) extends Job {
- val what = "Dummy job";
- private var _i: Long = 0; def cur = _i;
+ def beginOperation(what: String): OperationReporter
+ // = new OperationReporter(what);
- def cancel() { ??? }
- def run() {
- for (i <- 1l until max) { _i = i; publish(Update(i)); sleep(100); }
- publish(Done);
- }
-}
+ def job[T](model: Model)(body: JobReporter => T): T =
+ withReporter(beginJob(model), body);
-def testjob(n: Long) {
- val j = new ToyJob(n);
- TerminalEyecandy.begin(j);
- j.run();
+ def operation[T](what: String)(body: OperationReporter => T): T =
+ withReporter(beginOperation(what), body);
}
/*----- That's all, folks -------------------------------------------------*/