diff --git a/build.sbt b/build.sbt index a88375b06..06d065e38 100644 --- a/build.sbt +++ b/build.sbt @@ -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"), ) ) diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 615df64e3..1d17e59a3 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -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(_)), diff --git a/main/src/main/scala/sbt/internal/RunUtil.scala b/main/src/main/scala/sbt/internal/RunUtil.scala index c45e04697..02a0fd292 100644 --- a/main/src/main/scala/sbt/internal/RunUtil.scala +++ b/main/src/main/scala/sbt/internal/RunUtil.scala @@ -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 = diff --git a/main/src/test/scala/sbt/internal/RunUtilTest.scala b/main/src/test/scala/sbt/internal/RunUtilTest.scala new file mode 100644 index 000000000..5309c1410 --- /dev/null +++ b/main/src/test/scala/sbt/internal/RunUtilTest.scala @@ -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 diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala index b6b614ac5..742324d85 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala @@ -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)) } diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala index d406e035a..696d20ff0 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala @@ -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() } } diff --git a/protocol/src/main/contraband/worker.contra b/protocol/src/main/contraband/worker.contra index 45a68eb74..261299438 100644 --- a/protocol/src/main/contraband/worker.contra +++ b/protocol/src/main/contraband/worker.contra @@ -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.