packages feed

twitch 0.1.6.1 → 0.1.7.0

raw patch · 16 files changed

+348/−196 lines, 16 filesdep +filepathdep −QuickCheckdep −system-fileiodep −system-filepathnew-uploaderPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: filepath

Dependencies removed: QuickCheck, system-fileio, system-filepath

API changes (from Hackage documentation)

- Twitch: dirs :: Config -> [FilePath]
- Twitch: logger :: Config -> Issue -> IO ()
- Twitch: watchConfig :: Config -> WatchConfig
- Twitch.InternalRule: add :: InternalRule -> Action
- Twitch.InternalRule: delete :: InternalRule -> Action
- Twitch.InternalRule: dirs :: Config -> [FilePath]
- Twitch.InternalRule: filePath :: Event -> FilePath
- Twitch.InternalRule: fileTest :: InternalRule -> FileTest
- Twitch.InternalRule: instance Default Config
- Twitch.InternalRule: instance Default InternalRule
- Twitch.InternalRule: instance Show Config
- Twitch.InternalRule: instance Show InternalRule
- Twitch.InternalRule: instance Show Issue
- Twitch.InternalRule: logger :: Config -> Issue -> IO ()
- Twitch.InternalRule: modify :: InternalRule -> Action
- Twitch.InternalRule: name :: InternalRule -> String
- Twitch.InternalRule: time :: Event -> UTCTime
- Twitch.InternalRule: watchConfig :: Config -> WatchConfig
+ Twitch: [dirs] :: Config -> [FilePath]
+ Twitch: [logger] :: Config -> Issue -> IO ()
+ Twitch: [watchConfig] :: Config -> WatchConfig
+ Twitch.InternalRule: [add] :: InternalRule -> Action
+ Twitch.InternalRule: [delete] :: InternalRule -> Action
+ Twitch.InternalRule: [dirs] :: Config -> [FilePath]
+ Twitch.InternalRule: [fileTest] :: InternalRule -> FileTest
+ Twitch.InternalRule: [logger] :: Config -> Issue -> IO ()
+ Twitch.InternalRule: [modify] :: InternalRule -> Action
+ Twitch.InternalRule: [name] :: InternalRule -> String
+ Twitch.InternalRule: [watchConfig] :: Config -> WatchConfig
+ Twitch.InternalRule: instance Data.Default.Class.Default Twitch.InternalRule.Config
+ Twitch.InternalRule: instance Data.Default.Class.Default Twitch.InternalRule.InternalRule
+ Twitch.InternalRule: instance GHC.Show.Show Twitch.InternalRule.Config
+ Twitch.InternalRule: instance GHC.Show.Show Twitch.InternalRule.InternalRule
+ Twitch.InternalRule: instance GHC.Show.Show Twitch.InternalRule.Issue
- Twitch: Options :: LoggerType -> Maybe FilePath -> [FilePath] -> Bool -> DebounceType -> Double -> Int -> Bool -> Maybe FilePath -> Options
+ Twitch: Options :: LoggerType -> Maybe FilePath -> Maybe FilePath -> Bool -> DebounceType -> Double -> Int -> Bool -> Options

Files

