packages feed

irc-core-1.0: driver/CommandParser.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module CommandParser
  ( Parser
  , runParser
  , pChannel
  , pNick
  , pRemaining
  , pRemainingNoSp
  , pRemainingNoSpLimit
  , pToken
  , pValidToken
  , pTarget
  , pChar
  , pSatisfy
  , pHaskell
  , pAnyChar
  , pNumber
  , commandsParser
  , many'
  ) where

import Data.Char
import Data.List (find)
import Text.Read (readMaybe)
import Control.Monad
import Control.Lens
import Control.Applicative
import Graphics.Vty.Image hiding ((<|>))
import qualified Graphics.Vty.Image as Vty
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text

import ClientState
import Irc.Model
import Irc.Format
import HaskellHighlighter
import ImageUtils


newtype Parser a = Parser (String -> (String, Image, Maybe a))
  deriving (Functor)

runParser :: Parser a -> String -> (Image, Maybe a)
runParser (Parser p) s
  | all isSpace rest = (img Vty.<|> stringWithControls defAttr rest, res)
  | otherwise = (img Vty.<|> stringWithControls (withForeColor defAttr red) rest, Nothing)
  where
  (rest,img,res) = p s

instance Applicative Parser where
  pure x = Parser (\s -> (s,emptyImage,Just x))
  Parser f <*> Parser x = Parser (\s -> case f s of
                                          (s1,i1,r1) ->
                                             case x s1 of
                                               (s2,i2,r2) ->
                                                 (s2,i1 Vty.<|> i2,r1<*>r2))

instance Alternative Parser where
  empty = Parser (\s -> (s,emptyImage,Nothing))
  Parser x <|> Parser y = Parser $ \s ->
    case (x s,y s) of
      (rx@(_,_,Just{}),_) -> rx
      (_,ry@(_,_,Just{})) -> ry
      (rx,_) -> rx

pAnyChar :: Parser Char
pAnyChar = pValidToken "character" $ \t ->
  case t of
    [x] -> Just x
    _   -> Nothing

pValidToken :: String -> (String -> Maybe a) -> Parser a
pValidToken name validate = Parser $ \s ->
  let (w,s1) = span (==' ') s
      (t,s2) = break (==' ') s1
      img c  = stringWithControls (withForeColor defAttr c) (w ++ t)
  in if null t
       then ("", char defAttr ' ' Vty.<|>
                 stringWithControls (withStyle defAttr reverseVideo) name Vty.<|>
                 stringWithControls defAttr (drop (length name + 1) w)
              , Nothing)
       else case validate t of
              Just x -> (s2, img green, Just x)
              Nothing -> (s2, img red, Nothing)

pToken :: String -> Parser String
pToken name = pValidToken name Just

pRemaining :: Parser String
pRemaining = Parser (\s -> ("", stringWithControls defAttr s, Just s))

pRemainingNoSp :: Parser String
pRemainingNoSp = fmap (dropWhile isSpace) pRemaining

pRemainingNoSpLimit :: Int -> Parser String
pRemainingNoSpLimit len =
  Parser (\s ->
    let (w,s') = span (==' ') s
        (a,b) = splitAt len (dropWhile isSpace s')
    in if null b then ("", stringWithControls defAttr (w++a), Just a)
                 else ("", stringWithControls defAttr (w++a) Vty.<|>
                           stringWithControls (withForeColor defAttr red) b, Nothing))

pChannel :: ClientState -> Parser Identifier
pChannel st = pValidToken "channel" $ \chan ->
  do let ident = asIdentifier chan
     guard (isChannelName ident (view (clientServer0 . ccConnection) st))
     return ident

pTarget :: Parser Identifier
pTarget = pValidToken "target" (Just . asIdentifier)

pNick :: ClientState -> Parser Identifier
pNick st = pValidToken "nick" $ \nick ->
  do let ident = asIdentifier nick
     guard (isNickName ident (view (clientServer0 . ccConnection) st))
     return ident

asIdentifier :: String -> Identifier
asIdentifier = mkId . Text.encodeUtf8 . Text.pack

pChar :: Char -> Parser Char
pChar c = pSatisfy [c] (== c)

pSatisfy :: String -> (Char -> Bool) -> Parser Char
pSatisfy name f = Parser (\s ->
          case s of
            c1:s1 | f c1 -> (s1, char defAttr c1, Just c1)
                  | otherwise -> (s1, emptyImage, Nothing)
            [] -> (s, string (withStyle defAttr reverseVideo) (' ':name), Nothing))

pHaskell :: Parser String
pHaskell = Parser (\s ->
            ("", cleanText (Text.pack (highlightHaskell s)),
                        Just (drop 1 (highlightHaskell s))))

commandsParser ::
  String ->
  [(String, [String], Parser a)] ->
  (Image, Maybe a)
commandsParser input cmds =
  case find matchCommand cmds of
    Just p -> over _1 (\img -> char defAttr '/' Vty.<|>
                               stringWithControls (withForeColor defAttr yellow) cmd Vty.<|>
                               img)
                      (runParser (view _3 p) rest)

    Nothing -> ( char defAttr '/' Vty.<|>
                 stringWithControls (withForeColor defAttr red) (drop 1 input)
               , Nothing)
  where
  (cmd,rest) = break (==' ') (drop 1 input)
  matchCommand (c,cs,_) = cmd `elem` c:cs

-- | Many parser that works in this infinite model
-- NOTE: MUST BE END OF PARSER!
many' :: Parser a -> Parser [a]
many' p = [] <$ pEnd
      <|> liftA2 (:) p (many' p)

pEnd :: Parser ()
pEnd = Parser (\s ->
          if all isSpace s then ("", string defAttr s, Just ())
                           else (s , emptyImage      , Nothing))

pNumber :: Parser Int
pNumber = pValidToken "number" readMaybe