-- Copyright 2009-2011 Corey O'Connor {-# LANGUAGE BangPatterns #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE FlexibleContexts #-} module Graphics.Vty.Terminal.Generic ( module Graphics.Vty.Terminal.Generic , OutputBuffer ) where import Data.Marshalling import Graphics.Vty.Picture import Graphics.Vty.Span import Graphics.Vty.DisplayRegion import Graphics.Vty.DisplayAttributes import Control.Monad ( liftM ) import Control.Monad.Trans import qualified Data.ByteString.Internal as BSCore import Data.IORef import Data.String.UTF8 hiding ( foldl ) import qualified Data.Vector as Vector import System.IO data TerminalHandle where TerminalHandle :: Terminal t => t -> IORef TerminalState -> TerminalHandle state_ref :: TerminalHandle -> IORef TerminalState state_ref (TerminalHandle _ s_ref) = s_ref new_terminal_handle :: forall m t. ( MonadIO m, Terminal t ) => t -> m TerminalHandle new_terminal_handle t = do s_ref <- liftIO $ newIORef initial_terminal_state return $ TerminalHandle t s_ref data TerminalState = TerminalState { -- | The current terminal display attributes or Nothing if they are not known. known_fattr :: Maybe FixedAttr } initial_terminal_state :: TerminalState initial_terminal_state = TerminalState Nothing class Terminal t where -- | Text identifier for the terminal. Used for debugging. terminal_ID :: t -> String -- | release_terminal :: MonadIO m => t -> m () -- | Clear the display and initialize the terminal to some initial display state. -- -- The expectation of a program is that the display starts in some initial state. -- The initial state would consist of fixed values: -- - cursor at top left -- - UTF-8 character encoding -- - drawing characteristics are the default -- The abstract operation I think all these behaviors are instances of is reserving exclusive -- access to a display such that: -- - The previous state cannot be determined -- - When exclusive access to a display is release the display returns to the previous state. reserve_display :: MonadIO m => t -> m () -- | Return the display to the state before reserve_display -- If no previous state then set the display state to the initial state. release_display :: MonadIO m => t -> m () -- | Returns the current display bounds. display_bounds :: MonadIO m => t -> m DisplayRegion -- Internal method used to provide the DisplayTerminal instance to the DisplayHandle -- constructor. display_terminal_instance :: MonadIO m => t -> DisplayRegion -> (forall d. DisplayTerminal d => d -> DisplayHandle) -> m DisplayHandle -- | Output the byte buffer of the specified size to the terminal device. The size is equal to -- end_ptr - start_ptr output_byte_buffer :: t -> OutputBuffer -> Word -> IO () -- | Handle of output device output_handle :: t -> IO Handle instance Terminal TerminalHandle where terminal_ID (TerminalHandle t _) = terminal_ID t release_terminal (TerminalHandle t _) = release_terminal t reserve_display (TerminalHandle t _) = reserve_display t release_display (TerminalHandle t _) = release_display t display_bounds (TerminalHandle t _) = display_bounds t display_terminal_instance (TerminalHandle t _) = display_terminal_instance t output_byte_buffer (TerminalHandle t _) = output_byte_buffer t output_handle (TerminalHandle t _) = output_handle t data DisplayHandle where DisplayHandle :: forall d . DisplayTerminal d => d -> TerminalHandle -> DisplayState -> DisplayHandle -- | Acquire display access to the given region of the display. -- Currently all regions have the upper left corner of (0,0) and the lower right corner at -- (max display_width provided_width, max display_height provided_height) display_context :: MonadIO m => TerminalHandle -> DisplayRegion -> m DisplayHandle display_context t b = do s <- initial_display_state display_terminal_instance t b (\ d -> DisplayHandle d t s) data DisplayState = DisplayState { previous_output_ref :: IORef (Maybe DisplayOps) } initial_display_state :: MonadIO m => m DisplayState initial_display_state = liftM DisplayState $ liftIO $ newIORef Nothing class DisplayTerminal d where -- | Provide the bounds of the display context. context_region :: d -> DisplayRegion -- | Maximum number of colors supported by the context. context_color_count :: d -> Word -- | sets the output position to the specified row and column. Where the number of bytes -- required for the control codes can be specified seperate from the actual byte sequence. move_cursor_required_bytes :: d -> Word -> Word -> Word serialize_move_cursor :: MonadIO m => d -> Word -> Word -> OutputBuffer -> m OutputBuffer show_cursor_required_bytes :: d -> Word serialize_show_cursor :: MonadIO m => d -> OutputBuffer -> m OutputBuffer hide_cursor_required_bytes :: d -> Word serialize_hide_cursor :: MonadIO m => d -> OutputBuffer -> m OutputBuffer -- | Assure the specified output attributes will be applied to all the following text until the -- next output attribute change. Where the number of bytes required for the control codes can -- be specified seperate from the actual byte sequence. The required number of bytes must be -- at least the maximum number of bytes required by any attribute changes. The serialization -- equations must provide the ptr to the next byte to be specified in the output buffer. -- -- The currently applied display attributes are provided as well. The Attr data type can -- specify the style or color should not be changed from the currently applied display -- attributes. In order to support this the currently applied display attributes are required. -- In addition it may be possible to optimize the state changes based off the currently applied -- display attributes. attr_required_bytes :: d -> FixedAttr -> Attr -> DisplayAttrDiff -> Word serialize_set_attr :: MonadIO m => d -> FixedAttr -> Attr -> DisplayAttrDiff -> OutputBuffer -> m OutputBuffer -- | Reset the display attributes to the default display attributes default_attr_required_bytes :: d -> Word serialize_default_attr :: MonadIO m => d -> OutputBuffer -> m OutputBuffer -- | See Graphics.Vty.Terminal.XTermColor.inline_hack inline_hack :: MonadIO m => d -> m () inline_hack _d = return () instance DisplayTerminal DisplayHandle where context_region (DisplayHandle d _ _) = context_region d context_color_count (DisplayHandle d _ _) = context_color_count d move_cursor_required_bytes (DisplayHandle d _ _) = move_cursor_required_bytes d serialize_move_cursor (DisplayHandle d _ _) = serialize_move_cursor d show_cursor_required_bytes (DisplayHandle d _ _) = show_cursor_required_bytes d serialize_show_cursor (DisplayHandle d _ _) = serialize_show_cursor d hide_cursor_required_bytes (DisplayHandle d _ _) = hide_cursor_required_bytes d serialize_hide_cursor (DisplayHandle d _ _) = serialize_hide_cursor d attr_required_bytes (DisplayHandle d _ _) = attr_required_bytes d serialize_set_attr (DisplayHandle d _ _) = serialize_set_attr d default_attr_required_bytes (DisplayHandle d _ _) = default_attr_required_bytes d serialize_default_attr (DisplayHandle d _ _) = serialize_default_attr d inline_hack (DisplayHandle d _ _) = inline_hack d -- | All terminals serialize UTF8 text to the terminal device exactly as serialized in memory. utf8_text_required_bytes :: UTF8 BSCore.ByteString -> Word utf8_text_required_bytes str = let (_, _, src_bytes_length) = BSCore.toForeignPtr (toRep str) in toEnum src_bytes_length -- | All terminals serialize UTF8 text to the terminal device exactly as serialized in memory. serialize_utf8_text :: MonadIO m => UTF8 BSCore.ByteString -> OutputBuffer -> m OutputBuffer serialize_utf8_text str dest_ptr = let (src_fptr, src_ptr_offset, src_bytes_length) = BSCore.toForeignPtr (toRep str) in liftIO $ withForeignPtr src_fptr $ \src_ptr -> do let src_ptr' = src_ptr `plusPtr` src_ptr_offset BSCore.memcpy dest_ptr src_ptr' (toEnum src_bytes_length) return (dest_ptr `plusPtr` src_bytes_length) -- | Displays the given `Picture`. -- -- 0. The image is cropped to the display size. -- -- 1. Converted into a sequence of attribute changes and text spans. -- -- 2. The cursor is hidden. -- -- 3. Serialized to the display. -- -- 4. The cursor is then shown and positioned or kept hidden. -- -- -- todo: specify possible IO exceptions. -- abstract from IO monad to a MonadIO instance. output_picture :: MonadIO m => DisplayHandle -> Picture -> m () output_picture (DisplayHandle d t s) pic = do let !r = context_region d let !ops = spans_for_pic pic r let !initial_attr = FixedAttr default_style_mask Nothing Nothing -- Diff the previous output against the requested output. Differences are currently on a per-row -- basis. diffs :: [Bool] <- liftIO ( readIORef (previous_output_ref s) ) >>= \mprevious_ops -> case mprevious_ops of Nothing -> return $ replicate ( fromEnum $ region_height $ effected_region ops ) True Just previous_ops -> if effected_region previous_ops /= effected_region ops then return $ replicate ( fromEnum $ region_height $ effected_region ops ) True else return $ zipWith (/=) ( Vector.toList $ display_ops previous_ops ) ( Vector.toList $ display_ops ops ) -- determine the number of bytes required to completely serialize the output ops. let total = hide_cursor_required_bytes d + default_attr_required_bytes d + required_bytes d initial_attr diffs ops + case pic_cursor pic of NoCursor -> 0 Cursor x y -> let m = cursor_output_map ops $ pic_cursor pic ( ox, oy ) = char_to_output_pos m ( x, y ) in show_cursor_required_bytes d + move_cursor_required_bytes d ox oy -- ... then serialize liftIO $ allocaBytes (fromEnum total) $ \start_ptr -> do ptr <- serialize_hide_cursor d start_ptr ptr' <- serialize_default_attr d ptr ptr'' <- serialize_output_ops d ptr' initial_attr diffs ops end_ptr <- case pic_cursor pic of NoCursor -> return ptr'' Cursor x y -> do let m = cursor_output_map ops $ pic_cursor pic (ox, oy) = char_to_output_pos m (x,y) serialize_show_cursor d ptr'' >>= serialize_move_cursor d ox oy -- todo: How to handle exceptions? case end_ptr `minusPtr` start_ptr of count | count < 0 -> fail "End pointer before start of buffer." | toEnum count > total -> fail $ "End pointer past end of buffer by " ++ show (toEnum count - total) | otherwise -> output_byte_buffer t start_ptr (toEnum count) -- Cache the output spans. liftIO $ writeIORef (previous_output_ref s) (Just ops) return () required_bytes :: DisplayTerminal d => d -> FixedAttr -> [Bool] -> DisplayOps -> Word required_bytes d in_fattr diffs ops = let (_, n, _, _) = Vector.foldl' required_bytes' (0, 0, in_fattr, diffs) ( display_ops ops ) in n where required_bytes' (y, current_sum, fattr, True : diffs') span_ops = let (s, fattr') = span_ops_required_bytes d y fattr span_ops in ( y + 1, s + current_sum, fattr', diffs' ) required_bytes' (y, current_sum, fattr, False : diffs') _span_ops = ( y + 1, current_sum, fattr, diffs' ) required_bytes' (_y, _current_sum, _fattr, [] ) _span_ops = error "shouldn't be possible" span_ops_required_bytes :: DisplayTerminal d => d -> Word -> FixedAttr -> SpanOps -> (Word, FixedAttr) span_ops_required_bytes d y in_fattr span_ops = -- The first operation is to set the cursor to the start of the row let header_required_bytes = move_cursor_required_bytes d 0 y -- then the span ops are serialized in the order specified in Vector.foldl' ( \(current_sum, fattr) op -> let (c, fattr') = span_op_required_bytes d fattr op in (c + current_sum, fattr') ) (header_required_bytes, in_fattr) span_ops span_op_required_bytes :: DisplayTerminal d => d -> FixedAttr -> SpanOp -> (Word, FixedAttr) span_op_required_bytes d fattr (AttributeChange attr) = let attr' = limit_attr_for_display d attr diffs = display_attr_diffs fattr fattr' c = attr_required_bytes d fattr attr' diffs fattr' = fix_display_attr fattr attr' in (c, fattr') span_op_required_bytes _d fattr (TextSpan _ _ str) = (utf8_text_required_bytes str, fattr) serialize_output_ops :: ( MonadIO m, DisplayTerminal d ) => d -> OutputBuffer -> FixedAttr -> [Bool] -> DisplayOps -> m OutputBuffer serialize_output_ops d start_ptr in_fattr diffs ops = do (_, end_ptr, _, _) <- Vector.foldM' serialize_output_ops' ( 0, start_ptr, in_fattr, diffs ) ( display_ops ops ) return end_ptr where serialize_output_ops' ( y, out_ptr, fattr, True : diffs' ) span_ops = serialize_span_ops d y out_ptr fattr span_ops >>= return . ( \(out_ptr', fattr') -> ( y + 1, out_ptr', fattr', diffs' ) ) serialize_output_ops' ( y, out_ptr, fattr, False : diffs' ) _span_ops = return ( y + 1, out_ptr, fattr, diffs' ) serialize_output_ops' (_y, _out_ptr, _fattr, [] ) _span_ops = error "shouldn't be possible" serialize_span_ops :: ( MonadIO m, DisplayTerminal d ) => d -> Word -> OutputBuffer -> FixedAttr -> SpanOps -> m (OutputBuffer, FixedAttr) serialize_span_ops d y out_ptr in_fattr span_ops = do -- The first operation is to set the cursor to the start of the row out_ptr' <- serialize_move_cursor d 0 y out_ptr -- then the span ops are serialized in the order specified Vector.foldM ( \(out_ptr'', fattr) op -> serialize_span_op d op out_ptr'' fattr ) (out_ptr', in_fattr) span_ops serialize_span_op :: ( MonadIO m, DisplayTerminal d ) => d -> SpanOp -> OutputBuffer -> FixedAttr -> m (OutputBuffer, FixedAttr) serialize_span_op d (AttributeChange attr) out_ptr fattr = do let attr' = limit_attr_for_display d attr fattr' = fix_display_attr fattr attr' diffs = display_attr_diffs fattr fattr' out_ptr' <- serialize_set_attr d fattr attr' diffs out_ptr return (out_ptr', fattr') serialize_span_op _d (TextSpan _ _ str) out_ptr fattr = do out_ptr' <- serialize_utf8_text str out_ptr return (out_ptr', fattr) marshall_to_terminal :: ( Terminal t ) => t -> Word -> (Ptr Word8 -> IO (Ptr Word8)) -> IO () marshall_to_terminal t c f = do start_ptr <- mallocBytes (fromEnum c) -- -- todo: capture exceptions? end_ptr <- f start_ptr case end_ptr `minusPtr` start_ptr of count | count < 0 -> fail "End pointer before start pointer." | toEnum count > c -> fail $ "End pointer past end of buffer by " ++ show (toEnum count - c) | otherwise -> output_byte_buffer t start_ptr (toEnum count) free start_ptr return () data CursorOutputMap = CursorOutputMap { char_to_output_pos :: (Word, Word) -> (Word, Word) } cursor_output_map :: DisplayOps -> Cursor -> CursorOutputMap cursor_output_map span_ops _cursor = CursorOutputMap { char_to_output_pos = \(cx, cy) -> (cursor_column_offset span_ops cx cy, cy) } cursor_column_offset :: DisplayOps -> Word -> Word -> Word cursor_column_offset span_ops cx cy = let cursor_row_ops = Vector.unsafeIndex (display_ops span_ops) (fromEnum cy) (out_offset, _, _) = Vector.foldl' ( \(d, current_cx, done) op -> if done then (d, current_cx, done) else case span_op_has_width op of Nothing -> (d, current_cx, False) Just (cw, ow) -> case compare cx (current_cx + cw) of GT -> ( d + ow , current_cx + cw , False ) EQ -> ( d + ow , current_cx + cw , True ) LT -> ( d + columns_to_char_offset (cx - current_cx) op , current_cx + cw , True ) ) (0, 0, False) cursor_row_ops in out_offset limit_attr_for_display :: DisplayTerminal d => d -> Attr -> Attr limit_attr_for_display d attr = attr { attr_fore_color = clamp_color $ attr_fore_color attr , attr_back_color = clamp_color $ attr_back_color attr } where clamp_color Default = Default clamp_color KeepCurrent = KeepCurrent clamp_color (SetTo c) = clamp_color' c clamp_color' (ISOColor v) | context_color_count d < 8 = Default | context_color_count d < 16 && v >= 8 = SetTo $ ISOColor (v - 8) | otherwise = SetTo $ ISOColor v clamp_color' (Color240 v) -- TODO: Choose closes ISO color? | context_color_count d < 8 = Default | context_color_count d < 16 = Default | context_color_count d == 240 = SetTo $ Color240 v | otherwise = let p :: Double = fromIntegral v / 240.0 v' = floor $ p * (fromIntegral $ context_color_count d) in SetTo $ Color240 v'