mirror of https://github.com/sbt/sbt.git
feat: Set terminal window title when running applications (#8492)
Set window title to 'sbt <command>: <org> % <name> % <version>' when running sbt run, runMain, bgRun, or bgRunMain. For server-side runs, window title is set directly. For client-side runs (sbtn), window title is passed via RunInfo protocol and set by NetworkClient. Fixes #7586
This commit is contained in:
parent
0ec500392f
commit
add43bd230
|
|
@ -568,6 +568,8 @@ lazy val protocolProj = (project in file("protocol"))
|
|||
contrabandSettings,
|
||||
mimaSettings,
|
||||
mimaBinaryIssueFilters ++= Seq(
|
||||
exclude[DirectMissingMethodProblem]("sbt.internal.worker.RunInfo.apply"),
|
||||
exclude[IncompatibleMethTypeProblem]("sbt.internal.worker.RunInfo.apply"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -741,7 +741,13 @@ class NetworkClient(
|
|||
case _ => Failure(new MessageOnlyException(s"runInfo is not specified in $params"))
|
||||
}
|
||||
|
||||
private def setWindowTitle(title: String): Unit =
|
||||
if System.console() != null && System.getenv("TERM") != null then
|
||||
Console.print(s"\u001b]0;$title\u0007")
|
||||
Console.flush()
|
||||
|
||||
private def clientSideRun(runInfo: RunInfo): Try[Unit] = {
|
||||
runInfo.windowTitle.foreach(setWindowTitle)
|
||||
def jvmRun(info: JvmRunInfo): Try[Unit] = {
|
||||
val option = ForkOptions(
|
||||
javaHome = info.javaHome.map(new File(_)),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@ import sbt.util.CacheImplicits.given
|
|||
import xsbti.FileConverter
|
||||
|
||||
object RunUtil:
|
||||
private def setWindowTitle(title: String): Unit =
|
||||
if System.console() != null && System.getenv("TERM") != null then
|
||||
scala.Console.print(s"\u001b]0;$title\u0007")
|
||||
scala.Console.flush()
|
||||
|
||||
private[sbt] def mkWindowTitle(
|
||||
command: String,
|
||||
org: String,
|
||||
name: String,
|
||||
version: String
|
||||
): String =
|
||||
s"sbt $command: $org % $name % $version"
|
||||
|
||||
/**
|
||||
* Conventional server-side run implementation.
|
||||
*/
|
||||
|
|
@ -86,10 +99,10 @@ object RunUtil:
|
|||
mainClass: String,
|
||||
cp: Classpath,
|
||||
fo: ForkOptions,
|
||||
conv: FileConverter
|
||||
conv: FileConverter,
|
||||
windowTitle: Option[String] = None
|
||||
): RunInfo =
|
||||
val strategy = fo.outputStrategy.map(_.getClass().getSimpleName().filter(_ != '$'))
|
||||
// sbtn doesn't set java.home, so we need to do the fallback here
|
||||
val javaHome =
|
||||
fo.javaHome.map(IO.toURI).orElse(sys.props.get("java.home").map(x => IO.toURI(new File(x))))
|
||||
val jvmRunInfo = JvmRunInfo(
|
||||
|
|
@ -105,7 +118,9 @@ object RunUtil:
|
|||
)
|
||||
RunInfo(
|
||||
jvm = true,
|
||||
jvmRunInfo = jvmRunInfo,
|
||||
jvmRunInfo = Some(jvmRunInfo),
|
||||
nativeRunInfo = None,
|
||||
windowTitle = windowTitle,
|
||||
)
|
||||
|
||||
def defaultRunMainTask(
|
||||
|
|
@ -125,6 +140,7 @@ object RunUtil:
|
|||
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
||||
val fo = (run / forkOptions).value
|
||||
val state = Keys.state.value
|
||||
val windowTitle = mkWindowTitle("runMain", organization.value, name.value, version.value)
|
||||
if clientRun.value && state.isNetworkCommand then
|
||||
val workingDir = service.createWorkingDirectory
|
||||
val cp = service.copyClasspath(
|
||||
|
|
@ -133,7 +149,7 @@ object RunUtil:
|
|||
workingDir,
|
||||
conv,
|
||||
)
|
||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv)
|
||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv, Some(windowTitle))
|
||||
val result = ClientJobParams(
|
||||
runInfo = info
|
||||
)
|
||||
|
|
@ -141,6 +157,7 @@ object RunUtil:
|
|||
state.notifyEvent(Serialization.clientJob, result)
|
||||
result
|
||||
else
|
||||
setWindowTitle(windowTitle)
|
||||
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||
val handle = service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state):
|
||||
(logger, workingDir) =>
|
||||
|
|
@ -181,6 +198,7 @@ object RunUtil:
|
|||
val hashClasspath = (bgRun / bgHashClasspath).value
|
||||
val fo = (run / forkOptions).value
|
||||
val state = Keys.state.value
|
||||
val windowTitle = mkWindowTitle("run", organization.value, name.value, version.value)
|
||||
if clientRun.value && state.isNetworkCommand then
|
||||
val workingDir = service.createWorkingDirectory
|
||||
val cp = service.copyClasspath(
|
||||
|
|
@ -189,7 +207,7 @@ object RunUtil:
|
|||
workingDir,
|
||||
conv,
|
||||
)
|
||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv)
|
||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv, Some(windowTitle))
|
||||
val result = ClientJobParams(
|
||||
runInfo = info
|
||||
)
|
||||
|
|
@ -197,6 +215,7 @@ object RunUtil:
|
|||
state.notifyEvent(Serialization.clientJob, result)
|
||||
result
|
||||
else
|
||||
setWindowTitle(windowTitle)
|
||||
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||
val handle = service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state):
|
||||
(logger, workingDir) =>
|
||||
|
|
@ -235,6 +254,7 @@ object RunUtil:
|
|||
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
||||
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||
val converter = fileConverter.value
|
||||
setWindowTitle(mkWindowTitle("bgRunMain", organization.value, name.value, version.value))
|
||||
service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state.value) {
|
||||
(logger, workingDir) =>
|
||||
val cp =
|
||||
|
|
@ -275,6 +295,7 @@ object RunUtil:
|
|||
val hashClasspath = (bgRun / bgHashClasspath).value
|
||||
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||
val converter = fileConverter.value
|
||||
setWindowTitle(mkWindowTitle("bgRun", organization.value, name.value, version.value))
|
||||
service.runInBackgroundWithLoader(Keys.resolvedScoped.value, state.value) {
|
||||
(logger, workingDir) =>
|
||||
val cp =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
|
||||
import hedgehog.*
|
||||
import hedgehog.runner.*
|
||||
|
||||
object RunUtilTest extends Properties:
|
||||
override def tests: List[Test] = List(
|
||||
property("mkWindowTitle formats title correctly", testMkWindowTitle),
|
||||
property("mkWindowTitle handles empty strings", testMkWindowTitleEmpty),
|
||||
)
|
||||
|
||||
def testMkWindowTitle: Property =
|
||||
for
|
||||
command <- Gen.element1("run", "runMain", "bgRun", "bgRunMain").forAll
|
||||
org <- Gen.string(Gen.alpha, Range.linear(1, 20)).forAll
|
||||
name <- Gen.string(Gen.alpha, Range.linear(1, 20)).forAll
|
||||
version <- Gen.string(Gen.alphaNum, Range.linear(1, 10)).forAll
|
||||
yield
|
||||
val result = RunUtil.mkWindowTitle(command, org, name, version)
|
||||
val expected = s"sbt $command: $org % $name % $version"
|
||||
Result.assert(result == expected)
|
||||
|
||||
def testMkWindowTitleEmpty: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val result = RunUtil.mkWindowTitle("run", "", "", "")
|
||||
Result.assert(result == "sbt run: % % ")
|
||||
end RunUtilTest
|
||||
|
|
@ -7,22 +7,23 @@ package sbt.internal.worker
|
|||
final class RunInfo private (
|
||||
val jvm: Boolean,
|
||||
val jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo],
|
||||
val nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo]) extends Serializable {
|
||||
val nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo],
|
||||
val windowTitle: Option[String]) extends Serializable {
|
||||
|
||||
private def this(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo]) = this(jvm, jvmRunInfo, None)
|
||||
private def this(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo], windowTitle: Option[String]) = this(jvm, jvmRunInfo, None, windowTitle)
|
||||
|
||||
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
|
||||
case x: RunInfo => (this.jvm == x.jvm) && (this.jvmRunInfo == x.jvmRunInfo) && (this.nativeRunInfo == x.nativeRunInfo)
|
||||
case x: RunInfo => (this.jvm == x.jvm) && (this.jvmRunInfo == x.jvmRunInfo) && (this.nativeRunInfo == x.nativeRunInfo) && (this.windowTitle == x.windowTitle)
|
||||
case _ => false
|
||||
})
|
||||
override def hashCode: Int = {
|
||||
37 * (37 * (37 * (37 * (17 + "sbt.internal.worker.RunInfo".##) + jvm.##) + jvmRunInfo.##) + nativeRunInfo.##)
|
||||
37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.worker.RunInfo".##) + jvm.##) + jvmRunInfo.##) + nativeRunInfo.##) + windowTitle.##)
|
||||
}
|
||||
override def toString: String = {
|
||||
"RunInfo(" + jvm + ", " + jvmRunInfo + ", " + nativeRunInfo + ")"
|
||||
"RunInfo(" + jvm + ", " + jvmRunInfo + ", " + nativeRunInfo + ", " + windowTitle + ")"
|
||||
}
|
||||
private def copy(jvm: Boolean = jvm, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo] = jvmRunInfo, nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo] = nativeRunInfo): RunInfo = {
|
||||
new RunInfo(jvm, jvmRunInfo, nativeRunInfo)
|
||||
private def copy(jvm: Boolean = jvm, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo] = jvmRunInfo, nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo] = nativeRunInfo, windowTitle: Option[String] = windowTitle): RunInfo = {
|
||||
new RunInfo(jvm, jvmRunInfo, nativeRunInfo, windowTitle)
|
||||
}
|
||||
def withJvm(jvm: Boolean): RunInfo = {
|
||||
copy(jvm = jvm)
|
||||
|
|
@ -39,11 +40,17 @@ final class RunInfo private (
|
|||
def withNativeRunInfo(nativeRunInfo: sbt.internal.worker.NativeRunInfo): RunInfo = {
|
||||
copy(nativeRunInfo = Option(nativeRunInfo))
|
||||
}
|
||||
def withWindowTitle(windowTitle: Option[String]): RunInfo = {
|
||||
copy(windowTitle = windowTitle)
|
||||
}
|
||||
def withWindowTitle(windowTitle: String): RunInfo = {
|
||||
copy(windowTitle = Option(windowTitle))
|
||||
}
|
||||
}
|
||||
object RunInfo {
|
||||
|
||||
def apply(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo]): RunInfo = new RunInfo(jvm, jvmRunInfo)
|
||||
def apply(jvm: Boolean, jvmRunInfo: sbt.internal.worker.JvmRunInfo): RunInfo = new RunInfo(jvm, Option(jvmRunInfo))
|
||||
def apply(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo], nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo]): RunInfo = new RunInfo(jvm, jvmRunInfo, nativeRunInfo)
|
||||
def apply(jvm: Boolean, jvmRunInfo: sbt.internal.worker.JvmRunInfo, nativeRunInfo: sbt.internal.worker.NativeRunInfo): RunInfo = new RunInfo(jvm, Option(jvmRunInfo), Option(nativeRunInfo))
|
||||
def apply(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo], windowTitle: Option[String]): RunInfo = new RunInfo(jvm, jvmRunInfo, windowTitle)
|
||||
def apply(jvm: Boolean, jvmRunInfo: sbt.internal.worker.JvmRunInfo, windowTitle: String): RunInfo = new RunInfo(jvm, Option(jvmRunInfo), Option(windowTitle))
|
||||
def apply(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo], nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo], windowTitle: Option[String]): RunInfo = new RunInfo(jvm, jvmRunInfo, nativeRunInfo, windowTitle)
|
||||
def apply(jvm: Boolean, jvmRunInfo: sbt.internal.worker.JvmRunInfo, nativeRunInfo: sbt.internal.worker.NativeRunInfo, windowTitle: String): RunInfo = new RunInfo(jvm, Option(jvmRunInfo), Option(nativeRunInfo), Option(windowTitle))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ given RunInfoFormat: JsonFormat[sbt.internal.worker.RunInfo] = new JsonFormat[sb
|
|||
val jvm = unbuilder.readField[Boolean]("jvm")
|
||||
val jvmRunInfo = unbuilder.readField[Option[sbt.internal.worker.JvmRunInfo]]("jvmRunInfo")
|
||||
val nativeRunInfo = unbuilder.readField[Option[sbt.internal.worker.NativeRunInfo]]("nativeRunInfo")
|
||||
val windowTitle = unbuilder.readField[Option[String]]("windowTitle")
|
||||
unbuilder.endObject()
|
||||
sbt.internal.worker.RunInfo(jvm, jvmRunInfo, nativeRunInfo)
|
||||
sbt.internal.worker.RunInfo(jvm, jvmRunInfo, nativeRunInfo, windowTitle)
|
||||
case None =>
|
||||
deserializationError("Expected JsObject but found None")
|
||||
}
|
||||
|
|
@ -25,6 +26,7 @@ given RunInfoFormat: JsonFormat[sbt.internal.worker.RunInfo] = new JsonFormat[sb
|
|||
builder.addField("jvm", obj.jvm)
|
||||
builder.addField("jvmRunInfo", obj.jvmRunInfo)
|
||||
builder.addField("nativeRunInfo", obj.nativeRunInfo)
|
||||
builder.addField("windowTitle", obj.windowTitle)
|
||||
builder.endObject()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type RunInfo {
|
|||
jvm: Boolean!
|
||||
jvmRunInfo: sbt.internal.worker.JvmRunInfo,
|
||||
nativeRunInfo: sbt.internal.worker.NativeRunInfo @since("0.1.0"),
|
||||
windowTitle: String,
|
||||
}
|
||||
|
||||
## Client-side job support.
|
||||
|
|
|
|||
Loading…
Reference in New Issue