Manage ansi codes and color codes separately

The ConsoleAppender formatEnabledInEnv field was being used both as an
indicator that ansi codes were supported and that color codes are
enabled. There are cases in which general ansi codes are not supported
but color codes are and these use cases need to be handled separately.
To make things more explicit, this commit adds isColorEnabled and
isAnsiSupported to the Terminal companion object so that we can be more
specific about what the requirements are (general ansi escape codes or
just colors). There are a few cases in ConsoleAppender itself where
formatEnabledInEnv was used to set flags for both color and ansi codes.
When that is the case, we use Terminal.isAnsiSupported because when that
is true, colors should at least work but there are terminals that
support color but not general ansi escape codes.
This commit is contained in:
Ethan Atkins 2020-10-20 11:39:08 -07:00
parent 29d9c14edf
commit 78620cd902
14 changed files with 47 additions and 41 deletions

View File

@ -64,8 +64,8 @@ object ConsoleLogger {
*/ */
def apply( def apply(
out: ConsoleOut = ConsoleOut.systemOut, out: ConsoleOut = ConsoleOut.systemOut,
ansiCodesSupported: Boolean = ConsoleAppender.formatEnabledInEnv, ansiCodesSupported: Boolean = Terminal.isAnsiSupported,
useFormat: Boolean = ConsoleAppender.formatEnabledInEnv, useFormat: Boolean = Terminal.isColorEnabled,
suppressedMessage: SuppressedTraceContext => Option[String] = suppressedMessage: SuppressedTraceContext => Option[String] =
ConsoleAppender.noSuppressedMessage ConsoleAppender.noSuppressedMessage
): ConsoleLogger = ): ConsoleLogger =
@ -148,7 +148,8 @@ object ConsoleAppender {
* 3. -Dsbt.colour=always/auto/never/true/false * 3. -Dsbt.colour=always/auto/never/true/false
* 4. -Dsbt.log.format=always/auto/never/true/false * 4. -Dsbt.log.format=always/auto/never/true/false
*/ */
lazy val formatEnabledInEnv: Boolean = Terminal.formatEnabledInEnv @deprecated("Use Terminal.isAnsiSupported or Terminal.isColorEnabled", "1.4.0")
lazy val formatEnabledInEnv: Boolean = Terminal.isAnsiSupported
private[sbt] def parseLogOption(s: String): LogOption = Terminal.parseLogOption(s) match { private[sbt] def parseLogOption(s: String): LogOption = Terminal.parseLogOption(s) match {
case Some(true) => LogOption.Always case Some(true) => LogOption.Always
@ -204,7 +205,7 @@ object ConsoleAppender {
* @param out Where to write messages. * @param out Where to write messages.
* @return A new `ConsoleAppender` that writes to `out`. * @return A new `ConsoleAppender` that writes to `out`.
*/ */
def apply(name: String, out: ConsoleOut): Appender = apply(name, out, formatEnabledInEnv) def apply(name: String, out: ConsoleOut): Appender = apply(name, out, Terminal.isAnsiSupported)
/** /**
* A new `ConsoleAppender` identified by `name`, and that writes to `out`. * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
@ -218,8 +219,10 @@ object ConsoleAppender {
name: String, name: String,
out: ConsoleOut, out: ConsoleOut,
suppressedMessage: SuppressedTraceContext => Option[String] suppressedMessage: SuppressedTraceContext => Option[String]
): Appender = ): Appender = {
apply(name, out, formatEnabledInEnv, formatEnabledInEnv, suppressedMessage) val ansi = Terminal.isAnsiSupported
apply(name, out, ansi, ansi, suppressedMessage)
}
/** /**
* A new `ConsoleAppender` identified by `name`, and that writes to `out`. * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
@ -230,7 +233,7 @@ object ConsoleAppender {
* @return A new `ConsoleAppender` that writes to `out`. * @return A new `ConsoleAppender` that writes to `out`.
*/ */
def apply(name: String, out: ConsoleOut, useFormat: Boolean): Appender = def apply(name: String, out: ConsoleOut, useFormat: Boolean): Appender =
apply(name, out, useFormat || formatEnabledInEnv, useFormat, noSuppressedMessage) apply(name, out, useFormat || Terminal.isAnsiSupported, useFormat, noSuppressedMessage)
/** /**
* A new `ConsoleAppender` identified by `name`, and that writes to `out`. * A new `ConsoleAppender` identified by `name`, and that writes to `out`.

View File

@ -64,7 +64,7 @@ object ConsoleOut {
def println(s: String): Unit = synchronized { current.append(s); println() } def println(s: String): Unit = synchronized { current.append(s); println() }
def println(): Unit = synchronized { def println(): Unit = synchronized {
val s = current.toString val s = current.toString
if (ConsoleAppender.formatEnabledInEnv && last.exists(lmsg => f(s, lmsg))) if (Terminal.isAnsiSupported && last.exists(lmsg => f(s, lmsg)))
lockObject.print(OverwriteLine) lockObject.print(OverwriteLine)
lockObject.println(s) lockObject.println(s)
last = Some(s) last = Some(s)
@ -72,7 +72,7 @@ object ConsoleOut {
} }
def flush(): Unit = synchronized { def flush(): Unit = synchronized {
val s = current.toString val s = current.toString
if (ConsoleAppender.formatEnabledInEnv && last.exists(lmsg => f(s, lmsg))) if (Terminal.isAnsiSupported && last.exists(lmsg => f(s, lmsg)))
lockObject.print(OverwriteLine) lockObject.print(OverwriteLine)
lockObject.print(s) lockObject.print(s)
last = Some(s) last = Some(s)

View File

@ -73,7 +73,7 @@ private[sbt] object JLine3 {
term term
} }
private[sbt] def apply(term: Terminal): JTerminal = { private[sbt] def apply(term: Terminal): JTerminal = {
if (System.getProperty("jline.terminal", "") == "none" || !Terminal.formatEnabledInEnv) if (System.getProperty("jline.terminal", "") == "none" || !Terminal.isAnsiSupported)
new DumbTerminal(term.inputStream, term.outputStream) new DumbTerminal(term.inputStream, term.outputStream)
else wrapTerminal(term) else wrapTerminal(term)
} }

View File

@ -88,10 +88,10 @@ object MainAppender {
ConsoleAppender(name, console, suppressedMessage = suppressedMessage) ConsoleAppender(name, console, suppressedMessage = suppressedMessage)
def defaultBacked: PrintWriter => Appender = def defaultBacked: PrintWriter => Appender =
defaultBacked(generateGlobalBackingName, ConsoleAppender.formatEnabledInEnv) defaultBacked(generateGlobalBackingName, Terminal.isAnsiSupported)
def defaultBacked(loggerName: String): PrintWriter => Appender = def defaultBacked(loggerName: String): PrintWriter => Appender =
defaultBacked(loggerName, ConsoleAppender.formatEnabledInEnv) defaultBacked(loggerName, Terminal.isAnsiSupported)
def defaultBacked(useFormat: Boolean): PrintWriter => Appender = def defaultBacked(useFormat: Boolean): PrintWriter => Appender =
defaultBacked(generateGlobalBackingName, useFormat) defaultBacked(generateGlobalBackingName, useFormat)

View File

@ -306,7 +306,8 @@ object Terminal {
case _ => sys.props.get("sbt.log.format").flatMap(parseLogOption) case _ => sys.props.get("sbt.log.format").flatMap(parseLogOption)
} }
} }
private[sbt] lazy val formatEnabledInEnv: Boolean = logFormatEnabled.getOrElse(useColorDefault) private[sbt] lazy val isAnsiSupported: Boolean =
logFormatEnabled.getOrElse(useColorDefault && !isCI)
private[this] val isDumbTerminal = "dumb" == System.getenv("TERM") private[this] val isDumbTerminal = "dumb" == System.getenv("TERM")
private[this] val hasConsole = Option(java.lang.System.console).isDefined private[this] val hasConsole = Option(java.lang.System.console).isDefined
private[this] def useColorDefault: Boolean = { private[this] def useColorDefault: Boolean = {
@ -316,9 +317,10 @@ object Terminal {
} }
private[this] lazy val isColorEnabledProp: Option[Boolean] = private[this] lazy val isColorEnabledProp: Option[Boolean] =
sys.props.get("sbt.color").orElse(sys.props.get("sbt.colour")).flatMap(parseLogOption) sys.props.get("sbt.color").orElse(sys.props.get("sbt.colour")).flatMap(parseLogOption)
private[sbt] lazy val isColorEnabled = useColorDefault
private[sbt] def red(str: String, doRed: Boolean): String = private[sbt] def red(str: String, doRed: Boolean): String =
if (formatEnabledInEnv && doRed) Console.RED + str + Console.RESET if (isColorEnabled && doRed) Console.RED + str + Console.RESET
else str else str
/** /**
@ -331,7 +333,7 @@ object Terminal {
private[sbt] def withStreams[T](isServer: Boolean)(f: => T): T = private[sbt] def withStreams[T](isServer: Boolean)(f: => T): T =
// In ci environments, don't touch the io streams unless run with -Dsbt.io.virtual=true // In ci environments, don't touch the io streams unless run with -Dsbt.io.virtual=true
if (System.getProperty("sbt.io.virtual", "") == "true" || (logFormatEnabled.getOrElse(true) && !isCI)) { if (System.getProperty("sbt.io.virtual", "") == "true" || (logFormatEnabled.getOrElse(true) && !isCI)) {
hasProgress.set(isServer && formatEnabledInEnv) hasProgress.set(isServer && isAnsiSupported)
consoleTerminalHolder.set(newConsoleTerminal()) consoleTerminalHolder.set(newConsoleTerminal())
activeTerminal.set(consoleTerminalHolder.get) activeTerminal.set(consoleTerminalHolder.get)
try withOut(withIn(f)) try withOut(withIn(f))
@ -745,7 +747,7 @@ object Terminal {
private[this] def fixTerminalProperty(): Unit = { private[this] def fixTerminalProperty(): Unit = {
val terminalProperty = "jline.terminal" val terminalProperty = "jline.terminal"
val newValue = val newValue =
if (!formatEnabledInEnv) "none" if (!isAnsiSupported) "none"
else else
System.getProperty(terminalProperty) match { System.getProperty(terminalProperty) match {
case "jline.UnixTerminal" => "unix" case "jline.UnixTerminal" => "unix"
@ -794,7 +796,8 @@ object Terminal {
val size = system.getSize val size = system.getSize
(size.getColumns, size.getRows) (size.getColumns, size.getRows)
} }
override lazy val isAnsiSupported: Boolean = !isDumbTerminal && formatEnabledInEnv && !isCI override lazy val isAnsiSupported: Boolean =
!isDumbTerminal && Terminal.isAnsiSupported && !isCI
override private[sbt] def progressState: ProgressState = consoleProgressState.get override private[sbt] def progressState: ProgressState = consoleProgressState.get
override def isEchoEnabled: Boolean = override def isEchoEnabled: Boolean =
try system.echo() try system.echo()
@ -839,7 +842,7 @@ object Terminal {
override def isColorEnabled: Boolean = override def isColorEnabled: Boolean =
props props
.map(_.color) .map(_.color)
.getOrElse(isColorEnabledProp.getOrElse(formatEnabledInEnv)) .getOrElse(isColorEnabledProp.getOrElse(Terminal.isColorEnabled))
override def isSupershellEnabled: Boolean = override def isSupershellEnabled: Boolean =
props props
@ -963,8 +966,8 @@ object Terminal {
override def getStringCapability(capability: String): String = null override def getStringCapability(capability: String): String = null
override def getWidth: Int = 0 override def getWidth: Int = 0
override def inputStream: InputStream = nullInputStream override def inputStream: InputStream = nullInputStream
override def isAnsiSupported: Boolean = formatEnabledInEnv override def isAnsiSupported: Boolean = Terminal.isAnsiSupported
override def isColorEnabled: Boolean = isColorEnabledProp.getOrElse(formatEnabledInEnv) override def isColorEnabled: Boolean = isColorEnabledProp.getOrElse(Terminal.isColorEnabled)
override def isEchoEnabled: Boolean = false override def isEchoEnabled: Boolean = false
override def isSuccessEnabled: Boolean = true override def isSuccessEnabled: Boolean = true
override def isSupershellEnabled: Boolean = false override def isSupershellEnabled: Boolean = false

View File

@ -18,7 +18,7 @@ import sbt.util.Logger
import sbt.ConcurrentRestrictions.Tag import sbt.ConcurrentRestrictions.Tag
import sbt.protocol.testing._ import sbt.protocol.testing._
import sbt.internal.util.Util.{ AnyOps, none } import sbt.internal.util.Util.{ AnyOps, none }
import sbt.internal.util.{ ConsoleAppender, RunningProcesses } import sbt.internal.util.{ RunningProcesses, Terminal }
private[sbt] object ForkTests { private[sbt] object ForkTests {
def apply( def apply(
@ -97,7 +97,7 @@ private[sbt] object ForkTests {
val is = new ObjectInputStream(socket.getInputStream) val is = new ObjectInputStream(socket.getInputStream)
try { try {
val config = new ForkConfiguration(ConsoleAppender.formatEnabledInEnv, parallel) val config = new ForkConfiguration(Terminal.isAnsiSupported, parallel)
os.writeObject(config) os.writeObject(config)
val taskdefs = opts.tests.map { t => val taskdefs = opts.tests.map { t =>

View File

@ -10,13 +10,13 @@ package sbt
import java.util.regex.Pattern import java.util.regex.Pattern
import scala.Console.{ BOLD, RESET } import scala.Console.{ BOLD, RESET }
import sbt.internal.util.ConsoleAppender import sbt.internal.util.Terminal
object Highlight { object Highlight {
def showMatches(pattern: Pattern)(line: String): Option[String] = { def showMatches(pattern: Pattern)(line: String): Option[String] = {
val matcher = pattern.matcher(line) val matcher = pattern.matcher(line)
if (ConsoleAppender.formatEnabledInEnv) { if (Terminal.isColorEnabled) {
// ANSI codes like \033[39m (normal text color) don't work on Windows // ANSI codes like \033[39m (normal text color) don't work on Windows
val highlighted = matcher.replaceAll(scala.Console.RED + "$0" + RESET) val highlighted = matcher.replaceAll(scala.Console.RED + "$0" + RESET)
if (highlighted == line) None else Some(highlighted) if (highlighted == line) None else Some(highlighted)
@ -26,5 +26,5 @@ object Highlight {
None None
} }
def bold(s: String) = def bold(s: String) =
if (ConsoleAppender.formatEnabledInEnv) BOLD + s.replace(RESET, RESET + BOLD) + RESET else s if (Terminal.isColorEnabled) BOLD + s.replace(RESET, RESET + BOLD) + RESET else s
} }

View File

@ -15,7 +15,7 @@ import sbt.KeyRanks.{ DTask, Invisible }
import sbt.Scope.{ GlobalScope, ThisScope } import sbt.Scope.{ GlobalScope, ThisScope }
import sbt.internal.util.Types.const import sbt.internal.util.Types.const
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.internal.util._ import sbt.internal.util.{ Terminal => ITerminal, _ }
import Util._ import Util._
import sbt.util.Show import sbt.util.Show
import xsbti.VirtualFile import xsbti.VirtualFile
@ -173,7 +173,7 @@ object Def extends Init[Scope] with TaskMacroExtra with InitializeImplicits {
Scope.displayMasked(scoped.scope, scoped.key.label, mask, showZeroConfig) Scope.displayMasked(scoped.scope, scoped.key.label, mask, showZeroConfig)
def withColor(s: String, color: Option[String]): String = def withColor(s: String, color: Option[String]): String =
withColor(s, color, useColor = ConsoleAppender.formatEnabledInEnv) withColor(s, color, useColor = ITerminal.isColorEnabled)
def withColor(s: String, color: Option[String], useColor: Boolean): String = color match { def withColor(s: String, color: Option[String], useColor: Boolean): String = color match {
case Some(c) if useColor => c + s + scala.Console.RESET case Some(c) if useColor => c + s + scala.Console.RESET
case _ => s case _ => s

View File

@ -10,7 +10,7 @@ package sbt.std
import sbt.SettingKey import sbt.SettingKey
import sbt.dsl.LinterLevel import sbt.dsl.LinterLevel
import sbt.dsl.LinterLevel.{ Abort, Warn } import sbt.dsl.LinterLevel.{ Abort, Warn }
import sbt.internal.util.ConsoleAppender import sbt.internal.util.Terminal
import sbt.internal.util.appmacro.{ Convert, LinterDSL } import sbt.internal.util.appmacro.{ Convert, LinterDSL }
import scala.io.AnsiColor import scala.io.AnsiColor
@ -191,10 +191,10 @@ object OnlyTaskDynLinterDSL extends BaseTaskLinterDSL {
} }
object TaskLinterDSLFeedback { object TaskLinterDSLFeedback {
private final val startBold = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.BOLD else "" private final val startBold = if (Terminal.isColorEnabled) AnsiColor.BOLD else ""
private final val startRed = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.RED else "" private final val startRed = if (Terminal.isColorEnabled) AnsiColor.RED else ""
private final val startGreen = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.GREEN else "" private final val startGreen = if (Terminal.isColorEnabled) AnsiColor.GREEN else ""
private final val reset = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.RESET else "" private final val reset = if (Terminal.isColorEnabled) AnsiColor.RESET else ""
private final val ProblemHeader = s"${startRed}problem$reset" private final val ProblemHeader = s"${startRed}problem$reset"
private final val SolutionHeader = s"${startGreen}solution$reset" private final val SolutionHeader = s"${startGreen}solution$reset"

View File

@ -17,7 +17,7 @@ import sbt.Scope.Global
import sbt.internal.Aggregation.KeyValue import sbt.internal.Aggregation.KeyValue
import sbt.internal.TaskName._ import sbt.internal.TaskName._
import sbt.internal._ import sbt.internal._
import sbt.internal.util._ import sbt.internal.util.{ Terminal => ITerminal, _ }
import sbt.librarymanagement.{ Resolver, UpdateReport } import sbt.librarymanagement.{ Resolver, UpdateReport }
import sbt.std.Transform.DummyTaskMap import sbt.std.Transform.DummyTaskMap
import sbt.util.{ Logger, Show } import sbt.util.{ Logger, Show }
@ -368,7 +368,7 @@ object EvaluateTask {
for ((key, msg, ex) <- keyed if (msg.isDefined || ex.isDefined)) { for ((key, msg, ex) <- keyed if (msg.isDefined || ex.isDefined)) {
val msgString = (msg.toList ++ ex.toList.map(ErrorHandling.reducedToString)).mkString("\n\t") val msgString = (msg.toList ++ ex.toList.map(ErrorHandling.reducedToString)).mkString("\n\t")
val log = getStreams(key, streams).log val log = getStreams(key, streams).log
val display = contextDisplay(state, ConsoleAppender.formatEnabledInEnv) val display = contextDisplay(state, ITerminal.isColorEnabled)
log.error("(" + display.show(key) + ") " + msgString) log.error("(" + display.show(key) + ") " + msgString)
} }
} }

View File

@ -105,7 +105,7 @@ private[sbt] object xMain {
} finally { } finally {
// Clear any stray progress lines // Clear any stray progress lines
ShutdownHooks.close() ShutdownHooks.close()
if (ITerminal.formatEnabledInEnv) { if (ITerminal.isAnsiSupported) {
System.out.print(ConsoleAppender.ClearScreenAfterCursor) System.out.print(ConsoleAppender.ClearScreenAfterCursor)
System.out.flush() System.out.flush()
} }

View File

@ -14,7 +14,7 @@ import sbt.Def.ScopedKey
import sbt.Keys._ import sbt.Keys._
import sbt.Scope.GlobalScope import sbt.Scope.GlobalScope
import sbt.internal.util.MainAppender._ import sbt.internal.util.MainAppender._
import sbt.internal.util._ import sbt.internal.util.{ Terminal => ITerminal, _ }
import sbt.util.{ Level, LogExchange, Logger, LoggerContext } import sbt.util.{ Level, LogExchange, Logger, LoggerContext }
import org.apache.logging.log4j.core.{ Appender => XAppender } import org.apache.logging.log4j.core.{ Appender => XAppender }
@ -319,7 +319,7 @@ object LogManager {
private[this] def slog: Logger = private[this] def slog: Logger =
Option(ref.get) getOrElse sys.error("Settings logger used after project was loaded.") Option(ref.get) getOrElse sys.error("Settings logger used after project was loaded.")
override val ansiCodesSupported = ConsoleAppender.formatEnabledInEnv override val ansiCodesSupported = ITerminal.isAnsiSupported
override def trace(t: => Throwable) = slog.trace(t) override def trace(t: => Throwable) = slog.trace(t)
override def success(message: => String) = slog.success(message) override def success(message: => String) = slog.success(message)
override def log(level: Level.Value, message: => String) = slog.log(level, message) override def log(level: Level.Value, message: => String) = slog.log(level, message)

View File

@ -12,7 +12,7 @@ import java.util.Locale
import scala.util.control.NonFatal import scala.util.control.NonFatal
import scala.concurrent.duration._ import scala.concurrent.duration._
import sbt.internal.util.ConsoleAppender import sbt.internal.util.{ Terminal => ITerminal }
import sbt.internal.util.complete.SizeParser import sbt.internal.util.complete.SizeParser
// See also BuildPaths.scala // See also BuildPaths.scala
@ -106,7 +106,7 @@ object SysProp {
* 3. -Dsbt.colour=always/auto/never/true/false * 3. -Dsbt.colour=always/auto/never/true/false
* 4. -Dsbt.log.format=always/auto/never/true/false * 4. -Dsbt.log.format=always/auto/never/true/false
*/ */
lazy val color: Boolean = ConsoleAppender.formatEnabledInEnv lazy val color: Boolean = ITerminal.isColorEnabled
def closeClassLoaders: Boolean = getOrFalse("sbt.classloader.close") def closeClassLoaders: Boolean = getOrFalse("sbt.classloader.close")

View File

@ -9,7 +9,7 @@ package sbt
package internal.testing package internal.testing
import testing.{ Logger => TLogger } import testing.{ Logger => TLogger }
import sbt.internal.util.{ BufferedAppender, ConsoleAppender, ManagedLogger } import sbt.internal.util.{ BufferedAppender, ManagedLogger, Terminal }
import sbt.util.{ Level, ShowLines } import sbt.util.{ Level, ShowLines }
import sbt.protocol.testing._ import sbt.protocol.testing._
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -96,7 +96,7 @@ object TestLogger {
def debug(s: String) = log(Level.Debug, TestStringEvent(s)) def debug(s: String) = log(Level.Debug, TestStringEvent(s))
def trace(t: Throwable) = logger.trace(t) def trace(t: Throwable) = logger.trace(t)
private def log(level: Level.Value, event: TestStringEvent) = logger.logEvent(level, event) private def log(level: Level.Value, event: TestStringEvent) = logger.logEvent(level, event)
def ansiCodesSupported() = ConsoleAppender.formatEnabledInEnv def ansiCodesSupported() = Terminal.isAnsiSupported
} }
private[sbt] def toTestItemEvent(event: TestEvent): TestItemEvent = private[sbt] def toTestItemEvent(event: TestEvent): TestItemEvent =