Smooth progress bar (#78)
* [domain] rewrite and expand ansi codes available * [aws-api] refactor progress-bar * [domain] move progress-bar into Terminal * [aws-api] remove unused value * [domain] use unicode characters to get a smooth progress bar
This commit is contained in:
parent
0a92667d3c
commit
eec79066f3
4 changed files with 220 additions and 20 deletions
|
@ -2,31 +2,23 @@ package net.kemitix.thorp.aws.api
|
|||
|
||||
import net.kemitix.thorp.aws.api.UploadEvent.RequestEvent
|
||||
import net.kemitix.thorp.domain.SizeTranslation.sizeInEnglish
|
||||
import net.kemitix.thorp.domain.Terminal.{clearLine, returnToPreviousLine}
|
||||
import net.kemitix.thorp.domain.Terminal._
|
||||
import net.kemitix.thorp.domain.{LocalFile, Terminal}
|
||||
|
||||
import scala.io.AnsiColor._
|
||||
|
||||
trait UploadProgressLogging {
|
||||
|
||||
private val oneHundredPercent = 100
|
||||
|
||||
def logRequestCycle(localFile: LocalFile,
|
||||
event: RequestEvent,
|
||||
bytesTransferred: Long): Unit = {
|
||||
event: RequestEvent,
|
||||
bytesTransferred: Long): Unit = {
|
||||
val remoteKey = localFile.remoteKey.key
|
||||
val fileLength = localFile.file.length
|
||||
val consoleWidth = Terminal.width - 2
|
||||
val done = ((bytesTransferred.toDouble / fileLength.toDouble) * consoleWidth).toInt
|
||||
if (done < oneHundredPercent) {
|
||||
val head = s"$GREEN_B$GREEN#$RESET" * done
|
||||
val tail = " " * (consoleWidth - done)
|
||||
val bar = s"[$head$tail]"
|
||||
if (bytesTransferred < fileLength) {
|
||||
val bar = progressBar(bytesTransferred, fileLength.toDouble, Terminal.width)
|
||||
val transferred = sizeInEnglish(bytesTransferred)
|
||||
val fileSize = sizeInEnglish(fileLength)
|
||||
print(s"${clearLine}Uploading $transferred of $fileSize : $remoteKey\n$bar$returnToPreviousLine")
|
||||
print(s"${eraseLine}Uploading $transferred of $fileSize : $remoteKey\n$bar${cursorUp()}\r")
|
||||
} else
|
||||
print(clearLine)
|
||||
print(eraseLine)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package net.kemitix.thorp.aws.lib
|
|||
|
||||
import cats.effect.IO
|
||||
import net.kemitix.thorp.domain.SizeTranslation.sizeInEnglish
|
||||
import net.kemitix.thorp.domain.Terminal.clearLine
|
||||
import net.kemitix.thorp.domain.Terminal._
|
||||
import net.kemitix.thorp.domain.{LocalFile, Logger}
|
||||
|
||||
object UploaderLogging {
|
||||
|
@ -12,7 +12,7 @@ object UploaderLogging {
|
|||
(implicit logger: Logger): IO[Unit] = {
|
||||
val tryMessage = if (tryCount == 1) "" else s"try $tryCount"
|
||||
val size = sizeInEnglish(localFile.file.length)
|
||||
logger.info(s"${clearLine}upload:$tryMessage:$size:${localFile.remoteKey.key}")
|
||||
logger.info(s"${eraseLine}upload:$tryMessage:$size:${localFile.remoteKey.key}")
|
||||
}
|
||||
|
||||
def logMultiPartUploadFinished(localFile: LocalFile)
|
||||
|
|
|
@ -1,15 +1,127 @@
|
|||
package net.kemitix.thorp.domain
|
||||
|
||||
import scala.io.AnsiColor._
|
||||
|
||||
object Terminal {
|
||||
|
||||
private val esc = "\u001B"
|
||||
private val csi = esc + "["
|
||||
|
||||
/**
|
||||
* Move the cursor up, default 1 line.
|
||||
*
|
||||
* Stops at the edge of the screen.
|
||||
*/
|
||||
def cursorUp(lines: Int = 1) = csi + lines + "A"
|
||||
|
||||
/**
|
||||
* Move the cursor down, default 1 line.
|
||||
*
|
||||
* Stops at the edge of the screen.
|
||||
*/
|
||||
def cursorDown(lines: Int = 1) = csi + lines + "B"
|
||||
|
||||
/**
|
||||
* Move the cursor forward, default 1 column.
|
||||
*
|
||||
* Stops at the edge of the screen.
|
||||
*/
|
||||
def cursorForward(cols: Int = 1) = csi + cols + "C"
|
||||
|
||||
/**
|
||||
* Move the cursor back, default 1 column,
|
||||
*
|
||||
* Stops at the edge of the screen.
|
||||
*/
|
||||
def cursorBack(cols: Int = 1) = csi + cols + "D"
|
||||
|
||||
/**
|
||||
* Move the cursor to the beginning of the line, default 1, down.
|
||||
*/
|
||||
def cursorNextLine(lines: Int = 1) = csi + lines + "E"
|
||||
/**
|
||||
* Move the cursor to the beginning of the line, default 1, up.
|
||||
*/
|
||||
def cursorPrevLine(lines: Int = 1) = csi + lines + "F"
|
||||
|
||||
/**
|
||||
* Move the cursor to the column on the current line.
|
||||
*/
|
||||
def cursorHorizAbs(col: Int) = csi + col + "G"
|
||||
|
||||
/**
|
||||
* Move the cursor to the position on screen (1,1 is the top-left).
|
||||
*/
|
||||
def cursorPosition(row: Int, col: Int) = csi + row + ";" + col + "H"
|
||||
|
||||
/**
|
||||
* Clear from cursor to end of screen.
|
||||
*/
|
||||
val eraseToEndOfScreen = csi + "0J"
|
||||
|
||||
/**
|
||||
* Clear from cursor to beginning of screen.
|
||||
*/
|
||||
val eraseToStartOfScreen = csi + "1J"
|
||||
|
||||
/**
|
||||
* Clear screen and move cursor to top-left.
|
||||
*
|
||||
* On DOS the "2J" command also moves to 1,1, so we force that behaviour for all.
|
||||
*/
|
||||
val eraseScreen = csi + "2J" + cursorPosition(1, 1)
|
||||
|
||||
/**
|
||||
* Clear screen and scrollback buffer then move cursor to top-left.
|
||||
*
|
||||
* Anticipate that same DOS behaviour here, and to maintain consistency with {@link #eraseScreen}.
|
||||
*/
|
||||
val eraseScreenAndBuffer = csi + "3J"
|
||||
|
||||
/**
|
||||
* Clears the terminal line to the right of the cursor.
|
||||
*
|
||||
* Does not move the cursor.
|
||||
*/
|
||||
val eraseLineForward = csi + "0K"
|
||||
|
||||
/**
|
||||
* Clears the terminal line to the left of the cursor.
|
||||
*
|
||||
* Does not move the cursor.
|
||||
*/
|
||||
val eraseLineBack= csi + "1K"
|
||||
|
||||
/**
|
||||
* Clears the whole terminal line.
|
||||
*
|
||||
* Does not move the cursor.
|
||||
*/
|
||||
val clearLine = "\u001B[2K\r"
|
||||
val eraseLine = csi + "2K"
|
||||
|
||||
/**
|
||||
* Moves the cursor up one line and back to the start of the line.
|
||||
* Scroll page up, default 1, lines.
|
||||
*/
|
||||
val returnToPreviousLine = "\u001B[1A\r"
|
||||
def scrollUp(lines: Int = 1) = csi + lines + "S"
|
||||
|
||||
/**
|
||||
* Scroll page down, default 1, lines.
|
||||
*/
|
||||
def scrollDown(lines: Int = 1) = csi + lines + "T"
|
||||
|
||||
/**
|
||||
* Saves the cursor position/state.
|
||||
*/
|
||||
val saveCursorPosition = csi + "s"
|
||||
|
||||
/**
|
||||
* Restores the cursor position/state.
|
||||
*/
|
||||
val restoreCursorPosition = csi + "u"
|
||||
|
||||
val enableAlternateBuffer = csi + "?1049h"
|
||||
|
||||
val disableAlternateBuffer = csi + "?1049l"
|
||||
|
||||
/**
|
||||
* The Width of the terminal, as reported by the COLUMNS environment variable.
|
||||
|
@ -25,4 +137,29 @@ object Terminal {
|
|||
.getOrElse(80)
|
||||
}
|
||||
|
||||
val subBars = Map(
|
||||
0 -> " ",
|
||||
1 -> "▏",
|
||||
2 -> "▎",
|
||||
3 -> "▍",
|
||||
4 -> "▌",
|
||||
5 -> "▋",
|
||||
6 -> "▊",
|
||||
7 -> "▉")
|
||||
|
||||
def progressBar(pos: Double, max: Double, width: Int): String = {
|
||||
val barWidth = width - 2
|
||||
val phases = subBars.values.size
|
||||
val pxWidth = barWidth * phases
|
||||
val ratio = pos / max
|
||||
val pxDone = pxWidth * ratio
|
||||
val fullHeadSize: Int = (pxDone / phases).toInt
|
||||
val part = (pxDone % phases).toInt
|
||||
val partial = if (part != 0) subBars.getOrElse(part, "") else ""
|
||||
val head = ("█" * fullHeadSize) + partial
|
||||
val tailSize = barWidth - head.length
|
||||
val tail = " " * tailSize
|
||||
s"[$head$tail]"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package net.kemitix.thorp.domain
|
||||
|
||||
import scala.io.AnsiColor._
|
||||
|
||||
import org.scalatest.FunSpec
|
||||
|
||||
class TerminalTest extends FunSpec {
|
||||
|
||||
describe("progressBar") {
|
||||
describe("width 10 - 0%") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(0d, 10d, 12)
|
||||
assertResult("[ ]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 10 - 10%") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(1d, 10d, 12)
|
||||
assertResult("[█ ]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 8/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(8d, 8d, 3)
|
||||
assertResult("[█]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 7/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(7d, 8d, 3)
|
||||
assertResult("[▉]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 6/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(6d, 8d, 3)
|
||||
assertResult("[▊]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 5/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(5d, 8d, 3)
|
||||
assertResult("[▋]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 4/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(4d, 8d, 3)
|
||||
assertResult("[▌]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 3/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(3d, 8d, 3)
|
||||
assertResult("[▍]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 2/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(2d, 8d, 3)
|
||||
assertResult("[▎]")(bar)
|
||||
}
|
||||
}
|
||||
describe("width 1 - 1/8th") {
|
||||
it("should match") {
|
||||
val bar = Terminal.progressBar(1d, 8d, 3)
|
||||
assertResult("[▏]")(bar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue