Add Terminal abstraction

This commit aims to centralize all of the terminal interactions
throughout sbt. It also seeks to hide the jline implementation details
and only expose the apis that sbt needs for interacting with the
terminal.

In general, we should be able to assume that the terminal is in
canonical (line buffered) mode with echo enabled. To switch to raw mode
or to enable/disable echo, there are apis: Terminal.withRawSystemIn and
Terminal.withEcho that take a thunk as parameter to ensure that the
terminal is reset back to the canonical mode afterwards.
This commit is contained in:
Ethan Atkins 2019-12-12 11:24:48 -08:00
parent cd65543d10
commit 7902ec3b7d
11 changed files with 305 additions and 154 deletions

View File

@ -7,30 +7,26 @@
package sbt.internal.util
import java.io._
import jline.console.ConsoleReader
import jline.console.history.{ FileHistory, MemoryHistory }
import java.io.{ File, FileDescriptor, FileInputStream, FilterInputStream, InputStream }
import sbt.internal.util.complete.Parser
import complete.Parser
import jline.Terminal
import scala.concurrent.duration._
import scala.annotation.tailrec
import scala.concurrent.duration._
abstract class JLine extends LineReader {
protected[this] def handleCONT: Boolean
protected[this] def reader: ConsoleReader
protected[this] def injectThreadSleep: Boolean
protected[this] lazy val in: InputStream = {
// On Windows InputStream#available doesn't seem to return positive number.
JLine.makeInputStream(injectThreadSleep && !Util.isNonCygwinWindows)
}
@deprecated("For binary compatibility only", "1.4.0")
protected[this] def injectThreadSleep: Boolean = false
@deprecated("For binary compatibility only", "1.4.0")
protected[this] lazy val in: InputStream = Terminal.wrappedSystemIn
def readLine(prompt: String, mask: Option[Char] = None) =
override def readLine(prompt: String, mask: Option[Char] = None): Option[String] =
try {
JLine.withJLine {
unsynchronizedReadLine(prompt, mask)
}
Terminal.withRawSystemIn(unsynchronizedReadLine(prompt, mask))
} catch {
case _: InterruptedException =>
// println("readLine: InterruptedException")
@ -85,88 +81,53 @@ abstract class JLine extends LineReader {
}
private[this] def resume(): Unit = {
jline.TerminalFactory.reset
JLine.terminal.init
Terminal.reset()
reader.drawLine()
reader.flush()
}
}
private[sbt] object JLine {
private[this] val TerminalProperty = "jline.terminal"
fixTerminalProperty()
// translate explicit class names to type in order to support
// older Scala, since it shaded classes but not the system property
private[sbt] def fixTerminalProperty(): Unit = {
val newValue = System.getProperty(TerminalProperty) match {
case "jline.UnixTerminal" => "unix"
case null if System.getProperty("sbt.cygwin") != null => "unix"
case "jline.WindowsTerminal" => "windows"
case "jline.AnsiWindowsTerminal" => "windows"
case "jline.UnsupportedTerminal" => "none"
case x => x
}
if (newValue != null) {
System.setProperty(TerminalProperty, newValue)
()
}
}
@deprecated("For binary compatibility only", "1.4.0")
protected[this] val originalIn = new FileInputStream(FileDescriptor.in)
@deprecated("Handled by Terminal.fixTerminalProperty", "1.4.0")
private[sbt] def fixTerminalProperty(): Unit = ()
private[sbt] def makeInputStream(injectThreadSleep: Boolean): InputStream =
if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds)
else originalIn
// When calling this, ensure that enableEcho has been or will be called.
// TerminalFactory.get will initialize the terminal to disable echo.
private[sbt] def terminal: Terminal = jline.TerminalFactory.get
private def withTerminal[T](f: jline.Terminal => T): T =
synchronized {
val t = terminal
t.synchronized { f(t) }
}
@deprecated("Don't use jline.Terminal directly", "1.4.0")
private[sbt] def terminal: jline.Terminal = Terminal.deprecatedTeminal
/**
* For accessing the JLine Terminal object.
* This ensures synchronized access as well as re-enabling echo after getting the Terminal.
*/
@deprecated("Don't use jline.Terminal directly. Use Terminal.withCanonicalIn instead.", "1.4.0")
def usingTerminal[T](f: jline.Terminal => T): T =
withTerminal { t =>
t.restore
val result = f(t)
t.restore
result
}
Terminal.withCanonicalIn(f(Terminal.deprecatedTeminal))
def createReader(): ConsoleReader = createReader(None, JLine.makeInputStream(true))
def createReader(): ConsoleReader = createReader(None, Terminal.wrappedSystemIn)
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader =
usingTerminal { _ =>
val cr = new ConsoleReader(in, System.out)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
case None => new MemoryHistory
case Some(file) => (new FileHistory(file): MemoryHistory)
}
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = {
val cr = Terminal.createReader(in)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
case None => new MemoryHistory
case Some(file) => new FileHistory(file): MemoryHistory
}
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr
}
def withJLine[T](action: => T): T =
withTerminal { t =>
t.init
try {
action
} finally {
t.restore
}
}
@deprecated("Avoid referencing JLine directly. Use Terminal.withRawSystemIn instead.", "1.4.0")
def withJLine[T](action: => T): T = Terminal.withRawSystemIn(action)
def simple(
historyPath: Option[File],
@ -180,6 +141,7 @@ private[sbt] object JLine {
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
}
@deprecated("For binary compatibility only", "1.4.0")
private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration)
extends FilterInputStream(is) {
@tailrec final override def read(): Int =
@ -211,11 +173,17 @@ trait LineReader {
final class FullReader(
historyPath: Option[File],
complete: Parser[_],
val handleCONT: Boolean = JLine.HandleCONT,
val injectThreadSleep: Boolean = false
val handleCONT: Boolean,
inputStream: InputStream,
) extends JLine {
protected[this] val reader = {
val cr = JLine.createReader(historyPath, in)
def this(
historyPath: Option[File],
complete: Parser[_],
handleCONT: Boolean = JLine.HandleCONT,
injectThreadSleep: Boolean = false
) = this(historyPath, complete, handleCONT, JLine.makeInputStream(injectThreadSleep))
protected[this] val reader: ConsoleReader = {
val cr = JLine.createReader(historyPath, inputStream)
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
cr
}
@ -224,9 +192,12 @@ final class FullReader(
class SimpleReader private[sbt] (
historyPath: Option[File],
val handleCONT: Boolean,
val injectThreadSleep: Boolean
inputStream: InputStream
) extends JLine {
protected[this] val reader = JLine.createReader(historyPath, in)
def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
this(historyPath, handleCONT, Terminal.wrappedSystemIn)
protected[this] val reader: ConsoleReader =
JLine.createReader(historyPath, inputStream)
}
object SimpleReader extends SimpleReader(None, JLine.HandleCONT, false)

View File

@ -7,17 +7,16 @@
package sbt.internal.util
import sbt.util._
import java.io.{ PrintStream, PrintWriter }
import java.lang.StringBuilder
import java.util.Locale
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, AtomicReference }
import org.apache.logging.log4j.{ Level => XLevel }
import org.apache.logging.log4j.message.{ Message, ObjectMessage, ReusableObjectMessage }
import org.apache.logging.log4j.core.{ LogEvent => XLogEvent }
import org.apache.logging.log4j.core.appender.AbstractAppender
import scala.util.control.NonFatal
import ConsoleAppender._
import org.apache.logging.log4j.core.{ LogEvent => XLogEvent }
import org.apache.logging.log4j.message.{ Message, ObjectMessage, ReusableObjectMessage }
import org.apache.logging.log4j.{ Level => XLevel }
import sbt.internal.util.ConsoleAppender._
import sbt.util._
object ConsoleLogger {
// These are provided so other modules do not break immediately.
@ -110,9 +109,6 @@ object ConsoleAppender {
private[sbt] final val DeleteLine = "\u001B[2K"
private[sbt] final val CursorLeft1000 = "\u001B[1000D"
private[sbt] final val CursorDown1 = cursorDown(1)
private[sbt] lazy val terminalWidth = usingTerminal { t =>
t.getWidth
}
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
def showProgress: Boolean = showProgressHolder.get
@ -293,31 +289,7 @@ object ConsoleAppender {
private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
private[this] def jline1to2CompatMsg = "Found class jline.Terminal, but interface was expected"
private[this] def ansiSupported =
try {
usingTerminal { t =>
t.isAnsiSupported
}
} catch {
case NonFatal(_) => !isWindows
}
/**
* For accessing the JLine Terminal object.
* This ensures re-enabling echo after getting the Terminal.
*/
private[this] def usingTerminal[T](f: jline.Terminal => T): T = {
val t = jline.TerminalFactory.get
t.restore
val result = f(t)
t.restore
result
}
private[this] def os = System.getProperty("os.name")
private[this] def isWindows = os.toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
private[this] def ansiSupported: Boolean = Terminal.isAnsiSupported
}
// See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2
@ -356,7 +328,7 @@ class ConsoleAppender private[ConsoleAppender] (
out.println(s"$DeleteLine$l")
if (progress.length > 0) {
val pad = if (padding.get > 0) padding.decrementAndGet() else 0
val width = ConsoleAppender.terminalWidth
val width = Terminal.getWidth
val len: Int = progress.foldLeft(progress.length)(_ + terminalLines(width)(_))
deleteConsoleLines(blankZone + pad)
progress.foreach(printProgressLine)
@ -387,7 +359,7 @@ class ConsoleAppender private[ConsoleAppender] (
s" | => ${item.name} ${elapsed}s"
}
val width = ConsoleAppender.terminalWidth
val width = Terminal.getWidth
val currentLength = info.foldLeft(info.length)(_ + terminalLines(width)(_))
val previousLines = progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(previousLines.length)(_ + terminalLines(width)(_))

View File

@ -0,0 +1,210 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import java.io.{ InputStream, OutputStream }
import java.util.Locale
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.locks.ReentrantLock
import jline.console.ConsoleReader
import scala.util.control.NonFatal
object Terminal {
/**
* Gets the current width of the terminal. The implementation reads a property from the jline
* config which is updated if it has been more than a second since the last update. It is thus
* possible for this value to be stale.
*
* @return the terminal width.
*/
def getWidth: Int = terminal.getWidth
/**
* Gets the current height of the terminal. The implementation reads a property from the jline
* config which is updated if it has been more than a second since the last update. It is thus
* possible for this value to be stale.
*
* @return the terminal height.
*/
def getHeight: Int = terminal.getHeight
/**
* Returns true if the current terminal supports ansi characters.
*
* @return true if the current terminal supports ansi escape codes.
*/
def isAnsiSupported: Boolean =
try terminal.isAnsiSupported
catch { case NonFatal(_) => !isWindows }
/**
* Returns true if System.in is attached. When sbt is run as a subprocess, like in scripted or
* as a server, System.in will not be attached and this method will return false. Otherwise
* it will return true.
*
* @return true if System.in is attached.
*/
def systemInIsAttached: Boolean = attached.get
/**
* Provides a wrapper around System.in. The wrapped stream in will check if the terminal is attached
* in available and read. If a read returns -1, it will mark System.in as unattached so that
* it can be detected by [[systemInIsAttached]].
*
* @return the wrapped InputStream
*/
private[sbt] def wrappedSystemIn: InputStream = WrappedSystemIn
/**
* Restore the terminal to its initial state.
*/
private[sbt] def restore(): Unit = terminal.restore()
/**
* Runs a thunk ensuring that the terminal has echo enabled. Most of the time sbt should have
* echo mode on except when it is explicitly set to raw mode via [[withRawSystemIn]].
*
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withEcho[T](toggle: Boolean)(f: => T): T = {
val previous = terminal.isEchoEnabled
terminalLock.lockInterruptibly()
try {
terminal.setEchoEnabled(toggle)
f
} finally {
terminal.setEchoEnabled(previous)
terminalLock.unlock()
}
}
/**
* Runs a thunk ensuring that the terminal is in canonical mode:
* [[https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html Canonical or Not]].
* Most of the time sbt should be in canonical mode except when it is explicitly set to raw mode
* via [[withRawSystemIn]].
*
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withCanonicalIn[T](f: => T): T = withTerminal { t =>
t.restore()
f
}
/**
* Runs a thunk ensuring that the terminal is in in non-canonical mode:
* [[https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html Canonical or Not]].
* This should be used when sbt is reading user input, e.g. in `shell` or a continuous build.
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withRawSystemIn[T](f: => T): T = withTerminal { t =>
t.init()
f
}
private[this] def withTerminal[T](f: jline.Terminal => T): T = {
val t = terminal
terminalLock.lockInterruptibly()
try f(t)
finally {
t.restore()
terminalLock.unlock()
}
}
private[this] object WrappedSystemIn extends InputStream {
private[this] val in = terminal.wrapInIfNeeded(System.in)
override def available(): Int = if (attached.get) in.available else 0
override def read(): Int = synchronized {
if (attached.get) {
val res = in.read
if (res == -1) attached.set(false)
res
} else -1
}
}
private[this] val terminalLock = new ReentrantLock()
private[this] val attached = new AtomicBoolean(true)
private[this] val terminalHolder = new AtomicReference(wrap(jline.TerminalFactory.get))
private[this] lazy val isWindows =
System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
private[this] def wrap(terminal: jline.Terminal): jline.Terminal = {
val term: jline.Terminal = new jline.Terminal {
private[this] val hasConsole = System.console != null
private[this] def alive = hasConsole && attached.get
override def init(): Unit = if (alive) terminal.init()
override def restore(): Unit = if (alive) terminal.restore()
override def reset(): Unit = if (alive) terminal.reset()
override def isSupported: Boolean = terminal.isSupported
override def getWidth: Int = terminal.getWidth
override def getHeight: Int = terminal.getHeight
override def isAnsiSupported: Boolean = terminal.isAnsiSupported
override def wrapOutIfNeeded(out: OutputStream): OutputStream = terminal.wrapOutIfNeeded(out)
override def wrapInIfNeeded(in: InputStream): InputStream = terminal.wrapInIfNeeded(in)
override def hasWeirdWrap: Boolean = terminal.hasWeirdWrap
override def isEchoEnabled: Boolean = terminal.isEchoEnabled
override def setEchoEnabled(enabled: Boolean): Unit = if (alive) {
terminal.setEchoEnabled(enabled)
}
override def disableInterruptCharacter(): Unit =
if (alive) terminal.disableInterruptCharacter()
override def enableInterruptCharacter(): Unit =
if (alive) terminal.enableInterruptCharacter()
override def getOutputEncoding: String = terminal.getOutputEncoding
}
term.restore()
term.setEchoEnabled(true)
term
}
private[util] def reset(): Unit = {
jline.TerminalFactory.reset()
terminalHolder.set(wrap(jline.TerminalFactory.get))
}
// translate explicit class names to type in order to support
// older Scala, since it shaded classes but not the system property
private[this] def fixTerminalProperty(): Unit = {
val terminalProperty = "jline.terminal"
val newValue = System.getProperty(terminalProperty) match {
case "jline.UnixTerminal" => "unix"
case null if System.getProperty("sbt.cygwin") != null => "unix"
case "jline.WindowsTerminal" => "windows"
case "jline.AnsiWindowsTerminal" => "windows"
case "jline.UnsupportedTerminal" => "none"
case x => x
}
if (newValue != null) {
System.setProperty(terminalProperty, newValue)
()
}
}
fixTerminalProperty()
private[sbt] def createReader(in: InputStream): ConsoleReader =
new ConsoleReader(in, System.out, terminal)
private[this] def terminal: jline.Terminal = terminalHolder.get match {
case null => throw new IllegalStateException("Uninitialized terminal.")
case term => term
}
@deprecated("For compatibility only", "1.4.0")
private[sbt] def deprecatedTeminal: jline.Terminal = terminal
}

View File

@ -9,9 +9,10 @@ package sbt
import java.io.File
import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile }
import sbt.internal.util.JLine
import sbt.internal.util.Terminal
import sbt.util.Logger
import xsbti.compile.{ Inputs, Compilers }
import xsbti.compile.{ Compilers, Inputs }
import scala.util.Try
final class Console(compiler: AnalyzingCompiler) {
@ -51,10 +52,7 @@ final class Console(compiler: AnalyzingCompiler) {
loader,
bindings
)
JLine.usingTerminal { t =>
t.init
Run.executeTrapExit(console0, log)
}
Terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
}
}

View File

@ -9,15 +9,15 @@ package sbt
import java.nio.file.Paths
import sbt.util.Level
import sbt.internal.util.{ AttributeKey, FullReader }
import sbt.internal.util.{ AttributeKey, FullReader, JLine, Terminal }
import sbt.internal.util.complete.{
Completion,
Completions,
DefaultParsers,
History => CHistory,
HistoryCommands,
Parser,
TokenCompletions
TokenCompletions,
History => CHistory
}
import sbt.internal.util.Types.{ const, idFun }
import sbt.internal.util.Util.{ AnyOps, nil, nilSeq, none }
@ -25,13 +25,14 @@ import sbt.internal.inc.classpath.ClasspathUtil.toLoader
import sbt.internal.inc.ModuleUtilities
import sbt.internal.client.NetworkClient
import DefaultParsers._
import Function.tupled
import Command.applyEffect
import BasicCommandStrings._
import CommandUtil._
import BasicKeys._
import java.io.File
import sbt.io.IO
import scala.collection.mutable.ListBuffer
@ -372,7 +373,8 @@ object BasicCommands {
def oldshell: Command = Command.command(OldShell, Help.more(Shell, OldShellDetailed)) { s =>
val history = (s get historyPath) getOrElse (new File(s.baseDir, ".history")).some
val prompt = (s get shellPrompt) match { case Some(pf) => pf(s); case None => "> " }
val reader = new FullReader(history, s.combinedParser)
val reader =
new FullReader(history, s.combinedParser, JLine.HandleCONT, Terminal.wrappedSystemIn)
val line = reader.readLine(prompt)
line match {
case Some(line) =>

View File

@ -12,19 +12,21 @@ package client
import java.io.{ File, IOException }
import java.util.UUID
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import scala.collection.mutable.ListBuffer
import scala.util.control.NonFatal
import scala.util.{ Success, Failure }
import scala.sys.process.{ BasicIO, Process, ProcessLogger }
import sbt.protocol._
import sbt.internal.protocol._
import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams }
import sbt.internal.util.{ JLine, ConsoleAppender }
import sbt.util.Level
import sbt.io.syntax._
import sbt.internal.protocol._
import sbt.internal.util.{ ConsoleAppender, JLine }
import sbt.io.IO
import sbt.io.syntax._
import sbt.protocol._
import sbt.util.Level
import sjsonnew.support.scalajson.unsafe.Converter
import scala.collection.mutable.ListBuffer
import scala.sys.process.{ BasicIO, Process, ProcessLogger }
import scala.util.control.NonFatal
import scala.util.{ Failure, Success }
class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[String]) { self =>
private val channelName = new AtomicReference("_")
private val status = new AtomicReference("Ready")

View File

@ -7,7 +7,7 @@
package sbt
import sbt.internal.util.{ JLine, SimpleReader }
import sbt.internal.util.{ SimpleReader, Terminal }
trait CommandLineUIService extends InteractionService {
override def readLine(prompt: String, mask: Boolean): Option[String] = {
@ -27,9 +27,9 @@ trait CommandLineUIService extends InteractionService {
}
}
override def terminalWidth: Int = JLine.terminal.getWidth
override def terminalWidth: Int = Terminal.getWidth
override def terminalHeight: Int = JLine.terminal.getHeight
override def terminalHeight: Int = Terminal.getHeight
}
object CommandLineUIService extends CommandLineUIService

View File

@ -10,12 +10,11 @@ package sbt
import java.io.PrintWriter
import java.util.Properties
import jline.TerminalFactory
import sbt.internal.{ Aggregation, ShutdownHooks }
import sbt.internal.langserver.ErrorCodes
import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.util.complete.Parser
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking }
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Terminal }
import sbt.io.{ IO, Using }
import sbt.protocol._
import sbt.util.Logger
@ -31,7 +30,7 @@ object MainLoop {
// We've disabled jline shutdown hooks to prevent classloader leaks, and have been careful to always restore
// the jline terminal in finally blocks, but hitting ctrl+c prevents finally blocks from being executed, in that
// case the only way to restore the terminal is in a shutdown hook.
val shutdownHook = ShutdownHooks.add(() => TerminalFactory.get().restore())
val shutdownHook = ShutdownHooks.add(Terminal.restore)
try {
runLoggedLoop(state, state.globalLogging.backing)

View File

@ -10,7 +10,7 @@ package internal
import sbt.internal.classpath.AlternativeZincUtil
import sbt.internal.inc.{ ScalaInstance, ZincLmUtil }
import sbt.internal.util.JLine
import sbt.internal.util.Terminal
import sbt.util.Logger
import xsbti.compile.ClasspathOptionsUtil
@ -61,7 +61,7 @@ object ConsoleProject {
val importString = imports.mkString("", ";\n", ";\n\n")
val initCommands = importString + extra
JLine.usingTerminal { _ =>
Terminal.withCanonicalIn {
// TODO - Hook up dsl classpath correctly...
(new Console(compiler))(
unit.classpath,

View File

@ -26,7 +26,7 @@ import sbt.internal.io.WatchState
import sbt.internal.nio._
import sbt.internal.util.complete.Parser._
import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.{ AttributeKey, JLine, Util }
import sbt.internal.util.{ AttributeKey, Terminal, Util }
import sbt.nio.Keys.{ fileInputs, _ }
import sbt.nio.Watch.{ Creation, Deletion, ShowOptions, Update }
import sbt.nio.file.{ FileAttributes, Glob }
@ -272,11 +272,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
f(s, valid, invalid)
}
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = {
val terminal = JLine.terminal
terminal.init()
terminal.setEchoEnabled(true)
val wrapped = terminal.wrapInIfNeeded(System.in)
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = Terminal.withRawSystemIn {
val wrapped = Terminal.wrappedSystemIn
if (Util.isNonCygwinWindows) {
val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable {
private[this] val buffer = new java.util.LinkedList[Int]

View File

@ -8,13 +8,13 @@
package sbt
package internal
import sbt.internal.util.{ JLine }
import sbt.util.Show
import java.io.File
import Def.{ compiled, flattenLocals, ScopedKey }
import Predef.{ any2stringadd => _, _ }
import Def.{ ScopedKey, compiled, flattenLocals }
import sbt.internal.util.Terminal
import Predef.{ any2stringadd => _, _ }
import sbt.io.IO
object SettingGraph {
@ -82,7 +82,7 @@ object Graph {
// [info] |
// [info] +-quux
def toAscii[A](top: A, children: A => Seq[A], display: A => String, defaultWidth: Int): String = {
val maxColumn = math.max(JLine.terminal.getWidth, defaultWidth) - 8
val maxColumn = math.max(Terminal.getWidth, defaultWidth) - 8
val twoSpaces = " " + " " // prevent accidentally being converted into a tab
def limitLine(s: String): String =
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."