X-Git-Url: https://git.distorted.org.uk/~mdw/tripe-android/blobdiff_plain/68df6e8f5b575d7b737733339339b3b05ecc72a3..HEAD:/progress.scala diff --git a/progress.scala b/progress.scala index 9bfbc64..5f81615 100644 --- a/progress.scala +++ b/progress.scala @@ -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 -------------------------------------------------*/