summaryrefslogtreecommitdiff
path: root/src/System/IO/Terminal/Render.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/System/IO/Terminal/Render.hs')
-rw-r--r--src/System/IO/Terminal/Render.hs113
1 files changed, 84 insertions, 29 deletions
diff --git a/src/System/IO/Terminal/Render.hs b/src/System/IO/Terminal/Render.hs
index f3f0b59..cae01e9 100644
--- a/src/System/IO/Terminal/Render.hs
+++ b/src/System/IO/Terminal/Render.hs
@@ -1,4 +1,8 @@
-{-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE StandaloneDeriving #-}
{-|
Module : System.IO.Terminal.Render
Copyright : (c) UU, 2019
@@ -49,6 +53,9 @@ module System.IO.Terminal.Render
,module System.IO.Terminal.Characters)
where
+import Control.Concurrent.STM (atomically)
+import Control.Concurrent.STM.TChan (newTChan, writeTChan, TChan, tryReadTChan)
+import Control.Monad.IO.Unlift
import Control.Monad.State.Strict
import Data.Array.IO
import Data.Colour.SRGB hiding (RGB)
@@ -61,6 +68,12 @@ import System.IO.Terminal.Characters
import qualified System.IO.Terminal.IO as IO
import Utils.Monad
+#if defined(OS_LINUX) || defined(OS_MACOS)
+import qualified System.IO.Terminal.IO.Posix as IO
+#else
+import qualified System.IO.Terminal.IO.Windows as IO
+#endif
+
-- NOTE ON COORDINATES
-- Friendly coordinates are in the form (x, y), but the world is not
@@ -82,7 +95,8 @@ type RGB = (Word8, Word8, Word8)
-- wrapped monad should be a monad over 'IO' (i.e. needs to be an instance
-- of 'MonadIO').
newtype RenderT m a = RenderT { unRenderT :: StateT RState m a }
- deriving (Functor, Applicative, Monad, MonadState RState, MonadTrans)
+ deriving newtype (Functor, Applicative, Monad, MonadState RState, MonadTrans)
+ deriving MonadUnliftIO via StateTUnliftIOWrapper RState m
deriving instance MonadIO m => MonadIO (RenderT m)
@@ -92,7 +106,9 @@ type Render = RenderT IO
data RState = RState { rDrawBuffer :: !(IOArray (Int, Int) Cell)
, rScreenBuffer :: !(IOArray (Int, Int) Cell)
- , rCurrentStyle :: !Style }
+ , rCurrentStyle :: !Style
+ , rWinchChan :: !(TChan ())
+ , rForceFullRedraw :: !Bool }
data Cell = Cell { cChar :: !Char
, cStyle :: !Style }
@@ -113,34 +129,39 @@ data Style = Style { sFg :: !(Maybe RGB)
defaultStyle :: Style
defaultStyle = Style { sFg = Nothing, sBg = Nothing, sBold = False, sUnderline = False }
-initRState :: IO RState
-initRState = do
+allocateBuffer :: (Int, Int) -> IO (IOArray (Int, Int) Cell)
+allocateBuffer (wid, hei) = newArray ((0, 0), (hei - 1, wid - 1)) emptyCell
+ where emptyCell = Cell { cChar = ' ', cStyle = defaultStyle }
+
+initRState :: TChan () -> IO RState
+initRState winchChan = do
(wid, hei) <- IO.queryTermSize
- let emptyCell = Cell { cChar = ' ', cStyle = defaultStyle }
- bufferConstructor = newArray ((0, 0), (hei - 1, wid - 1)) emptyCell
- drawBuffer <- bufferConstructor
- screenBuffer <- bufferConstructor
+ drawBuffer <- allocateBuffer (wid, hei)
+ screenBuffer <- allocateBuffer (wid, hei)
return $ RState { rDrawBuffer = drawBuffer
, rScreenBuffer = screenBuffer
- , rCurrentStyle = defaultStyle }
+ , rCurrentStyle = defaultStyle
+ , rWinchChan = winchChan
+ , rForceFullRedraw = False }
-- | Run a 'Render' computation in an IO-monad.
-withRender :: MonadIO m => RenderT m a -> m a
+withRender :: MonadUnliftIO m => RenderT m a -> m a
withRender act = do
- whenM (liftIO $ not <$> hIsTerminalDevice stdout) $
- liftIO $ die "ERROR: Stdout is not connected to a terminal!"
+ liftIO $ whenM (not <$> hIsTerminalDevice stdout) $
+ die "ERROR: Stdout is not connected to a terminal!"
liftIO $ whenM (not <$> A.hSupportsANSI stdout) $
die "ERROR: Terminal is not capable of interpreting ANSI escape sequences!"
- liftIO IO.toAlternateScreen
- liftIO A.clearScreen
- liftIO A.hideCursor
- st <- liftIO initRState
- res <- evalStateT (unRenderT act) st
- liftIO A.showCursor
- liftIO IO.fromAlternateScreen
- return res
+ winchChan <- liftIO $ atomically newTChan
+
+ unliftBracket_ (liftIO IO.toAlternateScreen) (liftIO IO.fromAlternateScreen) $ do
+ liftIO A.clearScreen
+ unliftBracket_ (liftIO A.hideCursor) (liftIO A.showCursor) $ do
+ IO.withWinchHandler (atomically $ writeTChan winchChan ()) $ do
+ st <- liftIO $ initRState winchChan
+ res <- evalStateT (unRenderT act) st
+ return res
-- | Commit the changes from the draw buffer to the screen buffer.
redraw :: MonadIO m => RenderT m ()
@@ -154,7 +175,11 @@ redrawFull :: MonadIO m => RenderT m ()
redrawFull = redrawGen True
redrawGen :: MonadIO m => Bool -> RenderT m ()
-redrawGen full = do
+redrawGen fullRequest = do
+ fullForced <- gets rForceFullRedraw
+ modify $ \s -> s { rForceFullRedraw = False }
+ let full = fullRequest || fullForced
+
drawBuffer <- gets rDrawBuffer
screenBuffer <- gets rScreenBuffer
size@(wid, hei) <- getTermSize
@@ -162,18 +187,19 @@ redrawGen full = do
liftIO $ do
A.setSGR (styleToSGR defaultStyle)
- foldM_ (applyCell drawBuffer screenBuffer size) (defaultStyle, (-1, -1)) indices
+ foldM_ (applyCell full drawBuffer screenBuffer size) (defaultStyle, (-1, -1)) indices
hFlush stdout
liftIO $ copyArray screenBuffer drawBuffer
where
- applyCell :: IOArray (Int, Int) Cell -- drawbuf
+ applyCell :: Bool -- full redraw
+ -> IOArray (Int, Int) Cell -- drawbuf
-> IOArray (Int, Int) Cell -- screenbuf
-> (Int, Int) -- screen size@(y, x)
-> (Style, (Int, Int)) -- current (style, curpos@(y, x))
-> (Int, Int) -- index@(y, x)
-> IO (Style, (Int, Int)) -- new (style, curpos@(y, x))
- applyCell drawbuf screenbuf size (style, curpos) idx = do
+ applyCell full drawbuf screenbuf size (style, curpos) idx = do
dcell <- readArray drawbuf idx
scell <- readArray screenbuf idx
if dcell /= scell || full
@@ -185,12 +211,31 @@ redrawGen full = do
else do
return (style, curpos)
--- | Get the current terminal size. This is a cheap operation.
+-- | Get the current terminal size. This does not roundtrip via the terminal on
+-- every invocation (it only re-checks after a SIGWINCH), but does reallocate
+-- the window buffers if things changed.
+-- If the terminal size changed, the next redraw will automatically be a full
+-- redraw.
getTermSize :: MonadIO m => RenderT m (Int, Int)
getTermSize = do
- drawBuffer <- gets rDrawBuffer
- (_, (maxy, maxx)) <- liftIO (getBounds drawBuffer)
- return (maxx + 1, maxy + 1)
+ winchChan <- gets rWinchChan
+ liftIO (atomically $ tryReadTChan winchChan) >>= \case
+ Nothing -> do
+ drawBuffer <- gets rDrawBuffer
+ (_, (maxy, maxx)) <- liftIO (getBounds drawBuffer)
+ return (maxx + 1, maxy + 1)
+ Just () -> do
+ size <- liftIO IO.queryTermSize
+ oldDrawbuf <- gets rDrawBuffer
+ oldScreenbuf <- gets rScreenBuffer
+ newDrawbuf <- liftIO $ allocateBuffer size
+ newScreenbuf <- liftIO $ allocateBuffer size
+ liftIO $ copyArrayIntersection newDrawbuf oldDrawbuf
+ liftIO $ copyArrayIntersection newScreenbuf oldScreenbuf
+ modify $ \s -> s { rDrawBuffer = newDrawbuf
+ , rScreenBuffer = newScreenbuf
+ , rForceFullRedraw = True }
+ return size
-- | Write some text to the draw buffer at the given position.
write :: MonadIO m => (Int, Int) -> String -> RenderT m ()
@@ -317,3 +362,13 @@ copyArray dest src = do
forM_ (range destbounds) $ \idx ->
readArray src idx >>= writeArray dest idx
+
+-- Copies the intersection of the bounds.
+copyArrayIntersection :: MArray a e m => a (Int, Int) e -> a (Int, Int) e -> m ()
+copyArrayIntersection dest src = do
+ ((ds1, ds2), (de1, de2)) <- getBounds dest
+ ((ss1, ss2), (se1, se2)) <- getBounds src
+ let bounds = ((max ss1 ds1, max ss2 ds2), (min se1 de1, min se2 de2))
+
+ forM_ (range bounds) $ \idx ->
+ readArray src idx >>= writeArray dest idx