1#if __GLASGOW_HASKELL__ < 802 2{-# OPTIONS_GHC -Wno-redundant-constraints #-} 3#endif 4module System.Console.Haskeline.Backend.Terminfo( 5 Draw(), 6 runTerminfoDraw 7 ) 8 where 9 10import System.Console.Terminfo 11import Control.Monad 12import Control.Monad.Catch 13import Data.List(foldl') 14import System.IO 15import qualified Control.Exception as Exception 16import Data.Maybe (fromMaybe, mapMaybe) 17import qualified Data.IntMap as Map 18 19import System.Console.Haskeline.Monads as Monads 20import System.Console.Haskeline.LineState 21import System.Console.Haskeline.Term 22import System.Console.Haskeline.Backend.Posix 23import System.Console.Haskeline.Backend.WCWidth 24import System.Console.Haskeline.Key 25 26import qualified Control.Monad.Trans.Writer as Writer 27 28---------------------------------------------------------------- 29-- Low-level terminal output 30 31-- | Keep track of all of the output capabilities we can use. 32-- 33-- We'll be frequently using the (automatic) 'Monoid' instance for 34-- @Actions -> TermOutput@. 35data Actions = Actions {leftA, rightA, upA :: Int -> TermOutput, 36 clearToLineEnd :: TermOutput, 37 nl, cr :: TermOutput, 38 bellAudible,bellVisual :: TermOutput, 39 clearAllA :: LinesAffected -> TermOutput, 40 wrapLine :: TermOutput} 41 42getActions :: Capability Actions 43getActions = do 44 -- This capability is not strictly necessary, but is very widely supported 45 -- and assuming it makes for a much simpler implementation of printText. 46 autoRightMargin >>= guard 47 48 leftA' <- moveLeft 49 rightA' <- moveRight 50 upA' <- moveUp 51 clearToLineEnd' <- clearEOL 52 clearAll' <- clearScreen 53 nl' <- newline 54 cr' <- carriageReturn 55 -- Don't require the bell capabilities 56 bellAudible' <- bell `mplus` return mempty 57 bellVisual' <- visualBell `mplus` return mempty 58 wrapLine' <- getWrapLine (leftA' 1) 59 return Actions{leftA = leftA', rightA = rightA',upA = upA', 60 clearToLineEnd = clearToLineEnd', nl = nl',cr = cr', 61 bellAudible = bellAudible', bellVisual = bellVisual', 62 clearAllA = clearAll', 63 wrapLine = wrapLine'} 64 65-- If the wraparound glitch is in effect, force a wrap by printing a space. 66-- Otherwise, it'll wrap automatically. 67getWrapLine :: TermOutput -> Capability TermOutput 68getWrapLine left1 = (do 69 wraparoundGlitch >>= guard 70 return (termText " " <#> left1) 71 ) `mplus` return mempty 72 73---------------------------------------------------------------- 74-- The Draw monad 75 76-- denote in modular arithmetic; 77-- in particular, 0 <= termCol < width 78data TermPos = TermPos {termRow,termCol :: !Int} 79 deriving Show 80 81initTermPos :: TermPos 82initTermPos = TermPos {termRow = 0, termCol = 0} 83 84data TermRows = TermRows { 85 rowLengths :: !(Map.IntMap Int), 86 -- ^ The length of each nonempty row 87 lastRow :: !Int 88 -- ^ The last nonempty row, or zero if the entire line 89 -- is empty. Note that when the cursor wraps to the first 90 -- column of the next line, termRow > lastRow. 91 } 92 deriving Show 93 94initTermRows :: TermRows 95initTermRows = TermRows {rowLengths = Map.empty, lastRow=0} 96 97setRow :: Int -> Int -> TermRows -> TermRows 98setRow r len rs = TermRows {rowLengths = Map.insert r len (rowLengths rs), 99 lastRow=r} 100 101lookupCells :: TermRows -> Int -> Int 102lookupCells (TermRows rc _) r = Map.findWithDefault 0 r rc 103 104newtype Draw m a = Draw {unDraw :: (ReaderT Actions 105 (ReaderT Terminal 106 (StateT TermRows 107 (StateT TermPos 108 (PosixT m))))) a} 109 deriving (Functor, Applicative, Monad, MonadIO, 110 MonadMask, MonadThrow, MonadCatch, 111 MonadReader Actions, MonadReader Terminal, MonadState TermPos, 112 MonadState TermRows, MonadReader Handles) 113 114instance MonadTrans Draw where 115 lift = Draw . lift . lift . lift . lift . lift 116 117evalDraw :: forall m . (MonadReader Layout m, CommandMonad m) => Terminal -> Actions -> EvalTerm (PosixT m) 118evalDraw term actions = EvalTerm eval liftE 119 where 120 liftE = Draw . lift . lift . lift . lift 121 eval = evalStateT' initTermPos 122 . evalStateT' initTermRows 123 . runReaderT' term 124 . runReaderT' actions 125 . unDraw 126 127 128runTerminfoDraw :: Handles -> MaybeT IO RunTerm 129runTerminfoDraw h = do 130 mterm <- liftIO $ Exception.try setupTermFromEnv 131 case mterm of 132 Left (_::SetupTermError) -> mzero 133 Right term -> do 134 actions <- MaybeT $ return $ getCapability term getActions 135 liftIO $ posixRunTerm h (posixLayouts h ++ [tinfoLayout term]) 136 (terminfoKeys term) 137 (wrapKeypad (ehOut h) term) 138 (evalDraw term actions) 139 140-- If the keypad on/off capabilities are defined, wrap the computation with them. 141wrapKeypad :: (MonadIO m, MonadMask m) => Handle -> Terminal -> m a -> m a 142wrapKeypad h term f = (maybeOutput keypadOn >> f) 143 `finally` maybeOutput keypadOff 144 where 145 maybeOutput = liftIO . hRunTermOutput h term . 146 fromMaybe mempty . getCapability term 147 148tinfoLayout :: Terminal -> IO (Maybe Layout) 149tinfoLayout term = return $ getCapability term $ do 150 c <- termColumns 151 r <- termLines 152 return Layout {height=r,width=c} 153 154terminfoKeys :: Terminal -> [(String,Key)] 155terminfoKeys term = mapMaybe getSequence keyCapabilities 156 where 157 getSequence (cap,x) = do 158 keys <- getCapability term cap 159 return (keys,x) 160 keyCapabilities = 161 [(keyLeft, simpleKey LeftKey) 162 ,(keyRight, simpleKey RightKey) 163 ,(keyUp, simpleKey UpKey) 164 ,(keyDown, simpleKey DownKey) 165 ,(keyBackspace, simpleKey Backspace) 166 ,(keyDeleteChar, simpleKey Delete) 167 ,(keyHome, simpleKey Home) 168 ,(keyEnd, simpleKey End) 169 ,(keyPageDown, simpleKey PageDown) 170 ,(keyPageUp, simpleKey PageUp) 171 ,(keyEnter, simpleKey $ KeyChar '\n') 172 ] 173 174 175 176---------------------------------------------------------------- 177-- Terminal output actions 178-- 179-- We combine all of the drawing commands into one big TermAction, 180-- via a writer monad, and then output them all at once. 181-- This prevents flicker, i.e., the cursor appearing briefly 182-- in an intermediate position. 183 184type TermAction = Actions -> TermOutput 185 186type ActionT = Writer.WriterT TermAction 187 188type ActionM a = forall m . (MonadReader Layout m, MonadIO m) => ActionT (Draw m) a 189 190runActionT :: MonadIO m => ActionT (Draw m) a -> Draw m a 191runActionT m = do 192 (x,action) <- Writer.runWriterT m 193 toutput <- asks action 194 term <- ask 195 ttyh <- liftM ehOut ask 196 liftIO $ hRunTermOutput ttyh term toutput 197 return x 198 199output :: TermAction -> ActionM () 200output t = Writer.tell t -- NB: explicit argument enables build with ghc-6.12.3 201 -- (Probably related to the monomorphism restriction; 202 -- see GHC ticket #1749). 203 204outputText :: String -> ActionM () 205outputText = output . const . termText 206 207left,right,up :: Int -> TermAction 208left = flip leftA 209right = flip rightA 210up = flip upA 211 212clearAll :: LinesAffected -> TermAction 213clearAll = flip clearAllA 214 215mreplicate :: Monoid m => Int -> m -> m 216mreplicate n m 217 | n <= 0 = mempty 218 | otherwise = m `mappend` mreplicate (n-1) m 219 220-- We don't need to bother encoding the spaces. 221spaces :: Int -> TermAction 222spaces 0 = mempty 223spaces 1 = const $ termText " " -- share when possible 224spaces n = const $ termText $ replicate n ' ' 225 226 227changePos :: TermPos -> TermPos -> TermAction 228changePos TermPos {termRow=r1, termCol=c1} TermPos {termRow=r2, termCol=c2} 229 | r1 == r2 = if c1 < c2 then right (c2-c1) else left (c1-c2) 230 | r1 > r2 = cr <#> up (r1-r2) <#> right c2 231 | otherwise = cr <#> mreplicate (r2-r1) nl <#> right c2 232 233moveToPos :: TermPos -> ActionM () 234moveToPos p = do 235 oldP <- get 236 put p 237 output $ changePos oldP p 238 239moveRelative :: Int -> ActionM () 240moveRelative n = liftM3 (advancePos n) ask get get 241 >>= moveToPos 242 243-- Note that these move by a certain number of cells, not graphemes. 244changeRight, changeLeft :: Int -> ActionM () 245changeRight n | n <= 0 = return () 246 | otherwise = moveRelative n 247changeLeft n | n <= 0 = return () 248 | otherwise = moveRelative (negate n) 249 250 251-- TODO: this could be more efficient by only checking intermediate rows. 252-- TODO: this is worth handling with QuickCheck. 253advancePos :: Int -> Layout -> TermRows -> TermPos -> TermPos 254advancePos k Layout {width=w} rs p = indexToPos $ k + posIndex 255 where 256 posIndex = termCol p + sum' (map (lookupCells rs) 257 [0..termRow p-1]) 258 indexToPos n = loopFindRow 0 n 259 loopFindRow r m = r `seq` m `seq` let 260 thisRowSize = lookupCells rs r 261 in if m < thisRowSize 262 || (m == thisRowSize && m < w) 263 || thisRowSize <= 0 -- This shouldn't happen in practice, 264 -- but double-check to prevent an infinite loop 265 then TermPos {termRow=r, termCol=m} 266 else loopFindRow (r+1) (m-thisRowSize) 267 268sum' :: [Int] -> Int 269sum' = foldl' (+) 0 270 271---------------------------------------------------------------- 272-- Text printing actions 273 274printText :: [Grapheme] -> ActionM () 275printText [] = return () 276printText gs = do 277 -- First, get the monadic parameters: 278 w <- asks width 279 TermPos {termRow=r, termCol=c} <- get 280 -- Now, split off as much as will fit on the rest of this row: 281 let (thisLine,rest,thisWidth) = splitAtWidth (w-c) gs 282 let lineWidth = c + thisWidth 283 -- Finally, actually print out the relevant text. 284 outputText (graphemesToString thisLine) 285 modify $ setRow r lineWidth 286 if null rest && lineWidth < w 287 then -- everything fits on one line without wrapping 288 put TermPos {termRow=r, termCol=lineWidth} 289 else do -- Must wrap to the next line 290 put TermPos {termRow=r+1,termCol=0} 291 output $ if lineWidth == w then wrapLine else spaces (w-lineWidth) 292 printText rest 293 294---------------------------------------------------------------- 295-- High-level Term implementation 296 297drawLineDiffT :: LineChars -> LineChars -> ActionM () 298drawLineDiffT (xs1,ys1) (xs2,ys2) = case matchInit xs1 xs2 of 299 ([],[]) | ys1 == ys2 -> return () 300 (xs1',[]) | xs1' ++ ys1 == ys2 -> changeLeft (gsWidth xs1') 301 ([],xs2') | ys1 == xs2' ++ ys2 -> changeRight (gsWidth xs2') 302 (xs1',xs2') -> do 303 oldRS <- get 304 changeLeft (gsWidth xs1') 305 printText xs2' 306 p <- get 307 printText ys2 308 clearDeadText oldRS 309 moveToPos p 310 311-- The number of nonempty lines after the current row position. 312getLinesLeft :: ActionM Int 313getLinesLeft = do 314 p <- get 315 rc <- get 316 return $ max 0 (lastRow rc - termRow p) 317 318clearDeadText :: TermRows -> ActionM () 319clearDeadText oldRS = do 320 TermPos {termRow = r, termCol = c} <- get 321 let extraRows = lastRow oldRS - r 322 if extraRows < 0 323 || (extraRows == 0 && lookupCells oldRS r <= c) 324 then return () 325 else do 326 modify $ setRow r c 327 when (extraRows /= 0) 328 $ put TermPos {termRow = r + extraRows, termCol=0} 329 output $ clearToLineEnd <#> mreplicate extraRows (nl <#> clearToLineEnd) 330 331clearLayoutT :: ActionM () 332clearLayoutT = do 333 h <- asks height 334 output (clearAll h) 335 put initTermPos 336 337moveToNextLineT :: ActionM () 338moveToNextLineT = do 339 lleft <- getLinesLeft 340 output $ mreplicate (lleft+1) nl 341 put initTermPos 342 put initTermRows 343 344repositionT :: Layout -> LineChars -> ActionM () 345repositionT _ s = do 346 oldPos <- get 347 l <- getLinesLeft 348 output $ cr <#> mreplicate l nl 349 <#> mreplicate (l + termRow oldPos) (clearToLineEnd <#> up 1) 350 put initTermPos 351 put initTermRows 352 drawLineDiffT ([],[]) s 353 354instance (MonadIO m, MonadMask m, MonadReader Layout m) => Term (Draw m) where 355 drawLineDiff xs ys = runActionT $ drawLineDiffT xs ys 356 reposition layout lc = runActionT $ repositionT layout lc 357 358 printLines = mapM_ $ \line -> runActionT $ do 359 outputText line 360 output nl 361 clearLayout = runActionT clearLayoutT 362 moveToNextLine _ = runActionT moveToNextLineT 363 ringBell True = runActionT $ output bellAudible 364 ringBell False = runActionT $ output bellVisual 365