packages feed

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 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