@@ -15,14 +15,16 @@ import org.jline.reader.*
1515import org .jline .reader .impl .LineReaderImpl
1616import org .jline .reader .impl .history .DefaultHistory
1717import org .jline .terminal .TerminalBuilder
18+ import org .jline .terminal .Attributes
19+ import org .jline .terminal .Attributes .ControlChar
1820import org .jline .utils .AttributedString
1921
2022class 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 {
0 commit comments