1-- Copyright Corey O'Connor
2-- General philosophy is: MonadIO is for equations exposed to clients.
4{-# LANGUAGE BangPatterns #-}
5{-# LANGUAGE RankNTypes #-}
6{-# LANGUAGE ExistentialQuantification #-}
8{-# LANGUAGE FlexibleContexts #-}
10#ifndef MIN_VERSION_base
11#defined MIN_VERSION_base(x,y,z) 1
14module Graphics.Vty.Output.Interface
17import Graphics.Vty.Prelude
19import Graphics.Vty.Picture
20import Graphics.Vty.PictureToSpans
21import Graphics.Vty.Span
23import Graphics.Vty.DisplayAttributes
25import Blaze.ByteString.Builder (Write, writeToByteString)
26import Blaze.ByteString.Builder.ByteString (writeByteString)
28import Control.Monad.Trans
30import qualified Data.ByteString as BS
32import qualified Data.Text.Encoding as T
33import qualified Data.Text.Lazy as TL
34import qualified Data.Vector as Vector
36#if !(MIN_VERSION_base(4,8,0))
37import Data.Monoid (mempty, mappend)
40-- | Modal terminal features that can be enabled and disabled.
43 deriving (Eq, Read, Show)
46 { -- | Text identifier for the output device. Used for debugging.
48 , releaseTerminal :: forall m. MonadIO m => m ()
49 -- | Clear the display and initialize the terminal to some initial display state.
51 -- The expectation of a program is that the display starts in some initial state.
52 -- The initial state would consist of fixed values:
54 -- - cursor at top left
55 -- - UTF-8 character encoding
56 -- - drawing characteristics are the default
58 -- The abstract operation I think all these behaviors are instances of is reserving exclusive
59 -- access to a display such that:
61 -- - The previous state cannot be determined
62 -- - When exclusive access to a display is released the display returns to the previous state.
63 , reserveDisplay :: forall m. MonadIO m => m ()
64 -- | Return the display to the state before `reserveDisplay`
65 -- If no previous state then set the display state to the initial state.
66 , releaseDisplay :: forall m. MonadIO m => m ()
67 -- | Returns the current display bounds.
68 , displayBounds :: forall m. MonadIO m => m DisplayRegion
69 -- | Output the byte string to the terminal device.
70 , outputByteBuffer :: BS.ByteString -> IO ()
71 -- | Maximum number of colors supported by the context.
72 , contextColorCount :: Int
73 -- | if the cursor can be shown / hidden
74 , supportsCursorVisibility :: Bool
75 -- | Indicates support for terminal modes for this output device
76 , supportsMode :: Mode -> Bool
77 -- | Enables or disables a mode (does nothing if the mode is unsupported)
78 , setMode :: forall m. MonadIO m => Mode -> Bool -> m ()
79 -- | Returns whether a mode is enabled
80 , getModeStatus :: forall m. MonadIO m => Mode -> m Bool
81 , assumedStateRef :: IORef AssumedState
82 -- | Acquire display access to the given region of the display.
83 -- Currently all regions have the upper left corner of (0,0) and the lower right corner at
84 -- (max displayWidth providedWidth, max displayHeight providedHeight)
85 , mkDisplayContext :: forall m. MonadIO m => Output -> DisplayRegion -> m DisplayContext
86 -- | Ring the terminal bell if supported.
87 , ringTerminalBell :: forall m. MonadIO m => m ()
88 -- | Returns whether the terminal has an audio bell feature.
89 , supportsBell :: forall m. MonadIO m => m Bool
92displayContext :: MonadIO m => Output -> DisplayRegion -> m DisplayContext
93displayContext t = liftIO . mkDisplayContext t t
95data AssumedState = AssumedState
96 { prevFattr :: Maybe FixedAttr
97 , prevOutputOps :: Maybe DisplayOps
100initialAssumedState :: AssumedState
101initialAssumedState = AssumedState Nothing Nothing
103data DisplayContext = DisplayContext
104 { contextDevice :: Output
105 -- | Provide the bounds of the display context.
106 , contextRegion :: DisplayRegion
107 -- | sets the output position to the specified row and column. Where the number of bytes
108 -- required for the control codes can be specified seperate from the actual byte sequence.
109 , writeMoveCursor :: Int -> Int -> Write
110 , writeShowCursor :: Write
111 , writeHideCursor :: Write
112 -- | Assure the specified output attributes will be applied to all the following text until the
113 -- next output attribute change. Where the number of bytes required for the control codes can
114 -- be specified seperate from the actual byte sequence. The required number of bytes must be
115 -- at least the maximum number of bytes required by any attribute changes. The serialization
116 -- equations must provide the ptr to the next byte to be specified in the output buffer.
118 -- The currently applied display attributes are provided as well. The Attr data type can
119 -- specify the style or color should not be changed from the currently applied display
120 -- attributes. In order to support this the currently applied display attributes are required.
121 -- In addition it may be possible to optimize the state changes based off the currently applied
122 -- display attributes.
123 , writeSetAttr :: FixedAttr -> Attr -> DisplayAttrDiff -> Write
124 -- | Reset the display attributes to the default display attributes
125 , writeDefaultAttr :: Write
126 , writeRowEnd :: Write
127 -- | See `Graphics.Vty.Output.XTermColor.inlineHack`
128 , inlineHack :: IO ()
131-- | All terminals serialize UTF8 text to the terminal device exactly as serialized in memory.
132writeUtf8Text :: BS.ByteString -> Write
133writeUtf8Text = writeByteString
135-- | Displays the given `Picture`.
137-- 0. The image is cropped to the display size.
139-- 1. Converted into a sequence of attribute changes and text spans.
141-- 2. The cursor is hidden.
143-- 3. Serialized to the display.
145-- 4. The cursor is then shown and positioned or kept hidden.
147-- todo: specify possible IO exceptions.
148-- abstract from IO monad to a MonadIO instance.
149outputPicture :: MonadIO m => DisplayContext -> Picture -> m ()
150outputPicture dc pic = liftIO $ do
151 as <- readIORef (assumedStateRef $ contextDevice dc)
152 let manipCursor = supportsCursorVisibility (contextDevice dc)
154 ops = displayOpsForPic pic r
155 initialAttr = FixedAttr defaultStyleMask Nothing Nothing
156 -- Diff the previous output against the requested output. Differences are currently on a per-row
158 -- \todo handle resizes that crop the dominate directions better.
159 diffs :: [Bool] = case prevOutputOps as of
160 Nothing -> replicate (fromEnum $ regionHeight $ effectedRegion ops) True
161 Just previousOps -> if effectedRegion previousOps /= effectedRegion ops
162 then replicate (displayOpsRows ops) True
163 else zipWith (/=) (Vector.toList previousOps)
165 -- build the Write corresponding to the output image
166 out = (if manipCursor then writeHideCursor dc else mempty)
167 `mappend` writeOutputOps dc initialAttr diffs ops
169 (let (w,h) = contextRegion dc
170 clampX = max 0 . min (w-1)
171 clampY = max 0 . min (h-1) in
172 case picCursor pic of
173 _ | not manipCursor -> mempty
175 AbsoluteCursor x y ->
176 writeShowCursor dc `mappend`
177 writeMoveCursor dc (clampX x) (clampY y)
179 let m = cursorOutputMap ops $ picCursor pic
180 (ox, oy) = charToOutputPos m (x,y)
181 in writeShowCursor dc `mappend`
182 writeMoveCursor dc (clampX ox) (clampY oy)
184 -- ... then serialize
185 outputByteBuffer (contextDevice dc) (writeToByteString out)
186 -- Cache the output spans.
187 let as' = as { prevOutputOps = Just ops }
188 writeIORef (assumedStateRef $ contextDevice dc) as'
190writeOutputOps :: DisplayContext -> FixedAttr -> [Bool] -> DisplayOps -> Write
191writeOutputOps dc initialAttr diffs ops =
192 let (_, out, _) = Vector.foldl' writeOutputOps'
197 writeOutputOps' (y, out, True : diffs') spanOps
198 = let spanOut = writeSpanOps dc y initialAttr spanOps
199 out' = out `mappend` spanOut
200 in (y+1, out', diffs')
201 writeOutputOps' (y, out, False : diffs') _spanOps
202 = (y + 1, out, diffs')
203 writeOutputOps' (_y, _out, []) _spanOps
204 = error "vty - output spans without a corresponding diff."
206writeSpanOps :: DisplayContext -> Int -> FixedAttr -> SpanOps -> Write
207writeSpanOps dc y initialAttr spanOps =
208 -- The first operation is to set the cursor to the start of the row
209 let start = writeMoveCursor dc 0 y `mappend` writeDefaultAttr dc
210 -- then the span ops are serialized in the order specified
211 in fst $ Vector.foldl' (\(out, fattr) op -> case writeSpanOp dc op fattr of
212 (opOut, fattr') -> (out `mappend` opOut, fattr')
217writeSpanOp :: DisplayContext -> SpanOp -> FixedAttr -> (Write, FixedAttr)
218writeSpanOp dc (TextSpan attr _ _ str) fattr =
219 let attr' = limitAttrForDisplay (contextDevice dc) attr
220 fattr' = fixDisplayAttr fattr attr'
221 diffs = displayAttrDiffs fattr fattr'
222 out = writeSetAttr dc fattr attr' diffs
223 `mappend` writeUtf8Text (T.encodeUtf8 $ TL.toStrict str)
225writeSpanOp _dc (Skip _) _fattr = error "writeSpanOp for Skip"
226writeSpanOp dc (RowEnd _) fattr = (writeDefaultAttr dc `mappend` writeRowEnd dc, fattr)
228-- | The cursor position is given in X,Y character offsets. Due to multi-column characters this
229-- needs to be translated to column, row positions.
230data CursorOutputMap = CursorOutputMap
231 { charToOutputPos :: (Int, Int) -> (Int, Int)
234cursorOutputMap :: DisplayOps -> Cursor -> CursorOutputMap
235cursorOutputMap spanOps _cursor = CursorOutputMap
236 { charToOutputPos = \(cx, cy) -> (cursorColumnOffset spanOps cx cy, cy)
239cursorColumnOffset :: DisplayOps -> Int -> Int -> Int
240cursorColumnOffset ops cx cy =
241 let cursorRowOps = Vector.unsafeIndex ops (fromEnum cy)
243 = Vector.foldl' ( \(d, currentCx, done) op ->
244 if done then (d, currentCx, done) else case spanOpHasWidth op of
245 Nothing -> (d, currentCx, False)
246 Just (cw, ow) -> case compare cx (currentCx + cw) of
255 LT -> ( d + columnsToCharOffset (cx - currentCx) op
264-- | Not all terminals support all display attributes. This filters a display attribute to what the
265-- given terminal can display.
266limitAttrForDisplay :: Output -> Attr -> Attr
267limitAttrForDisplay t attr
268 = attr { attrForeColor = clampColor $ attrForeColor attr
269 , attrBackColor = clampColor $ attrBackColor attr
272 clampColor Default = Default
273 clampColor KeepCurrent = KeepCurrent
274 clampColor (SetTo c) = clampColor' c
275 clampColor' (ISOColor v)
276 | contextColorCount t < 8 = Default
277 | contextColorCount t < 16 && v >= 8 = SetTo $ ISOColor (v - 8)
278 | otherwise = SetTo $ ISOColor v
279 clampColor' (Color240 v)
280 -- TODO: Choose closes ISO color?
281 | contextColorCount t < 8 = Default
282 | contextColorCount t < 16 = Default
283 | contextColorCount t <= 256 = SetTo $ Color240 v
285 = let p :: Double = fromIntegral v / 240.0
286 v' = floor $ p * (fromIntegral $ contextColorCount t)
287 in SetTo $ Color240 v'
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list