README.md view
@@ -14,15 +14,15 @@ ```haskell {-# LANGUAGE OverloadedStrings #-} import Twitch-import Filesystem.Path.CurrentOS+import System.Process ( system )  main = defaultMain $ do-   "*.md"   |> \filePath -> system $ "pandoc -t html " ++ encodeString filePath+   "*.md"   |> \filePath -> system $ "pandoc -t html " ++ filePath    "*.html" |> \_ -> system $ "osascript refreshSafari.AppleScript" ```  Rules are specified in the `Dep` (for Dependency) monad. The library takes advantage-of the *OverloadedStrings* extension to create a Dep value from a glob pattern.+of the *OverloadedStrings* extension to create a `Dep` value from a glob pattern.  After creating a `Dep` value using a glob, event callbacks are added using prefix or infix API.
src/Twitch.hs view
@@ -105,7 +105,7 @@ import Twitch.InternalRule ( Config(..), Issue(..), InternalRule ) import Twitch.Main     ( DebounceType(..),-      Options(Options, currentDir, debounce, debounceAmount, dirsToWatch,+      Options(Options, debounce, debounceAmount, root,               logFile, pollInterval, recurseThroughDirectories, usePolling),       LoggerType(..),       defaultMain,
src/Twitch/Internal.hs view
@@ -2,15 +2,15 @@ {-# LANGUAGE OverloadedStrings               #-} {-# LANGUAGE FlexibleInstances               #-} module Twitch.Internal where-import Control.Applicative ( Applicative )-import Filesystem.Path ( FilePath )+import System.FilePath ( FilePath ) import Control.Monad.Trans.State as State-    ( State, put, modify, execState )+    ( State, put, modify, execState, get ) import qualified Twitch.Rule as Rule     ( addF, modifyF, deleteF, nameF ) import Twitch.Rule ( Rule )-import Data.Monoid ( Monoid(mempty, mappend) ) import Data.String ( IsString(..) )+import Control.Applicative -- satisfy GHC < 7.10+import Data.Monoid         -- satisfy GHC < 7.10 import Prelude hiding (FilePath)  -- | A polymorphic 'Dep'. Exported for completeness, ignore. @@ -40,9 +40,9 @@ addRule r = DepM $ State.modify (r :)  modHeadRule :: Dep -> (Rule -> Rule) -> Dep-modHeadRule dep f = do -  let res = runDep dep-  DepM $ case res of+modHeadRule (DepM dep) f = DepM $ do+  dep+  get >>= \res -> case res of     x:xs -> put $ f x : xs     r    -> put r 
src/Twitch/InternalRule.hs view
@@ -1,18 +1,21 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} module Twitch.InternalRule where-import Filesystem.Path ( FilePath )+import System.FilePath ( FilePath ) import Data.Time.Clock ( UTCTime ) import System.FSNotify-    ( Event(..),-      WatchConfig,-      watchDir,-      startManagerConf,-      WatchManager,-      defaultConfig )+  ( Event(..)+  , WatchConfig+  , WatchManager+  , defaultConfig+  , eventPath+  , eventTime+  , startManagerConf+  , watchDir+  ) import Data.Default ( Default(..) ) import Control.Monad ( when, void, forM_ )-import Data.Monoid ( Monoid(mempty), (<>) )+import Data.Monoid import Twitch.Rule ( Rule, RuleIssue ) import Prelude hiding (FilePath) import qualified Twitch.Rule as Rule@@ -52,7 +55,7 @@  toInternalRule :: FilePath -> Rule -> Either RuleIssue InternalRule toInternalRule currentDir rule = do-  test <- Rule.compilePattern currentDir $ Rule.pattern rule+  test <- Rule.compilePattern $ Rule.pattern $ Rule.makeAbsolute currentDir rule   return InternalRule      { name     = Rule.name rule     , fileTest = \x _ -> test x@@ -92,20 +95,6 @@   -- ^ logged every time an rule is fired   deriving Show --- | Retrieve the filePath of an Event-filePath :: Event -> FilePath-filePath e = case e of-  Added    x _ -> x-  Modified x _ -> x-  Removed  x _ -> x---- | Retrieve the time of an Event-time :: Event -> UTCTime-time e = case e of-  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@@ -116,13 +105,13 @@ -- | 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)+  let shouldFire = fileTest rule (eventPath event) (eventTime event)   when shouldFire $ do      logger $ IRuleFired event rule     fireRule event rule   -- TODO in the future this should use the recursive directory functions---      when appropiate+--      when appropriate -- | Start watching a directory, and run the rules on it. setupRuleForDir :: Config -> WatchManager -> [InternalRule] -> FilePath -> IO () setupRuleForDir config@(Config {..}) man rules dirPath =
src/Twitch/Main.hs view
@@ -1,49 +1,53 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} module Twitch.Main where-import Prelude hiding (log, FilePath)-import Data.Monoid ( (<>) )+import Control.Applicative -- satisfy GHC < 7.10+import Data.Monoid import Options.Applicative-    ( Applicative((<*>))-    , (<$>)-    , Parser-    , helper-    , execParser-    , value-    , short-    , progDesc-    , option-    , metavar-    , long-    , info-    , help-    , header-    , fullDesc-    , auto-    , eitherReader-    , many-    , switch-    , flag-    , ParserInfo-    )+  ( Parser+  , helper+  , execParser+  , value+  , short+  , progDesc+  , option+  , metavar+  , long+  , info+  , help+  , header+  , fullDesc+  , auto+  , eitherReader+  , switch+  , flag+  , ParserInfo+  ) import Data.Default ( Default(..) ) import qualified System.FSNotify as FS import Twitch.Path ( findAllDirs ) import qualified Twitch.InternalRule as IR import System.IO-    ( IOMode(AppendMode),-      Handle,-      hPrint,-      openFile,-      hClose )+  ( IOMode(AppendMode)+  , Handle+  , hPrint+  , openFile+  , hClose+  ) import Data.Foldable ( for_ ) import Twitch.Run ( runWithConfig ) import Twitch.Internal ( Dep ) import System.Directory ( getCurrentDirectory ) import Data.Maybe ( fromMaybe )-import Filesystem.Path (FilePath)-import qualified Filesystem.Path.CurrentOS as F+import System.FilePath+  ( FilePath+  , (</>)+  , isRelative+  , isValid+  ) import Control.Monad ( liftM )+-- Moved here to suppress redundant import warnings for GHC > 7.10+import Prelude hiding (log, FilePath) -- parse the command line -- @@ -62,48 +66,44 @@ toLogger filePath lt = case lt of   LogToStdout -> return (print, Nothing)   LogToFile -> do-    handle <- openFile (F.encodeString filePath) AppendMode+    handle <- openFile filePath AppendMode     return (hPrint handle, Just handle)   NoLogger -> return (const $ return (), Nothing)  data Options = Options   { log                       :: LoggerType   -- ^ The logger type.-  --   This cooresponds to the --log or -l argument. The valid options+  --   This corresponds to the --log or -l argument. The valid options   --   are "LogToStdout", "LogToFile", and "NoLogger"   --   If "LogToFile" a file can provide with the 'logFile' field.   , logFile                   :: Maybe FilePath-  -- ^ The file to log to+  -- ^ The file to log to.   --   This is only used if the 'log' field is set to "LogToFile".-  --   This cooresponds to the --log-file or -f argument-  , dirsToWatch               :: [FilePath]-  -- ^ The directories to watch.-  --   This cooresponds to the --directories and -d argument.-  --   By default this is empty and the current directory is used+  --   This corresponds to the --log-file or -f argument.+  , root                      :: Maybe FilePath+  -- ^ The root directory to watch.+  --   This corresponds to the --root and -r argument.+  --   By default this is empty and the current directory is used.   , recurseThroughDirectories :: Bool-  -- ^ If true, main will recurse throug all subdirectories of the 'dirsToWatch'+  -- ^ If true, main will recurse through all subdirectories of the 'dirsToWatch'   --   field. Otherwise the 'dirsToWatch' will be used literally.-  --   By default this is true, and disabled with the --no-recurse-flag+  --   By default this is true, and disabled with the --no-recurse-flag .   , debounce                  :: DebounceType   -- ^ This corresponds to the debounce type used in the fsnotify library   --   The argument for default main is --debounce or -b .-  --   Valid options are "DebounceDefault", "Debounce", "NoDebounce"-  --   If "Debounce" is used then a debounce amount must be specified with the-  --   'debounceAmount'+  --   Valid options are "DebounceDefault", "Debounce", "NoDebounce".+  --   If "Debounce" is used, then a debounce amount must be specified with the+  --   'debounceAmount'.   , debounceAmount            :: Double   -- ^ The amount to debounce. This is only meaningful when 'debounce' is set   --   to 'Debounce'.-  --   It cooresponds to the --debounce-amount or -a argument+  --   It corresponds to the --debounce-amount or -a argument.   , pollInterval              :: Int   -- ^ poll interval if polling is used.-  --   This cooresponds to the --poll-interval or -i argument+  --   This corresponds to the --poll-interval or -i argument.   , usePolling                :: Bool-  -- ^ Sets polling to true if used-  --   This cooresponds to the --should-poll or -p flag-  , currentDir                :: Maybe FilePath-  -- ^ The current directory to append to the glob patterns. If Nothing then-  --   the value is whatever is returned by 'getCurrentDirectory'-  --   This cooresponds to the --current-dir or -c arguments+  -- ^ Sets polling to true if used.+  --   This corresponds to the --should-poll or -p flag.   }  data DebounceType@@ -119,13 +119,12 @@   def = Options     { log                       = NoLogger     , logFile                   = Nothing-    , dirsToWatch               = []+    , root                      = Nothing     , recurseThroughDirectories = True     , debounce                  = DebounceDefault     , debounceAmount            = 0     , pollInterval              = 10^(6 :: Int) -- 1 second     , usePolling                = False-    , currentDir                = Nothing     }  dropDoubleQuotes :: String -> String@@ -140,8 +139,8 @@ -- strip quotes if they are there readFilePath :: String -> Either String FilePath readFilePath xs = -  let filePath = F.decodeString $ stripDoubleQuotes xs-  in if F.valid filePath then+  let filePath = stripDoubleQuotes xs+  in if isValid filePath then         Right filePath      else         Left $ "invalid filePath " ++ xs @@ -163,16 +162,16 @@        <> help "Log file"        <> value (logFile def)         )-  <*> many (option (eitherReader readFilePath)-        ( long "directory"-       <> short 'd'-       <> metavar "DIRECTORIES"-       <> help "Directories to watch"+  <*> option (Just <$> eitherReader readFilePath)+        ( long "root"+       <> short 'r'+       <> metavar "ROOT"+       <> help "Root directory to watch"+       <> value (root def)         )-      )   <*> flag True False         ( long "no-recurse"-       <> short 'r'+       <> short 'n'        <> help "flag to turn off recursing"         )   <*> option auto@@ -201,13 +200,6 @@        <> short 'p'        <> help "Whether to use polling or not. Off by default"         )-  <*> option (Just <$> eitherReader readFilePath)-        ( long "current-dir"-       <> short 'c'-       <> metavar "CURRENT_DIR"-       <> help "Directory to append to the glob patterns"-       <> value (currentDir def)-        )  -- This is like run, but the config params can be over written from the defaults @@ -217,27 +209,23 @@   Debounce        -> FS.Debounce $ fromRational $ toRational amount   NoDebounce      -> FS.NoDebounce -makeAbsolute :: F.FilePath -> F.FilePath -> F.FilePath+makeAbsolute :: FilePath -> FilePath -> FilePath makeAbsolute currentDir path = -  if F.relative path then-      currentDir F.</> path+  if isRelative path then+      currentDir </> path   else        path  optionsToConfig :: Options -> IO (FilePath, IR.Config, Maybe Handle) optionsToConfig Options {..} = do-  actualCurrentDir <- F.decodeString <$> getCurrentDirectory-  let currentDir' = fromMaybe actualCurrentDir currentDir-      dirsToWatch' = if null dirsToWatch then-                       [currentDir']-                     else-                       map (makeAbsolute currentDir') dirsToWatch+  currentDir <- getCurrentDirectory+  let root' = makeAbsolute currentDir $ fromMaybe currentDir root    (logger, mhandle) <- toLogger (fromMaybe "log.txt" logFile) log-  dirsToWatch'' <- if recurseThroughDirectories then-                   (dirsToWatch' ++) <$> concatMapM findAllDirs dirsToWatch'+  dirsToWatch <- if recurseThroughDirectories then+                   (root' :) <$> findAllDirs root'                  else-                   return dirsToWatch'+                   return [root']    let watchConfig = FS.WatchConfig         { FS.confDebounce     = toDB debounceAmount debounce@@ -247,10 +235,10 @@    let config = IR.Config         { logger      = logger-        , dirs        = dirsToWatch''+        , dirs        = dirsToWatch         , watchConfig = watchConfig         }-  return (currentDir', config, mhandle)+  return (root', config, mhandle)  opts :: ParserInfo Options opts = info (helper <*> pOptions)@@ -273,12 +261,10 @@ -- | A main file that uses manually supplied options instead of parsing the passed in arguments. defaultMainWithOptions :: Options -> Dep -> IO () defaultMainWithOptions options dep = do-  (currentDir, config, mhandle) <- optionsToConfig options-  manager <- runWithConfig currentDir config dep+  (root, config, mhandle) <- optionsToConfig options+  manager <- runWithConfig root config dep   putStrLn "Type anything to quit"   _ <- getLine   for_ mhandle hClose   FS.stopManager manager-- 
src/Twitch/Path.hs view
@@ -5,37 +5,38 @@ {-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-}  module Twitch.Path-       ( fp-       , findFiles+       ( findFiles        , findDirs        , findAllDirs        , canonicalizeDirPath        , canonicalizePath        ) where-import Filesystem.Path.CurrentOS ( FilePath, encodeString, decodeString, (</>) )-import Prelude hiding (FilePath)+import Control.Applicative -- satisfy GHC < 7.10 import Control.Monad-import qualified Filesystem as FS-import qualified Filesystem.Path as FP+import System.Directory+  ( canonicalizePath+  , doesDirectoryExist+  , doesFileExist+  , getDirectoryContents+  )+import System.FilePath+  ( FilePath+  , (</>)+  , addTrailingPathSeparator+  )+-- Moved here to suppress redundant import warnings for GHC > 7.10+import Prelude hiding (FilePath) --- 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+getDirectoryContentsPath path =+  map (path </>) . filter (`notElem` [".", ".."]) <$> getDirectoryContents path  fileDirContents :: FilePath -> IO ([FilePath],[FilePath]) fileDirContents path = do   contents <- getDirectoryContentsPath path-  files <- filterM FS.isFile contents-  dirs <- filterM FS.isDirectory contents+  files <- filterM doesFileExist contents+  dirs <- filterM doesDirectoryExist contents   return (files, dirs)  findAllFiles :: FilePath -> IO [FilePath]@@ -45,11 +46,11 @@   return (files ++ concat nestedFiles)  findImmediateFiles, findImmediateDirs :: FilePath -> IO [FilePath]-findImmediateFiles = getDirectoryContentsPath >=> filterM FS.isFile >=> canonicalize+findImmediateFiles = getDirectoryContentsPath >=> filterM doesFileExist >=> canonicalize   where     canonicalize :: [FilePath] -> IO [FilePath]-    canonicalize files = mapM FS.canonicalizePath files-findImmediateDirs  = getDirectoryContentsPath >=> filterM FS.isDirectory >=> canonicalize+    canonicalize files = mapM canonicalizePath files+findImmediateDirs  = getDirectoryContentsPath >=> filterM doesDirectoryExist >=> canonicalize   where     canonicalize :: [FilePath] -> IO [FilePath]     canonicalize dirs = mapM canonicalizeDirPath dirs@@ -62,23 +63,13 @@  findFiles :: Bool -> FilePath -> IO [FilePath] findFiles True path  = findAllFiles       =<< canonicalizeDirPath path-findFiles False path = findImmediateFiles =<<  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+canonicalizeDirPath path = addTrailingPathSeparator <$> 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
@@ -3,10 +3,8 @@ module Twitch.Rule where import Prelude hiding (FilePath) import Control.Monad ( void )-import Filesystem.Path ( FilePath )-import Filesystem.Path.CurrentOS ( encodeString )+import System.FilePath ( FilePath, isRelative, (</>) ) import System.FilePath.Glob ( simplify, match, tryCompileWith, compDefault )-import Data.Monoid ( (<>) ) import Data.String ( IsString(..) ) import Data.Default ( Default(..) ) import Control.Arrow ( ArrowChoice(left) )@@ -85,16 +83,20 @@  data RuleIssue   = PatternCompliationFailed PatternText String-  deriving Show+  deriving (Show, Eq)+  +makeAbsolutePath :: FilePath -> FilePath -> FilePath+makeAbsolutePath currentDir path = +  if isRelative path then+    currentDir </> path+  else+    path+  +makeAbsolute :: FilePath -> Rule -> Rule+makeAbsolute currentDir rule +  = rule { pattern = makeAbsolutePath currentDir $ pattern rule } -compilePattern :: FilePath -               -> PatternText +compilePattern :: PatternText                 -> Either RuleIssue (FilePath -> Bool)-compilePattern currentDir pat = left (PatternCompliationFailed pat) $ do -   -- TODO is this way of adding the current directory cross platfrom okay?-   -- Does the globbing even work on windows-   -   let absolutePattern = encodeString currentDir <> "/" <> pat-   p <- tryCompileWith compDefault absolutePattern-   let test = match $ simplify p-   return $ \x -> test $ encodeString x+compilePattern pat = left (PatternCompliationFailed pat) $ do +  tryCompileWith compDefault pat >>= return . match . simplify
src/Twitch/Run.hs view
@@ -5,9 +5,7 @@     ( Config(dirs, logger), InternalRule, toInternalRule, setupRules ) import Twitch.Rule ( RuleIssue ) import Data.Either ( partitionEithers )-import Filesystem.Path ( FilePath )-import Filesystem.Path.CurrentOS ( decodeString )-import Control.Applicative ( (<$>) )+import System.FilePath ( FilePath ) import System.FSNotify ( WatchManager ) import System.Directory ( getCurrentDirectory ) import Twitch.Path ( findAllDirs )@@ -17,29 +15,22 @@  run :: Dep -> IO WatchManager run dep =  do-  currentDir <- decodeString <$> getCurrentDirectory-  dirs'       <- findAllDirs currentDir+  currentDir <- getCurrentDirectory+  dirs'      <- findAllDirs currentDir   runWithConfig currentDir (def { logger = print, dirs = dirs' }) dep  runWithConfig :: FilePath -> Config -> Dep -> IO WatchManager-runWithConfig currentDir config dep = do-  let (_issues, rules) = depToRules currentDir dep+runWithConfig root config dep = do+  let (_issues, rules) = depToRules root dep   -- TODO handle the issues somehow   -- Log and perhaps error   setupRules config rules  depToRulesWithCurrentDir :: Dep -> IO ([RuleIssue], [InternalRule]) depToRulesWithCurrentDir dep = do -  currentDir <- decodeString <$> getCurrentDirectory +  currentDir <- getCurrentDirectory   return $ depToRules currentDir dep  depToRules :: FilePath -> Dep -> ([RuleIssue], [InternalRule]) depToRules currentDir    = partitionEithers . map (toInternalRule currentDir) . runDep---  ----  
+ tests/Tests/Twitch.hs view
@@ -0,0 +1,18 @@+module Tests.Twitch where+import Test.Hspec+import qualified Tests.Twitch.Internal     as Internal+import qualified Tests.Twitch.InternalRule as InternalRule+import qualified Tests.Twitch.Main         as Main+import qualified Tests.Twitch.Path         as Path+import qualified Tests.Twitch.Rule         as Rule+import qualified Tests.Twitch.Run          as Run++tests :: Spec +tests = foldl1 (>>) +    [ Internal.tests+    , InternalRule.tests+    , Main.tests+    , Path.tests+    , Rule.tests+    , Run.tests +    ]
+ tests/Tests/Twitch/Internal.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+module Tests.Twitch.Internal where+import Control.Monad ( forM_ )+import Test.Hspec+import Twitch.Internal ( modHeadRule, runDep )+import Twitch.Rule ( Rule (pattern) )++tests :: Spec+tests = do+  describe "modHeadRule" $ it "should thread state correctly" $ do+    -- `patterns` is used as `[Rule]` and `[Dep]` here+    let patterns = ["*.md", "*.html", "*.hs", "*.cpp", "*.js"]+        dep      = forM_ patterns (flip modHeadRule id)+    fmap pattern (runDep dep) `shouldBe` fmap pattern (reverse patterns)
+ tests/Tests/Twitch/InternalRule.hs view
@@ -0,0 +1,5 @@+module Tests.Twitch.InternalRule where+import Test.Hspec++tests :: Spec+tests = return ()
+ tests/Tests/Twitch/Main.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE OverloadedStrings #-}+module Tests.Twitch.Main where+import Test.Hspec+import Options.Applicative.Extra+import Data.Monoid+import Twitch.Main+import Options.Applicative.Builder+import Prelude hiding (log)++runParser :: [String] -> Maybe Options+runParser xs = getParseResult $ execParserPure (prefs mempty) opts xs++-- testParser :: String -> +testParser f initial expected +  = fmap f (runParser [initial]) `shouldBe` Just expected+  +testParserMulti f xs expected +  = fmap f (runParser xs) `shouldBe` Just expected++tests :: Spec+tests = do+  describe "Command Line Parser" $ do+    it "parses the log option" $ do+      -- long+      testParser log "--log=LogToStdout" LogToStdout+      testParser log "--log=LogToFile"   LogToFile+      testParser log "--log=NoLogger"    NoLogger+      -- short+      testParser log "-lLogToStdout" LogToStdout+      testParser log "-lLogToFile"   LogToFile+      testParser log "-lNoLogger"    NoLogger+    +    it "parses the log-file option (long)" $ +      testParser logFile "--log-file=./log.txt" $ Just "./log.txt" +    it "parses the log-file option (short)" $ +      testParser logFile "-f./log.txt"          $ Just "./log.txt"+    +    it "parses the root option" $ do+      testParser root "--root=./src" $ Just "./src"+      testParser root "-r./src"      $ Just "./src"+    +    it "parses the recurse option" $ do+      testParser recurseThroughDirectories "--no-recurse" False+      testParser recurseThroughDirectories "-n"  False++    it "parses the debounce option" $ do+      testParser debounce "--debounce=DebounceDefault" DebounceDefault+      testParser debounce "--debounce=Debounce"        Debounce+      testParser debounce "--debounce=NoDebounce"      NoDebounce+    +      testParser debounce "-bDebounceDefault" DebounceDefault+      testParser debounce "-bDebounce"        Debounce+      testParser debounce "-bNoDebounce"      NoDebounce+    +    it "parses the debounce-amount option" $ do+      testParser debounceAmount "--debounce-amount=1.0" 1.0+      testParser debounceAmount "-a1.0" 1.0+    +    it "parses the poll-interval" $ do+      testParser pollInterval "--poll-interval=1" 1+      testParser pollInterval "-i1" 1+      +    it "parses the poll" $ do+      testParser usePolling "--should-poll" True      +      testParser usePolling "-p"     True
+ tests/Tests/Twitch/Path.hs view
@@ -0,0 +1,5 @@+module Tests.Twitch.Path where+import Test.Hspec++tests :: Spec+tests = return ()
+ tests/Tests/Twitch/Rule.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE BangPatterns  #-}+module Tests.Twitch.Rule where+import Test.Hspec+import Twitch.Rule  +import Data.IORef+import Data.Default++testSetter sel setter = do+  ref <- newIORef ""+  let rule = def `setter` writeIORef ref+      expected = "yo"+  sel rule expected+  actual <- readIORef ref+  actual `shouldBe` expected+  +testAddModify f = do+  ref <- newIORef ""+  +  let rule = def `f` writeIORef ref+      expected = "yo"+  +  add rule expected+  actual <- readIORef ref  +  actual `shouldBe` expected++  modify rule expected+  actual <- readIORef ref  +  actual `shouldBe` expected++testName :: (Rule -> String -> Rule) -> Expectation+testName f = name (f def "name") `shouldBe` "name"+++tests :: Spec+tests = do+  describe "|+" $ it " sets the add member"    $ testSetter add (|+)+  describe "|-" $ it " sets the delete member" $ testSetter delete (|-)+  describe "|%" $ it " sets the modify member" $ testSetter modify (|%)+  +  describe "|>" $ it " sets the add and modify member" $ testAddModify (|>)+  describe "|#" $ it " sets the name member" $ testName (|#)++  describe "addF"       $ it " sets the add member"    $ testSetter add $ flip addF+  describe "modifyF"    $ it " sets the modify member" $ testSetter modify $ flip modifyF+  describe "deleteF"    $ it " sets the delete member" $ testSetter delete $ flip deleteF+  describe "addModifyF" $ it " sets the add and modify member" $ +    testAddModify (flip addModifyF)+  describe "nameF" $ it " sets the name member" $ testName $ flip nameF+  +  describe "makeAbsolutePath" $ do+    it "doesn't change an absolute path" $ +      makeAbsolutePath "/usr" "/home" `shouldBe` "/home"+    it "adds the current directory if it is relative" $ do+      makeAbsolutePath "/usr" "./home" `shouldBe` "/usr/./home"+      makeAbsolutePath "/usr"   "home" `shouldBe` "/usr/home"+  +  describe "compilePattern" $ do+    it "'s happy path works" $ do+      let Right test = compilePattern "/home/*.js" +      test "/home/help.js" `shouldBe` True+      test "/usr/help.js"  `shouldBe` False++    it "with an invalid pattern fails" $ do+      let Left (PatternCompliationFailed !_ !_) = compilePattern "/home/****.js" +      return () :: IO ()
+ tests/Tests/Twitch/Run.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}+module Tests.Twitch.Run where+import Test.Hspec+import Control.Arrow (second)+import qualified Twitch.InternalRule as IR+import Twitch.Run+import Twitch+-- test depToRules++tests :: Spec+tests = describe "Dep" $ do+  it "depToRules happy path" $ +    shouldBe (second (map IR.name) (depToRules "/usr/" ("*.js" |+ print)))  +             ([], ["*.js"])
twitch.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/  name:                twitch-version:             0.1.6.1+version:             0.1.7.0 synopsis:            A high level file watcher DSL description:  Twitch is a monadic DSL and library for file watching.@@ -17,19 +17,19 @@  @   -- Use OverloadedStrings. Can't get that to show up here :(   import Twitch-  import Filesystem.Path.CurrentOS+  import System.Process ( system )  .   main = defaultMain $ do-      "*.md"   |> \\filePath -> system $ "pandoc -t html " ++ encodeString filePath+      "*.md"   |> \\filePath -> system $ "pandoc -t html " ++ filePath       "*.html" |> \\_ -> system $ "osascript refreshSafari.AppleScript"  @  . homepage:            https://github.com/jfischoff/twitch license:             MIT license-file:        LICENSE-author:              Jonathan Fischoff+author:              Jonathan Fischoff, Andreas Schacker maintainer:          jonathangfischoff@gmail.com-copyright: (c) Jonathan Fischoff 2014+copyright: (c) Jonathan Fischoff 2015 category:            System build-type:          Simple extra-source-files:  README.md@@ -58,7 +58,8 @@                   , TypeSynonymInstances                   , TemplateHaskell   build-depends: base >=4.5 && <4.9-               , system-filepath+               , directory+               , filepath                , transformers                 , directory                 , Glob @@ -66,7 +67,6 @@                , data-default                , fsnotify                , optparse-applicative-               , system-fileio   hs-source-dirs: src   default-language: Haskell2010   ghc-options: -Wall@@ -76,10 +76,16 @@   main-is: Main.hs   hs-source-dirs: tests, src   default-language: Haskell2010+  other-modules: Tests.Twitch+               , Tests.Twitch.Internal+               , Tests.Twitch.InternalRule+               , Tests.Twitch.Main+               , Tests.Twitch.Path+               , Tests.Twitch.Rule+               , Tests.Twitch.Run   build-depends: base                , hspec-               , QuickCheck >= 2.7-               , system-filepath+               , filepath                , transformers                 , directory                 , Glob @@ -87,4 +93,3 @@                , data-default                , fsnotify                , optparse-applicative-               , system-fileio