packages feed

arbtt-0.4.1: src/Categorize.hs

module Categorize where

import Data

import qualified Text.Regex.PCRE.Light.Char8 as RE
import qualified Data.Map as M
import Control.Monad
import Control.Monad.Instances

import Text.ParserCombinators.Parsec hiding (Parser)
import Text.ParserCombinators.Parsec.Token
import Text.ParserCombinators.Parsec.Language
import Text.ParserCombinators.Parsec.Expr
import System.Exit
import Control.Applicative ((<*>),(<$>))
import Data.List
import Data.Maybe
import Data.Char
import Data.Time.Clock
import Debug.Trace
import Control.Arrow (second)

type Categorizer = TimeLog CaptureData -> TimeLog (Ctx, ActivityData)
type Rule = Ctx -> ActivityData

type Parser a = CharParser () a

data Ctx = Ctx
	{ cNow :: TimeLogEntry CaptureData
	, cPast :: [TimeLogEntry CaptureData]
	, cFuture :: [TimeLogEntry CaptureData]
	, cWindowInScope :: Maybe (Bool, String, String)
	, cSubsts :: [String]
	, cCurrentTime :: UTCTime
	}
  deriving (Show)

type Cond = Ctx -> Maybe [String]

readCategorizer :: FilePath -> IO Categorizer
readCategorizer filename = do
	content <- readFile filename
	time <- getCurrentTime
	case parse (do {r <- parseRules; eof ; return r}) filename content of
	  Left err -> do
	  	putStrLn "Parser error:"
		print err
		exitFailure
	  Right cat -> return ((fmap . fmap) (mkSecond (postpare . cat)) . prepare time)

applyCond :: String -> TimeLog (Ctx, ActivityData) -> TimeLog (Ctx, ActivityData)
applyCond s = 
	case parse (do {c <- parseCond; eof ; return c}) "commad line parameter" s of
	  Left err -> error (show err)
	  Right c  -> filter (isJust . c . fst . tlData)

prepare :: UTCTime -> TimeLog CaptureData -> TimeLog Ctx
prepare time tl = go' [] tl tl
  where go' past [] []
  		= []
        go' past (this:future) (now:rest)
	        = now {tlData = Ctx now past future Nothing [] time } :
	          go' (this:past) future rest

-- | Here, we filter out tags appearing twice, and make sure that only one of
--   each category survives
postpare :: ActivityData -> ActivityData
postpare = nubBy go
  where go (Activity (Just c1) _) (Activity (Just c2) _) = c1 == c2
        go a1                     a2                     = a1 == a2

lang :: TokenParser ()
lang = haskell

parseRules :: Parser Rule
parseRules = do 
	whiteSpace lang
	a <- option id (reserved lang "aliases" >> parens lang parseAliasSpecs)
	rb <- parseRulesBody
	return (a . rb)

parseAliasSpecs :: Parser (ActivityData -> ActivityData)
parseAliasSpecs = do as <- sepEndBy1 parseAliasSpec (comma lang)
		     return $ \ad -> foldr doAlias ad as

doAlias :: (String, String) -> ActivityData -> ActivityData
doAlias (s1,s2) = map go
  where go (Activity cat tag) = Activity (if cat == Just s1 then Just s2 else cat)
                                         (if tag == s1 then s2 else tag)

parseAliasSpec :: Parser (String, String)
parseAliasSpec = do s1 <- stringLiteral lang
                    reservedOp lang "->"
		    s2 <- stringLiteral lang
		    return (s1,s2)

parseRulesBody :: Parser Rule
parseRulesBody = do 
	x <- parseRule
	choice [ do comma lang
		    xs <- sepEndBy1 parseRule (comma lang)
		    return (matchAny (x:xs))
	       , do semi lang
	            xs <- many1 (semi lang >> parseRule)
		    return (matchFirst (x:xs))
	       ,    return x
	       ]

