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