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(
out: ConsoleOut = ConsoleOut.systemOut,
ansiCodesSupported: Boolean = ConsoleAppender.formatEnabledInEnv,
useFormat: Boolean = ConsoleAppender.formatEnabledInEnv,
ansiCodesSupported: Boolean = Terminal.isAnsiSupported,
useFormat: Boolean = Terminal.isColorEnabled,
suppressedMessage: SuppressedTraceContext => Option[String] =
ConsoleAppender.noSuppressedMessage
): ConsoleLogger =
@ -148,7 +148,8 @@ object ConsoleAppender {
* 3. -Dsbt.colour=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 {
case Some(true) => LogOption.Always
@ -204,7 +205,7 @@ object ConsoleAppender {
* @param out Where to write messages.
* @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`.
@ -218,8 +219,10 @@ object ConsoleAppender {
name: String,
out: ConsoleOut,
suppressedMessage: SuppressedTraceContext => Option[String]
): Appender =
apply(name, out, formatEnabledInEnv, formatEnabledInEnv, suppressedMessage)
): Appender = {
val ansi = Terminal.isAnsiSupported
apply(name, out, ansi, ansi, suppressedMessage)
}
/**
* 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`.
*/
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`.

View File

@ -64,7 +64,7 @@ object ConsoleOut {
def println(s: String): Unit = synchronized { current.append(s); println() }
def println(): Unit = synchronized {
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.println(s)
last = Some(s)
@ -72,7 +72,7 @@ object ConsoleOut {
}
def flush(): Unit = synchronized {
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(s)
last = Some(s)

View File

@ -73,7 +73,7 @@ private[sbt] object JLine3 {
term
}
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)
else wrapTerminal(term)
}

View File

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

View File

@ -306,7 +306,8 @@ object Terminal {
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 hasConsole = Option(java.lang.System.console).isDefined
private[this] def useColorDefault: Boolean = {
@ -316,9 +317,10 @@ object Terminal {
}
private[this] lazy val isColorEnabledProp: Option[Boolean] =
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 =
if (formatEnabledInEnv && doRed) Console.RED + str + Console.RESET
if (isColorEnabled && doRed) Console.RED + str + Console.RESET
else str
/**
@ -331,7 +333,7 @@ object Terminal {
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
if (System.getProperty("sbt.io.virtual", "") == "true" || (logFormatEnabled.getOrElse(true) && !isCI)) {
hasProgress.set(isServer && formatEnabledInEnv)
hasProgress.set(isServer && isAnsiSupported)
consoleTerminalHolder.set(newConsoleTerminal())
activeTerminal.set(consoleTerminalHolder.get)
try withOut(withIn(f))
@ -745,7 +747,7 @@ object Terminal {
private[this] def fixTerminalProperty(): Unit = {
val terminalProperty = "jline.terminal"
val newValue =
if (!formatEnabledInEnv) "none"
if (!isAnsiSupported) "none"
else
System.getProperty(terminalProperty) match {
case "jline.UnixTerminal" => "unix"
@ -794,7 +796,8 @@ object Terminal {
val size = system.getSize
(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 def isEchoEnabled: Boolean =
try system.echo()
@ -839,7 +842,7 @@ object Terminal {
override def isColorEnabled: Boolean =
props
.map(_.color)
.getOrElse(isColorEnabledProp.getOrElse(formatEnabledInEnv))
.getOrElse(isColorEnabledProp.getOrElse(Terminal.isColorEnabled))
override def isSupershellEnabled: Boolean =
props
@ -963,8 +966,8 @@ object Terminal {
override def getStringCapability(capability: String): String = null
override def getWidth: Int = 0
override def inputStream: InputStream = nullInputStream
override def isAnsiSupported: Boolean = formatEnabledInEnv
override def isColorEnabled: Boolean = isColorEnabledProp.getOrElse(formatEnabledInEnv)
override def isAnsiSupported: Boolean = Terminal.isAnsiSupported
override def isColorEnabled: Boolean = isColorEnabledProp.getOrElse(Terminal.isColorEnabled)
override def isEchoEnabled: Boolean = false
override def isSuccessEnabled: Boolean = true
override def isSupershellEnabled: Boolean = false

View File

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

View File

@ -10,13 +10,13 @@ package sbt
import java.util.regex.Pattern
import scala.Console.{ BOLD, RESET }
import sbt.internal.util.ConsoleAppender
import sbt.internal.util.Terminal
object Highlight {
def showMatches(pattern: Pattern)(line: String): Option[String] = {
val matcher = pattern.matcher(line)
if (ConsoleAppender.formatEnabledInEnv) {
if (Terminal.isColorEnabled) {
// ANSI codes like \033[39m (normal text color) don't work on Windows
val highlighted = matcher.replaceAll(scala.Console.RED + "$0" + RESET)
if (highlighted == line) None else Some(highlighted)
@ -26,5 +26,5 @@ object Highlight {
None
}
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.internal.util.Types.const
import sbt.internal.util.complete.Parser
import sbt.internal.util._
import sbt.internal.util.{ Terminal => ITerminal, _ }
import Util._
import sbt.util.Show
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)
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 {
case Some(c) if useColor => c + s + scala.Console.RESET
case _ => s

View File

@ -10,7 +10,7 @@ package sbt.std
import sbt.SettingKey
import sbt.dsl.LinterLevel
import sbt.dsl.LinterLevel.{ Abort, Warn }
import sbt.internal.util.ConsoleAppender
import sbt.internal.util.Terminal
import sbt.internal.util.appmacro.{ Convert, LinterDSL }
import scala.io.AnsiColor
@ -191,10 +191,10 @@ object OnlyTaskDynLinterDSL extends BaseTaskLinterDSL {
}
object TaskLinterDSLFeedback {
private final val startBold = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.BOLD else ""
private final val startRed = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.RED else ""
private final val startGreen = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.GREEN else ""
private final val reset = if (ConsoleAppender.formatEnabledInEnv) AnsiColor.RESET else ""
private final val startBold = if (Terminal.isColorEnabled) AnsiColor.BOLD else ""
private final val startRed = if (Terminal.isColorEnabled) AnsiColor.RED else ""
private final val startGreen = if (Terminal.isColorEnabled) AnsiColor.GREEN else ""
private final val reset = if (Terminal.isColorEnabled) AnsiColor.RESET else ""
private final val ProblemHeader = s"${startRed}problem$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.TaskName._
import sbt.internal._
import sbt.internal.util._
import sbt.internal.util.{ Terminal => ITerminal, _ }
import sbt.librarymanagement.{ Resolver, UpdateReport }
import sbt.std.Transform.DummyTaskMap
import sbt.util.{ Logger, Show }
@ -368,7 +368,7 @@ object EvaluateTask {
for ((key, msg, ex) <- keyed if (msg.isDefined || ex.isDefined)) {
val msgString = (msg.toList ++ ex.toList.map(ErrorHandling.reducedToString)).mkString("\n\t")
val log = getStreams(key, streams).log
val display = contextDisplay(state, ConsoleAppender.formatEnabledInEnv)
val display = contextDisplay(state, ITerminal.isColorEnabled)
log.error("(" + display.show(key) + ") " + msgString)
}
}

View File

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

View File

@ -14,7 +14,7 @@ import sbt.Def.ScopedKey
import sbt.Keys._
import sbt.Scope.GlobalScope
import sbt.internal.util.MainAppender._
import sbt.internal.util._
import sbt.internal.util.{ Terminal => ITerminal, _ }
import sbt.util.{ Level, LogExchange, Logger, LoggerContext }
import org.apache.logging.log4j.core.{ Appender => XAppender }
@ -319,7 +319,7 @@ object LogManager {
private[this] def slog: Logger =
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 success(message: => String) = slog.success(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.concurrent.duration._
import sbt.internal.util.ConsoleAppender
import sbt.internal.util.{ Terminal => ITerminal }
import sbt.internal.util.complete.SizeParser
// See also BuildPaths.scala
@ -106,7 +106,7 @@ object SysProp {
* 3. -Dsbt.colour=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")

View File

@ -9,7 +9,7 @@ package sbt
package internal.testing
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.protocol.testing._
import java.util.concurrent.atomic.AtomicInteger
@ -96,7 +96,7 @@ object TestLogger {
def debug(s: String) = log(Level.Debug, TestStringEvent(s))
def trace(t: Throwable) = logger.trace(t)
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 =