parseRule :: Parser Rule
parseRule = choice
	[    braces lang parseRules
	, do cond <- parseCond
	     reservedOp lang "==>"
	     rule <- parseRule
	     return (ifThenElse cond rule matchNone)
	, do reserved lang "if"
	     cond <- parseCond
	     reserved lang "then"
	     rule1 <- parseRule
	     reserved lang "else"
	     rule2 <- parseRule
	     return (ifThenElse cond rule1 rule2)
	, do reserved lang "tag"
	     parseSetTag
	]

parseCond :: Parser Cond
parseCond = buildExpressionParser [
		[ Prefix (reservedOp lang "!" >> return checkNot) ],
		[ Infix (reservedOp lang "&&" >> return checkAnd) AssocLeft ],
		[ Infix (reservedOp lang "||" >> return checkOr) AssocLeft ]
	    ] parseCondPrim

checkAnd :: Cond -> Cond -> Cond	    
checkAnd c1 c2 = do res1 <- c1
                    res2 <- c2
		    return $ res1 >> res2


checkOr :: Cond -> Cond -> Cond	    
checkOr c1 c2 = do res1 <- c1
                   res2 <- c2
		   return $ res1 `mplus` res2

checkNot :: Cond -> Cond
checkNot = liftM (maybe (Just []) (const Nothing))

parseCmp :: Ord a => Parser (a -> a -> Bool)
parseCmp = choice $ map (\(s,o) -> reservedOp lang s >> return o)
		     	[(">=",(>=)),
			 (">", (>)),
			 ("=", (==)),
			 ("==",(==)),
			 ("<",(<)),
			 ("<=",(<=))]


parseCondPrim :: Parser Cond
parseCondPrim = choice
	[    parens lang parseCond
	, do char '$'
	     varname <- show `liftM` natural lang <|> identifier lang
	     choice
	     	[ do guard $ varname `elem` ["title","program"]
		     choice
		     	[ do reservedOp lang "=~"
		             regex <- parseRegex
		             return $ checkRegex varname (RE.compile regex [])
			, do reservedOp lang "==" <|> reservedOp lang "="
			     str <- stringLiteral lang
			     return $ checkEq varname str
			, do reservedOp lang "/=" <|> reservedOp lang "!="
			     str <- stringLiteral lang
			     return $ checkNot (checkEq varname str)
			]
		, do guard $ varname == "idle"
		     op <- parseCmp
		     num <- natural lang
		     return $ checkNumCmp op varname num
		, do guard $ varname `elem` ["time","sampleage"]
		     op <- parseCmp 
		     time <- parseTime
		     return $ checkTimeCmp op varname time
		, do guard $ varname == "active"
		     return checkActive
		]
	, do reserved lang "current window"
	     cond <- parseCond
	     return $ checkCurrentwindow cond
	, do reserved lang "any window"
	     cond <- parseCond
	     return $ checkAnyWindow cond
	]

parseRegex :: Parser String
parseRegex = lexeme lang $ choice
	[ between (char '/') (char '/') (many1 (noneOf "/"))
	, do char 'm'
	     c <- anyChar
	     str <- many1 (noneOf [c])
	     char c
	     return str
	]
	     
-- | Parses a day-of-time specification (hh:mm)
parseTime :: Parser NominalDiffTime
parseTime = fmap fromIntegral $ lexeme lang $ do
               h <- digitToInt <$> digit
	       mh <- optionMaybe (digitToInt <$> digit)
	       char ':'
               m1 <- digitToInt <$> digit
               m2 <- digitToInt <$> digit
	       let hour = maybe h ((10*h)+) mh
	       return $ (hour * 60 + m1 * 10 + m2) * 60

parseSetTag :: Parser Rule
parseSetTag = lexeme lang $ do
                 firstPart <- parseTagPart 
		 choice [ do char ':'
			     secondPart <- parseTagPart
			     return $ do cat <- firstPart
			                 tag <- secondPart
					 return $ maybeToList $ do
					    cat <- cat
					    tag <- tag
					    return $ Activity (Just cat) tag
			,    return $ do tag <- firstPart
					 return $ maybeToList $ do
					    tag <- tag
					    return $ Activity Nothing tag
			]

