--- /dev/null
+/* -*-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 -------------------------------------------------*/