X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/tripe-android/blobdiff_plain/68df6e8f5b575d7b737733339339b3b05ecc72a3..04a5abaece151705e9bd7026653f79938a7a2fbc:/terminal.scala diff --git a/terminal.scala b/terminal.scala new file mode 100644 index 0000000..9722e2b --- /dev/null +++ b/terminal.scala @@ -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 . + */ + +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 -------------------------------------------------*/