chiark / gitweb /
More progress. More work.
[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
36 /*----- Main code ---------------------------------------------------------*/
37
38 object TerminalEyecandy extends Eyecandy {
39   private var last = "";
40   var eyecandyp = isatty(FileDescriptor.out);
41
42   /* Assume that characters take up one cell each.  This is going to fail
43    * badly for combining characters, zero-width characters, wide Asian
44    * characters, and lots of other Unicode characters.  The problem is that
45    * Java doesn't have any way to query the display width of a character,
46    * and, honestly, I don't care enough to do the (substantial) work required
47    * to do this properly.
48    */
49
50   def note(line: String) {
51     if (eyecandyp) {
52
53       /* If the old line is longer than the new one, then we must overprint
54        * the end part.
55        */
56       if (line.length < last.length) {
57         val n = last.length - line.length;
58         for (_ <- 0 until n) stdout.write('\b');
59         for (_ <- 0 until n) stdout.write(' ');
60       }
61
62       /* Figure out the length of the common prefix between what we had
63        * before and what we have now.
64        */
65       val m = (0 until (last.length min line.length)) prefixLength
66         { i => last(i) == line(i) };
67
68       /* Delete the tail from the old line and print the new version. */
69       for (_ <- m until last.length) stdout.write('\b');
70       stdout.print(line.substring(m));
71       stdout.flush();
72     }
73
74     /* Update the state. */
75     last = line;
76   }
77
78   def clear() { note(""); }
79
80   def commit() {
81     if (last != "") {
82       if (eyecandyp) stdout.write('\n');
83       else stdout.println(last);
84       last = "";
85     }
86   }
87
88   def done() { clear(); }
89   def failed(msg: String) { record(s"FAILED!  $msg"); }
90
91   def beginJob(model: Model): progress.JobReporter =
92     new JobReporter(model);
93
94   def beginOperation(what: String): progress.OperationReporter =
95     new OperationReporter(what);
96
97   private[this] class JobReporter(private[this] var model: Model)
98           extends progress.JobReporter {
99     private final val width = 40;
100     private final val spinner = """/-\|""";
101     private final val mingap = 100;
102     private[this] var step: Int = 0;
103     private[this] var sweep: Int = 0;
104     private[this] val t0 = currentTimeMillis;
105     private[this] var last: Long = -1;
106
107     def change(model: Model, cur: Long)
108       { last = -1; this.model = model; step(cur); }
109
110     def step(cur: Long) {
111       val now = currentTimeMillis;
112       if (last >= 0 && now - last < mingap) return;
113       last = now;
114
115       val max = model.max;
116       val sb = new StringBuilder;
117       sb ++= model.what; sb += ' ';
118
119       /* Step the spinner. */
120       sb += spinner(step); sb += ' ';
121       step += 1; if (step >= spinner.length) step = 0;
122
123       /* Progress bar. */
124       sb += '[';
125       if (max <= 0) {
126         val l = sweep; val r = width - 1 - sweep;
127         val (lo, hi, x, y) = if (l < r) (l, r, '>', '<')
128                              else (r, l, '<', '>');
129         for (_ <- 0 until lo) sb += ' ';
130         sb += x;
131         for (_ <- lo + 1 until hi) sb += ' ';
132         sb += y;
133         for (_ <- hi + 1 until width) sb += ' ';
134         sweep += 1; if (sweep >= width) sweep = 0;
135       } else {
136         val n = (width*cur/max).toInt;
137         for (_ <- 0 until n) sb += '=';
138         for (_ <- n until width) sb += ' ';
139       }
140       sb += ']';
141
142       /* Quantitative progress. */
143       val f = model.format(cur); if (f != "") { sb += ' '; sb ++= f; }
144       if (max > 0) sb ++= (100*cur/max).formatted(" %3d%%");
145
146       /* Estimated time to completion. */
147       val eta = model.eta(cur);
148       if (eta >= 0) {
149         sb += ' '; sb += '(';
150         sb ++= formatDuration(ceil(eta/1000.0).toInt);
151         sb += ')';
152       }
153
154       /* Done. */
155       note(sb.result);
156     }
157
158     def done() {
159       val t = formatDuration(ceil((currentTimeMillis - t0)/1000.0).toInt);
160       record(s"${model.what} done ($t)");
161     }
162
163     def failed(e: Exception)
164       { record(s"${model.what} FAILED: ${e.getMessage}"); }
165
166     step(0);
167   }
168
169   class OperationReporter(what: String) extends progress.OperationReporter {
170     def step(detail: String) { note(s"$what: $detail"); }
171     def done() { record(s"$what: ok"); }
172     def failed(e: Exception) { record(s"$what: ${e.getMessage}"); }
173     step("...");
174   }
175 }
176
177 /*----- That's all, folks -------------------------------------------------*/