Newer
Older
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
module QBar.Core where
import QBar.Pango
import Control.Exception (catch, finally, IOException)
import Control.Monad (forever)
import Control.Monad.Reader (ReaderT, runReaderT, ask, asks)
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Concurrent.Event as Event
import Control.Concurrent.MVar
import Control.Concurrent.STM.TChan (TChan, writeTChan)
import Data.Aeson.TH
import qualified Data.ByteString.Lazy.Char8 as C8
import qualified Data.HashMap.Lazy as HM
import Data.Int (Int64)
import Data.Maybe (fromMaybe, catMaybes)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as E
import qualified Data.Text.Lazy.IO as TIO
import Numeric (showHex)
import Pipes
import Pipes.Concurrent
import qualified Pipes.Prelude as PP
import System.Exit
import System.IO
import System.Process.Typed (shell, withProcessWait, setStdin, setStdout, getStdout, closed, createPipe, readProcessStdout)
import Data.Colour.RGBSpace
data Click = Click {
} deriving Show
$(deriveJSON defaultOptions ''Click)
data BlockOutput = BlockOutput {
values :: HM.HashMap T.Text T.Text,
clickAction :: Maybe (Click -> BarIO ())
}
instance Show BlockOutput where
show BlockOutput{values} = show values
data PushMode = PushMode
data PullMode = PullMode
data CachedMode = CachedMode
-- |Block that 'yield's an update whenever the block should be changed
-- |Block that generates an update on 'yield'. Should only be pulled when an update is required.
-- |Cached block. Always 'yield's the latest update, so it should only be pulled when the bar is rendered.
instance IsBlock PushBlock where
toCachedBlock = cachePushBlock
instance IsBlock CachedBlock where
class IsBlockMode a where
instance IsBlockMode PushMode where
exitBlock = return PushMode
instance IsBlockMode PullMode where
exitBlock = return PullMode
instance IsBlockMode CachedMode where
exitBlock = return CachedMode
data Bar = Bar {
requestBarUpdate :: IO (),
newBlockChan :: TChan CachedBlock
}
data BarUpdateChannel = BarUpdateChannel (IO ())
type BarUpdateEvent = Event.Event
defaultColor :: T.Text
defaultColor = "#969896"
activeColor :: T.Text
activeColor = "#ffffff"
updatingColor :: T.Text
--updatingColor = "#444444"
updatingColor = "#96989677"
createBlock :: T.Text -> BlockOutput
createBlock text = setColor defaultColor $ BlockOutput {
values = HM.singleton "full_text" text,
clickAction = Nothing
}
createErrorBlock = setColor "ff0000" . createBlock
setValue :: T.Text -> T.Text -> BlockOutput -> BlockOutput
setValue key val block = block {
values = HM.insert key val (values block)
}
getValue :: T.Text -> BlockOutput -> Maybe T.Text
getValue key block = HM.lookup key (values block)
adjustValue :: (T.Text -> T.Text) -> T.Text -> BlockOutput -> BlockOutput
adjustValue f k block = block {
values = HM.adjust f k (values block)
}
shortText :: T.Text -> BlockOutput -> BlockOutput
fullText :: T.Text -> BlockOutput -> BlockOutput
getFullText = fromMaybe "" . getValue "full_text"
setColor :: T.Text -> BlockOutput -> BlockOutput
setBlockName :: T.Text -> BlockOutput -> BlockOutput
pangoMarkup = setValue "markup" "pango"
adjustText :: (T.Text -> T.Text) -> BlockOutput -> BlockOutput
adjustText f = adjustValue f "full_text" . adjustValue f "short_text"
coloredText :: T.Text -> T.Text -> T.Text
coloredText color text = "<span color='" <> color <> "'>" <> text <> "</span>"
addIcon :: T.Text -> BlockOutput -> BlockOutput
addIcon icon block = prefixIcon "full_text" $ prefixIcon "short_text" block
where
prefixIcon = adjustValue ((icon <> " ") <>)
removePango block
| getValue "markup" block == Just "pango" = adjustText removePangoFromText $ block {
values = HM.delete "markup" (values block)
}
| otherwise = block
where
removePangoFromText :: T.Text -> T.Text
removePangoFromText text =
case parsePango text of
Left _ -> text
Right parsed -> removeFormatting parsed
modify :: (BlockOutput -> BlockOutput) -> Pipe BlockOutput BlockOutput BarIO r
autoPadding :: Pipe BlockOutput BlockOutput BarIO r
autoPadding = autoPadding' 0 0
where
autoPadding' :: Int64 -> Int64 -> Pipe BlockOutput BlockOutput BarIO r
autoPadding' fullLength shortLength = do
block <- await
let values' = (values block)
let fullLength' = T.length $ HM.lookupDefault "" "full_text" values'
let shortLength' = T.length $ HM.lookupDefault "" "short_text" values'
let values'' = HM.adjust (<> (T.take (fullLength - fullLength') $ T.repeat ' ')) "full_text" values'
let values''' = HM.adjust (<> (T.take (shortLength - shortLength') $ T.repeat ' ')) "short_text" values''
yield block { values = values''' }
autoPadding' (max fullLength fullLength') (max shortLength shortLength')
cacheFromInput :: Input BlockOutput -> CachedBlock
cacheFromInput input = const CachedMode <$> fromInput input
-- | Create a shared interval. Takes a BarUpdateChannel to signal bar updates and an interval (in seconds).Data.Maybe
-- Returns an IO action that can be used to attach blocks to the shared interval and an async that contains a reference to the scheduler thread.
sharedInterval :: Int -> BarIO (PullBlock -> CachedBlock)
sharedInterval seconds = do
clientsMVar <- liftIO $ newMVar ([] :: [(MVar PullBlock, Output BlockOutput)])
startEvent <- liftIO Event.new
task <- barAsync $ do
-- Wait for at least one subscribed client
liftIO $ Event.wait startEvent
forever $ do
liftIO $ threadDelay $ seconds * 1000000
-- Updates all client blocks
-- If send returns 'False' the clients mailbox has been closed, so it is removed
bar <- ask
liftIO $ modifyMVar_ clientsMVar $ fmap catMaybes . mapConcurrently (\r -> runReaderT (runAndFilterClient r) bar)
-- Then update the bar
updateBar
liftIO $ link task
return (addClient startEvent clientsMVar)
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
where
runAndFilterClient :: (MVar PullBlock, Output BlockOutput) -> BarIO (Maybe (MVar PullBlock, Output BlockOutput))
runAndFilterClient client = do
result <- runClient client
return $ if result then Just client else Nothing
runClient :: (MVar PullBlock, Output BlockOutput) -> BarIO Bool
runClient (blockProducerMVar, output) = do
bar <- ask
liftIO $ modifyMVar blockProducerMVar $ \blockProducer -> do
result <- runReaderT (next blockProducer) bar
case result of
Left _ -> return (exitBlock, False)
Right (blockOutput, blockProducer') -> do
success <- atomically $ send output blockOutput {
clickAction = Just (updateClickHandler blockOutput)
}
if success
-- Store new BlockProducer back into MVar
then return (blockProducer', True)
-- Mailbox is sealed, stop running producer
else return (exitBlock, False)
where
updateClickHandler :: BlockOutput -> Click -> BarIO ()
updateClickHandler block _ = do
-- Give user feedback that the block is updating
let outdatedBlock = setColor updatingColor $ removePango block
liftIO $ void $ atomically $ send output $ outdatedBlock
-- Notify bar about changed block state to display the feedback
updateBar
-- Run a normal block update to update the block to the new value
void $ runClient (blockProducerMVar, output)
-- Notify bar about changed block state, this is usually done by the shared interval handler
updateBar
addClient :: Event.Event -> MVar [(MVar PullBlock, Output BlockOutput)] -> PullBlock -> CachedBlock
addClient startEvent clientsMVar blockProducer = do
-- Spawn the mailbox that preserves the latest block
(output, input) <- liftIO $ spawn $ latest emptyBlock
blockProducerMVar <- liftIO $ newMVar blockProducer
-- Generate initial block and send it to the mailbox
lift $ void $ runClient (blockProducerMVar, output)
-- Register the client for regular updates
liftIO $ modifyMVar_ clientsMVar $ \ clients -> return ((blockProducerMVar, output):clients)
-- Start update thread (if not already started)
liftIO $ Event.set startEvent
-- Return a block producer from the mailbox
cacheFromInput input
blockScript :: FilePath -> PullBlock
blockScript path = forever $ yield =<< (lift $ blockScriptAction)
blockScriptAction = do
-- The exit code is used for i3blocks signaling but ignored here (=not implemented)
-- I am trying to replace i3blocks scripts with native haskell blocks, so I do not need it
(exitCode, output) <- liftIO $ readProcessStdout $ shell path
case exitCode of
ExitSuccess -> return $ case map E.decodeUtf8 (C8.lines output) of
(text:short:color:_) -> setColor color $ shortText short $ createScriptBlock text
(text:short:_) -> shortText short $ createScriptBlock text
(text:_) -> createScriptBlock text
[] -> createScriptBlock "-"
(ExitFailure nr) -> return $ createErrorBlock $ "[" <> (T.pack $ show nr) <> "]"
createScriptBlock text = pangoMarkup $ setBlockName (T.pack path) $ createBlock text
startPersistentBlockScript :: FilePath -> CachedBlock
-- This is only using 'CachedBlock' because the code was already written and tested
-- This could probably be massively simplified by using the new 'pushBlock'
startPersistentBlockScript path = do
bar <- lift $ ask
do
(output, input, seal) <- liftIO $ spawn' $ latest $ emptyBlock
initialDataEvent <- liftIO $ Event.new
let processConfig = setStdin closed $ setStdout createPipe $ shell path
finally (
catch (
withProcessWait processConfig $ \ process -> do
let handle = getStdout process
runEffect $ (fromHandle bar handle) >-> signalFirstBlock initialDataEvent >-> toOutput output
)
( \ e ->
-- output error
runEffect $ (yield $ createErrorBlock $ "[" <> (T.pack $ show (e :: IOException)) <> "]") >-> signalFirstBlock initialDataEvent >-> toOutput output
(atomically seal)
liftIO $ Event.wait initialDataEvent
cacheFromInput input
signalFirstBlock :: Event.Event -> Pipe BlockOutput BlockOutput IO ()
signalFirstBlock event = do
-- Await first block
await >>= yield
lift $ Event.set event
-- Replace with cat
cat
fromHandle :: Bar -> Handle -> Producer BlockOutput IO ()
fromHandle bar handle = forever $ do
line <- lift $ TIO.hGetLine handle
yield $ pangoMarkup $ createBlock line
pangoColor :: RGB Double -> T.Text
pangoColor (RGB r g b) =
let r' = hexColorComponent r
g' = hexColorComponent g
b' = hexColorComponent b
in "#" <> r' <> g' <> b'
where
hexColorComponent :: Double -> T.Text
hexColorComponent val = paddedHexComponent $ T.pack $ showHex (max 0 $ min 255 $ (truncate (val * 255) :: Int)) ""
paddedHexComponent hex =
let len = 2 - T.length hex
padding = if len == 1 then "0" else ""
in padding <> hex
addBlock :: IsBlock a => a -> BarIO ()
addBlock block = do
newBlockChan' <- asks newBlockChan
liftIO $ atomically $ writeTChan newBlockChan' $ toCachedBlock block
updateBar :: BarIO ()
updateBar = liftIO =<< asks requestBarUpdate
updateBar' :: Bar -> IO ()
updateBar' = runReaderT updateBar
barAsync :: BarIO a -> BarIO (Async a)
barAsync action = do
bar <- ask
lift $ async $ runReaderT action bar
cachePushBlock :: PushBlock -> CachedBlock
cachePushBlock pushBlock = lift (next pushBlock) >>= either (const exitBlock) withInitialBlock
withInitialBlock :: (BlockOutput, PushBlock) -> CachedBlock
withInitialBlock (initialBlockOutput, pushBlock') = do
(output, input, seal) <- liftIO $ spawn' $ latest $ Just initialBlockOutput
-- The async could be used to stop the block later, but for now we are just linking it to catch exceptions
task <- lift $ barAsync (sendProducerToMailbox output seal pushBlock')
liftIO $ link task
terminateOnMaybe $ fromInput input
sendProducerToMailbox :: Output (Maybe BlockOutput) -> STM () -> PushBlock -> BarIO ()
sendProducerToMailbox output seal pushBlock' = do
void $ runEffect $ for pushBlock' (sendOutputToMailbox output)
liftIO $ atomically $ void $ send output Nothing
updateBar
liftIO $ atomically seal
sendOutputToMailbox :: Output (Maybe BlockOutput) -> BlockOutput -> Effect BarIO ()
sendOutputToMailbox output blockOutput = do
-- The void is discarding the boolean result that indicates if the mailbox is sealed
-- This is ok because a cached block is never sealed from the receiving side
liftIO $ atomically $ void $ send output $ Just blockOutput
lift $ updateBar
terminateOnMaybe :: Producer (Maybe BlockOutput) BarIO () -> Producer BlockOutput BarIO CachedMode
terminateOnMaybe p = do
eitherMaybeValue <- lift $ next p
case eitherMaybeValue of
Right (Just value, newP) -> yield value >> terminateOnMaybe newP