+ 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;
+