packages feed

hs-term-emulator-0.1.0.0: src/System/Terminal/Emulator/Parsing/Internal.hs

{-# LANGUAGE OverloadedStrings #-}

module System.Terminal.Emulator.Parsing.Internal where

import Control.Applicative ((<|>))
import Data.Attoparsec.Text
import Data.Char (isDigit)
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.List.NonEmpty as NE
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Read as T
import qualified Data.Vector as V
import System.Terminal.Emulator.DECPrivateMode (intToDECPrivateMode)
import System.Terminal.Emulator.Parsing.Types (ControlSequenceIntroducer (..), DeviceStatusReport (..), EraseInDisplayParam (..), EraseInLineParam (..), EscapeSequence (..), Mode (..), OperatingSystemCommand (..), SendDeviceAttributesSecondary (RequestTerminalIdentificationCode), SingleCharacterFunction (..), TermAtom (..), WindowManipulation (..), codeToSGR)
import Prelude hiding (takeWhile)

parseTermAtom :: Parser TermAtom
parseTermAtom =
  parseVisibleChar <|> parseControl

parseVisibleChar :: Parser TermAtom
parseVisibleChar = TermAtom_VisibleChar <$> satisfy (not . isControl)

-- | This parser always succeeds
parseControl :: Parser TermAtom
parseControl = do
  c <- anyChar
  if c == '\ESC'
    then parseEscape
    else pure $ case singleCharacterFunction c of
      Nothing -> TermAtom_SingleCharacterFunctionUnknown c
      Just f -> TermAtom_SingleCharacterFunction f

singleCharacterFunction :: Char -> Maybe SingleCharacterFunction
singleCharacterFunction '\a' = Just Control_Bell
singleCharacterFunction '\b' = Just Control_Backspace
singleCharacterFunction '\r' = Just Control_CarriageReturn
singleCharacterFunction '\ENQ' = Just Control_ReturnTerminalStatus
singleCharacterFunction '\f' = Just Control_FormFeed
singleCharacterFunction '\n' = Just Control_LineFeed
singleCharacterFunction '\SI' = Just Control_SwitchToStandardCharacterSet
singleCharacterFunction '\SO' = Just Control_SwitchToAlternateCharacterSet
singleCharacterFunction '\t' = Just Control_Tab
singleCharacterFunction '\v' = Just Control_VerticalTab
singleCharacterFunction _ = Nothing

-- | This parser always succeeds
parseEscape :: Parser TermAtom
parseEscape = do
  c <- anyChar
  case c of
    '[' -> handleCsi
    ']' -> handleOsc
    '(' -> handleSetG0CharacterSet
    _ -> handleSingle c
  where
    handleCsi :: Parser TermAtom
    handleCsi = do
      csiInput <- parseControlSequenceIntroducer
      pure $ case processControlSequenceIntroducer csiInput of
        Nothing -> case processOtherControlSequenceIntroducer csiInput of
          Nothing -> TermAtom_EscapeSequenceUnknown (renderCsi csiInput)
          Just csi -> TermAtom_EscapeSequence (Esc_CSI csi)
        Just csi -> TermAtom_EscapeSequence (Esc_CSI csi)

    handleOsc :: Parser TermAtom
    handleOsc = do
      oscInput <- parseOperatingSystemCommand
      pure $ case processOperatingSystemCommand oscInput of
        Nothing -> TermAtom_EscapeSequenceUnknown (renderOsc oscInput)
        Just osc -> TermAtom_EscapeSequence (Esc_OSC osc)

    handleSingle :: Char -> Parser TermAtom
    handleSingle c = pure $ case singleCharacterEscapeSequence c of
      Just e -> TermAtom_EscapeSequence e
      Nothing -> TermAtom_EscapeSequenceUnknown ("\ESC" <> T.singleton c)

    handleSetG0CharacterSet :: Parser TermAtom
    handleSetG0CharacterSet =
      ( choice
          [ string "A",
            string "B",
            string "C",
            string "5",
            string "H",
            string "7",
            string "K",
            string "Q",
            string "9",
            string "R",
            string "f",
            string "Y",
            string "Z",
            string "4",
            string "\">",
            string "%2",
            string "%6",
            string "%=",
            string "=",
            string "`",
            string "E",
            string "6",
            string "0",
            string "<",
            string ">",
            string "\"4",
            string "\"?",
            string "%0",
            string "%5",
            string "&4",
            string "%3",
            string "&5"
          ]
          >>= pure . TermAtom_EscapeSequence . ESC_SetG0CharacterSet
      )
        <|> ( anyChar >>= \c ->
                pure (TermAtom_EscapeSequenceUnknown ("\ESC(" <> T.singleton c))
            )

-----------------------------------------------------------------------
-- CSI (Control Sequence Introducer) sequences
-----------------------------------------------------------------------

data ControlSequenceIntroducerInput = ControlSequenceIntroducerInput !Text
  deriving (Show, Eq)

data ControlSequenceIntroducerComponents
  = ControlSequenceIntroducerComponents
      !Bool
      -- ^ Private?
      !(NonEmpty Int)
      -- ^ Args
      !Char
      -- ^ Mode
  deriving (Show, Eq)

-- | Should be run after reading the sequence @ESC [@
--
-- This parser always succeeds
parseControlSequenceIntroducer :: Parser ControlSequenceIntroducerInput
parseControlSequenceIntroducer = do
  str <- takeTill ((`between` (0x40, 0x7E)) . fromEnum)
  c <- anyChar
  pure (ControlSequenceIntroducerInput ((str) <> T.singleton c))

parseControlSequenceIntroducerComponents :: ControlSequenceIntroducerInput -> Maybe ControlSequenceIntroducerComponents
parseControlSequenceIntroducerComponents (ControlSequenceIntroducerInput str) =
  case parseOnly (parser <* endOfInput) str of
    Left _ -> Nothing
    Right val -> Just val
  where
    parser :: Parser ControlSequenceIntroducerComponents
    parser = do
      private <- option False (char '?' >> pure True)
      first <- peekChar'
      args <-
        if isDigit first || first == ';'
          then sepBy (option 0 decimal) (char ';')
          else pure []
      mode <- anyChar
      pure (ControlSequenceIntroducerComponents private (listToNonEmpty 0 args) mode)

listToNonEmpty :: a -> [a] -> NonEmpty a
listToNonEmpty def [] = def :| []
listToNonEmpty _ (x : xs) = x :| xs

processControlSequenceIntroducerComponents :: ControlSequenceIntroducerComponents -> Maybe ControlSequenceIntroducer
processControlSequenceIntroducerComponents (ControlSequenceIntroducerComponents False args mode) = parseCsi mode args
processControlSequenceIntroducerComponents (ControlSequenceIntroducerComponents True args mode) = parsePrivCsi mode args

changeZero :: Int -> Int -> Int
changeZero toVal 0 = toVal
changeZero _ val = val

headChangeZero :: Int -> NonEmpty Int -> Int
headChangeZero toVal args = changeZero toVal (NE.head args)

parseCsi :: Char -> NonEmpty Int -> Maybe ControlSequenceIntroducer
parseCsi mode args = case mode of
  'A' -> Just (CSI_CursorUp (headChangeZero 1 args))
  'B' -> Just (CSI_CursorDown (headChangeZero 1 args))
  'C' -> Just (CSI_CursorForward (headChangeZero 1 args))
  'D' -> Just (CSI_CursorBack (headChangeZero 1 args))
  'K' ->
    CSI_EraseInLine <$> case NE.head args of
      0 -> Just ClearFromCursorToEndOfLine
      1 -> Just ClearFromCursorToBeginningOfLine
      2 -> Just ClearEntireLine
      _ -> Nothing
  '@' -> Just (CSI_InsertBlankCharacters (headChangeZero 1 args))
  'P' -> Just (CSI_DeleteChars (headChangeZero 1 args))
  'G' -> Just (CSI_CursorCharacterAbsolute (headChangeZero 1 args))
  'H' ->
    let (row, col) = case args of
          r :| [] -> (r, 0)
          r :| (c : _) -> (r, c)
     in Just (CSI_CursorPosition (changeZero 1 row) (changeZero 1 col))
  'J' -> case NE.head args of
    0 -> Just (CSI_EraseInDisplay EraseBelow)
    1 -> Just (CSI_EraseInDisplay EraseAbove)
    2 -> Just (CSI_EraseInDisplay EraseAll)
    3 -> Just (CSI_EraseInDisplay EraseSavedLines)
    _ -> Nothing
  'L' -> Just (CSI_InsertBlankLines (headChangeZero 1 args))
  'M' -> Just (CSI_DeleteLines (headChangeZero 1 args))
  'S' -> Just (CSI_ScrollUp (headChangeZero 1 args))
  'T' -> Just (CSI_ScrollDown (headChangeZero 1 args))
  'X' -> Just (CSI_EraseCharacters (headChangeZero 1 args))
  '`' -> Just (CSI_CharacterPositionAbsolute (headChangeZero 1 args))
  'a' -> Just (CSI_CharacterPositionRelative (headChangeZero 1 args))
  'c' -> case args of
    0 :| [] -> Just CSI_SendDeviceAttributes
    _ -> Nothing
  'd' -> Just (CSI_LinePositionAbsolute (headChangeZero 1 args))
  'e' -> Just (CSI_LinePositionRelative (headChangeZero 1 args))
  'f' ->
    let (row, col) = case args of
          r :| [] -> (r, 0)
          r :| (c : _) -> (r, c)
     in Just (CSI_HorizontalVerticalPosition (changeZero 1 row) (changeZero 1 col))
  't' -> case args of
    22 :| 0 : _ -> Just (CSI_WindowManipulation SaveIconAndWindowTitleOnStack)
    23 :| 0 : _ -> Just (CSI_WindowManipulation RestoreIconAndWindowTitleOnStack)
    _ -> Nothing
  'h' -> case args of
    2 :| [] -> Just (CSI_SetMode KeyboardActionMode)
    4 :| [] -> Just (CSI_SetMode InsertReplaceMode)
    12 :| [] -> Just (CSI_SetMode SendReceive)
    20 :| [] -> Just (CSI_SetMode AutomaticNewlineNormalLinefeed)
    _ -> Nothing
  'l' -> case args of
    2 :| [] -> Just (CSI_ResetMode KeyboardActionMode)
    4 :| [] -> Just (CSI_ResetMode InsertReplaceMode)
    12 :| [] -> Just (CSI_ResetMode SendReceive)
    20 :| [] -> Just (CSI_ResetMode AutomaticNewlineNormalLinefeed)
    _ -> Nothing
  'n' -> case NE.head args of
    5 -> Just (CSI_DeviceStatusReport StatusReport)
    6 -> Just (CSI_DeviceStatusReport ReportCursorPosition)
    _ -> Nothing
  'r' ->
    let (top, bottom) = case args of
          t :| [] -> (t, 0)
          t :| (b : _) -> (t, b)
     in Just
          ( CSI_DECSTBM
              (if top == 0 then Nothing else Just top)
              (if bottom == 0 then Nothing else Just bottom)
          )
  'm' -> Just $ CSI_SGR (V.fromList (mapMaybe codeToSGR (NE.toList args)))
  _ -> Nothing

parsePrivCsi :: Char -> NonEmpty Int -> Maybe ControlSequenceIntroducer
parsePrivCsi mode args = case mode of
  'h' ->
    let n = (headChangeZero 1 args)
     in Just $ case intToDECPrivateMode n of
          Just decset -> CSI_DECSET decset
          Nothing -> CSI_DECSET_Unknown n
  'l' ->
    let n = (headChangeZero 1 args)
     in Just $ case intToDECPrivateMode n of
          Just decset -> CSI_DECRST decset
          Nothing -> CSI_DECRST_Unknown n
  _ -> Nothing

processControlSequenceIntroducer :: ControlSequenceIntroducerInput -> Maybe ControlSequenceIntroducer
processControlSequenceIntroducer csiInput =
  parseControlSequenceIntroducerComponents csiInput
    >>= processControlSequenceIntroducerComponents

processOtherControlSequenceIntroducer :: ControlSequenceIntroducerInput -> Maybe ControlSequenceIntroducer
processOtherControlSequenceIntroducer (ControlSequenceIntroducerInput str) =
  case str of
    "!p" -> Just CSI_SoftTerminalReset
    ">c" -> Just (CSI_SendDeviceAttributesSecondary RequestTerminalIdentificationCode)
    ">0c" -> Just (CSI_SendDeviceAttributesSecondary RequestTerminalIdentificationCode)
    _
      | "?" `T.isPrefixOf` str && "$p" `T.isSuffixOf` str ->
        let modeStr = T.init (T.init (T.tail str))
         in case T.decimal modeStr of
              Left _ -> Nothing
              Right (mode, "") -> Just (CSI_RequestDECPrivateMode mode)
              Right (_, _) -> Nothing
    _ -> Nothing

-- | Used for error reporting
renderCsi :: ControlSequenceIntroducerInput -> Text
renderCsi (ControlSequenceIntroducerInput str) = "\ESC[" <> str

-----------------------------------------------------------------------
-- OSC (Operating System Command)
-----------------------------------------------------------------------

data OperatingSystemCommandInput = OperatingSystemCommandInput !Text

-- | Should be run after reading the sequence @ESC ]@
--
-- This parser always succeeds
parseOperatingSystemCommand :: Parser OperatingSystemCommandInput
parseOperatingSystemCommand = do
  str <-
    manyTill'
      anyChar
      ( (char '\a' >> pure ())
          <|> (string "\ESC\\" >> pure ())
      )
  pure (OperatingSystemCommandInput (T.pack str))

-- | Used for error reporting
renderOsc :: OperatingSystemCommandInput -> Text
renderOsc (OperatingSystemCommandInput str) = "\ESC]" <> str <> "\a"

processOperatingSystemCommand :: OperatingSystemCommandInput -> Maybe OperatingSystemCommand
processOperatingSystemCommand (OperatingSystemCommandInput str) =
  case parseOnly (parser <* endOfInput) str of
    Left _ -> Nothing
    Right val -> Just val
  where
    parser :: Parser OperatingSystemCommand
    parser =
      parseSetTitle
        <|> parseChangeTextForegroundColor
        <|> parseRequestTextForegroundColor
        <|> parseChangeTextBackgroundColor
        <|> parseRequestTextBackgroundColor
        <|> parseResetTextCursorColor

    parseSetTitle :: Parser OperatingSystemCommand
    parseSetTitle = do
      (icon, window) <- parseSetTitleMode
      _ <- char ';'
      title <- takeText
      pure (OSC_SetTitle icon window title)

    parseSetTitleMode :: Parser (Bool, Bool)
    parseSetTitleMode =
      (char '0' >> pure (True, True))
        <|> (char '1' >> pure (True, False))
        <|> (char '2' >> pure (False, True))

    parseChangeTextForegroundColor :: Parser OperatingSystemCommand
    parseChangeTextForegroundColor = do
      _ <- string "10;"
      c <- satisfy (/= '?')
      color <- takeText
      pure (OSC_ChangeTextForegroundColor (T.singleton c <> color))

    parseRequestTextForegroundColor :: Parser OperatingSystemCommand
    parseRequestTextForegroundColor = do
      _ <- string "10;?"
      pure OSC_RequestTextForegroundColor

    parseChangeTextBackgroundColor :: Parser OperatingSystemCommand
    parseChangeTextBackgroundColor = do
      _ <- string "11;"
      c <- satisfy (/= '?')
      color <- takeText
      pure (OSC_ChangeTextBackgroundColor (T.singleton c <> color))

    parseRequestTextBackgroundColor :: Parser OperatingSystemCommand
    parseRequestTextBackgroundColor = do
      _ <- string "11;?"
      pure OSC_RequestTextBackgroundColor

    parseResetTextCursorColor :: Parser OperatingSystemCommand
    parseResetTextCursorColor = do
      _ <- string "112"
      pure OSC_ResetTextCursorColor

-----------------------------------------------------------------------
-- Single Character Escape Sequence
-----------------------------------------------------------------------

singleCharacterEscapeSequence :: Char -> Maybe EscapeSequence
singleCharacterEscapeSequence c =
  case c of
    'M' -> Just Esc_ReverseIndex
    'c' -> Just Esc_RIS
    '=' -> Just Esc_DECPAM
    '>' -> Just Esc_DECPNM
    _ -> Nothing

-----------------------------------------------------------------------
-- Helper functions
-----------------------------------------------------------------------

between :: Ord a => a -> (a, a) -> Bool
between val (low, high) = val >= low && val <= high

isControlC0 :: Char -> Bool
isControlC0 c = fromEnum c `between` (0, 0x1F) || c == '\DEL'

isControlC1 :: Char -> Bool
isControlC1 c = fromEnum c `between` (0x80, 0x9f)

isControl :: Char -> Bool
isControl c = isControlC0 c || isControlC1 c