From 68df6e8f5b575d7b737733339339b3b05ecc72a3 Mon Sep 17 00:00:00 2001 From: Mark Wooding Date: Sat, 9 Jun 2018 13:22:16 +0100 Subject: [PATCH] progress.scala: Miscellaneous WIP. --- progress.scala | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 181 insertions(+), 15 deletions(-) diff --git a/progress.scala b/progress.scala index ddd2b6a..9bfbc64 100644 --- a/progress.scala +++ b/progress.scala @@ -23,13 +23,14 @@ * along with TrIPE. If not, see . */ -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 ---------------------------------------------------------*/ @@ -42,12 +43,12 @@ def formatTime(t: Int): String = 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" } @@ -56,20 +57,43 @@ trait Eyecandy { 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 @@ -77,14 +101,156 @@ trait Job with Publisher[ { */ 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 -------------------------------------------------*/ + +} -- 2.11.0