Skip to content

Commit 3c3de7b

Browse files
committed
.
1 parent e73ff2c commit 3c3de7b

File tree

2 files changed

+56
-19
lines changed

2 files changed

+56
-19
lines changed

repl/src/dotty/tools/repl/JLineTerminal.scala

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ import org.jline.reader.*
1515
import org.jline.reader.impl.LineReaderImpl
1616
import org.jline.reader.impl.history.DefaultHistory
1717
import org.jline.terminal.TerminalBuilder
18+
import org.jline.terminal.Attributes
19+
import org.jline.terminal.Attributes.ControlChar
1820
import org.jline.utils.AttributedString
1921

2022
class JLineTerminal extends java.io.Closeable {
2123
// import java.util.logging.{Logger, Level}
2224
// Logger.getLogger("org.jline").setLevel(Level.FINEST)
2325

2426
private val terminal =
25-
var builder = TerminalBuilder.builder()
27+
val builder = TerminalBuilder.builder()
2628
if System.getenv("TERM") == "dumb" then
2729
// Force dumb terminal if `TERM` is `"dumb"`.
2830
// Note: the default value for the `dumb` option is `null`, which allows
@@ -31,6 +33,18 @@ class JLineTerminal extends java.io.Closeable {
3133
// This option is used at https://github.com/jline/jline3/blob/894b5e72cde28a551079402add4caea7f5527806/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java#L528.
3234
builder.dumb(true)
3335
builder.build()
36+
37+
// Save original attributes before entering raw mode
38+
private val originalAttributes = terminal.getAttributes
39+
40+
// Disable VINTR so Ctrl-C is not converted to SIGINT by the tty driver, then enter raw mode
41+
// This disables special character processing so Ctrl-C is passed through as 0x03
42+
val noIntr = new Attributes(originalAttributes)
43+
noIntr.setControlChar(ControlChar.VINTR, 0)
44+
terminal.setAttributes(noIntr)
45+
terminal.enterRawMode()
46+
47+
3448
private val history = new DefaultHistory
3549

3650
private def magenta(str: String)(using Context) =
@@ -78,14 +92,43 @@ class JLineTerminal extends java.io.Closeable {
7892
.option(DISABLE_EVENT_EXPANSION, true) // don't process escape sequences in input
7993
.build()
8094

95+
lineReader.getKeyMaps.get(LineReader.MAIN).bind(
96+
new Widget { override def apply(): Boolean = throw new UserInterruptException("") },
97+
"\u0003"
98+
)
8199
lineReader.readLine(prompt)
82100
}
83101

84-
def close(): Unit = terminal.close()
102+
def close(): Unit =
103+
try terminal.setAttributes(originalAttributes)
104+
finally terminal.close()
105+
106+
/** Execute a block while monitoring for Ctrl-C keypresses.
107+
* Calls the handler when Ctrl-C is detected during block execution.
108+
*/
109+
def withMonitoringCtrlC[T](handler: () => Unit)(block: => T): T = {
110+
@volatile var monitoring = true
111+
val terminalReader = terminal.reader()
85112

86-
/** Register a signal handler and return the previous handler */
87-
def handle(signal: org.jline.terminal.Terminal.Signal, handler: org.jline.terminal.Terminal.SignalHandler): org.jline.terminal.Terminal.SignalHandler =
88-
terminal.handle(signal, handler)
113+
val monitorThread = new Thread(() => {
114+
while (monitoring) {
115+
val ch =
116+
try terminalReader.read(1) // timeout after 1ms so the loop gets a chance to check `monitoring`
117+
catch { case _: Exception => -1 } // Ignore all read errors, just continue
118+
119+
if (ch == 3 /* Ctrl-C is ASCII 0x03 */ && monitoring) handler()
120+
}
121+
}, "REPL-CtrlC-Monitor")
122+
monitorThread.setDaemon(true)
123+
monitorThread.start()
124+
125+
try block
126+
finally {
127+
monitoring = false
128+
Thread.interrupted() // clear any interrupted flag so the `join` below doesn't explode
129+
monitorThread.join()
130+
}
131+
}
89132

90133
/** Provide syntax highlighting */
91134
private class Highlighter(using Context) extends reader.Highlighter {

repl/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -225,37 +225,31 @@ class ReplDriver(settings: Array[String],
225225

226226
val res = readLine()
227227
if (res == Quit) state
228-
// Ctrl-C pressed at prompt - just continue with same state (line is cleared by JLine)
229228
else if (res == SigKill) loop(using state)()
230229
else {
231-
// Set up interrupt handler for command execution
230+
// Set up Ctrl-C monitoring for command execution
232231
var firstCtrlCEntered = false
233232
val thread = Thread.currentThread()
234233

235234
// Clear the stop flag before executing new code
236235
ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), false)
237236

238-
val previousSignalHandler = terminal.handle(
239-
org.jline.terminal.Terminal.Signal.INT,
240-
(sig: org.jline.terminal.Terminal.Signal) => {
237+
val newState = terminal.withMonitoringCtrlC(
238+
handler = () =>
241239
if (!firstCtrlCEntered) {
242240
firstCtrlCEntered = true
243241
// Set the stop flag to trigger throwIfReplStopped() in instrumented code
244242
ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), true)
245-
// Also interrupt the thread as a fallback for non-instrumented code
243+
// Also interrupt the thread as a fallback for non-instrumented code, e.g. IO/sleeps
246244
thread.interrupt()
247-
out.println("\nAttempting to interrupt running thread with `Thread.interrupt`")
245+
out.println("\nAttempting to interrupt running REPL command")
248246
} else {
249247
out.println("\nTerminating REPL Process...")
250248
System.exit(130) // Standard exit code for SIGINT
251249
}
252-
}
253-
)
254-
255-
val newState =
256-
try interpret(res)
257-
// Restore previous handler
258-
finally terminal.handle(org.jline.terminal.Terminal.Signal.INT, previousSignalHandler)
250+
) {
251+
interpret(res)
252+
}
259253

260254
loop(using newState)()
261255
}

0 commit comments

Comments
 (0)