packages feed

sensei-0.5.0: src/EventQueue.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
module EventQueue (
  EventQueue
, newQueue

, Event(..)
, FileEventType(..)
, emitEvent

, Status(..)
, processQueue

#ifdef TEST
, Action(..)
, processEvents
, combineFileEvents
, groupFileEvents
#endif
) where

import           Imports

import           Control.Monad.STM
import           Control.Concurrent.STM.TChan

import           Util

type EventQueue = TChan Event

data Event = TriggerAll | FileEvent FileEventType FilePath | Done
  deriving (Eq, Show)

data FileEventType = FileAdded | FileRemoved | FileModified
  deriving (Eq, Show)

newQueue :: IO EventQueue
newQueue = atomically $ newTChan

emitEvent :: EventQueue -> Event -> IO ()
emitEvent chan = atomically . writeTChan chan

readEvents :: EventQueue -> IO [Event]
readEvents chan = do
  e <- atomically $ readTChan chan
  unless (isKeyboardInput e) $ do
    threadDelay 100000
  es <- atomically emptyQueue
  return (e : es)
  where
    isKeyboardInput :: Event -> Bool
    isKeyboardInput event = event == Done || event == TriggerAll

    emptyQueue :: STM [Event]
    emptyQueue = do
      mEvent <- tryReadTChan chan
      case mEvent of
        Nothing -> return []
        Just e -> (e :) <$> emptyQueue

data Status = Terminate | Reload

processQueue :: EventQueue -> IO () -> IO () -> IO Status
processQueue chan triggerAll trigger = go
  where
    go = readEvents chan >>= processEvents >>= \ case
      NoneAction -> do
        go
      TriggerAction files -> do
        output files
        trigger
        go
      TriggerAllAction -> do
        triggerAll
        go
      ReloadAction file t -> do
        output [file <> " (" <> show t <> ", reloading)"]
        return Reload
      DoneAction -> do
        return Terminate

    output :: [String] -> IO ()
    output = withInfoColor . mapM_ (putStrLn . mappend "--> ")

data Action = NoneAction | TriggerAction [FilePath] | TriggerAllAction | ReloadAction FilePath FileEventType | DoneAction
  deriving (Eq, Show)

processEvents :: [Event] -> IO Action
processEvents events = do
  files <- fileEvents events
  return $ if
    | Done `elem` events -> DoneAction
    | (file, t) : _ <- filter shouldReload files -> ReloadAction file t
    | TriggerAll `elem` events -> TriggerAllAction
    | not (null files) -> TriggerAction $ nub . sort $ map fst files
    | otherwise -> NoneAction

shouldReload :: (FilePath, FileEventType) -> Bool
shouldReload (name, event) = "Spec.hs" `isSuffixOf` name && case event of
  FileAdded -> True
  FileRemoved -> True
  FileModified -> False

fileEvents :: [Event] -> IO [(FilePath, FileEventType)]
fileEvents events = filterGitIgnored $ combineFileEvents [(p, e) | FileEvent e p <- events]

filterGitIgnored :: [(FilePath, FileEventType)] -> IO [(FilePath, FileEventType)]
filterGitIgnored events = map f <$> filterGitIgnoredFiles (map fst events)
  where
    f :: FilePath -> (FilePath, FileEventType)
    f p = (p, fromJust $ lookup p events)

combineFileEvents :: [(FilePath, FileEventType)] -> [(FilePath, FileEventType)]
combineFileEvents events = [(file, e) | (file, Just e) <- map (second combineFileEventTypes) $ groupFileEvents events]

groupFileEvents :: [(FilePath, FileEventType)] -> [(FilePath, [FileEventType])]
groupFileEvents = map (second $ map snd) . groupOn fst

groupOn :: Eq b => (a -> b) -> [a] -> [(b, [a])]
groupOn f = go
  where
    go = \ case
      [] -> []
      x : xs -> case partition (\ a -> f a == f x) xs of
        (ys, zs) -> (f x, (x : ys)) : go zs

combineFileEventTypes :: [FileEventType] -> Maybe FileEventType
combineFileEventTypes = go
  where
    go events = case events of
      [] -> Nothing
      [e] -> Just e
      e1 : e2 : es -> go $ (combine e1 e2) es

    combine e1 e2 = case (e1, e2) of
      (FileAdded, FileAdded) -> ignoreDuplicate FileAdded
      (FileAdded, FileRemoved) -> id
      (FileAdded, FileModified) -> (FileAdded :)

      (FileRemoved, FileAdded) -> (FileModified :)
      (FileRemoved, FileRemoved) -> ignoreDuplicate FileRemoved
      (FileRemoved, FileModified) -> shouldNeverHappen

      (FileModified, FileAdded) -> shouldNeverHappen
      (FileModified, FileRemoved) -> (FileRemoved :)
      (FileModified, FileModified) -> ignoreDuplicate FileModified

    ignoreDuplicate = (:)
    shouldNeverHappen = (FileModified :)