packages feed

sarsi-0.0.5.1: src/Codec/Sarsi/Curses.hs

{-# LANGUAGE OverloadedStrings #-}

module Codec.Sarsi.Curses where

import Data.Attoparsec.Text
import qualified Data.Attoparsec.Text as AttoText
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text

-- note: expect a line that does NOT ends with a LF
cleanLine :: Text -> Text
cleanLine txt | Text.null txt = txt
cleanLine txt | Text.last txt == '\r' = fromMaybe Text.empty $ (f . fst) <$> Text.unsnoc txt
  where
    f x = case Text.breakOnAll "\r" x of
      [] -> x
      xs -> Text.tail $ (snd . last) xs
cleanLine txt = txt

-- Note: this parser remove CSI codes and do a best effort
-- at removing "clear line" instructions while keeping
-- all information exposed without any mangling.
cleaningCurses :: Parser Text
cleaningCurses = choice [multiples, single, none]
  where
    multiples = do
      before <- ln
      middle <- choice [silenceClearLines, silenceCSI]
      after <- choice [multiples, single]
      return $ Text.concat [before, "\n", middle, after]
      where
        ln = do
          before <- AttoText.takeWhile (breakAt . fromEnum)
          after <- choice [lineFinish, lineContinue]
          return (Text.concat [before, after])
          where
            breakAt 10 = False -- LF
            breakAt 27 = False -- ESC
            breakAt _ = True
        lineFinish = char '\n' >> (return $ Text.pack "\n")
        lineContinue = csi >> ln
        silenceClearLines = do
          _ <- AttoText.takeWhile (not . isEsc . fromEnum)
          _ <- choice [AttoText.many1 (cl <* "\n"), AttoText.many1 cl]
          return Text.empty
    single = do
      befores <- many1 $ choice [clearLine, silenceCSI]
      str <- AttoText.many1 anyChar
      _ <- endOfInput
      return (Text.concat [Text.concat befores, Text.pack str])
      where
        clearLine = do
          _ <- AttoText.takeWhile (not . isEsc . fromEnum)
          _ <- AttoText.many1 cl
          return Text.empty
    none = do
      ln <- (AttoText.takeWhile $ \w -> w /= '\n')
      _ <- "\n"
      return $ Text.concat [ln, "\n"]
    cl = csiHeader >> string "2K" >> return ()
    silenceCSI = do
      txt <- AttoText.takeWhile (not . isEsc . fromEnum)
      _ <- AttoText.many1 csi
      return txt

-- CSI (Control Sequence Introducer) sequences
csi :: Parser Text
csi = do
  _ <- csiHeader
  param <- takeWhileInRange 0x30 0x3F
  inter <- takeWhileInRange 0x20 0x2F
  final <- satisfy ((inRange 0x40 0x7E) . fromEnum)
  return $ Text.concat [param, inter, Text.singleton final]
  where
    takeWhileInRange l u = AttoText.takeWhile (inRange l u . fromEnum)
    inRange l u i | i >= l && i <= u = True
    inRange _ _ _ = False

csiHeader :: Parser ()
csiHeader = (satisfy (isEsc . fromEnum) <* char '[') >> return ()

isEsc :: Int -> Bool
isEsc 27 = True
isEsc _ = False