Search code examples
haskellinterruptread-eval-print-loopkeyboardinterruptcontrol-c

Using CTRL-C to discard current line and start the next line in a REPL


I'm writing a REPL in Haskell. The basic idea looks like this:

repl :: IO ()
repl = do putStr ">> "
          hFlush stdout
          input <- getLine
          ...
          repl

I'd like to mimic some GHCi behaviour that when CTRL-C is pressed, the REPL discards the current line and starts a new empty line for user input. It has come to my mind that SIGINT raises UserInterrupt of AsyncException, which can be handled by catch. Therefore some modifications on the input line:

repl :: IO ()
repl = do putStr ">> "
          hFlush stdout
          input <- getLine `catch` handleCC
          repl

handleCC :: AsyncException -> IO String
handleCC _ = do putStr "\n>> "
                hFlush stdout
                getLine `catch` handleCC

Within handleCC, AsyncException will be handled once more by recursion, so surely I can interrupt the REPL infinite times, right...?

Of course I can't; a second CTRL-C still terminates the REPL. But why?

>> something^C
>> another^C
[Process exited 130]

Solution

  • After digging deeper, I believe I can conclude that catch does not properly handle asynchronous exceptions, neither try or similar. mask seems to be a potential solution but I found its usage a bit of sophisticated even with the documentation.

    This answer provides a working solution by installing a persistent signal handler using the POSIX API. Integrated with my code:

    import           Control.Monad        (forever)
    import           System.IO            (hFlush, stdout)
    import           System.Posix.Signals (Handler (Catch), installHandler, sigINT)
    
    repl = do installHandler sigINT (Catch $ putStr "\n>> ") Nothing
              forever $ do putStr ">> "
                           hFlush stdout
                           input <- getLine
                           ...