packages feed

arbtt-0.11: src/Capture/X11.hs

{-# LANGUAGE RecordWildCards #-}
module Capture.X11 where

import Data
import Graphics.X11
import Graphics.X11.Xlib.Extras
import Control.Monad
import Control.Exception (bracket)
import System.IO.Error (catchIOError)
import Control.Applicative
import Data.Maybe
import Data.Time.Clock
import System.IO
import qualified Data.MyText as T

import System.Locale.SetLocale
import Graphics.X11.XScreenSaver (getXIdleTime, compiledWithXScreenSaver)

setupCapture :: IO ()
setupCapture = do
        loc <- supportsLocale
        unless loc $ hPutStrLn stderr "arbtt [Warning]: locale unsupported"
        dpy <- openDisplay ""
        xSetErrorHandler
        let rwin = defaultRootWindow dpy
        a <- internAtom dpy "_NET_CLIENT_LIST" False
        p <- getWindowProperty32 dpy a rwin
        when (isNothing p) $ do
                hPutStrLn stderr "arbtt: ERROR: No _NET_CLIENT_LIST set for the root window"
        closeDisplay dpy

captureData :: IO CaptureData
captureData = do
        dpy <- openDisplay ""
        xSetErrorHandler
        let rwin = defaultRootWindow dpy

        -- Desktop
        desktop <- getDesktops dpy
        current_desktop <- desktop <$> getDesktop "_NET_CURRENT_DESKTOP" dpy rwin

        -- Windows
        a <- internAtom dpy "_NET_CLIENT_LIST" False
        p <- getWindowProperty32 dpy a rwin

        wins <- case p of
                Just wins -> filterM (isInteresting dpy) (map fromIntegral wins)
                Nothing   -> return []

        (fsubwin,_) <- getInputFocus dpy
        fwin <- followTreeUntil dpy (`elem` wins) fsubwin

        winData <- forM wins $ \w -> do
            let wActive = w == fwin
            wHidden <- isHidden dpy w
            wTitle <- T.pack <$> getWindowTitle dpy w
            wProgram <- T.pack <$> getProgramName dpy w
            wDesktop <- T.pack . desktop <$> getDesktop "_NET_WM_DESKTOP" dpy w
            return WindowData{..}

        it <- fromIntegral `fmap` getXIdleTime dpy

        closeDisplay dpy
        return $ CaptureData winData it (T.pack current_desktop)

getWindowTitle :: Display -> Window -> IO String
getWindowTitle dpy =  myFetchName dpy

getProgramName :: Display -> Window -> IO String
getProgramName dpy = fmap resName . getClassHint dpy

-- Returns a parent of a window or zero.
getParent :: Display -> Window -> IO Window
getParent dpy w = do
  (_, parent, _) <- queryTree dpy w `catchIOError` (const $ return (0,0,[]))
  return parent

-- | Follows the tree of windows up until the condition is met or the root
-- window is reached.
followTreeUntil :: Display -> (Window -> Bool) -> Window -> IO Window 
followTreeUntil dpy cond = go
  where go w | cond w    = return w
             | otherwise = do p <- getParent dpy w
                              if p == 0 then return w
                                        else go p 

-- | Ignore, for example, Desktop and Docks windows
isInteresting :: Display -> Window -> IO Bool
isInteresting d w = do
    a <- internAtom d "_NET_WM_WINDOW_TYPE" False
    dock <- internAtom d "_NET_WM_WINDOW_TYPE_DOCK" False
    desk <- internAtom d "_NET_WM_WINDOW_TYPE_DESKTOP" False
    mbr <- getWindowProperty32 d a w
    case mbr of
        Just [r] -> return $ fromIntegral r `notElem` [dock, desk]
        _        -> return True

-- | better than fetchName from X11, as it supports _NET_WM_NAME and unicode
--
-- Code taken from XMonad.Managehook.title
myFetchName :: Display -> Window -> IO String
myFetchName d w = do
        let getProp =
                (internAtom d "_NET_WM_NAME" False >>= getTextProperty d w)
                `catchIOError`
                (\_ -> getTextProperty d w wM_NAME)

            extract prop = do l <- wcTextPropertyToTextList d prop
                              return $ if null l then "" else head l

        bracket getProp (xFree . tp_value) extract
            `catchIOError` \_ -> return ""

getDesktops :: Display -> IO (Maybe Int -> String)
getDesktops dpy = do
    desktops <- flip catchIOError (\_ -> return []) $ do
        a <- internAtom dpy "_NET_DESKTOP_NAMES" False
        tp <- getTextProperty dpy (defaultRootWindow dpy) a
        dropTailNull <$> wcTextPropertyToTextList dpy tp
    let name n | 0 <= n && n < length desktops = desktops !! n
               | otherwise                     = show n
    return $ maybe "" name
  where
    -- _NET_DESKTOP_NAMES is a list of NULL-terminated strings but
    -- wcTextPropertyToTextList treats NULL as a separator,
    -- so we need to drop the final empty string
    dropTailNull [""] = []
    dropTailNull (x:xs) = x : dropTailNull xs

getDesktop :: String -> Display -> Window -> IO (Maybe Int)
getDesktop prop dpy w = flip catchIOError (\_ -> return Nothing) $ do
    a <- internAtom dpy prop False
    p <- getWindowProperty32 dpy a w
    return $ do {[d] <- p; return (fromIntegral d)}

isHidden :: Display -> Window -> IO Bool
isHidden dpy w = flip catchIOError (\_ -> return False) $ do
    a <- internAtom dpy "WM_STATE" False
    Just (state:_) <- getWindowProperty32 dpy a w
    return $ fromIntegral state /= normalState