chiark / gitweb /
More work in progress.
[tripe-android] / terminal.scala
diff --git a/terminal.scala b/terminal.scala
new file mode 100644 (file)
index 0000000..9722e2b
--- /dev/null
@@ -0,0 +1,177 @@
+/* -*-scala-*-
+ *
+ * Terminal-based progress eyecandy
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package progress;
+
+/*----- Imports -----------------------------------------------------------*/
+
+import java.io.FileDescriptor;
+import java.lang.Math.ceil;
+import java.lang.System.{currentTimeMillis, out => stdout};
+
+import sys.isatty;
+
+/*----- Main code ---------------------------------------------------------*/
+
+object TerminalEyecandy extends Eyecandy {
+  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 note(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() { note(""); }
+
+  def commit() {
+    if (last != "") {
+      if (eyecandyp) stdout.write('\n');
+      else stdout.println(last);
+      last = "";
+    }
+  }
+
+  def done() { clear(); }
+  def failed(msg: String) { record(s"FAILED!  $msg"); }
+
+  def beginJob(model: Model): progress.JobReporter =
+    new JobReporter(model);
+
+  def beginOperation(what: String): progress.OperationReporter =
+    new OperationReporter(what);
+
+  private[this] class JobReporter(private[this] var model: Model)
+         extends progress.JobReporter {
+    private final val width = 40;
+    private final val spinner = """/-\|""";
+    private final val mingap = 100;
+    private[this] var step: Int = 0;
+    private[this] var sweep: Int = 0;
+    private[this] val t0 = currentTimeMillis;
+    private[this] var last: Long = -1;
+
+    def change(model: Model, cur: Long)
+      { last = -1; this.model = model; step(cur); }
+
+    def step(cur: Long) {
+      val now = currentTimeMillis;
+      if (last >= 0 && now - last < mingap) return;
+      last = now;
+
+      val max = model.max;
+      val sb = new StringBuilder;
+      sb ++= model.what; sb += ' ';
+
+      /* Step the spinner. */
+      sb += spinner(step); sb += ' ';
+      step += 1; if (step >= spinner.length) step = 0;
+
+      /* Progress bar. */
+      sb += '[';
+      if (max <= 0) {
+       val l = sweep; val r = width - 1 - sweep;
+       val (lo, hi, x, y) = if (l < r) (l, r, '>', '<')
+                            else (r, l, '<', '>');
+       for (_ <- 0 until lo) sb += ' ';
+       sb += x;
+       for (_ <- lo + 1 until hi) sb += ' ';
+       sb += y;
+       for (_ <- hi + 1 until width) sb += ' ';
+       sweep += 1; if (sweep >= width) sweep = 0;
+      } else {
+       val n = (width*cur/max).toInt;
+       for (_ <- 0 until n) sb += '=';
+       for (_ <- n until width) sb += ' ';
+      }
+      sb += ']';
+
+      /* Quantitative progress. */
+      val f = model.format(cur); if (f != "") { sb += ' '; sb ++= f; }
+      if (max > 0) sb ++= (100*cur/max).formatted(" %3d%%");
+
+      /* Estimated time to completion. */
+      val eta = model.eta(cur);
+      if (eta >= 0) {
+       sb += ' '; sb += '(';
+       sb ++= formatTime(ceil(eta/1000.0).toInt);
+       sb += ')';
+      }
+
+      /* Done. */
+      note(sb.result);
+    }
+
+    def done() {
+      val t = formatTime(ceil((currentTimeMillis - t0)/1000.0).toInt);
+      record(s"${model.what} done ($t)");
+    }
+
+    def failed(e: Exception)
+      { record(s"${model.what} FAILED: ${e.getMessage}"); }
+
+    step(0);
+  }
+
+  class OperationReporter(what: String) extends progress.OperationReporter {
+    def step(detail: String) { note(s"$what: $detail"); }
+    def done() { record(s"$what: ok"); }
+    def failed(e: Exception) { record(s"$what: ${e.getMessage}"); }
+    step("...");
+  }
+}
+
+/*----- That's all, folks -------------------------------------------------*/