module Graphics.Vty.Output.Interface
( Output(..)
, AssumedState(..)
, DisplayContext(..)
, Mode(..)
, displayContext
, outputPicture
, initialAssumedState
, limitAttrForDisplay
)
where
import Graphics.Vty.Attributes
import Graphics.Vty.Image (DisplayRegion, regionHeight)
import Graphics.Vty.Picture
import Graphics.Vty.PictureToSpans
import Graphics.Vty.Span
import Graphics.Vty.DisplayAttributes
import Blaze.ByteString.Builder (Write, writeToByteString)
import Blaze.ByteString.Builder.ByteString (writeByteString)
import Control.Monad.Trans
import qualified Data.ByteString as BS
import Data.IORef
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Vector as Vector
data Mode = Mouse
| BracketedPaste
| Focus
deriving (Eq, Read, Show)
data Output = Output
{
terminalID :: String
, releaseTerminal :: forall m. MonadIO m => m ()
, reserveDisplay :: forall m. MonadIO m => m ()
, releaseDisplay :: forall m. MonadIO m => m ()
, displayBounds :: forall m. MonadIO m => m DisplayRegion
, outputByteBuffer :: BS.ByteString -> IO ()
, contextColorCount :: Int
, supportsCursorVisibility :: Bool
, supportsMode :: Mode -> Bool
, setMode :: forall m. MonadIO m => Mode -> Bool -> m ()
, getModeStatus :: forall m. MonadIO m => Mode -> m Bool
, assumedStateRef :: IORef AssumedState
, mkDisplayContext :: forall m. MonadIO m => Output -> DisplayRegion -> m DisplayContext
, ringTerminalBell :: forall m. MonadIO m => m ()
, supportsBell :: forall m. MonadIO m => m Bool
}
displayContext :: MonadIO m => Output -> DisplayRegion -> m DisplayContext
displayContext t = liftIO . mkDisplayContext t t
data AssumedState = AssumedState
{ prevFattr :: Maybe FixedAttr
, prevOutputOps :: Maybe DisplayOps
}
initialAssumedState :: AssumedState
initialAssumedState = AssumedState Nothing Nothing
data DisplayContext = DisplayContext
{ contextDevice :: Output
, contextRegion :: DisplayRegion
, writeMoveCursor :: Int -> Int -> Write
, writeShowCursor :: Write
, writeHideCursor :: Write
, writeSetAttr :: FixedAttr -> Attr -> DisplayAttrDiff -> Write
, writeDefaultAttr :: Write
, writeRowEnd :: Write
, inlineHack :: IO ()
}
writeUtf8Text :: BS.ByteString -> Write
writeUtf8Text = writeByteString
outputPicture :: MonadIO m => DisplayContext -> Picture -> m ()
outputPicture dc pic = liftIO $ do
as <- readIORef (assumedStateRef $ contextDevice dc)
let manipCursor = supportsCursorVisibility (contextDevice dc)
r = contextRegion dc
ops = displayOpsForPic pic r
initialAttr = FixedAttr defaultStyleMask Nothing Nothing Nothing
diffs :: [Bool] = case prevOutputOps as of
Nothing -> replicate (fromEnum $ regionHeight $ affectedRegion ops) True
Just previousOps -> if affectedRegion previousOps /= affectedRegion ops
then replicate (displayOpsRows ops) True
else zipWith (/=) (Vector.toList previousOps)
(Vector.toList ops)
out = (if manipCursor then writeHideCursor dc else mempty)
`mappend` writeOutputOps dc initialAttr diffs ops
`mappend`
(let (w,h) = contextRegion dc
clampX = max 0 . min (w1)
clampY = max 0 . min (h1) in
case picCursor pic of
_ | not manipCursor -> mempty
NoCursor -> mempty
AbsoluteCursor x y ->
writeShowCursor dc `mappend`
writeMoveCursor dc (clampX x) (clampY y)
Cursor x y ->
let m = cursorOutputMap ops $ picCursor pic
(ox, oy) = charToOutputPos m (x,y)
in writeShowCursor dc `mappend`
writeMoveCursor dc (clampX ox) (clampY oy)
)
outputByteBuffer (contextDevice dc) (writeToByteString out)
let as' = as { prevOutputOps = Just ops }
writeIORef (assumedStateRef $ contextDevice dc) as'
writeOutputOps :: DisplayContext -> FixedAttr -> [Bool] -> DisplayOps -> Write
writeOutputOps dc initialAttr diffs ops =
let (_, out, _) = Vector.foldl' writeOutputOps'
(0, mempty, diffs)
ops
in out
where
writeOutputOps' (y, out, True : diffs') spanOps
= let spanOut = writeSpanOps dc y initialAttr spanOps
out' = out `mappend` spanOut
in (y+1, out', diffs')
writeOutputOps' (y, out, False : diffs') _spanOps
= (y + 1, out, diffs')
writeOutputOps' (_y, _out, []) _spanOps
= error "vty - output spans without a corresponding diff."
writeSpanOps :: DisplayContext -> Int -> FixedAttr -> SpanOps -> Write
writeSpanOps dc y initialAttr spanOps =
let start = writeMoveCursor dc 0 y `mappend` writeDefaultAttr dc
in fst $ Vector.foldl' (\(out, fattr) op -> case writeSpanOp dc op fattr of
(opOut, fattr') -> (out `mappend` opOut, fattr')
)
(start, initialAttr)
spanOps
writeSpanOp :: DisplayContext -> SpanOp -> FixedAttr -> (Write, FixedAttr)
writeSpanOp dc (TextSpan attr _ _ str) fattr =
let attr' = limitAttrForDisplay (contextDevice dc) attr
fattr' = fixDisplayAttr fattr attr'
diffs = displayAttrDiffs fattr fattr'
out = writeSetAttr dc fattr attr' diffs
`mappend` writeUtf8Text (T.encodeUtf8 $ TL.toStrict str)
in (out, fattr')
writeSpanOp _dc (Skip _) _fattr = error "writeSpanOp for Skip"
writeSpanOp dc (RowEnd _) fattr = (writeDefaultAttr dc `mappend` writeRowEnd dc, fattr)
data CursorOutputMap = CursorOutputMap
{ charToOutputPos :: (Int, Int) -> (Int, Int)
}
cursorOutputMap :: DisplayOps -> Cursor -> CursorOutputMap
cursorOutputMap spanOps _cursor = CursorOutputMap
{ charToOutputPos = \(cx, cy) -> (cursorColumnOffset spanOps cx cy, cy)
}
cursorColumnOffset :: DisplayOps -> Int -> Int -> Int
cursorColumnOffset ops cx cy =
let cursorRowOps = Vector.unsafeIndex ops (fromEnum cy)
(outOffset, _, _)
= Vector.foldl' ( \(d, currentCx, done) op ->
if done then (d, currentCx, done) else case spanOpHasWidth op of
Nothing -> (d, currentCx, False)
Just (cw, ow) -> case compare cx (currentCx + cw) of
GT -> ( d + ow
, currentCx + cw
, False
)
EQ -> ( d + ow
, currentCx + cw
, True
)
LT -> ( d + columnsToCharOffset (cx currentCx) op
, currentCx + cw
, True
)
)
(0, 0, False)
cursorRowOps
in outOffset
limitAttrForDisplay :: Output -> Attr -> Attr
limitAttrForDisplay t attr
= attr { attrForeColor = clampColor $ attrForeColor attr
, attrBackColor = clampColor $ attrBackColor attr
}
where
clampColor Default = Default
clampColor KeepCurrent = KeepCurrent
clampColor (SetTo c) = clampColor' c
clampColor' (ISOColor v)
| contextColorCount t < 8 = Default
| contextColorCount t < 16 && v >= 8 = SetTo $ ISOColor (v 8)
| otherwise = SetTo $ ISOColor v
clampColor' (Color240 v)
| contextColorCount t < 8 = Default
| contextColorCount t < 16 = Default
| contextColorCount t <= 256 = SetTo $ Color240 v
| otherwise
= let p :: Double = fromIntegral v / 240.0
v' = floor $ p * (fromIntegral $ contextColorCount t)
in SetTo $ Color240 v'