From efb86aeec8cfa35c9f623d74700adcda902d34d3 Mon Sep 17 00:00:00 2001 From: Daniel Huber Date: Thu, 13 Jun 2019 16:04:33 +0200 Subject: [PATCH] Add progress bars for checking and downloading libs in Bootstrap Launcher --- bootstrap/build.sbt | 1 + bootstrap/src/main/scala/Bootstrap.scala | 22 ++++- bootstrap/src/main/scala/ProgressBar.scala | 95 ++++++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 bootstrap/src/main/scala/ProgressBar.scala diff --git a/bootstrap/build.sbt b/bootstrap/build.sbt index 6c0eb260..d079293f 100644 --- a/bootstrap/build.sbt +++ b/bootstrap/build.sbt @@ -3,4 +3,5 @@ version := "0.1" assemblyJarName in assembly := "ChatOverflow.jar" libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.1.1" +libraryDependencies += "org.jline" % "jline-terminal-jansi" % "3.11.0" // used for terminal width fork := true \ No newline at end of file diff --git a/bootstrap/src/main/scala/Bootstrap.scala b/bootstrap/src/main/scala/Bootstrap.scala index da8f6a94..e465438b 100644 --- a/bootstrap/src/main/scala/Bootstrap.scala +++ b/bootstrap/src/main/scala/Bootstrap.scala @@ -125,18 +125,30 @@ object Bootstrap { * @return false, if there is a serious problem */ private def downloadMissingLibraries(dependencies: List[(String, String)]): Boolean = { + val pb = new ProgressBar(dependencies.length) + // using par here to make multiple http requests in parallel, otherwise its awfully slow on internet connections with high RTTs - val missing = dependencies.par.filterNot(dep => isLibraryDownloaded(dep._2)).toList + val missing = dependencies.par.filterNot(dep => { + val (name, url) = dep + pb.countUp() + pb.updateDescription(s"$name@$url") + + isLibraryDownloaded(url) + }).toList + + pb.finish() if (missing.isEmpty) { println("All required libraries are already downloaded.") } else { println(s"Downloading ${missing.length} missing libraries...") - for (i <- missing.indices) { - val (name, url) = missing(i) + val pb = new ProgressBar(missing.length) + + for ((name, url) <- missing) { + pb.countUp() + pb.updateDescription(s"$name@$url") - println(s"[${i + 1}/${missing.length}] $name ($url)") if (!downloadLibrary(name, url)) { // Second try, just in case if (!downloadLibrary(name, url)) { @@ -144,6 +156,8 @@ object Bootstrap { } } } + + pb.finish() } true // everything went fine } diff --git a/bootstrap/src/main/scala/ProgressBar.scala b/bootstrap/src/main/scala/ProgressBar.scala new file mode 100644 index 00000000..d6500488 --- /dev/null +++ b/bootstrap/src/main/scala/ProgressBar.scala @@ -0,0 +1,95 @@ +import org.jline.terminal.TerminalBuilder + +/** + * Progress bar used reporting the status while checking and downloading libs. + * + * @param max count of events e.g. length of the list which progress is monitored. + */ +class ProgressBar(max: Int) { + // Width of the terminal, used for size calculations + private val width = { + val width = TerminalBuilder.builder().dumb(true).build().getWidth + + // Size couldn't be figured out, use a default + if (width <= 10) + 80 + else + width + } + + private var count = 0 + private var description = "" + + // We need to create a empty line so that latest line before creation won't be overwritten by the draw method. + println() + draw() // Initial draw + + /** + * Increases count by 1 and re-draws the progress bar with the updated count. + */ + def countUp(): Unit = { + // Thread-safeness when working with parallel collections + count.synchronized { + count += 1 + draw() + } + } + + /** + * Updates the description and re-draws the description line. + * @param desc the new description + */ + def updateDescription(desc: String): Unit = { + // Thread-safeness when working with parallel collections + description.synchronized { + description = desc + drawDescription() + } + } + + /** + * Deletes the description to result in a blank line. The progress bar will still be visible. + * After this you can normally print at the beginning of a new line and don't start at the end of the description. + */ + def finish(): Unit = { + description = "" + drawDescription() + } + + /** + * Draws the progress bar in the line above the current one. + */ + private def draw(): Unit = { + val barWidth = width - 16 // Width of the bar without percentage and counts + val percentage = count * 100 / max + val equalsSigns = "=" * (barWidth * percentage / 100) + val whiteSpaces = " " * (barWidth - equalsSigns.length) + + val content = "%3d%% (%2d|%2d) [%s]".format(percentage, count, max, equalsSigns + ">" + whiteSpaces) + + print(s"\033[1A\r$content\n") + // | | | | + // Go up 1 line | | | + // Go to beginning of line + // | | + // Actual progress bar + // | + // Go back down for description + } + + /** + * Draws the description which is located in the current line. + */ + private def drawDescription(): Unit = { + // Cap the description at the width of the terminal, otherwise a new line is created and everything would shift. + // If the user needs to see a really long url he can just widen his terminal. + val content = description.take(width) + + print(s"\r\033[0K$content") + // | | + // Go to beginning + // | + // Clear from cursor to end of the line + } + +}