twitch (empty) → 0.1.0.0
raw patch · 11 files changed
+747/−0 lines, 11 filesdep +Globdep +basedep +containerssetup-changed
Dependencies added: Glob, base, containers, data-default, directory, fsnotify, mtl, optparse-applicative, stm-chans, system-fileio, system-filepath, text, time
Files
- LICENSE +20/−0
- README.md +0/−0
- Setup.hs +2/−0
- src/Twitch.hs +23/−0
- src/Twitch/Internal.hs +78/−0
- src/Twitch/InternalRule.hs +142/−0
- src/Twitch/Main.hs +197/−0
- src/Twitch/Path.hs +89/−0
- src/Twitch/Rule.hs +109/−0
- src/Twitch/Run.hs +45/−0
- twitch.cabal +42/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Jonathan Fischoff++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Twitch.hs view
@@ -0,0 +1,23 @@+module Twitch + ( Dep+ , DepM+ , run+ , defaultMain+ , (|+)+ , (|%)+ , (|-)+ , (|>)+ , (|#) + , add'+ , modify'+ , delete'+ , addModify+ , Rule (..)+ , RuleAction+ , Name+ , PatternText+ ) where+import Twitch.Internal +import Twitch.Main+import Twitch.Run+import Twitch.Rule (Rule, RuleAction, Name, PatternText)
+ src/Twitch/Internal.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+module Twitch.Internal where+import Data.Map (Map)+import Control.Applicative+import Control.Monad+import Filesystem.Path+import Filesystem.Path.CurrentOS+import Prelude hiding (FilePath)+import Data.List +import Data.Text (Text)+import qualified Data.Text as T+import Control.Monad.Reader+import Control.Monad.State as State+import System.Directory+import System.FilePath.Glob+import qualified Twitch.Rule as Rule+import Twitch.Rule (Rule)+import Data.Monoid+import Data.Time.Clock+import Data.String+import Data.Default+import Control.Arrow+import Data.Either+import System.FSNotify (WatchManager)++newtype DepM a = DepM { unDepM :: State [Rule] a}+ deriving ( Functor+ , Applicative+ , Monad+ , MonadState [Rule]+ )++runDep :: Dep -> [Rule]+runDep = runDepWithState mempty++runDepWithState :: [Rule] -> Dep -> [Rule] +runDepWithState xs = flip execState xs . unDepM ++type Dep = DepM ()++instance IsString Dep where+ fromString = addRule . fromString + +addRule r = State.modify (r :)++-- TODO this should probably issue a warning +modHeadRule :: Dep -> (Rule -> Rule) -> Dep+modHeadRule dep f = do + let res = runDep dep+ case res of+ x:xs -> put $ f x : xs+ r -> put r++infixl 8 |+, |%, |-, |>, |#+(|+), (|%), (|-), (|>) :: Dep -> (FilePath -> IO a) -> Dep+-- | Set the 'add' field+x |+ f = modHeadRule x $ Rule.add' f+-- | Set the modify field+x |% f = modHeadRule x $ Rule.modify' f+-- | Set the delete field+x |- f = modHeadRule x $ Rule.delete' f+-- | Set both the 'add' and 'modify' field to the same value+x |> f = x |+ f |% f++-- | Set the name+(|#) :: Dep -> Text -> Dep+r |# p = modHeadRule r $ Rule.name' p++-- Prefix API -----------------------------------------------------------------+add', modify', delete', addModify :: (FilePath -> IO a) -> Dep -> Dep+add' = flip (|+)+modify' = flip (|%)+delete' = flip (|-)+addModify = flip (|>)
+ src/Twitch/InternalRule.hs view
@@ -0,0 +1,142 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+module Twitch.InternalRule where+import Filesystem.Path+import Data.Time.Clock+import System.FSNotify ( Event (..)+ , WatchConfig+ , watchDir+ , startManagerConf+ , WatchManager+ , defaultConfig+ )+import Data.Default+import Control.Monad+import Data.Monoid+import qualified Data.Text as T+import Data.Text (Text)+import Prelude hiding (FilePath)+import Twitch.Rule (Rule, RuleIssue)+import qualified Twitch.Rule as Rule++-- | The actions that are run when file events are triggered+type Action = FilePath -> UTCTime -> IO ()+-- | The test function to determine if a event 'Action' should get fired+type FileTest = FilePath -> UTCTime -> Bool++data InternalRule = InternalRule + { name :: Text+ -- ^ A name for debugging mostly+ , fileTest :: FileTest+ -- ^ The test to determine if the rule actions should fire+ , modify :: Action + -- ^ The action to run on Modify events+ , add :: Action + -- ^ The action to run on Add events+ , delete :: Action + -- ^ The action to run on Delete events+ }+ +instance Default InternalRule where+ def = InternalRule + { name = mempty+ , fileTest = \_ _ -> False+ , modify = def+ , add = def+ , delete = def+ }++instance Show InternalRule where+ show InternalRule {..} + = T.unpack $ "Rule { name = " + <> name + <> " }"++toInternalRule :: FilePath -> Rule -> Either RuleIssue InternalRule+toInternalRule currentDir rule = do+ test <- Rule.compilePattern currentDir $ Rule.pattern rule+ return $ InternalRule + { name = Rule.name rule+ , fileTest = \x _ -> test x+ , add = \x _ -> Rule.add rule x+ , modify = \x _ -> Rule.modify rule x+ , delete = \x _ -> Rule.delete rule x+ }++-- | Configuration to run the file watcher+data Config = Config + { log :: Issue -> IO ()+ -- ^ A logger for the issues + , dirsToWatch :: [FilePath]+ -- ^ The directories to watch+ , watchConfig :: WatchConfig+ -- ^ config for the file watcher+ } + +instance Show Config where+ show Config {..} + = "Config { dirsToWatch = " + ++ show dirsToWatch+ ++ "}"++instance Default Config where+ def = Config+ { log = def+ , dirsToWatch = def+ , watchConfig = defaultConfig+ }+ +-- | A sum type for the various issues that can be logged+data Issue + = IEvent Event+ -- ^ logged every time an event is fired+ | IRuleFired Event InternalRule+ -- ^ logged every time an rule is fired+ deriving Show++-- | Retrieve the filePath of an Event+filePath :: Event -> FilePath+filePath = \case+ Added x _ -> x+ Modified x _ -> x+ Removed x _ -> x++-- | Retrieve the time of an Event+time :: Event -> UTCTime+time = \case+ Added _ x -> x+ Modified _ x -> x+ Removed _ x -> x++-- | Run the Rule action associated with the an event +fireRule :: Event -> InternalRule -> IO ()+fireRule event rule = case event of+ Added file time -> modify rule file time+ Modified file time -> add rule file time+ Removed file time -> delete rule file time++-- | Test to see if the rule should fire and fire it+testAndFireRule :: Config -> Event -> InternalRule -> IO ()+testAndFireRule Config {..} event rule = do+ let shouldFire = fileTest rule (filePath event) (time event)+ when shouldFire $ do + log $ IRuleFired event rule+ fireRule event rule ++-- TODO in the future this should use the recursive directory functions+-- when appropiate+-- | Start watching a directory, and run the rules on it.+setupRuleForDir :: Config -> WatchManager -> [InternalRule] -> FilePath -> IO ()+setupRuleForDir config@(Config {..}) man rules dirPath = do+ -- TODO Instead of const True, this should use the rule's fileTests+ void $ watchDir man dirPath (const True) $ \event -> do + log $ IEvent event+ forM_ rules $ testAndFireRule config event++-- | Setup all of the directory watches using the rules+setupRules :: Config -> [InternalRule] -> IO WatchManager+setupRules config@(Config {..}) rules = do + man <- startManagerConf watchConfig+ forM_ dirsToWatch $ setupRuleForDir config man rules+ return man
+ src/Twitch/Main.hs view
@@ -0,0 +1,197 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+module Twitch.Main where+import Data.Monoid+import Options.Applicative+import Data.Default+import qualified System.FSNotify as FS+import Twitch.Path+import qualified Twitch.InternalRule as IR+import System.IO+import Data.Foldable (for_)+import Twitch.Run+import Twitch.Internal+import System.Directory+import Data.Maybe+import Prelude hiding (log)+import qualified Filesystem.Path as F+import qualified Filesystem.Path.CurrentOS as F+import Data.Time.Clock+import Control.Concurrent.STM.TBMQueue+import Control.Concurrent+-- parse the command line+-- +++concatMapM f = fmap concat . mapM f++data LoggerType + = LogToStdout+ | LogToFile+ | NoLogger+ deriving (Eq, Show, Read, Ord)++toLogger :: FilePath + -> LoggerType+ -> IO (IR.Issue -> IO (), Maybe Handle)+toLogger filePath = \case+ LogToStdout -> return (print, Nothing)+ LogToFile -> do + handle <- openFile filePath AppendMode+ return (hPutStrLn handle . show, Just handle)+ NoLogger -> return (const $ return (), Nothing)++data Options = Options + { log :: LoggerType+ , logFile :: Maybe FilePath+ -- ^ A logger for the issues + , dirsToWatch :: [FilePath]+ -- ^ The directories to watch+ , recurseThroughDirectories :: Bool+ , debounce :: DebounceType+ , debounceAmount :: Double+ -- ^ Debounce configuration+ , pollInterval :: Int+ -- ^ poll interval+ , usePolling :: Bool+ -- ^ config for the file watch+ , currentDir :: Maybe FilePath+ }++data DebounceType + = DebounceDefault+ | Debounce+ | NoDebounce+ deriving (Eq, Show, Read, Ord)++instance Default Options where+ def = Options+ { log = NoLogger+ , logFile = Nothing+ , dirsToWatch = []+ , recurseThroughDirectories = True+ , debounce = DebounceDefault+ , debounceAmount = 0+ , pollInterval = 10^(6 :: Int) -- 1 second+ , usePolling = False+ , currentDir = Nothing+ }++pOptions :: Parser Options+pOptions + = Options+ <$> option+ ( long "log"+ <> short 'l'+ <> metavar "LOG_TYPE"+ <> help "Type of logger. Valid options are LogToStdout | LogToFile | NoLogger" + <> value (log def)+ )+ <*> option+ ( long "log-file"+ <> short 'f'+ <> metavar "LOG_FILE"+ <> help "Log file" + <> value (logFile def)+ )+ <*> option+ ( long "directories"+ <> short 'd'+ <> metavar "DIRECTORIES"+ <> help "Directories to watch"+ <> value (dirsToWatch def)+ )+ <*> option+ ( long "recurse"+ <> short 'r'+ <> metavar "RECURSE"+ <> help "Boolean to recurse or directories or not" + <> value (recurseThroughDirectories def)+ )+ <*> option+ ( long "debounce"+ <> short 'b'+ <> metavar "DEBOUNCE"+ <> help "Target for the greeting" + <> value (debounce def)+ )+ <*> option+ ( long "debounce-amount"+ <> short 'a'+ <> metavar "DEBOUNCE_AMOUNT"+ <> help "Target for the greeting" + <> value (debounceAmount def)+ )+ <*> option+ ( long "poll-interval"+ <> short 'i'+ <> metavar "POLL_INTERVAL"+ <> help "Poll interval if polling is used"+ <> value (pollInterval def)+ )+ <*> option+ ( long "poll"+ <> short 'p'+ <> metavar "POLL"+ <> help "Whether to use polling or not" + <> value (usePolling def)+ )+ <*> option+ ( long "current-dir"+ <> short 'c'+ <> metavar "CURRENT_DIR"+ <> help "Director to append to the glob patterns" + <> value (currentDir def)+ )++-- This is like run, but the config params can be over written from the defaults++toDB amount = \case+ DebounceDefault -> FS.DebounceDefault+ Debounce -> FS.Debounce $ fromRational $ toRational amount+ NoDebounce -> FS.NoDebounce++optionsToConfig :: Options -> IO (FilePath, IR.Config, Maybe Handle)+optionsToConfig Options {..} = do + actualCurrentDir <- getCurrentDirectory+ let currentDir' = fromMaybe actualCurrentDir currentDir+ dirsToWatch' = if null dirsToWatch then+ [currentDir']+ else+ dirsToWatch+ + (logger, mhandle) <- toLogger (fromMaybe "log.txt" logFile) log + dirsToWatch'' <- if recurseThroughDirectories then + concatMapM findAllDirs $ map F.decodeString dirsToWatch'+ else + return $ map F.decodeString dirsToWatch'+ + let watchConfig = FS.WatchConfig+ { FS.confDebounce = toDB debounceAmount debounce+ , FS.confPollInterval = pollInterval+ , FS.confUsePolling = usePolling+ }+ + let config = IR.Config+ { log = logger+ , dirsToWatch = dirsToWatch''+ , watchConfig = watchConfig+ }+ return (currentDir', config, mhandle)++defaultMain :: Dep -> IO ()+defaultMain dep = do+ let opts = info (helper <*> pOptions)+ ( fullDesc+ <> progDesc "Print a greeting for TARGET"+ <> header "hello - a test for optparse-applicative" + )+ (currentDir, config, mhandle) <- optionsToConfig =<< execParser opts+ let currentDir' = F.decodeString currentDir+ manager <- runWithConfig currentDir' config dep + putStrLn "Type anything to quit"+ _ <- getLine+ for_ mhandle hClose+ FS.stopManager manager+
+ src/Twitch/Path.hs view
@@ -0,0 +1,89 @@+--+-- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org+-- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org+--+{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-}++module Twitch.Path+ ( fp+ , findFiles+ , findDirs+ , findAllDirs+ , canonicalizeDirPath+ , canonicalizePath+ ) where++import Prelude hiding (FilePath)++import Control.Monad+-- import Filesystem+-- import Filesystem.Path hiding (concat)+import Filesystem.Path.CurrentOS hiding (concat)++import qualified Filesystem as FS+import qualified Filesystem.Path as FP++-- This will ensure than any calls to fp for type coercion in FSNotify will not+-- break when/if the dependent package moves from using String to the more+-- efficient Filesystem.Path.FilePath+class ConvertFilePath a b where+ fp :: a -> b+instance ConvertFilePath FilePath String where fp = encodeString+instance ConvertFilePath String FilePath where fp = decodeString+instance ConvertFilePath String String where fp = id+instance ConvertFilePath FilePath FilePath where fp = id++getDirectoryContentsPath :: FilePath -> IO [FilePath]+getDirectoryContentsPath path = fmap (map (path </>)) $ FS.listDirectory path++fileDirContents :: FilePath -> IO ([FilePath],[FilePath])+fileDirContents path = do+ contents <- getDirectoryContentsPath path+ files <- filterM FS.isFile contents+ dirs <- filterM FS.isDirectory contents+ return (files, dirs)++findAllFiles :: FilePath -> IO [FilePath]+findAllFiles path = do+ (files, dirs) <- fileDirContents path+ nestedFiles <- mapM findAllFiles dirs+ return (files ++ concat nestedFiles)++findImmediateFiles, findImmediateDirs :: FilePath -> IO [FilePath]+findImmediateFiles = getDirectoryContentsPath >=> filterM FS.isFile >=> canonicalize+ where+ canonicalize :: [FilePath] -> IO [FilePath]+ canonicalize files = mapM FS.canonicalizePath files+findImmediateDirs = getDirectoryContentsPath >=> filterM FS.isDirectory >=> canonicalize+ where+ canonicalize :: [FilePath] -> IO [FilePath]+ canonicalize dirs = mapM canonicalizeDirPath dirs++findAllDirs :: FilePath -> IO [FilePath]+findAllDirs path = do+ dirs <- findImmediateDirs path+ nestedDirs <- mapM findAllDirs dirs+ return (dirs ++ concat nestedDirs)++findFiles :: Bool -> FilePath -> IO [FilePath]+findFiles True path = findAllFiles =<< canonicalizeDirPath path+findFiles False path = findImmediateFiles =<< canonicalizeDirPath path++findDirs :: Bool -> FilePath -> IO [FilePath]+findDirs True path = findAllDirs =<< canonicalizeDirPath path+findDirs False path = findImmediateDirs =<< canonicalizeDirPath path++-- | add a trailing slash to ensure the path indicates a directory+addTrailingSlash :: FilePath -> FilePath+addTrailingSlash p =+ if FP.null (FP.filename p) then p else+ p FP.</> FP.empty++canonicalizeDirPath :: FilePath -> IO FilePath+canonicalizeDirPath path = addTrailingSlash `fmap` FS.canonicalizePath path++-- | bugfix older version of canonicalizePath (system-fileio <= 0.3.7) loses trailing slash+canonicalizePath :: FilePath -> IO FilePath+canonicalizePath path = let was_dir = FP.null (FP.filename path) in+ if not was_dir then FS.canonicalizePath path+ else canonicalizeDirPath path
+ src/Twitch/Rule.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+module Twitch.Rule where+import Data.Map (Map)+import Control.Applicative+import Control.Monad+import Filesystem.Path+import Filesystem.Path.CurrentOS+import Prelude hiding (FilePath)+import Data.Text (Text)+import qualified Data.Text as T+import System.Directory+import System.FilePath.Glob+import Data.Monoid+import Data.Time.Clock+import Data.String+import Data.Default+import Control.Arrow+import Data.Either+import Debug.Trace+import System.FSNotify (WatchManager)++-- It doesn't appear that the current directory is need in the monad+-- The new way I am thinking about this+-- This is a Rule type and what is currently called Rule is called +-- InternalRule++type Name = Text+type PatternText = Text++-- | TODO maybe change this to have the timestamp+type RuleAction = FilePath -> IO ()++-- | The pattern entity holds a name and pattern that is compiled +-- when the rules are evaluated++-- It is worth noting that the entire API could just be this+-- Record with Default+-- There are actually three apis+-- "foo.x" .# "name" .$ \x -> print x+-- "foo.x" |> \x -> print x+-- "foo.txt" +-- { add = \x -> print x+-- , modify = \x -> print x+-- }++data Rule = Rule + { name :: Text+ , pattern :: PatternText+ , add :: RuleAction+ , modify :: RuleAction+ , delete :: RuleAction+ }++instance Default Rule where+ def = Rule + { name = ""+ , pattern = ""+ , add = def+ , modify = def+ , delete = def+ }++instance IsString Rule where+ fromString x = let packed = T.pack x in def { pattern = packed, name = packed} ++--- Infix API------------------------------------------------------------------+infixl 8 |+, |%, |-, |>, |#+(|+), (|%), (|-), (|>) :: Rule -> (FilePath -> IO a) -> Rule+-- | Set the 'add' field+x |+ f = x { add = void . f }+-- | Set the modify field+x |% f = x { modify = void . f }+-- | Set the delete field+x |- f = x { delete = void . f }+-- | Set both the 'add' and 'modify' field to the same value+x |> f = x |+ f |% f++-- | Set the name+(|#) :: Rule -> Text -> Rule+r |# p = r { name = p }++-- Prefix API -----------------------------------------------------------------+add', modify', delete', addModify :: (FilePath -> IO a) -> Rule -> Rule+add' = flip (|+)+modify' = flip (|%)+delete' = flip (|-)+name' = flip (|#)+addModify = flip (|>)++-- def & add foo & modify foo & delete foo & test tester+-- def & add foo & modify foo & delete foo & pattern tester++data RuleIssue+ = PatternCompliationFailed PatternText String+ deriving Show++compilePattern :: FilePath -> PatternText -> Either RuleIssue (FilePath -> Bool)+compilePattern currentDir pattern = left (PatternCompliationFailed pattern) $ do + -- TODO is this way of adding the current directory cross platfrom okay?+ -- Does the globbing even work on windows+ + let absolutePattern = traceShowId $ encodeString currentDir <> "/" <> T.unpack pattern+ p <- tryCompileWith compDefault absolutePattern+ let test = match $ simplify p+ return $ \x -> test $ encodeString x
+ src/Twitch/Run.hs view
@@ -0,0 +1,45 @@+module Twitch.Run where+import Twitch.Internal+import Twitch.InternalRule+import Twitch.Rule (RuleIssue)+import Data.Either+import Prelude hiding (FilePath, log)+import Filesystem.Path+import Filesystem.Path.CurrentOS+import Control.Applicative+import System.FSNotify+import System.Directory+import Twitch.Path+import Data.Default+import Debug.Trace++-- This the main interface for running a Dep++run :: Dep -> IO WatchManager+run dep = do+ currentDir <- decodeString <$> getCurrentDirectory+ dirs <- findAllDirs currentDir+ runWithConfig currentDir (def { log = print, dirsToWatch = dirs }) dep++runWithConfig :: FilePath -> Config -> Dep -> IO WatchManager+runWithConfig currentDir config dep = do+ let (issues, rules) = depToRules currentDir dep+ -- TODO handle the issues somehow+ -- Log and perhaps error+ setupRules config rules++depToRulesWithCurrentDir :: Dep -> IO ([RuleIssue], [InternalRule])+depToRulesWithCurrentDir dep = do + currentDir <- decodeString <$> getCurrentDirectory + return $ depToRules currentDir dep++depToRules :: FilePath -> Dep -> ([RuleIssue], [InternalRule])+depToRules currentDir + = partitionEithers . map (toInternalRule currentDir) . runDep+++ ++++
+ twitch.cabal view
@@ -0,0 +1,42 @@+-- Initial twitch.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/++name: twitch+version: 0.1.0.0+synopsis: A high level file watcher API+-- description: +homepage: https://github.com/jfischoff/twitch+license: MIT+license-file: LICENSE+author: Jonathan Fischoff+maintainer: jonathangfischoff@gmail.com+-- copyright: +category: System+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++library+ exposed-modules: Twitch + other-modules: Twitch.Internal+ , Twitch.InternalRule+ , Twitch.Main+ , Twitch.Path+ , Twitch.Rule+ , Twitch.Run+ other-extensions: RecordWildCards, LambdaCase, GeneralizedNewtypeDeriving, OverloadedStrings, FlexibleInstances, RankNTypes, DeriveDataTypeable, ScopedTypeVariables, MultiParamTypeClasses, TypeSynonymInstances, TemplateHaskell+ build-depends: base >=4.7 && <4.8+ , containers >=0.5 && <0.6+ , system-filepath >=0.4 && <0.5+ , text >=1.1 && <1.2+ , mtl >=2.1 && <2.2+ , directory >=1.2 && <1.3+ , Glob >=0.7 && <0.8+ , time >=1.4 && <1.5+ , data-default >=0.5 && <0.6+ , fsnotify >=0.1 && <0.2+ , optparse-applicative >=0.8 && <0.9+ , stm-chans >=3.0 && <3.1+ , system-fileio >=0.3 && <0.4+ hs-source-dirs: src+ default-language: Haskell2010