6

Using the terminal package, my code basically looks like this:

main :: IO ()
main = withTerminal $ runTerminalT $ do
    eraseInDisplay EraseAll
    hideCursor
    setAutoWrap False

    forever $ do
        calculate
        updateScreen

If the user exits this program with Ctrl-C, the terminal settings (in this concrete case, the hidden cursor) remain. I am looking for a lightweight way to restore the terminal settings as they were before running the program. By lightweight, I mean something that I can just wrap around the whole runTerminalT shebang, instead of having to manually call checkInterrupt at various parts of my code etc.

Cactus
  • 27,075
  • 9
  • 69
  • 149
  • Bypassing the Haskell layer, dropping closer to the OS layer... At least in Unix/Posixy system, I think you need to have the Ctrl-C handler (catching SIGINT) and perform the required terminal setting restorations therein. Thinking purely on the POSIX level, this is not much code, but I am not able to recommend any fancy Haskell library to use or an appropriate monady way to approach this. – FooF Feb 27 '21 at 07:07
  • 1
    Sometimes it's best not to do anything fancy. I think you can just [`installHandler`](https://hackage.haskell.org/package/unix-2.7.2.2/docs/System-Posix-Signals.html#v:installHandler) once and be done with it? – luqui Feb 27 '21 at 08:27

2 Answers2

3

There's no need to mess around with POSIX signals to do something like Daniel Wagner's answer. Control.Exception has everything you need.

import Control.Exception (handleJust, AsyncException (UserInterrupt))
import Control.Monad (guard)
import System.Exit (exitWith, ExitCode (ExitFailure))

main = withTerminal $ \term ->
  handleJust
         (guard . (== UserInterrupt))
         (\ ~() -> do
           resetTerminalOrWhatever term
           exitWith (ExitFailure 1)) $
         ...

You may actually want something broader than that, since you probably want to reset the terminal on any abnormal termination:

main = withTerminal $ \term -> ... `onException` resetTerminalOrWhatever term

Note that this second version will be activated by a call to exitWith as well. If that's not desired, then you could catch/rethrow manually and refrain from resetting the terminal on ExitSuccess.


Note that withTerminal can actually operate within a more general context than just IO, but it always requires a MonadMask constraint, which should be sufficient for this general approach.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
1

I haven't tested to know for sure whether this works out right, but I guess you would just install a signal handler that does your cleanup, then exits just like the default one would.

import System.Exit
import System.Posix.Signals

main = do
    installHandler sigINT Nothing . Catch $ do
        resetTerminalOrWhatever
        exitWith (ExitFailure 1)
    withTerminal $ {- ... -}

The signals module is available from the unix package.

If you're paranoid, you could check what the old handler was and try to incorporate it into yours:

main = do
    mfix $ \oldHandler ->
        installHandler sigINT Nothing . CatchInfo $ \si -> case oldHandler of
            Default -> reset >> exit
            Ignore -> reset >> exit
            Catch act -> reset >> act
            CatchOnce act -> reset >> act >> exit
            CatchInfo f -> reset >> f si
            CatchInfoOnce f -> reset >> f si >> exit
    withTerminal $ {- ... -}

...or something like that.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • Why would you need to install a signal handler? `SIGINT` raises a Haskell exception; just `catch` it. – dfeuer Feb 28 '21 at 08:14
  • @dfeuer Sounds like you've got a competing answer up your sleeve! I encourage you to post it. – Daniel Wagner Feb 28 '21 at 18:54
  • 1
    Posted. I had refrained from posting anything because I don't know anything about actually resetting the terminal, but I believe my approach to `SIGINT` is a lot simpler (and more portable) than yours. – dfeuer Feb 28 '21 at 23:16
  • I think you need to install the handler under `withTerminal` where you have access to the terminal to reset it. – dfeuer Mar 01 '21 at 13:04