mirror of https://github.com/sbt/sbt.git
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:
parent
cd65543d10
commit
7902ec3b7d
|
|
@ -7,30 +7,26 @@
|
||||||
|
|
||||||
package sbt.internal.util
|
package sbt.internal.util
|
||||||
|
|
||||||
|
import java.io._
|
||||||
|
|
||||||
import jline.console.ConsoleReader
|
import jline.console.ConsoleReader
|
||||||
import jline.console.history.{ FileHistory, MemoryHistory }
|
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.annotation.tailrec
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
abstract class JLine extends LineReader {
|
abstract class JLine extends LineReader {
|
||||||
protected[this] def handleCONT: Boolean
|
protected[this] def handleCONT: Boolean
|
||||||
protected[this] def reader: ConsoleReader
|
protected[this] def reader: ConsoleReader
|
||||||
protected[this] def injectThreadSleep: Boolean
|
@deprecated("For binary compatibility only", "1.4.0")
|
||||||
protected[this] lazy val in: InputStream = {
|
protected[this] def injectThreadSleep: Boolean = false
|
||||||
// On Windows InputStream#available doesn't seem to return positive number.
|
@deprecated("For binary compatibility only", "1.4.0")
|
||||||
JLine.makeInputStream(injectThreadSleep && !Util.isNonCygwinWindows)
|
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 {
|
try {
|
||||||
JLine.withJLine {
|
Terminal.withRawSystemIn(unsynchronizedReadLine(prompt, mask))
|
||||||
unsynchronizedReadLine(prompt, mask)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
case _: InterruptedException =>
|
case _: InterruptedException =>
|
||||||
// println("readLine: InterruptedException")
|
// println("readLine: InterruptedException")
|
||||||
|
|
@ -85,88 +81,53 @@ abstract class JLine extends LineReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
private[this] def resume(): Unit = {
|
private[this] def resume(): Unit = {
|
||||||
jline.TerminalFactory.reset
|
Terminal.reset()
|
||||||
JLine.terminal.init
|
|
||||||
reader.drawLine()
|
reader.drawLine()
|
||||||
reader.flush()
|
reader.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private[sbt] object JLine {
|
private[sbt] object JLine {
|
||||||
private[this] val TerminalProperty = "jline.terminal"
|
@deprecated("For binary compatibility only", "1.4.0")
|
||||||
|
|
||||||
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)
|
|
||||||
()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected[this] val originalIn = new FileInputStream(FileDescriptor.in)
|
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 =
|
private[sbt] def makeInputStream(injectThreadSleep: Boolean): InputStream =
|
||||||
if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds)
|
if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds)
|
||||||
else originalIn
|
else originalIn
|
||||||
|
|
||||||
// When calling this, ensure that enableEcho has been or will be called.
|
// When calling this, ensure that enableEcho has been or will be called.
|
||||||
// TerminalFactory.get will initialize the terminal to disable echo.
|
// TerminalFactory.get will initialize the terminal to disable echo.
|
||||||
private[sbt] def terminal: Terminal = jline.TerminalFactory.get
|
@deprecated("Don't use jline.Terminal directly", "1.4.0")
|
||||||
|
private[sbt] def terminal: jline.Terminal = Terminal.deprecatedTeminal
|
||||||
private def withTerminal[T](f: jline.Terminal => T): T =
|
|
||||||
synchronized {
|
|
||||||
val t = terminal
|
|
||||||
t.synchronized { f(t) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For accessing the JLine Terminal object.
|
* For accessing the JLine Terminal object.
|
||||||
* This ensures synchronized access as well as re-enabling echo after getting the Terminal.
|
* 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 =
|
def usingTerminal[T](f: jline.Terminal => T): T =
|
||||||
withTerminal { t =>
|
Terminal.withCanonicalIn(f(Terminal.deprecatedTeminal))
|
||||||
t.restore
|
|
||||||
val result = f(t)
|
|
||||||
t.restore
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
def createReader(): ConsoleReader = createReader(None, JLine.makeInputStream(true))
|
def createReader(): ConsoleReader = createReader(None, Terminal.wrappedSystemIn)
|
||||||
|
|
||||||
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader =
|
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = {
|
||||||
usingTerminal { _ =>
|
val cr = Terminal.createReader(in)
|
||||||
val cr = new ConsoleReader(in, System.out)
|
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
|
||||||
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
|
cr.setBellEnabled(false)
|
||||||
cr.setBellEnabled(false)
|
val h = historyPath match {
|
||||||
val h = historyPath match {
|
case None => new MemoryHistory
|
||||||
case None => new MemoryHistory
|
case Some(file) => new FileHistory(file): MemoryHistory
|
||||||
case Some(file) => (new FileHistory(file): MemoryHistory)
|
|
||||||
}
|
|
||||||
h.setMaxSize(MaxHistorySize)
|
|
||||||
cr.setHistory(h)
|
|
||||||
cr
|
|
||||||
}
|
}
|
||||||
|
h.setMaxSize(MaxHistorySize)
|
||||||
|
cr.setHistory(h)
|
||||||
|
cr
|
||||||
|
}
|
||||||
|
|
||||||
def withJLine[T](action: => T): T =
|
@deprecated("Avoid referencing JLine directly. Use Terminal.withRawSystemIn instead.", "1.4.0")
|
||||||
withTerminal { t =>
|
def withJLine[T](action: => T): T = Terminal.withRawSystemIn(action)
|
||||||
t.init
|
|
||||||
try {
|
|
||||||
action
|
|
||||||
} finally {
|
|
||||||
t.restore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def simple(
|
def simple(
|
||||||
historyPath: Option[File],
|
historyPath: Option[File],
|
||||||
|
|
@ -180,6 +141,7 @@ private[sbt] object JLine {
|
||||||
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
|
!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)
|
private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration)
|
||||||
extends FilterInputStream(is) {
|
extends FilterInputStream(is) {
|
||||||
@tailrec final override def read(): Int =
|
@tailrec final override def read(): Int =
|
||||||
|
|
@ -211,11 +173,17 @@ trait LineReader {
|
||||||
final class FullReader(
|
final class FullReader(
|
||||||
historyPath: Option[File],
|
historyPath: Option[File],
|
||||||
complete: Parser[_],
|
complete: Parser[_],
|
||||||
val handleCONT: Boolean = JLine.HandleCONT,
|
val handleCONT: Boolean,
|
||||||
val injectThreadSleep: Boolean = false
|
inputStream: InputStream,
|
||||||
) extends JLine {
|
) extends JLine {
|
||||||
protected[this] val reader = {
|
def this(
|
||||||
val cr = JLine.createReader(historyPath, in)
|
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)
|
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
|
||||||
cr
|
cr
|
||||||
}
|
}
|
||||||
|
|
@ -224,9 +192,12 @@ final class FullReader(
|
||||||
class SimpleReader private[sbt] (
|
class SimpleReader private[sbt] (
|
||||||
historyPath: Option[File],
|
historyPath: Option[File],
|
||||||
val handleCONT: Boolean,
|
val handleCONT: Boolean,
|
||||||
val injectThreadSleep: Boolean
|
inputStream: InputStream
|
||||||
) extends JLine {
|
) 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)
|
object SimpleReader extends SimpleReader(None, JLine.HandleCONT, false)
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,16 @@
|
||||||
|
|
||||||
package sbt.internal.util
|
package sbt.internal.util
|
||||||
|
|
||||||
import sbt.util._
|
|
||||||
import java.io.{ PrintStream, PrintWriter }
|
import java.io.{ PrintStream, PrintWriter }
|
||||||
import java.lang.StringBuilder
|
import java.lang.StringBuilder
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, AtomicReference }
|
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 org.apache.logging.log4j.core.appender.AbstractAppender
|
||||||
import scala.util.control.NonFatal
|
import org.apache.logging.log4j.core.{ LogEvent => XLogEvent }
|
||||||
import ConsoleAppender._
|
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 {
|
object ConsoleLogger {
|
||||||
// These are provided so other modules do not break immediately.
|
// 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 DeleteLine = "\u001B[2K"
|
||||||
private[sbt] final val CursorLeft1000 = "\u001B[1000D"
|
private[sbt] final val CursorLeft1000 = "\u001B[1000D"
|
||||||
private[sbt] final val CursorDown1 = cursorDown(1)
|
private[sbt] final val CursorDown1 = cursorDown(1)
|
||||||
private[sbt] lazy val terminalWidth = usingTerminal { t =>
|
|
||||||
t.getWidth
|
|
||||||
}
|
|
||||||
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
|
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
|
||||||
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
|
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
|
||||||
def showProgress: Boolean = showProgressHolder.get
|
def showProgress: Boolean = showProgressHolder.get
|
||||||
|
|
@ -293,31 +289,7 @@ object ConsoleAppender {
|
||||||
|
|
||||||
private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
|
private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
|
||||||
|
|
||||||
private[this] def jline1to2CompatMsg = "Found class jline.Terminal, but interface was expected"
|
private[this] def ansiSupported: Boolean = Terminal.isAnsiSupported
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2
|
// 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")
|
out.println(s"$DeleteLine$l")
|
||||||
if (progress.length > 0) {
|
if (progress.length > 0) {
|
||||||
val pad = if (padding.get > 0) padding.decrementAndGet() else 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)(_))
|
val len: Int = progress.foldLeft(progress.length)(_ + terminalLines(width)(_))
|
||||||
deleteConsoleLines(blankZone + pad)
|
deleteConsoleLines(blankZone + pad)
|
||||||
progress.foreach(printProgressLine)
|
progress.foreach(printProgressLine)
|
||||||
|
|
@ -387,7 +359,7 @@ class ConsoleAppender private[ConsoleAppender] (
|
||||||
s" | => ${item.name} ${elapsed}s"
|
s" | => ${item.name} ${elapsed}s"
|
||||||
}
|
}
|
||||||
|
|
||||||
val width = ConsoleAppender.terminalWidth
|
val width = Terminal.getWidth
|
||||||
val currentLength = info.foldLeft(info.length)(_ + terminalLines(width)(_))
|
val currentLength = info.foldLeft(info.length)(_ + terminalLines(width)(_))
|
||||||
val previousLines = progressLines.getAndSet(info)
|
val previousLines = progressLines.getAndSet(info)
|
||||||
val prevLength = previousLines.foldLeft(previousLines.length)(_ + terminalLines(width)(_))
|
val prevLength = previousLines.foldLeft(previousLines.length)(_ + terminalLines(width)(_))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -9,9 +9,10 @@ package sbt
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile }
|
import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile }
|
||||||
import sbt.internal.util.JLine
|
import sbt.internal.util.Terminal
|
||||||
import sbt.util.Logger
|
import sbt.util.Logger
|
||||||
import xsbti.compile.{ Inputs, Compilers }
|
import xsbti.compile.{ Compilers, Inputs }
|
||||||
|
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
final class Console(compiler: AnalyzingCompiler) {
|
final class Console(compiler: AnalyzingCompiler) {
|
||||||
|
|
@ -51,10 +52,7 @@ final class Console(compiler: AnalyzingCompiler) {
|
||||||
loader,
|
loader,
|
||||||
bindings
|
bindings
|
||||||
)
|
)
|
||||||
JLine.usingTerminal { t =>
|
Terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
|
||||||
t.init
|
|
||||||
Run.executeTrapExit(console0, log)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@ package sbt
|
||||||
|
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import sbt.util.Level
|
import sbt.util.Level
|
||||||
import sbt.internal.util.{ AttributeKey, FullReader }
|
import sbt.internal.util.{ AttributeKey, FullReader, JLine, Terminal }
|
||||||
import sbt.internal.util.complete.{
|
import sbt.internal.util.complete.{
|
||||||
Completion,
|
Completion,
|
||||||
Completions,
|
Completions,
|
||||||
DefaultParsers,
|
DefaultParsers,
|
||||||
History => CHistory,
|
|
||||||
HistoryCommands,
|
HistoryCommands,
|
||||||
Parser,
|
Parser,
|
||||||
TokenCompletions
|
TokenCompletions,
|
||||||
|
History => CHistory
|
||||||
}
|
}
|
||||||
import sbt.internal.util.Types.{ const, idFun }
|
import sbt.internal.util.Types.{ const, idFun }
|
||||||
import sbt.internal.util.Util.{ AnyOps, nil, nilSeq, none }
|
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.inc.ModuleUtilities
|
||||||
import sbt.internal.client.NetworkClient
|
import sbt.internal.client.NetworkClient
|
||||||
import DefaultParsers._
|
import DefaultParsers._
|
||||||
|
|
||||||
import Function.tupled
|
import Function.tupled
|
||||||
import Command.applyEffect
|
import Command.applyEffect
|
||||||
import BasicCommandStrings._
|
import BasicCommandStrings._
|
||||||
import CommandUtil._
|
import CommandUtil._
|
||||||
import BasicKeys._
|
import BasicKeys._
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
import sbt.io.IO
|
import sbt.io.IO
|
||||||
|
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
@ -372,7 +373,8 @@ object BasicCommands {
|
||||||
def oldshell: Command = Command.command(OldShell, Help.more(Shell, OldShellDetailed)) { s =>
|
def oldshell: Command = Command.command(OldShell, Help.more(Shell, OldShellDetailed)) { s =>
|
||||||
val history = (s get historyPath) getOrElse (new File(s.baseDir, ".history")).some
|
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 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)
|
val line = reader.readLine(prompt)
|
||||||
line match {
|
line match {
|
||||||
case Some(line) =>
|
case Some(line) =>
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,21 @@ package client
|
||||||
import java.io.{ File, IOException }
|
import java.io.{ File, IOException }
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
|
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.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams }
|
||||||
import sbt.internal.util.{ JLine, ConsoleAppender }
|
import sbt.internal.protocol._
|
||||||
import sbt.util.Level
|
import sbt.internal.util.{ ConsoleAppender, JLine }
|
||||||
import sbt.io.syntax._
|
|
||||||
import sbt.io.IO
|
import sbt.io.IO
|
||||||
|
import sbt.io.syntax._
|
||||||
|
import sbt.protocol._
|
||||||
|
import sbt.util.Level
|
||||||
import sjsonnew.support.scalajson.unsafe.Converter
|
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 =>
|
class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[String]) { self =>
|
||||||
private val channelName = new AtomicReference("_")
|
private val channelName = new AtomicReference("_")
|
||||||
private val status = new AtomicReference("Ready")
|
private val status = new AtomicReference("Ready")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
package sbt
|
package sbt
|
||||||
|
|
||||||
import sbt.internal.util.{ JLine, SimpleReader }
|
import sbt.internal.util.{ SimpleReader, Terminal }
|
||||||
|
|
||||||
trait CommandLineUIService extends InteractionService {
|
trait CommandLineUIService extends InteractionService {
|
||||||
override def readLine(prompt: String, mask: Boolean): Option[String] = {
|
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
|
object CommandLineUIService extends CommandLineUIService
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,11 @@ package sbt
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
import jline.TerminalFactory
|
|
||||||
import sbt.internal.{ Aggregation, ShutdownHooks }
|
import sbt.internal.{ Aggregation, ShutdownHooks }
|
||||||
import sbt.internal.langserver.ErrorCodes
|
import sbt.internal.langserver.ErrorCodes
|
||||||
import sbt.internal.protocol.JsonRpcResponseError
|
import sbt.internal.protocol.JsonRpcResponseError
|
||||||
import sbt.internal.util.complete.Parser
|
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.io.{ IO, Using }
|
||||||
import sbt.protocol._
|
import sbt.protocol._
|
||||||
import sbt.util.Logger
|
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
|
// 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
|
// 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.
|
// 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 {
|
try {
|
||||||
runLoggedLoop(state, state.globalLogging.backing)
|
runLoggedLoop(state, state.globalLogging.backing)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ package internal
|
||||||
|
|
||||||
import sbt.internal.classpath.AlternativeZincUtil
|
import sbt.internal.classpath.AlternativeZincUtil
|
||||||
import sbt.internal.inc.{ ScalaInstance, ZincLmUtil }
|
import sbt.internal.inc.{ ScalaInstance, ZincLmUtil }
|
||||||
import sbt.internal.util.JLine
|
import sbt.internal.util.Terminal
|
||||||
import sbt.util.Logger
|
import sbt.util.Logger
|
||||||
import xsbti.compile.ClasspathOptionsUtil
|
import xsbti.compile.ClasspathOptionsUtil
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ object ConsoleProject {
|
||||||
val importString = imports.mkString("", ";\n", ";\n\n")
|
val importString = imports.mkString("", ";\n", ";\n\n")
|
||||||
val initCommands = importString + extra
|
val initCommands = importString + extra
|
||||||
|
|
||||||
JLine.usingTerminal { _ =>
|
Terminal.withCanonicalIn {
|
||||||
// TODO - Hook up dsl classpath correctly...
|
// TODO - Hook up dsl classpath correctly...
|
||||||
(new Console(compiler))(
|
(new Console(compiler))(
|
||||||
unit.classpath,
|
unit.classpath,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import sbt.internal.io.WatchState
|
||||||
import sbt.internal.nio._
|
import sbt.internal.nio._
|
||||||
import sbt.internal.util.complete.Parser._
|
import sbt.internal.util.complete.Parser._
|
||||||
import sbt.internal.util.complete.{ Parser, Parsers }
|
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.Keys.{ fileInputs, _ }
|
||||||
import sbt.nio.Watch.{ Creation, Deletion, ShowOptions, Update }
|
import sbt.nio.Watch.{ Creation, Deletion, ShowOptions, Update }
|
||||||
import sbt.nio.file.{ FileAttributes, Glob }
|
import sbt.nio.file.{ FileAttributes, Glob }
|
||||||
|
|
@ -272,11 +272,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
||||||
f(s, valid, invalid)
|
f(s, valid, invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = {
|
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = Terminal.withRawSystemIn {
|
||||||
val terminal = JLine.terminal
|
val wrapped = Terminal.wrappedSystemIn
|
||||||
terminal.init()
|
|
||||||
terminal.setEchoEnabled(true)
|
|
||||||
val wrapped = terminal.wrapInIfNeeded(System.in)
|
|
||||||
if (Util.isNonCygwinWindows) {
|
if (Util.isNonCygwinWindows) {
|
||||||
val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable {
|
val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable {
|
||||||
private[this] val buffer = new java.util.LinkedList[Int]
|
private[this] val buffer = new java.util.LinkedList[Int]
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@
|
||||||
package sbt
|
package sbt
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import sbt.internal.util.{ JLine }
|
|
||||||
import sbt.util.Show
|
import sbt.util.Show
|
||||||
|
|
||||||
import java.io.File
|
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
|
import sbt.io.IO
|
||||||
|
|
||||||
object SettingGraph {
|
object SettingGraph {
|
||||||
|
|
@ -82,7 +82,7 @@ object Graph {
|
||||||
// [info] |
|
// [info] |
|
||||||
// [info] +-quux
|
// [info] +-quux
|
||||||
def toAscii[A](top: A, children: A => Seq[A], display: A => String, defaultWidth: Int): String = {
|
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
|
val twoSpaces = " " + " " // prevent accidentally being converted into a tab
|
||||||
def limitLine(s: String): String =
|
def limitLine(s: String): String =
|
||||||
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."
|
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue