Upgrade LineReader to JLine3

This commit upgrades sbt to using jline3. The advantage to jline3 is
that it has a significantly better tab completion engine that is more
similar to what you get from zsh or fish.

The diff is bigger than I'd hoped because there are a number of
behaviors that are different in jline3 vs jline2 in how the library
consumes input streams and implements various features. I also was
unable to remove jline2 because we need it for older versions of the
scala console to work correctly with the thin client. As a result, the
changes are largely additive.

A good amount of this commit was in adding more protocol so that the
remote client can forward its jline3 terminal information to the server.

There were a number of minor changes that I made that either fixed
outstanding ui bugs from #5620 or regressions due to differences between
jline3 and jline2.

The number one thing that caused problems is that the jline3 LineReader
insists on using a NonBlockingInputStream. The implementation ofo
NonBlockingInputStream seems buggy. Moreover, sbt internally uses a
non blocking input stream for system in so jline is adding non blocking
to an already non blocking stream, which is frustrating.

A long term solution might be to consider insourcing LineReader.java
from jline3 and just adapting it to use an sbt terminal rather than
fighting with the jline3 api. This would also have the advantage of not
conflicting with other versions of jline3. Even if we don't, we may want to
shade jline3 if that is possible.
This commit is contained in:
Ethan Atkins 2020-06-30 08:57:57 -07:00
parent ed4d40d3e2
commit 2ecf5967ee
40 changed files with 1330 additions and 309 deletions

View File

