packages feed

arbtt 0.10.4 → 0.11

raw patch · 32 files changed

+331/−177 lines, 32 filesdep ~tastydep ~tasty-goldenbinary-added

Dependency ranges changed: tasty, tasty-golden

Files

arbtt.cabal view
@@ -1,5 +1,5 @@ name:               arbtt-version:            0.10.4+version:            0.11 license:            GPL-2 license-file:       LICENSE category:           Desktop@@ -288,8 +288,8 @@     TimeLog   Build-depends:       base >= 4.7 && < 5-      , tasty >= 0.7 && < 1.3-      , tasty-golden >= 2.2.0.2  && <= 2.3.3.2+      , tasty >= 0.7 && < 1.5+      , tasty-golden >= 2.2.0.2  && <= 2.3.4       , tasty-hunit >= 0.2  && < 0.11       , process-extras >= 0.2 && < 0.8       , deepseq
doc/arbtt.xml view
@@ -182,7 +182,9 @@       <itemizedlist>         <listitem><simpara>the window title</simpara></listitem>         <listitem><simpara>the program name</simpara></listitem>+        <listitem><simpara>desktop (workspace) the window belongs to</simpara></listitem>         <listitem><simpara>whether the window was the active window</simpara></listitem>+        <listitem><simpara>whether the window is hidden (minimized, on another desktop, unmapped, …)</simpara></listitem>       </itemizedlist>       Based on this information and on the rules in       <filename>categorize.cfg</filename>, the categorizer@@ -233,8 +235,9 @@     <para>       When applying the rules, the categorizer has a notion of       the <emphasis>window in scope</emphasis>, and the variables-      <literal>$title</literal>, <literal>$program</literal> and-      <literal>$active</literal> always refer to the window in scope.+      <literal>$title</literal>, <literal>$program</literal>,+      <literal>$wdesktop</literal>, <literal>$active</literal> and+      <literal>$hidden</literal> always refer to the window in scope.       By default, there is no window is in scope. Condition should be prefixed       with either <literal>current window</literal> or <literal>any       window</literal>, to define scope of these variables.@@ -403,6 +406,7 @@                  def="#g-cond"/>             </rhs>         <rhs> <quote>$active</quote> </rhs>+        <rhs> <quote>$hidden</quote> </rhs>             <rhs> <nonterminal def="#g-string"/> <nonterminal def="#g-cmpop"/>          <nonterminal def="#g-string"/> </rhs>             <rhs> <nonterminal def="#g-string"/> <nonterminal def="#g-cmpop"/>@@ -430,6 +434,7 @@         <lhs>String</lhs>         <rhs> <quote>$title</quote> </rhs>         <rhs> <quote>$program</quote> </rhs>+        <rhs> <quote>$wdesktop</quote> </rhs>         <rhs> <quote>$desktop</quote> </rhs>         <rhs> <quote>format</quote> <nonterminal def="#g-date" /> </rhs>         <rhs> <quote>"</quote> string literal <quote>"</quote> </rhs>@@ -1773,6 +1778,22 @@   <para>   The version history with changes relevant for the user is documented here.   </para>++  <sect2 id="release-notes-0.11">+    <title>Version 0.11</title>+    <itemizedlist>+      <listitem>+        <para>+	  Support for the <literal>$hidden</literal> and <literal>$wdesktop</literal> variables in the configuration file, thanks to Tomas Janousek.+	</para>+      </listitem>+      <listitem>+        <para>+	  The <option>--filter</option> option of <command>arbtt-stats</command> is greatly sped up, thanks to Tomas Janousek.+	</para>+      </listitem>+    </itemizedlist>+  </sect2>    <sect2 id="release-notes-0.10.4">     <title>Version 0.10.4</title>
src/Capture/OSX.hs view
@@ -16,9 +16,8 @@         titles <- fetchWindowTitles         foreground <- getForegroundWindow -        let winData = map (-                \(h,t,p) -> (h == foreground, T.pack t, T.pack p)-                ) titles+        let winData = [ fromWDv0 (h == foreground, T.pack t, T.pack p)+                      | (h, t, p) <- titles]          it <- fromIntegral `fmap` getIdleTime 
src/Capture/Win32.hs view
@@ -20,9 +20,8 @@         titles <- fetchWindowTitles         foreground <- getForegroundWindow -        let winData = map (-                \(h,t,p) -> (h == foreground, T.pack t, T.pack p)-                ) titles+        let winData = [ fromWDv0 (h == foreground, T.pack t, T.pack p)+                      | (h, t, p) <- titles]          it <- fromIntegral `fmap` getIdleTime 
src/Capture/X11.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE RecordWildCards #-} module Capture.X11 where  import Data@@ -17,8 +18,6 @@  setupCapture :: IO () setupCapture = do-        unless compiledWithXScreenSaver $-                hPutStrLn stderr "arbtt [Warning]: X11 was compiled without support for XScreenSaver"         loc <- supportsLocale         unless loc $ hPutStrLn stderr "arbtt [Warning]: locale unsupported"         dpy <- openDisplay ""@@ -36,21 +35,10 @@         xSetErrorHandler         let rwin = defaultRootWindow dpy -        -- Desktops-        current_desktop <- flip catchIOError (\_ -> return "") $ do-            a <- internAtom dpy "_NET_CURRENT_DESKTOP" False-            p <- getWindowProperty32 dpy a rwin-            let desk_index = do {[d] <- p; return (fromIntegral d)}--            a <- internAtom dpy "_NET_DESKTOP_NAMES" False-            tp <- getTextProperty dpy rwin a-            names <- wcTextPropertyToTextList dpy tp+        -- Desktop+        desktop <- getDesktops dpy+        current_desktop <- desktop <$> getDesktop "_NET_CURRENT_DESKTOP" dpy rwin -            return $ case desk_index of-                  Nothing -> ""-                  Just n -> if 0 <= n && n < length names-                            then names !! n-                            else show n         -- Windows         a <- internAtom dpy "_NET_CLIENT_LIST" False         p <- getWindowProperty32 dpy a rwin@@ -62,10 +50,13 @@         (fsubwin,_) <- getInputFocus dpy         fwin <- followTreeUntil dpy (`elem` wins) fsubwin -        winData <- forM wins $ \w -> (,,)-            (w == fwin) <$>-            (T.pack <$> getWindowTitle dpy w) <*>-            (T.pack <$> getProgramName dpy w)+        winData <- forM wins $ \w -> do+            let wActive = w == fwin+            wHidden <- isHidden dpy w+            wTitle <- T.pack <$> getWindowTitle dpy w+            wProgram <- T.pack <$> getProgramName dpy w+            wDesktop <- T.pack . desktop <$> getDesktop "_NET_WM_DESKTOP" dpy w+            return WindowData{..}          it <- fromIntegral `fmap` getXIdleTime dpy @@ -120,3 +111,30 @@         bracket getProp (xFree . tp_value) extract             `catchIOError` \_ -> return "" +getDesktops :: Display -> IO (Maybe Int -> String)+getDesktops dpy = do+    desktops <- flip catchIOError (\_ -> return []) $ do+        a <- internAtom dpy "_NET_DESKTOP_NAMES" False+        tp <- getTextProperty dpy (defaultRootWindow dpy) a+        dropTailNull <$> wcTextPropertyToTextList dpy tp+    let name n | 0 <= n && n < length desktops = desktops !! n+               | otherwise                     = show n+    return $ maybe "" name+  where+    -- _NET_DESKTOP_NAMES is a list of NULL-terminated strings but+    -- wcTextPropertyToTextList treats NULL as a separator,+    -- so we need to drop the final empty string+    dropTailNull [""] = []+    dropTailNull (x:xs) = x : dropTailNull xs++getDesktop :: String -> Display -> Window -> IO (Maybe Int)+getDesktop prop dpy w = flip catchIOError (\_ -> return Nothing) $ do+    a <- internAtom dpy prop False+    p <- getWindowProperty32 dpy a w+    return $ do {[d] <- p; return (fromIntegral d)}++isHidden :: Display -> Window -> IO Bool+isHidden dpy w = flip catchIOError (\_ -> return False) $ do+    a <- internAtom dpy "WM_STATE" False+    Just (state:_) <- getWindowProperty32 dpy a w+    return $ fromIntegral state /= normalState
src/Categorize.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE Rank2Types, CPP, FlexibleContexts #-} module Categorize where @@ -6,15 +8,13 @@ import qualified Text.Regex.PCRE.Light.Text as RE import qualified Data.MyText as T import Data.MyText (Text)-import Control.Applicative (empty, (<*))+import Control.Applicative (empty, (<*), (<$), (<$>)) import Control.Monad-import Control.Monad.Instances() import Control.Monad.Trans.Reader hiding (local) import Control.Monad.Reader.Class (local) import Control.Monad.Trans.Class import Data.Functor.Identity -import Control.Applicative ((<$>)) import Control.DeepSeq import Data.Char import Data.List@@ -39,8 +39,10 @@ #endif import Debug.Trace import Text.Printf+import GHC.Generics (Generic)  type Categorizer = TimeLog CaptureData -> TimeLog (Ctx, ActivityData)+type ApplyCond = TimeLogEntry (Ctx, ActivityData) -> Bool type Rule = Ctx -> ActivityData type Environment = Map String Cond @@ -48,16 +50,12 @@  data Ctx = Ctx         { cNow :: TimeLogEntry CaptureData-        , cCurrentWindow :: Maybe (Bool, Text, Text)-        , cWindowInScope :: Maybe (Bool, Text, Text)+        , cCurrentWindow :: Maybe WindowData+        , cWindowInScope :: Maybe WindowData         , cSubsts :: [Text]-        , cCurrentTime :: UTCTime-        , cTimeZone :: TimeZone+        , cCurrentTime :: ZonedTime         , conditionBindings :: Map String Cond-        } deriving Show--instance NFData Ctx where-    rnf (Ctx a b c d e f g) = a `deepseq` b `deepseq` c `deepseq` d `deepseq` e `deepseq` f `deepseq` g `deepseq` ()+        } deriving (Show, Generic, NFData)  type Cond = CtxFun [Text] @@ -94,25 +92,28 @@ readCategorizer filename = withFile filename ReadMode $ \h -> do         hSetEncoding h utf8         content <- hGetContents h-        time <- getCurrentTime-        tz <- getCurrentTimeZone-        case runParserStack (tz, Map.empty) (between (return ()) eof parseRules) filename content of+        time <- getZonedTime+        case runParserStack (zonedTimeZone time, Map.empty) (between (return ()) eof parseRules) filename content of           Left err -> do                 putStrLn "Parser error:"                 print err                 exitFailure           Right cat -> return-                (map (fmap (mkSecond (postpare . cat))) . prepare time tz)+                (map (fmap (mkSecond (postpare . cat))) . prepare time) -applyCond :: String -> TimeZone -> Environment -> TimeLogEntry (Ctx, ActivityData) -> Bool-applyCond s tz env =-        case runParserStack (tz, env) (parseCond <* eof) "command line parameter" s of-          Left err -> error (show err)-          Right c  -> isJust . c . fst . tlData+mkApplyCond :: String -> IO ApplyCond+mkApplyCond s = do+        tz <- getCurrentTimeZone+        case runParserStack (tz, Map.empty) (parseCond <* eof) "command line parameter" s of+          Left err -> do+                putStrLn "Parser error:"+                print err+                exitFailure+          Right c -> return (isJust . c . fst . tlData) -prepare :: UTCTime -> TimeZone -> TimeLog CaptureData -> TimeLog Ctx-prepare time tz = map go-  where go now  = now {tlData = Ctx now (findActive (cWindows (tlData now))) Nothing [] time tz Map.empty }+prepare :: ZonedTime -> TimeLog CaptureData -> TimeLog Ctx+prepare time = map go+  where go now  = now {tlData = Ctx now (find wActive (cWindows (tlData now))) Nothing [] time Map.empty }  -- | Here, we filter out tags appearing twice, and make sure that only one of --   each category survives@@ -134,7 +135,9 @@                 , reservedOpNames= []                 , reservedNames  = [ "title"                                    , "program"+                                   , "wdesktop"                                    , "active"+                                   , "hidden"                                    , "idle"                                    , "time"                                    , "sampleage"@@ -338,7 +341,7 @@ -- Day of week is an integer in [1..7]. evalDayOfWeek :: CondPrim -> Erring CondPrim evalDayOfWeek (CondDate df) = Right $ CondInteger $ \ctx ->-  let tz = cTimeZone ctx in+  let tz = zonedTimeZone (cCurrentTime ctx) in   (toInteger . trd3 . toWeekDate . localDay . utcToLocalTime tz) `fmap` df ctx evalDayOfWeek cp = Left $ printf   "Cannot apply day of week to an expression of type %s, only to $date."@@ -347,7 +350,7 @@ -- Day of month is an integer in [1..31]. evalDayOfMonth :: CondPrim -> Erring CondPrim evalDayOfMonth (CondDate df) = Right $ CondInteger $ \ctx ->-  let tz = cTimeZone ctx in+  let tz = zonedTimeZone (cCurrentTime ctx) in   (toInteger . trd3 . toGregorian . localDay . utcToLocalTime tz) `fmap` df ctx evalDayOfMonth cp = Left $ printf   "Cannot apply day of month to an expression of type %s, only to $date."@@ -356,7 +359,7 @@ -- Month is an integer in [1..12]. evalMonth :: CondPrim -> Erring CondPrim evalMonth (CondDate df) = Right $ CondInteger $ \ctx ->-  let tz = cTimeZone ctx in+  let tz = zonedTimeZone (cCurrentTime ctx) in   (toInteger . snd3 . toGregorian . localDay . utcToLocalTime tz) `fmap` df ctx evalMonth cp = Left $ printf   "Cannot apply month to an expression of type %s, only to $date."@@ -364,7 +367,7 @@  evalYear :: CondPrim -> Erring CondPrim evalYear (CondDate df) = Right $ CondInteger $ \ctx ->-  let tz = cTimeZone ctx in+  let tz = zonedTimeZone (cCurrentTime ctx) in   (fst3 . toGregorian . localDay . utcToLocalTime tz) `fmap` df ctx evalYear cp = Left $ printf   "Cannot apply year to an expression of type %s, only to $date."@@ -373,7 +376,7 @@ -- format date according to ISO 8601 (YYYY-MM-DD) formatDate :: CondPrim -> Erring CondPrim formatDate (CondDate df) = Right $ CondString $ \ctx ->-  let tz = cTimeZone ctx+  let tz = zonedTimeZone (cCurrentTime ctx)       local = utcToLocalTime tz `fmap` df ctx    in T.pack . formatTime defaultTimeLocale (iso8601DateFormat Nothing) <$> local formatDate cp = Left $ printf@@ -405,7 +408,9 @@                   return $ CondString (getBackref backref)              , choice [ reserved lang "title" >> return (CondString (getVar "title"))                       , reserved lang "program" >> return (CondString (getVar "program"))+                      , reserved lang "wdesktop" >> return (CondString (getVar "wdesktop"))                       , reserved lang "active" >> return (CondCond checkActive)+                      , reserved lang "hidden" >> return (CondCond checkHidden)                       , reserved lang "idle" >> return (CondInteger (getNumVar NvIdle))                       , reserved lang "time" >> return (CondTime (getTimeVar TvTime))                       , reserved lang "sampleage" >> return (CondTime (getTimeVar TvSampleAge))@@ -526,12 +531,9 @@                 let var = drop (length "current.") v                 win <- cCurrentWindow 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+getVar "title"   ctx = wTitle <$> cWindowInScope ctx+getVar "program" ctx = wProgram <$> cWindowInScope ctx+getVar "wdesktop" ctx = wDesktop <$> cWindowInScope ctx getVar "desktop" ctx = return $ cDesktop (tlData (cNow ctx)) getVar v _ = error $ "Unknown variable " ++ v @@ -541,23 +543,19 @@ getTimeVar :: TimeVar -> CtxFun NominalDiffTime getTimeVar TvTime ctx = Just $    let utc = tlTime . cNow $ ctx-       tz = cTimeZone ctx+       tz = zonedTimeZone (cCurrentTime ctx)        local = utcToLocalTime tz utc        midnightUTC = localTimeToUTC tz $ local { localTimeOfDay = midnight }     in utc `diffUTCTime` midnightUTC-getTimeVar TvSampleAge ctx = Just $ cCurrentTime ctx `diffUTCTime` tlTime (cNow ctx)+getTimeVar TvSampleAge ctx = Just $ zonedTimeToUTC (cCurrentTime ctx) `diffUTCTime` tlTime (cNow ctx)  getDateVar :: DateVar -> CtxFun UTCTime getDateVar DvDate = Just . tlTime . cNow-getDateVar DvNow = Just . cCurrentTime--findActive :: [(Bool, t, t1)] -> Maybe (Bool, t, t1)-findActive = find (\(a,_,_) -> a)+getDateVar DvNow = Just . zonedTimeToUTC . cCurrentTime -checkActive :: Cond-checkActive ctx = do (a,_,_) <- cWindowInScope ctx-                     guard a-                     return []+checkActive, checkHidden :: Cond+checkActive ctx = [] <$ (guard =<< wActive <$> cWindowInScope ctx)+checkHidden ctx = [] <$ (guard =<< wHidden <$> cWindowInScope ctx)  matchNone :: Rule matchNone = const []
src/Data.hs view
@@ -1,6 +1,11 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} module Data where +import GHC.Generics (Generic) import Data.Time import Text.ParserCombinators.ReadPrec (readP_to_Prec) import Text.ParserCombinators.ReadP hiding (get)@@ -10,6 +15,7 @@ import Data.Binary.Put import Data.Binary.Get import Data.Binary.StringRef+import Data.Bits import qualified Data.MyText as T import Data.MyText (Text) import Control.Applicative@@ -22,25 +28,24 @@         { tlTime :: UTCTime         , tlRate :: Integer -- ^ in milli-seconds         , tlData :: a }-  deriving (Show, Read)--instance Functor TimeLogEntry where-        fmap f tl = tl { tlData = f (tlData tl) }--instance NFData a => NFData (TimeLogEntry a) where-    rnf (TimeLogEntry a b c) = a `deepseq` b `deepseq` c `deepseq` ()+  deriving (Show, Read, Functor, Generic, NFData)  data CaptureData = CaptureData-        { cWindows :: [ (Bool, Text, Text) ]-                -- ^ Active window, window title, programm name+        { cWindows :: [WindowData]         , cLastActivity :: Integer -- ^ in milli-seconds         , cDesktop :: Text                 -- ^ Current desktop name         }-  deriving (Show, Read)+  deriving (Show, Read, Generic, NFData) -instance NFData CaptureData where-    rnf (CaptureData a b c) = a `deepseq` b `deepseq` c `deepseq` ()+data WindowData = WindowData+        { wActive :: Bool+        , wHidden :: Bool+        , wTitle :: Text+        , wProgram :: Text+        , wDesktop :: Text+        }+  deriving (Show, Read, Generic, NFData)  type ActivityData = [Activity] @@ -48,10 +53,7 @@         { activityCategory :: Maybe Category         , activityName :: Text         }-  deriving (Ord, Eq)--instance NFData Activity where-    rnf (Activity a b) = a `deepseq` b `deepseq` ()+  deriving (Ord, Eq, Generic, NFData)  -- | An activity with special meaning: ignored by default (i.e. for idle times) inactiveActivity = Activity Nothing "inactive"@@ -104,35 +106,63 @@         return $ UTCTime (ModifiedJulianDay d) ({-# SCC diffTimeFromRational #-} fromRational t)  instance ListOfStringable CaptureData where-  listOfStrings = concatMap (\(b,t,p) -> [t,p]) . cWindows+  -- backward compat hack: skip empty wDesktop to keep original order,+  -- but add an empty string at the end to compact empty strings as well+  listOfStrings cd = concatMap listW (cWindows cd) ++ [""]+    where listW wd = [wTitle wd, wProgram wd]+                  ++ [wDesktop wd | wDesktop wd /= ""]  instance StringReferencingBinary CaptureData where -- Versions: -- 1 First version -- 2 Using ListOfStringable+-- 3 Add cDesktop+-- 4 WindowData instead of 3-tuple; CompactNum  ls_put strs cd = do         -- A version tag-        putWord8 3+        putWord8 4         ls_put strs (cWindows cd)         ls_put strs (cLastActivity cd)         ls_put strs (cDesktop cd)  ls_get strs = do         v <- getWord8         case v of-         1 -> CaptureData <$> get <*> get <*> pure ""-         2 -> CaptureData <$> ls_get strs <*> ls_get strs <*> pure ""-         3 -> CaptureData <$> ls_get strs <*> ls_get strs <*> ls_get strs+         1 -> CaptureData <$> (map fromWDv0 . fromIntLenW <$> get) <*> get <*> pure ""+         2 -> CaptureData <$> (map fromWDv0 . fromIntLenW <$> ls_get strs) <*> ls_get strs <*> pure ""+         3 -> CaptureData <$> (map fromWDv0 . fromIntLenW <$> ls_get strs) <*> ls_get strs <*> (fromIntLen <$> ls_get strs)+         4 -> CaptureData <$> ls_get strs <*> ls_get strs <*> ls_get strs          _ -> error $ "Unsupported CaptureData version tag " ++ show v ++ "\n" ++                       "You can try to recover your data using arbtt-recover." -  -- | 'getMany n' get 'n' elements in order, without blowing the stack.-  --   From Data.Binary-getMany :: Binary a => Int -> Get [a]-getMany n = go [] n- where-    go xs 0 = return $! reverse xs-    go xs i = do x <- get-                 -- we must seq x to avoid stack overflows due to laziness in-                 -- (>>=)-                 x `seq` go (x:xs) (i-1)-{-# INLINE getMany #-}+fromIntLenW :: IntLen [(Bool, IntLen Text, IntLen Text)] -> [(Bool, Text, Text)]+fromIntLenW ws = [(a, t, p) | (a, IntLen t, IntLen p) <- fromIntLen ws]++fromWDv0 :: (Bool, Text, Text) -> WindowData+fromWDv0 (a, t, p) = WindowData{+  -- wHidden = not wActive for old data, so that rules that look at visible+  -- windows don't misfire; uncategorized is better than categorized wrong+  wActive = a, wHidden = not a, wTitle = t, wProgram = p, wDesktop = "" }++instance StringReferencingBinary WindowData where+-- Versions:+-- 0 3-tuple without version tag, handled in `instance StringReferencingBinary CaptureData`+-- 1 WindowData record; Added wHidden, wDesktop; CompactNum; bitfield+  ls_put strs WindowData{..} = do+        putWord8 1+        putWord8 ((if wActive then bit 0 else 0) .|. (if wHidden then bit 1 else 0))+        ls_put strs wTitle+        ls_put strs wProgram+        ls_put strs wDesktop+  ls_get strs = do+        v <- getWord8+        case v of+         1 -> do+             bits <- getWord8+             let wActive = testBit bits 0+             let wHidden = testBit bits 1+             wTitle <- ls_get strs+             wProgram <- ls_get strs+             wDesktop <- ls_get strs+             return WindowData{..}+         _ -> error $ "Unsupported WindowData version tag " ++ show v ++ "\n" +++                      "You can try to recover your data using arbtt-recover."
src/Data/Binary/StringRef.hs view
@@ -1,8 +1,10 @@-{-# LANGUAGE FlexibleInstances, UndecidableInstances, TypeSynonymInstances, OverlappingInstances#-}+{-# LANGUAGE FlexibleInstances, UndecidableInstances, TypeSynonymInstances #-}+{-# LANGUAGE LambdaCase #-} -module Data.Binary.StringRef +module Data.Binary.StringRef         ( ListOfStringable(..)         , StringReferencingBinary(..)+        , IntLen(..)         , ls_encode         , ls_decode         ) where@@ -47,11 +49,29 @@     ls_get strs                 = liftM5 (,,,,) (ls_get strs) (ls_get strs) (ls_get strs) (ls_get strs) (ls_get strs)  +newtype CompactNum a = CompactNum { fromCompactNum :: a }++instance (Integral a, Num a, Binary a) => StringReferencingBinary (CompactNum a) where+    ls_put _ (CompactNum i)+          | 0 <= i && i < 255 = putWord8 (fromIntegral i)+          | otherwise         = putWord8 255 >> put i+    ls_get _ = fmap CompactNum $ getWord8 >>= \case+        i | 0 <= i && i < 255 -> return (fromIntegral i)+          | otherwise         -> get+ instance StringReferencingBinary a => StringReferencingBinary [a] where-    ls_put strs l  = ls_put strs (length l) >> mapM_ (ls_put strs) l-    ls_get strs    = do n <- (ls_get strs) :: Get Int-                        ls_getMany strs n+    ls_put strs l = ls_put strs (CompactNum (length l)) >> mapM_ (ls_put strs) l+    ls_get strs   = ls_getMany strs . fromCompactNum =<< ls_get strs +instance StringReferencingBinary Text where+        ls_put strs s = case elemIndex s strs of+                Just i | 0 <= i && i < 255 ->+                        putWord8 (fromIntegral (succ i))+                _ ->    putWord8 0 >> ls_put strs (T.unpack s)+        ls_get strs = getWord8 >>= \case+                0 -> T.pack <$> ls_get strs+                i -> return $! strs !! fromIntegral (pred i)+ -- | 'ls_get strsMany n' ls_get strs 'n' elements in order, without blowing the stack. ls_getMany :: StringReferencingBinary a => [Text] -> Int -> Get [a] ls_getMany strs n = go [] n@@ -64,16 +84,28 @@ {-# INLINE ls_getMany #-}  -instance StringReferencingBinary Text where-        ls_put strs s = case elemIndex s strs of-                Just i | 0 <= i && i  < 255 - 2 ->-                        put (fromIntegral (succ i) :: Word8)-                _ ->    put (0 :: Word8) >> put s-        ls_get strs = do-                tag <- get-                case tag :: Word8 of-                  0 -> get-                  i -> return $! strs !! fromIntegral (pred i)+-- compat newtype for deserialization of v2-v4 CaptureData+newtype IntLen a = IntLen { fromIntLen :: a }++-- compat instance for deserialization of v1 CaptureData+instance Binary a => Binary (IntLen a) where+    put = put . fromIntLen+    get = IntLen <$> get++-- compat instance for deserialization of v2-v4 CaptureData+instance StringReferencingBinary a => StringReferencingBinary (IntLen [a]) where+    ls_put strs (IntLen l) = ls_put strs (length l) >> mapM_ (ls_put strs) l+    ls_get strs            = fmap IntLen $ ls_getMany strs =<< ls_get strs++-- compat instance for deserialization of v2-v4 CaptureData+instance StringReferencingBinary (IntLen Text) where+        ls_put strs (IntLen s) = case elemIndex s strs of+                Just i | 0 <= i && i < 255 ->+                        putWord8 (fromIntegral (succ i))+                _ ->    putWord8 0 >> ls_put strs (IntLen (T.unpack s))+        ls_get strs = fmap IntLen $ getWord8 >>= \case+                0 -> T.pack . fromIntLen <$> ls_get strs+                i -> return $! strs !! fromIntegral (pred i)  {- instance Binary a => StringReferencingBinary a where
src/DumpFormat.hs view
@@ -41,7 +41,7 @@         "date" .= tlTime,         "rate" .= tlRate,         "inactive" .= cLastActivity tlData,-        "windows" .= map (\(a,t,p) -> object ["active" .= a, "title" .= t, "program" .= p]) (cWindows tlData),+        "windows" .= cWindows tlData,         "desktop" .= cDesktop tlData         ] @@ -50,15 +50,30 @@         tlTime <- v .: "date"         tlRate <- v .: "rate"         cLastActivity <- v .: "inactive"-        cWindows  <- (v .: "windows") >>=-            withArray "windows" (mapM (withObject "window" $ \v ->-                (,,) <$> v .: "active" <*> v .: "title" <*> v .: "program"-            ) . toList)+        cWindows  <- v .: "windows"         cDesktop <- v .: "desktop"         let tlData = CaptureData {..}         let entry = TimeLogEntry {..}         pure entry +instance ToJSON WindowData where+    toJSON WindowData{..} = object+        [ "active" .= wActive+        , "hidden" .= wHidden+        , "title" .= wTitle+        , "program" .= wProgram+        , "desktop" .= wDesktop+        ]++instance FromJSON WindowData where+    parseJSON = withObject "window" $ \v -> do+        wActive <- v .: "active"+        wHidden <- v .:! "hidden" .!= not wActive+        wTitle <- v .: "title"+        wProgram <- v .: "program"+        wDesktop <- v .:! "desktop" .!= ""+        pure WindowData{..}+ readDumpFormat :: String -> Maybe DumpFormat readDumpFormat arg =     case map toLower arg of@@ -67,50 +82,55 @@         "json"       -> return DFJSON         _            -> Nothing -dumpActivity :: TimeLog (CaptureData, TimeZone, ActivityData) -> IO ()+dumpActivity :: TimeLog (CaptureData, ActivityData) -> IO () dumpActivity = mapM_ go  where     go tle = do-        dumpHeader tz (tlTime tle) (cLastActivity cd)+        dumpHeader (tlTime tle) (cLastActivity cd)         dumpDesktop (cDesktop cd)         mapM_ dumpWindow (cWindows cd)         dumpTags ad       where-        (cd, tz, ad) = tlData tle+        (cd, ad) = tlData tle  dumpTags :: ActivityData -> IO () dumpTags = mapM_ go   where go act = printf "    %s\n" (show act) -dumpHeader :: TimeZone -> UTCTime -> Integer -> IO ()-dumpHeader tz time lastActivity = do+dumpHeader :: UTCTime -> Integer -> IO ()+dumpHeader time lastActivity = do+    tz <- getCurrentTimeZone     printf "%s (%dms inactive):\n"         (formatTime defaultTimeLocale "%F %X" (utcToLocalTime tz time))         lastActivity -dumpWindow :: (Bool, Text, Text) -> IO ()-dumpWindow (active, title, program) = do-    printf "    %s %-15s %s\n"-        (if active then ("(*)"::String) else "( )")-        (unpack program ++ ":")-        (unpack title)+dumpWindow :: WindowData -> IO ()+dumpWindow WindowData{..} = do+    printf "    (%c)%-*s %-15s %s\n" a (dw :: Int) d p t+  where a | wActive   = '*'+          | wHidden   = ' '+          | otherwise = '.'+        (dw, d) | wDesktop == "" = (0, "")+                | otherwise      = (15, " [" ++ unpack wDesktop  ++ "]")+        p = unpack wProgram ++ ":"+        t = unpack wTitle  dumpDesktop :: Text -> IO () dumpDesktop d     | null d    = return ()     | otherwise = printf "    Current Desktop: %s\n" (unpack d) -dumpSample :: TimeZone -> TimeLogEntry CaptureData -> IO ()-dumpSample tz tle = do-    dumpHeader tz (tlTime tle) (cLastActivity (tlData tle))+dumpSample :: TimeLogEntry CaptureData -> IO ()+dumpSample tle = do+    dumpHeader (tlTime tle) (cLastActivity (tlData tle))     dumpDesktop (cDesktop (tlData tle))     mapM_ dumpWindow (cWindows (tlData tle)) -dumpSamples :: TimeZone -> DumpFormat -> TimeLog CaptureData -> IO ()-dumpSamples _ DFShow = mapM_ print+dumpSamples :: DumpFormat -> TimeLog CaptureData -> IO ()+dumpSamples DFShow = mapM_ print -dumpSamples tz DFHuman = mapM_ (dumpSample tz)+dumpSamples DFHuman = mapM_ dumpSample -dumpSamples _ DFJSON = enclose . sequence_ . intersperse (putStrLn ",") . map (LBS.putStr . encode)+dumpSamples DFJSON = enclose . sequence_ . intersperse (putStrLn ",") . map (LBS.putStr . encode)   where     enclose m = putStrLn "[" >> m >> putStrLn "]"
src/Stats.hs view
@@ -10,7 +10,7 @@     defaultFilter,     defaultReportOptions,     parseActivityMatcher,-    filterPredicate,+    mkFilterPredicate,     prepareCalculations,     processReport,     processRepeater,@@ -32,6 +32,7 @@ import System.Locale (defaultTimeLocale) #endif import Control.Applicative+import Control.Monad (forM) import Data.Strict ((:!:), Pair(..)) import qualified Data.Strict as Strict import Data.Traversable (sequenceA)@@ -94,15 +95,16 @@         | ListOfIntervals String [Interval]         | MultipleReportResults [ReportResults]         | RepeatedReportResults String [(String, ReportResults)]-        | DumpResult (TimeLog (CaptureData, TimeZone, ActivityData))+        | DumpResult (TimeLog (CaptureData, ActivityData))  -filterPredicate :: [Filter] -> TimeLogEntry (Ctx, ActivityData) -> Bool-filterPredicate filters tl =-       all (\flag -> case flag of-                Exclude act  -> excludeTag act tl-                Only act     -> onlyTag act tl-                GeneralCond s-> applyCond s (cTimeZone (fst (tlData tl))) M.empty tl) filters+mkFilterPredicate :: [Filter] -> IO ApplyCond+mkFilterPredicate filters = do+    fs <- forM filters $ \flag -> case flag of+                Exclude act  -> return $ excludeTag act+                Only act     -> return $ onlyTag act+                GeneralCond s-> mkApplyCond s+    return $ \tl -> all ($ tl) fs  filterActivity :: [ActivityFilter] -> ActivityData -> ActivityData filterActivity fs = filter (applyActivityFilter fs)@@ -274,7 +276,7 @@  processReport tz opts DumpSamples =     DumpResult <$> onSelected (mapElems toList $ fmap $-        \(cd,ad) -> (tlData (cNow cd), cTimeZone cd, filterActivity (roActivityFilter opts) ad)+        \(cd,ad) -> (tlData (cNow cd), filterActivity (roActivityFilter opts) ad)         )  calcCategories :: LeftFold (TimeLogEntry (Ctx, ActivityData)) [Category]
src/UpgradeLog1.hs view
@@ -57,6 +57,7 @@ upgrade = map $ \(TimeLogEntry a b c) -> D.TimeLogEntry a b (upgradeCD c)  upgradeCD :: CaptureData -> D.CaptureData-upgradeCD (CaptureData a b) = D.CaptureData (map (\(b,s1,s2) -> (b, T.pack s1, T.pack s2)) a) b (T.pack "")+upgradeCD (CaptureData a b) = D.CaptureData (map upgrageWD a) b (T.pack "")+ where upgrageWD (b, s1, s2) = D.fromWDv0 (b, T.pack s1, T.pack s1)  
src/capture-main.hs view
@@ -81,8 +81,7 @@      if optDump flags then do         setupCapture-        tz <- getCurrentTimeZone-        captureData >>= mkTimeLogEntry (optSampleRate flags * 1000) >>= dumpSample tz+        captureData >>= mkTimeLogEntry (optSampleRate flags * 1000) >>= dumpSample       else do         createDirectoryIfMissing False dir         lockFile (optLogFile flags)
src/dump-main.hs view
@@ -91,5 +91,4 @@     Nothing -> return captures     Just n  -> return $ takeR n captures -  tz <- getCurrentTimeZone-  dumpSamples tz (optFormat flags) captures+  dumpSamples (optFormat flags) captures
src/stats-main.hs view
@@ -202,6 +202,7 @@      exitFailure    let filters = (if optAlsoInactive flags then id else (defaultFilter:)) $ optFilters flags+  filter <- mkFilterPredicate filters    let rep = case optReports flags of                 [] -> TotalTime@@ -210,7 +211,7 @@   let repeater = foldr (.) id $ map (processRepeater tz) (optRepeater flags)    let opts = optReportOptions flags-  let fold = filterPredicate filters `adjoin` repeater (processReport tz opts rep)+  let fold = filter `adjoin` repeater (processReport tz opts rep)   let result = runLeftFold fold allTags    -- Force the results a bit, to ensure the progress bar to be shown before the title
+ tests/binversions.log view

binary file changed (absent → 3107 bytes)

+ tests/binversions_borked.log view

binary file changed (absent → 3158 bytes)

+ tests/binversions_borked_recover.out view

binary file changed (absent → 982 bytes)

+ tests/binversions_dump.out view
@@ -0,0 +1,8 @@+TimeLogEntry {tlTime = 2020-01-02 01:23:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title3", wProgram = "p3", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title5", wProgram = "p5", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = ""}], cLastActivity = 2345, cDesktop = ""}}+TimeLogEntry {tlTime = 2020-01-02 01:24:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title3", wProgram = "p3", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title5", wProgram = "p5", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = ""}], cLastActivity = 2345, cDesktop = ""}}+TimeLogEntry {tlTime = 2020-01-02 01:23:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title3", wProgram = "p3", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title5", wProgram = "p5", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = ""}], cLastActivity = 2345, cDesktop = ""}}+TimeLogEntry {tlTime = 2020-01-02 01:24:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title3", wProgram = "p3", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title5", wProgram = "p5", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = ""}], cLastActivity = 2345, cDesktop = ""}}+TimeLogEntry {tlTime = 2020-01-02 01:23:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title3", wProgram = "p3", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title5", wProgram = "p5", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = ""}], cLastActivity = 2345, cDesktop = "2"}}+TimeLogEntry {tlTime = 2020-01-02 01:24:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title3", wProgram = "p3", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "title5", wProgram = "p5", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = ""}], cLastActivity = 2345, cDesktop = "2"}}+TimeLogEntry {tlTime = 2020-01-02 01:23:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = "1"},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = "1"},WindowData {wActive = False, wHidden = False, wTitle = "title3", wProgram = "p3", wDesktop = "2"},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = "2"},WindowData {wActive = False, wHidden = False, wTitle = "title5", wProgram = "p5", wDesktop = "3"},WindowData {wActive = False, wHidden = False, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = "3"}], cLastActivity = 2345, cDesktop = "2"}}+TimeLogEntry {tlTime = 2020-01-02 01:24:45.6789 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "title1", wProgram = "p1", wDesktop = "1"},WindowData {wActive = False, wHidden = True, wTitle = "title2", wProgram = "p2", wDesktop = "1"},WindowData {wActive = False, wHidden = False, wTitle = "title3", wProgram = "p3", wDesktop = "2"},WindowData {wActive = True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = "2"},WindowData {wActive = False, wHidden = False, wTitle = "title5", wProgram = "p5", wDesktop = "3"},WindowData {wActive = False, wHidden = False, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = "3"}], cLastActivity = 2345, cDesktop = "2"}}
tests/small_borked_recover.out view

binary file changed (546 → 460 bytes)

tests/small_dump.out view
@@ -1,3 +1,3 @@-TimeLogEntry {tlTime = 2010-03-20 13:59:15.075576 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [(False,"stats-main.hs (~/darcs/arbtt/src) - GVIM","gvim"),(False,"`ghci' mrtrac@curie:~ ","urxvt"),(False,"`irssi' mrtrac@curie:~ ","urxvt"),(True,"mrtrac@curie:~/darcs/arbtt ","urxvt"),(False,"`mutt' mrtrac@curie:~ ","urxvt"),(False,"HackageDB: network-bytestring-0.1.2.1 - Vimperator","Navigator")], cLastActivity = 2909, cDesktop = ""}}-TimeLogEntry {tlTime = 2010-03-20 14:00:15.157669 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [(False,"stats-main.hs (~/darcs/arbtt/src) - GVIM","gvim"),(False,"`ghci' mrtrac@curie:~ ","urxvt"),(False,"`irssi' mrtrac@curie:~ ","urxvt"),(True,"mrtrac@curie:~/darcs/arbtt ","urxvt"),(False,"`mutt' mrtrac@curie:~ ","urxvt"),(False,"HackageDB: network-bytestring-0.1.2.1 - Vimperator","Navigator")], cLastActivity = 557, cDesktop = ""}}-TimeLogEntry {tlTime = 2010-03-20 14:01:15.220521 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [(False,"stats-main.hs (~/darcs/arbtt/src) - GVIM","gvim"),(False,"`ghci' mrtrac@curie:~ ","urxvt"),(False,"`irssi' mrtrac@curie:~ ","urxvt"),(True,"mrtrac@curie:~/darcs/arbtt ","urxvt"),(False,"`mutt' mrtrac@curie:~ ","urxvt"),(False,"HackageDB: network-bytestring-0.1.2.1 - Vimperator","Navigator")], cLastActivity = 58004, cDesktop = ""}}+TimeLogEntry {tlTime = 2010-03-20 13:59:15.075576 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "stats-main.hs (~/darcs/arbtt/src) - GVIM", wProgram = "gvim", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`ghci' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`irssi' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "mrtrac@curie:~/darcs/arbtt ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`mutt' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "HackageDB: network-bytestring-0.1.2.1 - Vimperator", wProgram = "Navigator", wDesktop = ""}], cLastActivity = 2909, cDesktop = ""}}+TimeLogEntry {tlTime = 2010-03-20 14:00:15.157669 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "stats-main.hs (~/darcs/arbtt/src) - GVIM", wProgram = "gvim", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`ghci' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`irssi' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "mrtrac@curie:~/darcs/arbtt ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`mutt' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "HackageDB: network-bytestring-0.1.2.1 - Vimperator", wProgram = "Navigator", wDesktop = ""}], cLastActivity = 557, cDesktop = ""}}+TimeLogEntry {tlTime = 2010-03-20 14:01:15.220521 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "stats-main.hs (~/darcs/arbtt/src) - GVIM", wProgram = "gvim", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`ghci' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`irssi' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "mrtrac@curie:~/darcs/arbtt ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`mutt' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "HackageDB: network-bytestring-0.1.2.1 - Vimperator", wProgram = "Navigator", wDesktop = ""}], cLastActivity = 58004, cDesktop = ""}}
tests/small_dump_json.out view
@@ -1,4 +1,4 @@ [-{"desktop":"","inactive":2909,"date":"2010-03-20T13:59:15.075576Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000},-{"desktop":"","inactive":557,"date":"2010-03-20T14:00:15.157669Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000},-{"desktop":"","inactive":58004,"date":"2010-03-20T14:01:15.220521Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}]+{"desktop":"","inactive":2909,"date":"2010-03-20T13:59:15.075576Z","windows":[{"desktop":"","active":false,"program":"gvim","hidden":true,"title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`ghci' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`irssi' mrtrac@curie:~ "},{"desktop":"","active":true,"program":"urxvt","hidden":false,"title":"mrtrac@curie:~/darcs/arbtt "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`mutt' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"Navigator","hidden":true,"title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000},+{"desktop":"","inactive":557,"date":"2010-03-20T14:00:15.157669Z","windows":[{"desktop":"","active":false,"program":"gvim","hidden":true,"title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`ghci' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`irssi' mrtrac@curie:~ "},{"desktop":"","active":true,"program":"urxvt","hidden":false,"title":"mrtrac@curie:~/darcs/arbtt "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`mutt' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"Navigator","hidden":true,"title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000},+{"desktop":"","inactive":58004,"date":"2010-03-20T14:01:15.220521Z","windows":[{"desktop":"","active":false,"program":"gvim","hidden":true,"title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`ghci' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`irssi' mrtrac@curie:~ "},{"desktop":"","active":true,"program":"urxvt","hidden":false,"title":"mrtrac@curie:~/darcs/arbtt "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`mutt' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"Navigator","hidden":true,"title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}]
tests/small_import.in view
@@ -1,3 +1,3 @@-TimeLogEntry {tlTime = 2010-03-20 13:59:15.075576 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [(False,"stats-main.hs (~/darcs/arbtt/src) - GVIM","gvim"),(False,"`ghci' mrtrac@curie:~ ","urxvt"),(False,"`irssi' mrtrac@curie:~ ","urxvt"),(True,"mrtrac@curie:~/darcs/arbtt ","urxvt"),(False,"`mutt' mrtrac@curie:~ ","urxvt"),(False,"HackageDB: network-bytestring-0.1.2.1 - Vimperator","Navigator")], cLastActivity = 2909, cDesktop = ""}}-TimeLogEntry {tlTime = 2010-03-20 14:00:15.157669 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [(False,"stats-main.hs (~/darcs/arbtt/src) - GVIM","gvim"),(False,"`ghci' mrtrac@curie:~ ","urxvt"),(False,"`irssi' mrtrac@curie:~ ","urxvt"),(True,"mrtrac@curie:~/darcs/arbtt ","urxvt"),(False,"`mutt' mrtrac@curie:~ ","urxvt"),(False,"HackageDB: network-bytestring-0.1.2.1 - Vimperator","Navigator")], cLastActivity = 557, cDesktop = ""}}-TimeLogEntry {tlTime = 2010-03-20 14:01:15.220521 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [(False,"stats-main.hs (~/darcs/arbtt/src) - GVIM","gvim"),(False,"`ghci' mrtrac@curie:~ ","urxvt"),(False,"`irssi' mrtrac@curie:~ ","urxvt"),(True,"mrtrac@curie:~/darcs/arbtt ","urxvt"),(False,"`mutt' mrtrac@curie:~ ","urxvt"),(False,"HackageDB: network-bytestring-0.1.2.1 - Vimperator","Navigator")], cLastActivity = 58004, cDesktop = ""}}+TimeLogEntry {tlTime = 2010-03-20 13:59:15.075576 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "stats-main.hs (~/darcs/arbtt/src) - GVIM", wProgram = "gvim", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`ghci' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`irssi' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "mrtrac@curie:~/darcs/arbtt ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`mutt' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "HackageDB: network-bytestring-0.1.2.1 - Vimperator", wProgram = "Navigator", wDesktop = ""}], cLastActivity = 2909, cDesktop = ""}}+TimeLogEntry {tlTime = 2010-03-20 14:00:15.157669 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "stats-main.hs (~/darcs/arbtt/src) - GVIM", wProgram = "gvim", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`ghci' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`irssi' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "mrtrac@curie:~/darcs/arbtt ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`mutt' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "HackageDB: network-bytestring-0.1.2.1 - Vimperator", wProgram = "Navigator", wDesktop = ""}], cLastActivity = 557, cDesktop = ""}}+TimeLogEntry {tlTime = 2010-03-20 14:01:15.220521 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden = True, wTitle = "stats-main.hs (~/darcs/arbtt/src) - GVIM", wProgram = "gvim", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`ghci' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`irssi' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = True, wHidden = False, wTitle = "mrtrac@curie:~/darcs/arbtt ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "`mutt' mrtrac@curie:~ ", wProgram = "urxvt", wDesktop = ""},WindowData {wActive = False, wHidden = True, wTitle = "HackageDB: network-bytestring-0.1.2.1 - Vimperator", wProgram = "Navigator", wDesktop = ""}], cLastActivity = 58004, cDesktop = ""}}
tests/small_import.out view

binary file changed (546 → 460 bytes)

tests/small_import_json.in view
@@ -1,5 +1,5 @@ {"desktop":"","inactive":2909,"date":"2010-03-20T13:59:15.075576Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}  -{"desktop":"","inactive":557,"date":"2010-03-20T14:00:15.157669Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}-{"desktop":"","inactive":58004,"date":"2010-03-20T14:01:15.220521Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}+{"desktop":"","inactive":557,"date":"2010-03-20T14:00:15.157669Z","windows":[{"desktop":"","active":false,"program":"gvim","hidden":true,"title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`ghci' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`irssi' mrtrac@curie:~ "},{"desktop":"","active":true,"program":"urxvt","hidden":false,"title":"mrtrac@curie:~/darcs/arbtt "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`mutt' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"Navigator","hidden":true,"title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}+{"desktop":"","inactive":58004,"date":"2010-03-20T14:01:15.220521Z","windows":[{"desktop":"","active":false,"program":"gvim","hidden":true,"title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`ghci' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`irssi' mrtrac@curie:~ "},{"desktop":"","active":true,"program":"urxvt","hidden":false,"title":"mrtrac@curie:~/darcs/arbtt "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`mutt' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"Navigator","hidden":true,"title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}
tests/small_import_json.out view

binary file changed (546 → 460 bytes)

tests/small_import_json_list.in view
@@ -3,6 +3,6 @@ {"desktop":"","inactive":2909,"date":"2010-03-20T13:59:15.075576Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000},  -{"desktop":"","inactive":557,"date":"2010-03-20T14:00:15.157669Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000},-{"desktop":"","inactive":58004,"date":"2010-03-20T14:01:15.220521Z","windows":[{"active":false,"program":"gvim","title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"active":false,"program":"urxvt","title":"`ghci' mrtrac@curie:~ "},{"active":false,"program":"urxvt","title":"`irssi' mrtrac@curie:~ "},{"active":true,"program":"urxvt","title":"mrtrac@curie:~/darcs/arbtt "},{"active":false,"program":"urxvt","title":"`mutt' mrtrac@curie:~ "},{"active":false,"program":"Navigator","title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000}+{"desktop":"","inactive":557,"date":"2010-03-20T14:00:15.157669Z","windows":[{"desktop":"","active":false,"program":"gvim","hidden":true,"title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`ghci' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`irssi' mrtrac@curie:~ "},{"desktop":"","active":true,"program":"urxvt","hidden":false,"title":"mrtrac@curie:~/darcs/arbtt "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`mutt' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"Navigator","hidden":true,"title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000},+{"desktop":"","inactive":58004,"date":"2010-03-20T14:01:15.220521Z","windows":[{"desktop":"","active":false,"program":"gvim","hidden":true,"title":"stats-main.hs (~/darcs/arbtt/src) - GVIM"},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`ghci' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`irssi' mrtrac@curie:~ "},{"desktop":"","active":true,"program":"urxvt","hidden":false,"title":"mrtrac@curie:~/darcs/arbtt "},{"desktop":"","active":false,"program":"urxvt","hidden":true,"title":"`mutt' mrtrac@curie:~ "},{"desktop":"","active":false,"program":"Navigator","hidden":true,"title":"HackageDB: network-bytestring-0.1.2.1 - Vimperator"}],"rate":60000} ]
tests/small_import_json_list.out view

binary file changed (546 → 460 bytes)

+ tests/small_v4.cfg view
@@ -0,0 +1,3 @@+any window (! $hidden && $wdesktop == "1") ==> tag d1_visible,+any window (! $hidden && $wdesktop == "2") ==> tag d2_visible,+any window (! $hidden && $wdesktop == "3") ==> tag d3_visible,
+ tests/small_v4.log view

binary file changed (absent → 617 bytes)

+ tests/small_v4_import.in view
@@ -0,0 +1,2 @@+TimeLogEntry {tlTime = 2020-01-02 01:23:45.678900 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden =  True, wTitle = "title1", wProgram = "p1", wDesktop = "1"} ,WindowData {wActive = False, wHidden =  True, wTitle = "title2", wProgram = "p2", wDesktop = "1"} ,WindowData {wActive = False, wHidden = False, wTitle = "title3", wProgram = "p3", wDesktop = "2"} ,WindowData {wActive =  True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = "2"} ,WindowData {wActive = False, wHidden = False, wTitle = "title5", wProgram = "p5", wDesktop = "3"} ,WindowData {wActive = False, wHidden = False, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = "3"} ], cLastActivity = 2345, cDesktop = "2"}}+TimeLogEntry {tlTime = 2020-01-02 01:24:45.678900 UTC, tlRate = 60000, tlData = CaptureData {cWindows = [WindowData {wActive = False, wHidden =  True, wTitle = "title1", wProgram = "p1", wDesktop = "1"} ,WindowData {wActive = False, wHidden =  True, wTitle = "title2", wProgram = "p2", wDesktop = "1"} ,WindowData {wActive = False, wHidden = False, wTitle = "title3", wProgram = "p3", wDesktop = "2"} ,WindowData {wActive =  True, wHidden = False, wTitle = "title4", wProgram = "p4", wDesktop = "2"} ,WindowData {wActive = False, wHidden = False, wTitle = "title5", wProgram = "p5", wDesktop = "3"} ,WindowData {wActive = False, wHidden = False, wTitle = "long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long title6", wProgram = "p6", wDesktop = "3"} ], cLastActivity = 2345, cDesktop = "2"}}
+ tests/small_v4_stats.out view
@@ -0,0 +1,5 @@+Total time per tag+==================+_______Tag_|___Time_|_Percentage_+d2_visible |  2m00s |     100.00+d3_visible |  2m00s |     100.00
tests/test.hs view
@@ -37,13 +37,15 @@ regressionTests = testGroup "Regression tests"     [ testCase "Issue #4" $ do         cat <- readCategorizer "tests/issue4.cfg"-        let sample = TimeLogEntry undefined 0 (CaptureData [(True, "aa", "program")] 0 "")+        let wd = WindowData{ wActive = True, wHidden = False, wTitle = "aa", wProgram = "program", wDesktop = "" }+        let sample = TimeLogEntry undefined 0 (CaptureData [wd] 0 "")         let [TimeLogEntry _ _ (_,acts)] = cat [sample]         [Activity (Just "Cat") "aa"] @=? acts         return ()     , testCase "Issue #5" $ do         cat <- readCategorizer "tests/issue5.cfg"-        let sample = TimeLogEntry undefined 0 (CaptureData [(True, "aa", "program")] 0 "")+        let wd = WindowData{ wActive = True, wHidden = False, wTitle = "aa", wProgram = "program", wDesktop = "" }+        let sample = TimeLogEntry undefined 0 (CaptureData [wd] 0 "")         let [TimeLogEntry _ _ (_,acts)] = cat [sample]         [Activity Nothing "A2"] @=? acts         return ()@@ -52,7 +54,8 @@         now <- getCurrentTime         let backThen = (-60*60*101) `addUTCTime` now -        let sample = TimeLogEntry backThen 0 (CaptureData [(True, "aa", "program")] 0 "")+        let wd = WindowData{ wActive = True, wHidden = False, wTitle = "aa", wProgram = "program", wDesktop = "" }+        let sample = TimeLogEntry backThen 0 (CaptureData [wd] 0 "")         let [TimeLogEntry _ _ (_,acts)] = cat [sample]         [Activity Nothing "old"] @=? acts         return ()@@ -84,7 +87,7 @@             run "arbtt-import" ["-f","tests/small_import_json_list.out.actual", "-t", "JSON"]     , goldenVsFile "recover small"         "tests/small_borked_recover.out" "tests/small_borked_recover.out.actual" $ void $-        run "arbtt-recover" ["-i","tests/small_borked_recover.out", "-o", "tests/small_borked_recover.out.actual"] B.empty+        run "arbtt-recover" ["-i","tests/small_borked.log", "-o", "tests/small_borked_recover.out.actual"] B.empty     , goldenVsString "stats small"         "tests/small_stats.out" $         run "arbtt-stats" ["--logfile", "tests/small.log", "--categorize", "tests/small.cfg"] B.empty@@ -100,6 +103,20 @@     , goldenVsString "condition binding stats"         "tests/condition_bindings_stats.out" $         run "arbtt-stats" ["--logfile", "tests/small.log", "--categorize", "tests/condition_bindings.cfg"] B.empty+    , goldenVsString "dump binversions"+        "tests/binversions_dump.out" $+        run "arbtt-dump" ["-f","tests/binversions.log", "-t", "Show"] B.empty+    , goldenVsFile "recover binversions"+        "tests/binversions_borked_recover.out" "tests/binversions_borked_recover.out.actual" $ void $+        run "arbtt-recover" ["-i","tests/binversions_borked.log", "-o", "tests/binversions_borked_recover.out.actual"] B.empty+    , goldenVsFile "import small_v4"+        "tests/small_v4.log" "tests/small_v4_import.out.actual" $ void $ do+        tryIOError $ removeFile "tests/small_v4_import.out.actual"+        B.readFile "tests/small_v4_import.in" >>=+            run "arbtt-import" ["-f","tests/small_v4_import.out.actual"]+    , goldenVsString "stats small_v4 ($hidden, $wdesktop)"+        "tests/small_v4_stats.out" $+        run "arbtt-stats" ["--logfile", "tests/small_v4.log", "--categorize", "tests/small_v4.cfg"] B.empty     ]  testParser env parser input = do