terminfo-0.3.0.1: System/Console/Terminfo/Base.hs
-- |
-- Maintainer : judah.jacobson@gmail.com
-- Stability : experimental
-- Portability : portable (FFI)
--
-- This module provides a low-level interface to the C functions of the
-- terminfo library.
--
-- NOTE: Since this library is built on top of the curses interface, it is not thread-safe.
module System.Console.Terminfo.Base(
-- * Initialization
Terminal(),
setupTerm,
setupTermFromEnv,
SetupTermError,
-- * Capabilities
Capability,
getCapability,
tiGetFlag,
tiGuardFlag,
tiGetNum,
tiGetStr,
-- * Output
TermOutput(),
runTermOutput,
hRunTermOutput,
termText,
tiGetOutput,
LinesAffected,
tiGetOutput1,
OutputCap,
-- ** Monoid functions
Monoid(..),
(<#>)
) where
import Control.Monad
import Data.Monoid
import Foreign.C
import Foreign.ForeignPtr
import Foreign.Ptr
import Foreign.Marshal
import Foreign.Storable (peek,poke)
import System.Environment (getEnv)
import System.IO.Unsafe (unsafePerformIO)
import System.IO
import Control.Exception.Extensible
import Data.Typeable
data TERMINAL = TERMINAL
newtype Terminal = Terminal (ForeignPtr TERMINAL)
foreign import ccall "&" cur_term :: Ptr (Ptr TERMINAL)
foreign import ccall set_curterm :: Ptr TERMINAL -> IO (Ptr TERMINAL)
foreign import ccall "&" del_curterm :: FunPtr (Ptr TERMINAL -> IO ())
foreign import ccall setupterm :: CString -> CInt -> Ptr CInt -> IO ()
-- | Initialize the terminfo library to the given terminal entry.
--
-- Throws a 'SetupTermError' if the terminfo database could not be read.
setupTerm :: String -> IO Terminal
setupTerm term = bracket (peek cur_term) (poke cur_term) $ \_ ->
withCString term $ \c_term ->
with 0 $ \ret_ptr -> do
-- NOTE: I believe that for the way we use terminfo
-- (i.e. custom output function)
-- this parameter does not affect anything.
let stdOutput = 1
{-- Force ncurses to return a new struct rather than
a copy of the current one (which it would do if the
terminal names are the same). This prevents problems
when calling del_term on a struct shared by more than one
Terminal. --}
poke cur_term nullPtr
-- Call setupterm and check the return value.
setupterm c_term stdOutput ret_ptr
ret <- peek ret_ptr
if (ret /=1)
then throwIO $ SetupTermError
$ "Couldn't look up terminfo entry " ++ show term
else do
cterm <- peek cur_term
fmap Terminal $ newForeignPtr del_curterm cterm
data SetupTermError = SetupTermError String
deriving Typeable
instance Show SetupTermError where
show (SetupTermError str) = "setupTerm: " ++ str
instance Exception SetupTermError where
-- | Initialize the terminfo library, using the @TERM@ environmental variable.
-- If @TERM@ is not set, we use the generic, minimal entry @dumb@.
--
-- Throws a 'SetupTermError' if the terminfo database could not be read.
setupTermFromEnv :: IO Terminal
setupTermFromEnv = do
env_term <- handle handleBadEnv $ getEnv "TERM"
let term = if null env_term then "dumb" else env_term
setupTerm term
where
handleBadEnv :: IOException -> IO String
handleBadEnv _ = return ""
withCurTerm :: Terminal -> IO a -> IO a
withCurTerm (Terminal term) f = withForeignPtr term $ \cterm -> do
old_term <- peek cur_term
if old_term /= cterm
then do
set_curterm cterm
x <- f
set_curterm old_term
return x
else f
-- | A feature or operation which a 'Terminal' may define.
newtype Capability a = Capability (IO (Maybe a))
getCapability :: Terminal -> Capability a -> Maybe a
getCapability term (Capability f) = unsafePerformIO $ withCurTerm term f
-- Note that the instances for Capability of Functor, Monad and MonadPlus
-- use the corresponding instances for Maybe.
instance Functor Capability where
fmap f (Capability g) = Capability (fmap (fmap f) g)
instance Monad Capability where
return = Capability . return . Just
Capability f >>= g = Capability $ do
mx <- f
case mx of
Nothing -> return Nothing
Just x -> let Capability g' = g x in g'
instance MonadPlus Capability where
mzero = Capability (return Nothing)
Capability f `mplus` Capability g = Capability $ do
mx <- f
case mx of
Nothing -> g
_ -> return mx
foreign import ccall tigetnum :: CString -> IO CInt
-- | Look up a numeric capability in the terminfo database.
tiGetNum :: String -> Capability Int
tiGetNum cap = Capability $ do
n <- fmap fromEnum (withCString cap tigetnum)
if n >= 0
then return (Just n)
else return Nothing
foreign import ccall tigetflag :: CString -> IO CInt
-- | Look up a boolean capability in the terminfo database.
--
-- Unlike 'tiGuardFlag', this capability never fails; it returns 'False' if the
-- capability is absent or set to false, and returns 'True' otherwise.
--
tiGetFlag :: String -> Capability Bool
tiGetFlag cap = Capability $ fmap (Just . (>0)) $
withCString cap tigetflag
-- | Look up a boolean capability in the terminfo database, and fail if
-- it\'s not defined.
tiGuardFlag :: String -> Capability ()
tiGuardFlag cap = tiGetFlag cap >>= guard
foreign import ccall tigetstr :: CString -> IO CString
-- | Look up a string capability in the terminfo database.
--
-- Note: Do not use this function for terminal output; use 'tiGetOutput'
-- instead.
tiGetStr :: String -> Capability String
tiGetStr cap = Capability $ do
result <- withCString cap tigetstr
if result == nullPtr || result == neg1Ptr
then return Nothing
else fmap Just (peekCString result)
where
-- hack; tigetstr sometimes returns (-1)
neg1Ptr = nullPtr `plusPtr` (-1)
foreign import ccall tparm ::
CString -> CLong -> CLong -> CLong -> CLong -> CLong -> CLong
-> CLong -> CLong -> CLong -- p1,...,p9
-> IO CString
-- Note: I may want to cut out the middleman and pipe tGoto/tGetStr together
-- with tput without a String marshall in the middle.
-- directly without
tParm :: String -> [Int] -> IO String
tParm cap ps = tparm' (map toEnum ps ++ repeat 0)
where tparm' (p1:p2:p3:p4:p5:p6:p7:p8:p9:_)
= withCString cap $ \c_cap -> do
result <- tparm c_cap p1 p2 p3 p4 p5 p6 p7 p8 p9
peekCString result
-- | Look up an output capability in the terminfo database.
tiGetOutput :: String -> Capability ([Int] -> LinesAffected -> TermOutput)
tiGetOutput cap = flip fmap (tiGetStr cap) $
\str ps la -> TermOutput $ \_ putc -> do
outStr <- tParm str ps
tPuts outStr la putc
type CharOutput = CInt -> IO CInt
foreign import ccall "wrapper" mkCallback :: CharOutput -> IO (FunPtr CharOutput)
foreign import ccall tputs :: CString -> CInt -> FunPtr CharOutput -> IO ()
-- | A parameter to specify the number of lines affected. Some capabilities
-- (e.g., @clear@ and @dch1@) use
-- this parameter on some terminals to compute variable-length padding.
type LinesAffected = Int
-- | Output a string capability. Applys padding information to the string if
-- necessary.
tPuts :: String -> LinesAffected -> FunPtr CharOutput -> IO ()
tPuts s n putc = withCString s $ \c_str -> tputs c_str (toEnum n) putc
-- | An action which sends output to the terminal. That output may mix plain text with control
-- characters and escape sequences, along with delays (called \"padding\") required by some older
-- terminals.
newtype TermOutput = TermOutput (Handle -> FunPtr CharOutput -> IO ())
-- | Write the terminal output to the standard output device.
runTermOutput :: Terminal -> TermOutput -> IO ()
runTermOutput = hRunTermOutput stdout
-- | Write the terminal output to the terminal or file managed by the given
-- 'Handle'.
hRunTermOutput :: Handle -> Terminal -> TermOutput -> IO ()
hRunTermOutput h term (TermOutput to) = do
putc_ptr <- mkCallback putc
withCurTerm term (to h putc_ptr)
freeHaskellFunPtr putc_ptr
where
putc c = let c' = toEnum $ fromEnum c
in hPutChar h c' >> hFlush h >> return c
-- | Output plain text containing no control characters or escape sequences.
termText :: String -> TermOutput
termText str = TermOutput $ \h _ -> hPutStr h str >> hFlush h
instance Monoid TermOutput where
mempty = TermOutput $ \_ _ -> return ()
TermOutput f `mappend` TermOutput g = TermOutput $ \h putc -> f h putc >> g h putc
-- | A type class to encapsulate capabilities which take in zero or more
-- parameters.
class OutputCap f where
outputCap :: ([Int] -> TermOutput) -> [Int] -> f
instance OutputCap TermOutput where
outputCap f xs = f (reverse xs)
instance (Enum a, OutputCap f) => OutputCap (a -> f) where
outputCap f xs = \x -> outputCap f (fromEnum x:xs)
-- | Look up an output capability which takes a fixed number of parameters
-- (for example, @Int -> Int -> TermOutput@).
--
-- For capabilities which may contain variable-length
-- padding, use 'tiGetOutput' instead.
tiGetOutput1 :: OutputCap f => String -> Capability f
tiGetOutput1 str = fmap (\f -> outputCap (flip f 1) []) $ tiGetOutput str
infixl 2 <#>
-- | An operator version of 'mappend'.
(<#>) :: Monoid m => m -> m -> m
(<#>) = mappend