diff --git a/cli/src/main/scala/coursier/cli/TermDisplay.scala b/cli/src/main/scala/coursier/cli/TermDisplay.scala index 78e00fd42..e4c7358b9 100644 --- a/cli/src/main/scala/coursier/cli/TermDisplay.scala +++ b/cli/src/main/scala/coursier/cli/TermDisplay.scala @@ -10,15 +10,58 @@ import coursier.Files.Logger import scala.annotation.tailrec import scala.collection.mutable.ArrayBuffer -class TermDisplay(out: Writer) extends Logger { +class TermDisplay( + out: Writer, + var fallbackMode: Boolean = false +) extends Logger { private val ansi = new Ansi(out) private var width = 80 private val refreshInterval = 1000 / 60 + private val fallbackRefreshInterval = 1000 + private val lock = new AnyRef private val t = new Thread("TermDisplay") { override def run() = lock.synchronized { + val baseExtraWidth = width / 5 + + def reflowed(url: String, info: Info) = { + val pctOpt = info.pct.map(100.0 * _) + val extra = + if (info.length.isEmpty && info.downloaded == 0L) + "" + else + s"(${pctOpt.map(pct => f"$pct%.2f %%, ").mkString}${info.downloaded}${info.length.map(" / " + _).mkString})" + + val total = url.length + 1 + extra.length + val (url0, extra0) = + if (total >= width) { // or > ? If equal, does it go down 2 lines? + val overflow = total - width + 1 + + val extra0 = + if (extra.length > baseExtraWidth) + extra.take((baseExtraWidth max (extra.length - overflow)) - 1) + "…" + else + extra + + val total0 = url.length + 1 + extra0.length + val overflow0 = total0 - width + 1 + + val url0 = + if (total0 >= width) + url.take(((width - baseExtraWidth - 1) max (url.length - overflow0)) - 1) + "…" + else + url + + (url0, extra0) + } else + (url, extra) + + (url0, extra0) + } + + @tailrec def helper(lineCount: Int): Unit = Option(q.poll(100L, TimeUnit.MILLISECONDS)) match { case None => helper(lineCount) @@ -35,36 +78,8 @@ class TermDisplay(out: Writer) extends Logger { for ((url, info) <- downloads0) { assert(info != null, s"Incoherent state ($url)") - val pctOpt = info.pct.map(100.0 * _) - val extra = - if (info.length.isEmpty && info.downloaded == 0L) - "" - else - s"(${pctOpt.map(pct => f"$pct%.2f %%, ").mkString}${info.downloaded}${info.length.map(" / " + _).mkString})" - val total = url.length + 1 + extra.length - val (url0, extra0) = - if (total >= width) { // or > ? If equal, does it go down 2 lines? - val overflow = total - width + 1 - - val extra0 = - if (extra.length > baseExtraWidth) - extra.take((baseExtraWidth max (extra.length - overflow)) - 1) + "…" - else - extra - - val total0 = url.length + 1 + extra0.length - val overflow0 = total0 - width + 1 - - val url0 = - if (total0 >= width) - url.take(((width - baseExtraWidth - 1) max (url.length - overflow0)) - 1) + "…" - else - url - - (url0, extra0) - } else - (url, extra) + val (url0, extra0) = reflowed(url, info) ansi.clearLine(2) out.write(s"$url0 $extra0\n") @@ -88,15 +103,54 @@ class TermDisplay(out: Writer) extends Logger { helper(downloads0.length) } - helper(0) + + @tailrec def fallbackHelper(previous: Set[String]): Unit = + Option(q.poll(100L, TimeUnit.MILLISECONDS)) match { + case None => fallbackHelper(previous) + case Some(Left(())) => // poison pill + case Some(Right(())) => + val downloads0 = downloads.synchronized { + downloads + .toVector + .map { url => url -> infos.get(url) } + .sortBy { case (_, info) => - info.pct.sum } + } + + var displayedSomething = false + for ((url, info) <- downloads0 if previous(url)) { + assert(info != null, s"Incoherent state ($url)") + + val (url0, extra0) = reflowed(url, info) + + displayedSomething = true + out.write(s"$url0 $extra0\n") + } + + if (displayedSomething) + out.write("\n") + + out.flush() + Thread.sleep(fallbackRefreshInterval) + fallbackHelper(previous ++ downloads0.map { case (url, _) => url }) + } + + if (fallbackMode) + fallbackHelper(Set.empty) + else + helper(0) } } t.setDaemon(true) def init(): Unit = { - width = TTY.consoleDim("cols") - ansi.clearLine(2) + try { + width = TTY.consoleDim("cols") + ansi.clearLine(2) + } catch { case _: Exception => + fallbackMode = true + } + t.start() } @@ -123,6 +177,12 @@ class TermDisplay(out: Writer) extends Logger { val prev = infos.putIfAbsent(url, Info(0L, None)) assert(prev == null) + if (fallbackMode) { + // FIXME What about concurrent accesses to out from the thread above? + out.write(s"Downloading $url\n") + out.flush() + } + downloads.synchronized { downloads.append(url) } @@ -150,6 +210,12 @@ class TermDisplay(out: Writer) extends Logger { downloads -= url } + if (fallbackMode) { + // FIXME What about concurrent accesses to out from the thread above? + out.write(s"Downloaded $url\n") + out.flush() + } + val info = infos.remove(url) assert(info != null) diff --git a/plugin/src/main/scala/coursier/CoursierPlugin.scala b/plugin/src/main/scala/coursier/CoursierPlugin.scala index c55f64ace..182de2b7d 100644 --- a/plugin/src/main/scala/coursier/CoursierPlugin.scala +++ b/plugin/src/main/scala/coursier/CoursierPlugin.scala @@ -36,18 +36,10 @@ object CoursierPlugin extends AutoPlugin { ) ++ sys.props private def createLogger() = Some { - if (sys.env.get("COURSIER_NO_TERM").nonEmpty) - new coursier.Files.Logger { - override def downloadingArtifact(url: String, file: File): Unit = { - println(s"$url\n -> $file") - } - override def downloadedArtifact(url: String, success: Boolean): Unit = { - println(s"$url: ${if (success) "Success" else "Failed"}") - } - def init() = {} - } - else - new TermDisplay(new OutputStreamWriter(System.err)) + new TermDisplay( + new OutputStreamWriter(System.err), + fallbackMode = sys.env.get("COURSIER_NO_TERM").nonEmpty + ) }