keys.scala, etc.: Make merging public keys have a progress bar.
[tripe-android] / progress.scala
index 9bfbc64..5f81615 100644 (file)
@@ -29,226 +29,104 @@ package uk.org.distorted.tripe; package object progress {
 
 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();
+class SimpleModel(val what: String, val max: Long) extends Model;
 
-      case Cancelled =>
-       set(s"${job.what} CANCELLED"); commit();
+class DetailedModel(what: String, max: Long) extends SimpleModel(what, max) {
+  var detail: String = null;
+  override def format(cur: Long): String = {
+    val sb = new StringBuilder;
+    sb ++= super.format(cur);
+    if (detail != null) { sb += ' '; sb ++= detail; }
+    sb.result
+  }
+}
 
-      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, P <: BaseReporter](rep: P, body: P => 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 -------------------------------------------------*/