arbtt 0.1.5 → 0.2.0
raw patch · 7 files changed
+161/−40 lines, 7 files
Files
- README +24/−4
- arbtt.cabal +1/−1
- categorize.cfg +11/−0
- src/Categorize.hs +60/−21
- src/Stats.hs +11/−9
- src/capture-main.hs +51/−5
- src/stats-main.hs +3/−0
README view
@@ -25,12 +25,15 @@ full path to arbtt-capture in the Exec line there, if you did not do a system wide installation. +If you want to record samples at a different rate than one per minute, you+will have to pass the "--sample-rate" parameter to arbtt-capture. + Configuration ============= Once arbtt-capture is running, it will start recording, without further configuration. The configuration is only needed to do an analysis of the-recorded data. Thus, if you improve your categorization there, it will+recorded data. Thus, if you improve your categorization later, it will apply even to previous data samples! The configuration file needs to be placed in ~/.arbtt/categorize.cfg. An@@ -50,13 +53,17 @@ | "$" SVar "==" String | "$" SVar "/=" String | "$" SVar "=~" "/" Regex "/" | | "$" NVar NOp Number+ | "$" TVar NOp TSpec | "$" BVar- | "current window" $cond- | "any window" $cond+ | "current window" Cond+ | "any window" Cond +NOp := "<=" | "<" | "==" | ">" | ">="+ SVar := "title" | "program" NVar := "idle" BVar := "active"+TVar := "time" | "sampleage" The variables $title, $program, $active refer to the "window in scope". At first, no window is in scope. Only when evaluating the condition passed to@@ -87,6 +94,12 @@ The title or the name of the currently active program. If no program happens to be active, this tag will be ignored. +The variable $time refers to the time-of-day of the sample (i.e. the time+since 0:00 that day), while $sampleage refers to the timespan from when the+sample was created until now, the time of evaluating the statistics. They+can be compared with expressions of the type "hh:mm", for example:+ $time >= 8:00 && $time < 12:00 ==> tag time-of-day:morning,+ Statistics ========== @@ -100,6 +113,10 @@ be mixed with the report options (-m) and the reports (-i, -t, -c), but will apply to all reports. +The flag --filter (-f) allows you to sepecify an arbitrary complex+condition using the same syntax for conditions that you can use in the+categorize.cfg file.+ Some useful examples: # Only consider the time when I was programming in Haskell @@ -107,7 +124,10 @@ # Tell me what evolution folders I spend my time in when I actually do # work with e-Mail- are arbtt-stats -o Program:evolution -c Evo-Folder+ arbtt-stats -o Program:evolution -c Evo-Folder++ # Generate statistics about the last hour+ arbtt-stats -f '$sampleage < 1:00' Development ===========
arbtt.cabal view
@@ -1,5 +1,5 @@ name: arbtt-version: 0.1.5+version: 0.2.0 license: GPL license-file: LICENSE category: Desktop
categorize.cfg view
@@ -28,3 +28,14 @@ -- Out of curiosity: what percentage of my time am I actually coding Haskell? current window ($program == "gvim" && $title =~ /^[^ ]+\.hs \(/ ) ==> tag Editing-Haskell,++-- To be able to match on the time of day, I introduce tags for that as well+$time >= 2:00 && $time < 8:00 ==> tag time-of-day:night,+$time >= 8:00 && $time < 12:00 ==> tag time-of-day:morning,+$time >= 12:00 && $time < 14:00 ==> tag time-of-day:lunchtime,+$time >= 14:00 && $time < 18:00 ==> tag time-of-day:afternoon,+$time >= 18:00 && $time < 22:00 ==> tag time-of-day:evening,+$time >= 22:00 || $time < 2:00 ==> tag time-of-day:late-evening,++-- This tag always refers to the last 24h+$sampleage <= 24:00 ==> tag last-day,
src/Categorize.hs view
@@ -3,7 +3,6 @@ import Data import Data.Maybe--- import Text.Regex import qualified Text.Regex.PCRE.Light.Char8 as RE import qualified Data.Map as M import Control.Monad@@ -18,19 +17,22 @@ 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 ActivityData+type Categorizer = TimeLog CaptureData -> TimeLog (Ctx, ActivityData) type Rule = Ctx -> ActivityData type Parser a = CharParser () a data Ctx = Ctx- { cNow :: CaptureData- , cPast :: [CaptureData]- , cFuture :: [CaptureData]+ { cNow :: TimeLogEntry CaptureData+ , cPast :: [TimeLogEntry CaptureData]+ , cFuture :: [TimeLogEntry CaptureData] , cWindowInScope :: Maybe (Bool, String, String) , cSubsts :: [String]+ , cCurrentTime :: UTCTime } deriving (Show) @@ -39,19 +41,27 @@ 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:" putStrLn (show err) exitFailure- Right cat -> return ((fmap . fmap) (postpare . cat) . prepare)+ Right cat -> return ((fmap . fmap) (mkSecond (postpare . cat)) . prepare time) -prepare :: TimeLog CaptureData -> TimeLog Ctx-prepare tl = go' [] (map tlData tl) tl+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 -> do+ 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 this past future Nothing [] } :+ = 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@@ -86,7 +96,7 @@ s2 <- stringLiteral lang return (s1,s2) -parseRulesBody :: Parser (Ctx -> ActivityData)+parseRulesBody :: Parser Rule parseRulesBody = do x <- parseRule choice [ do comma lang@@ -137,6 +147,16 @@ 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@@ -156,15 +176,13 @@ return $ checkNot (checkEq varname str) ] , do guard $ varname == "idle"- op <- choice $ map (\(s,o) -> reservedOp lang s >> return o)- [(">=",(>=)),- (">", (>)),- ("=", (==)),- ("==",(==)),- ("<",(<)),- ("<=",(<=))]+ 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 ]@@ -186,6 +204,16 @@ return str ] +-- | Parses a day-of-time specification (hh:mm)+parseTime :: Parser NominalDiffTime+parseTime = fmap fromIntegral $ lexeme lang $ do+ h <- digitToInt <$> digit+ mh <- optionMaybe (do{ 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@@ -237,7 +265,7 @@ listToMaybe (drop (n-1) (cSubsts ctx)) getVar v ctx | "current" `isPrefixOf` v = do let var = drop (length "current.") v- win <- findActive $ cWindows (cNow ctx)+ win <- findActive $ cWindows (tlData (cNow ctx)) getVar var (ctx { cWindowInScope = Just win }) getVar "title" ctx = do (_,t,_) <- cWindowInScope ctx@@ -259,11 +287,11 @@ findActive = find (\(a,_,_) -> a) checkCurrentwindow :: Cond -> Cond-checkCurrentwindow cond ctx = cond (ctx { cWindowInScope = findActive (cWindows (cNow ctx)) })+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 (cNow ctx))+ (cWindows (tlData (cNow ctx))) checkActive :: Cond checkActive ctx = do (a,_,_) <- cWindowInScope ctx@@ -271,11 +299,22 @@ return [] checkNumCmp :: (Integer -> Integer -> Bool) -> String -> Integer -> Cond-checkNumCmp op "idle" num ctx = [] `justIf` op (cLastActivity (cNow ctx)) (num*1000)+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)
src/Stats.hs view
@@ -10,28 +10,30 @@ import qualified Data.Map as M import Data+import Categorize data Report = GeneralInfos | TotalTime | Category String deriving Eq -data Filter = Exclude Activity | Only Activity | AlsoInactive+data Filter = Exclude Activity | Only Activity | AlsoInactive | GeneralCond String deriving Eq data ReportOption = MinPercentage Double deriving Eq -applyFilters :: [Filter] -> TimeLog ActivityData -> TimeLog ActivityData+applyFilters :: [Filter] -> TimeLog (Ctx, ActivityData) -> TimeLog (Ctx, ActivityData) applyFilters filters tle = foldr (\flag -> case flag of Exclude act -> excludeTag act Only act -> onlyTag act AlsoInactive -> id+ GeneralCond s-> applyCond s ) (if AlsoInactive `elem` filters then tle else defaultFilter tle) filters -excludeTag act = filter (notElem act . tlData)-onlyTag act = filter (elem act . tlData)+excludeTag act = filter (notElem act . snd . tlData)+onlyTag act = filter (elem act . snd . tlData) defaultFilter = excludeTag inactiveActivity -- | to be used lazily, to re-use computation when generating more than one@@ -46,11 +48,11 @@ , fractionSel :: Double , fractionSelRec :: Double , sums :: M.Map Activity Integer- , allTags :: TimeLog ActivityData- , tags :: TimeLog ActivityData+ , allTags :: TimeLog (Ctx, ActivityData)+ , tags :: TimeLog (Ctx, ActivityData) } -prepareCalculations :: TimeLog ActivityData -> TimeLog ActivityData -> Calculations+prepareCalculations :: TimeLog (Ctx, ActivityData) -> TimeLog (Ctx, ActivityData) -> Calculations prepareCalculations allTags tags = let c = Calculations { firstDate = tlTime (head allTags)@@ -67,9 +69,9 @@ } in c -- | Sums up each occurence of an 'Activity', weighted by the sampling rate-sumUp :: TimeLog ActivityData -> M.Map Activity Integer+sumUp :: TimeLog (Ctx, ActivityData) -> M.Map Activity Integer sumUp = foldr go (M.empty) - where go tl m = foldr go' m (tlData tl)+ where go tl m = foldr go' m (snd (tlData tl)) where go' act = M.insertWith (+) act (tlRate tl)
src/capture-main.hs view
@@ -2,9 +2,6 @@ import Control.Monad import Control.Concurrent--import Capture-import TimeLog import System.Directory import System.FilePath import Graphics.X11.XScreenSaver (compiledWithXScreenSaver)@@ -13,10 +10,41 @@ import System.IO.Error import System.Exit import System.Locale.SetLocale+import System.Console.GetOpt+import System.Environment+import Data.Maybe+import Data.Version (showVersion) --- | sampleRate in seconds-sampleRate = 60 +import Capture+import TimeLog +import Paths_arbtt (version)+++data Conf = Conf {+ cSampleRate :: Integer+ }+defaultConf = Conf 60++data Opts = Help | Version | SetRate Integer+ deriving Eq++versionStr = "arbtt-capture " ++ showVersion version+header = "Usage: arbtt-capture [OPTIONS...]"++options :: [OptDescr Opts]+options = + [ Option "h?" ["help"]+ (NoArg Help)+ "show this help"+ , Option ['V'] ["version"]+ (NoArg Version)+ "show the version number"+ , Option ['r'] ["sample-rate"]+ (ReqArg (SetRate . read) "RATE")+ "set the sample rate in seconds (default: 60)"+ ] + -- | This is very raw, someone ought to improve this lockFile filename = flip catch (\e -> hPutStrLn stderr ("arbtt [Error]: Could not aquire lock for " ++ filename ++"!") >> exitFailure) $ do fd <- openFd (filename ++ ".lck") WriteOnly (Just 0644) defaultFileFlags@@ -26,6 +54,24 @@ setLocale LC_ALL (Just "") unless compiledWithXScreenSaver $ hPutStrLn stderr "arbtt [Warning]: X11 was compiled without support for XScreenSaver"+ + args <- getArgs+ flags <- case getOpt Permute options args of+ (o, [], []) | Help `notElem` o && Version `notElem` o -> return o+ (o, _, _) | Version `elem` o -> do+ hPutStrLn stderr versionStr+ exitSuccess+ (o, _, _) | Help `elem` o -> do+ hPutStr stderr (usageInfo header options)+ exitSuccess+ (_,_,errs) -> do+ hPutStr stderr (concat errs ++ usageInfo header options)+ exitFailure++ let sampleRate = foldr (.) id+ (map (\f -> case f of {SetRate r -> const r; _ -> id}) flags)+ 60+ dir <- getAppUserDataDirectory "arbtt" createDirectoryIfMissing False dir let captureFile = dir </> "capture.log"
src/stats-main.hs view
@@ -53,6 +53,9 @@ , Option [] ["also-inactive"] (NoArg (Filter AlsoInactive)) "include samples with the tag \"inactive\""+ , Option ['f'] ["filter"]+ (ReqArg (Filter . GeneralCond) "COND")+ "only consider samples matching the condition" , Option ['m'] ["min-percentage"] (ReqArg (ReportOption . MinPercentage . read) "PERC") "do not show tags with a percentage lower than PERC% (default: 1)"