keys.scala, etc.: Make merging public keys have a progress bar.
[tripe-android] / terminal.scala
1 /* -*-scala-*-
2 *
3 * Terminal-based progress eyecandy
4 *
5 * (c) 2018 Straylight/Edgeware
6 */
7
8 /*----- Licensing notice --------------------------------------------------*
9 *
10 * This file is part of the Trivial IP Encryption (TrIPE) Android app.
11 *
12 * TrIPE is free software: you can redistribute it and/or modify it under
13 * the terms of the GNU General Public License as published by the Free
14 * Software Foundation; either version 3 of the License, or (at your
15 * option) any later version.
16 *
17 * TrIPE is distributed in the hope that it will be useful, but WITHOUT
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
20 * for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
24 */
25
26 package uk.org.distorted.tripe; package progress;
27
28 /*----- Imports -----------------------------------------------------------*/
29
30 import java.io.FileDescriptor;
31 import java.lang.Math.ceil;
32 import java.lang.System.{currentTimeMillis, out => stdout};
33
34 import sys.isatty;
35 import Implicits.truish;
36
37 /*----- Main code ---------------------------------------------------------*/
38
39 object TerminalEyecandy extends Eyecandy {
40 private var last = "";
41 var eyecandyp = isatty(FileDescriptor.out);
42
43 /* Assume that characters take up one cell each. This is going to fail
44 * badly for combining characters, zero-width characters, wide Asian
45 * characters, and lots of other Unicode characters. The problem is that
46 * Java doesn't have any way to query the display width of a character,
47 * and, honestly, I don't care enough to do the (substantial) work required
48 * to do this properly.
49 */
50
51 def note(line: String) {
52 if (eyecandyp) {
53
54 /* If the old line is longer than the new one, then we must overprint
55 * the end part.
56 */
57 if (line.length < last.length) {
58 val n = last.length - line.length;
59 for (_ <- 0 until n) stdout.write('\b');
60 for (_ <- 0 until n) stdout.write(' ');
61 }
62
63 /* Figure out the length of the common prefix between what we had
64 * before and what we have now.
65 */
66 val m = (0 until (last.length min line.length)) prefixLength
67 { i => last(i) == line(i) };
68
69 /* Delete the tail from the old line and print the new version. */
70 for (_ <- m until last.length) stdout.write('\b');
71 stdout.print(line.substring(m));
72 stdout.flush();
73 }
74
75 /* Update the state. */
76 last = line;
77 }
78
79 def clear() { note(""); }
80
81 def commit() {
82 if (last) {
83 if (eyecandyp) stdout.write('\n');
84 else stdout.println(last);
85 last = "";
86 }
87 }
88
89 def done() { clear(); }
90 def failed(msg: String) { record(s"FAILED! $msg"); }
91
92 def beginJob(model: Model): progress.JobReporter =
93 new JobReporter(model);
94
95 def beginOperation(what: String): progress.OperationReporter =
96 new OperationReporter(what);
97
98 private[this] class JobReporter(private[this] var model: Model)
99 extends progress.JobReporter {
100 private final val width = 40;
101 private final val spinner = """/-\|""";
102 private final val mingap = 50;
103 private[this] var step: Int = 0;
104 private[this] var sweep: Int = 0;
105 private[this] val t0 = currentTimeMillis;
106 private[this] var last: Long = -1;
107
108 def change(model: Model, cur: Long)
109 { last = -1; this.model = model; step(cur); }
110
111 def step(cur: Long) {
112 val now = currentTimeMillis;
113 if (last >= 0 && now - last < mingap) return;
114 last = now;
115
116 val max = model.max;
117 val sb = new StringBuilder;
118 sb ++= model.what; sb += ':'; sb += ' ';
119
120 /* Step the spinner. */
121 sb += spinner(step); sb += ' ';
122 step += 1; if (step >= spinner.length) step = 0;
123
124 /* Progress bar. */
125 sb += '[';
126 if (max <= 0) {
127 val l = sweep; val r = width - 1 - sweep;
128 val (lo, hi, x, y) = if (l < r) (l, r, '>', '<')
129 else (r, l, '<', '>');
130 for (_ <- 0 until lo) sb += ' ';
131 sb += x;
132 for (_ <- lo + 1 until hi) sb += ' ';
133 sb += y;
134 for (_ <- hi + 1 until width) sb += ' ';
135 sweep += 1; if (sweep >= width) sweep = 0;
136 } else {
137 val n = (width*cur/max).toInt;
138 for (_ <- 0 until n) sb += '=';
139 for (_ <- n until width) sb += ' ';
140 }
141 sb += ']';
142
143 /* Quantitative progress. */
144 val f = model.format(cur); if (f) { sb += ' '; sb ++= f; }
145 if (max > 0) sb ++= (100*cur/max).formatted(" %3d%%");
146
147 /* Estimated time to completion. */
148 val eta = model.eta(cur);
149 if (eta >= 0) {
150 sb += ' '; sb += '(';
151 sb ++= formatDuration(ceil(eta/1000.0).toInt);
152 sb += ')';
153 }
154
155 /* Done. */
156 note(sb.result);
157 }
158
159 def done() {
160 val t = formatDuration(ceil((currentTimeMillis - t0)/1000.0).toInt);
161 record(s"${model.what}: done ($t)");
162 }
163
164 def failed(e: Exception)
165 { record(s"${model.what}: FAILED: ${e.getMessage}"); }
166
167 step(0);
168 }
169
170 class OperationReporter(what: String) extends progress.OperationReporter {
171 def step(detail: String) { note(s"$what: $detail"); }
172 def done() { record(s"$what: ok"); }
173 def failed(e: Exception) { record(s"$what: ${e.getMessage}"); }
174 step("...");
175 }
176 }
177
178 /*----- That's all, folks -------------------------------------------------*/