From 901767b8e475f28fbd6844c73cb25e49561d174e Mon Sep 17 00:00:00 2001
From: Jens Nolte <jens@nightmarestudio.de>
Date: Fri, 31 Jan 2020 19:44:27 +0100
Subject: [PATCH] Split 'Server' into generic 'Host' and sway/i3-bar specific
 'Server'

---
 src/QBar/Host.hs   | 149 ++++++++++++++++++++++++-
 src/QBar/Server.hs | 271 +++++++++++++--------------------------------
 2 files changed, 224 insertions(+), 196 deletions(-)

diff --git a/src/QBar/Host.hs b/src/QBar/Host.hs
index dfb8f58..5cc2fe8 100644
--- a/src/QBar/Host.hs
+++ b/src/QBar/Host.hs
@@ -1,12 +1,134 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+
 module QBar.Host where
 
+import QBar.BlockOutput
 import QBar.Core
 
+import Control.Concurrent (forkIO, forkFinally, threadDelay)
 import Control.Concurrent.Event as Event
-import Control.Concurrent.STM.TChan (TChan, newTChanIO)
+import Control.Concurrent.STM.TChan (TChan, newTChanIO, tryReadTChan)
+import Control.Exception (SomeException, catch)
+import Control.Lens hiding (each, (.=))
+import Control.Monad (when)
+import Control.Monad.STM (atomically)
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
+import Data.Maybe (catMaybes, mapMaybe)
+import qualified Data.Text.Lazy as T
+import Pipes
+import System.IO (stderr, hPutStrLn)
+import System.Posix.Signals
+
+data HostHandle = HostHandle {
+  barUpdateEvent :: BarUpdateEvent,
+  newBlockChan :: TChan CachedBlock,
+  eventHandlerListIORef :: IORef [(T.Text, BlockEventHandler)]
+}
+
+installSignalHandlers :: Bar -> IO ()
+installSignalHandlers bar = void $ installHandler sigCONT (Catch sigContAction) Nothing
+  where
+    sigContAction :: IO ()
+    sigContAction = do
+      hPutStrLn stderr "SIGCONT received"
+      updateBar' bar
+
+eventDispatcher :: IORef [(T.Text, BlockEventHandler)] -> Consumer BlockEvent BarIO ()
+eventDispatcher eventHandlerListIORef = eventDispatcher'
+  where
+    eventDispatcher' :: Consumer BlockEvent BarIO ()
+    eventDispatcher' = do
+      blockEvent <- await
+      bar <- askBar
+      eventHandlerList <- liftIO $ readIORef eventHandlerListIORef
+      let maybeEventHandler = getEventHandler eventHandlerList blockEvent
+      case maybeEventHandler of
+        Just eventHandler -> liftIO . void . forkIO $ catch (runBarIO bar $ eventHandler blockEvent) (\(e :: SomeException) -> hPutStrLn stderr $ "event handler failed: " <> show e)
+        Nothing -> return ()
+      eventDispatcher'
+    getEventHandler :: [(T.Text, BlockEventHandler)] -> BlockEvent -> Maybe BlockEventHandler
+    getEventHandler eventHandlerList blockEvent = lookup (name blockEvent) eventHandlerList
+
+
+runBlocks :: HostHandle -> Producer [BlockOutput] BarIO ()
+runBlocks HostHandle{barUpdateEvent, newBlockChan, eventHandlerListIORef} = runBlocks' []
+  where
+    runBlocks' :: [CachedBlock] -> Producer [BlockOutput] BarIO ()
+    runBlocks' blocks = do
+      liftIO $ do
+        -- Wait for an update request
+        Event.wait barUpdateEvent
+
+        -- Wait for 10ms after first events to catch (almost-)simultaneous event updates
+        threadDelay 10000
+        Event.clear barUpdateEvent
+
+      blocks' <- lift $ addNewBlocks blocks
+
+      (blockStates, blocks'') <- lift $ getBlockStates blocks'
+
+      -- Pass blocks to output
+      yield $ map fst $ catMaybes blockStates
+
+      -- Register new event handlers immediately after rendering
+      liftIO $ updateEventHandlers blockStates
+
+      -- Wait for 90ms after rendering a line to limit cpu load of rapid events
+      liftIO $ threadDelay 90000
+
+      -- Loop
+      runBlocks' blocks''
+
+    addNewBlocks :: [CachedBlock] -> BarIO [CachedBlock]
+    addNewBlocks blocks = do
+      maybeNewBlock <- liftIO $ atomically $ tryReadTChan newBlockChan
+      case maybeNewBlock of
+        Nothing -> return blocks
+        Just newBlock -> addNewBlocks (newBlock:blocks)
 