@ -289,6 +289,7 @@ val completeProj = (project in file("internal") / "util-complete")
testedBaseSettings,
name := "Completion",
libraryDependencies += jline,
libraryDependencies += jline3,
mimaSettings,
// Parser is used publicly, so we can't break bincompat.
mimaBinaryIssueFilters := Seq(
@ -343,12 +344,20 @@ lazy val utilPosition = (project in file("internal") / "util-position")
lazy val utilLogging = (project in file("internal") / "util-logging")
.enablePlugins(ContrabandPlugin, JsonCodecPlugin)
.dependsOn(utilInterface)
.dependsOn(utilInterface, collectionProj)
.settings(
utilCommonSettings,
name := "Util Logging",
libraryDependencies ++=
Seq(jline, log4jApi, log4jCore, disruptor, sjsonNewScalaJson.value, scalaReflect.value),
Seq(
jline,
jline3,
log4jApi,
log4jCore,
disruptor,
sjsonNewScalaJson.value,
scalaReflect.value
),
libraryDependencies ++= Seq(scalacheck % "test", scalatest % "test"),
libraryDependencies ++= (scalaVersion.value match {
case v if v.startsWith("2.12.") => List(compilerPlugin(silencerPlugin))
@ -1047,8 +1056,7 @@ lazy val sbtClientProj = (project in file("client"))
crossPaths := false,
exportJars := true,
libraryDependencies += jansi,
libraryDependencies += "net.java.dev.jna" % "jna" % "5.5.0",
libraryDependencies += "net.java.dev.jna" % "jna-platform" % "5.5.0",
libraryDependencies += jline3Jansi,
libraryDependencies += scalatest % "test",
/*
* On windows, the raw classpath is too large to be a command argument to an

View File

@ -1,6 +1,19 @@
{
"resources":[
{"pattern":"jline/console/completer/CandidateListCompletionHandler.properties"},
{"pattern":"org/jline/utils/ansi.caps"},
{"pattern":"org/jline/utils/capabilities.txt"},
{"pattern":"org/jline/utils/colors.txt"},
{"pattern":"org/jline/utils/dumb-color.caps"},
{"pattern":"org/jline/utils/xterm.caps"},
{"pattern":"org/jline/utils/xterm-256color.caps"},
{"pattern":"org/jline/utils/windows-256color.caps"},
{"pattern":"org/jline/utils/screen-256color.caps"},
{"pattern":"org/jline/utils/windows.caps"},
{"pattern":"org/jline/utils/windows-conemu.caps"},
{"pattern":"org/jline/utils/dumb.caps"},
{"pattern":"org/jline/utils/windows-vtp.caps"},
{"pattern":"org/jline/utils/screen.caps"},
{"pattern":"library.properties"},
{"pattern":"darwin/x86_64/libsbtipcsocket.dylib"},
{"pattern":"linux/x86_64/libsbtipcsocket.so"},

View File

@ -8,16 +8,28 @@
package sbt.internal.util
import java.io._
import java.util.{ List => JList }
import jline.console.ConsoleReader
import jline.console.history.{ FileHistory, MemoryHistory }
import org.jline.reader.{
Candidate,
Completer,
EndOfFileException,
LineReader => JLineReader,
LineReaderBuilder,
ParsedLine,
UserInterruptException,
}
import sbt.internal.util.complete.Parser
import scala.annotation.tailrec
import scala.concurrent.duration._
import java.nio.channels.ClosedByInterruptException
trait LineReader {
trait LineReader extends AutoCloseable {
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
override def close(): Unit = {}
}
object LineReader {
@ -25,7 +37,67 @@ object LineReader {
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
val MaxHistorySize = 500
private def completer(parser: Parser[_]): Completer = new Completer {
def complete(lr: JLineReader, pl: ParsedLine, candidates: JList[Candidate]): Unit = {
Parser.completions(parser, pl.line(), 10).get.foreach { c =>
/*
* For commands like `~` that delegate parsing to another parser, the `~` may be
* excluded from the completion result. For example,
* ~testOnly <TAB>
* might return results like
* 'testOnly ;'
* 'testOnly com.foo.FooSpec'
* ...
* If we use the raw display, JLine will reject the completions because they are
* missing the leading `~`. To workaround this, we append to the result to the
* line provided the line does not end with " ". This fixes the missing `~` in
* the prefix problem. We also need to split the line on space and take the
* last token and append to that otherwise the completion will double print
* the prefix, so that `testOnly com<Tab>` might expand to something like:
* `testOnly testOnly\ com.foo.FooSpec` instead of `testOnly com.foo.FooSpec`.
*/
if (c.append.nonEmpty) {
if (!pl.line().endsWith(" ")) {
candidates.add(new Candidate(pl.line().split(" ").last + c.append))
} else {
candidates.add(new Candidate(c.append))
}
}
}
}
}
def createReader(
historyPath: Option[File],
parser: Parser[_],
terminal: Terminal,
prompt: Prompt = Prompt.Running,
): LineReader = {
val term = JLine3(terminal)
// We may want to consider insourcing LineReader.java from jline. We don't otherwise
// directly need jline3 for sbt.
val reader = LineReaderBuilder.builder().terminal(term).completer(completer(parser)).build()
historyPath.foreach(f => reader.setVariable(JLineReader.HISTORY_FILE, f))
new LineReader {
override def readLine(prompt: String, mask: Option[Char]): Option[String] = {
try terminal.withRawSystemIn {
Option(mask.map(reader.readLine(prompt, _)).getOrElse(reader.readLine(prompt)))
} catch {
case e: EndOfFileException =>
if (terminal == Terminal.console && System.console == null) None
else Some("exit")
case _: IOError => Some("exit")
case _: UserInterruptException | _: ClosedByInterruptException |
_: UncheckedIOException =>
throw new InterruptedException
} finally {
terminal.prompt.reset()
term.close()
}
}
}
}
def createJLine2Reader(
historyPath: Option[File],
terminal: Terminal,
prompt: Prompt = Prompt.Running,
@ -42,7 +114,6 @@ object LineReader {
cr.setHistoryEnabled(true)
cr
}
def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal)
def simple(
historyPath: Option[File],
@ -230,7 +301,7 @@ final class FullReader(
Terminal.console
)
protected[this] val reader: ConsoleReader = {
val cr = LineReader.createReader(historyPath, terminal)
val cr = LineReader.createJLine2Reader(historyPath, terminal)
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
cr
}
@ -244,7 +315,7 @@ class SimpleReader private[sbt] (
def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
this(historyPath, handleCONT, Terminal.console)
protected[this] val reader: ConsoleReader =
LineReader.createReader(historyPath, terminal)
LineReader.createJLine2Reader(historyPath, terminal)
}
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) {

View File

@ -9,6 +9,7 @@ package sbt.internal.util
import java.io.{ PrintStream, PrintWriter }
import java.lang.StringBuilder
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import org.apache.logging.log4j.core.appender.AbstractAppender
@ -394,7 +395,8 @@ class ConsoleAppender private[ConsoleAppender] (
override def append(event: XLogEvent): Unit = {
val level = ConsoleAppender.toLevel(event.getLevel)
val message = event.getMessage
appendMessage(level, message)
try appendMessage(level, message)
catch { case _: ClosedChannelException => }
}
/**

View File

@ -181,9 +181,10 @@ object EscHelpers {
else res(index) = 32
case 'm' =>
case ';' => state = csi
case _ =>
case b => state = csi
}
digit.clear()
case b if state == esc => state = 0
case b =>
res(index) = b
index += 1

View File

@ -0,0 +1,223 @@
/*
* 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.{ EOFException, InputStream, OutputStream, PrintWriter }
import java.nio.charset.Charset
import java.util.{ Arrays, EnumSet }
import java.util.concurrent.atomic.AtomicBoolean
import org.jline.utils.InfoCmp.Capability
import org.jline.utils.{ NonBlocking, OSUtils }
import org.jline.terminal.{ Attributes, Size, Terminal => JTerminal }
import org.jline.terminal.Terminal.SignalHandler
import org.jline.terminal.impl.AbstractTerminal
import org.jline.terminal.impl.jansi.JansiSupportImpl
import org.jline.terminal.impl.jansi.win.JansiWinSysTerminal
import scala.collection.JavaConverters._
import scala.util.Try
private[util] object JLine3 {
private val capabilityMap = Capability
.values()
.map { c =>
c.toString -> c
}
.toMap
private[util] def system = {
/*
* For reasons that are unclear to me, TerminalBuilder fails to build
* windows terminals. The instructions about the classpath did not work:
* https://stackoverflow.com/questions/52851232/jline3-issues-with-windows-terminal
* We can deconstruct what TerminalBuilder does and inline it for now.
* It is possible that this workaround will break WSL but I haven't checked that.
*/
if (Util.isNonCygwinWindows) {
val support = new JansiSupportImpl
val winConsole = support.isWindowsConsole();
try {
val term = JansiWinSysTerminal.createTerminal(
"console",
"ansi",
OSUtils.IS_CONEMU,
Charset.forName("UTF-8"),
-1,
false,
SignalHandler.SIG_DFL,
true
)
term.disableScrolling()
term
} catch {
case _: Exception =>
org.jline.terminal.TerminalBuilder
.builder()
.system(false)
.paused(true)
.jansi(true)
.streams(Terminal.console.inputStream, Terminal.console.outputStream)
.build()
}
} else {
org.jline.terminal.TerminalBuilder
.builder()
.system(System.console != null)
.paused(true)
.jna(false)
.jansi(true)
.build()
}
}
private[sbt] def apply(term: Terminal): JTerminal = {
new AbstractTerminal(term.name, "ansi", Charset.forName("UTF-8"), SignalHandler.SIG_DFL) {
val closed = new AtomicBoolean(false)
setOnClose { () =>
if (closed.compareAndSet(false, true)) {
// This is necessary to shutdown the non blocking input reader
// so that it doesn't keep blocking
term.inputStream match {
case w: Terminal.WriteableInputStream => w.cancel()
case _ =>
}
}
}
parseInfoCmp()
override val input: InputStream = new InputStream {
override def read: Int = {
val res = try term.inputStream.read
catch { case _: InterruptedException => -2 }
if (res == 4 && term.prompt.render().endsWith(term.prompt.mkPrompt()))
throw new EOFException
res
}
}
override val output: OutputStream = new OutputStream {
override def write(b: Int): Unit = write(Array[Byte](b.toByte))
override def write(b: Array[Byte]): Unit = if (!closed.get) term.withPrintStream { ps =>
term.prompt match {
case a: Prompt.AskUser => a.write(b)
case _ =>
}
ps.write(b)
}
override def write(b: Array[Byte], offset: Int, len: Int) =
write(Arrays.copyOfRange(b, offset, offset + len))
override def flush(): Unit = term.withPrintStream(_.flush())
}
override val reader =
NonBlocking.nonBlocking(term.name, input, Charset.defaultCharset())
override val writer: PrintWriter = new PrintWriter(output, true)
/*
* For now assume that the terminal capabilities for client and server
* are the same.
*/
override def getStringCapability(cap: Capability): String = {
term.getStringCapability(cap.toString, jline3 = true)
}
override def getNumericCapability(cap: Capability): Integer = {
term.getNumericCapability(cap.toString, jline3 = true)
}
override def getBooleanCapability(cap: Capability): Boolean = {
term.getBooleanCapability(cap.toString, jline3 = true)
}
def getAttributes(): Attributes = attributesFromMap(term.getAttributes)
def getSize(): Size = new Size(term.getWidth, term.getHeight)
def setAttributes(a: Attributes): Unit = term.setAttributes(toMap(a))
def setSize(size: Size): Unit = term.setSize(size.getColumns, size.getRows)
/**
* Override enterRawMode because the default implementation modifies System.in
* to be non-blocking which means it immediately returns -1 if there is no
* data available, which is not desirable for us.
*/
override def enterRawMode(): Attributes = enterRawModeImpl(this)
}
}
private def enterRawModeImpl(term: JTerminal): Attributes = {
val prvAttr = term.getAttributes()
val newAttr = new Attributes(prvAttr)
newAttr.setLocalFlags(
EnumSet
.of(Attributes.LocalFlag.ICANON, Attributes.LocalFlag.ECHO, Attributes.LocalFlag.IEXTEN),
false
)
newAttr.setInputFlags(
EnumSet
.of(Attributes.InputFlag.IXON, Attributes.InputFlag.ICRNL, Attributes.InputFlag.INLCR),
false
)
term.setAttributes(newAttr)
prvAttr
}
private[util] def enterRawMode(term: JTerminal): Map[String, String] =
toMap(enterRawModeImpl(term))
private[util] def toMap(jattributes: Attributes): Map[String, String] = {
val result = new java.util.LinkedHashMap[String, String]
result.put(
"iflag",
jattributes.getInputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"oflag",
jattributes.getOutputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"cflag",
jattributes.getControlFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"lflag",
jattributes.getLocalFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"cchars",
jattributes.getControlChars.entrySet.iterator.asScala
.map { e =>
s"${e.getKey.name.toLowerCase},${e.getValue}"
}
.mkString(" ")
)
result.asScala.toMap
}
private[this] val iflagMap: Map[String, Attributes.InputFlag] =
Attributes.InputFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val oflagMap: Map[String, Attributes.OutputFlag] =
Attributes.OutputFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val cflagMap: Map[String, Attributes.ControlFlag] =
Attributes.ControlFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val lflagMap: Map[String, Attributes.LocalFlag] =
Attributes.LocalFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val charMap: Map[String, Attributes.ControlChar] =
Attributes.ControlChar.values().map(f => f.name.toLowerCase -> f).toMap
private[util] def attributesFromMap(map: Map[String, String]): Attributes = {
val attributes = new Attributes
map.get("iflag").foreach { flags =>
flags.split(" ").foreach(f => iflagMap.get(f).foreach(attributes.setInputFlag(_, true)))
}
map.get("oflag").foreach { flags =>
flags.split(" ").foreach(f => oflagMap.get(f).foreach(attributes.setOutputFlag(_, true)))
}
map.get("cflag").foreach { flags =>
flags.split(" ").foreach(f => cflagMap.get(f).foreach(attributes.setControlFlag(_, true)))
}
map.get("lflag").foreach { flags =>
flags.split(" ").foreach(f => lflagMap.get(f).foreach(attributes.setLocalFlag(_, true)))
}
map.get("cchars").foreach { chars =>
chars.split(" ").foreach { keyValue =>
keyValue.split(",") match {
case Array(k, v) =>
Try(v.toInt).foreach(i => charMap.get(k).foreach(c => attributes.setControlChar(c, i)))
case _ =>
}
}
}
attributes
}
}

View File

@ -14,7 +14,6 @@ import sbt.internal.util.ConsoleAppender.{
ClearScreenAfterCursor,
CursorLeft1000,
DeleteLine,
cursorLeft,
cursorUp,
}
@ -33,6 +32,10 @@ private[sbt] final class ProgressState(
blankZone,
new AtomicReference(new ArrayBuffer[Byte]),
)
def currentLine: Option[String] =
new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption
.map(EscHelpers.stripColorsAndMoves)
.filter(_.nonEmpty)
def reset(): Unit = {
progressLines.set(Nil)
padding.set(0)
@ -44,8 +47,9 @@ private[sbt] final class ProgressState(
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[util] def addBytes(terminal: Terminal, bytes: ArrayBuffer[Byte]): Unit = {
val previous = currentLineBytes.get
private[this] val lineSeparatorBytes: Array[Byte] = System.lineSeparator.getBytes("UTF-8")
private[util] def addBytes(terminal: Terminal, bytes: Seq[Byte]): Unit = {
val previous: ArrayBuffer[Byte] = currentLineBytes.get
val padding = this.padding.get
val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0
previous ++= bytes
@ -54,6 +58,16 @@ private[sbt] final class ProgressState(
val diff = newLineCount - prevLineCount
this.padding.set(math.max(padding - diff, 0))
}
val lines = new String(previous.toArray, "UTF-8")
if (lines.contains(System.lineSeparator)) {
currentLineBytes.set(new ArrayBuffer[Byte])
if (!lines.endsWith(System.lineSeparator)) {
lines
.split(System.lineSeparator)
.lastOption
.foreach(currentLineBytes.get ++= _.getBytes("UTF-8"))
}
}
}
private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit =
@ -62,31 +76,50 @@ private[sbt] final class ProgressState(
val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes
pmpt.foreach(b => printStream.write(b & 0xFF))
}
private[util] def reprint(terminal: Terminal, printStream: PrintStream): Unit = {
printPrompt(terminal, printStream)
private[util] def write(
terminal: Terminal,
bytes: Array[Byte],
printStream: PrintStream,
hasProgress: Boolean
): Unit = {
addBytes(terminal, bytes)
if (hasProgress && terminal.prompt != Prompt.Loading) {
terminal.prompt match {
case a: Prompt.AskUser if a.render.nonEmpty =>
printStream.print(System.lineSeparator + ClearScreenAfterCursor + CursorLeft1000)
printStream.flush()
case _ =>
}
printStream.write(bytes)
printStream.write(ClearScreenAfterCursor.getBytes("UTF-8"))
printStream.flush()
if (bytes.endsWith(lineSeparatorBytes)) {
if (progressLines.get.nonEmpty) {
val lines = printProgress(terminal, terminal.getLastLine.getOrElse(""))
val lastLine = terminal.prompt match {
case a: Prompt.AskUser => a.render()
case _ => currentLine.getOrElse("")
}
val lines = printProgress(terminal, lastLine)
printStream.print(ClearScreenAfterCursor + lines)
}
}
printPrompt(terminal, printStream)
} else printStream.write(bytes)
}
private[util] def printProgress(
terminal: Terminal,
lastLine: String
): String = {
private[util] def printProgress(terminal: Terminal, lastLine: String): String = {
val previousLines = progressLines.get
if (previousLines.nonEmpty) {
val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val (height, width) = terminal.getLineHeightAndWidth(lastLine)
val left = cursorLeft(1000) // resets the position to the left
val offset = width > 0
val pad = math.max(padding.get - height, 0)
val start = (if (offset) "\n" else "")
val start = (if (offset) s"\n$CursorLeft1000" else "")
val totalSize = currentLength + blankZone + pad
val blank = left + s"\n$DeleteLine" * (totalSize - currentLength)
val blank = CursorLeft1000 + s"\n$DeleteLine" * (totalSize - currentLength)
val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine")
val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0))
val resetCursor = resetCursorUp + left + lastLine
val resetCursor = resetCursorUp + CursorLeft1000 + lastLine
start + blank + lines + resetCursor
} else {
ClearScreenAfterCursor
@ -108,6 +141,7 @@ private[sbt] object ProgressState {
terminal: Terminal
): Unit = {
val state = terminal.progressState
val isAskUser = terminal.prompt.isInstanceOf[Prompt.AskUser]
val isRunning = terminal.prompt == Prompt.Running
val isBatch = terminal.prompt == Prompt.Batch
val isWatch = terminal.prompt == Prompt.Watch
@ -115,9 +149,8 @@ private[sbt] object ProgressState {
if (terminal.isSupershellEnabled) {
if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) {
terminal.withPrintStream { ps =>
val info =
if ((isRunning || isBatch || noPrompt) && pe.channelName
.fold(true)(_ == terminal.name)) {
val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name)
val info = if ((isRunning || isBatch || noPrompt) && commandFromThisTerminal) {
pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L
s" | => ${item.name} ${elapsed}s"
@ -132,14 +165,11 @@ private[sbt] object ProgressState {
val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_))
val previousLines = state.progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val lastLine = terminal.prompt match {
case Prompt.Running | Prompt.Batch => terminal.getLastLine.getOrElse("")
case a => a.render()
}
val prevSize = prevLength + state.padding.get
val newPadding = math.max(0, prevSize - currentLength)
state.padding.set(newPadding)
val lastLine =
if (isAskUser) terminal.prompt.render() else terminal.getLastLine.getOrElse("")
state.padding.set(math.max(0, prevSize - currentLength))
state.printPrompt(terminal, ps)
ps.print(state.printProgress(terminal, lastLine))
ps.flush()

View File

@ -7,39 +7,29 @@
package sbt.internal.util
import java.io.OutputStream
import java.util.concurrent.LinkedBlockingQueue
import scala.collection.JavaConverters._
private[sbt] sealed trait Prompt {
def mkPrompt: () => String
def render(): String
def wrappedOutputStream(terminal: Terminal): OutputStream
def reset(): Unit
}
private[sbt] object Prompt {
private[sbt] case class AskUser(override val mkPrompt: () => String) extends Prompt {
private[this] val bytes = new LinkedBlockingQueue[Int]
override def wrappedOutputStream(terminal: Terminal): OutputStream = new OutputStream {
override def write(b: Int): Unit = {
if (b == 10) bytes.clear()
else bytes.put(b)
terminal.withPrintStream { p =>
p.write(b)
p.flush()
private[this] val bytes = new LinkedBlockingQueue[Byte]
def write(b: Array[Byte]): Unit = b.foreach(bytes.put)
override def render(): String = {
val res = new String(bytes.asScala.toArray, "UTF-8")
if (res.endsWith(System.lineSeparator)) "" else res
}
}
override def flush(): Unit = terminal.withPrintStream(_.flush())
}
override def render(): String =
EscHelpers.stripMoves(new String(bytes.asScala.toArray.map(_.toByte)))
override def reset(): Unit = bytes.clear()
}
private[sbt] trait NoPrompt extends Prompt {
override val mkPrompt: () => String = () => ""
override def render(): String = ""
override def wrappedOutputStream(terminal: Terminal): OutputStream = terminal.outputStream
override def reset(): Unit = {}
}
private[sbt] case object Running extends NoPrompt
private[sbt] case object Batch extends NoPrompt

View File

@ -7,19 +7,17 @@
package sbt.internal.util
import java.io.{ InputStream, OutputStream, PrintStream }
import java.io.{ InputStream, InterruptedIOException, OutputStream, PrintStream }
import java.nio.channels.ClosedChannelException
import java.util.Locale
import java.util.{ Arrays, Locale }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.{ ConcurrentHashMap, Executors, LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.{ ArrayBlockingQueue, Executors, LinkedBlockingQueue, TimeUnit }
import jline.DefaultTerminal2
import jline.console.ConsoleReader
import sbt.internal.util.ConsoleAppender.{ ClearScreenAfterCursor, CursorLeft1000 }
import scala.annotation.tailrec
import scala.collection.mutable.ArrayBuffer
import scala.util.Try
import scala.util.control.NonFatal
trait Terminal extends AutoCloseable {
@ -111,9 +109,12 @@ trait Terminal extends AutoCloseable {
*/
private[sbt] def getLastLine: Option[String]
private[sbt] def getBooleanCapability(capability: String): Boolean
private[sbt] def getNumericCapability(capability: String): Int
private[sbt] def getStringCapability(capability: String): String
private[sbt] def getBooleanCapability(capability: String, jline3: Boolean): Boolean
private[sbt] def getNumericCapability(capability: String, jline3: Boolean): Integer
private[sbt] def getStringCapability(capability: String, jline3: Boolean): String
private[sbt] def getAttributes: Map[String, String]
private[sbt] def setAttributes(attributes: Map[String, String]): Unit
private[sbt] def setSize(width: Int, height: Int): Unit
private[sbt] def name: String
private[sbt] def withRawSystemIn[T](f: => T): T = f
@ -142,7 +143,8 @@ trait Terminal extends AutoCloseable {
val len = l.length
if (width > 0 && len > 0) (len - 1 + width) / width else 0
}
lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_))
if (lines.nonEmpty) lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_))
else 0
}
}
@ -188,13 +190,13 @@ object Terminal {
override def enableInterruptCharacter(): Unit = {}
override def getOutputEncoding: String = null
override def getBooleanCapability(capability: String): Boolean = {
term.getBooleanCapability(capability)
term.getBooleanCapability(capability, jline3 = false)
}
override def getNumericCapability(capability: String): Integer = {
term.getNumericCapability(capability)
term.getNumericCapability(capability, jline3 = false)
}
override def getStringCapability(capability: String): String = {
term.getStringCapability(capability)
term.getStringCapability(capability, jline3 = false)
}
}
}
@ -253,19 +255,46 @@ object Terminal {
*/
private[sbt] def restore(): Unit = console.toJLine.restore()
private[this] val hasProgress: AtomicBoolean = new AtomicBoolean(false)
/**
*
* @param progress toggles whether or not the console terminal has progress
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withStreams[T](f: => T): T =
private[sbt] def withStreams[T](isServer: Boolean)(f: => T): T =
if (System.getProperty("sbt.io.virtual", "true") == "true") {
hasProgress.set(isServer)
try withOut(withIn(f))
finally {
jline.TerminalFactory.reset()
if (isServer) {
console match {
case c: ConsoleTerminal if !isWindows =>
/*
* Entering raw mode in this way causes the standard in InputStream
* to become non-blocking. After we set it to non-blocking, we spin
* up a thread that reads from the inputstream and the resets it
* back to blocking mode. We can then close the console. We do
* this on a background thread to avoid blocking sbt's exit.
*/
val prev = c.system.enterRawMode()
val runnable: Runnable = () => {
c.inputStream.read()
c.system.setAttributes(prev)
c.close()
}
val thread = new Thread(runnable, "sbt-console-background-close")
thread.setDaemon(true)
thread.start()
case c => c.close()
}
} else {
console.close()
}
}
} else f
private[this] object ProxyTerminal extends Terminal {
@ -281,10 +310,16 @@ object Terminal {
override def isEchoEnabled: Boolean = t.isEchoEnabled
override def isSuccessEnabled: Boolean = t.isSuccessEnabled
override def isSupershellEnabled: Boolean = t.isSupershellEnabled
override def getBooleanCapability(capability: String): Boolean =
t.getBooleanCapability(capability)
override def getNumericCapability(capability: String): Int = t.getNumericCapability(capability)
override def getStringCapability(capability: String): String = t.getStringCapability(capability)
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
t.getBooleanCapability(capability, jline3)
override def getNumericCapability(capability: String, jline3: Boolean): Integer =
t.getNumericCapability(capability, jline3)
override def getStringCapability(capability: String, jline3: Boolean): String =
t.getStringCapability(capability, jline3)
override private[sbt] def getAttributes: Map[String, String] = t.getAttributes
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit =
t.setAttributes(attributes)
override private[sbt] def setSize(width: Int, height: Int): Unit = t.setSize(width, height)
override def withRawSystemIn[T](f: => T): T = t.withRawSystemIn(f)
override def withCanonicalIn[T](f: => T): T = t.withCanonicalIn(f)
override def printStream: PrintStream = t.printStream
@ -322,18 +357,35 @@ object Terminal {
}
}
private[this] val originalOut = System.out
val sepBytes = System.lineSeparator.getBytes("UTF-8")
private class LinePrintStream(outputStream: OutputStream)
extends PrintStream(outputStream, true) {
override def println(s: String): Unit = synchronized {
out.write(s.getBytes("UTF-8") ++ sepBytes)
out.flush()
}
}
private[this] val originalOut = new LinePrintStream(System.out)
private[this] val originalIn = System.in
private[sbt] class WriteableInputStream(in: InputStream, name: String)
extends InputStream
with AutoCloseable {
final def write(bytes: Int*): Unit = bytes.foreach(i => buffer.put(i))
final def write(bytes: Int*): Unit = waiting.synchronized {
waiting.poll match {
case null =>
bytes.foreach(b => buffer.put(b))
case w =>
if (bytes.length > 1) bytes.tail.foreach(b => buffer.put(b))
bytes.headOption.foreach(b => w.put(b))
}
}
private[this] val executor =
Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader"))
private[this] val buffer = new LinkedBlockingQueue[Integer]
private[this] val closed = new AtomicBoolean(false)
private[this] val resultQueue = new LinkedBlockingQueue[LinkedBlockingQueue[Int]]
private[this] val waiting = ConcurrentHashMap.newKeySet[LinkedBlockingQueue[Int]]
private[this] val readQueue = new LinkedBlockingQueue[Unit]
private[this] val waiting = new ArrayBlockingQueue[LinkedBlockingQueue[Integer]](1)
private[this] val readThread = new AtomicReference[Thread]
/*
* Starts a loop that waits for consumers of the InputStream to call read.
* When read is called, we enqueue a `LinkedBlockingQueue[Int]` to which
@ -354,11 +406,14 @@ object Terminal {
*/
private[this] val runnable: Runnable = () => {
@tailrec def impl(): Unit = {
val result = resultQueue.take
val _ = readQueue.take
val b = in.read
// The downstream consumer may have been interrupted. Buffer the result
// when that hapens.
if (waiting.contains(result)) result.put(b) else buffer.put(b)
waiting.poll match {
case null => buffer.put(b)
case q => q.put(b)
}
if (b != -1 && !Thread.interrupted()) impl()
else closed.set(true)
}
@ -370,20 +425,27 @@ object Terminal {
if (closed.get) -1
else
synchronized {
buffer.poll match {
readThread.set(Thread.currentThread)
try buffer.poll match {
case null =>
val result = new LinkedBlockingQueue[Int]
waiting.add(result)
resultQueue.offer(result)
try result.take
val result = new LinkedBlockingQueue[Integer]
waiting.synchronized(waiting.put(result))
readQueue.put(())
try result.take.toInt
catch {
case e: InterruptedException =>
waiting.remove(result)
throw e
-1
}
case b if b == -1 => throw new ClosedChannelException
case b => b
case b => b.toInt
} finally readThread.set(null)
}
def cancel(): Unit = waiting.synchronized {
Option(readThread.getAndSet(null)).foreach(_.interrupt())
waiting.forEach(_.put(-2))
waiting.clear()
readQueue.clear()
}
override def available(): Int = {
@ -524,7 +586,7 @@ object Terminal {
}
override def flush(): Unit = os.flush()
}
private[this] val proxyPrintStream = new PrintStream(proxyOutputStream, true) {
private[this] val proxyPrintStream = new LinePrintStream(proxyOutputStream) {
override def toString: String = s"proxyPrintStream($proxyOutputStream)"
}
private[this] lazy val isWindows =
@ -592,9 +654,21 @@ object Terminal {
case t: jline.Terminal2 => t
case _ => new DefaultTerminal2(terminal)
}
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 init(): Unit =
if (alive)
try terminal.init()
catch {
case _: InterruptedException =>
}
override def restore(): Unit =
if (alive)
try terminal.restore()
catch {
case _: InterruptedException =>
}
override def reset(): Unit =
try terminal.reset()
catch { case _: InterruptedException => }
override def isSupported: Boolean = terminal.isSupported
override def getWidth: Int = props.map(_.width).getOrElse(terminal.getWidth)
override def getHeight: Int = props.map(_.height).getOrElse(terminal.getHeight)
@ -650,7 +724,7 @@ object Terminal {
fixTerminalProperty()
private[sbt] def createReader(term: Terminal, prompt: Prompt): ConsoleReader = {
new ConsoleReader(term.inputStream, prompt.wrappedOutputStream(term), term.toJLine) {
new ConsoleReader(term.inputStream, term.outputStream, term.toJLine) {
override def readLine(prompt: String, mask: Character): String =
term.withRawSystemIn(super.readLine(prompt, mask))
override def readLine(prompt: String): String = term.withRawSystemIn(super.readLine(prompt))
@ -662,6 +736,9 @@ object Terminal {
case term => term
}
private val capabilityMap =
org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap
@deprecated("For compatibility only", "1.4.0")
private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine
private class ConsoleTerminal(
@ -669,28 +746,36 @@ object Terminal {
in: InputStream,
out: OutputStream
) extends TerminalImpl(in, out, "console0") {
private[util] lazy val system = JLine3.system
private[this] def isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")
override def getWidth: Int = term.getWidth
override def getHeight: Int = term.getHeight
override def getWidth: Int = system.getSize.getColumns
override def getHeight: Int = system.getSize.getRows
override def isAnsiSupported: Boolean = term.isAnsiSupported && !isCI
override def isEchoEnabled: Boolean = term.isEchoEnabled
override def isEchoEnabled: Boolean = system.echo()
override def isSuccessEnabled: Boolean = true
override def getBooleanCapability(capability: String): Boolean =
term.getBooleanCapability(capability)
override def getNumericCapability(capability: String): Int =
term.getNumericCapability(capability)
override def getStringCapability(capability: String): String =
term.getStringCapability(capability)
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
if (jline3) capabilityMap.get(capability).fold(false)(system.getBooleanCapability)
else term.getBooleanCapability(capability)
override def getNumericCapability(capability: String, jline3: Boolean): Integer =
if (jline3) capabilityMap.get(capability).fold(null: Integer)(system.getNumericCapability)
else term.getNumericCapability(capability)
override def getStringCapability(capability: String, jline3: Boolean): String =
if (jline3) capabilityMap.get(capability).fold(null: String)(system.getStringCapability)
else term.getStringCapability(capability)
override private[sbt] def restore(): Unit = term.restore()
override private[sbt] def getAttributes: Map[String, String] =
JLine3.toMap(system.getAttributes)
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit =
system.setAttributes(JLine3.attributesFromMap(attributes))
override private[sbt] def setSize(width: Int, height: Int): Unit =
system.setSize(new org.jline.terminal.Size(width, height))
override def withRawSystemIn[T](f: => T): T = term.synchronized {
try {
term.init()
term.setEchoEnabled(false)
f
} finally {
term.restore()
term.setEchoEnabled(true)
val prev = JLine3.enterRawMode(system)
try f
catch { case _: InterruptedIOException => throw new InterruptedException } finally {
setAttributes(prev)
}
}
override def isColorEnabled: Boolean =
@ -705,29 +790,33 @@ object Terminal {
case "true" => true
case _ => false
})
override def close(): Unit = {
try system.close()
catch { case NonFatal(_) => }
super.close()
}
}
private[sbt] abstract class TerminalImpl private[sbt] (
val in: InputStream,
val out: OutputStream,
override private[sbt] val name: String
) extends Terminal {
private[this] val directWrite = new AtomicBoolean(false)
private[this] val currentLine = new AtomicReference(new ArrayBuffer[Byte])
private[this] val lineBuffer = new LinkedBlockingQueue[Byte]
private[this] val flushQueue = new LinkedBlockingQueue[Seq[Byte]]
private[this] val writeLock = new AnyRef
private[this] val writeableInputStream = in match {
case w: WriteableInputStream => w
case _ => new WriteableInputStream(in, name)
}
def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f
override def getLastLine: Option[String] = progressState.currentLine
private val combinedOutputStream = new OutputStream {
override def write(b: Int): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.write(b))
out.write(b)
}
override def write(b: Array[Byte]): Unit = write(b, 0, b.length)
override def write(b: Array[Byte]): Unit = {
write(b, 0, b.length)
}
override def write(b: Array[Byte], offset: Int, len: Int): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.write(b, offset, len))
out.write(b, offset, len)
@ -740,54 +829,19 @@ object Terminal {
override val outputStream = new OutputStream {
override def write(b: Int): Unit = throwIfClosed {
writeLock.synchronized {
if (b == Int.MinValue) currentLine.set(new ArrayBuffer[Byte])
else doWrite(Vector((b & 0xFF).toByte))
if (b == 10) combinedOutputStream.flush()
}
}
override def write(b: Array[Byte]): Unit = throwIfClosed(write(b, 0, b.length))
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
throwIfClosed {
writeLock.synchronized {
val lo = math.max(0, off)
val hi = math.min(math.max(off + len, 0), b.length)
doWrite(b.slice(off, off + len).toSeq)
write(Array((b & 0xFF).toByte))
}
override def write(b: Array[Byte]): Unit = throwIfClosed {
writeLock.synchronized(doWrite(b))
}
override def write(b: Array[Byte], offset: Int, length: Int): Unit = throwIfClosed {
write(Arrays.copyOfRange(b, offset, offset + length))
}
override def flush(): Unit = combinedOutputStream.flush()
private[this] val clear = s"$CursorLeft1000$ClearScreenAfterCursor"
private def doWrite(bytes: Seq[Byte]): Unit = {
def doWrite(b: Byte): Unit = out.write(b & 0xFF)
val remaining = bytes.foldLeft(new ArrayBuffer[Byte]) { (buf, i) =>
if (i == 10) {
progressState.addBytes(TerminalImpl.this, buf)
progressState.clearBytes()
val cl = currentLine.get
if (buf.nonEmpty && isAnsiSupported && cl.isEmpty) clear.getBytes.foreach(doWrite)
combinedOutputStream.write(buf.toArray)
combinedOutputStream.write(10)
currentLine.get match {
case s if s.nonEmpty => currentLine.set(new ArrayBuffer[Byte])
case _ =>
}
if (prompt != Prompt.Loading) progressState.reprint(TerminalImpl.this, rawPrintStream)
new ArrayBuffer[Byte]
} else buf += i
}
if (remaining.nonEmpty) {
val cl = currentLine.get
if (isAnsiSupported && cl.isEmpty) {
clear.getBytes.foreach(doWrite)
}
cl ++= remaining
combinedOutputStream.write(remaining.toArray)
}
combinedOutputStream.flush()
}
}
override private[sbt] val printStream: PrintStream = new PrintStream(outputStream, true)
private def doWrite(bytes: Array[Byte]): Unit =
progressState.write(TerminalImpl.this, bytes, rawPrintStream, hasProgress.get)
override private[sbt] val printStream: PrintStream = new LinePrintStream(outputStream)
override def inputStream: InputStream = writeableInputStream
private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*)
@ -801,17 +855,7 @@ object Terminal {
case _ => (0, 0)
}
override def getLastLine: Option[String] = currentLine.get match {
case bytes if bytes.isEmpty => None
case bytes =>
// TODO there are ghost characters when the user deletes prompt characters
// when they are given the cancellation option
Some(new String(bytes.toArray).replaceAllLiterally(ClearScreenAfterCursor, ""))
}
private[this] val rawPrintStream: PrintStream = new PrintStream(combinedOutputStream, true) {
override def close(): Unit = {}
}
private[this] val rawPrintStream: PrintStream = new LinePrintStream(combinedOutputStream)
override def withPrintStream[T](f: PrintStream => T): T =
writeLock.synchronized(f(rawPrintStream))
@ -821,12 +865,12 @@ object Terminal {
}
private[sbt] val NullTerminal = new Terminal {
override def close(): Unit = {}
override def getBooleanCapability(capability: String): Boolean = false
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false
override def getHeight: Int = 0
override def getLastLine: Option[String] = None
override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0)
override def getNumericCapability(capability: String): Int = -1
override def getStringCapability(capability: String): String = null
override def getNumericCapability(capability: String, jline3: Boolean): Integer = null
override def getStringCapability(capability: String, jline3: Boolean): String = null
override def getWidth: Int = 0
override def inputStream: java.io.InputStream = () => {
try this.synchronized(this.wait)
@ -839,6 +883,9 @@ object Terminal {
override def isSuccessEnabled: Boolean = false
override def isSupershellEnabled: Boolean = false
override def outputStream: java.io.OutputStream = _ => {}
override private[sbt] def getAttributes: Map[String, String] = Map.empty
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = {}
override private[sbt] def setSize(width: Int, height: Int): Unit = {}
override private[sbt] def name: String = "NullTerminal"
override private[sbt] val printStream: java.io.PrintStream =
new PrintStream(outputStream, false)

View File

@ -63,4 +63,11 @@ class CleanStringSpec extends FlatSpec {
val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62))
assert(EscHelpers.stripMoves(original) == "foo" + colorArrow + " " + scala.Console.RESET)
}
it should "remove unusual escape characters" in {
val original = new String(
Array[Byte](27, 91, 63, 49, 108, 27, 62, 27, 91, 63, 49, 48, 48, 48, 108, 27, 91, 63, 50, 48,
48, 52, 108)
)
assert(EscHelpers.stripColorsAndMoves(original).isEmpty)
}
}

View File

@ -12,7 +12,6 @@ package client
import java.io.{ File, IOException, InputStream, PrintStream }
import java.lang.ProcessBuilder.Redirect
import java.net.Socket
import java.nio.channels.ClosedChannelException
import java.nio.file.Files
import java.util.UUID
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
@ -43,10 +42,14 @@ import Serialization.{
promptChannel,
systemIn,
systemOut,
systemOutFlush,
terminalCapabilities,
terminalCapabilitiesResponse,
terminalPropertiesQuery,
terminalPropertiesResponse
terminalPropertiesResponse,
getTerminalAttributes,
setTerminalAttributes,
setTerminalSize,
}
import NetworkClient.Arguments
@ -199,7 +202,6 @@ class NetworkClient(
case _ => (false, None)
}
if (rebootCommands.nonEmpty) {
if (Terminal.console.getLastLine.isDefined) Terminal.console.printStream.println()
rebooting.set(true)
attached.set(false)
connectionHolder.getAndSet(null) match {
@ -212,7 +214,7 @@ class NetworkClient(
rebooting.set(false)
rebootCommands match {
case Some((execId, cmd)) if execId.nonEmpty =>
if (batchMode.get && !pendingResults.contains(execId) && cmd.isEmpty) {
if (batchMode.get && !pendingResults.containsKey(execId) && cmd.nonEmpty) {
console.appendLog(
Level.Error,
s"received request to re-run unknown command '$cmd' after reboot"
@ -230,8 +232,6 @@ class NetworkClient(
} else {
if (!rebooting.get() && running.compareAndSet(true, false) && log) {
if (!arguments.commandArguments.contains(Shutdown)) {
if (Terminal.console.getLastLine.isDefined)
Terminal.console.printStream.println()
console.appendLog(Level.Error, "sbt server disconnected")
exitClean.set(false)
}
@ -306,7 +306,6 @@ class NetworkClient(
Some(process)
case _ =>
if (log) {
if (Terminal.console.getLastLine.isDefined) Terminal.console.printStream.println()
console.appendLog(Level.Info, "sbt server is booting up")
}
None
@ -522,17 +521,15 @@ class NetworkClient(
}
} else Vector()
case (`systemOut`, Some(json)) =>
Converter.fromJson[Seq[Byte]](json) match {
case Success(params) =>
if (params.nonEmpty) {
if (attached.get) {
printStream.write(params.toArray)
printStream.flush()
}
}
case Failure(_) =>
Converter.fromJson[Array[Byte]](json) match {
case Success(bytes) if bytes.nonEmpty && attached.get =>
synchronized(printStream.write(bytes))
case _ =>
}
Vector.empty
case (`systemOutFlush`, _) =>
synchronized(printStream.flush())
Vector.empty
case (`promptChannel`, _) =>
batchMode.set(false)
Vector.empty
@ -589,16 +586,23 @@ class NetworkClient(
}
def onRequest(msg: JsonRpcRequestMessage): Unit = {
import sbt.protocol.codec.JsonProtocol._
(msg.method, msg.params) match {
case (`terminalCapabilities`, Some(json)) =>
import sbt.protocol.codec.JsonProtocol._
Converter.fromJson[TerminalCapabilitiesQuery](json) match {
case Success(terminalCapabilitiesQuery) =>
val jline3 = terminalCapabilitiesQuery.jline3
val response = TerminalCapabilitiesResponse(
terminalCapabilitiesQuery.boolean.map(Terminal.console.getBooleanCapability),
terminalCapabilitiesQuery.numeric.map(Terminal.console.getNumericCapability),
terminalCapabilitiesQuery.boolean
.map(Terminal.console.getBooleanCapability(_, jline3)),
terminalCapabilitiesQuery.numeric
.map(
c => Option(Terminal.console.getNumericCapability(c, jline3)).fold(-1)(_.toInt)
),
terminalCapabilitiesQuery.string
.map(s => Option(Terminal.console.getStringCapability(s)).getOrElse("null")),
.map(
s => Option(Terminal.console.getStringCapability(s, jline3)).getOrElse("null")
),
)
sendCommandResponse(
terminalCapabilitiesResponse,
@ -617,6 +621,37 @@ class NetworkClient(
isEchoEnabled = Terminal.console.isEchoEnabled
)
sendCommandResponse(terminalPropertiesResponse, response, msg.id)
case (`setTerminalAttributes`, Some(json)) =>
Converter.fromJson[TerminalSetAttributesCommand](json) match {
case Success(attributes) =>
val attrs = Map(
"iflag" -> attributes.iflag,
"oflag" -> attributes.oflag,
"cflag" -> attributes.cflag,
"lflag" -> attributes.lflag,
"cchars" -> attributes.cchars,
)
Terminal.console.setAttributes(attrs)
sendCommandResponse("", TerminalSetAttributesResponse(), msg.id)
case Failure(_) =>
}
case (`getTerminalAttributes`, _) =>
val attrs = Terminal.console.getAttributes
val response = TerminalAttributesResponse(
iflag = attrs.getOrElse("iflag", ""),
oflag = attrs.getOrElse("oflag", ""),
cflag = attrs.getOrElse("cflag", ""),
lflag = attrs.getOrElse("lflag", ""),
cchars = attrs.getOrElse("cchars", ""),
)
sendCommandResponse("", response, msg.id)
case (`setTerminalSize`, Some(json)) =>
Converter.fromJson[TerminalSetSizeCommand](json) match {
case Success(size) =>
Terminal.console.setSize(size.width, size.height)
sendCommandResponse("", TerminalSetSizeResponse(), msg.id)
case Failure(_) =>
}
case _ =>
}
}
@ -851,7 +886,7 @@ class NetworkClient(
}
}
try Terminal.console.withRawSystemIn(read())
catch { case _: InterruptedException | _: ClosedChannelException => stopped.set(true) }
catch { case NonFatal(_) => stopped.set(true) }
}
def drain(): Unit = inLock.synchronized {
@ -897,20 +932,18 @@ object NetworkClient {
override def success(msg: String): Unit = appender.success(msg)
}
}
private def simpleConsoleInterface(printStream: PrintStream): ConsoleInterface =
private def simpleConsoleInterface(doPrintln: String => Unit): ConsoleInterface =
new ConsoleInterface {
import scala.Console.{ GREEN, RED, RESET, YELLOW }
override def appendLog(level: Level.Value, message: => String): Unit = {
override def appendLog(level: Level.Value, message: => String): Unit = synchronized {
val prefix = level match {
case Level.Error => s"[$RED$level$RESET]"
case Level.Warn => s"[$YELLOW$level$RESET]"
case _ => s"[$RESET$level$RESET]"
}
message.split("\n").foreach { line =>
if (!line.trim.isEmpty) printStream.println(s"$prefix $line")
message.linesIterator.foreach(line => doPrintln(s"$prefix $line"))
}
}
override def success(msg: String): Unit = printStream.println(s"[${GREEN}success$RESET] $msg")
override def success(msg: String): Unit = doPrintln(s"[${GREEN}success$RESET] $msg")
}
private[client] class Arguments(
val baseDirectory: File,
@ -961,8 +994,29 @@ object NetworkClient {
baseDirectory: File,
args: Array[String],
inputStream: InputStream,
errorStream: PrintStream,
printStream: PrintStream,
errorStream: PrintStream,
useJNI: Boolean
): Int = {
val client =
simpleClient(
NetworkClient.parseArgs(args).withBaseDirectory(baseDirectory),
inputStream,
printStream,
errorStream,
useJNI,
)
try {
if (client.connect(log = true, promptCompleteUsers = false)) client.run()
else 1
} catch { case _: Exception => 1 } finally client.close()
}
def client(
baseDirectory: File,
args: Array[String],
inputStream: InputStream,
errorStream: PrintStream,
terminal: Terminal,
useJNI: Boolean
): Int = {
val client =
@ -970,8 +1024,8 @@ object NetworkClient {
NetworkClient.parseArgs(args).withBaseDirectory(baseDirectory),
inputStream,
errorStream,
printStream,
useJNI,
terminal
)
try {
if (client.connect(log = true, promptCompleteUsers = false)) client.run()
@ -982,17 +1036,27 @@ object NetworkClient {
arguments: Arguments,
inputStream: InputStream,
errorStream: PrintStream,
printStream: PrintStream,
useJNI: Boolean,
): NetworkClient =
new NetworkClient(
arguments,
NetworkClient.simpleConsoleInterface(printStream),
inputStream,
errorStream,
printStream,
useJNI,
)
terminal: Terminal
): NetworkClient = {
val doPrint: String => Unit = line => {
if (terminal.getLastLine.isDefined) terminal.printStream.println()
terminal.printStream.println(line)
}
val interface = NetworkClient.simpleConsoleInterface(doPrint)
val printStream = terminal.printStream
new NetworkClient(arguments, interface, inputStream, errorStream, printStream, useJNI)
}
private def simpleClient(
arguments: Arguments,
inputStream: InputStream,
printStream: PrintStream,
errorStream: PrintStream,
useJNI: Boolean,
): NetworkClient = {
val interface = NetworkClient.simpleConsoleInterface(printStream.println)
new NetworkClient(arguments, interface, inputStream, errorStream, printStream, useJNI)
}
def main(args: Array[String]): Unit = {
val (jnaArg, restOfArgs) = args.partition(_ == "--jna")
val useJNI = jnaArg.isEmpty
@ -1005,8 +1069,9 @@ object NetworkClient {
System.out.flush()
})
Runtime.getRuntime.addShutdownHook(hook)
System.exit(Terminal.withStreams {
try client(base, restOfArgs, System.in, System.err, System.out, useJNI)
System.exit(Terminal.withStreams(false) {
val term = Terminal.console
try client(base, restOfArgs, term.inputStream, System.err, term, useJNI)
finally {
Runtime.getRuntime.removeShutdownHook(hook)
hook.run()

View File

@ -11,14 +11,14 @@ import java.io.File
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicBoolean
import jline.console.history.PersistentHistory
//import jline.console.history.PersistentHistory
import sbt.BasicCommandStrings.{ Cancel, TerminateAction, Shutdown }
import sbt.BasicKeys.{ historyPath, terminalShellPrompt }
import sbt.State
import sbt.internal.CommandChannel
import sbt.internal.util.ConsoleAppender.{ ClearPromptLine, ClearScreenAfterCursor, DeleteLine }
import sbt.internal.util._
import sbt.internal.util.complete.{ JLineCompletion, Parser }
import sbt.internal.util.complete.{ Parser }
import scala.annotation.tailrec
@ -47,44 +47,31 @@ private[sbt] object UITask {
def terminalReader(parser: Parser[_])(
terminal: Terminal,
state: State
): Reader = {
val lineReader = LineReader.createReader(history(state), terminal, terminal.prompt)
JLineCompletion.installCustomCompletor(lineReader, parser)
() => {
val clear = terminal.ansi(ClearPromptLine, "")
): Reader = { () =>
try {
val clear = terminal.ansi(ClearPromptLine, "")
@tailrec def impl(): Either[String, String] = {
lineReader.readLine(clear + terminal.prompt.mkPrompt()) match {
case null if terminal == Terminal.console && System.console == null =>
val reader = LineReader.createReader(history(state), parser, terminal, terminal.prompt)
(try reader.readLine(clear + terminal.prompt.mkPrompt())
finally reader.close) match {
case None if terminal == Terminal.console && System.console == null =>
// No stdin is attached to the process so just ignore the result and
// block until the thread is interrupted.
this.synchronized(this.wait())
Right("") // should be unreachable
// JLine returns null on ctrl+d when there is no other input. This interprets
// ctrl+d with no imput as an exit
case null => Left(TerminateAction)
case s: String =>
lineReader.getHistory match {
case p: PersistentHistory =>
p.add(s)
p.flush()
case _ =>
}
s match {
case None => Left(TerminateAction)
case Some(s: String) =>
s.trim() match {
case "" => impl()
case cmd @ (`Shutdown` | `TerminateAction` | `Cancel`) => Left(cmd)
case cmd =>
if (terminal.prompt != Prompt.Batch) terminal.setPrompt(Prompt.Running)
terminal.printStream.write(Int.MinValue)
Right(cmd)
case cmd => Right(cmd)
}
}
}
impl()
} catch {
case _: InterruptedException => Right("")
} finally lineReader.close()
}
} catch { case e: InterruptedException => Right("") }
}
}
private[this] def history(s: State): Option[File] =

View File

@ -61,7 +61,7 @@ public final class MetaBuildLoader extends URLClassLoader {
*/
public static MetaBuildLoader makeLoader(final AppProvider appProvider) throws IOException {
final Pattern pattern =
Pattern.compile("(test-interface-[0-9.]+|jline-[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar");
Pattern.compile("^(test-interface-[0-9.]+|jline-[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar");
final File[] cp = appProvider.mainClasspath();
final URL[] interfaceURLs = new URL[3];
final File[] extra =

View File

@ -1504,13 +1504,13 @@ object Defaults extends BuildCommon {
def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() }
print(s)
Terminal.get.withRawSystemIn {
Terminal.get.inputStream.read match {
case -1 => None
try Terminal.get.inputStream.read match {
case -1 | -2 => None
case b =>
val res = b.toChar.toString
println(res)
Some(res)
}
} catch { case e: InterruptedException => None }
}
}),
classes

View File

@ -78,7 +78,7 @@ private[sbt] object xMain {
BspClient.run(dealiasBaseDirectory(configuration))
} else {
bootServerSocket.foreach(l => Terminal.setBootStreams(l.inputStream, l.outputStream))
Terminal.withStreams {
Terminal.withStreams(true) {
if (clientModByEnv || userCommands.exists(isClient)) {
val args = userCommands.toList.filterNot(isClient)
NetworkClient.run(dealiasBaseDirectory(configuration), args)

View File

@ -133,10 +133,17 @@ private[sbt] final class CommandExchange {
}
}
// Do not manually run GC until the user has been idling for at least the min gc interval.
impl(interval match {
val exec = impl(interval match {
case d: FiniteDuration => Some(d.fromNow)
case _ => None
}, idleDeadline)
exec.source.foreach { s =>
channelForName(s.channelName).foreach {
case c if c.terminal.prompt != Prompt.Batch => c.terminal.setPrompt(Prompt.Running)
case _ =>
}
}
exec
}
private def addConsoleChannel(): Unit =
@ -412,6 +419,10 @@ private[sbt] final class CommandExchange {
case _ =>
}
case _ =>
channels.foreach {
case nc: NetworkChannel => nc.shutdown(true, Some(("", "")))
case c => c.shutdown(false)
}
}
private[sbt] def shutdown(name: String): Unit = {
@ -448,7 +459,9 @@ private[sbt] final class CommandExchange {
case mt: FastTrackTask =>
mt.task match {
case `attach` => mt.channel.prompt(ConsolePromptEvent(lastState.get))
case `Cancel` => Option(currentExecRef.get).foreach(cancel)
case `Cancel` =>
Option(currentExecRef.get).foreach(cancel)
mt.channel.prompt(ConsolePromptEvent(lastState.get))
case t if t.startsWith(ContinuousCommands.stopWatch) =>
ContinuousCommands.stopWatchImpl(mt.channel.name)
mt.channel match {
@ -458,6 +471,10 @@ private[sbt] final class CommandExchange {
commandQueue.add(Exec(t, None, None))
case `TerminateAction` => exit(mt)
case `Shutdown` =>
val console = Terminal.console
val needNewLine = console.prompt.isInstanceOf[Prompt.AskUser]
console.setPrompt(Prompt.Batch)
if (needNewLine) console.printStream.println()
channels.find(_.name == mt.channel.name) match {
case Some(c: NetworkChannel) => c.shutdown(false)
case _ =>

View File

@ -1217,7 +1217,6 @@ private[sbt] object ContinuousCommands {
) extends Thread(s"sbt-${channel.name}-watch-ui-thread")
with UITask {
override private[sbt] def reader: UITask.Reader = () => {
channel.terminal.printStream.write(Int.MinValue)
def stop = Right(s"${ContinuousCommands.stopWatch} ${channel.name}")
val exitAction: Watch.Action = {
Watch.apply(

View File

@ -53,11 +53,12 @@ private[sbt] class TaskProgress private ()
if (firstTime.compareAndSet(true, activeExceedingThreshold.isEmpty)) threshold
else sleepDuration
val limit = duration.fromNow
while (Deadline.now < limit) {
while (Deadline.now < limit && !isClosed.get && active.nonEmpty) {
var task = tasks.poll((limit - Deadline.now).toMillis, TimeUnit.MILLISECONDS)
while (task != null) {
if (containsSkipTasks(Vector(task)) || lastTaskCount.get == 0) doReport()
task = tasks.poll
tasks.clear()
}
}
} catch {

View File

@ -99,7 +99,6 @@ final class NetworkChannel(
addFastTrackTask(attach)
}
private[sbt] def prompt(): Unit = {
terminal.setPrompt(Prompt.Running)
interactive.set(true)
jsonRpcNotify(promptChannel, "")
}
@ -641,7 +640,7 @@ final class NetworkChannel(
case -1 => throw new ClosedChannelException()
case b => b
}
} catch { case _: IOException => -1 }
} catch { case e: IOException => -1 }
}
override def available(): Int = inputBuffer.size
}
@ -774,25 +773,82 @@ final class NetworkChannel(
Some(result(queue.take))
}
}
override def getBooleanCapability(capability: String): Boolean =
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
getCapability(
TerminalCapabilitiesQuery(boolean = Some(capability), numeric = None, string = None),
TerminalCapabilitiesQuery(
boolean = Some(capability),
numeric = None,
string = None,
jline3
),
_.boolean.getOrElse(false)
).getOrElse(false)
override def getNumericCapability(capability: String): Int =
override def getNumericCapability(capability: String, jline3: Boolean): Integer =
getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = Some(capability), string = None),
_.numeric.getOrElse(-1)
).getOrElse(-1)
override def getStringCapability(capability: String): String =
TerminalCapabilitiesQuery(
boolean = None,
numeric = Some(capability),
string = None,
jline3
),
(_: TerminalCapabilitiesResponse).numeric.map(Integer.valueOf).getOrElse(-1: Integer)
).getOrElse(-1: Integer)
override def getStringCapability(capability: String, jline3: Boolean): String =
getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = None, string = Some(capability)),
TerminalCapabilitiesQuery(
boolean = None,
numeric = None,
string = Some(capability),
jline3
),
_.string.flatMap {
case "null" => None
case s => Some(s)
}.orNull
).getOrElse("")
override private[sbt] def getAttributes: Map[String, String] =
if (closed.get) Map.empty
else {
import sbt.protocol.codec.JsonProtocol._
val queue = VirtualTerminal.sendTerminalAttributesQuery(
name,
jsonRpcRequest
)
try {
val a = queue.take
Map(
"iflag" -> a.iflag,
"oflag" -> a.oflag,
"cflag" -> a.cflag,
"lflag" -> a.lflag,
"cchars" -> a.cchars
)
} catch { case _: InterruptedException => Map.empty }
}
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit =
if (!closed.get) {
import sbt.protocol.codec.JsonProtocol._
val attrs = TerminalSetAttributesCommand(
iflag = attributes.getOrElse("iflag", ""),
oflag = attributes.getOrElse("oflag", ""),
cflag = attributes.getOrElse("cflag", ""),
lflag = attributes.getOrElse("lflag", ""),
cchars = attributes.getOrElse("cchars", ""),
)
val queue = VirtualTerminal.setTerminalAttributes(name, jsonRpcRequest, attrs)
try queue.take
catch { case _: InterruptedException => }
}
override def setSize(width: Int, height: Int): Unit =
if (!closed.get) {
import sbt.protocol.codec.JsonProtocol._
val size = TerminalSetSizeCommand(width, height)
val queue = VirtualTerminal.setTerminalSize(name, jsonRpcRequest, size)
try queue.take
catch { case _: InterruptedException => }
}
override def toString: String = s"NetworkTerminal($name)"
override def close(): Unit = if (closed.compareAndSet(false, true)) {
val threads = blockedThreads.synchronized {

View File

@ -25,16 +25,27 @@ import sbt.protocol.Serialization.{
import sjsonnew.support.scalajson.unsafe.Converter
import sbt.protocol.{
Attach,
TerminalAttributesQuery,
TerminalAttributesResponse,
TerminalCapabilitiesQuery,
TerminalCapabilitiesResponse,
TerminalPropertiesResponse
TerminalPropertiesResponse,
TerminalSetAttributesCommand,
TerminalSetSizeCommand,
}
import sbt.protocol.codec.JsonProtocol._
object VirtualTerminal {
private[this] val pendingTerminalProperties =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalPropertiesResponse]]()
private[this] val pendingTerminalCapabilities =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalCapabilitiesResponse]]
private[this] val pendingTerminalAttributes =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalAttributesResponse]]
private[this] val pendingTerminalSetAttributes =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]]
private[this] val pendingTerminalSetSize =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]]
private[sbt] def sendTerminalPropertiesQuery(
channelName: String,
jsonRpcRequest: (String, String, String) => Unit
@ -70,6 +81,39 @@ object VirtualTerminal {
case _ =>
}
}
private[sbt] def sendTerminalAttributesQuery(
channelName: String,
jsonRpcRequest: (String, String, TerminalAttributesQuery) => Unit,
): ArrayBlockingQueue[TerminalAttributesResponse] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[TerminalAttributesResponse](1)
pendingTerminalAttributes.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, TerminalAttributesQuery())
queue
}
private[sbt] def setTerminalAttributes(
channelName: String,
jsonRpcRequest: (String, String, TerminalSetAttributesCommand) => Unit,
query: TerminalSetAttributesCommand
): ArrayBlockingQueue[Unit] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[Unit](1)
pendingTerminalSetAttributes.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, query)
queue
}
private[sbt] def setTerminalSize(
channelName: String,
jsonRpcRequest: (String, String, TerminalSetSizeCommand) => Unit,
query: TerminalSetSizeCommand
): ArrayBlockingQueue[Unit] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[Unit](1)
pendingTerminalSetSize.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, query)
queue
}
val handler = ServerHandler { cb =>
ServerIntent(requestHandler(cb), responseHandler(cb), notificationHandler(cb))
}
@ -77,7 +121,6 @@ object VirtualTerminal {
private val requestHandler: Handler[JsonRpcRequestMessage] =
callback => {
case r if r.method == attach =>
import sbt.protocol.codec.JsonProtocol.AttachFormat
val isInteractive = r.params
.flatMap(Converter.fromJson[Attach](_).toOption.map(_.interactive))
.exists(identity)
@ -89,7 +132,6 @@ object VirtualTerminal {
private val responseHandler: Handler[JsonRpcResponseMessage] =
callback => {
case r if pendingTerminalProperties.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response =
r.result.flatMap(Converter.fromJson[TerminalPropertiesResponse](_).toOption)
pendingTerminalProperties.remove((callback.name, r.id)) match {
@ -97,7 +139,6 @@ object VirtualTerminal {
case buffer => response.foreach(buffer.put)
}
case r if pendingTerminalCapabilities.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response =
r.result.flatMap(
Converter.fromJson[TerminalCapabilitiesResponse](_).toOption
@ -107,6 +148,24 @@ object VirtualTerminal {
case buffer =>
buffer.put(response.getOrElse(TerminalCapabilitiesResponse(None, None, None)))
}
case r if pendingTerminalAttributes.get((callback.name, r.id)) != null =>
val response =
r.result.flatMap(Converter.fromJson[TerminalAttributesResponse](_).toOption)
pendingTerminalAttributes.remove((callback.name, r.id)) match {
case null =>
case buffer =>
buffer.put(response.getOrElse(TerminalAttributesResponse("", "", "", "", "")))
}
case r if pendingTerminalSetAttributes.get((callback.name, r.id)) != null =>
pendingTerminalSetAttributes.remove((callback.name, r.id)) match {
case null =>
case buffer => buffer.put(())
}
case r if pendingTerminalSetSize.get((callback.name, r.id)) != null =>
pendingTerminalSetSize.remove((callback.name, r.id)) match {
case null =>
case buffer => buffer.put(())
}
}
private val notificationHandler: Handler[JsonRpcNotificationMessage] =
callback => {

View File

@ -84,7 +84,9 @@ object Dependencies {
val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash")
val jline = "org.scala-sbt.jline" % "jline" % "2.14.7-sbt-5e51b9d4f9631ebfa29753ce4accc57808e7fd6b"
val jansi = "org.fusesource.jansi" % "jansi" % "1.12"
val jline3 = "org.jline" % "jline" % "3.15.0"
val jline3Jansi = "org.jline" % "jline-terminal-jansi" % "3.15.0"
val jansi = "org.fusesource.jansi" % "jansi" % "1.18"
val scalatest = "org.scalatest" %% "scalatest" % "3.0.8"
val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0"
val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1"

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalAttributesQuery private () extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case _: TerminalAttributesQuery => true
case _ => false
}
override def hashCode: Int = {
37 * (17 + "sbt.protocol.TerminalAttributesQuery".##)
}
override def toString: String = {
"TerminalAttributesQuery()"
}
private[this] def copy(): TerminalAttributesQuery = {
new TerminalAttributesQuery()
}
}
object TerminalAttributesQuery {
def apply(): TerminalAttributesQuery = new TerminalAttributesQuery()
}

View File

@ -0,0 +1,48 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalAttributesResponse private (
val iflag: String,
val oflag: String,
val cflag: String,
val lflag: String,
val cchars: String) extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalAttributesResponse => (this.iflag == x.iflag) && (this.oflag == x.oflag) && (this.cflag == x.cflag) && (this.lflag == x.lflag) && (this.cchars == x.cchars)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalAttributesResponse".##) + iflag.##) + oflag.##) + cflag.##) + lflag.##) + cchars.##)
}
override def toString: String = {
"TerminalAttributesResponse(" + iflag + ", " + oflag + ", " + cflag + ", " + lflag + ", " + cchars + ")"
}
private[this] def copy(iflag: String = iflag, oflag: String = oflag, cflag: String = cflag, lflag: String = lflag, cchars: String = cchars): TerminalAttributesResponse = {
new TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars)
}
def withIflag(iflag: String): TerminalAttributesResponse = {
copy(iflag = iflag)
}
def withOflag(oflag: String): TerminalAttributesResponse = {
copy(oflag = oflag)
}
def withCflag(cflag: String): TerminalAttributesResponse = {
copy(cflag = cflag)
}
def withLflag(lflag: String): TerminalAttributesResponse = {
copy(lflag = lflag)
}
def withCchars(cchars: String): TerminalAttributesResponse = {
copy(cchars = cchars)
}
}
object TerminalAttributesResponse {
def apply(iflag: String, oflag: String, cflag: String, lflag: String, cchars: String): TerminalAttributesResponse = new TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars)
}

View File

@ -7,22 +7,23 @@ package sbt.protocol
final class TerminalCapabilitiesQuery private (
val boolean: Option[String],
val numeric: Option[String],
val string: Option[String]) extends sbt.protocol.CommandMessage() with Serializable {
val string: Option[String],
val jline3: Boolean) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalCapabilitiesQuery => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string)
case x: TerminalCapabilitiesQuery => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string) && (this.jline3 == x.jline3)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesQuery".##) + boolean.##) + numeric.##) + string.##)
37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesQuery".##) + boolean.##) + numeric.##) + string.##) + jline3.##)
}
override def toString: String = {
"TerminalCapabilitiesQuery(" + boolean + ", " + numeric + ", " + string + ")"
"TerminalCapabilitiesQuery(" + boolean + ", " + numeric + ", " + string + ", " + jline3 + ")"
}
private[this] def copy(boolean: Option[String] = boolean, numeric: Option[String] = numeric, string: Option[String] = string): TerminalCapabilitiesQuery = {
new TerminalCapabilitiesQuery(boolean, numeric, string)
private[this] def copy(boolean: Option[String] = boolean, numeric: Option[String] = numeric, string: Option[String] = string, jline3: Boolean = jline3): TerminalCapabilitiesQuery = {
new TerminalCapabilitiesQuery(boolean, numeric, string, jline3)
}
def withBoolean(boolean: Option[String]): TerminalCapabilitiesQuery = {
copy(boolean = boolean)
@ -42,9 +43,12 @@ final class TerminalCapabilitiesQuery private (
def withString(string: String): TerminalCapabilitiesQuery = {
copy(string = Option(string))
}
def withJline3(jline3: Boolean): TerminalCapabilitiesQuery = {
copy(jline3 = jline3)
}
}
object TerminalCapabilitiesQuery {
def apply(boolean: Option[String], numeric: Option[String], string: Option[String]): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string)
def apply(boolean: String, numeric: String, string: String): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(string))
def apply(boolean: Option[String], numeric: Option[String], string: Option[String], jline3: Boolean): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string, jline3)
def apply(boolean: String, numeric: String, string: String, jline3: Boolean): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(string), jline3)
}

View File

@ -0,0 +1,48 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetAttributesCommand private (
val iflag: String,
val oflag: String,
val cflag: String,
val lflag: String,
val cchars: String) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalSetAttributesCommand => (this.iflag == x.iflag) && (this.oflag == x.oflag) && (this.cflag == x.cflag) && (this.lflag == x.lflag) && (this.cchars == x.cchars)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalSetAttributesCommand".##) + iflag.##) + oflag.##) + cflag.##) + lflag.##) + cchars.##)
}
override def toString: String = {
"TerminalSetAttributesCommand(" + iflag + ", " + oflag + ", " + cflag + ", " + lflag + ", " + cchars + ")"
}
private[this] def copy(iflag: String = iflag, oflag: String = oflag, cflag: String = cflag, lflag: String = lflag, cchars: String = cchars): TerminalSetAttributesCommand = {
new TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars)
}
def withIflag(iflag: String): TerminalSetAttributesCommand = {
copy(iflag = iflag)
}
def withOflag(oflag: String): TerminalSetAttributesCommand = {
copy(oflag = oflag)
}
def withCflag(cflag: String): TerminalSetAttributesCommand = {
copy(cflag = cflag)
}
def withLflag(lflag: String): TerminalSetAttributesCommand = {
copy(lflag = lflag)
}
def withCchars(cchars: String): TerminalSetAttributesCommand = {
copy(cchars = cchars)
}
}
object TerminalSetAttributesCommand {
def apply(iflag: String, oflag: String, cflag: String, lflag: String, cchars: String): TerminalSetAttributesCommand = new TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars)
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetAttributesResponse private () extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case _: TerminalSetAttributesResponse => true
case _ => false
}
override def hashCode: Int = {
37 * (17 + "sbt.protocol.TerminalSetAttributesResponse".##)
}
override def toString: String = {
"TerminalSetAttributesResponse()"
}
private[this] def copy(): TerminalSetAttributesResponse = {
new TerminalSetAttributesResponse()
}
}
object TerminalSetAttributesResponse {
def apply(): TerminalSetAttributesResponse = new TerminalSetAttributesResponse()
}

View File

@ -0,0 +1,36 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetSizeCommand private (
val width: Int,
val height: Int) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalSetSizeCommand => (this.width == x.width) && (this.height == x.height)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (17 + "sbt.protocol.TerminalSetSizeCommand".##) + width.##) + height.##)
}
override def toString: String = {
"TerminalSetSizeCommand(" + width + ", " + height + ")"
}
private[this] def copy(width: Int = width, height: Int = height): TerminalSetSizeCommand = {
new TerminalSetSizeCommand(width, height)
}
def withWidth(width: Int): TerminalSetSizeCommand = {
copy(width = width)
}
def withHeight(height: Int): TerminalSetSizeCommand = {
copy(height = height)
}
}
object TerminalSetSizeCommand {
def apply(width: Int, height: Int): TerminalSetSizeCommand = new TerminalSetSizeCommand(width, height)
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetSizeResponse private () extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case _: TerminalSetSizeResponse => true
case _ => false
}
override def hashCode: Int = {
37 * (17 + "sbt.protocol.TerminalSetSizeResponse".##)
}
override def toString: String = {
"TerminalSetSizeResponse()"
}
private[this] def copy(): TerminalSetSizeResponse = {
new TerminalSetSizeResponse()
}
}
object TerminalSetSizeResponse {
def apply(): TerminalSetSizeResponse = new TerminalSetSizeResponse()
}

View File

@ -6,6 +6,6 @@
package sbt.protocol.codec
import _root_.sjsonnew.JsonFormat
trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats =>
implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat5[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery]("type")
trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats with sbt.protocol.codec.TerminalSetAttributesCommandFormats with sbt.protocol.codec.TerminalAttributesQueryFormats with sbt.protocol.codec.TerminalSetSizeCommandFormats =>
implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat8[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery, sbt.protocol.TerminalSetAttributesCommand, sbt.protocol.TerminalAttributesQuery, sbt.protocol.TerminalSetSizeCommand]("type")
}

View File

@ -6,6 +6,6 @@
package sbt.protocol.codec
import _root_.sjsonnew.JsonFormat
trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats with sbt.internal.util.codec.JValueFormats with sbt.protocol.codec.SettingQuerySuccessFormats with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats =>
implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat7[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure, sbt.protocol.TerminalPropertiesResponse, sbt.protocol.TerminalCapabilitiesResponse]("type")
trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats with sbt.internal.util.codec.JValueFormats with sbt.protocol.codec.SettingQuerySuccessFormats with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats with sbt.protocol.codec.TerminalSetAttributesResponseFormats with sbt.protocol.codec.TerminalAttributesResponseFormats with sbt.protocol.codec.TerminalSetSizeResponseFormats =>
implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat10[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure, sbt.protocol.TerminalPropertiesResponse, sbt.protocol.TerminalCapabilitiesResponse, sbt.protocol.TerminalSetAttributesResponse, sbt.protocol.TerminalAttributesResponse, sbt.protocol.TerminalSetSizeResponse]("type")
}

View File

@ -10,6 +10,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.protocol.codec.SettingQueryFormats
with sbt.protocol.codec.AttachFormats
with sbt.protocol.codec.TerminalCapabilitiesQueryFormats
with sbt.protocol.codec.TerminalSetAttributesCommandFormats
with sbt.protocol.codec.TerminalAttributesQueryFormats
with sbt.protocol.codec.TerminalSetSizeCommandFormats
with sbt.protocol.codec.CommandMessageFormats
with sbt.protocol.codec.CompletionParamsFormats
with sbt.protocol.codec.ChannelAcceptedEventFormats
@ -20,6 +23,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.protocol.codec.SettingQueryFailureFormats
with sbt.protocol.codec.TerminalPropertiesResponseFormats
with sbt.protocol.codec.TerminalCapabilitiesResponseFormats
with sbt.protocol.codec.TerminalSetAttributesResponseFormats
with sbt.protocol.codec.TerminalAttributesResponseFormats
with sbt.protocol.codec.TerminalSetSizeResponseFormats
with sbt.protocol.codec.EventMessageFormats
with sbt.protocol.codec.SettingQueryResponseFormats
with sbt.protocol.codec.CompletionResponseFormats

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalAttributesQueryFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalAttributesQueryFormat: JsonFormat[sbt.protocol.TerminalAttributesQuery] = new JsonFormat[sbt.protocol.TerminalAttributesQuery] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalAttributesQuery = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
unbuilder.endObject()
sbt.protocol.TerminalAttributesQuery()
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalAttributesQuery, builder: Builder[J]): Unit = {
builder.beginObject()
builder.endObject()
}
}
}

View File

@ -0,0 +1,35 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalAttributesResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalAttributesResponseFormat: JsonFormat[sbt.protocol.TerminalAttributesResponse] = new JsonFormat[sbt.protocol.TerminalAttributesResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalAttributesResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val iflag = unbuilder.readField[String]("iflag")
val oflag = unbuilder.readField[String]("oflag")
val cflag = unbuilder.readField[String]("cflag")
val lflag = unbuilder.readField[String]("lflag")
val cchars = unbuilder.readField[String]("cchars")
unbuilder.endObject()
sbt.protocol.TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalAttributesResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("iflag", obj.iflag)
builder.addField("oflag", obj.oflag)
builder.addField("cflag", obj.cflag)
builder.addField("lflag", obj.lflag)
builder.addField("cchars", obj.cchars)
builder.endObject()
}
}
}

View File

@ -14,8 +14,9 @@ implicit lazy val TerminalCapabilitiesQueryFormat: JsonFormat[sbt.protocol.Termi
val boolean = unbuilder.readField[Option[String]]("boolean")
val numeric = unbuilder.readField[Option[String]]("numeric")
val string = unbuilder.readField[Option[String]]("string")
val jline3 = unbuilder.readField[Boolean]("jline3")
unbuilder.endObject()
sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string)
sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string, jline3)
case None =>
deserializationError("Expected JsObject but found None")
}
@ -25,6 +26,7 @@ implicit lazy val TerminalCapabilitiesQueryFormat: JsonFormat[sbt.protocol.Termi
builder.addField("boolean", obj.boolean)
builder.addField("numeric", obj.numeric)
builder.addField("string", obj.string)
builder.addField("jline3", obj.jline3)
builder.endObject()
}
}

View File

@ -0,0 +1,35 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetAttributesCommandFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetAttributesCommandFormat: JsonFormat[sbt.protocol.TerminalSetAttributesCommand] = new JsonFormat[sbt.protocol.TerminalSetAttributesCommand] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetAttributesCommand = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val iflag = unbuilder.readField[String]("iflag")
val oflag = unbuilder.readField[String]("oflag")
val cflag = unbuilder.readField[String]("cflag")
val lflag = unbuilder.readField[String]("lflag")
val cchars = unbuilder.readField[String]("cchars")
unbuilder.endObject()
sbt.protocol.TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetAttributesCommand, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("iflag", obj.iflag)
builder.addField("oflag", obj.oflag)
builder.addField("cflag", obj.cflag)
builder.addField("lflag", obj.lflag)
builder.addField("cchars", obj.cchars)
builder.endObject()
}
}
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetAttributesResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetAttributesResponseFormat: JsonFormat[sbt.protocol.TerminalSetAttributesResponse] = new JsonFormat[sbt.protocol.TerminalSetAttributesResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetAttributesResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
unbuilder.endObject()
sbt.protocol.TerminalSetAttributesResponse()
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetAttributesResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.endObject()
}
}
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetSizeCommandFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetSizeCommandFormat: JsonFormat[sbt.protocol.TerminalSetSizeCommand] = new JsonFormat[sbt.protocol.TerminalSetSizeCommand] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetSizeCommand = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val width = unbuilder.readField[Int]("width")
val height = unbuilder.readField[Int]("height")
unbuilder.endObject()
sbt.protocol.TerminalSetSizeCommand(width, height)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetSizeCommand, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("width", obj.width)
builder.addField("height", obj.height)
builder.endObject()
}
}
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetSizeResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetSizeResponseFormat: JsonFormat[sbt.protocol.TerminalSetSizeResponse] = new JsonFormat[sbt.protocol.TerminalSetSizeResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetSizeResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
unbuilder.endObject()
sbt.protocol.TerminalSetSizeResponse()
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetSizeResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.endObject()
}
}
}

View File

@ -97,6 +97,7 @@ type TerminalCapabilitiesQuery implements CommandMessage {
boolean: String
numeric: String
string: String
jline3: Boolean!
}
type TerminalCapabilitiesResponse implements EventMessage {
@ -104,3 +105,30 @@ type TerminalCapabilitiesResponse implements EventMessage {
numeric: Int
string: String
}
type TerminalSetAttributesCommand implements CommandMessage {
iflag: String!,
oflag: String!,
cflag: String!,
lflag: String!,
cchars: String!,
}
type TerminalSetAttributesResponse implements EventMessage {}
type TerminalAttributesQuery implements CommandMessage {}
type TerminalAttributesResponse implements EventMessage {
iflag: String!,
oflag: String!,
cflag: String!,
lflag: String!,
cchars: String!,
}
type TerminalSetSizeCommand implements CommandMessage {
width: Int!
height: Int!
}
type TerminalSetSizeResponse implements EventMessage {}

View File

@ -26,6 +26,7 @@ object Serialization {
private[sbt] val VsCode = "application/vscode-jsonrpc; charset=utf-8"
val systemIn = "sbt/systemIn"
val systemOut = "sbt/systemOut"
val systemOutFlush = "sbt/systemOutFlush"
val terminalPropertiesQuery = "sbt/terminalPropertiesQuery"
val terminalPropertiesResponse = "sbt/terminalPropertiesResponse"
val terminalCapabilities = "sbt/terminalCapabilities"
@ -34,6 +35,9 @@ object Serialization {
val attachResponse = "sbt/attachResponse"
val cancelRequest = "sbt/cancelRequest"
val promptChannel = "sbt/promptChannel"
val setTerminalAttributes = "sbt/setTerminalAttributes"
val getTerminalAttributes = "sbt/getTerminalAttributes"
val setTerminalSize = "sbt/setTerminalSize"
val CancelAll = "__CancelAll"
@deprecated("unused", since = "1.4.0")