module Graphics.Vty.Output.TerminfoBased
( reserveTerminal
)
where
import Control.Monad (when)
import Data.ByteString.Internal (toForeignPtr)
import Data.Terminfo.Parse
import Data.Terminfo.Eval
import Graphics.Vty.Attributes
import Graphics.Vty.Image (DisplayRegion)
import Graphics.Vty.DisplayAttributes
import Graphics.Vty.Output.Interface
import Blaze.ByteString.Builder (Write, writeToByteString)
import Control.Monad.Trans
import Data.Bits ((.&.))
import Data.IORef
import Data.Maybe (isJust, isNothing, fromJust)
import Data.Word
import Foreign.C.Types ( CInt(..), CLong(..) )
import Foreign.ForeignPtr (withForeignPtr)
import Foreign.Ptr (Ptr, plusPtr)
import qualified System.Console.Terminfo as Terminfo
import System.Posix.IO (fdWriteBuf)
import System.Posix.Types (Fd(..))
data TerminfoCaps = TerminfoCaps
{ smcup :: Maybe CapExpression
, rmcup :: Maybe CapExpression
, cup :: CapExpression
, cnorm :: Maybe CapExpression
, civis :: Maybe CapExpression
, supportsNoColors :: Bool
, useAltColorMap :: Bool
, setForeColor :: CapExpression
, setBackColor :: CapExpression
, setDefaultAttr :: CapExpression
, clearScreen :: CapExpression
, clearEol :: CapExpression
, displayAttrCaps :: DisplayAttrCaps
, ringBellAudio :: Maybe CapExpression
}
data DisplayAttrCaps = DisplayAttrCaps
{ setAttrStates :: Maybe CapExpression
, enterStandout :: Maybe CapExpression
, exitStandout :: Maybe CapExpression
, enterUnderline :: Maybe CapExpression
, exitUnderline :: Maybe CapExpression
, enterReverseVideo :: Maybe CapExpression
, enterDimMode :: Maybe CapExpression
, enterBoldMode :: Maybe CapExpression
}
fdWriteAll :: Fd -> Ptr Word8 -> Int -> Int -> IO Int
fdWriteAll outFd ptr len count
| len < 0 = fail "fdWriteAll: len is less than 0"
| len == 0 = return count
| otherwise = do
writeCount <- fromEnum <$> fdWriteBuf outFd ptr (toEnum len)
let len' = len writeCount
ptr' = ptr `plusPtr` writeCount
count' = count + writeCount
fdWriteAll outFd ptr' len' count'
sendCapToTerminal :: Output -> CapExpression -> [CapParam] -> IO ()
sendCapToTerminal t cap capParams = do
outputByteBuffer t $ writeToByteString $ writeCapExpr cap capParams
reserveTerminal :: ( Applicative m, MonadIO m ) => String -> Fd -> m Output
reserveTerminal termName outFd = liftIO $ do
ti <- Terminfo.setupTerm termName
msetaf <- probeCap ti "setaf"
msetf <- probeCap ti "setf"
let (noColors, useAlt, setForeCap)
= case msetaf of
Just setaf -> (False, False, setaf)
Nothing -> case msetf of
Just setf -> (False, True, setf)
Nothing -> (True, True, error $ "no fore color support for terminal " ++ termName)
msetab <- probeCap ti "setab"
msetb <- probeCap ti "setb"
let set_back_cap
= case msetab of
Nothing -> case msetb of
Just setb -> setb
Nothing -> error $ "no back color support for terminal " ++ termName
Just setab -> setab
terminfoCaps <- pure TerminfoCaps
<*> probeCap ti "smcup"
<*> probeCap ti "rmcup"
<*> requireCap ti "cup"
<*> probeCap ti "cnorm"
<*> probeCap ti "civis"
<*> pure noColors
<*> pure useAlt
<*> pure setForeCap
<*> pure set_back_cap
<*> requireCap ti "sgr0"
<*> requireCap ti "clear"
<*> requireCap ti "el"
<*> currentDisplayAttrCaps ti
<*> probeCap ti "bel"
newAssumedStateRef <- newIORef initialAssumedState
let t = Output
{ terminalID = termName
, releaseTerminal = liftIO $ do
sendCap setDefaultAttr []
maybeSendCap cnorm []
, supportsBell = return $ isJust $ ringBellAudio terminfoCaps
, ringTerminalBell = liftIO $ maybeSendCap ringBellAudio []
, reserveDisplay = liftIO $ do
maybeSendCap smcup []
sendCap clearScreen []
, releaseDisplay = liftIO $ do
maybeSendCap rmcup []
maybeSendCap cnorm []
, displayBounds = do
rawSize <- liftIO $ getWindowSize outFd
case rawSize of
(w, h) | w < 0 || h < 0 -> fail $ "getwinsize returned < 0 : " ++ show rawSize
| otherwise -> return (w,h)
, outputByteBuffer = \outBytes -> do
let (fptr, offset, len) = toForeignPtr outBytes
actualLen <- withForeignPtr fptr
$ \ptr -> fdWriteAll outFd (ptr `plusPtr` offset) len 0
when (toEnum len /= actualLen) $ fail $ "Graphics.Vty.Output: outputByteBuffer "
++ "length mismatch. " ++ show len ++ " /= " ++ show actualLen
++ " Please report this bug to vty project."
, contextColorCount
= case supportsNoColors terminfoCaps of
False -> case Terminfo.getCapability ti (Terminfo.tiGetNum "colors" ) of
Nothing -> 8
Just v -> toEnum v
True -> 1
, supportsCursorVisibility = isJust $ civis terminfoCaps
, supportsMode = const False
, setMode = const $ const $ return ()
, getModeStatus = const $ return False
, assumedStateRef = newAssumedStateRef
, mkDisplayContext = \tActual -> liftIO . terminfoDisplayContext tActual terminfoCaps
}
sendCap s = sendCapToTerminal t (s terminfoCaps)
maybeSendCap s = when (isJust $ s terminfoCaps) . sendCap (fromJust . s)
return t
requireCap :: (Applicative m, MonadIO m) => Terminfo.Terminal -> String -> m CapExpression
requireCap ti capName
= case Terminfo.getCapability ti (Terminfo.tiGetStr capName) of
Nothing -> fail $ "Terminal does not define required capability \"" ++ capName ++ "\""
Just capStr -> parseCap capStr
probeCap :: (Applicative m, MonadIO m) => Terminfo.Terminal -> String -> m (Maybe CapExpression)
probeCap ti capName
= case Terminfo.getCapability ti (Terminfo.tiGetStr capName) of
Nothing -> return Nothing
Just capStr -> Just <$> parseCap capStr
parseCap :: (Applicative m, MonadIO m) => String -> m CapExpression
parseCap capStr = do
case parseCapExpression capStr of
Left e -> fail $ show e
Right cap -> return cap
currentDisplayAttrCaps :: ( Applicative m, MonadIO m )
=> Terminfo.Terminal
-> m DisplayAttrCaps
currentDisplayAttrCaps ti
= pure DisplayAttrCaps
<*> probeCap ti "sgr"
<*> probeCap ti "smso"
<*> probeCap ti "rmso"
<*> probeCap ti "smul"
<*> probeCap ti "rmul"
<*> probeCap ti "rev"
<*> probeCap ti "dim"
<*> probeCap ti "bold"
foreign import ccall "gwinsz.h vty_c_get_window_size" c_getWindowSize :: Fd -> IO CLong
getWindowSize :: Fd -> IO (Int,Int)
getWindowSize fd = do
(a,b) <- (`divMod` 65536) `fmap` c_getWindowSize fd
return (fromIntegral b, fromIntegral a)
terminfoDisplayContext :: Output -> TerminfoCaps -> DisplayRegion -> IO DisplayContext
terminfoDisplayContext tActual terminfoCaps r = return dc
where dc = DisplayContext
{ contextDevice = tActual
, contextRegion = r
, writeMoveCursor = \x y -> writeCapExpr (cup terminfoCaps) [toEnum y, toEnum x]
, writeShowCursor = case cnorm terminfoCaps of
Nothing -> error "this terminal does not support show cursor"
Just c -> writeCapExpr c []
, writeHideCursor = case civis terminfoCaps of
Nothing -> error "this terminal does not support hide cursor"
Just c -> writeCapExpr c []
, writeSetAttr = terminfoWriteSetAttr dc terminfoCaps
, writeDefaultAttr = writeCapExpr (setDefaultAttr terminfoCaps) []
, writeRowEnd = writeCapExpr (clearEol terminfoCaps) []
, inlineHack = return ()
}
terminfoWriteSetAttr :: DisplayContext -> TerminfoCaps -> FixedAttr -> Attr -> DisplayAttrDiff -> Write
terminfoWriteSetAttr dc terminfoCaps prevAttr reqAttr diffs = do
case (foreColorDiff diffs == ColorToDefault) || (backColorDiff diffs == ColorToDefault) of
True -> do
case reqDisplayCapSeqFor (displayAttrCaps terminfoCaps)
(fixedStyle attr )
(styleToApplySeq $ fixedStyle attr) of
EnterExitSeq caps -> writeDefaultAttr dc
`mappend`
foldMap (\cap -> writeCapExpr cap []) caps
`mappend`
setColors
SetState state -> writeCapExpr (fromJust $ setAttrStates
$ displayAttrCaps
$ terminfoCaps
)
(sgrArgsForState state)
`mappend`
setColors
False -> do
case reqDisplayCapSeqFor (displayAttrCaps terminfoCaps)
(fixedStyle attr)
(styleDiffs diffs) of
EnterExitSeq caps -> foldMap (\cap -> writeCapExpr cap []) caps
`mappend`
writeColorDiff setForeColor (foreColorDiff diffs)
`mappend`
writeColorDiff setBackColor (backColorDiff diffs)
SetState state -> writeCapExpr (fromJust $ setAttrStates
$ displayAttrCaps terminfoCaps
)
(sgrArgsForState state)
`mappend` setColors
where
colorMap = case useAltColorMap terminfoCaps of
False -> ansiColorIndex
True -> altColorIndex
attr = fixDisplayAttr prevAttr reqAttr
setColors =
(case fixedForeColor attr of
Just c -> writeCapExpr (setForeColor terminfoCaps)
[toEnum $ colorMap c]
Nothing -> mempty)
`mappend`
(case fixedBackColor attr of
Just c -> writeCapExpr (setBackColor terminfoCaps)
[toEnum $ colorMap c]
Nothing -> mempty)
writeColorDiff _f NoColorChange
= mempty
writeColorDiff _f ColorToDefault
= error "ColorToDefault is not a possible case for applyColorDiffs"
writeColorDiff f (SetColor c)
= writeCapExpr (f terminfoCaps) [toEnum $ colorMap c]
ansiColorIndex :: Color -> Int
ansiColorIndex (ISOColor v) = fromEnum v
ansiColorIndex (Color240 v) = 16 + fromEnum v
altColorIndex :: Color -> Int
altColorIndex (ISOColor 0) = 0
altColorIndex (ISOColor 1) = 4
altColorIndex (ISOColor 2) = 2
altColorIndex (ISOColor 3) = 6
altColorIndex (ISOColor 4) = 1
altColorIndex (ISOColor 5) = 5
altColorIndex (ISOColor 6) = 3
altColorIndex (ISOColor 7) = 7
altColorIndex (ISOColor v) = fromEnum v
altColorIndex (Color240 v) = 16 + fromEnum v
data DisplayAttrSeq
= EnterExitSeq [CapExpression]
| SetState DisplayAttrState
data DisplayAttrState = DisplayAttrState
{ applyStandout :: Bool
, applyUnderline :: Bool
, applyReverseVideo :: Bool
, applyBlink :: Bool
, applyDim :: Bool
, applyBold :: Bool
}
sgrArgsForState :: DisplayAttrState -> [CapParam]
sgrArgsForState attrState = map (\b -> if b then 1 else 0)
[ applyStandout attrState
, applyUnderline attrState
, applyReverseVideo attrState
, applyBlink attrState
, applyDim attrState
, applyBold attrState
, False
, False
, False
]
reqDisplayCapSeqFor :: DisplayAttrCaps -> Style -> [StyleStateChange] -> DisplayAttrSeq
reqDisplayCapSeqFor caps s diffs
= case (any noEnterExitCap diffs, isJust $ setAttrStates caps) of
( False, _ ) -> EnterExitSeq $ map enterExitCap diffs
( True, False ) -> EnterExitSeq $ map enterExitCap
$ filter (not . noEnterExitCap) diffs
( True, True ) -> SetState $ stateForStyle s
where
noEnterExitCap ApplyStandout = isNothing $ enterStandout caps
noEnterExitCap RemoveStandout = isNothing $ exitStandout caps
noEnterExitCap ApplyUnderline = isNothing $ enterUnderline caps
noEnterExitCap RemoveUnderline = isNothing $ exitUnderline caps
noEnterExitCap ApplyReverseVideo = isNothing $ enterReverseVideo caps
noEnterExitCap RemoveReverseVideo = True
noEnterExitCap ApplyBlink = True
noEnterExitCap RemoveBlink = True
noEnterExitCap ApplyDim = isNothing $ enterDimMode caps
noEnterExitCap RemoveDim = True
noEnterExitCap ApplyBold = isNothing $ enterBoldMode caps
noEnterExitCap RemoveBold = True
enterExitCap ApplyStandout = fromJust $ enterStandout caps
enterExitCap RemoveStandout = fromJust $ exitStandout caps
enterExitCap ApplyUnderline = fromJust $ enterUnderline caps
enterExitCap RemoveUnderline = fromJust $ exitUnderline caps
enterExitCap ApplyReverseVideo = fromJust $ enterReverseVideo caps
enterExitCap ApplyDim = fromJust $ enterDimMode caps
enterExitCap ApplyBold = fromJust $ enterBoldMode caps
enterExitCap _ = error "enterExitCap applied to diff that was known not to have one."
stateForStyle :: Style -> DisplayAttrState
stateForStyle s = DisplayAttrState
{ applyStandout = isStyleSet standout
, applyUnderline = isStyleSet underline
, applyReverseVideo = isStyleSet reverseVideo
, applyBlink = isStyleSet blink
, applyDim = isStyleSet dim
, applyBold = isStyleSet bold
}
where isStyleSet = hasStyle s
styleToApplySeq :: Style -> [StyleStateChange]
styleToApplySeq s = concat
[ applyIfRequired ApplyStandout standout
, applyIfRequired ApplyUnderline underline
, applyIfRequired ApplyReverseVideo reverseVideo
, applyIfRequired ApplyBlink blink
, applyIfRequired ApplyDim dim
, applyIfRequired ApplyBlink bold
]
where
applyIfRequired op flag
= if 0 == (flag .&. s)
then []
else [op]