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