Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • jens/qbar
  • jktr/qbar
  • snowball/qbar
3 results
Show changes
Showing
with 1426 additions and 260 deletions
module QBar.Blocks.Date where module QBar.Blocks.Date (
dateBlock,
) where
import QBar.BlockHelper import QBar.BlockHelper
import QBar.BlockOutput import QBar.BlockOutput
import QBar.Core import QBar.Core
import QBar.Prelude
import QBar.Time import QBar.Time
import qualified Data.Text.Lazy as T
import Data.Time.Format import Data.Time.Format
import Data.Time.LocalTime import Data.Time.LocalTime
import Data.Text.Lazy qualified as T
dateBlock :: Block dateBlock :: Block
dateBlock = runPollBlock' (everyNSeconds 60) $ forever $ do dateBlock = runPollBlock' (everyNSeconds 60) $ forever $ do
zonedTime <- liftIO getZonedTime zonedTime <- liftIO getZonedTime
let logo :: Text = "📅\xFE0E "
let date = T.pack (formatTime defaultTimeLocale "%a %F" zonedTime) let date = T.pack (formatTime defaultTimeLocale "%a %F" zonedTime)
let time = T.pack (formatTime defaultTimeLocale "%R" zonedTime) let time = T.pack (formatTime defaultTimeLocale "%R" zonedTime)
let text = normalText ("📅\xFE0E " <> date <> " ") <> activeText time let text = normalText (logo <> date <> " ") <> activeText time
yieldBlockUpdate $ mkBlockOutput text let short = normalText logo <> activeText time
yieldBlockUpdate $ (mkBlockOutput text) { _shortText = Just short }
module QBar.Blocks.DiskUsage (
diskUsageBlock,
) where
import QBar.BlockHelper
import QBar.BlockOutput
import QBar.Core
import QBar.Prelude
import Data.ByteString.Lazy.Char8 qualified as C8
import Data.Text.Lazy qualified as T
import Data.Text.Lazy.Encoding qualified as T
import System.Exit
import System.Process.Typed (shell, readProcessStdout)
diskIcon :: T.Text
diskIcon = "💾\xFE0E"
diskUsageBlock :: Text -> Block
diskUsageBlock path = runPollBlock $ forever $ do
output <- liftBarIO action
yieldBlockUpdate $ addIcon diskIcon output
where
action :: BarIO BlockOutput
action = do
(exitCode, output) <- liftIO $ readProcessStdout $ shell $ "df --human-readable --local --output=avail " <> T.unpack path
return $ case exitCode of
ExitSuccess -> createBlockOutput output
(ExitFailure nr) -> mkErrorOutput $ "exit code " <> T.pack (show nr) <> ""
createBlockOutput :: C8.ByteString -> BlockOutput
createBlockOutput output = case map T.decodeUtf8 (C8.lines output) of
[] -> mkErrorOutput "no output"
[_header] -> mkErrorOutput "invalid output"
(_header:values) -> mkBlockOutput $ normalText $ T.intercalate " " $ map T.strip values
module QBar.Blocks.NetworkManager (
getDBusProperty,
networkManagerBlock,
runExceptT_,
) where
import Control.Monad.Except (ExceptT, MonadError, runExceptT, throwError)
import DBus qualified
import DBus.Client qualified as DBus
import Data.Map qualified as Map
import Data.Text.Lazy qualified as T
import Data.Word (Word32, Word8)
import QBar.BlockHelper
import QBar.BlockOutput
import QBar.Blocks.Utils
import QBar.Core
import QBar.Prelude
data ConnectionInfo = WifiConnection Text Int | WwanConnection Text Int | EthernetConnection Text
deriving (Show)
fromJust :: MonadError () m => Maybe a -> m a
fromJust (Just a) = return a
fromJust Nothing = throwError ()
runExceptT_ :: Monad m => ExceptT () m a -> m (Maybe a)
runExceptT_ a = either (const Nothing) Just <$> runExceptT a
getDBusProperty :: (MonadError () m, MonadIO m, DBus.IsVariant a) => DBus.Client -> DBus.BusName -> DBus.ObjectPath -> DBus.InterfaceName -> DBus.MemberName -> m a
getDBusProperty client busName objectPath interfaceName memberName = do
result' <- tryMaybe' $ do
let methodCall = ((DBus.methodCall objectPath interfaceName memberName) {DBus.methodCallDestination = Just busName})
result <- either (const Nothing) Just <$> DBus.getProperty client methodCall
return $ DBus.fromVariant =<< result
fromJust result'
getConnectionInfo :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m ConnectionInfo
getConnectionInfo client connectionObjectPath = do
connectionType <- getActiveConnectionType client connectionObjectPath
connectionName <- getActiveConnectionName client connectionObjectPath
case connectionType of
"802-11-wireless" -> do
devices <- getActiveConnectionDevice client connectionObjectPath
device <- fromJust $ listToMaybe devices
accessPoint <- getDeviceAccessPoint client device
signalStrength <- fromIntegral <$> getAccessPointSignalStrength client accessPoint
return $ WifiConnection connectionName signalStrength
"gsm" -> do
devices <- getActiveConnectionDevice client connectionObjectPath
device <- fromJust $ listToMaybe devices
udi <- getDeviceUdi client device
signalQuality <- getModemSignalQuality client $ DBus.objectPath_ udi
return $ WwanConnection connectionName . fromIntegral . fst $ signalQuality
"802-3-ethernet" -> return $ EthernetConnection connectionName
_ -> throwError ()
networkManagerBlock :: Block
networkManagerBlock = runSignalBlockConfiguration $ SignalBlockConfiguration {
aquire,
release,
signalThread = Nothing,
signalBlock = networkManagerBlock',
interval = Just defaultInterval
}
where
aquire :: (() -> IO ()) -> BarIO DBus.Client
aquire trigger = liftIO $ do
client <- DBus.connectSystem
let matchRule = DBus.matchAny {
DBus.matchPath = Just "/org/freedesktop/NetworkManager",
DBus.matchInterface = Just "org.freedesktop.DBus.Properties"
}
void . DBus.addMatch client matchRule $ dbusSignalHandler trigger
return client
release :: DBus.Client -> BarIO ()
release = liftIO . DBus.disconnect
networkManagerBlock' :: DBus.Client -> SignalBlock ()
networkManagerBlock' client
= (liftBarIO . networkManagerBlock'' client)
>=> (\x -> respondBlockUpdate x) -- why doesn't this type check without \->?
>=> networkManagerBlock' client
networkManagerBlock'' :: DBus.Client -> Signal () -> BarIO BlockOutput
networkManagerBlock'' client _ = do
primaryConnection <- runExceptT_ $ getPrimaryConnectionPath client
primaryConnectionInfo <- case primaryConnection of
Just primaryConnection' -> runExceptT_ $ getConnectionInfo client primaryConnection'
Nothing -> return Nothing
return . mkBlockOutput $ fullText' primaryConnectionInfo
fullText' :: Maybe ConnectionInfo -> BlockText
fullText' connectionInfo = fullText'' connectionInfo
where
importanceLevel :: Importance
importanceLevel = case connectionInfo of
Nothing -> errorImportant
(Just (WifiConnection _ strength)) -> toImportance (0, 85 ,100, 100, 100) . (100 -) $ strength
(Just (WwanConnection _ strength)) -> toImportance (0, 85 ,100, 100, 100) . (100 -) $ strength
(Just (EthernetConnection _)) -> normalImportant
fullText'' :: Maybe ConnectionInfo -> BlockText
fullText'' Nothing = importantText importanceLevel "❌\xFE0E No Connection"
fullText'' (Just (WifiConnection name strength)) = importantText importanceLevel $ "📡\xFE0E " <> name <> " " <> formatPercent strength
fullText'' (Just (WwanConnection _ signalQuality)) = importantText importanceLevel $ "📶\xFE0E " <> formatPercent signalQuality
fullText'' (Just (EthernetConnection _)) = importantText importanceLevel "🔌\xFE0E Ethernet"
dbusSignalHandler :: (() -> IO ()) -> DBus.Signal -> IO ()
dbusSignalHandler trigger signal = when (primaryConnectionHasChanged signal) $ trigger ()
primaryConnectionHasChanged :: DBus.Signal -> Bool
primaryConnectionHasChanged = any (maybe False (containsKey "PrimaryConnection") . DBus.fromVariant) . DBus.signalBody
containsKey :: String -> Map.Map String DBus.Variant -> Bool
containsKey = Map.member
formatPercent :: Int -> Text
formatPercent a = T.justifyRight 3 ' ' $ (T.pack . show . max 0 . min 100) a <> "%"
getPrimaryConnectionPath :: (MonadError () m, MonadIO m) => DBus.Client -> m DBus.ObjectPath
getPrimaryConnectionPath client =
getDBusProperty
client
"org.freedesktop.NetworkManager"
"/org/freedesktop/NetworkManager"
"org.freedesktop.NetworkManager"
"PrimaryConnection"
getActiveConnectionType :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m Text
getActiveConnectionType client objectPath =
getDBusProperty
client
"org.freedesktop.NetworkManager"
objectPath
"org.freedesktop.NetworkManager.Connection.Active"
"Type"
getActiveConnectionName :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m Text
getActiveConnectionName client objectPath =
getDBusProperty
client
"org.freedesktop.NetworkManager"
objectPath
"org.freedesktop.NetworkManager.Connection.Active"
"Id"
getActiveConnectionDevice :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m [DBus.ObjectPath]
getActiveConnectionDevice client objectPath =
getDBusProperty
client
"org.freedesktop.NetworkManager"
objectPath
"org.freedesktop.NetworkManager.Connection.Active"
"Devices"
getDeviceAccessPoint :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m DBus.ObjectPath
getDeviceAccessPoint client objectPath =
getDBusProperty
client
"org.freedesktop.NetworkManager"
objectPath
"org.freedesktop.NetworkManager.Device.Wireless"
"ActiveAccessPoint"
getDeviceUdi :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m String
getDeviceUdi client objectPath =
getDBusProperty
client
"org.freedesktop.NetworkManager"
objectPath
"org.freedesktop.NetworkManager.Device"
"Udi"
getAccessPointSignalStrength :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m Word8
getAccessPointSignalStrength client objectPath =
getDBusProperty
client
"org.freedesktop.NetworkManager"
objectPath
"org.freedesktop.NetworkManager.AccessPoint"
"Strength"
getModemSignalQuality :: (MonadError () m, MonadIO m) => DBus.Client -> DBus.ObjectPath -> m (Word32, Bool)
getModemSignalQuality client objectPath =
getDBusProperty
client
"org.freedesktop.ModemManager1"
objectPath
"org.freedesktop.ModemManager1.Modem"
"SignalQuality"
module QBar.Blocks.Pipe where module QBar.Blocks.Pipe (
runPipeClient,
) where
import QBar.ControlSocket import QBar.ControlSocket
import QBar.Core import QBar.Core
import QBar.Prelude
import QBar.TagParser import QBar.TagParser
import Control.Concurrent.Async import Control.Concurrent.Async
import Data.Aeson (encode) import Data.Aeson (encode)
import qualified Data.ByteString.Lazy.Char8 as BSC import Data.ByteString.Lazy.Char8 qualified as BSC
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import Pipes import Pipes
import Pipes.Concurrent import Pipes.Concurrent
import qualified Pipes.Prelude as PP import Pipes.Prelude qualified as PP
import System.IO import System.IO
runPipeClient :: Bool -> MainOptions -> IO () runPipeClient :: Bool -> MainOptions -> IO ()
...@@ -20,7 +23,7 @@ runPipeClient enableEvents mainOptions = do ...@@ -20,7 +23,7 @@ runPipeClient enableEvents mainOptions = do
inputTask <- async $ runEffect $ PP.stdinLn >-> toOutput output inputTask <- async $ runEffect $ PP.stdinLn >-> toOutput output
void $ waitEitherCancel hostTask inputTask void $ waitEitherCancel hostTask inputTask
where where
-- |Special block that reads the processes stdin line-by-line and shows the latest line in the block. Must never be used in a 'server' process or when stdin/stdout is used in another way. -- Special block that reads the processes stdin line-by-line and shows the latest line in the block. Must never be used in a 'server' process or when stdin/stdout is used in another way.
pipeBlock :: Producer String BarIO () -> Block pipeBlock :: Producer String BarIO () -> Block
pipeBlock source = ExitBlock <$ source >-> pack pipeBlock source = ExitBlock <$ source >-> pack
where where
...@@ -33,4 +36,4 @@ runPipeClient enableEvents mainOptions = do ...@@ -33,4 +36,4 @@ runPipeClient enableEvents mainOptions = do
else pushBlockUpdate output else pushBlockUpdate output
handler :: BlockEventHandler handler :: BlockEventHandler
handler event = liftIO $ BSC.hPutStrLn stderr $ encode event handler event = liftIO $ BSC.hPutStrLn stdout $ encode event
module QBar.Blocks.Qubes (
diskUsageQubesBlock,
qubesMonitorPropertyBlock,
qubesVMCountBlock,
) where
import QBar.BlockHelper
import QBar.BlockOutput
import QBar.Core
import QBar.Prelude
import QBar.Qubes.AdminAPI (
QubesPropertyInfo(..),
QubesVMState(..),
QubesVMInfo(..),
qubesEvents,
qubesGetProperty,
qubesListVMs,
qubesListVMsP,
qubesMonitorProperty,
qubesUsageOfDefaultPool,
vmState,
)
import Data.ByteString.Lazy qualified as BL
import Data.Map qualified as M
import Data.Text.Encoding.Error (lenientDecode)
import Data.Text.Lazy qualified as T
import Data.Text.Lazy.Encoding (decodeUtf8With)
import Pipes as P
import Pipes.Core as P
diskIcon :: T.Text
diskIcon = "💾\xFE0E"
diskUsageQubesBlock :: Block
diskUsageQubesBlock = runPollBlock $ forever $ do
output <- liftBarIO action
yieldBlockUpdate $ addIcon diskIcon output
where
action :: BarIO BlockOutput
action = liftIO qubesUsageOfDefaultPool >>= \case
(Just usage, Just size) -> return $ createBlockOutput $ size - usage
_ -> return $ mkErrorOutput "unknown"
createBlockOutput :: Int -> BlockOutput
createBlockOutput free =
mkBlockOutput $ chooseColor free $ formatSize free
chooseColor :: Int -> Text -> BlockText
chooseColor free = if free < 40 * 1024*1024*1024
then activeText
else normalText
sizeUnits :: [(Text, Int)]
sizeUnits = [
("T", 1024*1024*1024*1024),
("G", 1024*1024*1024),
("M", 1024*1024),
("k", 1024),
(" bytes", 1)
]
formatSize size = case filter ((< size) . snd) sizeUnits of
((unit, factor) : _) -> T.pack (show $ size `div` factor) <> unit
_ -> T.pack (show size) <> " bytes"
pipeBlockWithEvents :: forall a. Producer a BarIO () -> (Either BlockEvent a -> BarIO (Maybe BlockOutput)) -> Block
pipeBlockWithEvents prod block = runSignalBlock Nothing (Just produce) sblock
where
produce :: (a -> IO ()) -> BarIO ()
produce yield' = runEffect $ prod >-> forever (await >>= liftIO . yield')
sblock :: Signal a -> P.Server (Signal a) (Maybe BlockOutput) BarIO ExitBlock
sblock = lift . sblock' >=> respond >=> sblock
sblock' :: Signal a -> BarIO (Maybe BlockOutput)
sblock' RegularSignal = return Nothing -- ignore timer
sblock' (UserSignal x) = block $ Right x
sblock' (EventSignal x) = block $ Left x
qubesMonitorPropertyBlock :: BL.ByteString -> Block
qubesMonitorPropertyBlock name = pipeBlockWithEvents (qubesMonitorProperty qubesEvents name) handle
where
handle :: Either a QubesPropertyInfo -> BarIO (Maybe BlockOutput)
handle = fmap handle' . either (const $ liftIO $ qubesGetProperty name) return
handle' QubesPropertyInfo {propValue, propIsDefault} = Just $ mkBlockOutput $ normalText $ decode propValue <> (if propIsDefault then " (D)" else "")
decode = decodeUtf8With lenientDecode
qubesVMCountBlock :: Block
qubesVMCountBlock = pipeBlockWithEvents qubesListVMsP $ fmap countVMs . either (const $ liftIO qubesListVMs) return
where
countVMs :: M.Map BL.ByteString QubesVMInfo -> Maybe BlockOutput
countVMs = Just . format . M.size . M.filterWithKey isRunningVM
isRunningVM :: BL.ByteString -> QubesVMInfo -> Bool
isRunningVM name x = vmState x == VMRunning && name /= "dom0"
format :: Int -> BlockOutput
format n = mkBlockOutput $ normalText $ T.pack (show n) <> " Qube" <> (if n /= 1 then "s" else "")
module QBar.Blocks.Script (
pollScriptBlock,
scriptBlock,
) where
import QBar.BlockHelper
import QBar.BlockOutput
import QBar.Core
import QBar.Prelude
import QBar.TagParser
import QBar.Time
import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
import Control.Exception (IOException, handle)
import Data.Aeson (encode)
import Data.ByteString.Lazy.Char8 qualified as C8
import Data.Text.Lazy qualified as T
import Data.Text.Lazy.Encoding qualified as E
import Data.Text.Lazy.IO qualified as TIO
import Pipes
import Pipes.Safe (catchP)
import System.Exit
import System.IO hiding (stdin, stdout)
import System.IO.Error (isEOFError)
import System.Process.Typed (
Process,
ProcessConfig,
closed,
createPipe,
getExitCode,
getStdin,
getStdout,
readProcessStdout,
setStdin,
setStdout,
shell,
startProcess,
stopProcess,
)
pollScriptBlock :: Interval -> FilePath -> Block
pollScriptBlock interval path = runPollBlock' interval $ forever $ do
-- Why doesn't this typecheck when using >>= instead?
x <- lift blockScriptAction
yieldBlockUpdate x
where
blockScriptAction :: BarIO BlockOutput
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
return $ case exitCode of
ExitSuccess -> createScriptBlockOutput output
(ExitFailure nr) -> mkErrorOutput $ "exit code " <> T.pack (show nr) <> ""
createScriptBlockOutput :: C8.ByteString -> BlockOutput
createScriptBlockOutput output = case map E.decodeUtf8 (C8.lines output) of
(text:short:_) -> parseTags'' text short
(text:_) -> parseTags' text
[] -> emptyBlock
scriptBlock :: Bool -> FilePath -> Block
-- The outer catchP only catches errors that occur during process creation
scriptBlock clickEvents path = startScriptProcess
where
handleError :: Maybe ExitCode -> IOException -> Block
handleError exitCode exc = case result of
Left msg -> do
signal <- liftIO newEmptyMVar
pushBlockUpdate' (const $ liftIO $ putMVar signal ()) $
mkErrorOutput msg
liftIO $ takeMVar signal
startScriptProcess
Right x -> x
where
result :: Either Text Block
result = case (isEOFError exc, exitCode) of
(True, Just ExitSuccess) -> Right exitBlock
(True, Just (ExitFailure nr)) ->
Left $ "exit code " <> T.pack (show nr)
(True, Nothing) ->
-- This will happen if we hit the race condition (see below)
-- or the process closes its stdout without exiting.
Left "exit code unavailable"
_ -> Left $ T.pack (show exc)
ignoreIOException :: a -> IO a -> IO a
ignoreIOException errValue = handle $ \(_ :: IOException) -> return errValue
handleErrorWithProcess :: Process i o e -> IOException -> Block
handleErrorWithProcess process exc = do
-- We want to know whether the process has already exited or we are
-- killing it because of some other error. stopProcess determines
-- that but it doesn't tell us. getExitCode is unreliable before
-- stopProcess because it will return Nothing while the waiter threat
-- hasn't noticed that the process is dead.
-- Furthermore, stopProcess may fail in waitForProcess if the process
-- has died really quickly.
-- I don't think there is anything we can do about this. We do try
-- to make the races less likely by waiting a bit.
exitCode <- liftIO $ do
threadDelay 100000
ignoreIOException Nothing (getExitCode process)
<* ignoreIOException () (stopProcess process)
handleError exitCode exc
startScriptProcess :: Block
startScriptProcess = flip catchP (handleError Nothing) $
if clickEvents
then startScriptProcessWithEvents
else startScriptProcessNoEvents
startScriptProcessNoEvents :: Block
startScriptProcessNoEvents = do
let
processConfig :: ProcessConfig () Handle ()
processConfig = setStdin closed $ setStdout createPipe $ shell path
process <- startProcess processConfig
-- The inner catchP catches errors that happen after the process has been created
-- This handler will also make sure the process is stopped
catchP (blockFromHandle Nothing $ getStdout process) (handleErrorWithProcess process)
startScriptProcessWithEvents :: Block
startScriptProcessWithEvents = do
let processConfig = setStdin createPipe $ setStdout createPipe $ shell path
process <- startProcess processConfig
-- The inner catchP catches errors that happen after the process has been created
-- This handler will also make sure the process is stopped
blockFromHandle (Just $ getStdin process) (getStdout process)
`catchP` handleErrorWithProcess process
blockFromHandle :: Maybe Handle -> Handle -> Block
blockFromHandle stdin stdout = forever $ do
line <- liftIO $ TIO.hGetLine stdout
let blockOutput = parseTags' line
case stdin of
Nothing -> pushBlockUpdate blockOutput
Just h -> pushBlockUpdate' (handleClick h) blockOutput
handleClick :: Handle -> BlockEventHandler
handleClick stdin ev = liftIO $ do
C8.hPutStrLn stdin $ encode ev
hFlush stdin
module QBar.Blocks.Squeekboard (
squeekboardBlock,
) where
import QBar.BlockHelper
import QBar.BlockOutput
import QBar.Blocks.NetworkManager (getDBusProperty, runExceptT_)
import QBar.Core
import QBar.Prelude
import Control.Monad.Except (MonadError)
import DBus qualified
import DBus.Client qualified as DBus
import DBus.Internal.Message (signalBody)
import Data.Either (isRight)
import Pipes.Core
squeekboardBlock :: Bool -> Block
squeekboardBlock autoHide = runSignalBlockConfiguration $ SignalBlockConfiguration {
aquire,
release,
signalThread = Nothing,
signalBlock = networkManagerBlock',
interval = Nothing
}
where
aquire :: (() -> IO ()) -> BarIO DBus.Client
aquire trigger = liftIO $ do
client <- DBus.connectSession
let matchRule = DBus.matchAny {
DBus.matchPath = Just "/sm/puri/OSK0",
DBus.matchInterface = Just "org.freedesktop.DBus.Properties",
DBus.matchMember = Just "PropertiesChanged"
}
void . DBus.addMatch client matchRule $ dbusSignalHandler trigger
let matchRule2 = DBus.matchAny {
DBus.matchSender = Just "org.freedesktop.DBus",
DBus.matchPath = Just "/org/freedesktop/DBus",
DBus.matchInterface = Just "org.freedesktop.DBus",
DBus.matchMember = Just "NameOwnerChanged"
}
void . DBus.addMatch client matchRule2 $ \signal -> when (nameOwnerChangedIsPuriOSK0 (signalBody signal)) $ dbusSignalHandler trigger signal
return client
nameOwnerChangedIsPuriOSK0 :: [DBus.Variant] -> Bool
nameOwnerChangedIsPuriOSK0 (path:_) = path == DBus.toVariant ("sm.puri.OSK0" :: String)
nameOwnerChangedIsPuriOSK0 _ = False
release :: DBus.Client -> BarIO ()
release = liftIO . DBus.disconnect
networkManagerBlock' :: DBus.Client -> SignalBlock ()
networkManagerBlock' client = (liftBarIO . networkManagerBlock'' client) >=> respond >=> networkManagerBlock' client
networkManagerBlock'' :: DBus.Client -> Signal () -> BarIO (Maybe BlockOutput)
networkManagerBlock'' client (EventSignal Click{button=1}) = do
mCurrent <- runExceptT_ (getVisible client)
case mCurrent of
(Just current) -> void $ setVisible client (not current)
Nothing -> return ()
networkManagerBlock''' client
networkManagerBlock'' client _ = networkManagerBlock''' client
networkManagerBlock''' :: DBus.Client -> BarIO (Maybe BlockOutput)
networkManagerBlock''' client = blockOutput <$> runExceptT_ (getVisible client)
blockOutput :: Maybe Bool -> Maybe BlockOutput
blockOutput (Just isEnabled) = Just (mkBlockOutput (mkText isEnabled normalImportant "⌨\xFE0E osk"))
blockOutput Nothing = if autoHide then Nothing else Just (mkBlockOutput $ mkText False errorImportant "⌨\xFE0E n/a")
dbusSignalHandler :: (() -> IO ()) -> DBus.Signal -> IO ()
dbusSignalHandler trigger _signal = trigger ()
getVisible :: (MonadError () m, MonadIO m) => DBus.Client -> m Bool
getVisible client = do
getDBusProperty
client
"sm.puri.OSK0"
"/sm/puri/OSK0"
"sm.puri.OSK0"
"Visible"
setVisible :: (MonadIO m) => DBus.Client -> Bool -> m Bool
setVisible client value = do
let methodCall = ((DBus.methodCall "/sm/puri/OSK0" "sm.puri.OSK0" "SetVisible") {DBus.methodCallDestination = Just "sm.puri.OSK0", DBus.methodCallBody = [DBus.toVariant value]})
isRight <$> liftIO (DBus.call client methodCall)
{-# LANGUAGE ScopedTypeVariables #-} module QBar.Blocks.Utils (
ensure,
formatFloatN,
parseFile,
tryMaybe',
tryMaybe,
) where
module QBar.Blocks.Utils where import QBar.Prelude
import Control.Exception (SomeException, catch) import Control.Exception (SomeException, catch)
import qualified Data.Attoparsec.Text.Lazy as AT import Data.Attoparsec.Text.Lazy qualified as AT
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import qualified Data.Text.Lazy.IO as TIO import Data.Text.Lazy.IO qualified as TIO
import Numeric (showFFloat) import Numeric (showFFloat)
formatFloatN :: RealFloat a => Int -> a -> T.Text formatFloatN :: RealFloat a => Int -> a -> T.Text
......
{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE TemplateHaskell #-}
module QBar.Cli where module QBar.Cli (
runQBar,
) where
import QBar.Blocks import QBar.Blocks
import QBar.Blocks.Pipe import QBar.Blocks.Pipe
import QBar.ControlSocket import QBar.ControlSocket
import QBar.Core import QBar.Core
import QBar.DefaultConfig import QBar.DefaultConfig
import QBar.Prelude
import QBar.Qubes.AdminAPI (printEvents, qubesVMStats, qubesEvents)
import QBar.Server import QBar.Server
import QBar.Theme import QBar.Theme
import QBar.Time
import Control.Monad (join) import Control.Monad (join)
import qualified Data.Text.Lazy as T import Data.Maybe (fromMaybe)
import Data.Text.Lazy qualified as T
import Development.GitRev
import Options.Applicative import Options.Applicative
-- |Entry point. -- |Entry point.
...@@ -28,12 +35,18 @@ parseMain = customExecParser parserPrefs parser ...@@ -28,12 +35,18 @@ parseMain = customExecParser parserPrefs parser
parserPrefs :: ParserPrefs parserPrefs :: ParserPrefs
parserPrefs = prefs showHelpOnEmpty parserPrefs = prefs showHelpOnEmpty
versionInformation :: String
versionInformation = "Branch: " <> $gitBranch <> "\n"
<> "Commit: " <> $gitHash <> (if $gitDirty then " (dirty)" else "") <> "\n"
<> "Commit date: " <> $gitCommitDate
mainParser :: Parser (IO ()) mainParser :: Parser (IO ())
mainParser = do mainParser = do
verbose <- switch $ long "verbose" <> short 'v' <> help "Print more diagnostic output to stderr (including a copy of every bar update)." verbose <- switch $ long "verbose" <> short 'v' <> help "Print more diagnostic output to stderr (including a copy of every bar update)."
indicator <- switch $ long "indicator" <> short 'i' <> help "Show render indicator." indicator <- switch $ long "indicator" <> short 'i' <> help "Show render indicator."
socketLocation <- optional $ strOption $ long "socket" <> short 's' <> metavar "SOCKET" <> help "Control socket location. By default determined by WAYLAND_SOCKET location." socketLocation <- optional $ strOption $ long "socket" <> short 's' <> metavar "SOCKET" <> help "Control socket location. By default determined by WAYLAND_SOCKET location."
barCommand <- barCommandParser barCommand <- barCommandParser
infoOption versionInformation $ long "version" <> help "Shows version information about the executable."
return (barCommand MainOptions {verbose, indicator, socketLocation}) return (barCommand MainOptions {verbose, indicator, socketLocation})
barCommandParser :: Parser (MainOptions -> IO ()) barCommandParser :: Parser (MainOptions -> IO ())
...@@ -41,14 +54,16 @@ barCommandParser = hsubparser ( ...@@ -41,14 +54,16 @@ barCommandParser = hsubparser (
command "server" (info serverCommandParser (progDesc "Start a new server.")) <> command "server" (info serverCommandParser (progDesc "Start a new server.")) <>
command "mirror" (info mirrorCommandParser (progDesc "Mirror the output of a running server.")) <> command "mirror" (info mirrorCommandParser (progDesc "Mirror the output of a running server.")) <>
command "pipe" (info pipeClientParser (progDesc "Redirects the stdin of this process to a running bar.")) <> command "pipe" (info pipeClientParser (progDesc "Redirects the stdin of this process to a running bar.")) <>
command "theme" (info themeCommandParser (progDesc "Change the theme of the running qbar server.")) command "theme" (info themeCommandParser (progDesc "Change the theme of the running qbar server.")) <>
command "qubes" (info qubesCommandParser (progDesc "Display information about Qubes."))
) )
serverCommandParser :: Parser (MainOptions -> IO ()) serverCommandParser :: Parser (MainOptions -> IO ())
serverCommandParser = hsubparser ( serverCommandParser = hsubparser (
command "swaybar" (info (runBarServer <$> barConfigurationParser) (progDesc "Start a new server. Should be called by swaybar.")) <> command "swaybar" (info (runBarServer <$> barConfigurationParser) (progDesc "Start a new server. Should be called by swaybar.")) <>
command "i3bar" (info (runBarServer <$> barConfigurationParser) (progDesc "Start a new server. Should be called by i3bar.")) <> command "i3bar" (info (runBarServer <$> barConfigurationParser) (progDesc "Start a new server. Should be called by i3bar.")) <>
command "send" (info (sendBlockStream <$> barConfigurationParser) (progDesc "Run blocks on this process but send them to another qbar server.")) command "send" (info (sendBlockStream <$> barConfigurationParser) (progDesc "Run blocks on this process but send them to another qbar server.")) <>
command "send-stdio" (info (sendBlockStreamStdio <$> barConfigurationParser) (progDesc "Run blocks on this process but send them to another qbar server using stdin and stdout."))
) )
where where
barConfigurationParser :: Parser (BarIO ()) barConfigurationParser :: Parser (BarIO ())
...@@ -77,8 +92,7 @@ blockParser = ...@@ -77,8 +92,7 @@ blockParser =
subparser ( subparser (
commandGroup "Available presets:" <> commandGroup "Available presets:" <>
metavar "CONFIG..." <> metavar "CONFIG..." <>
command "default" (info (pure defaultBarConfig) (progDesc "Load default set of blocks.")) <> command "default" (info (pure defaultBarConfig) (progDesc "Load default set of blocks."))
command "legacy" (info (pure legacyBarConfig) (progDesc "Load the legacy configuration. Requires some custom block scripts."))
) )
<|> <|>
subparser ( subparser (
...@@ -86,12 +100,49 @@ blockParser = ...@@ -86,12 +100,49 @@ blockParser =
hidden <> hidden <>
command "date" (info (pure $ addBlock dateBlock) (progDesc "Load the date and time block.")) <> command "date" (info (pure $ addBlock dateBlock) (progDesc "Load the date and time block.")) <>
command "cpu" (info (pure $ addBlock $ cpuUsageBlock 1) (progDesc "Load the cpu usage block.")) <> command "cpu" (info (pure $ addBlock $ cpuUsageBlock 1) (progDesc "Load the cpu usage block.")) <>
command "battery" (info (pure $ addBlock $ batteryBlock) (progDesc "Load the battery block.")) <> command "battery" (info (pure $ addBlock batteryBlock) (progDesc "Load the battery block.")) <>
command "script" (info scriptBlockParser (progDesc "Display the output of an external script as a block.")) command "disk" (info diskUsageBlockParser (progDesc "Load the disk usage block.")) <>
command "networkmanager" (info (pure $ addBlock networkManagerBlock) (progDesc "Load the network-manager block.")) <>
command "script" (info scriptBlockParser (progDesc "Display the output of an external script as a block.")) <>
command "squeekboard" (info squeekboardParser (progDesc "Toggles the visibility of the 'squeekboard' on-screen-keyboard when clicked (squeekboard must be running).")) <>
command "diskQubesPool" (info (pure $ addBlock diskUsageQubesBlock) (progDesc "Load a block that shows free space in Qubes' default pool.")) <>
command "qubesProperty" (info qubesPropertyBlockParser (progDesc "Display the current value of a Qubes property.")) <>
command "qubesCount" (info (pure $ addBlock qubesVMCountBlock) (progDesc "Display the number of running Qubes (VMs)."))
) )
diskUsageBlockParser :: Parser (BarIO ())
diskUsageBlockParser = do
file <- strArgument (metavar "FILE" <> help "The FILE by which the file system is selected.")
return $ addBlock $ diskUsageBlock file
scriptBlockParser :: Parser (BarIO ()) scriptBlockParser :: Parser (BarIO ())
scriptBlockParser = helper <*> do scriptBlockParser = helper <*> do
poll <- switch $ long "poll" <> short 'p' <> help "Run script in poll mode (every line of output updates the block)." poll <- switch $ long "poll" <> short 'p' <> help "Run script in poll mode (at regular intervals)"
-- HACK optparse-applicative does not support options of style --poll[=INTERVAL],
-- so we add a second option to specify the interval explicitly instead
-- https://github.com/pcapriotti/optparse-applicative/issues/243
pollInterval <- fromMaybe defaultInterval <$> optional (IntervalSeconds <$> option auto (
long "interval" <>
short 'i' <>
metavar "SECONDS" <>
help ("Interval to use for --poll mode (default: " <> humanReadableInterval defaultInterval <> ")")
))
clickEvents <- switch $ long "events" <> short 'e' <> help "Send click events to stdin of the script"
script <- strArgument (metavar "SCRIPT" <> help "The script that will be executed with a shell.") script <- strArgument (metavar "SCRIPT" <> help "The script that will be executed with a shell.")
return $ (if poll then addBlock . pollScriptBlock else addBlock . scriptBlock) script return $ (if poll then addBlock . pollScriptBlock pollInterval else addBlock . scriptBlock clickEvents) script
squeekboardParser :: Parser (BarIO ())
squeekboardParser = do
autoHide <- switch $ long "auto-hide" <> short 'q' <> help "Hide the block (instead of showing an error) when squeekboard is not running."
return $ addBlock (squeekboardBlock autoHide)
qubesPropertyBlockParser :: Parser (BarIO ())
qubesPropertyBlockParser = do
name <- strArgument (metavar "NAME" <> help "The NAME of the property.")
return $ addBlock $ qubesMonitorPropertyBlock name
qubesCommandParser :: Parser (MainOptions -> IO ())
qubesCommandParser = hsubparser (
command "stats" (info (pure $ const $ printEvents qubesVMStats) (progDesc "Subscribe to VM stats and print them to stdout.")) <>
command "events" (info (pure $ const $ printEvents qubesEvents) (progDesc "Subscribe to events and print them to stdout."))
)
module QBar.Color where module QBar.Color (
Color(..),
colorParser,
hexColorText,
) where
import QBar.Prelude
import Data.Aeson import Data.Aeson
import Data.Aeson.Types qualified as AT
import Data.Attoparsec.Text.Lazy as A
import Data.Bits ((.|.), shiftL) import Data.Bits ((.|.), shiftL)
import Data.Char (ord) import Data.Char (ord)
import Data.Attoparsec.Text.Lazy as A
import Data.Colour.RGBSpace import Data.Colour.RGBSpace
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import Numeric (showHex) import Numeric (showHex)
data Color = ColorRGB (RGB Double) | ColorRGBA (RGB Double) Double data Color = ColorRGB (RGB Double) | ColorRGBA (RGB Double) Double
deriving (Eq, Show) deriving (Eq, Show)
instance FromJSON Color where instance FromJSON Color where
parseJSON = withText "Color" $ either fail pure . parseOnly (colorParser <* endOfInput) parseJSON :: Value -> AT.Parser Color
parseJSON = withText "Color" $ either fail pure . A.parseOnly (colorParser <* endOfInput) . T.fromStrict
instance ToJSON Color where instance ToJSON Color where
toJSON = String . T.toStrict . hexColorText toJSON = String . T.toStrict . hexColorText
...@@ -33,28 +43,29 @@ hexColorText = hexColor' ...@@ -33,28 +43,29 @@ hexColorText = hexColor'
paddedHexComponent :: Text -> Text paddedHexComponent :: Text -> Text
paddedHexComponent hex = paddedHexComponent hex =
let len = 2 - T.length hex let len = 2 - T.length hex
padding = if len == 1 then "0" else "" padding :: Text = if len == 1 then "0" else ""
in padding <> hex in padding <> hex
colorParser :: Parser Color colorParser :: A.Parser Color
colorParser = do colorParser = do
void $ char '#' void $ char '#'
rgb <- RGB <$> doubleFromHex2 <*> doubleFromHex2 <*> doubleFromHex2 rgb <- RGB <$> doubleFromHex2 <*> doubleFromHex2 <*> doubleFromHex2
option (ColorRGB rgb) (ColorRGBA rgb <$> doubleFromHex2) option (ColorRGB rgb) (ColorRGBA rgb <$> doubleFromHex2)
where where
doubleFromHex2 :: Parser Double doubleFromHex2 :: A.Parser Double
doubleFromHex2 = (/ 256) . fromIntegral <$> hexadecimal'' 2 doubleFromHex2 = (/ 256) . fromIntegral <$> hexadecimal'' 2
-- |Variant of 'Data.Attoparsec.Text.hexadecimal' that parses a fixed amount of digits. -- Variant of 'Data.Attoparsec.Text.hexadecimal' that parses a fixed amount of digits.
hexadecimal'' :: Int -> Parser Int hexadecimal'' :: Int -> A.Parser Int
hexadecimal'' digits = foldl step 0 <$> count digits (satisfy isHexDigit) hexadecimal'' digits = foldl step 0 <$> count digits (satisfy isHexDigit)
where where
isHexDigit c = (c >= '0' && c <= '9') || isHexDigit c = (c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') || (c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F') (c >= 'A' && c <= 'F')
step a c | w >= 48 && w <= 57 = (a `shiftL` 4) .|. fromIntegral (w - 48)
| w >= 97 = (a `shiftL` 4) .|. fromIntegral (w - 87)
| otherwise = (a `shiftL` 4) .|. fromIntegral (w - 55)
where w = ord c
step :: Int -> Char -> Int
step a c | w >= 48 && w <= 57 = (a `shiftL` 4) .|. (w - 48)
| w >= 97 = (a `shiftL` 4) .|. (w - 87)
| otherwise = (a `shiftL` 4) .|. (w - 55)
where w = ord c
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE InstanceSigs #-}
module QBar.ControlSocket where module QBar.ControlSocket (
Command(..),
CommandResult(..),
Down,
Up,
addServerMirrorStream,
listenUnixSocketAsync,
sendBlockStream,
sendBlockStreamStdio,
sendIpc,
) where
import QBar.BlockOutput import QBar.BlockOutput
import QBar.Core import QBar.Core
import QBar.Host import QBar.Host
import QBar.Util import QBar.Prelude
import QBar.Time
import QBar.Utils
import Control.Exception (SomeException, handle)
import Control.Concurrent (forkFinally) import Control.Concurrent (forkFinally)
import Control.Concurrent.Async import Control.Concurrent.Async
import Control.Exception (SomeException, IOException, handle, onException)
import Data.Aeson (FromJSON, ToJSON) import Data.Aeson (FromJSON, ToJSON)
import Data.Aeson.TH import Data.ByteString qualified as BS
import qualified Data.ByteString.Char8 as BSC import Data.ByteString.Char8 qualified as BSC
import System.FilePath ((</>))
import System.IO
import Data.Text.Lazy (pack) import Data.Text.Lazy (pack)
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import Data.Text.Lazy.IO qualified as T
import Data.Time.Clock (getCurrentTime, addUTCTime)
import GHC.Generics
import Network.Socket import Network.Socket
import Pipes import Pipes
import Pipes.Concurrent as PC (Output, spawn, spawn', unbounded, newest, toOutput, fromInput, send, atomically)
import Pipes.Parse
import qualified Pipes.Prelude as PP
import Pipes.Aeson (decode, DecodingError) import Pipes.Aeson (decode, DecodingError)
import Pipes.Aeson.Unchecked (encode) import Pipes.Aeson.Unchecked (encode)
import Pipes.Concurrent as PC (Output, spawn, spawn', unbounded, newest, toOutput, fromInput, send, atomically)
import Pipes.Network.TCP (fromSocket, toSocket) import Pipes.Network.TCP (fromSocket, toSocket)
import Pipes.Parse
import Pipes.Prelude qualified as PP
import Pipes.Safe (catch)
import System.Directory (removeFile, doesFileExist) import System.Directory (removeFile, doesFileExist)
import System.Environment (getEnv) import System.Environment (getEnv)
import System.Exit (exitSuccess)
import System.FilePath ((</>))
import System.IO
type CommandHandler = Command -> IO CommandResult type CommandHandler = Command -> IO CommandResult
...@@ -46,15 +56,20 @@ class (ToJSON (Up s), FromJSON (Up s), ToJSON (Down s), FromJSON (Down s)) => Is ...@@ -46,15 +56,20 @@ class (ToJSON (Up s), FromJSON (Up s), ToJSON (Down s), FromJSON (Down s)) => Is
toStreamType :: s -> StreamType toStreamType :: s -> StreamType
streamClient :: s -> MainOptions -> BarIO (Consumer (Up s) IO (), Producer (Down s) IO ()) streamClient :: s -> MainOptions -> BarIO (Consumer (Up s) IO (), Producer (Down s) IO ())
streamClient s options@MainOptions{verbose} = do streamClient s options = do
sock <- liftIO $ connectIpcSocket options sock <- liftIO $ connectIpcSocket options
runEffect (encode (StartStream $ toStreamType s) >-> toSocket sock) streamClient' s options (toSocket sock) (fromSocket sock 4096)
let up = forever (await >>= encode) >-> verbosePrintP >-> toSocket sock
let down = decodeStreamSafe options (fromSocket sock 4096 >-> verbosePrintP) streamClient' :: s -> MainOptions -> Consumer ByteString IO () -> Producer ByteString IO () -> BarIO (Consumer (Up s) IO (), Producer (Down s) IO ())
streamClient' s options@MainOptions{verbose} sink source = liftIO $ do
runEffect (encode (StartStream $ toStreamType s) >-> sink)
let up = forever (await >>= encode) >-> verbosePrintP >-> sink
let down = decodeStreamSafe options (source >-> verbosePrintP)
return (up, down) return (up, down)
where where
verbosePrintP :: Pipe ByteString ByteString IO () verbosePrintP :: Pipe ByteString ByteString IO ()
verbosePrintP = if verbose then (PP.chain $ BSC.hPutStrLn stderr) else cat verbosePrintP = if verbose then PP.chain $ BSC.hPutStrLn stderr else cat
handleByteStream :: s -> MainOptions -> Producer ByteString IO () -> Consumer ByteString IO () -> BarIO () handleByteStream :: s -> MainOptions -> Producer ByteString IO () -> Consumer ByteString IO () -> BarIO ()
handleByteStream s options up down = do handleByteStream s options up down = do
(handleUp, handleDown, cleanup) <- streamHandler s (handleUp, handleDown, cleanup) <- streamHandler s
...@@ -66,6 +81,40 @@ class (ToJSON (Up s), FromJSON (Up s), ToJSON (Down s), FromJSON (Down s)) => Is ...@@ -66,6 +81,40 @@ class (ToJSON (Up s), FromJSON (Up s), ToJSON (Down s), FromJSON (Down s)) => Is
void $ waitEitherCancel readTask writeTask void $ waitEitherCancel readTask writeTask
cleanup cleanup
data ReconnectMode a = ReconnectNoResend | ReconnectSendLatest a
reconnectClient :: forall up down. ReconnectMode up -> BarIO (Consumer up IO (), Producer down IO ()) -> BarIO (Consumer up IO (), Producer down IO ())
reconnectClient reconnectMode connectClient = do
(upConsumer, upProducer) <- case reconnectMode of
ReconnectNoResend -> liftIO mkBroadcastP
ReconnectSendLatest initial -> liftIO $ mkBroadcastCacheP initial
(downOutput, downInput) <- liftIO $ spawn unbounded
let downConsumer = toOutput downOutput
let downProducer = fromInput downInput
task <- barAsync $ forever $ do
(upStreamConsumer, downStreamProducer) <- connectRetry
liftIO $ do
readTask <- async $ runEffect $ downStreamProducer >-> downConsumer
writeTask <- async $ runEffect $ upProducer >-> upStreamConsumer
void $ waitEitherCancel readTask writeTask
liftIO $ link task
return (upConsumer, downProducer)
where
connectRetry :: BarIO (Consumer up IO (), Producer down IO ())
connectRetry = catch connectClient (\(_ :: IOException) -> liftIO (hPutStrLn stderr "Socket connection failed. Retrying...") >> reconnectDelay >> silentConnectRetry)
silentConnectRetry :: BarIO (Consumer up IO (), Producer down IO ())
silentConnectRetry = catch connectClient (\(_ :: IOException) -> reconnectDelay >> silentConnectRetry)
reconnectDelay :: BarIO ()
reconnectDelay = do
time <- liftIO getCurrentTime
let nextSecond = addUTCTime 1 time
sleepUntil nextSecond
decodeStreamSafe :: FromJSON v => MainOptions -> Producer ByteString IO () -> Producer v IO () decodeStreamSafe :: FromJSON v => MainOptions -> Producer ByteString IO () -> Producer v IO ()
decodeStreamSafe MainOptions{verbose} inputStream = decodeStream inputStream >-> failOnEmptyStream >-> failOnDecodingError decodeStreamSafe MainOptions{verbose} inputStream = decodeStream inputStream >-> failOnEmptyStream >-> failOnDecodingError
...@@ -95,13 +144,19 @@ decodeStreamSafe MainOptions{verbose} inputStream = decodeStream inputStream >-> ...@@ -95,13 +144,19 @@ decodeStreamSafe MainOptions{verbose} inputStream = decodeStream inputStream >->
Right v -> yield v >> failOnDecodingError' Right v -> yield v >> failOnDecodingError'
data StreamType = BlockStreamType BlockStream | MirrorStreamType MirrorStream data StreamType
= BlockStreamType BlockStream
| MirrorStreamType MirrorStream
deriving Generic
mapStreamType :: StreamType -> (forall a. IsStream a => a -> b) -> b mapStreamType :: StreamType -> (forall a. IsStream a => a -> b) -> b
mapStreamType (BlockStreamType a) f = f a mapStreamType (BlockStreamType a) f = f a
mapStreamType (MirrorStreamType a) f = f a mapStreamType (MirrorStreamType a) f = f a
data BlockStream = BlockStream data BlockStream = BlockStream
deriving Generic
instance IsStream BlockStream where instance IsStream BlockStream where
type Up BlockStream = [BlockOutput] type Up BlockStream = [BlockOutput]
type Down BlockStream = BlockEvent type Down BlockStream = BlockEvent
...@@ -141,6 +196,8 @@ instance IsStream BlockStream where ...@@ -141,6 +196,8 @@ instance IsStream BlockStream where
data MirrorStream = MirrorStream data MirrorStream = MirrorStream
deriving Generic
instance IsStream MirrorStream where instance IsStream MirrorStream where
type Up MirrorStream = BlockEvent type Up MirrorStream = BlockEvent
type Down MirrorStream = [BlockOutput] type Down MirrorStream = [BlockOutput]
...@@ -156,12 +213,13 @@ instance IsStream MirrorStream where ...@@ -156,12 +213,13 @@ instance IsStream MirrorStream where
data Request = Command Command | StartStream StreamType data Request = Command Command | StartStream StreamType
deriving Generic
data Command = SetTheme T.Text data Command = SetTheme T.Text | CheckServer
deriving Show deriving (Show, Generic)
data CommandResult = Success | Error Text data CommandResult = Success | Error Text
deriving Show deriving (Show, Generic)
ipcSocketAddress :: MainOptions -> IO FilePath ipcSocketAddress :: MainOptions -> IO FilePath
...@@ -170,19 +228,15 @@ ipcSocketAddress MainOptions{socketLocation} = maybe defaultSocketPath (return . ...@@ -170,19 +228,15 @@ ipcSocketAddress MainOptions{socketLocation} = maybe defaultSocketPath (return .
defaultSocketPath :: IO FilePath defaultSocketPath :: IO FilePath
defaultSocketPath = do defaultSocketPath = do
waylandSocketPath' <- waylandSocketPath waylandSocketPath' <- waylandSocketPath
maybe (maybe headlessSocketPath return =<< i3SocketPath) return waylandSocketPath' maybe fallbackSocketPath return waylandSocketPath'
where where
waylandSocketPath :: IO (Maybe FilePath) waylandSocketPath :: IO (Maybe FilePath)
waylandSocketPath = handleEnvError $ do waylandSocketPath = handleEnvError $ do
xdgRuntimeDir <- getEnv "XDG_RUNTIME_DIR" xdgRuntimeDir <- getEnv "XDG_RUNTIME_DIR"
waylandDisplay <- getEnv "WAYLAND_DISPLAY" waylandDisplay <- getEnv "WAYLAND_DISPLAY"
return $ xdgRuntimeDir </> waylandDisplay <> "-qbar" return $ xdgRuntimeDir </> waylandDisplay <> "-qbar"
i3SocketPath :: IO (Maybe FilePath) fallbackSocketPath :: IO FilePath
i3SocketPath = handleEnvError $ do fallbackSocketPath = do
i3SocketPath' <- getEnv "I3_SOCKET_PATH"
return $ i3SocketPath' <> "-qbar"
headlessSocketPath :: IO FilePath
headlessSocketPath = do
xdgRuntimeDir <- getEnv "XDG_RUNTIME_DIR" xdgRuntimeDir <- getEnv "XDG_RUNTIME_DIR"
return $ xdgRuntimeDir </> "qbar" return $ xdgRuntimeDir </> "qbar"
handleEnvError :: IO FilePath -> IO (Maybe FilePath) handleEnvError :: IO FilePath -> IO (Maybe FilePath)
...@@ -195,36 +249,63 @@ connectIpcSocket options = do ...@@ -195,36 +249,63 @@ connectIpcSocket options = do
connect sock $ SockAddrUnix socketPath connect sock $ SockAddrUnix socketPath
return sock return sock
$(deriveJSON defaultOptions ''Request)
$(deriveJSON defaultOptions ''Command)
$(deriveJSON defaultOptions ''CommandResult)
$(deriveJSON defaultOptions ''StreamType)
$(deriveJSON defaultOptions ''BlockStream)
$(deriveJSON defaultOptions ''MirrorStream)
sendIpc :: Command -> MainOptions -> IO () sendIpc :: Command -> MainOptions -> IO ()
sendIpc command options@MainOptions{verbose} = do sendIpc command options@MainOptions{verbose} = do
let request = Command command result <- sendIpc' command options
sock <- connectIpcSocket options case result of
runEffect $ encode request >-> toSocket sock Left err -> T.hPutStrLn stderr err
Right () -> when verbose $ hPutStrLn stderr "Success"
decodeResult <- evalStateT decode $ fromSocket sock 4096 sendIpc' :: Command -> MainOptions -> IO (Either Text ())
maybe exitEmptyStream (either exitInvalidResult showResponse) decodeResult sendIpc' command options = catch sendCommand handleException
where where
exitEmptyStream :: IO () sendCommand :: IO (Either Text ())
exitEmptyStream = hPutStrLn stderr "Empty stream" sendCommand = do
exitInvalidResult :: DecodingError -> IO () sock <- connectIpcSocket options
exitInvalidResult = hPrint stderr runEffect $ encode (Command command) >-> toSocket sock
showResponse :: CommandResult -> IO ()
showResponse Success = when verbose $ hPutStrLn stderr "Success" decodeResult <- evalStateT decode $ fromSocket sock 4096
showResponse (Error message) = hPrint stderr message return $ maybe onEmptyStream (either onInvalidResult showResponse) decodeResult
handleException :: SomeException -> IO (Either Text ())
handleException = return . Left . T.pack . show
onEmptyStream :: Either Text ()
onEmptyStream = Left "Empty stream"
onInvalidResult :: DecodingError -> Either Text ()
onInvalidResult = Left . T.pack . show
showResponse :: CommandResult -> Either Text ()
showResponse Success = Right ()
showResponse (Error message) = Left message
sendBlockStream :: BarIO () -> MainOptions -> IO () sendBlockStream :: BarIO () -> MainOptions -> IO ()
sendBlockStream loadBlocks options = runBarHost (streamClient BlockStream options) loadBlocks sendBlockStream loadBlocks options = runBarHost blockStreamClient loadBlocks
where
blockStreamClient :: BarIO (Consumer [BlockOutput] IO (), Producer BlockEvent IO ())
blockStreamClient = reconnectClient (ReconnectSendLatest []) $ streamClient BlockStream options
sendBlockStreamStdio :: BarIO () -> MainOptions -> IO ()
sendBlockStreamStdio loadBlocks options = runBarHost blockStreamClient loadBlocks
where
blockStreamClient :: BarIO (Consumer [BlockOutput] IO (), Producer BlockEvent IO ())
blockStreamClient = streamClient' BlockStream options sink source
sink :: Consumer ByteString IO ()
sink = forever $ do
value <- await
-- Close when connection to upstream qbar is lost
liftIO $ (BS.hPut stdout value >> hFlush stdout) `onException` (hPutStrLn stderr "Stdout closed" >> exitSuccess)
source :: Producer ByteString IO ()
source = forever $ do
value <- liftIO (BS.hGetSome stdin 4096)
-- Close when connection to upstream qbar is lost
when (BS.null value) $ liftIO $ do
hPutStrLn stderr "Stdin closed"
exitSuccess
yield value
addServerMirrorStream :: MainOptions -> BarIO () addServerMirrorStream :: MainOptions -> BarIO ()
addServerMirrorStream options = do addServerMirrorStream options = do
(blockEventConsumer, blockOutputProducer) <- streamClient MirrorStream options (blockEventConsumer, blockOutputProducer) <- reconnectClient ReconnectNoResend $ streamClient MirrorStream options
(eventOutput, eventInput) <- liftIO $ spawn unbounded (eventOutput, eventInput) <- liftIO $ spawn unbounded
bar <- askBar bar <- askBar
...@@ -263,22 +344,33 @@ listenUnixSocketAsync options bar commandHandler = async $ listenUnixSocket opti ...@@ -263,22 +344,33 @@ listenUnixSocketAsync options bar commandHandler = async $ listenUnixSocket opti
listenUnixSocket :: MainOptions -> Bar -> CommandHandler -> IO () listenUnixSocket :: MainOptions -> Bar -> CommandHandler -> IO ()
listenUnixSocket options@MainOptions{verbose} bar commandHandler = do listenUnixSocket options@MainOptions{verbose} bar commandHandler = do
socketPath <- ipcSocketAddress options socketPath <- ipcSocketAddress options
hPutStrLn stderr $ "Creating control socket at " <> socketPath
socketExists <- doesFileExist socketPath socketExists <- doesFileExist socketPath
when socketExists $ removeFile socketPath if socketExists
sock <- socket AF_UNIX Stream defaultProtocol then do
socketTestResult <- sendIpc' CheckServer options
case socketTestResult of
Right _ -> hPutStrLn stderr $ "Could not create control socket at " <> socketPath <> ": another server is already running"
Left _ -> do
removeFile socketPath
listenUnixSocket' socketPath
else
listenUnixSocket' socketPath
where
listenUnixSocket' :: FilePath -> IO b
listenUnixSocket' socketPath = do
hPutStrLn stderr $ "Creating control socket at " <> socketPath
sock <- socket AF_UNIX Stream defaultProtocol
#if MIN_VERSION_network(3,0,0) #if MIN_VERSION_network(3,0,0)
withFdSocket sock setCloseOnExecIfNeeded withFdSocket sock setCloseOnExecIfNeeded
#else #else
setCloseOnExecIfNeeded $ fdSocket sock setCloseOnExecIfNeeded $ fdSocket sock
#endif #endif
bind sock (SockAddrUnix socketPath) bind sock (SockAddrUnix socketPath)
listen sock 5 listen sock 5
forever $ do forever $ do
(conn, _) <- accept sock (conn, _) <- accept sock
void $ forkFinally (socketHandler conn) (handleSocketResult conn) void $ forkFinally (socketHandler conn) (handleSocketResult conn)
where
handleSocketResult :: Socket -> Either SomeException () -> IO () handleSocketResult :: Socket -> Either SomeException () -> IO ()
handleSocketResult conn (Left err) = do handleSocketResult conn (Left err) = do
when verbose $ hPutStrLn stderr $ "Ipc connection closed with error " <> show err when verbose $ hPutStrLn stderr $ "Ipc connection closed with error " <> show err
...@@ -315,3 +407,21 @@ listenUnixSocket options@MainOptions{verbose} bar commandHandler = do ...@@ -315,3 +407,21 @@ listenUnixSocket options@MainOptions{verbose} bar commandHandler = do
handleError = encode . Error . pack . show handleError = encode . Error . pack . show
errorResponse :: Text -> Producer ByteString IO () errorResponse :: Text -> Producer ByteString IO ()
errorResponse message = encode $ Error message errorResponse message = encode $ Error message
instance FromJSON BlockStream
instance ToJSON BlockStream
instance FromJSON Command
instance ToJSON Command
instance FromJSON CommandResult
instance ToJSON CommandResult
instance FromJSON MirrorStream
instance ToJSON MirrorStream
instance FromJSON Request
instance ToJSON Request
instance FromJSON StreamType
instance ToJSON StreamType
{-# LANGUAGE FlexibleInstances #-} module QBar.Core (
{-# LANGUAGE TemplateHaskell #-} Bar(..),
{-# LANGUAGE RankNTypes #-} BarIO,
BarUpdateChannel(..),
module QBar.Core where BarUpdateEvent,
Block',
Block,
BlockCache,
BlockEvent(..),
BlockEventHandler,
BlockState,
BlockUpdate,
BlockUpdateReason(..),
ExitBlock(..),
IsCachable(..),
MainOptions(..),
MonadBarIO(..),
addBlock,
addBlockCache,
askBar,
autoPadding,
barAsync,
defaultInterval,
exitBlock,
hasEventHandler,
invalidateBlockState,
mkBlockState',
mkBlockState,
modify,
newCache',
newCache,
newCacheIO,
pushBlockUpdate',
pushBlockUpdate,
pushEmptyBlockUpdate,
runBarIO,
updateBar',
updateBar,
updateBarDefault',
updateBarDefault,
updateEventHandler,
) where
import QBar.BlockOutput import QBar.BlockOutput
import QBar.Prelude
import QBar.Time import QBar.Time
import QBar.Util import QBar.Utils
import Control.Concurrent.Async import Control.Concurrent.Async
import qualified Control.Concurrent.Event as Event import Control.Concurrent.Event qualified as Event
import Control.Concurrent.MVar import Control.Concurrent.MVar
import Control.Concurrent.STM.TChan import Control.Concurrent.STM.TChan
import Control.Lens import Control.Lens
import Control.Monad.Reader (ReaderT, runReaderT, ask) import Control.Monad.Reader (ReaderT, runReaderT, ask)
import Control.Monad.State (StateT) import Control.Monad.State (StateT)
import Control.Monad.Writer (WriterT) import Control.Monad.Writer (WriterT)
import Data.Aeson.TH import Data.Aeson
import Data.Int (Int64) import Data.Int (Int64)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import GHC.Generics
import Pipes import Pipes
import Pipes.Concurrent import Pipes.Concurrent
import Pipes.Prelude qualified as PP
import Pipes.Safe (SafeT, runSafeT) import Pipes.Safe (SafeT, runSafeT)
import qualified Pipes.Prelude as PP
data MainOptions = MainOptions { data MainOptions = MainOptions {
verbose :: Bool, verbose :: Bool,
...@@ -34,16 +73,17 @@ data MainOptions = MainOptions { ...@@ -34,16 +73,17 @@ data MainOptions = MainOptions {
data BlockEvent = Click { data BlockEvent = Click {
name :: T.Text, name :: T.Text,
button :: Int button :: Int
} deriving (Eq, Show) } deriving (Eq, Show, Generic)
$(deriveJSON defaultOptions ''BlockEvent)
instance FromJSON BlockEvent
instance ToJSON BlockEvent
data ExitBlock = ExitBlock data ExitBlock = ExitBlock
type BlockEventHandler = BlockEvent -> BarIO () type BlockEventHandler = BlockEvent -> BarIO ()
type BlockState = Maybe (BlockOutput, Maybe BlockEventHandler) type BlockState = Maybe (BlockOutput, Maybe BlockEventHandler)
data BlockUpdateReason = DefaultUpdate | PollUpdate | UserUpdate data BlockUpdateReason = DefaultUpdate | PollUpdate | EventUpdate
type BlockUpdate = (BlockState, BlockUpdateReason) type BlockUpdate = (BlockState, BlockUpdateReason)
-- |Block that 'yield's an update whenever the block should be changed -- |Block that 'yield's an update whenever the block should be changed
...@@ -129,7 +169,7 @@ hasEventHandler (Just (_, Just _)) = True ...@@ -129,7 +169,7 @@ hasEventHandler (Just (_, Just _)) = True
hasEventHandler _ = False hasEventHandler _ = False
invalidateBlockState :: BlockState -> BlockState invalidateBlockState :: BlockState -> BlockState
invalidateBlockState = (_Just . _1) %~ invalidateBlock invalidateBlockState = ((_Just . _2) .~ Nothing) . ((_Just . _1) %~ invalidateBlock)
runBarIO :: MonadIO m => Bar -> BarIO r -> m r runBarIO :: MonadIO m => Bar -> BarIO r -> m r
...@@ -208,7 +248,7 @@ newCache'' = do ...@@ -208,7 +248,7 @@ newCache'' = do
-- |Creates a cache from a block. -- |Creates a cache from a block.
cacheBlock :: Block -> BlockCache cacheBlock :: Block -> BlockCache
-- 'Block's 'yield' an update whenever they want to update the cache. -- 'Block's 'yield' an update whenever they want to update the cache.
cacheBlock pushBlock = newCache $ () <$ (pushBlock >-> updateBarP >-> addBlockName >-> PP.map (\a -> [a])) cacheBlock pushBlock = newCache $ void $ pushBlock >-> updateBarP >-> addBlockName >-> PP.map (: [])
where where
updateBarP :: Pipe BlockUpdate BlockState BarIO r updateBarP :: Pipe BlockUpdate BlockState BarIO r
updateBarP = forever $ do updateBarP = forever $ do
...@@ -216,7 +256,7 @@ cacheBlock pushBlock = newCache $ () <$ (pushBlock >-> updateBarP >-> addBlockNa ...@@ -216,7 +256,7 @@ cacheBlock pushBlock = newCache $ () <$ (pushBlock >-> updateBarP >-> addBlockNa
yield state yield state
updateBar reason updateBar reason
-- |Sets 'blockName' to a random (but static) identifier if an event handler is set but the 'blockName' is not set. -- Sets 'blockName' to a random (but static) identifier if an event handler is set but the 'blockName' is not set.
addBlockName :: Pipe BlockState BlockState BarIO r addBlockName :: Pipe BlockState BlockState BarIO r
addBlockName = do addBlockName = do
defaultBlockName <- randomIdentifier defaultBlockName <- randomIdentifier
...@@ -236,9 +276,9 @@ autoPadding = autoPadding' 0 0 ...@@ -236,9 +276,9 @@ autoPadding = autoPadding' 0 0
maybeBlock <- await maybeBlock <- await
case maybeBlock of case maybeBlock of
(Just (block, eventHandler), reason) -> do (Just (block, eventHandler), reason) -> do
let fullLength' = max fullLength . printedLength $ block^.fullText let fullLength' = max fullLength . printedLength $ block ^. fullText
let shortLength' = max shortLength . printedLength $ block^.shortText._Just let shortLength' = max shortLength . printedLength $ block ^. shortText._Just
yield $ (Just (padFullText fullLength' . padShortText shortLength' $ block, eventHandler), reason) yield (Just (padFullText fullLength' . padShortText shortLength' $ block, eventHandler), reason)
autoPadding' fullLength' shortLength' autoPadding' fullLength' shortLength'
(Nothing, reason) -> do (Nothing, reason) -> do
yield (Nothing, reason) yield (Nothing, reason)
......
module QBar.DefaultConfig (
defaultBarConfig
) where
import QBar.Blocks
import QBar.Core
import QBar.Prelude
defaultBarConfig :: BarIO ()
defaultBarConfig = do
-- TODO: commented-out blocks should be added as soon as they are implemented in qbar
addBlock dateBlock
addBlock batteryBlock
--addBlock volumeBlock
addBlock $ cpuUsageBlock 1
--addBlock ramUsageBlock
--addBlock freeDiskSpaceBlock
--addBlock cpuTemperatureBlock
addBlock networkManagerBlock
{-# LANGUAGE ScopedTypeVariables #-} module QBar.Host (
{-# LANGUAGE DuplicateRecordFields #-} HostHandle(..),
attachBarOutput,
module QBar.Host where eventDispatcher,
filterDuplicates,
installSignalHandlers,
requestBarUpdateHandler,
runBarHost',
runBarHost,
runBlocks,
) where
import QBar.BlockOutput import QBar.BlockOutput
import QBar.Core import QBar.Core
import QBar.Prelude
import QBar.Time import QBar.Time
import QBar.Utils
import Control.Concurrent (forkIO, forkFinally, threadDelay) import Control.Concurrent (forkIO, forkFinally, threadDelay)
import Control.Concurrent.Async (async, wait, waitBoth) import Control.Concurrent.Async (async, wait, waitAny)
import qualified Control.Concurrent.Event as Event import Control.Concurrent.Event qualified as Event
import Control.Concurrent.MVar (MVar, newMVar, modifyMVar_, swapMVar) import Control.Concurrent.MVar
import Control.Concurrent.STM.TChan import Control.Concurrent.STM.TChan
import Control.Concurrent.STM.TVar import Control.Exception (SomeException, catch, fromException)
import Control.Exception (SomeException, catch)
import Control.Lens hiding (each, (.=)) import Control.Lens hiding (each, (.=))
import Control.Monad.STM (atomically) import Control.Monad.STM (atomically)
import Data.IORef (IORef, newIORef, readIORef, writeIORef) import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Maybe (catMaybes, mapMaybe) import Data.Maybe (catMaybes, mapMaybe)
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import Pipes import Pipes
import Pipes.Concurrent (spawn, unbounded, toOutput, fromInput) import Pipes.Concurrent (spawn, unbounded, toOutput, fromInput)
import System.Exit (ExitCode, exitWith)
import System.IO (stderr, hPutStrLn) import System.IO (stderr, hPutStrLn)
import System.Posix.Signals (Handler(..), sigCONT, installHandler) import System.Posix.Signals (Handler(..), sigCONT, installHandler)
...@@ -114,13 +123,13 @@ runBlocks bar HostHandle{barUpdateEvent, barUpdatedEvent, followupEventWaitTimeM ...@@ -114,13 +123,13 @@ runBlocks bar HostHandle{barUpdateEvent, barUpdatedEvent, followupEventWaitTimeM
writeIORef eventHandlerListIORef eventHandlerList writeIORef eventHandlerListIORef eventHandlerList
where where
eventHandlerList :: [(T.Text, BlockEventHandler)] eventHandlerList :: [(T.Text, BlockEventHandler)]
eventHandlerList = mapMaybe getEventHandler $ blockStates eventHandlerList = mapMaybe getEventHandler blockStates
getEventHandler :: BlockState -> Maybe (T.Text, BlockEventHandler) getEventHandler :: BlockState -> Maybe (T.Text, BlockEventHandler)
getEventHandler Nothing = Nothing getEventHandler Nothing = Nothing
getEventHandler (Just (_, Nothing)) = Nothing getEventHandler (Just (_, Nothing)) = Nothing
getEventHandler (Just (blockOutput, Just eventHandler)) = do getEventHandler (Just (blockOutput, Just eventHandler)) = do
blockName' <- blockOutput^.blockName blockName' <- blockOutput ^. blockName
return (blockName', eventHandler) return (blockName', eventHandler)
...@@ -140,8 +149,8 @@ filterDuplicates = do ...@@ -140,8 +149,8 @@ filterDuplicates = do
followupEventWaitTime :: BlockUpdateReason -> Int followupEventWaitTime :: BlockUpdateReason -> Int
followupEventWaitTime DefaultUpdate = 10000 followupEventWaitTime DefaultUpdate = 10000
followupEventWaitTime PollUpdate = 50000 followupEventWaitTime PollUpdate = 50000
-- 'followupEventWaitTime' for 'UserUpdate' has to be zero, or blocks would be blocked blocked for this time when sending a 'UserUpdate'. -- 'followupEventWaitTime' for 'EventUpdate' has to be zero, or blocks would be blocked blocked for this time when sending a 'UserUpdate'.
followupEventWaitTime UserUpdate = 0 followupEventWaitTime EventUpdate = 0
followupEventWaitTimeDefault :: Int followupEventWaitTimeDefault :: Int
followupEventWaitTimeDefault = followupEventWaitTime PollUpdate followupEventWaitTimeDefault = followupEventWaitTime PollUpdate
...@@ -153,7 +162,7 @@ requestBarUpdateHandler HostHandle{barUpdateEvent, barUpdatedEvent, followupEven ...@@ -153,7 +162,7 @@ requestBarUpdateHandler HostHandle{barUpdateEvent, barUpdatedEvent, followupEven
signalHost blockUpdateReason signalHost blockUpdateReason
where where
signalHost :: BlockUpdateReason -> IO () signalHost :: BlockUpdateReason -> IO ()
signalHost UserUpdate = do signalHost EventUpdate = do
-- Start waiting before triggering the event cannot be missed -- Start waiting before triggering the event cannot be missed
task <- async $ Event.wait barUpdatedEvent task <- async $ Event.wait barUpdatedEvent
Event.set barUpdateEvent Event.set barUpdateEvent
...@@ -164,8 +173,8 @@ requestBarUpdateHandler HostHandle{barUpdateEvent, barUpdatedEvent, followupEven ...@@ -164,8 +173,8 @@ requestBarUpdateHandler HostHandle{barUpdateEvent, barUpdatedEvent, followupEven
attachBarOutput :: (Consumer [BlockOutput] IO (), Producer BlockEvent IO ()) -> BarIO () attachBarOutput :: (Consumer [BlockOutput] IO (), Producer BlockEvent IO ()) -> BarIO ()
attachBarOutput (blockOutputConsumer, blockEventProducer) = do attachBarOutput (blockOutputConsumer, blockEventProducer) = do
bar <- askBar Bar{attachBarOutputInternal} <- askBar
liftIO $ attachBarOutputInternal bar (blockOutputConsumer, blockEventProducer) liftIO $ attachBarOutputInternal (blockOutputConsumer, blockEventProducer)
runBarHost :: BarIO (Consumer [BlockOutput] IO (), Producer BlockEvent IO ()) -> BarIO () -> IO () runBarHost :: BarIO (Consumer [BlockOutput] IO (), Producer BlockEvent IO ()) -> BarIO () -> IO ()
...@@ -188,6 +197,8 @@ runBarHost' initializeBarAction = do ...@@ -188,6 +197,8 @@ runBarHost' initializeBarAction = do
-- Create IORef for event handlers -- Create IORef for event handlers
eventHandlerListIORef <- newIORef [] eventHandlerListIORef <- newIORef []
exitCodeMVar <- newEmptyMVar
let hostHandle = HostHandle { let hostHandle = HostHandle {
barUpdateEvent, barUpdateEvent,
barUpdatedEvent, barUpdatedEvent,
...@@ -199,11 +210,10 @@ runBarHost' initializeBarAction = do ...@@ -199,11 +210,10 @@ runBarHost' initializeBarAction = do
(eventOutput, eventInput) <- spawn unbounded (eventOutput, eventInput) <- spawn unbounded
-- Create cache for block outputs -- Create cache for block outputs
cache <- (,) <$> newTVarIO [] <*> newBroadcastTChanIO (cacheConsumer, cacheProducer) <- mkBroadcastCacheP []
let blockOutputProducer = blockOutputFromCache cache
-- Important: both monads (output producer / event consumer) will be forked whenever a new output connects! -- Important: both monads (output producer / event consumer) will be forked whenever a new output connects!
let attachBarOutputInternal = attachBarOutputImpl blockOutputProducer (toOutput eventOutput) let attachBarOutputInternal = attachBarOutputImpl exitCodeMVar cacheProducer (toOutput eventOutput)
let requestBarUpdate = requestBarUpdateHandler hostHandle let requestBarUpdate = requestBarUpdateHandler hostHandle
...@@ -217,38 +227,35 @@ runBarHost' initializeBarAction = do ...@@ -217,38 +227,35 @@ runBarHost' initializeBarAction = do
runBarIO bar initializeBarAction runBarIO bar initializeBarAction
-- Run blocks and send filtered output to connected clients -- Run blocks and send filtered output to connected clients
blockTask <- async $ runEffect $ runBlocks bar hostHandle >-> filterDuplicates >-> blockOutputToCache cache blockTask <- async $ runEffect $ runBlocks bar hostHandle >-> filterDuplicates >-> cacheConsumer
-- Dispatch incoming events to blocks -- Dispatch incoming events to blocks
eventTask <- async $ runEffect $ fromInput eventInput >-> eventDispatcher bar eventHandlerListIORef eventTask <- async $ runEffect $ fromInput eventInput >-> eventDispatcher bar eventHandlerListIORef
exitTask <- async $ takeMVar exitCodeMVar >>= exitWith
void $ waitBoth blockTask eventTask
where
blockOutputToCache :: (TVar [BlockOutput], TChan [BlockOutput]) -> Consumer [BlockOutput] IO ()
blockOutputToCache (var, chan) = forever $ do
value <- await
liftIO . atomically $ do
writeTVar var value
writeTChan chan value
-- Monad will be forked when new outputs connect
blockOutputFromCache :: (TVar [BlockOutput], TChan [BlockOutput]) -> Producer [BlockOutput] IO ()
blockOutputFromCache (var, chan) = do
(outputChan, value) <- liftIO . atomically $ do
value <- readTVar var
outputChan <- dupTChan chan
return (outputChan, value)
yield value void $ waitAny [blockTask, eventTask, exitTask]
forever $ yield =<< (liftIO . atomically $ readTChan outputChan) where
attachBarOutputImpl :: MVar ExitCode -> Producer [BlockOutput] IO () -> Consumer BlockEvent IO () -> (Consumer [BlockOutput] IO (), Producer BlockEvent IO ()) -> IO ()
attachBarOutputImpl exitMVar blockOutputProducer eventConsumer (barOutputConsumer, barEventProducer) = do
attachBarOutputImpl :: Producer [BlockOutput] IO () -> Consumer BlockEvent IO () -> (Consumer [BlockOutput] IO (), Producer BlockEvent IO ()) -> IO () let
attachBarOutputImpl blockOutputProducer eventConsumer (barOutputConsumer, barEventProducer) = do handleBarEventInput :: IO ()
handleBarEventInput = liftIO $ runEffect $ barEventProducer >-> eventConsumer
liftIO $ void $ forkFinally handleBarEventInput $ handleOnExitCodeException (\result -> hPutStrLn stderr $ "An event input handler failed: " <> show result)
let handleBarEventInput = liftIO $ runEffect $ barEventProducer >-> eventConsumer let
liftIO $ void $ forkFinally handleBarEventInput (\result -> hPutStrLn stderr $ "An event input handler failed: " <> show result) handleBarOutput :: IO ()
handleBarOutput = liftIO $ runEffect $ blockOutputProducer >-> filterDuplicates >-> barOutputConsumer
liftIO $ void $ forkFinally handleBarOutput $ handleOnExitCodeException (\result -> hPutStrLn stderr $ "A bar output handler failed: " <> show result)
let handleBarOutput = liftIO $ runEffect $ blockOutputProducer >-> filterDuplicates >-> barOutputConsumer where
liftIO $ void $ forkFinally handleBarOutput (\result -> hPutStrLn stderr $ "A bar output handler failed: " <> show result) -- Calls the next handler unless the exception is an ExitCode.
handleOnExitCodeException :: (Either SomeException () -> IO ()) -> Either SomeException () -> IO ()
handleOnExitCodeException nextHandler x@(Left ex) = case fromException ex of
Just exitCode -> do
hPutStrLn stderr "Exiting"
putMVar exitMVar exitCode
Nothing -> nextHandler x
handleOnExitCodeException nextHandler x = nextHandler x
{-# LANGUAGE DuplicateRecordFields #-} module QBar.Pango (
PangoText,
module QBar.Pango (PangoText, renderPango) where renderPango,
) where
import QBar.Color import QBar.Color
import QBar.Prelude
import QBar.Theme import QBar.Theme
type PangoText = Text type PangoText = Text
......
{-# LANGUAGE NoImplicitPrelude #-} module QBar.Prelude (
module Prelude,
module Prelude (<=<),
( module BasePrelude, (>=>),
ByteString.ByteString, ByteString.ByteString,
(>=>), Control.Monad.IO.Class.MonadIO,
(<=<), Control.Monad.IO.Class.liftIO,
Control.Monad.forever, Control.Monad.forever,
Control.Monad.unless, Control.Monad.unless,
Control.Monad.void, Control.Monad.void,
Control.Monad.when, Control.Monad.when,
Control.Monad.IO.Class.MonadIO, Maybe.listToMaybe,
Control.Monad.IO.Class.liftIO, Text.Text,
Text.Text, error,
Maybe.listToMaybe, errorWithoutStackTrace,
error, head,
errorWithoutStackTrace, intercalate,
head, trace,
intercalate, traceIO,
trace, traceId,
traceId, traceM,
traceShow, traceShow,
traceShowId, traceShowIO,
traceM, traceShowId,
traceShowM, traceShowIdIO,
traceIO, traceShowM,
traceShowIO, undefined,
traceShowIdIO, ) where
undefined,
) import Prelude hiding
where
import BasePrelude hiding
( error, ( error,
errorWithoutStackTrace, errorWithoutStackTrace,
head, head,
undefined, undefined,
) )
import qualified BasePrelude as P
import qualified Control.Monad
import Control.Monad ((>=>), (<=<)) import Control.Monad ((>=>), (<=<))
import qualified Control.Monad.IO.Class import Control.Monad qualified
import qualified Data.ByteString as ByteString import Control.Monad.IO.Class qualified
import qualified Data.Maybe as Maybe import Data.ByteString qualified as ByteString
import qualified Data.Text.Lazy as Text import Data.Maybe qualified as Maybe
import qualified Debug.Trace as Trace import Data.Text.Lazy qualified as Text
import qualified GHC.Stack.Types import Debug.Trace qualified as Trace
import GHC.Stack.Types
import Prelude qualified as P
{-# DEPRECATED head "Partial Function." #-} {-# DEPRECATED head "Partial Function." #-}
head :: [a] -> a head :: [a] -> a
......
{-# OPTIONS_GHC -Wno-partial-fields #-}
module QBar.Qubes.AdminAPI (
QubesPropertyInfo(..),
QubesVMInfo(..),
QubesVMState(..),
printEvents,
qubesEvents,
qubesGetProperty,
qubesListLabelNames,
qubesListProperties,
qubesListVMs,
qubesListVMsP,
qubesMonitorProperty,
qubesUsageOfDefaultPool,
qubesVMStats,
) where
import QBar.Prelude
import Control.Monad (forM_)
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.ByteString.Lazy.Char8 qualified as BLC
import Data.Char (isAlphaNum)
import Data.Function ((&))
import Data.Map qualified as Map
import Data.Maybe (fromMaybe)
import Network.HostName
import Pipes
import Pipes.Prelude qualified as P
import Pipes.Safe qualified as P
import System.IO (Handle, hSetBinaryMode)
import System.Process.Typed
import Text.Read (readMaybe)
data QubesAdminReturn
= Ok { okContent :: BL.ByteString }
| Event { evSubject :: BL.ByteString, evEvent :: BL.ByteString, evProperties :: [(BL.ByteString, BL.ByteString)] }
| Exception { excType :: BL.ByteString, excTraceback :: BL.ByteString, excFormatString :: BL.ByteString, excFields :: [BL.ByteString] }
deriving (Eq, Ord, Show, Read)
putLazyByteStringNul :: BL.ByteString -> Put
putLazyByteStringNul x = do
when (0 `BL.elem` x) $ error "String must not contain any \\x00 bytes"
putLazyByteString x
putWord8 0x00
instance Binary QubesAdminReturn where
put Ok {okContent} = do
putWord8 0x30 >> putWord8 0x00
putLazyByteString okContent
put Event {evSubject, evEvent, evProperties} = do
putWord8 0x31 >> putWord8 0x00
putLazyByteStringNul evSubject
putLazyByteStringNul evEvent
forM_ evProperties $ \(k, v) -> do
putLazyByteStringNul k
putLazyByteStringNul v
putWord8 0x00
put Exception {excType, excTraceback, excFormatString, excFields} = do
putWord8 0x32 >> putWord8 0x00
putLazyByteStringNul excType
putLazyByteStringNul excTraceback
putLazyByteStringNul excFormatString
forM_ excFields putLazyByteStringNul
putWord8 0x00
get = do
msgType <- getWord8
zero <- getWord8
case (msgType, zero) of
(0x30, 0x00) -> Ok <$> getRemainingLazyByteString
(0x31, 0x00) -> Event <$> getLazyByteStringNul <*> getLazyByteStringNul <*> getPairs
(0x32, 0x00) -> Exception <$> getLazyByteStringNul <*> getLazyByteStringNul <*> getLazyByteStringNul <*> getFields
_ -> fail $ "unsupported message type " <> show msgType <> ", " <> show zero
where
getPairs = untilZeroByte $ (,) <$> getLazyByteStringNul <*> getLazyByteStringNul
getFields = untilZeroByte getLazyByteStringNul
untilZeroByte :: Get a -> Get [a]
untilZeroByte inner = lookAhead getWord8 >>= \case
0x00 -> getWord8 >> return []
_ -> inner >>= \x -> (x:) <$> untilZeroByte inner
qubesAdminConnect :: BL.ByteString -> [BL.ByteString] -> IO (Process () Handle ())
qubesAdminConnect serviceName args = do
hostname <- getHostName
let concatArgs sep = mconcat (map (sep <>) args)
let cmd = if hostname == "dom0"
then "qubesd-query dom0 " <> serviceName <> " dom0" <> concatArgs " "
else "qrexec-client-vm dom0 " <> serviceName <> concatArgs "+"
--NOTE qubesd-query and qrexec-client-vm don't like it if their input
-- is closed rather than empty.
-- hangs: qrexec-client-vm dom0 admin.vm.List <&-
-- works: qrexec-client-vm dom0 admin.vm.List </dev/null
let processConfig = setStdin nullStream $ setStdout createPipe $ shell $ BLC.unpack cmd
startProcess processConfig
qubesTryAdminCall :: BL.ByteString -> [BL.ByteString] -> IO QubesAdminReturn
qubesTryAdminCall serviceName args = do
process <- qubesAdminConnect serviceName args
let stdout = getStdout process
hSetBinaryMode stdout True
reply <- decode <$> BL.hGetContents stdout
case reply of
Ok {} -> return reply
Exception {} -> return reply
Event {} -> fail "service has returned events instead of a reply"
qubesAdminCall :: BL.ByteString -> [BL.ByteString] -> IO BL.ByteString
qubesAdminCall serviceName args = qubesTryAdminCall serviceName args >>= extract where
extract :: QubesAdminReturn -> IO BLC.ByteString
extract Ok {okContent} = return okContent
extract x@Exception {} = fail $ "service has returned an exception: " <> show x
extract Event {} = fail "service has returned events instead of a reply"
qubesAdminCallP :: forall m. (P.MonadSafe m, MonadFail m)
=> BL.ByteString -> [BL.ByteString] -> Producer QubesAdminReturn m ()
qubesAdminCallP serviceName args = do
process <- liftIO $ qubesAdminConnect serviceName args
let stdout = getStdout process
liftIO $ hSetBinaryMode stdout True
let go :: Decoder QubesAdminReturn -> Producer QubesAdminReturn m ()
go = \case
Done remainder _ value -> do
yield value
go $ pushChunk (runGetIncremental get) remainder
d@(Partial _) -> do
chunk <- liftIO $ BS.hGetSome stdout 1024
if not (BS.null chunk)
then go $ pushChunk d chunk
else case pushEndOfInput d of
Done _ _ value -> yield value
_ -> return ()
Fail _ _ msg ->
fail $ "decoding reply from QubesAdmin failed: " <> msg
go (runGetIncremental get)
`P.finally` stopProcess process
qubesAdminEvents :: forall m. (P.MonadSafe m, MonadFail m)
=> BL.ByteString -> [BL.ByteString] -> Producer QubesAdminReturn m ()
qubesAdminEvents serviceName args = qubesAdminCallP serviceName args >-> onlyEvents
where
onlyEvents :: Pipe QubesAdminReturn QubesAdminReturn m ()
onlyEvents = forever $ await >>= \reply -> case reply of
Ok {} -> fail "service has returned OK instead of events"
Exception {} -> fail $ "service has returned an exception: " ++ show reply
Event {} -> yield reply
qubesVMStatsRaw :: forall m. (P.MonadSafe m, MonadFail m)
=> Producer QubesAdminReturn m ()
qubesVMStatsRaw = qubesAdminEvents "admin.vm.Stats" []
data QubesVMStats = QubesVMStats { statsVMName :: BL.ByteString, memoryKB :: Int, cpuTime :: Int, cpuUsageRaw :: Int, cpuUsage :: Int }
deriving (Eq, Ord, Show, Read)
qubesVMStats :: forall m. (P.MonadSafe m, MonadFail m)
=> Producer QubesVMStats m ()
qubesVMStats = qubesVMStatsRaw >-> P.mapFoldable parse where
parse :: QubesAdminReturn -> Maybe QubesVMStats
parse Event {evSubject, evEvent, evProperties}
| evEvent == "connection-established" = Nothing
| evEvent == "vm-stats" = Just $ addProperties evProperties $ QubesVMStats evSubject absent absent absent absent
| otherwise = Nothing -- shouldn't happen -> report error?
parse _ = Nothing -- shouldn't happen -> report error?
absent :: Int = -1
readBL :: BLC.ByteString -> Int
readBL = read . BLC.unpack
addProperties :: [(BL.ByteString, BL.ByteString)] -> QubesVMStats -> QubesVMStats
addProperties (("memory_kb", x) : xs) st = addProperties xs $ st { memoryKB = readBL x }
addProperties (("cpu_time", x) : xs) st = addProperties xs $ st { cpuTime = readBL x }
addProperties (("cpu_usage_raw", x) : xs) st = addProperties xs $ st { cpuUsageRaw = readBL x }
addProperties (("cpu_usage", x) : xs) st = addProperties xs $ st { cpuUsage = readBL x }
addProperties (_ : xs) st = addProperties xs st
addProperties [] st = st
data QubesEvent
= OtherEvent QubesAdminReturn
| DomainPreStart { domainName :: BL.ByteString, startGuid :: Maybe Bool }
| DomainStart { domainName :: BL.ByteString, startGuid :: Maybe Bool }
| DomainUnpaused { domainName :: BL.ByteString }
| DomainStopped { domainName :: BL.ByteString }
| DomainShutdown { domainName :: BL.ByteString }
| DomainUpdatesAvailable { domainName :: BL.ByteString, updatesAvailable :: Bool, updatesAvailableOld :: Bool }
| DomainStartFailed { domainName :: BL.ByteString, reason :: BL.ByteString }
| PropertySet { domainName :: BL.ByteString, changedProperty :: BL.ByteString, newValue :: BL.ByteString, oldValue :: BL.ByteString }
| PropertyDel { domainName :: BL.ByteString, changedProperty :: BL.ByteString, oldValue :: BL.ByteString } -- reset to default value
deriving (Eq, Ord, Show, Read)
qubesEventsRaw :: forall m. (P.MonadSafe m, MonadFail m)
=> Producer QubesAdminReturn m ()
qubesEventsRaw = qubesAdminEvents "admin.Events" []
qubesEvents :: forall m. (P.MonadSafe m, MonadFail m)
=> Producer QubesEvent m ()
qubesEvents = qubesEventsRaw >-> P.mapFoldable parse where
parse :: QubesAdminReturn -> Maybe QubesEvent
parse Event {evEvent="connection-established"} = Nothing
parse ev@(Event {evSubject, evEvent, evProperties}) =
Just $ case evEvent of
"domain-pre-start" -> DomainPreStart evSubject (boolProp "start_guid")
"domain-start" -> DomainStart evSubject (boolProp "start_guid")
"domain-unpaused" -> DomainUnpaused evSubject
"domain-stopped" -> DomainStopped evSubject
"domain-shutdown" -> DomainShutdown evSubject
"domain-feature-set:updates-available" ->
DomainUpdatesAvailable evSubject (boolPropViaInt "value") (boolPropViaInt "oldvalue")
"domain-start-failed" ->
DomainStartFailed evSubject (fromMaybe "" $ getProp "reason")
_ -> case BLC.break (== ':') evEvent of
("property-set", _) ->
PropertySet evSubject (fromMaybe "" $ getProp "name") (fromMaybe "" $ getProp "newvalue") (fromMaybe "" $ getProp "oldvalue")
("property-del", _) ->
PropertyDel evSubject (fromMaybe "" $ getProp "name") (fromMaybe "" $ getProp "oldvalue")
_ -> OtherEvent ev
where
getProp :: BL.ByteString -> Maybe BL.ByteString
getProp name = lookup name evProperties
readProp :: Read a => BL.ByteString -> Maybe a
readProp name = read . BLC.unpack <$> getProp name
intProp :: BL.ByteString -> Maybe Int
intProp = readProp
boolProp :: BL.ByteString -> Maybe Bool
boolProp = readProp
boolPropViaInt :: BL.ByteString -> Bool
boolPropViaInt = maybe False (/= 0) . intProp
parse _ = Nothing -- shouldn't happen -> report error?
printEvents :: Show a => Producer a (P.SafeT IO) () -> IO ()
printEvents prod = P.runSafeT $ runEffect $ prod >-> forever (await >>= liftIO . print)
data QubesVMState = VMRunning | VMHalted | UnknownState
deriving (Eq, Ord, Enum)
data QubesVMClass = AdminVM | AppVM | TemplateVM | DispVM | StandaloneVM | UnknownClass
deriving (Eq, Ord, Enum, Show, Read)
data QubesVMInfo = QubesVMInfo { vmState :: QubesVMState, vmClass :: QubesVMClass }
deriving (Eq, Ord, Show, Read)
instance Show QubesVMState where
show VMRunning = "Running"
show VMHalted = "Halted"
show UnknownState = "??"
instance Read QubesVMState where
readsPrec _ s = [(value, remainder)]
where
(word, remainder) = span isAlphaNum s
value = case word of
"Running" -> VMRunning
"Halted" -> VMHalted
_ -> UnknownState
qubesAdminCallLines :: BL.ByteString -> [BL.ByteString] -> IO [BL.ByteString]
qubesAdminCallLines serviceName args = qubesAdminCall serviceName args >>= parse
where
parse :: BLC.ByteString -> IO [BLC.ByteString]
parse reply = BLC.split '\n' reply
& filter (/= "")
& return
qubesListVMs :: IO (Map.Map BL.ByteString QubesVMInfo)
qubesListVMs = parse <$> qubesAdminCallLines "admin.vm.List" []
where
parse :: [BLC.ByteString] -> Map.Map BLC.ByteString QubesVMInfo
parse = Map.fromList . map parseLine
parseLine :: BLC.ByteString -> (BLC.ByteString, QubesVMInfo)
parseLine line =
(vmName, QubesVMInfo (readPropEmpty "state") (tryReadProp "class" & fromMaybe UnknownClass))
where
(vmName : propsRaw) = BLC.split ' ' line
props = map (fmap BLC.tail . BLC.break (== '=')) propsRaw
getProp :: BL.ByteString -> Maybe BL.ByteString
getProp name = lookup name props
readPropEmpty :: Read a => BL.ByteString -> a
readPropEmpty name = read . BLC.unpack . fromMaybe "" $ getProp name
tryReadProp :: Read a => BL.ByteString -> Maybe a
tryReadProp name = readMaybe . BLC.unpack =<< getProp name
qubesListVMsP :: forall m. (P.MonadSafe m, MonadFail m)
=> Producer (Map.Map BL.ByteString QubesVMInfo) m ()
qubesListVMsP = liftIO qubesListVMs >>= yield >> qubesEvents >-> P.mapM (const $ liftIO qubesListVMs)
data QubesPropertyInfo = QubesPropertyInfo { propIsDefault :: Bool, propType :: BL.ByteString, propValue :: BL.ByteString }
deriving (Eq, Ord, Show, Read)
qubesGetProperty :: BL.ByteString -> IO QubesPropertyInfo
qubesGetProperty name = parse <$> qubesAdminCall "admin.property.Get" [name]
where
parse reply = QubesPropertyInfo (isDefault == "default=True") (BL.drop 5 typeStr) value
where
splitOn ch = fmap BLC.tail . BLC.break (== ch)
(isDefault, (typeStr, value)) = splitOn ' ' reply & fmap (splitOn ' ')
qubesListProperties :: IO [(BL.ByteString, QubesPropertyInfo)]
qubesListProperties = qubesListLabelNames >>= mapM (toSndM qubesGetProperty)
where
toSndM :: Applicative m => (a -> m b) -> a -> m (a, b)
toSndM f x = sequenceA (x, f x)
qubesGetDefaultPool :: IO BL.ByteString
qubesGetDefaultPool = propValue <$> qubesGetProperty "default_pool"
qubesGetPoolInfo :: BL.ByteString -> IO [(BL.ByteString, BL.ByteString)]
qubesGetPoolInfo name = map parseLine <$> qubesAdminCallLines "admin.pool.Info" [name]
where
parseLine = fmap BLC.tail . BLC.break (== '=')
qubesUsageOfDefaultPool :: IO (Maybe Int, Maybe Int)
qubesUsageOfDefaultPool = qubesGetDefaultPool >>= qubesGetPoolInfo >>= extract
where
extract :: [(BLC.ByteString, BLC.ByteString)] -> IO (Maybe Int, Maybe Int)
extract props = return (tryReadProp "usage" props, tryReadProp "size" props)
tryReadProp :: Read a => BL.ByteString -> [(BL.ByteString, BL.ByteString)] -> Maybe a
tryReadProp name props = readMaybe . BLC.unpack =<< lookup name props
qubesListLabelNames :: IO [BL.ByteString]
qubesListLabelNames = qubesAdminCallLines "admin.label.List" []
qubesMonitorProperty :: forall m. MonadIO m
=> Producer QubesEvent m () -> BL.ByteString -> Producer QubesPropertyInfo m ()
qubesMonitorProperty events name = events >-> P.filter isRelevant >-> fetchValue
where
fetchValue :: Proxy () QubesEvent () QubesPropertyInfo m b
fetchValue = liftIO (qubesGetProperty name) >>= go
go :: QubesPropertyInfo -> Proxy () QubesEvent () QubesPropertyInfo m b
go x = do
yield x
ev <- await
case ev of
PropertySet {newValue} -> go $ x { propIsDefault = False, propValue = newValue }
PropertyDel {} -> fetchValue
_ -> go x
isRelevant PropertySet {changedProperty} = name == changedProperty
isRelevant PropertyDel {changedProperty} = name == changedProperty
isRelevant _ = False
{-# LANGUAGE DuplicateRecordFields #-} module QBar.Server (
{-# LANGUAGE ScopedTypeVariables #-} runBarServer,
runBarServerMirror,
module QBar.Server where ) where
import QBar.BlockOutput import QBar.BlockOutput
import QBar.Core
import QBar.ControlSocket import QBar.ControlSocket
import QBar.Core
import QBar.Host import QBar.Host
import QBar.Pango import QBar.Pango
import QBar.Prelude
import QBar.Theme import QBar.Theme
import QBar.Util import QBar.Utils
import Control.Monad (forM_)
import Control.Concurrent.Async (async, link) import Control.Concurrent.Async (async, link)
import Control.Concurrent.Event as Event import Control.Concurrent.Event as Event
import Control.Concurrent.MVar (MVar, newMVar, modifyMVar, modifyMVar_) import Control.Concurrent.MVar (MVar, newMVar, modifyMVar, modifyMVar_)
import Control.Exception (throw) import Control.Exception (throw)
import Control.Monad (forM_)
import Data.Aeson (encode, decode, ToJSON, toJSON, object, (.=)) import Data.Aeson (encode, decode, ToJSON, toJSON, object, (.=))
import Data.Aeson.Types qualified as AT
import Data.ByteString.Char8 qualified as BSSC8
import Data.ByteString.Lazy (hPut) import Data.ByteString.Lazy (hPut)
import qualified Data.ByteString.Char8 as BSSC8 import Data.ByteString.Lazy qualified as BS
import qualified Data.ByteString.Lazy as BS import Data.ByteString.Lazy.Char8 qualified as C8
import qualified Data.ByteString.Lazy.Char8 as C8
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import Pipes import Pipes
import Pipes.Concurrent (Input, spawn, latest, toOutput, fromInput) import Pipes.Concurrent (Input, spawn, latest, toOutput, fromInput)
import qualified Pipes.Prelude as PP import Pipes.Prelude qualified as PP
import System.IO (stdin, stdout, stderr, hFlush) import System.IO (stdin, stdout, stderr, hFlush)
data ServerMode = Host | Mirror
data ServerOutput = Sway | Headless
renderIndicators :: [Text] renderIndicators :: [Text]
renderIndicators = ["*"] <> cycle ["/", "-", "\\", "|"] renderIndicators = ["*"] <> cycle ["/", "-", "\\", "|"]
...@@ -43,9 +42,16 @@ instance ToJSON PangoBlock where ...@@ -43,9 +42,16 @@ instance ToJSON PangoBlock where
toJSON PangoBlock{pangoBlockFullText, pangoBlockShortText, pangoBlockName} = object $ toJSON PangoBlock{pangoBlockFullText, pangoBlockShortText, pangoBlockName} = object $
fullText' <> shortText' <> blockName' <> pango' fullText' <> shortText' <> blockName' <> pango'
where where
fullText' :: [AT.Pair]
fullText' = [ "full_text" .= pangoBlockFullText ] fullText' = [ "full_text" .= pangoBlockFullText ]
shortText' :: [AT.Pair]
shortText' = fromMaybe (\s -> ["short_text" .= s]) mempty pangoBlockShortText shortText' = fromMaybe (\s -> ["short_text" .= s]) mempty pangoBlockShortText
blockName' :: [AT.Pair]
blockName' = fromMaybe (\s -> ["name" .= s]) mempty pangoBlockName blockName' = fromMaybe (\s -> ["name" .= s]) mempty pangoBlockName
pango' :: [AT.Pair]
pango' = [ "markup" .= ("pango" :: T.Text) ] pango' = [ "markup" .= ("pango" :: T.Text) ]
...@@ -63,7 +69,9 @@ swayBarInput MainOptions{verbose} = swayBarInput' ...@@ -63,7 +69,9 @@ swayBarInput MainOptions{verbose} = swayBarInput'
liftIO $ BSSC8.hPutStrLn stderr line liftIO $ BSSC8.hPutStrLn stderr line
hFlush stderr hFlush stderr
let maybeBlockEvent = decode $ removeComma $ BS.fromStrict line let
maybeBlockEvent :: Maybe BlockEvent
maybeBlockEvent = decode $ removeComma $ BS.fromStrict line
forM_ maybeBlockEvent yield forM_ maybeBlockEvent yield
swayBarInput' swayBarInput'
...@@ -111,7 +119,7 @@ swayBarOutput options@MainOptions{indicator} = do ...@@ -111,7 +119,7 @@ swayBarOutput options@MainOptions{indicator} = do
hPut stderr "\n" hPut stderr "\n"
hFlush stderr hFlush stderr
encodeOutput :: [ThemedBlockOutput] -> BS.ByteString encodeOutput :: [ThemedBlockOutput] -> BS.ByteString
encodeOutput blocks = encode $ map renderPangoBlock $ blocks encodeOutput blocks = encode $ map renderPangoBlock blocks
renderPangoBlock :: ThemedBlockOutput -> PangoBlock renderPangoBlock :: ThemedBlockOutput -> PangoBlock
renderPangoBlock ThemedBlockOutput{_fullText, _shortText, _blockName} = PangoBlock { renderPangoBlock ThemedBlockOutput{_fullText, _shortText, _blockName} = PangoBlock {
pangoBlockFullText = renderPango _fullText, pangoBlockFullText = renderPango _fullText,
...@@ -121,7 +129,7 @@ swayBarOutput options@MainOptions{indicator} = do ...@@ -121,7 +129,7 @@ swayBarOutput options@MainOptions{indicator} = do
runBarServerMirror :: BarIO () -> MainOptions -> IO () runBarServerMirror :: BarIO () -> MainOptions -> IO ()
runBarServerMirror loadBlocks options = do runBarServerMirror loadBlocks options = do
-- TODO: apply theme from remote -- It would be nice to apply the theme from the remote, but because of the current split between Host and Server some redesign is required first.
(blockConsumer, eventProducer, _setTheme') <- themingBarServer options (blockConsumer, eventProducer, _setTheme') <- themingBarServer options
runBarHost (return (blockConsumer, eventProducer)) $ do runBarHost (return (blockConsumer, eventProducer)) $ do
addServerMirrorStream options addServerMirrorStream options
...@@ -148,6 +156,7 @@ barServerWithSocket options = do ...@@ -148,6 +156,7 @@ barServerWithSocket options = do
return (blockConsumer, eventProducer) return (blockConsumer, eventProducer)
where where
commandHandler :: (Theme -> IO ()) -> Command -> IO CommandResult commandHandler :: (Theme -> IO ()) -> Command -> IO CommandResult
commandHandler _ CheckServer = return Success
commandHandler setTheme' (SetTheme name) = commandHandler setTheme' (SetTheme name) =
case findTheme name of case findTheme name of
Left err -> return $ Error err Left err -> return $ Error err
...@@ -191,7 +200,7 @@ themingBarServer options = do ...@@ -191,7 +200,7 @@ themingBarServer options = do
(themedBlocks, isAnimated'') <- liftIO $ modifyMVar themedBlockProducerMVar (\(themedBlockProducer, isAnimated') -> do (themedBlocks, isAnimated'') <- liftIO $ modifyMVar themedBlockProducerMVar (\(themedBlockProducer, isAnimated') -> do
result <- next themedBlockProducer result <- next themedBlockProducer
case result of case result of
-- TODO: fix type safety on this somehow? -- Maybe type safety can be improved so this pattern match is no longer needed?
Left _ -> throw $ userError "Unexpected behavior: Themes and output cache mailbox should never return" Left _ -> throw $ userError "Unexpected behavior: Themes and output cache mailbox should never return"
Right (themedBlocks, nextThemedBlockProducer) -> Right (themedBlocks, nextThemedBlockProducer) ->
return ((nextThemedBlockProducer, isAnimated'), (themedBlocks, isAnimated')) return ((nextThemedBlockProducer, isAnimated'), (themedBlocks, isAnimated'))
......
module QBar.TagParser where module QBar.TagParser (
TagState,
parseTags,
parseTags',
parseTags'',
) where
import QBar.BlockOutput import QBar.BlockOutput
import QBar.Color import QBar.Color
import QBar.Prelude
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Data.Attoparsec.Text.Lazy as A import Data.Attoparsec.Text.Lazy as A
import Data.Functor (($>)) import Data.Functor (($>))
import Data.Maybe (catMaybes) import Data.Maybe (catMaybes)
import qualified Data.Text as TS import Data.Text qualified as TS
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
type TagState = (Bool, Importance) type TagState = (Bool, Importance)
...@@ -22,7 +28,11 @@ tagParser = parser (False, normalImportant) ...@@ -22,7 +28,11 @@ tagParser = parser (False, normalImportant)
singleElementParser = choice [textParser, activeTagParser, importanceTagParser, spanParser] singleElementParser = choice [textParser, activeTagParser, importanceTagParser, spanParser]
textParser :: Parser BlockText textParser :: Parser BlockText
textParser = mkText active importance . T.fromStrict <$> A.takeWhile1 (notInClass "<>") textParser = mkText active importance . replaceSymbols . T.fromStrict <$> A.takeWhile1 (notInClass "<>")
replaceSymbols :: Text -> Text
-- replaces &amp; last to prevent the '&' from being interpreted again
replaceSymbols = T.replace "&amp;" "&" . T.replace "&lt;" "<" . T.replace "&gt;" ">"
activeTagParser :: Parser BlockText activeTagParser :: Parser BlockText
activeTagParser = string "<active>" *> parser (True, importance) <* string "</active>" activeTagParser = string "<active>" *> parser (True, importance) <* string "</active>"
...@@ -50,12 +60,12 @@ tagParser = parser (False, normalImportant) ...@@ -50,12 +60,12 @@ tagParser = parser (False, normalImportant)
spanParser :: Parser BlockText spanParser :: Parser BlockText
spanParser = do spanParser = do
void $ string "<span" void $ string "<span"
(colors, backgrounds) <- unzip <$> (many' $ colorAttribute <|> backgroundAttribute) (colors, backgrounds) <- unzip <$> many' (colorAttribute <|> backgroundAttribute)
let color = listToMaybe . catMaybes $ colors let color = listToMaybe . catMaybes $ colors
let background = listToMaybe . catMaybes $ backgrounds let background = listToMaybe . catMaybes $ backgrounds
void $ char '>' void $ char '>'
content <- T.fromStrict <$> A.takeWhile1 (notInClass "<>") content <- T.fromStrict <$> A.takeWhile1 (notInClass "<>")
void $ string $ "</span>" void $ string "</span>"
return $ mkStyledText color background content return $ mkStyledText color background content
where where
colorAttributeParser :: Text -> Parser Color colorAttributeParser :: Text -> Parser Color
...@@ -65,11 +75,8 @@ tagParser = parser (False, normalImportant) ...@@ -65,11 +75,8 @@ tagParser = parser (False, normalImportant)
skipSpace skipSpace
void $ char '=' void $ char '='
skipSpace skipSpace
value <- ( char '\'' *> colorParser <* char '\'' <|>
char '\'' *> colorParser <* char '\'' char '"' *> colorParser <* char '"'
<|> char '"' *> colorParser <* char '"'
)
return value
colorAttribute :: Parser (Maybe Color, Maybe Color) colorAttribute :: Parser (Maybe Color, Maybe Color)
colorAttribute = do colorAttribute = do
...@@ -83,7 +90,7 @@ tagParser = parser (False, normalImportant) ...@@ -83,7 +90,7 @@ tagParser = parser (False, normalImportant)
parseTags :: T.Text -> Either String BlockText parseTags :: T.Text -> Either String BlockText
parseTags text = parseOnly (tagParser <* endOfInput) (T.toStrict text) parseTags = parseOnly (tagParser <* endOfInput)
parseTags' :: T.Text -> BlockOutput parseTags' :: T.Text -> BlockOutput
parseTags' = either (mkErrorOutput . T.pack) mkBlockOutput . parseTags parseTags' = either (mkErrorOutput . T.pack) mkBlockOutput . parseTags
......
{-# LANGUAGE DuplicateRecordFields #-} module QBar.Theme (
{-# LANGUAGE Rank2Types #-} Theme(..),
ThemedBlockOutput(..),
module QBar.Theme where ThemedBlockText(..),
ThemedBlockTextSegment(..),
defaultTheme,
findTheme,
isAnimated,
mkTheme,
mkThemedBlockOutput,
themeNames,
themes,
whiteThemedBlockOutput,
) where
import QBar.BlockOutput import QBar.BlockOutput
import QBar.Color import QBar.Color
import QBar.Prelude
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Lens ((^.)) import Control.Lens ((^.))
import Control.Monad.State.Lazy (State, evalState, get, put) import Control.Monad.State.Lazy (State, evalState, get, put)
import Data.Colour.RGBSpace import Data.Colour.RGBSpace
import Data.Colour.RGBSpace.HSV (hsv) import Data.Colour.RGBSpace.HSV (hsv)
import qualified Data.HashMap.Lazy as HM import Data.HashMap.Lazy qualified as HM
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import qualified Data.Text.Lazy as T import Data.Text.Lazy qualified as T
import Data.Time.Clock.POSIX (getPOSIXTime) import Data.Time.Clock.POSIX (getPOSIXTime)
import Pipes import Pipes
...@@ -39,7 +50,6 @@ data ThemedBlockTextSegment = ThemedBlockTextSegment { ...@@ -39,7 +50,6 @@ data ThemedBlockTextSegment = ThemedBlockTextSegment {
} }
deriving (Eq, Show) deriving (Eq, Show)
data Theme = StaticTheme StaticTheme | AnimatedTheme AnimatedTheme data Theme = StaticTheme StaticTheme | AnimatedTheme AnimatedTheme
type StaticTheme = [BlockOutput] -> [ThemedBlockOutput] type StaticTheme = [BlockOutput] -> [ThemedBlockOutput]
...@@ -67,6 +77,7 @@ themes = HM.fromList themesList ...@@ -67,6 +77,7 @@ themes = HM.fromList themesList
findTheme :: Text -> Either Text Theme findTheme :: Text -> Either Text Theme
findTheme themeName = maybe invalidThemeName Right $ HM.lookup themeName themes findTheme themeName = maybe invalidThemeName Right $ HM.lookup themeName themes
where where
invalidThemeName :: Either Text Theme
invalidThemeName = Left $ "Invalid theme: " <> themeName invalidThemeName = Left $ "Invalid theme: " <> themeName
mkTheme :: SimplifiedTheme -> Theme mkTheme :: SimplifiedTheme -> Theme
...@@ -111,7 +122,6 @@ mkThemedSegment (color, backgroundColor) text = ThemedBlockTextSegment{themedSeg ...@@ -111,7 +122,6 @@ mkThemedSegment (color, backgroundColor) text = ThemedBlockTextSegment{themedSeg
whiteThemedBlockOutput :: Text -> ThemedBlockOutput whiteThemedBlockOutput :: Text -> ThemedBlockOutput
whiteThemedBlockOutput = mkThemedBlockOutput (ColorRGB (RGB 1 1 1), Nothing) whiteThemedBlockOutput = mkThemedBlockOutput (ColorRGB (RGB 1 1 1), Nothing)
invalidColor :: Color invalidColor :: Color
invalidColor = ColorRGBA (RGB (0x96 / 255) (0x98 / 255) (0x96 / 255)) (0x77 / 255) invalidColor = ColorRGBA (RGB (0x96 / 255) (0x98 / 255) (0x96 / 255)) (0x77 / 255)
...@@ -131,14 +141,13 @@ defaultTheme = mkTheme defaultTheme' ...@@ -131,14 +141,13 @@ defaultTheme = mkTheme defaultTheme'
defaultTheme' True (NormalImportant _) = (ColorRGB (RGB 1 1 1), Nothing) defaultTheme' True (NormalImportant _) = (ColorRGB (RGB 1 1 1), Nothing)
defaultTheme' False (NormalImportant _) = (ColorRGB (RGB (0x96 / 255) (0x98 / 255) (0x96 / 255)), Nothing) defaultTheme' False (NormalImportant _) = (ColorRGB (RGB (0x96 / 255) (0x98 / 255) (0x96 / 255)), Nothing)
rainbowTheme :: Theme rainbowTheme :: Theme
rainbowTheme = AnimatedTheme rainbowThemePipe rainbowTheme = AnimatedTheme rainbowThemePipe
where where
rainbowThemePipe :: AnimatedTheme rainbowThemePipe :: AnimatedTheme
rainbowThemePipe = do rainbowThemePipe = do
time <- liftIO $ fromRational . toRational <$> getPOSIXTime time <- liftIO $ fromRational . toRational <$> getPOSIXTime
yield =<< rainbowThemePipe' time <$> await yield . rainbowThemePipe' time =<< await
rainbowThemePipe rainbowThemePipe
rainbowThemePipe' :: Double -> StaticTheme rainbowThemePipe' :: Double -> StaticTheme
rainbowThemePipe' time blocks = reverse $ evalState (mapM rainbowBlock $ reverse blocks) 0 rainbowThemePipe' time blocks = reverse $ evalState (mapM rainbowBlock $ reverse blocks) 0
...@@ -146,18 +155,18 @@ rainbowTheme = AnimatedTheme rainbowThemePipe ...@@ -146,18 +155,18 @@ rainbowTheme = AnimatedTheme rainbowThemePipe
rainbowBlock :: BlockOutput -> State Integer ThemedBlockOutput rainbowBlock :: BlockOutput -> State Integer ThemedBlockOutput
rainbowBlock block@BlockOutput{_blockName} = do rainbowBlock block@BlockOutput{_blockName} = do
let text = rawText $ block ^. fullText let text = rawText $ block ^. fullText
let chars = T.unpack . T.reverse $ text let chars = reverse . splitToChars $ text
coloredChars <- mapM rainbowChar chars coloredChars <- mapM rainbowChar chars
let rainbowText = reverse $ coloredChars let rainbowText = reverse coloredChars
return $ ThemedBlockOutput { return $ ThemedBlockOutput {
_blockName, _blockName,
_fullText = ThemedBlockText rainbowText, _fullText = ThemedBlockText rainbowText,
_shortText = Nothing _shortText = Nothing
} }
rainbowChar :: Char -> State Integer ThemedBlockTextSegment rainbowChar :: T.Text -> State Integer ThemedBlockTextSegment
rainbowChar char = do rainbowChar char = do
color <- nextRainbowColor color <- nextRainbowColor
return $ mkThemedSegment (color, Nothing) $ T.singleton char return $ mkThemedSegment (color, Nothing) $ char
nextRainbowColor :: State Integer Color nextRainbowColor :: State Integer Color
-- nextRainbowColor = state $ \index -> (rainbowColor (fromInteger index), index + 1) -- nextRainbowColor = state $ \index -> (rainbowColor (fromInteger index), index + 1)
nextRainbowColor = do nextRainbowColor = do
...@@ -169,3 +178,13 @@ rainbowTheme = AnimatedTheme rainbowThemePipe ...@@ -169,3 +178,13 @@ rainbowTheme = AnimatedTheme rainbowThemePipe
let hue' = position * 3 let hue' = position * 3
color = hsv hue' 0.8 1.0 color = hsv hue' 0.8 1.0
in ColorRGB color in ColorRGB color
splitToChars :: T.Text -> [T.Text]
splitToChars = splitStringToChars . T.unpack
splitStringToChars :: String -> [T.Text]
splitStringToChars [] = []
splitStringToChars ('&':xs) = splitStringToCharsAmp "&" xs
splitStringToChars (x:xs) = T.singleton x : splitStringToChars xs
splitStringToCharsAmp :: String -> String -> [T.Text]
splitStringToCharsAmp _ [] = []
splitStringToCharsAmp acc (';':xs) = T.pack (acc <> ";") : splitStringToChars xs
splitStringToCharsAmp acc (x:xs) = splitStringToCharsAmp (acc <> [x]) xs