-runBarHost :: (TChan CachedBlock -> BarUpdateEvent -> BarIO ()) -> IO ()
-runBarHost host = do
+    getBlockStates :: [CachedBlock] -> BarIO ([BlockState], [CachedBlock])
+    getBlockStates blocks = unzip . catMaybes <$> mapM getBlockState blocks
+
+    getBlockState :: CachedBlock -> BarIO (Maybe (BlockState, CachedBlock))
+    getBlockState producer = do
+      next' <- next producer
+      return $ case next' of
+        Left _ -> Nothing
+        Right (blockState, newProducer) -> Just (blockState, newProducer)
+
+    updateEventHandlers :: [BlockState] -> IO ()
+    updateEventHandlers blockStates =
+      writeIORef eventHandlerListIORef eventHandlerList
+      where
+        eventHandlerList :: [(T.Text, BlockEventHandler)]
+        eventHandlerList = mapMaybe getEventHandler $ blockStates
+
+        getEventHandler :: BlockState -> Maybe (T.Text, BlockEventHandler)
+        getEventHandler Nothing = Nothing
+        getEventHandler (Just (_, Nothing)) = Nothing
+        getEventHandler (Just (blockOutput, Just eventHandler)) = do
+          blockName' <- blockOutput^.blockName
+          return (blockName', eventHandler)
+
+
+filterDuplicates :: (Monad m, Eq a) => Pipe a a m r
+filterDuplicates = do
+  value <- await
+  yield value
+  filterDuplicates' value
+  where
+    filterDuplicates' :: (Monad m, Eq a) => a -> Pipe a a m r
+    filterDuplicates' lastValue = do
+      value <- await
+      when (lastValue /= value) $ yield value
+      filterDuplicates' value
+
+
+runBarHost :: Consumer [BlockOutput] BarIO ()
+  -> Producer BlockEvent BarIO ()
+  -> IO ()
+runBarHost host barEventProducer = do
   -- Create an event used to signal bar updates
   barUpdateEvent <- Event.newSet
   let requestBarUpdate = Event.set barUpdateEvent
@@ -15,4 +137,23 @@ runBarHost host = do
   newBlockChan <- newTChanIO
 
   let bar = Bar { requestBarUpdate, newBlockChan }
-  runBarIO bar (host newBlockChan barUpdateEvent)
+
+  -- Install signal handler for SIGCONT
+  installSignalHandlers bar
+
+  -- Create IORef for event handlers
+  eventHandlerListIORef <- newIORef []
+
+  let hostHandle = HostHandle {
+    barUpdateEvent,
+    newBlockChan,
+    eventHandlerListIORef
+  }
+
+  let handleStdin = runEffect $ barEventProducer >-> eventDispatcher eventHandlerListIORef
+  -- Fork stdin handler
+  void $ forkFinally (runBarIO bar handleStdin) (\result -> hPutStrLn stderr $ "handleStdin failed: " <> show result)
+
+  -- Run bar host
+  runBarIO bar $ runEffect $ runBlocks hostHandle >-> filterDuplicates >-> host
+
diff --git a/src/QBar/Server.hs b/src/QBar/Server.hs
index f466972..24729d1 100644
--- a/src/QBar/Server.hs
+++ b/src/QBar/Server.hs
@@ -1,6 +1,7 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+
 module QBar.Server where
 
-import QBar.Blocks
 import QBar.BlockOutput
 import QBar.BlockText
 import QBar.Core
@@ -10,45 +11,26 @@ import QBar.Filter
 import QBar.Host
 import QBar.Themes
 
-import Control.Monad (forever, when, unless)
+import Control.Monad (forever, when, unless, forM_)
 import Control.Monad.STM (atomically)
-import Control.Concurrent (threadDelay, forkFinally)
 import Control.Concurrent.Async
-import Control.Concurrent.Event as Event
-import Control.Concurrent.STM.TChan (TChan, newTChanIO, readTChan, tryReadTChan)
+import Control.Concurrent.STM.TChan (newTChanIO, readTChan)
 import Data.Aeson (encode, decode, ToJSON, toJSON, object, (.=))
 import Data.ByteString.Lazy (hPut)
 import qualified Data.ByteString.Char8 as BSSC8
 import qualified Data.ByteString.Lazy as BS
 import qualified Data.ByteString.Lazy.Char8 as C8
 import Data.IORef
-import Data.Maybe (catMaybes, mapMaybe, fromMaybe)
+import Data.Maybe (fromMaybe)
 import qualified Data.Text.Lazy as T
-import Data.Time.Clock.POSIX
 import Pipes
-import System.IO (stdin, stdout, stderr, hFlush, hPutStrLn)
-import System.Posix.Signals
+import System.IO (stdin, stdout, stderr, hFlush)
 import Control.Lens hiding (each, (.=))
 
-data Handle = Handle {
-  handleActionList :: IORef [(T.Text, BlockEventHandler)],
-  handleActiveFilter :: IORef Filter
-}
-
 renderIndicator :: CachedBlock
 -- Using 'cachedBlock' is a hack to actually get the block to update on every bar update (by doing this it will not get a cache later in the pipeline).
 renderIndicator = forever $ each $ map (mkBlockState . mkBlockOutput . normalText) ["/", "-", "\\", "|"]
 
-runBlock :: CachedBlock -> BarIO (Maybe (BlockState, CachedBlock))
-runBlock producer = do
-  next' <- next producer
-  return $ case next' of
-    Left _ -> Nothing
-    Right (blockState, newProducer) -> Just (blockState, newProducer)
-
-runBlocks :: [CachedBlock] -> BarIO ([BlockState], [CachedBlock])
-runBlocks blocks = unzip . catMaybes <$> mapM runBlock blocks
-
 data RenderBlock = RenderBlock T.Text (Maybe T.Text) (Maybe T.Text)
   deriving(Show)
 instance ToJSON RenderBlock where
@@ -56,195 +38,100 @@ instance ToJSON RenderBlock where
     fullText'' <> shortText'' <> blockName'' <> pango''
     where
       fullText'' = [ "full_text" .= fullText' ]
-      shortText'' = fromMaybe (\s -> ["short_text".=s]) mempty shortText'
-      blockName'' = fromMaybe (\s -> ["name".=s]) mempty blockName'
+      shortText'' = fromMaybe (\s -> ["short_text" .= s]) mempty shortText'
+      blockName'' = fromMaybe (\s -> ["name" .= s]) mempty blockName'
       pango'' = [ "markup" .= ("pango" :: T.Text) ]
 
 
+-- |A consumer that accepts lists of 'BlockOutput' and renders them to stdout using the {sway,i3}bar-protocol.
+swayBarOutput :: MainOptions -> Consumer [BlockOutput] BarIO ()
+swayBarOutput MainOptions{verbose} = do
+  -- Output header
+  liftIO $ do
+    putStrLn "{\"version\":1,\"click_events\":true}"
+    putStrLn "["
 
-renderLoop :: MainOptions -> Handle -> BarUpdateEvent -> BS.ByteString -> TChan CachedBlock -> BarIO ()
-renderLoop options handle@Handle{handleActiveFilter} barUpdateEvent previousBarOutput newBlockChan = renderLoop' previousBarOutput []
+  swayBarOutput'
   where
-    addNewBlocks :: [CachedBlock] -> BarIO [CachedBlock]
-    addNewBlocks blocks = do
-      maybeNewBlock <- liftIO $ atomically $ tryReadTChan newBlockChan
-      case maybeNewBlock of
-        Nothing -> return blocks
-        Just newBlock -> addNewBlocks (newBlock:blocks)
-    renderLoop' :: BS.ByteString -> [CachedBlock] -> BarIO ()
-    renderLoop' previousBarOutput' blocks = do
-      blockFilter <- liftIO $ readIORef handleActiveFilter
-
-      -- Wait for an event (unless the filter is animated)
-      unless (isAnimatedFilter blockFilter) $ liftIO $ Event.wait barUpdateEvent
-
-      -- Wait for 10ms after first events to catch (almost-)simultaneous event updates
-      liftIO $ threadDelay 10000
-      liftIO $ Event.clear barUpdateEvent
-
-      blocks' <- addNewBlocks blocks
-
-      (blockStates, blocks'') <- runBlocks blocks'
-
-      currentBarOutput <- liftIO $ renderLine options handle blockFilter blockStates previousBarOutput'
-
-      -- Wait for 100ms after rendering a line to limit cpu load of rapid events
-      liftIO $ threadDelay 100000
-
-      renderLoop' currentBarOutput blocks''
-
-renderLine :: MainOptions -> Handle -> Filter -> [BlockState] -> BS.ByteString -> IO BS.ByteString
-renderLine MainOptions{verbose} Handle{handleActionList} blockFilter blockStates previousEncodedOutput = do
-  time <- fromRational . toRational <$> getPOSIXTime
-  let blockOutputs = map fst $ catMaybes blockStates
-  let filteredBlocks = applyFilter blockFilter time blockOutputs
-  -- let encodedOutput = encode $ map values filteredBlocks
-  let encodedOutput = encodeOutput filteredBlocks
-  let changed = previousEncodedOutput /= encodedOutput
-  when changed $ do
-    hPut stdout encodedOutput
-    putStrLn ","
-    hFlush stdout
-    -- Echo output to stderr when verbose flag is set
-    when verbose $ do
-      hPut stderr encodedOutput
-      hPut stderr "\n"
-      hFlush stderr
-
-  when verbose $ unless changed $ hPutStrLn stderr "Output unchanged"
-
-  -- Register all event handlers regardless of bar changes, because we cannot easily check if any handler has changed
-  writeIORef handleActionList eventHandlerList
-
-  return encodedOutput
-  where
-    theme :: Theme
-    theme = defaultTheme
+    swayBarOutput' :: Consumer [BlockOutput] BarIO ()
+    swayBarOutput' = do
+      blocks <- await
+
+      let encodedOutput = encodeOutput blocks
+
+      liftIO $ do
+        hPut stdout encodedOutput
+        putStrLn ","
+        hFlush stdout
+        -- Echo output to stderr when verbose flag is set
+        when verbose $ do
+          hPut stderr encodedOutput
+          hPut stderr "\n"
+          hFlush stderr
+
+      swayBarOutput'
     encodeOutput :: [BlockOutput] -> BS.ByteString
-    encodeOutput bs = encode $ zipWith encodeBlock bs $ theme bs
+    encodeOutput bs = encode $ zipWith encodeBlock bs $ defaultTheme bs
     encodeBlock :: BlockOutput -> (T.Text, Maybe T.Text) -> RenderBlock
     encodeBlock b (fullText', shortText') = RenderBlock fullText' shortText' (b^.blockName)
-    eventHandlerList :: [(T.Text, BlockEventHandler)]
-    eventHandlerList = mapMaybe getEventHandler $ blockStates
-    getEventHandler :: BlockState -> Maybe (T.Text, BlockEventHandler)
-    getEventHandler Nothing = Nothing
-    getEventHandler (Just (_, Nothing)) = Nothing
-    getEventHandler (Just (blockOutput, Just eventHandler)) = do
-      blockName' <- blockOutput^.blockName
-      return (blockName', eventHandler)
 
-createBarUpdateChannel :: IO (IO (), BarUpdateEvent)
-createBarUpdateChannel = do
-  event <- Event.newSet
-  return (Event.set event, event)
+-- |A producer that reads swaybar/i3bar-input events from stdin and emits them as 'BlockEvent's.
+swayBarInput :: MainOptions -> Producer BlockEvent BarIO ()
+swayBarInput MainOptions{verbose} = swayBarInput'
+  where
+    swayBarInput' :: Producer BlockEvent BarIO ()
+    swayBarInput' = do
+      line <- liftIO $ BSSC8.hGetLine stdin
 
-handleStdin :: MainOptions -> IORef [(T.Text, BlockEventHandler)] -> BarIO ()
-handleStdin options eventHandlerListIORef = do
-  bar <- askBar
-  liftIO $ forever $ do
-    line <- BSSC8.hGetLine stdin
+      unless (line == "[") $ do
+        -- Echo input to stderr when verbose flag is set
+        when verbose $ liftIO $ do
+          liftIO $ BSSC8.hPutStrLn stderr line
+          hFlush stderr
 
-    unless (line == "[") $ do
-      -- Echo input to stderr when verbose flag is set
-      when (verbose options) $ do
-        BSSC8.hPutStrLn stderr line
-        hFlush stderr
+        let maybeBlockEvent = decode $ removeComma $ BS.fromStrict line
+        forM_ maybeBlockEvent yield
 
-      let maybeBlockEvent = decode $ removeComma $ BS.fromStrict line
-      case maybeBlockEvent of
-        Just blockEvent -> do
-          eventHandlerList <- readIORef eventHandlerListIORef
-          let maybeEventHandler = getEventHandler eventHandlerList blockEvent
-          case maybeEventHandler of
-            Just eventHandler -> async (runBarIO bar (eventHandler blockEvent)) >>= link
-            Nothing -> return ()
-        Nothing -> return ()
+      swayBarInput'
 
-  where
-    getEventHandler :: [(T.Text, BlockEventHandler)] -> BlockEvent -> Maybe BlockEventHandler
-    getEventHandler eventHandlerList blockEvent = lookup (name blockEvent) eventHandlerList
     removeComma :: C8.ByteString -> C8.ByteString
     removeComma line
       | C8.head line == ',' = C8.tail line
       | C8.last line == ',' = C8.init line
       | otherwise = line
 
-installSignalHandlers :: BarIO ()
-installSignalHandlers = do
-  bar <- askBar
-  liftIO $ void $ installHandler sigCONT (Catch (sigContAction bar)) Nothing
-  where
-    sigContAction :: Bar -> IO ()
-    sigContAction bar = do
-      hPutStrLn stderr "SIGCONT received"
-      updateBar' bar
-
-renderInitialBlocks :: MainOptions -> Handle -> Filter -> IO C8.ByteString
-renderInitialBlocks options handle blockFilter = do
-  date <- dateBlockOutput
-  let initialBlocks = [mkBlockState date]
-  -- Attach spinner indicator when verbose flag is set
-  let initialBlocks' = if indicator options then initialBlocks <> [mkBlockState $ mkBlockOutput . normalText $ "*"] else initialBlocks
-  -- Render initial time block so the bar is not empty after startup
-  renderLine options handle blockFilter initialBlocks' ""
-
 
 runBarServer :: BarIO () -> MainOptions -> IO ()
-runBarServer defaultBarConfig options = do
-  putStrLn "{\"version\":1,\"click_events\":true}"
-  putStrLn "["
-
-  runBarHost (\newBlockChan barUpdateEvent -> do
-
-
-    -- Create IORef to contain the active filter
-    let initialBlockFilter = StaticFilter None
-    activeFilter <- liftIO $ newIORef initialBlockFilter
-
-    -- Create IORef for event handlers
-    eventHandlerListIORef <- liftIO $ newIORef []
-
-    let handle = Handle {
-      handleActionList = eventHandlerListIORef,
-      handleActiveFilter = activeFilter
-    }
-
-    initialOutput <- liftIO $ renderInitialBlocks options handle initialBlockFilter
-
-    bar <- askBar
-    -- Fork stdin handler
-    liftIO $ void $ forkFinally (runBarIO bar (handleStdin options eventHandlerListIORef)) (\result -> hPutStrLn stderr $ "handleStdin failed: " <> show result)
-
-
-    loadBlocks
-
-    -- Install signal handler for SIGCONT
-    installSignalHandlers
-
-    -- Create control socket
-    commandChan <- liftIO createCommandChan
-    controlSocketAsync <- liftIO $ listenUnixSocketAsync options commandChan
-    liftIO $ link controlSocketAsync
-
-    -- Update bar on control socket messages
-    socketUpdateAsync <- liftIO $ async $ forever $ do
-      command <- atomically $ readTChan commandChan
-      case command of
-        SetFilter blockFilter -> atomicWriteIORef activeFilter blockFilter
-        Block -> error "TODO"
-      updateBar' bar
-    liftIO $ link socketUpdateAsync
-
-    renderLoop options handle barUpdateEvent initialOutput newBlockChan
-    )
-      where
-        loadBlocks :: BarIO ()
-        loadBlocks = do
-          when (indicator options) $ addBlock renderIndicator
-
-          defaultBarConfig
-
-
+runBarServer defaultBarConfig options = runBarHost barServer (swayBarInput options)
+  where
+    barServer :: Consumer [BlockOutput] BarIO ()
+    barServer = do
+      -- Create IORef to contain the active filter
+      let initialBlockFilter = StaticFilter None
+      activeFilter <- liftIO $ newIORef initialBlockFilter
+
+      -- Load blocks
+      lift $ do
+        when (indicator options) $ addBlock renderIndicator
+        defaultBarConfig
+
+      -- Create control socket
+      commandChan <- liftIO createCommandChan
+      controlSocketAsync <- liftIO $ listenUnixSocketAsync options commandChan
+      liftIO $ link controlSocketAsync
+
+      bar <- askBar
+
+      -- Update bar on control socket messages
+      socketUpdateAsync <- liftIO $ async $ forever $ do
+        command <- atomically $ readTChan commandChan
+        case command of
+          SetFilter blockFilter -> atomicWriteIORef activeFilter blockFilter
+          Block -> error "TODO"
+        updateBar' bar
+      liftIO $ link socketUpdateAsync
+
+      swayBarOutput options
 
 createCommandChan :: IO CommandChan
 createCommandChan = newTChanIO
-- 
GitLab