packages feed

terminal-0.1.0.0: platform/posix/src/System/Terminal/Platform.hsc

{-# LANGUAGE LambdaCase #-}
module System.Terminal.Platform
  ( withTerminal
  ) where

import           Control.Concurrent
import qualified Control.Concurrent.Async      as A
import           Control.Concurrent.STM.TChan
import           Control.Concurrent.STM.TVar
import           Control.Concurrent.STM.TMVar
import qualified Control.Exception             as E
import           Control.Monad                 (forever, when, unless, void)
import           Control.Monad.Catch hiding    (handle)
import           Control.Monad.IO.Class
import           Control.Monad.STM
import           Data.Bits
import qualified Data.ByteString.Char8         as BS8
import           Data.Maybe
import qualified Data.Text                     as Text
import qualified Data.Text.IO                  as Text
import           Foreign.C.Types
import           Foreign.Marshal.Alloc
import           Foreign.Ptr
import           Foreign.Storable
import           System.Environment
import qualified System.IO                     as IO
import qualified GHC.Conc                      as Conc
import qualified Data.Dynamic                  as Dyn
import           System.Posix.Types            (Fd(..))

import           Control.Monad.Terminal.Terminal
import           Control.Monad.Terminal.Input

#include "Rts.h"
#include "hs_terminal.h"

withTerminal :: (MonadIO m, MonadMask m) => (Terminal -> m a) -> m a
withTerminal action = do
  term        <- BS8.pack . fromMaybe "xterm" <$> liftIO (lookupEnv "TERM")
  mainThread  <- liftIO myThreadId
  interrupt   <- liftIO (newTVarIO False)
  output      <- liftIO newEmptyTMVarIO
  outputFlush <- liftIO newEmptyTMVarIO
  events      <- liftIO newTChanIO
  screenSize  <- liftIO (newTVarIO =<< getWindowSize)
  withTermiosSettings $ \termios->
    withResizeHandler (handleResize screenSize events) $
    withInputProcessing mainThread termios interrupt events $ 
    withOutputProcessing output outputFlush $ action $ Terminal {
        termType           = term
      , termInput          = readTChan events
      , termInterrupt      = swapTVar interrupt False >>= check
      , termOutput         = putTMVar output
      , termFlush          = putTMVar outputFlush ()
      , termScreenSize     = readTVar screenSize
      , termSpecialChars   = \case
          '\n'   -> Just $ KeyEvent EnterKey mempty
          '\t'   -> Just $ KeyEvent TabKey mempty
          '\SP'  -> Just $ KeyEvent SpaceKey mempty
          '\b'   -> Just $ KeyEvent (if termiosVERASE termios == '\b'   then BackspaceKey else DeleteKey) mempty
          '\DEL' -> Just $ KeyEvent (if termiosVERASE termios == '\DEL' then DeleteKey else BackspaceKey) mempty
          _      -> Nothing
      }
  where
    handleResize screenSize events = do
      ws <- getWindowSize
      atomically $ do
        writeTVar screenSize ws
        writeTChan events (WindowEvent $ WindowSizeChanged ws)

withTermiosSettings :: (MonadIO m, MonadMask m) => (Termios -> m a) -> m a
withTermiosSettings fma = bracket before after between
  where
    before  = liftIO $ do
      termios <- getTermios
      let termios' = termios { termiosISIG = False, termiosICANON = False, termiosECHO = False }
      setTermios termios'
      pure termios
    after   = liftIO . setTermios
    between = fma

withResizeHandler :: (MonadIO m, MonadMask m) => IO () -> m a -> m a
withResizeHandler handler = bracket installHandler restoreHandler . const
  where
    installHandler = liftIO $ do
      Conc.ensureIOManagerIsRunning
      oldHandler <- Conc.setHandler (#const SIGWINCH) (Just (const handler, Dyn.toDyn handler))
      oldAction  <- stg_sig_install (#const SIGWINCH) (#const STG_SIG_HAN) nullPtr
      pure (oldHandler,oldAction)
    restoreHandler (oldHandler,oldAction) = liftIO $ do
      void $ Conc.setHandler (#const SIGWINCH) oldHandler
      void $ stg_sig_install (#const SIGWINCH) oldAction nullPtr
      pure ()

withOutputProcessing :: (MonadIO m, MonadMask m) => TMVar Text.Text -> TMVar () -> m a -> m a
withOutputProcessing output outputFlush = bracket
  ( liftIO $ A.async run )
  ( liftIO . A.cancel ) . const
  where
    run = forever $ atomically ((Just <$> takeTMVar output) `orElse` (takeTMVar outputFlush >> pure Nothing)) >>= \case
        Nothing -> IO.hFlush IO.stdout
        Just t  -> Text.hPutStr IO.stdout t

withInputProcessing :: (MonadIO m, MonadMask m) => ThreadId -> Termios -> TVar Bool -> TChan Event -> m a -> m a
withInputProcessing mainThread termios interrupt events = bracket
  ( liftIO $ A.async run )
  ( liftIO . A.cancel ) . const
  where
    run :: IO ()
    run = forever $ do 
        IO.hGetChar handle >>= \case
          c | c == termiosVINTR  termios -> handleInterrupt c
            | c == termiosVERASE termios -> atomically $ writeChar c >> writeKey BackspaceKey
            | otherwise                  -> atomically $ writeChar c
        writeFillCharacterAfterTimeout

    handle     :: IO.Handle
    handle      = IO.stdin
    writeEvent :: Event -> STM ()
    writeEvent  = writeTChan events
    writeKey   :: Key -> STM ()
    writeKey k  = writeTChan events (KeyEvent k mempty)
    writeChar  :: Char -> STM ()
    writeChar c = writeTChan events (KeyEvent (CharKey c) mempty)
    -- This function is responsible for passing interrupt events and
    -- eventually throwing an exception to the main thread in case it
    -- detects that the main thread is not serving its duty to process
    -- interrupt events. It does this by setting a flag each time an interrupt
    -- occurs - if the flag is still set when a new interrupt occurs, it assumes
    -- the main thread is not responsive.
    handleInterrupt  :: Char -> IO ()
    handleInterrupt c =  do
      unhandledInterrupt <- liftIO (atomically $ writeChar c >> writeEvent InterruptEvent >> swapTVar interrupt True)
      when unhandledInterrupt (E.throwTo mainThread E.UserInterrupt)
    -- This function first evaluates whether more input is immediately available.
    -- If this is the case it just returns. Otherwise it registers interest in
    -- the file descriptor and waits for either input becoming available or a timeout
    -- to occur. When the timeout triggers, a NUL character is appended to the
    -- event stream to enable subsequent decoders to unambigously decode all
    -- cases without the need to take timing into consideration anymore.
    writeFillCharacterAfterTimeout :: IO ()
    writeFillCharacterAfterTimeout = do
      ready <- IO.hReady handle
      unless ready $ bracket (threadWaitReadSTM (Fd 0)) snd $ \(inputAvailable,_)-> do
        timeout <- registerDelay timeoutMicroseconds >>= \t-> pure (readTVar t >>= check)
        atomically $ inputAvailable `orElse` (timeout >> writeChar '\NUL')
    -- The timeout duration has been choosen as a tradeoff between correctness
    -- (actual transmission or scheduling delays shall not be misinterpreted) and
    -- responsiveness for a human user (50 ms are barely noticable, but 1000 ms are).
    -- I.e. when the user presses the ESC key (as vim users sometimes do ;-)
    -- it shall be reflected in the application behavior quite instantly and
    -- certainly _before_ the user presses the next key (thereby assuming that the
    -- user is not able to type more than 20 characters per second).
    -- For escape sequences it shall also be taken into consideration that they are
    -- usually transmitted and received as chunks. Only on very rare occasions (buffer
    -- boundaries) it might happen that they are split right after the sequence
    -- introducer. In a modern environment with virtual terminals there is good
    -- reason to consider this more unlikely than a user that types so fast
    -- that his input might be misinterpreted as an escape sequence.
    timeoutMicroseconds :: Int
    timeoutMicroseconds  = 50000

getWindowSize :: IO (Int, Int)
getWindowSize =
  alloca $ \ptr->
    unsafeIOCtl 0 (#const TIOCGWINSZ) ptr >>= \case
      0 -> peek ptr >>= \ws-> pure (fromIntegral $ wsRow ws, fromIntegral $ wsCol ws)
      _ -> undefined

getTermios :: IO Termios
getTermios = 
  alloca $ \ptr->
    unsafeGetTermios 0 ptr >>= \case
      0 -> peek ptr
      _ -> undefined

setTermios :: Termios -> IO ()
setTermios t =
  alloca $ \ptr->
    unsafeGetTermios 0 ptr >>= \case
      0 -> do 
        poke ptr t
        unsafeSetTermios 0 (#const TCSANOW) ptr >>= \case
          0 -> pure ()
          _ -> undefined
      _ -> undefined

data Winsize
  = Winsize
  { wsRow :: !CUShort
  , wsCol :: !CUShort
  } deriving (Eq, Ord, Show)

data Termios
  = Termios
  { termiosVEOF   :: !Char
  , termiosVERASE :: !Char
  , termiosVINTR  :: !Char
  , termiosVKILL  :: !Char
  , termiosVQUIT  :: !Char
  , termiosISIG   :: !Bool
  , termiosICANON :: !Bool
  , termiosECHO   :: !Bool
  } deriving (Eq, Ord, Show)

instance Storable Winsize where
  sizeOf    _ = (#size struct winsize)
  alignment _ = (#alignment struct winsize)
  peek ptr    = Winsize
    <$> (#peek struct winsize, ws_row) ptr
    <*> (#peek struct winsize, ws_col) ptr
  poke ptr ws = do
    (#poke struct winsize, ws_row) ptr (wsRow ws)
    (#poke struct winsize, ws_col) ptr (wsCol ws)

instance Storable Termios where
  sizeOf    _ = (#size struct termios)
  alignment _ = (#alignment struct termios)
  peek ptr    = do
    lflag <- peekLFlag
    Termios
      <$> (toEnum . fromIntegral <$> peekVEOF)
      <*> (toEnum . fromIntegral <$> peekVERASE)
      <*> (toEnum . fromIntegral <$> peekVINTR)
      <*> (toEnum . fromIntegral <$> peekVKILL)
      <*> (toEnum . fromIntegral <$> peekVQUIT)
      <*> pure (lflag .&. (#const ISIG)   /= 0)
      <*> pure (lflag .&. (#const ICANON) /= 0)
      <*> pure (lflag .&. (#const ECHO)   /= 0)
    where
      peekVEOF       = (#peek struct termios, c_cc[VEOF])   ptr :: IO CUChar
      peekVERASE     = (#peek struct termios, c_cc[VERASE]) ptr :: IO CUChar
      peekVINTR      = (#peek struct termios, c_cc[VINTR])  ptr :: IO CUChar
      peekVKILL      = (#peek struct termios, c_cc[VKILL])  ptr :: IO CUChar
      peekVQUIT      = (#peek struct termios, c_cc[VQUIT])  ptr :: IO CUChar
      peekLFlag      = (#peek struct termios, c_lflag)      ptr :: IO CUInt 
  poke ptr termios = do
    pokeVEOF   $ fromIntegral $ fromEnum $ termiosVEOF   termios
    pokeVERASE $ fromIntegral $ fromEnum $ termiosVERASE termios
    pokeVINTR  $ fromIntegral $ fromEnum $ termiosVINTR  termios
    pokeVKILL  $ fromIntegral $ fromEnum $ termiosVKILL  termios
    pokeVQUIT  $ fromIntegral $ fromEnum $ termiosVQUIT  termios
    peekLFlag >>= \flag-> pokeLFlag (if termiosISIG   termios then flag .|. (#const ISIG)   else flag .&. complement (#const ISIG))
    peekLFlag >>= \flag-> pokeLFlag (if termiosICANON termios then flag .|. (#const ICANON) else flag .&. complement (#const ICANON))
    peekLFlag >>= \flag-> pokeLFlag (if termiosECHO   termios then flag .|. (#const ECHO)   else flag .&. complement (#const ECHO))
    where
      pokeVEOF       = (#poke struct termios, c_cc[VEOF])   ptr :: CUChar -> IO ()
      pokeVERASE     = (#poke struct termios, c_cc[VERASE]) ptr :: CUChar -> IO ()
      pokeVINTR      = (#poke struct termios, c_cc[VINTR])  ptr :: CUChar -> IO ()
      pokeVKILL      = (#poke struct termios, c_cc[VKILL])  ptr :: CUChar -> IO ()
      pokeVQUIT      = (#poke struct termios, c_cc[VQUIT])  ptr :: CUChar -> IO ()
      pokeLFlag      = (#poke struct termios, c_lflag)      ptr :: CUInt -> IO ()
      peekLFlag      = (#peek struct termios, c_lflag)      ptr :: IO CUInt

foreign import ccall unsafe "tcgetattr"
  unsafeGetTermios :: CInt -> Ptr Termios -> IO CInt

foreign import ccall unsafe "tcsetattr"
  unsafeSetTermios :: CInt -> CInt -> Ptr Termios -> IO CInt

foreign import ccall unsafe "ioctl"
  unsafeIOCtl :: CInt -> CInt -> Ptr a -> IO CInt

foreign import ccall unsafe
  stg_sig_install :: CInt -> CInt -> Ptr a -> IO CInt