packages feed

sensei-0.8.0: src/EventQueue.hs

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

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

, Status(..)
, processQueue

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

import           Imports
import qualified Data.Set as Set

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

import           Util

type EventQueue = TChan Event

data Event =
    TriggerAll
  | FileEvent FileEventType FilePath
  | RestartWith [String]
  | 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 100_000
  es <- atomically emptyQueue
  return (e : es)
  where
    isKeyboardInput :: Event -> Bool
    isKeyboardInput = \ case
      TriggerAll -> True
      FileEvent {} -> False
      RestartWith {} -> True
      Done -> True

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

data Status = Terminate | Restart (Maybe [String])
  deriving (Eq, Show)

processQueue :: IO () -> (String -> IO ()) -> FilePath -> EventQueue -> IO () -> IO () -> IO Status
processQueue cleanup echo dir chan triggerAll trigger = go
  where
    go :: IO Status
    go = do
      action <- readEvents chan >>= processEvents echo dir
      runCleanup action
      case action of
        NoneAction -> do
          go
        TriggerAction files -> do
          output files
          trigger
          go
        TriggerAllAction -> do
          triggerAll
          go
        RestartAction file t -> do
          output [file <> " (" <> show t <> ", restarting)"]
          return $ Restart Nothing
        RestartWithAction args -> do
          return $ Restart (Just args)
        DoneAction -> do
          return Terminate

    runCleanup :: Action -> IO ()
    runCleanup = \ case
      NoneAction -> pass
      TriggerAction {} -> cleanup
      TriggerAllAction -> cleanup
      RestartAction {} -> cleanup
      RestartWithAction {} -> cleanup
      DoneAction -> cleanup

    output :: [String] -> IO ()
    output = mapM_ (\ name -> echo . withInfoColor $ "--> " <> name <> "\n")

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

processEvents :: (String -> IO ()) -> FilePath -> [Event] -> IO Action
processEvents echo dir events = do
  files <- fileEvents echo dir events
  return $ if
    | Done `elem` events -> DoneAction
    | args : _ <- [args | RestartWith args <- reverse events] -> RestartWithAction args
    | (file, t) : _ <- filter shouldRestart files -> RestartAction file t
    | TriggerAll `elem` events -> TriggerAllAction
    | not (null files) -> TriggerAction . Set.toList . Set.fromList $ map fst files
    | otherwise -> NoneAction

shouldRestart :: (FilePath, FileEventType) -> Bool
shouldRestart = (||) <$> specAddedOrRemoved <*> dotGhciModified
  where
    specAddedOrRemoved :: (FilePath, FileEventType) -> Bool
    specAddedOrRemoved (name, event) = "Spec.hs" `isSuffixOf` name && case event of
      FileAdded -> True
      FileRemoved -> True
      FileModified -> False

    dotGhciModified :: (FilePath, FileEventType) -> Bool
    dotGhciModified (name, _) = takeFileName name == ".ghci"

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

filterGitIgnored :: (String -> IO ()) -> FilePath -> [(FilePath, FileEventType)] -> IO [(FilePath, FileEventType)]
filterGitIgnored echo dir events = map f <$> filterGitIgnoredFiles echo dir (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 :)