terminal-0.2.0.0: platform/posix/src/System/Terminal/Platform.hsc
module System.Terminal.Platform
( withTerminal
, LocalTerminal ()
) where
import Control.Applicative
import Control.Concurrent
import qualified Control.Concurrent.Async as A
import Control.Concurrent.STM.TVar
import Control.Concurrent.STM.TMVar
import qualified Control.Exception as E
import Control.Monad (forM_, void, when)
import Control.Monad.Catch hiding (handle)
import Control.Monad.IO.Class
import Control.Monad.STM
import Data.Bits
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import Data.Maybe
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.Terminal.Terminal
import System.Terminal.MonadInput
import System.Terminal.MonadScreen hiding (getWindowSize)
import System.Terminal.Decoder
import System.Terminal.Encoder
#include "Rts.h"
#include "hs_terminal.h"
data LocalTerminal
= LocalTerminal
{ localType :: BS.ByteString
, localEvent :: STM Event
, localInterrupt :: STM Interrupt
, localGetCursorPosition :: IO Position
}
instance Terminal LocalTerminal where
termType = localType
termEvent = localEvent
termInterrupt = localInterrupt
termCommand _ c = Text.hPutStr IO.stdout (defaultEncode c)
termFlush _ = IO.hFlush IO.stdout
termGetWindowSize _ = getWindowSize
termGetCursorPosition = localGetCursorPosition
withTerminal :: (MonadIO m, MonadMask m) => (LocalTerminal -> m a) -> m a
withTerminal action = do
term <- BS8.pack . fromMaybe "xterm" <$> liftIO (lookupEnv "TERM")
mainThread <- liftIO myThreadId
interrupt <- liftIO (newTVarIO False)
windowChanged <- liftIO (newTVarIO False)
events <- liftIO newEmptyTMVarIO
cursorPosition <- liftIO newEmptyTMVarIO
withTermiosSettings $ \termios->
withInterruptHandler (handleInterrupt mainThread interrupt) $
withResizeHandler (handleResize windowChanged) $
withInputProcessing termios cursorPosition events $
action LocalTerminal
{ localType = term
, localEvent = do
changed <- swapTVar windowChanged False
if changed
then pure (WindowEvent WindowSizeChanged)
else takeTMVar events
, localInterrupt = swapTVar interrupt False >>= check >> pure Interrupt
, localGetCursorPosition = do
-- Empty the result variable.
atomically (void (takeTMVar cursorPosition) <|> pure ())
-- Send cursor position report request.
Text.hPutStr IO.stdout (defaultEncode GetCursorPosition)
IO.hFlush IO.stdout
-- Wait for the result variable to be filled by the input processor.
atomically (takeTMVar cursorPosition)
}
where
handleResize :: TVar Bool -> IO ()
handleResize windowChanged =
atomically (writeTVar windowChanged True)
-- This function is responsible for passing interrupt signals 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 signals. 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 :: ThreadId -> TVar Bool -> IO ()
handleInterrupt mainThread interrupt = do
unhandledInterrupt <- atomically (swapTVar interrupt True)
when unhandledInterrupt (E.throwTo mainThread E.UserInterrupt)
specialChar :: Termios -> Modifiers -> Char -> Maybe Event
specialChar t mods = \case
c | c == termiosVERASE t -> Just $ KeyEvent BackspaceKey mods
| c == '\n' -> Just $ KeyEvent EnterKey mods
| c == '\t' -> Just $ KeyEvent TabKey mods
| c == '\b' -> Just $ KeyEvent DeleteKey mods
| c == '\SP' -> Just $ KeyEvent SpaceKey mods
| c == '\DEL' -> Just $ KeyEvent DeleteKey mods
| otherwise -> Nothing
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 { 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 ()
withInterruptHandler :: (MonadIO m, MonadMask m) => IO () -> m a -> m a
withInterruptHandler handler = bracket installHandler restoreHandler . const
where
installHandler = liftIO do
Conc.ensureIOManagerIsRunning
oldHandler <- Conc.setHandler (#const SIGINT) (Just (const handler, Dyn.toDyn handler))
oldAction <- stg_sig_install (#const SIGINT) (#const STG_SIG_HAN) nullPtr
pure (oldHandler,oldAction)
restoreHandler (oldHandler,oldAction) = liftIO do
void $ Conc.setHandler (#const SIGINT) oldHandler
void $ stg_sig_install (#const SIGINT) oldAction nullPtr
pure ()
withInputProcessing :: (MonadIO m, MonadMask m) =>
Termios -> TMVar Position -> TMVar Event -> m a -> m a
withInputProcessing termios cursorPosition events =
bracket (liftIO $ A.async $ run decoder) (liftIO . A.cancel) . const
where
run :: Decoder -> IO ()
run d = do
c <- IO.hGetChar IO.stdin
case feedDecoder d mempty c of
-- The decoder is not in final state.
-- There are sequences depending on timing (escape either is literal
-- escape or the beginning of a sequence).
-- This block evaluates whether more input is available within
-- a limited timespan. If this is the case it just recurses
-- with the decoder continuation.
-- Otherwise, a NUL character is fed in order to tell the decoder
-- that there is no more input belonging to the sequence.
Left d' -> IO.hWaitForInput IO.stdin timeoutMilliseconds >>= \case
True -> run d'
False -> case feedDecoder d' mempty '\NUL' of
Left d'' -> run d''
Right evs -> do
forM_ evs writeEvent
run decoder
-- The decoder reached a final state.
-- All recognized events are appended to the event stream.
Right evs -> do
forM_ evs writeEvent
run decoder
decoder :: Decoder
decoder = defaultDecoder (specialChar termios)
-- Adds events to the event stream and catches certain events
-- that require special treatment.
writeEvent :: Event -> IO ()
writeEvent = \case
ev@(DeviceEvent (CursorPositionReport pos)) -> atomically do
-- One of the alternatives will succeed.
-- The second one is not strictly required but a fail safe in order
-- to never block in case the terminal sends a report without request.
putTMVar cursorPosition pos <|> void (swapTMVar cursorPosition pos)
putTMVar events ev
ev -> atomically (putTMVar events ev)
-- 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.
timeoutMilliseconds :: Int
timeoutMilliseconds = 50
getWindowSize :: IO Size
getWindowSize =
alloca $ \ptr->
unsafeIOCtl 0 (#const TIOCGWINSZ) ptr >>= \case
0 -> peek ptr >>= \ws-> pure $ Size (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