Merge pull request #5904 from eatkins/sbt-terminal

Add `terminal` task
This commit is contained in:
eugene yokota 2020-09-27 20:45:30 -04:00 committed by GitHub
commit d96ecb2c21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 118 additions and 35 deletions

View File

@ -261,6 +261,13 @@ private[sbt] object JLine3 {
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[sbt] def setMode(term: Terminal, canonical: Boolean, echo: Boolean): Unit = {
val prev = attributesFromMap(term.getAttributes)
val newAttrs = new Attributes(prev)
newAttrs.setLocalFlag(LocalFlag.ICANON, canonical)
newAttrs.setLocalFlag(LocalFlag.ECHO, echo)
term.setAttributes(toMap(newAttrs))
}
private[util] def attributesFromMap(map: Map[String, String]): Attributes = {
val attributes = new Attributes
map.get("iflag").foreach { flags =>

View File

@ -7,12 +7,12 @@
package sbt
import sbt.internal.util.{ SimpleReader, Terminal }
import sbt.internal.util.{ SimpleReader, Terminal => ITerminal }
trait CommandLineUIService extends InteractionService {
override def readLine(prompt: String, mask: Boolean): Option[String] = {
val maskChar = if (mask) Some('*') else None
SimpleReader(Terminal.get).readLine(prompt, maskChar)
SimpleReader(ITerminal.get).readLine(prompt, maskChar)
}
// TODO - Implement this better.
override def confirm(msg: String): Boolean = {
@ -21,15 +21,15 @@ trait CommandLineUIService extends InteractionService {
(in == "y" || in == "yes")
}
}
SimpleReader(Terminal.get).readLine(msg + " (yes/no): ", None) match {
SimpleReader(ITerminal.get).readLine(msg + " (yes/no): ", None) match {
case Some(Assent()) => true
case _ => false
}
}
override def terminalWidth: Int = Terminal.get.getWidth
override def terminalWidth: Int = ITerminal.get.getWidth
override def terminalHeight: Int = Terminal.get.getHeight
override def terminalHeight: Int = ITerminal.get.getHeight
}
object CommandLineUIService extends CommandLineUIService

View File

@ -57,7 +57,7 @@ import sbt.internal.server.{
import sbt.internal.testing.TestLogger
import sbt.internal.util.Attributed.data
import sbt.internal.util.Types._
import sbt.internal.util._
import sbt.internal.util.{ Terminal => ITerminal, _ }
import sbt.internal.util.complete._
import sbt.io.Path._
import sbt.io._
@ -333,7 +333,7 @@ object Defaults extends BuildCommon {
if (!useScalaReplJLine.value) classOf[org.jline.terminal.Terminal].getClassLoader
else appConfiguration.value.provider.scalaProvider.launcher.topLoader.getParent
},
useSuperShell := { if (insideCI.value) false else Terminal.console.isSupershellEnabled },
useSuperShell := { if (insideCI.value) false else ITerminal.console.isSupershellEnabled },
superShellThreshold :== SysProp.supershellThreshold,
superShellMaxTasks :== SysProp.supershellMaxTasks,
superShellSleep :== SysProp.supershellSleep.millis,
@ -388,6 +388,7 @@ object Defaults extends BuildCommon {
pollInterval :== Watch.defaultPollInterval,
canonicalInput :== true,
echoInput :== true,
terminal := state.value.get(terminalKey).getOrElse(Terminal(ITerminal.get)),
) ++ LintUnused.lintSettings
++ DefaultBackgroundJobService.backgroundJobServiceSettings
++ RemoteCache.globalSettings
@ -1663,13 +1664,13 @@ object Defaults extends BuildCommon {
def askForMainClass(classes: Seq[String]): Option[String] =
sbt.SelectMainClass(
if (classes.length >= 10) Some(SimpleReader(Terminal.get).readLine(_))
if (classes.length >= 10) Some(SimpleReader(ITerminal.get).readLine(_))
else
Some(s => {
def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() }
print(s)
Terminal.get.withRawInput {
try Terminal.get.inputStream.read match {
ITerminal.get.withRawInput {
try ITerminal.get.inputStream.read match {
case -1 | -2 => None
case b =>
val res = b.toChar.toString
@ -1705,7 +1706,7 @@ object Defaults extends BuildCommon {
private[this] def termWrapper(canonical: Boolean, echo: Boolean): (() => Unit) => (() => Unit) =
(f: () => Unit) =>
() => {
val term = Terminal.get
val term = ITerminal.get
if (!canonical) {
term.enterRawMode()
if (echo) term.setEchoEnabled(echo)
@ -3952,7 +3953,7 @@ object Classpaths {
}
}
def shellPromptFromState: State => String = shellPromptFromState(Terminal.console.isColorEnabled)
def shellPromptFromState: State => String = shellPromptFromState(ITerminal.console.isColorEnabled)
def shellPromptFromState(isColorEnabled: Boolean): State => String = { s: State =>
val extracted = Project.extract(s)
(name in extracted.currentRef).get(extracted.structure.data) match {

View File

@ -70,6 +70,8 @@ object Keys {
val serverLog = taskKey[Unit]("A dummy task to set server log level using Global / serverLog / logLevel.").withRank(CTask)
val canonicalInput = settingKey[Boolean]("Toggles whether a task should use canonical input (line buffered with echo) or raw input").withRank(DSetting)
val echoInput = settingKey[Boolean]("Toggles whether a task should echo user input").withRank(DSetting)
val terminal = taskKey[Terminal]("The Terminal associated with a task").withRank(DTask)
private[sbt] val terminalKey = AttributeKey[Terminal]("terminal-key", Invisible)
// Project keys
val autoGeneratedProject = settingKey[Boolean]("If it exists, represents that the project (and name) were automatically created, rather than user specified.").withRank(DSetting)

View File

@ -26,7 +26,7 @@ import sbt.internal.inc.ScalaInstance
import sbt.internal.nio.{ CheckBuildSources, FileTreeRepository }
import sbt.internal.server.{ BuildServerProtocol, NetworkChannel }
import sbt.internal.util.Types.{ const, idFun }
import sbt.internal.util._
import sbt.internal.util.{ Terminal => ITerminal, _ }
import sbt.internal.util.complete.{ Parser, SizeParser }
import sbt.io._
import sbt.io.syntax._
@ -78,8 +78,8 @@ private[sbt] object xMain {
if (userCommands.exists(isBsp)) {
BspClient.run(dealiasBaseDirectory(configuration))
} else {
bootServerSocket.foreach(l => Terminal.setBootStreams(l.inputStream, l.outputStream))
Terminal.withStreams(true) {
bootServerSocket.foreach(l => ITerminal.setBootStreams(l.inputStream, l.outputStream))
ITerminal.withStreams(true) {
if (clientModByEnv || userCommands.exists(isClient)) {
val args = userCommands.toList.filterNot(isClient)
NetworkClient.run(dealiasBaseDirectory(configuration), args)
@ -105,7 +105,7 @@ private[sbt] object xMain {
} finally {
// Clear any stray progress lines
ShutdownHooks.close()
if (Terminal.formatEnabledInEnv) {
if (ITerminal.formatEnabledInEnv) {
System.out.print(ConsoleAppender.ClearScreenAfterCursor)
System.out.flush()
}
@ -118,9 +118,9 @@ private[sbt] object xMain {
try (Some(new BootServerSocket(configuration)) -> None)
catch {
case _: ServerAlreadyBootingException
if System.console != null && !Terminal.startedByRemoteClient =>
if System.console != null && !ITerminal.startedByRemoteClient =>
println("sbt server is already booting. Create a new server? y/n (default y)")
val exit = Terminal.get.withRawInput(System.in.read) match {
val exit = ITerminal.get.withRawInput(System.in.read) match {
case 110 => Some(Exit(1))
case _ => None
}
@ -841,7 +841,7 @@ object BuiltinCommands {
@tailrec
private[this] def doLoadFailed(s: State, loadArg: String): State = {
s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)")
val result = try Terminal.get.withRawInput(System.in.read) match {
val result = try ITerminal.get.withRawInput(System.in.read) match {
case -1 => 'q'.toInt
case b => b
} catch { case _: ClosedChannelException => 'q' }
@ -957,7 +957,7 @@ object BuiltinCommands {
val threshold =
extracted.getOpt(Keys.superShellThreshold).getOrElse(SysProp.supershellThreshold)
val maxItems = extracted.getOpt(Keys.superShellMaxTasks).getOrElse(SysProp.supershellMaxTasks)
Terminal.setConsoleProgressState(new ProgressState(1, maxItems))
ITerminal.setConsoleProgressState(new ProgressState(1, maxItems))
s.put(Keys.superShellSleep.key, sleep)
.put(Keys.superShellThreshold.key, threshold)
.put(Keys.superShellMaxTasks.key, maxItems)
@ -1032,10 +1032,10 @@ object BuiltinCommands {
* by a remote client and only one is able to start a server. This seems to
* happen primarily on windows.
*/
if (Terminal.startedByRemoteClient && !exchange.hasServer) {
if (ITerminal.startedByRemoteClient && !exchange.hasServer) {
Exec(Shutdown, None) +: s1
} else {
if (Terminal.console.prompt == Prompt.Batch) Terminal.console.setPrompt(Prompt.Pending)
if (ITerminal.console.prompt == Prompt.Batch) ITerminal.console.setPrompt(Prompt.Pending)
exchange prompt ConsolePromptEvent(s0)
val minGCInterval = Project
.extract(s1)

View File

@ -15,7 +15,7 @@ import sbt.internal.ShutdownHooks
import sbt.internal.langserver.ErrorCodes
import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.nio.CheckBuildSources.CheckBuildSourcesKey
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Prompt, Terminal }
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Prompt, Terminal => ITerminal }
import sbt.internal.{ ShutdownHooks, TaskProgress }
import sbt.io.{ IO, Using }
import sbt.protocol._
@ -35,7 +35,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(Terminal.restore)
val shutdownHook = ShutdownHooks.add(ITerminal.restore)
try {
runLoggedLoop(state, state.globalLogging.backing)
@ -219,19 +219,19 @@ object MainLoop {
}
exchange.setState(progressState)
exchange.setExec(Some(exec))
val restoreTerminal = channelName.flatMap(exchange.channelForName) match {
val (restoreTerminal, termState) = channelName.flatMap(exchange.channelForName) match {
case Some(c) =>
val prevTerminal = Terminal.set(c.terminal)
val prevTerminal = ITerminal.set(c.terminal)
val prevPrompt = c.terminal.prompt
// temporarily set the prompt to running during task evaluation
c.terminal.setPrompt(Prompt.Running)
() => {
(() => {
c.terminal.setPrompt(prevPrompt)
Terminal.set(prevTerminal)
ITerminal.set(prevTerminal)
c.terminal.setPrompt(prevPrompt)
c.terminal.flush()
}
case _ => () => ()
}) -> progressState.put(Keys.terminalKey, Terminal(c.terminal))
case _ => (() => ()) -> progressState.put(Keys.terminalKey, Terminal(ITerminal.get))
}
/*
* FastTrackCommands.evaluate can be significantly faster than Command.process because
@ -241,8 +241,8 @@ object MainLoop {
*/
val newState = try {
FastTrackCommands
.evaluate(progressState, exec.commandLine)
.getOrElse(Command.process(exec.commandLine, progressState))
.evaluate(termState, exec.commandLine)
.getOrElse(Command.process(exec.commandLine, termState))
} finally {
// Flush the terminal output after command evaluation to ensure that all output
// is displayed in the thin client before we report the command status. Also
@ -262,7 +262,7 @@ object MainLoop {
}
exchange.setExec(None)
newState.get(sbt.Keys.currentTaskProgress).foreach(_.progress.stop())
newState.remove(sbt.Keys.currentTaskProgress)
newState.remove(sbt.Keys.currentTaskProgress).remove(Keys.terminalKey)
}
state.get(CheckBuildSourcesKey) match {
case Some(cbs) =>

View File

@ -0,0 +1,73 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import java.io.{ InputStream, PrintStream }
import sbt.internal.util.{ JLine3, Terminal => ITerminal }
/**
* A Terminal represents a ui connection to sbt. It may control the embedded console
* for an sbt server or it may control a remote client connected through sbtn. The
* Terminal is particularly useful whenever an sbt task needs to receive input from
* the user.
*
*/
trait Terminal {
/**
* Returns the width of the terminal.
*
* @return the width ot the terminal
*/
def getWidth: Int
/**
* Returns the height of the terminal
*
* @return the height of the terminal
*/
def getHeight: Int
/**
* An input stream associated with the terminal. Bytes inputted by the
* user may be read from this input stream.
*
* @return the terminal's input stream
*/
def inputStream: InputStream
/**
* A print stream associated with the terminal. Writing to this output
* stream should display text on the terminal.
*
* @return the terminal's input stream
*/
def printStream: PrintStream
/**
* Sets the mode of the terminal. By default,the terminal will be in canonical mode
* with echo enabled. This means that the terminal's inputStream will not return any
* bytes until a newline is received and that all of the characters inputed by the
* user will be echoed to the terminal's output stream.
*
* @param canonical toggles whether or not the terminal input stream is line buffered
* @param echo toggles whether or not to echo the characters received from the terminal input stream
*/
def setMode(canonical: Boolean, echo: Boolean): Unit
}
private[sbt] object Terminal {
private[sbt] def apply(term: ITerminal): Terminal = new Terminal {
override def getHeight: Int = term.getHeight
override def getWidth: Int = term.getWidth
override def inputStream: InputStream = term.inputStream
override def printStream: PrintStream = term.printStream
override def setMode(canonical: Boolean, echo: Boolean): Unit =
JLine3.setMode(term, canonical, echo)
}
}

View File

@ -18,7 +18,7 @@ import sbt.internal.util.{
GlobalLogging,
MainAppender,
Settings,
Terminal
Terminal => ITerminal,
}
object PluginCommandTestPlugin0 extends AutoPlugin { override def requires = empty }
@ -77,7 +77,7 @@ object FakeState {
val logFile = File.createTempFile("sbt", ".log")
try {
val state = FakeState(logFile, enabledPlugins: _*)
Terminal.withOut(new PrintStream(outBuffer, true)) {
ITerminal.withOut(new PrintStream(outBuffer, true)) {
MainLoop.processCommand(Exec(input, None), state)
}
new String(outBuffer.toByteArray)