reflex-process-0.3.3.1: src/Reflex/Process/Lines.hs
{-# Language OverloadedStrings #-}
module Reflex.Process.Lines where
import Control.Monad.Fix (MonadFix)
import qualified Data.ByteString.Char8 as C8
import Data.ByteString.Char8 (ByteString)
import Data.Foldable (toList)
import Data.Maybe (fromMaybe)
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Reflex
-- * Output lines
-- | Accumulator for line-based output that keeps track of any dangling,
-- unterminated line
data Lines = Lines
{ _lines_terminated :: Seq C8.ByteString
, _lines_unterminated :: Maybe C8.ByteString
}
deriving (Show, Eq, Ord, Read)
-- | Empty output
emptyLines :: Lines
emptyLines = Lines Seq.empty Nothing
-- | Add some raw output to a 'Lines'. This will chop the raw output up into lines.
addLines :: ByteString -> Lines -> Lines
addLines new (Lines t u) =
let newLines' = Seq.fromList $ filter (not . C8.null) (C8.lines new)
in
case u of
Nothing -> if "\n" `C8.isSuffixOf` new
then Lines (t <> newLines') Nothing
else case Seq.viewr newLines' of
Seq.EmptyR -> Lines t Nothing
(t' Seq.:> u') -> Lines (t <> t') (Just u')
Just u' -> addLines (u' <> new) $ Lines t Nothing
-- | Convert a 'ByteString' into a 'Lines'
linesFromBS :: C8.ByteString -> Lines
linesFromBS = flip addLines mempty
instance Semigroup Lines where
a <> b = addLines (unLines b) a
instance Monoid Lines where
mempty = emptyLines
-- | Convert a 'Lines' back into a 'ByteString'
unLines :: Lines -> ByteString
unLines (Lines t u) =
C8.unlines (toList t) <> fromMaybe "" u
-- | Convenience accessor for the last whole line received by a 'Lines'.
-- Ignores any unterminated line that may follow.
lastWholeLine :: Lines -> Maybe C8.ByteString
lastWholeLine (Lines t _) = case Seq.viewr t of
Seq.EmptyR -> Nothing
_ Seq.:> x -> Just x
-- | Split lines into two. The sequence that satisfies the predicate is
-- consumed and will not appear in either resulting 'Lines'.
splitLinesOn :: (ByteString -> Bool) -> Lines -> Maybe (Lines, Lines)
splitLinesOn test (Lines t u) =
let (before, after) = Seq.breakl test t
in if Seq.null after then Nothing else Just (Lines before Nothing, Lines (Seq.drop 1 after) u)
-- | Given an event of raw bytes, fire an output event of *terminated* lines.
-- Unterminated lines are held until the line they belong to is completed or
-- until the flush event fires.
newLines
:: (Reflex t, MonadHold t m, MonadFix m)
=> Event t ByteString
-> Event t () -- ^ Event that flushes any remaining unterminated lines
-> m (Event t Lines) -- ^ These will be complete lines except when the flush event fires, in which it may include unterminated lines
newLines e flush = do
x <- foldDyn ($) (mempty, mempty) $ mergeWith (.)
[ ffor e $ \new (_, old) ->
let Lines t u = addLines new old
in (Lines t Nothing, Lines mempty u)
, ffor flush $ \_ (_, old) -> (old, emptyLines)
]
pure $ fforMaybe (updated x) $ \(terminatedLines, _) -> if terminatedLines == mempty
then Nothing
else Just terminatedLines