Commit | Line | Data |
---|---|---|
04a5abae MW |
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 ++= formatTime(ceil(eta/1000.0).toInt); | |
151 | sb += ')'; | |
152 | } | |
153 | ||
154 | /* Done. */ | |
155 | note(sb.result); | |
156 | } | |
157 | ||
158 | def done() { | |
159 | val t = formatTime(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 -------------------------------------------------*/ |