haskeline-0.7.5.0: System/Console/Haskeline/Backend/Terminfo.hs
#if __GLASGOW_HASKELL__ <= 802
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
#endif
module System.Console.Haskeline.Backend.Terminfo(
Draw(),
runTerminfoDraw
)
where
import System.Console.Terminfo
import Control.Monad
import Data.List(foldl')
import System.IO
import qualified Control.Exception as Exception
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.IntMap as Map
import System.Console.Haskeline.Monads as Monads
import System.Console.Haskeline.LineState
import System.Console.Haskeline.Term
import System.Console.Haskeline.Backend.Posix
import System.Console.Haskeline.Backend.WCWidth
import System.Console.Haskeline.Key
import qualified Control.Monad.Trans.Writer as Writer
----------------------------------------------------------------
-- Low-level terminal output
-- | Keep track of all of the output capabilities we can use.
--
-- We'll be frequently using the (automatic) 'Monoid' instance for
-- @Actions -> TermOutput@.
data Actions = Actions {leftA, rightA, upA :: Int -> TermOutput,
clearToLineEnd :: TermOutput,
nl, cr :: TermOutput,
bellAudible,bellVisual :: TermOutput,
clearAllA :: LinesAffected -> TermOutput,
wrapLine :: TermOutput}
getActions :: Capability Actions
getActions = do
-- This capability is not strictly necessary, but is very widely supported
-- and assuming it makes for a much simpler implementation of printText.
autoRightMargin >>= guard
leftA' <- moveLeft
rightA' <- moveRight
upA' <- moveUp
clearToLineEnd' <- clearEOL
clearAll' <- clearScreen
nl' <- newline
cr' <- carriageReturn
-- Don't require the bell capabilities
bellAudible' <- bell `mplus` return mempty
bellVisual' <- visualBell `mplus` return mempty
wrapLine' <- getWrapLine (leftA' 1)
return Actions{leftA = leftA', rightA = rightA',upA = upA',
clearToLineEnd = clearToLineEnd', nl = nl',cr = cr',
bellAudible = bellAudible', bellVisual = bellVisual',
clearAllA = clearAll',
wrapLine = wrapLine'}
-- If the wraparound glitch is in effect, force a wrap by printing a space.
-- Otherwise, it'll wrap automatically.
getWrapLine :: TermOutput -> Capability TermOutput
getWrapLine left1 = (do
wraparoundGlitch >>= guard
return (termText " " <#> left1)
) `mplus` return mempty
----------------------------------------------------------------
-- The Draw monad
-- denote in modular arithmetic;
-- in particular, 0 <= termCol < width
data TermPos = TermPos {termRow,termCol :: !Int}
deriving Show
initTermPos :: TermPos
initTermPos = TermPos {termRow = 0, termCol = 0}
data TermRows = TermRows {
rowLengths :: !(Map.IntMap Int),
-- ^ The length of each nonempty row
lastRow :: !Int
-- ^ The last nonempty row, or zero if the entire line
-- is empty. Note that when the cursor wraps to the first
-- column of the next line, termRow > lastRow.
}
deriving Show
initTermRows :: TermRows
initTermRows = TermRows {rowLengths = Map.empty, lastRow=0}
setRow :: Int -> Int -> TermRows -> TermRows
setRow r len rs = TermRows {rowLengths = Map.insert r len (rowLengths rs),
lastRow=r}
lookupCells :: TermRows -> Int -> Int
lookupCells (TermRows rc _) r = Map.findWithDefault 0 r rc
newtype Draw m a = Draw {unDraw :: (ReaderT Actions
(ReaderT Terminal
(StateT TermRows
(StateT TermPos
(PosixT m))))) a}
deriving (Functor, Applicative, Monad, MonadIO, MonadException,
MonadReader Actions, MonadReader Terminal, MonadState TermPos,
MonadState TermRows, MonadReader Handles)
instance MonadTrans Draw where
lift = Draw . lift . lift . lift . lift . lift
evalDraw :: forall m . (MonadReader Layout m, CommandMonad m) => Terminal -> Actions -> EvalTerm (PosixT m)
evalDraw term actions = EvalTerm eval liftE
where
liftE = Draw . lift . lift . lift . lift
eval = evalStateT' initTermPos
. evalStateT' initTermRows
. runReaderT' term
. runReaderT' actions
. unDraw
runTerminfoDraw :: Handles -> MaybeT IO RunTerm
runTerminfoDraw h = do
mterm <- liftIO $ Exception.try setupTermFromEnv
case mterm of
Left (_::SetupTermError) -> mzero
Right term -> do
actions <- MaybeT $ return $ getCapability term getActions
liftIO $ posixRunTerm h (posixLayouts h ++ [tinfoLayout term])
(terminfoKeys term)
(wrapKeypad (ehOut h) term)
(evalDraw term actions)
-- If the keypad on/off capabilities are defined, wrap the computation with them.
wrapKeypad :: MonadException m => Handle -> Terminal -> m a -> m a
wrapKeypad h term f = (maybeOutput keypadOn >> f)
`finally` maybeOutput keypadOff
where
maybeOutput = liftIO . hRunTermOutput h term .
fromMaybe mempty . getCapability term
tinfoLayout :: Terminal -> IO (Maybe Layout)
tinfoLayout term = return $ getCapability term $ do
c <- termColumns
r <- termLines
return Layout {height=r,width=c}
terminfoKeys :: Terminal -> [(String,Key)]
terminfoKeys term = mapMaybe getSequence keyCapabilities
where
getSequence (cap,x) = do
keys <- getCapability term cap
return (keys,x)
keyCapabilities =
[(keyLeft, simpleKey LeftKey)
,(keyRight, simpleKey RightKey)
,(keyUp, simpleKey UpKey)
,(keyDown, simpleKey DownKey)
,(keyBackspace, simpleKey Backspace)
,(keyDeleteChar, simpleKey Delete)
,(keyHome, simpleKey Home)
,(keyEnd, simpleKey End)
,(keyPageDown, simpleKey PageDown)
,(keyPageUp, simpleKey PageUp)
,(keyEnter, simpleKey $ KeyChar '\n')
]
----------------------------------------------------------------
-- Terminal output actions
--
-- We combine all of the drawing commands into one big TermAction,
-- via a writer monad, and then output them all at once.
-- This prevents flicker, i.e., the cursor appearing briefly
-- in an intermediate position.
type TermAction = Actions -> TermOutput
type ActionT = Writer.WriterT TermAction
type ActionM a = forall m . (MonadReader Layout m, MonadIO m) => ActionT (Draw m) a
runActionT :: MonadIO m => ActionT (Draw m) a -> Draw m a
runActionT m = do
(x,action) <- Writer.runWriterT m
toutput <- asks action
term <- ask
ttyh <- liftM ehOut ask
liftIO $ hRunTermOutput ttyh term toutput
return x
output :: TermAction -> ActionM ()
output t = Writer.tell t -- NB: explicit argument enables build with ghc-6.12.3
-- (Probably related to the monomorphism restriction;
-- see GHC ticket #1749).
outputText :: String -> ActionM ()
outputText = output . const . termText
left,right,up :: Int -> TermAction
left = flip leftA
right = flip rightA
up = flip upA
clearAll :: LinesAffected -> TermAction
clearAll = flip clearAllA
mreplicate :: Monoid m => Int -> m -> m
mreplicate n m
| n <= 0 = mempty
| otherwise = m `mappend` mreplicate (n-1) m
-- We don't need to bother encoding the spaces.
spaces :: Int -> TermAction
spaces 0 = mempty
spaces 1 = const $ termText " " -- share when possible
spaces n = const $ termText $ replicate n ' '
changePos :: TermPos -> TermPos -> TermAction
changePos TermPos {termRow=r1, termCol=c1} TermPos {termRow=r2, termCol=c2}
| r1 == r2 = if c1 < c2 then right (c2-c1) else left (c1-c2)
| r1 > r2 = cr <#> up (r1-r2) <#> right c2
| otherwise = cr <#> mreplicate (r2-r1) nl <#> right c2
moveToPos :: TermPos -> ActionM ()
moveToPos p = do
oldP <- get
put p
output $ changePos oldP p
moveRelative :: Int -> ActionM ()
moveRelative n = liftM3 (advancePos n) ask get get
>>= moveToPos
-- Note that these move by a certain number of cells, not graphemes.
changeRight, changeLeft :: Int -> ActionM ()
changeRight n | n <= 0 = return ()
| otherwise = moveRelative n
changeLeft n | n <= 0 = return ()
| otherwise = moveRelative (negate n)
-- TODO: this could be more efficient by only checking intermediate rows.
-- TODO: this is worth handling with QuickCheck.
advancePos :: Int -> Layout -> TermRows -> TermPos -> TermPos
advancePos k Layout {width=w} rs p = indexToPos $ k + posIndex
where
posIndex = termCol p + sum' (map (lookupCells rs)
[0..termRow p-1])
indexToPos n = loopFindRow 0 n
loopFindRow r m = r `seq` m `seq` let
thisRowSize = lookupCells rs r
in if m < thisRowSize
|| (m == thisRowSize && m < w)
|| thisRowSize <= 0 -- This shouldn't happen in practice,
-- but double-check to prevent an infinite loop
then TermPos {termRow=r, termCol=m}
else loopFindRow (r+1) (m-thisRowSize)
sum' :: [Int] -> Int
sum' = foldl' (+) 0
----------------------------------------------------------------
-- Text printing actions
printText :: [Grapheme] -> ActionM ()
printText [] = return ()
printText gs = do
-- First, get the monadic parameters:
w <- asks width
TermPos {termRow=r, termCol=c} <- get
-- Now, split off as much as will fit on the rest of this row:
let (thisLine,rest,thisWidth) = splitAtWidth (w-c) gs
let lineWidth = c + thisWidth
-- Finally, actually print out the relevant text.
outputText (graphemesToString thisLine)
modify $ setRow r lineWidth
if null rest && lineWidth < w
then -- everything fits on one line without wrapping
put TermPos {termRow=r, termCol=lineWidth}
else do -- Must wrap to the next line
put TermPos {termRow=r+1,termCol=0}
output $ if lineWidth == w then wrapLine else spaces (w-lineWidth)
printText rest
----------------------------------------------------------------
-- High-level Term implementation
drawLineDiffT :: LineChars -> LineChars -> ActionM ()
drawLineDiffT (xs1,ys1) (xs2,ys2) = case matchInit xs1 xs2 of
([],[]) | ys1 == ys2 -> return ()
(xs1',[]) | xs1' ++ ys1 == ys2 -> changeLeft (gsWidth xs1')
([],xs2') | ys1 == xs2' ++ ys2 -> changeRight (gsWidth xs2')
(xs1',xs2') -> do
oldRS <- get
changeLeft (gsWidth xs1')
printText xs2'
p <- get
printText ys2
clearDeadText oldRS
moveToPos p
-- The number of nonempty lines after the current row position.
getLinesLeft :: ActionM Int
getLinesLeft = do
p <- get
rc <- get
return $ max 0 (lastRow rc - termRow p)
clearDeadText :: TermRows -> ActionM ()
clearDeadText oldRS = do
TermPos {termRow = r, termCol = c} <- get
let extraRows = lastRow oldRS - r
if extraRows < 0
|| (extraRows == 0 && lookupCells oldRS r <= c)
then return ()
else do
modify $ setRow r c
when (extraRows /= 0)
$ put TermPos {termRow = r + extraRows, termCol=0}
output $ clearToLineEnd <#> mreplicate extraRows (nl <#> clearToLineEnd)
clearLayoutT :: ActionM ()
clearLayoutT = do
h <- asks height
output (clearAll h)
put initTermPos
moveToNextLineT :: ActionM ()
moveToNextLineT = do
lleft <- getLinesLeft
output $ mreplicate (lleft+1) nl
put initTermPos
put initTermRows
repositionT :: Layout -> LineChars -> ActionM ()
repositionT _ s = do
oldPos <- get
l <- getLinesLeft
output $ cr <#> mreplicate l nl
<#> mreplicate (l + termRow oldPos) (clearToLineEnd <#> up 1)
put initTermPos
put initTermRows
drawLineDiffT ([],[]) s
instance (MonadException m, MonadReader Layout m) => Term (Draw m) where
drawLineDiff xs ys = runActionT $ drawLineDiffT xs ys
reposition layout lc = runActionT $ repositionT layout lc
printLines = mapM_ $ \line -> runActionT $ do
outputText line
output nl
clearLayout = runActionT clearLayoutT
moveToNextLine _ = runActionT moveToNextLineT
ringBell True = runActionT $ output bellAudible
ringBell False = runActionT $ output bellVisual