From 07acb8985844bf1df34eeab13abc0f6ca279d93f Mon Sep 17 00:00:00 2001 From: Alexander Foremny Date: Sun, 21 Jul 2024 17:41:45 +0200 Subject: fix: depend on sensors --- app/Sensor.hs | 855 +++++++++++++++++++++++++++------------------------------- 1 file changed, 390 insertions(+), 465 deletions(-) (limited to 'app/Sensor.hs') diff --git a/app/Sensor.hs b/app/Sensor.hs index 7d9324b..55d2554 100644 --- a/app/Sensor.hs +++ b/app/Sensor.hs @@ -1,20 +1,21 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE Arrows #-} {-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE PartialTypeSignatures #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE StrictData #-} +{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} {-# OPTIONS_GHC -fno-warn-type-defaults #-} {-# OPTIONS_GHC -fno-warn-unused-top-binds #-} module Sensor - ( SensorM, - runSensorM, - poll, - Sensor (..), - cpu, + ( cpu, io, net, mem, @@ -28,18 +29,21 @@ module Sensor ) where +import Control.Arrow +import Control.DeepSeq import Control.Monad import Control.Monad.Reader import Data.Char import Data.Dynamic import Data.Functor.WithIndex import Data.List -import Data.Map qualified as M import Data.Maybe +import Data.Sensor qualified as S import Data.Set qualified as S import Data.Time.Clock import Data.Time.Format import Data.Time.LocalTime +import GHC.Generics (Generic) import Graphics.X11 qualified as X import Graphics.X11.Xlib.Extras qualified as X import Pretty qualified as P @@ -47,494 +51,415 @@ import Process.Shell (sh) import Safe import System.Directory import System.FilePath -import System.IO.Error -import System.Linux.Inotify qualified as I +import System.IO (hGetContents) import System.Posix.StatVFS +import System.Posix.Types (Fd (..)) import Text.Printf +import UnliftIO import UnliftIO.Concurrent -import UnliftIO.Exception -import UnliftIO.Memoize -import UnliftIO.STM import Witherable (ifilter) +import Prelude hiding (readFile) -type SensorM a = - ReaderT (MVar (M.Map String (Memoized Dynamic))) IO a - -runSensorM :: SensorM a -> IO a -runSensorM m = do - g <- newMVar M.empty - runReaderT m g - -poll :: (Typeable a) => Sensor a -> SensorM (TVar a) -poll (Sensor id vM') = do - gM <- ask - g <- takeMVar gM - vM <- case M.lookup id g of - Just vM -> do - putMVar gM g - pure (fromJust . fromDynamic <$> vM) - Nothing -> do - -- liftIO (printf "[%s] start\n" id) - vM <- vM' - putMVar gM (M.insert id (toDyn <$> vM) g) - pure vM - runMemoized vM - -data Sensor a = Sensor String (SensorM (Memoized (TVar a))) - -data CpuStat = CpuStat - { total :: Int, - idle :: Int +data CpuStatData = CpuStatData + { used :: Int, + total :: Int } - deriving (Show, Eq) - -cpuStat :: Sensor CpuStat -cpuStat = pollFile "cpuStat" aggregate "/proc/stat" (5 * 10 ^ 5) - where - aggregate fp = liftIO do - stat <- readFile fp - case filter ("cpu " `isPrefixOf`) (lines stat) of - [] -> error "/proc/stat: no cpu line" - (cpu : _) -> - case map read (drop 1 (words cpu)) of - xs@(_ : _ : _ : idle : _) -> - pure $ CpuStat (sum xs) idle - _ -> error "/proc/stat: unexpected cpu line" - -cpu' :: Sensor [CpuStat] -cpu' = histogram "cpu'" [] (\b a -> take 7 (a : b)) cpuStat - -data Cpu = Cpu {unCpu :: [Float]} - -instance P.Pretty Cpu where - pretty = P.diagram 3 . unCpu - -cpu :: Sensor Cpu -cpu = transform "cpu" f cpu' - where - f xs = do - let xs' = - zipWith - ( \next prev -> - let total = next.total - prev.total - idle = next.idle - prev.idle - in fromIntegral (total - idle) / fromIntegral total - ) - (fromMaybe [] (initMay xs)) - (fromMaybe [] (tailMay xs)) - pure (Cpu xs') - -data IoStat = IoStat {unIoStat :: Int} deriving (Show, Eq) - -ioStat :: Sensor IoStat -ioStat = poll' "ioStat" aggregate (5 * 10 ^ 5) - where - aggregate = liftIO do - fmap (IoStat . sum) . mapM (aggregate' . ("/sys/block" )) - =<< listDirectory "/sys/block" - aggregate' fp = do - stat <- readFile (fp "stat") - case words stat of - [_, _, read', _, _, _, write', _, _, _, _, _, _, _, _, _, _] -> - pure (read read' + read write') - _ -> error (printf "%s/stat: malformed" fp) - -io' :: Sensor [IoStat] -io' = histogram "io'" [] (\b a -> take 7 (a : b)) ioStat - -data Io = Io {unIo :: [Float]} deriving (Show, Eq) - -instance P.Pretty Io where - pretty = P.diagram 3 . unIo - -io :: Sensor Io -io = transform "io" f io' - where - f (map unIoStat -> xs) = do - let xs' = - zipWith - (-) - (fromMaybe [] (initMay xs)) - (fromMaybe [] (tailMay xs)) - x' = max 1 (fromMaybe 0 (maximumMay xs')) - pure . Io $ - map (\x -> fromIntegral x / fromIntegral x') xs' - -data NetStat = NetStat {unNetStat :: Int} deriving (Show, Eq) - -netStat :: Sensor NetStat -netStat = poll' "netStat" aggregate (5 * 10 ^ 5) - where - aggregate = liftIO do - is <- listDirectory "/sys/class/net" - NetStat . sum <$> mapM (aggregate' . ("/sys/class/net" )) is - aggregate' fp = do - (+) - <$> (read <$> readFile (fp "statistics/rx_bytes")) - <*> (read <$> readFile (fp "statistics/tx_bytes")) - -net' :: Sensor [NetStat] -net' = histogram "net'" [] (\b a -> take 7 (a : b)) netStat - -data Net = Net {unNet :: [Float]} deriving (Show, Eq) - -instance P.Pretty Net where - pretty = P.diagram 3 . unNet - -net :: Sensor Net -net = transform "net" f net' - where - f (map unNetStat -> xs) = do - let xs' = - zipWith - (-) - (fromMaybe [] (initMay xs)) - (fromMaybe [] (tailMay xs)) - x' = max 1 (fromMaybe 0 (maximumMay xs')) - pure $ Net $ map (\x -> fromIntegral x / fromIntegral x') xs' - -data MemStat = MemStat {unMemStat :: Float} deriving (Show, Eq) - -memStat :: Sensor MemStat -memStat = pollFile "memStat" aggregate "/proc/meminfo" (5 * 10 ^ 5) + deriving (Eq, Show, Generic, NFData) + +data CpuStat = CpuStat deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m CpuStat CpuStatData where + aggregate _ = forever do S.yield =<< parse <* sleep + where + parse = readFile "/proc/stat" $ \stat -> do + case filter (["cpu"] `isPrefixOf`) (words <$> lines stat) of + [] -> error "/proc/stat: no cpu line" + ((drop 1 . fmap read -> (xs@(_ : _ : _ : idle : _))) : _) -> do + let total = sum xs + used = total - idle + pure (CpuStatData used total) + _ -> error "/proc/stat: unexpected cpu line" + +readFile :: (MonadUnliftIO m, NFData a) => FilePath -> (String -> m a) -> m a +readFile fp f = + withFile fp ReadMode $ \h -> + evaluate =<< f =<< (liftIO (hGetContents h)) + +sleep :: (MonadIO m) => S.AggregateT s m () +sleep = threadDelay (5 * 10 ^ 5) + +cpuStat :: (S.MonadSensor m) => S.Sensor m () CpuStatData +cpuStat = S.sensor CpuStat + +cpu :: (S.MonadSensor m) => S.Sensor m () (P.Diagram [Float]) +cpu = step <$$> diagram 3 cpuStat where - aggregate fp = liftIO do - meminfo <- readFile fp - case foldl - ( \(total, avail) xs -> - case xs of - ["MemTotal:", v, "kB"] -> (Just (read v), avail) - ["MemAvailable:", v, "kB"] -> (total, Just (read v)) - _ -> (total, avail) + step xs = + zipWith + ( \next prev -> + let CpuStatData {used, total} = + CpuStatData (next.used - prev.used) (next.total - prev.total) + in if total == 0 then 0 else fromIntegral used / fromIntegral total ) - (Nothing, Nothing) - (map words (lines meminfo)) of - (Just total, Just avail) -> pure $ MemStat (1 - avail / total) - (Nothing, _) -> error (printf "%s: MemTotal missing" fp) - (_, Nothing) -> error (printf "%s: MemAvail missing" fp) - -mem' :: Sensor [MemStat] -mem' = histogram "mem'" [] (\b a -> take 7 (a : b)) memStat - -data Mem = Mem {unMem :: [Float]} deriving (Show, Eq) - -instance P.Pretty Mem where - pretty = P.diagram 3 . unMem - -mem :: Sensor Mem -mem = transform "mem" (\(map unMemStat -> xs) -> pure (Mem xs)) mem' - -data BatStat = BatStat {unBatStat :: Float} deriving (Show, Eq) - -batStat :: Sensor BatStat -batStat = poll' "batStat" aggregate (5 * 10 ^ 5) - where - aggregate = liftIO do - fmap (BatStat . product) . mapM (aggregate' . ("/sys/class/power_supply" )) - =<< listDirectory "/sys/class/power_supply" - aggregate' fp = - choice 1 $ - [ (/) - <$> (read <$> readFile (fp "charge_now")) - <*> (read <$> readFile (fp "charge_full")), - (/) - <$> (read <$> readFile (fp "energy_now")) - <*> (read <$> readFile (fp "energy_full")) - ] + (fromMaybe [] (tailMay xs)) + (fromMaybe [] (initMay xs)) + +diagram :: (Monad m) => Int -> S.Sensor m () a -> S.Sensor m () (P.Diagram [a]) +diagram n sf = S.feedbackS [] $ proc ((), xs) -> do + x <- sf -< () + returnA -< (P.diagram n (reverse (x : xs)), take (2 * (n - 1) + 1) (x : xs)) + +data IoStat = IoStat deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m IoStat Int where + aggregate _ = forever do S.yield =<< parse <* sleep + where + parse = liftIO do + fmap sum . mapM (parse1 . ("/sys/block" )) + =<< listDirectory "/sys/block" + parse1 :: FilePath -> IO Int + parse1 fp = do + readFile (fp "stat") $ \stat -> do + case words stat of + [_, _, read', _, _, _, write, _, _, _, _, _, _, _, _, _, _] -> + pure (read read' + read write) + _ -> error (printf "%s/stat: malformed" fp) + +ioStat :: (S.MonadSensor m) => S.Sensor m () Int +ioStat = S.sensor IoStat + +io :: (S.MonadSensor m) => S.Sensor m () (P.Diagram [Float]) +io = diagram 3 (maxS (rate ioStat)) + +maxS :: (Monad m, Num a, Ord a, Integral a) => S.Sensor m () a -> S.Sensor m () Float +maxS sf = S.feedbackS 0 $ proc ((), xMax) -> do + x <- fi <$> sf -< () + let xMax' = max xMax x + returnA -< (if xMax <= 0 then 0 else x / xMax', xMax') + +rate :: (Monad m, Num a) => S.Sensor m () a -> S.Sensor m () a +rate sf = S.feedbackS 0 $ proc ((), x') -> do + x <- sf -< () + returnA -< (x - x', x) + +data NetStat = NetStat deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m NetStat Int where + aggregate _ = forever do S.yield =<< parse <* sleep + where + parse = liftIO do + is <- listDirectory "/sys/class/net" + sum <$> mapM (parse1 . ("/sys/class/net" )) is + parse1 :: FilePath -> IO Int + parse1 fp = do + (+) + <$> (readFile (fp "statistics/rx_bytes") (pure . read)) + <*> (readFile (fp "statistics/tx_bytes") (pure . read)) + +netStat :: (S.MonadSensor m) => S.Sensor m () Int +netStat = S.sensor NetStat + +net :: (S.MonadSensor m) => S.Sensor m () (P.Diagram [Float]) +net = diagram 3 (maxS (rate netStat)) + +data MemStat = MemStat deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m MemStat Float where + aggregate _ = forever do S.yield =<< parse <* sleep + where + parse = readFile "/proc/meminfo" $ \meminfo -> do + case foldl + ( \(total, avail) xs -> + case xs of + ["MemTotal:", v, "kB"] -> (Just (read v), avail) + ["MemAvailable:", v, "kB"] -> (total, Just (read v)) + _ -> (total, avail) + ) + (Nothing, Nothing) + (map words (lines meminfo)) of + (Just total, Just avail) -> pure (1 - avail / total) + (Nothing, _) -> error "/proc/meminfo: MemTotal missing" + (_, Nothing) -> error "/proc/meminfo: MemAvail missing" + +memStat :: (S.MonadSensor m) => S.Sensor m () Float +memStat = S.sensor MemStat + +mem :: (S.MonadSensor m) => S.Sensor m () (P.Diagram [Float]) +mem = diagram 3 memStat + +data BatStat = BatStat deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m BatStat Float where + aggregate _ = forever do S.yield =<< parse <* sleep + where + parse = liftIO do + fmap product . mapM (parse1 . ("/sys/class/power_supply" )) + =<< listDirectory "/sys/class/power_supply" + parse1 :: FilePath -> IO Float + parse1 fp = + choice 1 $ + [ (/) + <$> (readFile (fp "charge_now") (pure . read)) + <*> (readFile (fp "charge_full") (pure . read)), + (/) + <$> (readFile (fp "energy_now") (pure . read)) + <*> (readFile (fp "energy_full") (pure . read)) + ] + +bat :: (S.MonadSensor m) => S.Sensor m () Float +bat = S.sensor BatStat choice :: a -> [IO a] -> IO a choice def [] = pure def choice def (x : xs) = x `catch` (\(_ :: SomeException) -> choice def xs) -bat' :: Sensor [BatStat] -bat' = histogram "bat'" [] (\b a -> take 7 (a : b)) batStat - -data Bat = Bat {unBat :: [Float]} deriving (Show, Eq) - -instance P.Pretty Bat where - pretty = P.diagram 1 . unBat - -bat :: Sensor Bat -bat = transform "bat" (\(map unBatStat -> xs) -> pure (Bat xs)) bat' - -data Snd = Snd {unSnd :: Float} deriving (Show, Eq) +data Snd = Snd deriving (Show) -instance P.Pretty Snd where - pretty = P.diagram 1 . (: []) . unSnd +instance (S.MonadSensor m) => S.Aggregate m Snd Float where + aggregate _ = forever do S.yield =<< parse <* sleep + where + parse = liftIO do (/ 153) . read <$> [sh|pamixer --get-volume|] -snd :: Sensor Snd -snd = Sensor "snd" $ memoizeMVar do - stateT <- newTVarIO (Snd 1) - void $ forkIO $ forever do - x <- liftIO aggregate - atomically do writeTVar stateT (Snd x) - threadDelay (2 * 10 ^ 6) - pure stateT - where - aggregate = do - (/ 153) . read <$> [sh|pamixer --get-volume|] - -data Disk = Disk {unDisk :: [Float]} deriving (Show, Eq) - -instance P.Pretty Disk where - pretty = P.diagram 1 . unDisk - -disk :: Sensor Disk -disk = Sensor "disk" $ memoizeMVar do - stateT <- newTVarIO (Disk []) - void $ forkIO $ forever do - x <- liftIO aggregate - atomically do - readTVar stateT >>= \(Disk xs) -> - writeTVar stateT (Disk (x : xs)) - threadDelay (2 * 10 ^ 6) - pure stateT - where - aggregate = do - stat <- statVFS "/" - pure $ - (fromIntegral stat.statVFS_bfree) - / fromIntegral (stat.statVFS_bfree + stat.statVFS_bavail) - -currentTime :: Sensor UTCTime -currentTime = Sensor "currentTime" $ memoizeMVar do - stateT <- newTVarIO =<< liftIO getCurrentTime - void $ forkIO $ forever do - x <- liftIO getCurrentTime - atomically . writeTVar stateT =<< liftIO getCurrentTime - threadDelay (((10 ^ 12) - (fromEnum (utctDayTime x) `mod` (10 ^ 12))) `div` (10 ^ 6)) - pure stateT +snd :: (S.MonadSensor m) => S.Sensor m () Float +snd = S.sensor Snd -data Date = Date {unDate :: String} deriving (Show, Eq) - -instance P.Pretty Date where - pretty = P.color P.White . P.pretty . unDate - -date :: Sensor Date -date = transform "date" f currentTime - where - f x = do - z <- liftIO getCurrentTimeZone - pure . Date . formatTime defaultTimeLocale "%b %e" $ - utcToLocalTime z x +data DiskStat = DiskStat deriving (Show) -data Time = Time {unTime :: String} deriving (Show, Eq) +instance (S.MonadSensor m) => S.Aggregate m DiskStat Float where + aggregate _ = forever do S.yield =<< parse <* sleep + where + parse = liftIO do + stat <- statVFS "/" + pure $ + (fi stat.statVFS_bfree) + / fi (stat.statVFS_bfree + stat.statVFS_bavail) -instance P.Pretty Time where - pretty = P.color P.White . P.pretty . unTime +diskStat :: (S.MonadSensor m) => S.Sensor m () Float +diskStat = S.sensor DiskStat -time :: Sensor Time -time = transform "time" f currentTime - where - f x = do - z <- liftIO getCurrentTimeZone - pure . Time . formatTime defaultTimeLocale "%R" $ - utcToLocalTime z x - -data WmName = WmName {unWmName :: String} deriving (Show, Eq) - -instance P.Pretty WmName where - pretty = P.color P.White . P.pretty . unWmName - -wmName' :: Sensor WmName -wmName' = watchXPropertyChanges "wmName'" as aggregate - where - as = ["_NET_ACTIVE_WINDOW"] - aggregate dpy = liftIO do - let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) - netActiveWindow <- X.internAtom dpy "_NET_ACTIVE_WINDOW" False - X.getWindowProperty32 dpy netActiveWindow root >>= \case - Just (win : _) -> do - netWmName <- X.internAtom dpy "_NET_WM_NAME" False - fmap (WmName . strip . head) . X.wcTextPropertyToTextList dpy - =<< do - let loop 0 = do - error "failed to read _NET_WM_NAME" - loop n = do - catch - (X.getTextProperty dpy (fromIntegral win) netWmName) - ( \(e :: IOError) -> do - if isUserError e - then threadDelay 1000 >> loop (n - 1) - else throwIO e - ) - loop 16 - _ -> pure (WmName "") - strip = reverse . dropWhile isSpace . reverse . dropWhile isSpace +disk :: (S.MonadSensor m) => S.Sensor m () (P.Diagram [Float]) +disk = diagram 1 diskStat -wmName :: Sensor String -wmName = transform "wmName" (\(WmName x) -> pure x) wmName' +data CurrentTime = CurrentTime deriving (Show) -data WmWorkspaces = WmWorkspaces {unWmWorkspaces :: [Workspace]} - deriving (Show, Eq) +instance (S.MonadSensor m) => S.Aggregate m CurrentTime UTCTime where + aggregate _ = forever do + x <- liftIO getCurrentTime + S.yield =<< liftIO getCurrentTime + threadDelay + (((10 ^ 12) - (fromEnum (utctDayTime x) `mod` (10 ^ 12))) `div` (10 ^ 6)) + +data CurrentTimeZone = CurrentTimeZone deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m CurrentTimeZone TimeZone where + aggregate _ = forever do + S.yield =<< liftIO getCurrentTimeZone + sleep + +currentTimeZone :: (S.MonadSensor m) => S.Sensor m () TimeZone +currentTimeZone = S.sensor CurrentTimeZone + +currentTime :: (S.MonadSensor m) => S.Sensor m () UTCTime +currentTime = S.sensor CurrentTime + +date :: (S.MonadSensor m) => S.Sensor m () String +date = + ((formatTime defaultTimeLocale "%b %e" .) . utcToLocalTime) + <$> currentTimeZone + <*> currentTime + +time :: (S.MonadSensor m) => S.Sensor m () String +time = + ((formatTime defaultTimeLocale "%R" .) . utcToLocalTime) + <$> currentTimeZone + <*> currentTime + +wmName :: (S.MonadSensor m) => S.Sensor m () String +wmName = activeWindow >>= maybe (pure "") wmNameOf + +data ActiveWindow = ActiveWindow deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m ActiveWindow (Maybe X.Window) where + aggregate _ = do + bracket + ( liftIO do + dpy <- X.openDisplay "" + let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) + X.selectInput dpy root X.propertyChangeMask + X.sync dpy False + pure (dpy, root) + ) + (\(dpy, _) -> liftIO (X.closeDisplay dpy)) + ( \(dpy, root) -> do + netActiveWindow <- liftIO (X.internAtom dpy "_NET_ACTIVE_WINDOW" False) + let waitForEvent = do + liftIO (X.pending dpy) >>= \case + 0 -> do + liftIO do threadWaitRead (Fd (X.connectionNumber dpy)) + waitForEvent + _ -> do + e <- + liftIO + ( X.allocaXEvent $ \ev -> do + X.nextEvent dpy ev + X.getEvent ev + ) + when (X.ev_atom e /= netActiveWindow) waitForEvent + + readActiveWindow = do + liftIO (X.getWindowProperty32 dpy netActiveWindow root) >>= \case + Just (0 : _) -> S.yield Nothing + Just (win : _) -> S.yield (Just (fi win)) + _ -> S.yield Nothing + forever do + readActiveWindow + waitForEvent + ) + +activeWindow :: (S.MonadSensor m) => S.Sensor m () (Maybe X.Window) +activeWindow = S.sensor ActiveWindow + +data WmNameOf = WmNameOf X.Window deriving (Show) + +instance (S.MonadSensor m) => S.Aggregate m WmNameOf String where + aggregate (WmNameOf win) = do + bracket + ( liftIO do + dpy <- X.openDisplay "" + X.selectInput dpy win X.propertyChangeMask + X.sync dpy False + pure dpy + ) + (liftIO . X.closeDisplay) + ( \dpy -> do + netWmName <- liftIO (X.internAtom dpy "_NET_WM_NAME" False) + let waitForEvent = do + liftIO (X.pending dpy) >>= \case + 0 -> do + liftIO do threadWaitRead (Fd (X.connectionNumber dpy)) + waitForEvent + _ -> do + e <- + liftIO + ( X.allocaXEvent $ \ev -> do + X.nextEvent dpy ev + X.getEvent ev + ) + when (X.ev_atom e /= netWmName) waitForEvent + + readWmName = do + S.yield + =<< fmap (strip . head) . liftIO . X.wcTextPropertyToTextList dpy + =<< liftIO (X.getTextProperty dpy win netWmName) + forever do + readWmName + waitForEvent + ) + +wmNameOf :: (S.MonadSensor m) => X.Window -> S.Sensor m () String +wmNameOf = S.sensor . WmNameOf + +data WmWorkspaces = WmWorkspaces deriving (Show) data Workspace = Active String | Inactive String - deriving (Show, Eq) + deriving (Eq, Typeable, Show) + +instance (S.MonadSensor m) => S.Aggregate m WmWorkspaces [Workspace] where + aggregate WmWorkspaces = do + bracket + ( liftIO do + dpy <- X.openDisplay "" + let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) + X.selectInput dpy root X.propertyChangeMask + X.sync dpy False + pure (dpy, root) + ) + (\(dpy, _) -> liftIO (X.closeDisplay dpy)) + ( \(dpy, root) -> do + netClientList <- liftIO $ X.internAtom dpy "_NET_CLIENT_LIST" False + netCurrentDesktop <- liftIO $ X.internAtom dpy "_NET_CURRENT_DESKTOP" False + netDesktopNames <- liftIO $ X.internAtom dpy "_NET_DESKTOP_NAMES" False + netWmDesktop <- liftIO $ X.internAtom dpy "_NET_WM_DESKTOP" False + let waitForEvent = do + liftIO (X.pending dpy) >>= \case + 0 -> do + liftIO (threadWaitRead (Fd (X.connectionNumber dpy))) + waitForEvent + _ -> do + e <- + liftIO + ( X.allocaXEvent $ \ev -> do + X.nextEvent dpy ev + X.getEvent ev + ) + when + ( ((not .) . elem) + (X.ev_atom e) + [ netClientList, + netCurrentDesktop, + netDesktopNames, + netWmDesktop + ] + ) + waitForEvent + + readWmWorkspaces = do + S.yield + =<< liftIO do + currentDesktop <- + fmap (fi . head) + <$> X.getWindowProperty32 dpy netCurrentDesktop root + occupiedDesktops <- + fmap (S.unions . catMaybes) + . mapM + ( \win -> do + fmap (S.singleton . fi . head) + <$> X.getWindowProperty32 dpy netWmDesktop win + ) + . map fi + . fromMaybe [] + =<< X.getWindowProperty32 dpy netClientList root + fmap + ( ifilter + ( \i _ -> + Just i == currentDesktop + || i `S.member` occupiedDesktops + ) + . imap + ( \i -> + if Just i == currentDesktop + then Active + else Inactive + ) + ) + . X.wcTextPropertyToTextList dpy + =<< X.getTextProperty dpy root netDesktopNames + forever do + readWmWorkspaces + waitForEvent + ) + +wmWorkspaces :: (S.MonadSensor m) => S.Sensor m () [Workspace] +wmWorkspaces = S.sensor WmWorkspaces -instance P.Pretty WmWorkspaces where +instance P.Pretty [Workspace] where pretty = - P.Col . intersperse (P.pretty " ") . map P.pretty . unWmWorkspaces + P.Col . intersperse (P.pretty " ") . map P.pretty instance P.Pretty Workspace where pretty (Active s) = P.color P.White (P.pretty s) pretty (Inactive s) = P.colorDull P.White (P.pretty s) -wmWorkspaces :: Sensor WmWorkspaces -wmWorkspaces = watchXPropertyChanges "wmWorkspaces" as aggregate - where - as = - [ "_NET_CLIENT_LIST", - "_NET_CURRENT_DESKTOP", - "_NET_DESKTOP_NAMES", - "_NET_WM_DESKTOP" - ] - aggregate dpy = liftIO do - let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) - netClientList <- liftIO $ X.internAtom dpy "_NET_CLIENT_LIST" False - netCurrentDesktop <- liftIO $ X.internAtom dpy "_NET_CURRENT_DESKTOP" False - netDesktopNames <- liftIO $ X.internAtom dpy "_NET_DESKTOP_NAMES" False - netWmDesktop <- liftIO $ X.internAtom dpy "_NET_WM_DESKTOP" False - currentDesktop <- fmap (fromIntegral . head) <$> X.getWindowProperty32 dpy netCurrentDesktop root - occupiedDesktops <- - fmap (S.unions . catMaybes) - . mapM - ( \win -> do - fmap (S.singleton . fromIntegral . head) - <$> X.getWindowProperty32 dpy netWmDesktop win - ) - . map fromIntegral - . fromMaybe [] - =<< X.getWindowProperty32 dpy netClientList root - fmap - ( WmWorkspaces - . ifilter (\i _ -> Just i == currentDesktop || i `S.member` occupiedDesktops) - . imap - ( \i -> - if Just i == currentDesktop - then Active - else Inactive - ) - ) - . X.wcTextPropertyToTextList dpy - =<< X.getTextProperty dpy root netDesktopNames - -watchFile :: String -> (FilePath -> SensorM a) -> FilePath -> Sensor a -watchFile id aggregate fp = Sensor id $ memoizeMVar do - i <- liftIO do - i <- I.init - void (I.addWatch i fp I.in_CLOSE) - pure i - stateT <- newTVarIO =<< aggregate fp - void $ forkIO $ forever do - void (liftIO (I.getEvent i)) - -- liftIO (printf "[%s] aggregate\n" id) - x <- aggregate fp - atomically (writeTVar stateT x) - pure stateT - -pollFile :: String -> (FilePath -> SensorM a) -> FilePath -> Int -> Sensor a -pollFile id aggregate fp delay = Sensor id $ memoizeMVar do - stateT <- newTVarIO =<< aggregate fp - void $ forkIO $ forever do - -- liftIO (printf "[%s] aggregate\n" id) - x <- aggregate fp - threadDelay delay - atomically (writeTVar stateT x) - pure stateT - -poll' :: String -> SensorM a -> Int -> Sensor a -poll' id aggregate delay = Sensor id $ memoizeMVar do - stateT <- newTVarIO =<< aggregate - void $ forkIO $ forever do - -- liftIO (printf "[%s] aggregate\n" id) - x <- aggregate - threadDelay delay - atomically (writeTVar stateT x) - pure stateT - -watchXPropertyChanges :: - String -> - [String] -> - (X.Display -> SensorM a) -> - Sensor a -watchXPropertyChanges id as' aggregate = Sensor id $ memoizeMVar do - (dpy, as) <- liftIO do - dpy <- X.openDisplay "" - let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) - X.selectInput dpy root X.propertyChangeMask - as <- S.fromList <$> mapM (\a -> X.internAtom dpy a False) as' - pure (dpy, as) - stateT <- newTVarIO =<< aggregate dpy - void $ forkIO $ forever $ do - as' <- liftIO $ X.allocaXEvent $ \ev -> do - X.nextEvent dpy ev - e <- X.getEvent ev - let loop es = do - isTimeout <- X.waitForEvent dpy 0 - if isTimeout - then pure (S.fromList (map (X.ev_atom) es)) - else do - X.nextEvent dpy ev - e <- X.getEvent ev - loop (e : es) - loop [e] - unless (S.null (as `S.intersection` as')) do - atomically . writeTVar stateT =<< aggregate dpy - pure stateT - -histogram :: - (Eq a, Typeable a) => - String -> - b -> - (b -> a -> b) -> - Sensor a -> - Sensor b -histogram id def acc sensor = transform' id def f sensor - where - f b a = pure (acc b a) - -transform' :: - (Eq a, Typeable a) => - String -> - b -> - (b -> a -> SensorM b) -> - Sensor a -> - Sensor b -transform' id def fM sensor = Sensor id $ memoizeMVar do - xT <- poll sensor - x <- readTVarIO xT - x'T <- newTVarIO x - stateT <- newTVarIO =<< fM def x - void $ forkIO $ forever $ do - (def, x) <- atomically do - x' <- readTVar x'T - x <- readTVar xT - checkSTM (x /= x') - def <- readTVar stateT - writeTVar x'T x - pure (def, x) - atomically . writeTVar stateT =<< fM def x - pure stateT - -transform :: - (Eq a, Typeable a) => - String -> - (a -> SensorM b) -> - Sensor a -> - Sensor b -transform id fM sensor = Sensor id $ memoizeMVar do - xT <- poll sensor - x <- readTVarIO xT - x'T <- newTVarIO x - stateT <- newTVarIO =<< fM x - void $ forkIO $ forever $ do - x <- atomically do - x' <- readTVar x'T - x <- readTVar xT - checkSTM (x /= x') - writeTVar x'T x - pure x - atomically . writeTVar stateT =<< fM x - pure stateT +(<$$>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b) +f <$$> x = fmap f <$> x + +infixl 4 <$$> + +fi :: (Integral a, Num b) => a -> b +fi = fromIntegral + +strip :: String -> String +strip = + reverse . dropWhile isSpace . reverse . dropWhile isSpace -- cgit v1.2.3