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