From 7902ec3b7d2e0764816b4b6b04a66d49e08bbd84 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 12 Dec 2019 11:24:48 -0800 Subject: [PATCH] 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. --- .../scala/sbt/internal/util/LineReader.scala | 127 ++++------- .../sbt/internal/util/ConsoleAppender.scala | 46 +--- .../scala/sbt/internal/util/Terminal.scala | 210 ++++++++++++++++++ main-actions/src/main/scala/sbt/Console.scala | 10 +- .../src/main/scala/sbt/BasicCommands.scala | 12 +- .../sbt/internal/client/NetworkClient.scala | 20 +- .../main/scala/sbt/CommandLineUIService.scala | 6 +- main/src/main/scala/sbt/MainLoop.scala | 5 +- .../scala/sbt/internal/ConsoleProject.scala | 4 +- .../main/scala/sbt/internal/Continuous.scala | 9 +- .../scala/sbt/internal/SettingGraph.scala | 10 +- 11 files changed, 305 insertions(+), 154 deletions(-) create mode 100644 internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala index d23bc91ff..b8704c97c 100644 --- a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala +++ b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala @@ -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) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala index 4142071b5..84af63381 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -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)(_)) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala new file mode 100644 index 000000000..5322b5869 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -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 +} diff --git a/main-actions/src/main/scala/sbt/Console.scala b/main-actions/src/main/scala/sbt/Console.scala index 413fac896..f05a599e0 100644 --- a/main-actions/src/main/scala/sbt/Console.scala +++ b/main-actions/src/main/scala/sbt/Console.scala @@ -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)) } } diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index bc541a90e..0fe030fb6 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -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) => diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 2419f4747..1de8de473 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -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") diff --git a/main/src/main/scala/sbt/CommandLineUIService.scala b/main/src/main/scala/sbt/CommandLineUIService.scala index e88521268..f2265a75f 100644 --- a/main/src/main/scala/sbt/CommandLineUIService.scala +++ b/main/src/main/scala/sbt/CommandLineUIService.scala @@ -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 diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 7f0f8d890..e6943ce54 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -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) diff --git a/main/src/main/scala/sbt/internal/ConsoleProject.scala b/main/src/main/scala/sbt/internal/ConsoleProject.scala index 9ea6a14c7..5e8bd9928 100644 --- a/main/src/main/scala/sbt/internal/ConsoleProject.scala +++ b/main/src/main/scala/sbt/internal/ConsoleProject.scala @@ -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, diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index 7b26e0776..82826c8fa 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -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] diff --git a/main/src/main/scala/sbt/internal/SettingGraph.scala b/main/src/main/scala/sbt/internal/SettingGraph.scala index 77e33c097..ae63dd713 100644 --- a/main/src/main/scala/sbt/internal/SettingGraph.scala +++ b/main/src/main/scala/sbt/internal/SettingGraph.scala @@ -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) + ".."