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; | |
b1ec59e3 | 35 | import Implicits.truish; |
04a5abae MW |
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() { | |
b1ec59e3 | 82 | if (last) { |
04a5abae MW |
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 = """/-\|"""; | |
b1ec59e3 | 102 | private final val mingap = 50; |
04a5abae MW |
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; | |
b1ec59e3 | 118 | sb ++= model.what; sb += ':'; sb += ' '; |
04a5abae MW |
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. */ | |
b1ec59e3 | 144 | val f = model.format(cur); if (f) { sb += ' '; sb ++= f; } |
04a5abae MW |
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 += '('; | |
a5ec891a | 151 | sb ++= formatDuration(ceil(eta/1000.0).toInt); |
04a5abae MW |
152 | sb += ')'; |
153 | } | |
154 | ||
155 | /* Done. */ | |
156 | note(sb.result); | |
157 | } | |
158 | ||
159 | def done() { | |
a5ec891a | 160 | val t = formatDuration(ceil((currentTimeMillis - t0)/1000.0).toInt); |
b1ec59e3 | 161 | record(s"${model.what}: done ($t)"); |
04a5abae MW |
162 | } |
163 | ||
164 | def failed(e: Exception) | |
b1ec59e3 | 165 | { record(s"${model.what}: FAILED: ${e.getMessage}"); } |
04a5abae MW |
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 -------------------------------------------------*/ |