chiark / gitweb /
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 -------------------------------------------------*/