xman/app/Main.hs

179 lines
5.1 KiB
Haskell

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Main where
import Control.Monad (forM_, forever, void, when)
import Control.Monad.Reader
import Control.Monad.State
import Data.List (any)
import Data.Maybe (isJust)
import Foreign.C.String (castCCharToChar)
import Foreign.C.Types (CLong)
import Graphics.X11.Types
import Graphics.X11.Xlib.Atom
import Graphics.X11.Xlib.Display
import Graphics.X11.Xlib.Event
import Graphics.X11.Xlib.Extras
import Graphics.X11.Xlib.Misc
import Graphics.X11.Xlib.Types
import Text.Regex.TDFA
import System.Environment
import System.Posix.IO
import System.Posix.Signals
import System.Process
type WindowTitle = String
-- I wonder where this idea came from...
newtype XMan a = XMan (ReaderT XMConf (StateT XMState IO) a) deriving
(Functor, Monad, MonadIO, MonadState XMState, MonadReader XMConf)
instance Applicative XMan where
pure = return
(<*>) = ap
data XMState = XMState
{ currentTitle :: Maybe WindowTitle
, xcapeProcess :: Maybe ProcessHandle
}
data XMConf = XMConf
{ display :: Display
, theRoot :: Window
, netActiveWindow :: Atom
, netWMName :: Atom
, regexps :: [String]
, xcapeKeys :: String
}
io :: MonadIO m => IO a -> m a
io = liftIO
runXMan :: XMConf -> XMState -> XMan a -> IO (a, XMState)
runXMan c s (XMan a) = runStateT (runReaderT a c) s
parse :: [String] -> IO ()
parse [_] = usage
parse (x:rs) = initXMan x rs
parse _ = usage
usage :: IO ()
usage = putStrLn "xman XCAPE_KEYS REGEXP [[REGEXP]...]"
main :: IO ()
main = getArgs >>= parse
initXMan :: String -> [String] -> IO ()
initXMan x r = do
void $ installHandler sigCHLD Ignore Nothing
dpy <- openDisplay ""
root <- rootWindow dpy $ defaultScreen dpy
naw <- internAtom dpy "_NET_ACTIVE_WINDOW" False
nwn <- internAtom dpy "_NET_WM_NAME" False
let cf = XMConf
{ display = dpy
, theRoot = root
, netActiveWindow = naw
, netWMName = nwn
, regexps = r
, xcapeKeys = x
}
st = XMState { currentTitle = Nothing, xcapeProcess = Nothing }
allocaSetWindowAttributes $ \a -> do
set_event_mask a propertyChangeMask
changeWindowAttributes dpy root cWEventMask a
void $ allocaXEvent $ \e ->
runXMan cf st $ do
updateXCape
forever $ handle =<< io (nextEvent dpy e >> getEvent e)
updateXCape :: XMan ()
updateXCape = do
dpy <- asks display
atom <- asks netActiveWindow
root <- asks theRoot
prop <- io $ getWindowProperty32 dpy atom root
case prop of
Just [aw] -> getTitle aw >>= updateTitle >> startOrKillXCape
_ -> return ()
handle :: Event -> XMan ()
handle PropertyEvent { ev_atom = a } = do
atom <- asks netActiveWindow
when (a == atom) updateXCape
handle _ = return ()
getTitle :: CLong -> XMan (Maybe WindowTitle)
getTitle winID = do
nwn <- asks netWMName
-- try getting _NET_WM_NAME first before trying legacy WM_NAME
doMaybe [nwn, wM_NAME] $ getTitle' winID
where
doMaybe (x:xs) f = f x >>= (\r -> if isJust r then return r else doMaybe xs f)
doMaybe [] _ = return Nothing
getTitle' :: CLong -> Atom -> XMan (Maybe WindowTitle)
getTitle' winID atom = do
dpy <- asks display
title' <- io $ permitBadWindow $ getWindowProperty8 dpy atom
$ fromIntegral winID
return $ fmap (fmap castCCharToChar) title'
permitBadWindow :: IO a -> IO a
permitBadWindow action = do
handler <- mkXErrorHandler $ \_ e ->
getErrorEvent e >>= handleError >> return 0
original <- _xSetErrorHandler handler
res <- action
void $ _xSetErrorHandler original
return res
where
-- totally ignore BadWindow errors
-- TODO also ignore badvalue errors?
handleError ErrorEvent { ev_error_code = t }
| fromIntegral t == badWindow = return ()
-- anything not a BadWindow is an unexpected error
handleError _ = print "actual error"
updateTitle :: Maybe WindowTitle -> XMan ()
updateTitle newTitle = modify (\s -> s { currentTitle = newTitle } )
startOrKillXCape :: XMan ()
startOrKillXCape = do
title <- gets currentTitle
case title of
Just t -> asks regexps >>= \r ->
if any (t =~) r then stopXCape else startXCape
Nothing -> startXCape
startXCape :: XMan ()
startXCape = do
pID <- gets xcapeProcess
unless (isJust pID) $ do
x <- asks xcapeKeys
h <- io $ runXcape x
modify $ \s -> s { xcapeProcess = Just h }
io $ print "started xcape"
stopXCape :: XMan ()
stopXCape = do
pID <- gets xcapeProcess
forM_ pID $ \p -> do
io $ terminateProcess p
modify $ \s -> s { xcapeProcess = Nothing }
io $ print "stopped xcape"
runXcape :: String -> IO ProcessHandle
runXcape keys = do
dn <- fmap UseHandle $ fdToHandle
=<< openFd "/dev/null" ReadOnly Nothing defaultFileFlags
let cp = proc "xcape" $ ["-d", "-t", "500", "-e"] ++ [keys]
(_, _, _, h) <- createProcess $ cp { std_err = dn, std_out = dn }
return h