mirror of https://github.com/sbt/sbt.git
Make new more interactive
If the terminal supports ANSI control sequence, this displays the template list in an interactive way. The focused template is rendered reversed, and arrow key can be used to move the focus up/down.
This commit is contained in:
parent
3d1349a37d
commit
51591bde5b
|
|
@ -176,9 +176,40 @@ trait Terminal extends AutoCloseable {
|
||||||
else 0
|
else 0
|
||||||
}
|
}
|
||||||
private[sbt] def flush(): Unit = printStream.flush()
|
private[sbt] def flush(): Unit = printStream.flush()
|
||||||
|
|
||||||
|
private[sbt] def readArrow: Int = withRawInput {
|
||||||
|
val in = System.in
|
||||||
|
val ESC = '\u001B'
|
||||||
|
val EOT = '\u0004'
|
||||||
|
var result: Int = -1
|
||||||
|
def readBracket: Int =
|
||||||
|
in.read() match {
|
||||||
|
case '[' => readAnsiControl
|
||||||
|
case _ => 0
|
||||||
|
}
|
||||||
|
def readAnsiControl: Int =
|
||||||
|
in.read() match {
|
||||||
|
case 'A' => Terminal.VK_UP
|
||||||
|
case 'B' => Terminal.VK_DOWN
|
||||||
|
case 'C' => Terminal.VK_RIGHT
|
||||||
|
case 'D' => Terminal.VK_LEFT
|
||||||
|
case _ => 0
|
||||||
|
}
|
||||||
|
in.read() match {
|
||||||
|
case ESC => readBracket
|
||||||
|
// Ctrl+D to quit
|
||||||
|
case EOT => -1
|
||||||
|
case c => c
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Terminal {
|
object Terminal {
|
||||||
|
private[sbt] final val VK_UP = 256
|
||||||
|
private[sbt] final val VK_DOWN = 257
|
||||||
|
private[sbt] final val VK_RIGHT = 258
|
||||||
|
private[sbt] final val VK_LEFT = 259
|
||||||
|
|
||||||
val NO_BOOT_CLIENTS_CONNECTED: Int = -2
|
val NO_BOOT_CLIENTS_CONNECTED: Int = -2
|
||||||
// Disable noisy jline log spam
|
// Disable noisy jline log spam
|
||||||
if (System.getProperty("sbt.jline.verbose", "false") != "true")
|
if (System.getProperty("sbt.jline.verbose", "false") != "true")
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ private[sbt] object xMain {
|
||||||
Seq(defaults, early),
|
Seq(defaults, early),
|
||||||
runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil
|
runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil
|
||||||
)
|
)
|
||||||
|
.put(BasicKeys.detachStdio, detachStdio)
|
||||||
StandardMain.runManaged(state)
|
StandardMain.runManaged(state)
|
||||||
}
|
}
|
||||||
case _ if clientModByEnv || userCommands.exists(isClient) =>
|
case _ if clientModByEnv || userCommands.exists(isClient) =>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import sbt.BasicCommandStrings.TerminateAction
|
||||||
import sbt.SlashSyntax0._
|
import sbt.SlashSyntax0._
|
||||||
import sbt.io._, syntax._
|
import sbt.io._, syntax._
|
||||||
import sbt.util._
|
import sbt.util._
|
||||||
|
import sbt.internal.util.{ ConsoleAppender, Terminal => ITerminal }
|
||||||
import sbt.internal.util.complete.{ DefaultParsers, Parser }, DefaultParsers._
|
import sbt.internal.util.complete.{ DefaultParsers, Parser }, DefaultParsers._
|
||||||
import xsbti.AppConfiguration
|
import xsbti.AppConfiguration
|
||||||
import sbt.librarymanagement._
|
import sbt.librarymanagement._
|
||||||
|
|
@ -161,39 +162,96 @@ private[sbt] object TemplateCommandUtil {
|
||||||
private final val ScalaToolkitSlug = "scala/toolkit.local"
|
private final val ScalaToolkitSlug = "scala/toolkit.local"
|
||||||
private final val TypelevelToolkitSlug = "typelevel/toolkit.local"
|
private final val TypelevelToolkitSlug = "typelevel/toolkit.local"
|
||||||
private final val SbtCrossPlatformSlug = "sbt/cross-platform.local"
|
private final val SbtCrossPlatformSlug = "sbt/cross-platform.local"
|
||||||
private def fortifyArgs(): List[String] = {
|
private lazy val term: ITerminal = ITerminal.get
|
||||||
val templates = List(
|
private lazy val isAnsiSupported = term.isAnsiSupported
|
||||||
ScalaToolkitSlug -> "Scala Toolkit (beta) by Scala Center and VirtusLab",
|
private lazy val nonMoveLetters = ('a' to 'z').toList diff List('h', 'j', 'k', 'l', 'q')
|
||||||
TypelevelToolkitSlug -> "Toolkit to start building Typelevel apps",
|
private lazy val templates = List(
|
||||||
SbtCrossPlatformSlug -> "A cross-JVM/JS/Native project",
|
ScalaToolkitSlug -> "Scala Toolkit (beta) by Scala Center and VirtusLab",
|
||||||
"scala/scala-seed.g8" -> "Scala 2 seed template",
|
TypelevelToolkitSlug -> "Toolkit to start building Typelevel apps",
|
||||||
"playframework/play-scala-seed.g8" -> "A Play project in Scala",
|
SbtCrossPlatformSlug -> "A cross-JVM/JS/Native project",
|
||||||
"playframework/play-java-seed.g8" -> "A Play project in Java",
|
"scala/scala-seed.g8" -> "Scala 2 seed template",
|
||||||
"scala-js/vite.g8" -> "A Scala.JS + Vite project",
|
"playframework/play-scala-seed.g8" -> "A Play project in Scala",
|
||||||
"holdenk/sparkProjectTemplate.g8" -> "A Scala Spark project",
|
"playframework/play-java-seed.g8" -> "A Play project in Java",
|
||||||
"spotify/scio.g8" -> "A Scio project",
|
"scala-js/vite.g8" -> "A Scala.JS + Vite project",
|
||||||
"disneystreaming/smithy4s.g8" -> "A Smithy4s project",
|
"holdenk/sparkProjectTemplate.g8" -> "A Scala Spark project",
|
||||||
)
|
"spotify/scio.g8" -> "A Scio project",
|
||||||
val mappingList = templates.zipWithIndex.map {
|
"disneystreaming/smithy4s.g8" -> "A Smithy4s project",
|
||||||
case (v, idx) => (idx + 1) -> v
|
)
|
||||||
|
private def fortifyArgs(): List[String] =
|
||||||
|
if (System.console eq null) Nil
|
||||||
|
else
|
||||||
|
ITerminal.withStreams(true, false) {
|
||||||
|
val mappingList = templates.zipWithIndex.map {
|
||||||
|
case (v, idx) => nonMoveLetters(idx).toString -> v
|
||||||
|
}
|
||||||
|
val out = term.printStream
|
||||||
|
out.println("")
|
||||||
|
out.println("Welcome to sbt new!")
|
||||||
|
out.println("Here are some templates to get started:")
|
||||||
|
val ans = askTemplate(mappingList, 0)
|
||||||
|
val mappings = Map(mappingList: _*)
|
||||||
|
mappings.get(ans).map(_._1).toList
|
||||||
|
}
|
||||||
|
|
||||||
|
private def askTemplate(mappingList: List[(String, (String, String))], focus: Int): String = {
|
||||||
|
val msg = "Select a template"
|
||||||
|
displayMappings(mappingList, focus)
|
||||||
|
val focusValue = ('a' + focus).toChar.toString
|
||||||
|
if (!isAnsiSupported) ask(msg, focusValue)
|
||||||
|
else {
|
||||||
|
val out = term.printStream
|
||||||
|
out.print(s"$msg: ")
|
||||||
|
val ans0 = term.readArrow
|
||||||
|
def printThenReturn(ans: String): String = {
|
||||||
|
out.println(ans) // this is necessary to move the cursor
|
||||||
|
out.flush()
|
||||||
|
ans
|
||||||
|
}
|
||||||
|
ans0 match {
|
||||||
|
case '\r' | '\n' => printThenReturn(focusValue)
|
||||||
|
case 'q' | 'Q' | -1 => printThenReturn("")
|
||||||
|
case 'j' | 'J' | ITerminal.VK_DOWN =>
|
||||||
|
clearMenu(mappingList)
|
||||||
|
askTemplate(mappingList, math.min(focus + 1, mappingList.size - 1))
|
||||||
|
case 'k' | 'K' | ITerminal.VK_UP =>
|
||||||
|
clearMenu(mappingList)
|
||||||
|
askTemplate(mappingList, math.max(focus - 1, 0))
|
||||||
|
case c if nonMoveLetters.contains(c.toChar) =>
|
||||||
|
printThenReturn(c.toChar.toString)
|
||||||
|
case _ =>
|
||||||
|
clearMenu(mappingList)
|
||||||
|
askTemplate(mappingList, focus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
System.out.println("")
|
}
|
||||||
System.out.println("Welcome to sbt new!")
|
|
||||||
System.out.println("Here are some templates to get started:")
|
private def clearMenu(mappingList: List[(String, (String, String))]): Unit = {
|
||||||
mappingList.foreach {
|
val out = term.printStream
|
||||||
case (k, (slug, desc)) =>
|
out.print(ConsoleAppender.CursorLeft1000)
|
||||||
val key = if (k < 10) s" $k" else k.toString
|
out.print(ConsoleAppender.cursorUp(mappingList.size + 1))
|
||||||
System.out.println(s" $key) ${slug.padTo(33, ' ')} - $desc")
|
}
|
||||||
|
|
||||||
|
private def displayMappings(mappingList: List[(String, (String, String))], focus: Int): Unit = {
|
||||||
|
import scala.Console.{ RESET, REVERSED }
|
||||||
|
val out = term.printStream
|
||||||
|
mappingList.zipWithIndex.foreach {
|
||||||
|
case ((k, (slug, desc)), idx) =>
|
||||||
|
if (idx == focus && isAnsiSupported) {
|
||||||
|
out.print(REVERSED)
|
||||||
|
}
|
||||||
|
out.print(s" $k) ${slug.padTo(33, ' ')} - $desc")
|
||||||
|
if (idx == focus && isAnsiSupported) {
|
||||||
|
out.print(RESET)
|
||||||
|
}
|
||||||
|
out.println()
|
||||||
}
|
}
|
||||||
System.out.println(" q) quit")
|
out.println(" q) quit")
|
||||||
val ans = ask("Select a template", "1")
|
out.flush()
|
||||||
val mappings = Map((mappingList.map { case (k, v) => k.toString -> v }): _*)
|
|
||||||
mappings.get(ans).map(_._1).toList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def ask(question: String, default: String): String = {
|
private def ask(question: String, default: String): String = {
|
||||||
System.out.print(s"$question (default: $default): ")
|
System.out.print(s"$question (default: $default): ")
|
||||||
val ans0 = System.console().readLine()
|
val ans0 = System.console.readLine()
|
||||||
if (ans0 == "") default
|
if (ans0 == "") default
|
||||||
else ans0
|
else ans0
|
||||||
}
|
}
|
||||||
|
|
@ -212,16 +270,17 @@ private[sbt] object TemplateCommandUtil {
|
||||||
|
|
||||||
private final val defaultScalaV = "3.2.2"
|
private final val defaultScalaV = "3.2.2"
|
||||||
private def scalaToolkitTemplate(): Unit = {
|
private def scalaToolkitTemplate(): Unit = {
|
||||||
val defaultScalaToolkitV = "0.1.6"
|
val defaultScalaToolkitV = "0.1.7"
|
||||||
val scalaV = ask("Scala version", defaultScalaV)
|
val scalaV = ask("Scala version", defaultScalaV)
|
||||||
val toolkitV = ask("Scala Toolkit version", defaultScalaToolkitV)
|
val toolkitV = ask("Scala Toolkit version", defaultScalaToolkitV)
|
||||||
val content = s"""
|
val content = s"""
|
||||||
val toolkit = "org.scala-lang" %% "toolkit" % "$toolkitV"
|
val toolkitV = "$toolkitV"
|
||||||
// val toolkitTest = "org.scala-lang" %% "toolkit-test" % "$toolkitV"
|
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
|
||||||
|
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV
|
||||||
|
|
||||||
ThisBuild / scalaVersion := "$scalaV"
|
ThisBuild / scalaVersion := "$scalaV"
|
||||||
libraryDependencies += toolkit
|
libraryDependencies += toolkit
|
||||||
// libraryDependencies += (toolkitTest % Test)
|
libraryDependencies += (toolkitTest % Test)
|
||||||
"""
|
"""
|
||||||
IO.write(new File("build.sbt"), content)
|
IO.write(new File("build.sbt"), content)
|
||||||
copyResource("ScalaMain.scala.txt", new File("src/main/scala/example/Main.scala"))
|
copyResource("ScalaMain.scala.txt", new File("src/main/scala/example/Main.scala"))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue