diff --git a/src/QBar/ControlSocket.hs b/src/QBar/ControlSocket.hs index 17ea5df4b848b1f4920695fff9b7dba144f89f86..9b3bc85f56e22cdecdefc3ac066b5e6e914ab7f3 100644 --- a/src/QBar/ControlSocket.hs +++ b/src/QBar/ControlSocket.hs @@ -12,7 +12,7 @@ import Control.Monad (forever, void, when) import Control.Monad.STM (atomically) import Control.Concurrent (forkFinally) import Control.Concurrent.Async -import Control.Concurrent.STM.TChan (TChan, writeTChan) +import Control.Concurrent.STM.TChan (TChan, newTChanIO, writeTChan) import Data.Aeson.TH import Data.ByteString (ByteString) import System.FilePath ((</>)) @@ -33,7 +33,7 @@ import System.Environment (getEnv) type CommandChan = TChan Command -data Command = SetTheme T.Text +data Command = SetTheme TL.Text deriving Show data SocketResponse = Success | Error Text @@ -42,6 +42,9 @@ data SocketResponse = Success | Error Text $(deriveJSON defaultOptions ''Command) $(deriveJSON defaultOptions ''SocketResponse) +createCommandChan :: IO CommandChan +createCommandChan = newTChanIO + ipcSocketAddress :: MainOptions -> IO FilePath ipcSocketAddress MainOptions{socketLocation} = maybe defaultSocketPath (return . T.unpack) socketLocation where diff --git a/src/QBar/Server.hs b/src/QBar/Server.hs index fb42fe2b04371845690d10b76ed3dd93bc4ff2c1..ec09b0d67f0051f0648c46df7570fe3aad1e7d5f 100644 --- a/src/QBar/Server.hs +++ b/src/QBar/Server.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE ScopedTypeVariables #-} module QBar.Server where @@ -9,10 +10,15 @@ import QBar.ControlSocket import QBar.Host import QBar.Pango import QBar.Theme +import QBar.Util import Control.Monad (forever, when, unless, forM_) --- import Control.Concurrent.Async -import Control.Concurrent.STM.TChan (newTChanIO) +import Control.Concurrent.Async (async, link) +import Control.Concurrent.Event as Event +import Control.Concurrent.MVar (MVar, newMVar, modifyMVar, modifyMVar_) +import Control.Concurrent.STM (atomically) +import Control.Concurrent.STM.TChan (readTChan) +import Control.Exception (throw) import Data.Aeson (encode, decode, ToJSON, toJSON, object, (.=)) import Data.ByteString.Lazy (hPut) import qualified Data.ByteString.Char8 as BSSC8 @@ -20,7 +26,10 @@ import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy.Char8 as C8 import Data.Maybe (fromMaybe) import qualified Data.Text.Lazy as T +import qualified Data.Text.Lazy.IO as TIO import Pipes +import Pipes.Concurrent (Input, spawn, latest, toOutput, fromInput) +import qualified Pipes.Prelude as PP import System.IO (stdin, stdout, stderr, hFlush) renderIndicator :: CachedBlock @@ -42,43 +51,6 @@ instance ToJSON PangoBlock where 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 "[" - - swayBarOutput' - where - swayBarOutput' :: Consumer [BlockOutput] BarIO () - swayBarOutput' = do - blocks <- await - - let themedOutput = defaultTheme blocks - let encodedOutput = encodeOutput themedOutput - - 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 :: [ThemedBlockOutput] -> BS.ByteString - encodeOutput blocks = encode $ map renderPangoBlock $ blocks - renderPangoBlock :: ThemedBlockOutput -> PangoBlock - renderPangoBlock ThemedBlockOutput{_fullText, _shortText, _blockName} = PangoBlock { - pangoBlockFullText = renderPango _fullText, - pangoBlockShortText = renderPango <$> _shortText, - pangoBlockName = _blockName - } - -- |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' @@ -105,6 +77,29 @@ swayBarInput MainOptions{verbose} = swayBarInput' | otherwise = line +outputLine :: MainOptions -> [ThemedBlockOutput] -> IO () +outputLine MainOptions{verbose} themedBlocks = do + let encodedOutput = encodeOutput themedBlocks + + 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 + where + encodeOutput :: [ThemedBlockOutput] -> BS.ByteString + encodeOutput blocks = encode $ map renderPangoBlock $ blocks + renderPangoBlock :: ThemedBlockOutput -> PangoBlock + renderPangoBlock ThemedBlockOutput{_fullText, _shortText, _blockName} = PangoBlock { + pangoBlockFullText = renderPango _fullText, + pangoBlockShortText = renderPango <$> _shortText, + pangoBlockName = _blockName + } + runBarServer :: BarIO () -> MainOptions -> IO () runBarServer defaultBarConfig options = runBarHost barServer (swayBarInput options) where @@ -115,24 +110,71 @@ runBarServer defaultBarConfig options = runBarHost barServer (swayBarInput optio when (indicator options) $ addBlock renderIndicator defaultBarConfig - -- Create control socket - -- commandChan <- liftIO createCommandChan - -- controlSocketAsync <- liftIO $ listenUnixSocketAsync options commandChan - -- liftIO $ link controlSocketAsync - -- bar <- askBar + -- Event to render the bar (fired when block output or theme is changed) + renderEvent <- liftIO Event.new + + -- Mailbox to store the latest 'BlockOutput's + (output, input) <- liftIO $ spawn $ latest [] + + -- MVar that holds the current theme, linked to the input from the above mailbox + (themedBlockProducerMVar :: MVar (Producer [ThemedBlockOutput] IO (), Bool)) <- liftIO $ newMVar $ (return (), False) + + + -- 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 - -- void $ error "TODO" - -- updateBar' bar - -- liftIO $ link socketUpdateAsync + socketUpdateAsync <- liftIO $ async $ forever $ do + command <- atomically $ readTChan commandChan + case command of + SetTheme name -> do + let result = findTheme name + case result of + Left err -> TIO.hPutStrLn stderr err + Right theme -> do + setTheme input themedBlockProducerMVar theme + Event.signal renderEvent + liftIO $ link socketUpdateAsync - swayBarOutput options + liftIO $ do + -- Set default theme + setTheme input themedBlockProducerMVar defaultTheme + + -- Print header + putStrLn "{\"version\":1,\"click_events\":true}" + putStrLn "[" + -- Run render loop + liftIO $ link =<< async (renderLoop renderEvent themedBlockProducerMVar) + + -- Return a consumer that accepts BlockOutputs from the bar host, moves them to the mailbox and signals the renderer to update the bar. + signalPipe renderEvent >-> toOutput output + + renderLoop :: Event.Event -> MVar (Producer [ThemedBlockOutput] IO (), Bool) -> IO () + renderLoop renderEvent themedBlockProducerMVar = forever $ do + (themedBlocks, isAnimated'') <- modifyMVar themedBlockProducerMVar (\(themedBlockProducer, isAnimated') -> do + result <- next themedBlockProducer + case result of + -- TODO: fix type safety on this somehow? + Left _ -> throw $ userError "Unexpected behavior: themes and mailboxes should never return" + Right (themedBlocks, nextThemedBlockProducer) -> + return ((nextThemedBlockProducer, isAnimated'), (themedBlocks, isAnimated')) + ) + outputLine options themedBlocks + if isAnimated'' + -- Limit to 10 FPS because swaybar rendering is surprisingly expensive + -- TODO: make FPS configurable + then void $ Event.waitTimeout renderEvent 100000 + else Event.wait renderEvent + + setTheme :: Input [BlockOutput] -> MVar (Producer [ThemedBlockOutput] IO (), Bool) -> Theme -> IO () + setTheme blockOutputInput themedBlockProducerMVar (StaticTheme theme) = + modifyMVar_ themedBlockProducerMVar (\_ -> return (fromInput blockOutputInput >-> PP.map theme, False)) + setTheme blockOutputInput themedBlockProducerMVar (AnimatedTheme theme) = + modifyMVar_ themedBlockProducerMVar (\_ -> return (fromInput blockOutputInput >-> theme, True)) -createCommandChan :: IO CommandChan -createCommandChan = newTChanIO -- |Entry point. runQBar :: BarIO () -> MainOptions -> IO () diff --git a/src/QBar/Theme.hs b/src/QBar/Theme.hs index e890636ebb030408384aa538364e9f2af869e485..cc589390116e8b8c5dfd976a464d6a8b6e69102b 100644 --- a/src/QBar/Theme.hs +++ b/src/QBar/Theme.hs @@ -1,4 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE Rank2Types #-} + module QBar.Theme where import QBar.BlockOutput @@ -37,13 +39,25 @@ data ThemedBlockTextSegment = ThemedBlockTextSegment { deriving (Eq, Show) -type Theme = [BlockOutput] -> [ThemedBlockOutput] +data Theme = StaticTheme StaticTheme | AnimatedTheme AnimatedTheme + +type StaticTheme = [BlockOutput] -> [ThemedBlockOutput] type SimplifiedTheme = Bool -> Importance -> (Color, Maybe Color) -type AnimatedTheme = Pipe [BlockOutput] [ThemedBlockOutput] IO () +type AnimatedTheme = forall r. Pipe [BlockOutput] [ThemedBlockOutput] IO r + +isAnimated :: Theme -> Bool +isAnimated (AnimatedTheme _) = True +isAnimated _ = False + + +findTheme :: Text -> Either Text Theme +findTheme "default" = Right defaultTheme +findTheme "rainbow" = Right rainbowTheme +findTheme name = Left $ "Invalid theme: " <> name mkTheme :: SimplifiedTheme -> Theme -mkTheme theming' = map themeBlock +mkTheme theming' = StaticTheme $ map themeBlock where themeBlock :: BlockOutput -> ThemedBlockOutput themeBlock block@BlockOutput{_blockName} = ThemedBlockOutput{_fullText = fullText', _shortText = shortText', _blockName} @@ -68,15 +82,9 @@ mkThemedSegment (color, backgroundColor) text = ThemedBlockTextSegment{themedSeg invalidColor :: Color invalidColor = ColorRGBA (RGB (0x96 / 255) (0x98 / 255) (0x96 / 255)) (0x77 / 255) - invalidSimplifiedTheme :: SimplifiedTheme invalidSimplifiedTheme _ _ = (invalidColor, Nothing) - -invalidTheme :: Theme -invalidTheme = mkTheme invalidSimplifiedTheme - - defaultTheme :: Theme defaultTheme = mkTheme defaultTheme' where @@ -92,13 +100,16 @@ defaultTheme = mkTheme defaultTheme' | otherwise = (ColorRGB (RGB (0x96 / 255) (0x98 / 255) (0x96 / 255)), Nothing) -rainbowTheme :: AnimatedTheme -rainbowTheme = do - time <- liftIO $ fromRational . toRational <$> getPOSIXTime - yield =<< rainbowTheme' time <$> await +rainbowTheme :: Theme +rainbowTheme = AnimatedTheme rainbowThemePipe where - rainbowTheme' :: Double -> Theme - rainbowTheme' time blocks = reverse $ evalState (mapM rainbowBlock $ reverse blocks) 0 + rainbowThemePipe :: AnimatedTheme + rainbowThemePipe = do + time <- liftIO $ fromRational . toRational <$> getPOSIXTime + yield =<< rainbowThemePipe' time <$> await + rainbowThemePipe + rainbowThemePipe' :: Double -> StaticTheme + rainbowThemePipe' time blocks = reverse $ evalState (mapM rainbowBlock $ reverse blocks) 0 where rainbowBlock :: BlockOutput -> State Integer ThemedBlockOutput rainbowBlock block@BlockOutput{_blockName} = do diff --git a/src/QBar/Util.hs b/src/QBar/Util.hs new file mode 100644 index 0000000000000000000000000000000000000000..72d211f30bd78c6f114feedeb268abb1e0c18b7f --- /dev/null +++ b/src/QBar/Util.hs @@ -0,0 +1,15 @@ +module QBar.Util where + +import Control.Concurrent.Event as Event +import Pipes + +-- Pipe that signals an 'Event' after every value that passes through +signalPipe :: MonadIO m => Event.Event -> Pipe a a m r +signalPipe event = signalPipe' + where + signalPipe' :: MonadIO m => Pipe a a m r + signalPipe' = do + value <- await + yield value + liftIO $ Event.signal event + signalPipe'