From b8217f326f4e042ec9d7c86e08608cdce09e0c53 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 21 Oct 2024 19:58:01 +0800 Subject: [PATCH] Fix ANSI color sequences that apply to multiple lines in the terminal logs (#3797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the `linePrefix` would end with `RESET` and wipe out the colors after each line; now we parse each line and look at the ending color and print that at the start of the next line. There'll be some overhead, but `fansi.Str` is pretty fast so hopefully it's OK Tested manually, we can see the multi-line ansi colors that were problematic earlier now work, both preserving color, going from color to non-color and color to non-color Screenshot 2024-10-21 at 6 52 15 PM Fixes https://github.com/com-lihaoyi/mill/issues/3793 --- .../src/mill/util/LinePrefixOutputStream.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 5a83c38c70f..b06aac9d62e 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -20,16 +20,31 @@ class LinePrefixOutputStream( private[this] val linePrefixNonEmpty = linePrefixBytes.length != 0 private[this] var isNewLine = true val buffer = new ByteArrayOutputStream() + + // Make sure we preserve the end-of-line ANSI colors every time we write out the buffer, and + // re-apply them after every line prefix. This helps ensure the line prefix color/resets does + // not muck up the rendering of color sequences that affect multiple lines in the terminal + private var endOfLastLineColor: Long = 0 override def write(b: Array[Byte]): Unit = write(b, 0, b.length) private[this] def writeLinePrefixIfNecessary(): Unit = { if (isNewLine && linePrefixNonEmpty) { isNewLine = false buffer.write(linePrefixBytes) + if (linePrefixNonEmpty) + buffer.write(fansi.Attrs.emitAnsiCodes(0, endOfLastLineColor).getBytes()) } } def writeOutBuffer(): Unit = { if (buffer.size() > 0) reportPrefix() + + if (linePrefixNonEmpty) { + val bufferString = buffer.toString + if (bufferString.length > 0) { + val s = fansi.Str.apply(bufferString, errorMode = fansi.ErrorMode.Sanitize) + endOfLastLineColor = s.getColor(s.length - 1) + } + } out.synchronized { buffer.writeTo(out) } buffer.reset() }