├── .scalafmt.conf ├── .gitignore ├── project └── build.properties ├── gif └── pb_rec.gif ├── README.md └── src ├── test └── scala │ └── pb.scala └── main └── scala └── pb.scala /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.5.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .bsp 3 | .idea -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.4.8 2 | -------------------------------------------------------------------------------- /gif/pb_rec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a8m/pb-scala/HEAD/gif/pb_rec.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terminal progress bar for Scala 2 | 3 | Console progress bar for Scala Inspired from [pb](http://github.com/cheggaaa/pb). 4 | 5 | ![Screenshot](https://github.com/a8m/pb-scala/blob/master/gif/pb_rec.gif) 6 | 7 | ## Examples 8 | 1. simple example 9 | ```scala 10 | object Main { 11 | def main(args: Array[String]) { 12 | var count = 1000 13 | var pb = new ProgressBar(count) 14 | pb.showSpeed = false 15 | for (_ <- 1 to count) { 16 | pb += 1 17 | Thread.sleep(10) 18 | } 19 | println("done") 20 | } 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /src/test/scala/pb.scala: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpecLike 5 | 6 | trait MockOutput extends Output { 7 | var messages: Seq[String] = Seq() 8 | 9 | override def print(s: String) = messages = messages :+ s 10 | } 11 | 12 | class ProgressBarTest extends AnyWordSpecLike with Matchers { 13 | 14 | "A ProgressBar" when { 15 | "Call add()" should { 16 | "increment `current` in n" in { 17 | var pb = new ProgressBar(100) with MockOutput 18 | pb.add(10) 19 | pb.current should be(10) 20 | pb.messages.last should (startWith("\r10 / 100") and 21 | endWith regex "10.00 % (\\d+)/s (\\d+s *)*".r) 22 | } 23 | } 24 | "Using += operator" should { 25 | "increment `current` in n" in { 26 | var pb = new ProgressBar(100) with MockOutput 27 | pb += 10 28 | pb.current should be(10) 29 | } 30 | } 31 | "Call finish" should { 32 | "set `current` to `total`" in { 33 | var pb = new ProgressBar(1) with MockOutput 34 | pb.finish() 35 | pb.current should be(pb.total) 36 | pb.isFinish should be(true) 37 | pb.messages.size should be(1) 38 | } 39 | } 40 | "Set format" should { 41 | var pb = new ProgressBar(10) with MockOutput 42 | "not except string with len less than 5" in { 43 | pb.format("halo") 44 | pb.add(1) 45 | pb.messages.last should include regex "\\[=+>-+\\]".r 46 | } 47 | "replace bar box format" in { 48 | pb.format("[-> ]") 49 | pb += 1 50 | pb.messages.last should include regex "\\[-+> +\\]".r 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/pb.scala: -------------------------------------------------------------------------------- 1 | package pb 2 | import com.github.nscala_time.time.Imports._ 3 | import jline.{TerminalFactory} 4 | 5 | /** Output type format, indicate which format wil be used in 6 | * the speed box. 7 | */ 8 | object Units extends Enumeration { 9 | type Units = Value 10 | val Default, Bytes = Value 11 | } 12 | import Units._ 13 | 14 | /** We're using Output as a trait of ProgressBar, so be able 15 | * to mock the tty in the tests(i.e: override `print(...)`) 16 | */ 17 | trait Output { 18 | def print(s: String) = Console.print(s) 19 | } 20 | 21 | object ProgressBar { 22 | private val Format = "[=>-]" 23 | 24 | def kbFmt(n: Double): String = { 25 | var kb = 1024 26 | n match { 27 | case x if x >= Math.pow(kb, 4) => "%.2f TB".format(x / Math.pow(kb, 4)) 28 | case x if x >= Math.pow(kb, 3) => "%.2f GB".format(x / Math.pow(kb, 3)) 29 | case x if x >= Math.pow(kb, 2) => "%.2f MB".format(x / Math.pow(kb, 2)) 30 | case x if x >= kb => "%.2f KB".format(x / kb) 31 | case _ => "%.0f B".format(n) 32 | } 33 | } 34 | } 35 | 36 | /** By calling new ProgressBar with Int as a total, you'll 37 | * create a new ProgressBar with default configuration. 38 | */ 39 | class ProgressBar(_total: Int) extends Output { 40 | val total = _total 41 | var current = 0 42 | private var startTime = DateTime.now 43 | private var units = Units.Default 44 | private var barStart, barCurrent, barCurrentN, barRemain, barEnd = "" 45 | var isFinish = false 46 | var showBar, showSpeed, showPercent, showCounter, showTimeLeft = true 47 | 48 | format(ProgressBar.Format) 49 | 50 | /** Add to current value 51 | * 52 | * @param i the number to add to current value 53 | * @return current value 54 | */ 55 | def add(i: Int): Int = { 56 | current += i 57 | if (current <= total) draw() 58 | current 59 | } 60 | 61 | /** Add value using += operator 62 | */ 63 | def +=(i: Int): Int = add(i) 64 | 65 | /** Set Units size 66 | * the default is simple numbers, but you can use Bytes type instead. 67 | */ 68 | def setUnits(u: Units) = units = u 69 | 70 | /** Set custom format to the drawing bar, default is `[=>-]` 71 | */ 72 | def format(fmt: String) { 73 | if (fmt.length >= 5) { 74 | val v = fmt.split("").toList 75 | barStart = v(0) 76 | barCurrent = v(1) 77 | barCurrentN = v(2) 78 | barRemain = v(3) 79 | barEnd = v(4) 80 | } 81 | } 82 | 83 | private def draw() { 84 | val width = TerminalFactory.get().getWidth() 85 | var prefix, base, suffix = "" 86 | // percent box 87 | if (showPercent) { 88 | var percent = current.toFloat / (total.toFloat / 100) 89 | suffix += " %.2f %% ".format(percent) 90 | } 91 | // speed box 92 | if (showSpeed) { 93 | val fromStart = (startTime to DateTime.now).millis.toFloat 94 | val speed = current / (fromStart / 1.seconds.millis) 95 | suffix += (units match { 96 | case Default => "%.0f/s ".format(speed) 97 | case Bytes => "%s/s ".format(ProgressBar.kbFmt(speed)) 98 | }) 99 | } 100 | // time left box 101 | if (showTimeLeft) { 102 | val fromStart = (startTime to DateTime.now).millis.toFloat 103 | val left = (fromStart / current) * (total - current) 104 | val dur = Duration.millis(Math.ceil(left).toLong) 105 | if (dur.seconds > 0) { 106 | if (dur.seconds < 1.minutes.seconds) suffix += "%ds".format(dur.seconds) 107 | else suffix += "%dm".format(dur.minutes) 108 | } 109 | } 110 | // counter box 111 | if (showCounter) { 112 | prefix += (units match { 113 | case Default => "%d / %d ".format(current, total) 114 | case Bytes => "%s / %s ".format(ProgressBar.kbFmt(current), ProgressBar.kbFmt(total)) 115 | }) 116 | } 117 | // bar box 118 | if (showBar) { 119 | val size = width - (prefix + suffix).length - 3 120 | if (size > 0) { 121 | val curCount = Math.ceil((current.toFloat / total) * size).toInt 122 | val remCount = size - curCount 123 | base = barStart 124 | if (remCount > 0) { 125 | base += barCurrent * (curCount - 1) + barCurrentN 126 | } else { 127 | base += barCurrent * curCount 128 | } 129 | base += barRemain * remCount + barEnd 130 | } 131 | } 132 | // out 133 | var out = prefix + base + suffix 134 | if (out.length < width) { 135 | out += " " * (width - out.length) 136 | } 137 | // print 138 | print("\r" + out) 139 | } 140 | 141 | /** Calling finish manually will set current to total and draw 142 | * the last time 143 | */ 144 | def finish() { 145 | if (current < total) add(total - current) 146 | println() 147 | isFinish = true 148 | } 149 | } 150 | --------------------------------------------------------------------------------