179 lines
5.1 KiB
Haskell
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
|