Skip to content

Commit ad57ee0

Browse files
Merge pull request #1614 from alexarchambault/better-revolver-input
Better revolver output
2 parents bdfb303 + 263f651 commit ad57ee0

File tree

2 files changed

+77
-16
lines changed

2 files changed

+77
-16
lines changed

modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scala.cli.commands
22

3+
import scala.annotation.tailrec
4+
35
object WatchUtil {
46

57
lazy val isDevMode: Boolean =
@@ -9,18 +11,47 @@ object WatchUtil {
911
def waitMessage(message: String): String = {
1012
// Both short cuts actually always work, but Ctrl+C also exits mill in mill watch mode.
1113
val shortCut = if (isDevMode) "Ctrl+D" else "Ctrl+C"
12-
val gray = "\u001b[90m"
13-
val reset = Console.RESET
14-
s"$gray$message, press $shortCut to exit, or press Enter to re-run.$reset"
14+
gray(s"$message, press $shortCut to exit, or press Enter to re-run.")
15+
}
16+
17+
private def gray(message: String): String = {
18+
val gray = "\u001b[90m"
19+
val reset = Console.RESET
20+
s"$gray$message$reset"
1521
}
1622

1723
def printWatchMessage(): Unit =
1824
System.err.println(waitMessage("Watching sources"))
1925

20-
def waitForCtrlC(onPressEnter: () => Unit = () => ()): Unit = {
26+
def printWatchWhileRunningMessage(): Unit =
27+
System.err.println(gray("Watching sources while your program is running."))
28+
29+
def waitForCtrlC(
30+
onPressEnter: () => Unit = () => (),
31+
shouldReadInput: () => Boolean = () => true
32+
): Unit = synchronized {
33+
34+
@tailrec
35+
def readNextChar(): Int =
36+
if (shouldReadInput())
37+
try System.in.read()
38+
catch {
39+
case _: InterruptedException =>
40+
// Actually never called, as System.in.read isn't interruptible…
41+
// That means we sometimes read input when we shouldn't.
42+
readNextChar()
43+
}
44+
else {
45+
try wait()
46+
catch {
47+
case _: InterruptedException =>
48+
}
49+
readNextChar()
50+
}
51+
2152
var readKey = -1
2253
while ({
23-
readKey = System.in.read()
54+
readKey = readNextChar()
2455
readKey != -1
2556
})
2657
if (readKey == '\n')

modules/cli/src/main/scala/scala/cli/commands/run/Run.scala

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,17 +149,22 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
149149
val onExitProcess = process.onExit().thenApply { p1 =>
150150
val retCode = p1.exitValue()
151151
onExitOpt.foreach(_())
152-
if (retCode != 0)
153-
if (allowTerminate)
152+
(retCode, allowTerminate) match {
153+
case (0, true) =>
154+
case (0, false) =>
155+
val gray = "\u001b[90m"
156+
val reset = Console.RESET
157+
System.err.println(s"${gray}Program exited with return code $retCode.$reset")
158+
case (_, true) =>
154159
sys.exit(retCode)
155-
else {
160+
case (_, false) =>
156161
val red = Console.RED
157162
val lightRed = "\u001b[91m"
158163
val reset = Console.RESET
159164
System.err.println(
160165
s"${red}Program exited with return code $lightRed$retCode$red.$reset"
161166
)
162-
}
167+
}
163168
}
164169

165170
Some((process, onExitProcess))
@@ -191,7 +196,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
191196
)
192197

193198
if (options.sharedRun.watch.watchMode) {
194-
var processOpt = Option.empty[(Process, CompletableFuture[_])]
199+
var processOpt = Option.empty[(Process, CompletableFuture[_])]
200+
var shouldReadInput = false
201+
var mainThreadOpt = Option.empty[Thread]
195202
val watcher = Build.watch(
196203
inputs,
197204
initialBuildOptions,
@@ -202,16 +209,23 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
202209
buildTests = false,
203210
partial = None,
204211
actionableDiagnostics = actionableDiagnostics,
205-
postAction = () => WatchUtil.printWatchMessage()
212+
postAction = () =>
213+
if (processOpt.exists(_._1.isAlive()))
214+
WatchUtil.printWatchWhileRunningMessage()
215+
else
216+
WatchUtil.printWatchMessage()
206217
) { res =>
207218
for ((process, onExitProcess) <- processOpt) {
208219
onExitProcess.cancel(true)
209220
ProcUtil.interruptProcess(process, logger)
210221
}
211222
res.orReport(logger).map(_.main).foreach {
212223
case s: Build.Successful =>
213-
for ((proc, _) <- processOpt) // If the process doesn't exit, send SIGKILL
214-
if (proc.isAlive) ProcUtil.forceKillProcess(proc, logger)
224+
for ((proc, _) <- processOpt if proc.isAlive)
225+
// If the process doesn't exit, send SIGKILL
226+
ProcUtil.forceKillProcess(proc, logger)
227+
shouldReadInput = false
228+
mainThreadOpt.foreach(_.interrupt())
215229
val maybeProcess = maybeRun(
216230
s,
217231
allowTerminate = false,
@@ -221,18 +235,34 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
221235
)
222236
.orReport(logger)
223237
.flatten
238+
.map {
239+
case (proc, onExit) =>
240+
if (options.sharedRun.watch.restart)
241+
onExit.thenApply { _ =>
242+
shouldReadInput = true
243+
mainThreadOpt.foreach(_.interrupt())
244+
}
245+
(proc, onExit)
246+
}
224247
s.copyOutput(options.shared)
225248
if (options.sharedRun.watch.restart)
226249
processOpt = maybeProcess
227-
else
250+
else {
228251
for ((proc, onExit) <- maybeProcess)
229252
ProcUtil.waitForProcess(proc, onExit)
253+
shouldReadInput = true
254+
mainThreadOpt.foreach(_.interrupt())
255+
}
230256
case _: Build.Failed =>
231257
System.err.println("Compilation failed")
232258
}
233259
}
234-
try WatchUtil.waitForCtrlC(() => watcher.schedule())
235-
finally watcher.dispose()
260+
mainThreadOpt = Some(Thread.currentThread())
261+
try WatchUtil.waitForCtrlC(() => watcher.schedule(), () => shouldReadInput)
262+
finally {
263+
mainThreadOpt = None
264+
watcher.dispose()
265+
}
236266
}
237267
else {
238268
val builds =

0 commit comments

Comments
 (0)