parseTagPart :: Parser (Ctx -> Maybe String)
parseTagPart = do parts <- many1 (choice 
			[ do char '$'
			     varname <- many1 (letter <|> oneOf ".") <|> many1 digit
			     return $ getVar varname
			, do s <- many1 (letter <|> oneOf "-_")
			     return $ const (Just s)
			])
		  return $ (fmap concat . sequence) <$> sequence parts

ifThenElse :: Cond -> Rule -> Rule -> Rule
ifThenElse cond r1 r2 = do res <- cond
                           case res of 
			    Just substs -> r1 . setSubsts substs
			    Nothing -> r2
  where setSubsts :: [String] -> Ctx -> Ctx
        setSubsts substs ctx = ctx { cSubsts = substs }
	

matchAny :: [Rule] -> Rule
matchAny rules = concat <$> sequence rules
matchFirst :: [Rule] -> Rule
matchFirst rules = takeFirst <$> sequence rules
  where takeFirst [] = []
        takeFirst ([]:xs) = takeFirst xs
	takeFirst (x:xs) = x

getVar :: String -> Ctx -> Maybe String
getVar v ctx | all isNumber  v = 
		let n = read v in
		listToMaybe (drop (n-1) (cSubsts ctx))
getVar v ctx | "current" `isPrefixOf` v = do
		let var = drop (length "current.") v
		win <- findActive $ cWindows (tlData (cNow ctx))
		getVar var (ctx { cWindowInScope = Just win })
getVar "title"   ctx = do
		(_,t,_) <- cWindowInScope ctx
                return t
getVar "program" ctx = do
		(_,_,p) <- cWindowInScope ctx
                return p

checkRegex :: String -> RE.Regex -> Cond
checkRegex varname regex ctx = do s <- getVar varname ctx
		                  matches <- RE.match regex s []
				  return (tail matches)

checkEq :: String -> String -> Cond
checkEq varname str ctx = do s <- getVar varname ctx
		             [] `justIf` (s == str)

findActive :: [(Bool, t, t1)] -> Maybe (Bool, t, t1)
findActive = find (\(a,_,_) -> a)				  

checkCurrentwindow :: Cond -> Cond
checkCurrentwindow cond ctx = cond (ctx { cWindowInScope = findActive (cWindows (tlData (cNow ctx))) })

checkAnyWindow :: Cond -> Cond
checkAnyWindow cond ctx = msum $ map (\w -> cond (ctx { cWindowInScope = Just w }))
                                     (cWindows (tlData (cNow ctx)))

checkActive :: Cond
checkActive ctx = do (a,_,_) <- cWindowInScope ctx
                     guard a
		     return []

checkNumCmp ::  (Integer -> Integer -> Bool) -> String -> Integer -> Cond
checkNumCmp (<?>) "idle" num ctx = [] `justIf` (cLastActivity (tlData (cNow ctx)) <?> (num*1000))

checkTimeCmp ::  (NominalDiffTime -> NominalDiffTime -> Bool) -> String -> NominalDiffTime -> Cond
checkTimeCmp (<?>) "time" num ctx =
	let time = tlTime (cNow ctx) `diffUTCTime` (tlTime (cNow ctx)) { utctDayTime = fromIntegral 0}
	in [] `justIf` (time <?> num)
checkTimeCmp (<?>) "sampleage" num ctx =
	let age = cCurrentTime ctx `diffUTCTime` tlTime (cNow ctx)
	in [] `justIf` (age <?> num)

matchNone :: Rule
matchNone = const []

justIf :: a -> Bool -> Maybe a
justIf x True = Just x
justIf x False = Nothing

mkSecond :: (a -> b) -> a -> (a, b)
mkSecond f a = (a, f a)