@@ -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,16 @@ 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 any modifications
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+ val noIntr = new Attributes (originalAttributes)
42+ noIntr.setControlChar(ControlChar .VINTR , 0 )
43+ terminal.setAttributes(noIntr)
44+ terminal.enterRawMode()
45+
3446 private val history = new DefaultHistory
3547
3648 private def magenta (str : String )(using Context ) =
@@ -78,14 +90,43 @@ class JLineTerminal extends java.io.Closeable {
7890 .option(DISABLE_EVENT_EXPANSION , true ) // don't process escape sequences in input
7991 .build()
8092
93+ lineReader.getKeyMaps.get(LineReader .MAIN ).bind(
94+ new Widget { override def apply (): Boolean = throw new UserInterruptException (" " ) },
95+ " \u0003 "
96+ )
8197 lineReader.readLine(prompt)
8298 }
8399
84- def close (): Unit = terminal.close()
100+ def close (): Unit =
101+ try terminal.setAttributes(originalAttributes)
102+ finally terminal.close()
103+
104+ /** Execute a block while monitoring for Ctrl-C keypresses.
105+ * Calls the handler when Ctrl-C is detected during block execution.
106+ */
107+ def withMonitoringCtrlC [T ](handler : () => Unit )(block : => T ): T = {
108+ @ volatile var monitoring = true
109+ val terminalReader = terminal.reader()
110+
111+ val monitorThread = new Thread (() => {
112+ while (monitoring) {
113+ val ch =
114+ try terminalReader.peek(1 ) // timeout after 1ms so the loop gets a chance to check `monitoring`
115+ catch { case _ : Exception => - 1 } // Ignore all read errors, just continue
85116
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)
117+ if (ch == 3 /* Ctrl-C is ASCII 0x03 */ && monitoring) handler()
118+ }
119+ }, " REPL-CtrlC-Monitor" )
120+ monitorThread.setDaemon(true )
121+ monitorThread.start()
122+
123+ try block
124+ finally {
125+ monitoring = false
126+ Thread .interrupted() // clear any interrupted flag so the `join` below doesn't explode
127+ monitorThread.join()
128+ }
129+ }
89130
90131 /** Provide syntax highlighting */
91132 private class Highlighter (using Context ) extends reader.Highlighter {
0 commit comments