xman/app/Main.hs

239 lines
8.6 KiB
Haskell

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
--------------------------------------------------------------------------------
-- | Xcape MANager (XMan) - a wrapper for managing xcape
--
-- xcape is a program to map keyrelease events to keysyms, and is very useful
-- for making custom keymaps. However, it is not always desirable to have this
-- running all the time; for example, VirtualBox will blend the xkb keymap with
-- that if the Guest OS, so xcape may end up producing an extra keypress. The
-- solution is to turn off xcape when certain windows are in focus.
--
-- The process for doing this using Xlib:
-- 1) Listen for PropertyNotify events from the root window
-- 2) Of those events, filter those where the _NET_ACTIVE_WINDOW atom has changed
-- 3) Using the value of _NET_ACTIVE_WINDOW, get the app name of the active window
-- 4) If the app name matches a certain criteria, turn off xcape (vice versa)
--
-- The matching criteria in (4) are POSIX regular expressions.
--
-- Known limitations:
-- this is agnostic to any keymap changes, so if the keymap is changed, xcape
-- will not be updated or restarted. Furthermore, it is outside the scope of
-- this program to bind multiple xcape mappings with multiple keymaps
module Main (main) where
import Control.Monad
import Control.Monad.Reader
import Control.Monad.State
import Data.Maybe (isJust)
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.Directory
import System.Environment
import System.Posix.IO
import System.Posix.Signals
import System.Process
--------------------------------------------------------------------------------
-- | Central State+Reader+IO Monad (I wonder where this idea came from...)
--
-- The Reader portion holds some of the key data structures from X that we care
-- about as well as the regular expression patterns to match the app names we
-- care about and and the bindings to pass to the xcape command.
--
-- The State portion holds the xcape process handle (so we can kill it later)
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
newtype XMState = XMState { xcapeHandle :: Maybe ProcessHandle }
data XMConf = XMConf
{ display :: Display
, theRoot :: Window
, netActiveWindow :: Atom
, regexps :: Patterns
, xcapeProcess :: CreateProcess
}
-- | timeout for xcape
type Timeout = Maybe String
-- | bindings for xcape
type Bindings = String
-- | regular expression patterns
type Patterns = [String]
-- | window app name
type AppName = String
--------------------------------------------------------------------------------
main :: IO ()
main = getArgs >>= parse
-- | Given a list of arguments, either start the program or print the usage
parse :: [String] -> IO ()
parse [_] = usage
parse ("-t":t:b:rs) = initXMan rs $ mkXcapeProcess (Just t) b
parse (b:rs) = initXMan rs $ mkXcapeProcess Nothing b
parse _ = usage
-- | The name of the xcape executable
xcapeExe :: String
xcapeExe = "xcape"
-- | Given a timeout and bindings for xcape, return a process record. This will
-- run xcape in debug mode (which will make it run as a foreground process,
-- otherwise it will fork unnecessarily).
mkXcapeProcess :: Timeout -> Bindings -> CreateProcess
mkXcapeProcess (Just t) b = proc xcapeExe $ ["-t", t, "-d", "-e"] ++ [b]
mkXcapeProcess Nothing b = proc xcapeExe $ ["-d", "-e"] ++ [b]
-- | Print the usage and exit
usage :: IO ()
usage = putStrLn "xman [-t TIMEOUT] BINDINGS REGEXP [[REGEXP] ...]"
-- | Given xcape bindings and regular expression patterns to match the window
-- titles we care about, initialize the XMan monad and run the main event loop
initXMan :: Patterns -> CreateProcess -> IO ()
initXMan rs cp = do
r <- checkXcape
if r then initX else putStrLn "could not find xcape binary"
where
initX = do
-- ignore SIGCHLD so we don't produce zombie processes
void $ installHandler sigCHLD Ignore Nothing
dpy <- openDisplay ""
root <- rootWindow dpy $ defaultScreen dpy
naw <- internAtom dpy "_NET_ACTIVE_WINDOW" False
let cf = XMConf
{ display = dpy
, theRoot = root
, netActiveWindow = naw
, regexps = rs
, xcapeProcess = cp
}
st = XMState { xcapeHandle = Nothing }
-- listen only for PropertyNotify events on the root window
allocaSetWindowAttributes $ \a -> do
set_event_mask a propertyChangeMask
changeWindowAttributes dpy root cWEventMask a
void $ allocaXEvent $ \e ->
runXMan cf st $ do
updateXCape -- set the initial state before entering main loop
forever $ handle =<< io (nextEvent dpy e >> getEvent e)
-- | Return true if xcape is installed
checkXcape :: IO Bool
checkXcape = isJust <$> findExecutable xcapeExe
-- | Lift an IO monad into the XMan context
io :: MonadIO m => IO a -> m a
io = liftIO
-- | Given an initial state and configuration, run the XMan monad
runXMan :: XMConf -> XMState -> XMan a -> IO (a, XMState)
runXMan c s (XMan a) = runStateT (runReaderT a c) s
-- | Update the xcape status given the state of XMan
updateXCape :: XMan ()
updateXCape = do
dpy <- asks display
atom <- asks netActiveWindow
root <- asks theRoot
-- find the active window; if none are found, assume there are no windows
-- open, in which case xcape should be running
prop <- io $ getWindowProperty32 dpy atom root
case prop of
Just [aw] -> getAppName (fromIntegral aw) >>= startOrKillXCape
_ -> startXCape
-- | Given an event, call a handler. In this case the only thing we care about
-- are PropertyNotify events where the atom is _NET_ACTIVE_WINDOW, which will
-- initiated the xcape update logic.
handle :: Event -> XMan ()
handle PropertyEvent { ev_atom = a } = do
atom <- asks netActiveWindow
when (a == atom) updateXCape
handle _ = return ()
-- | Given a window, return its app name
getAppName :: Window -> XMan AppName
getAppName w = io . fmap resName . permitBadWindow . flip getClassHint w =<<
asks display
-- | Given an IO action (which is assumed to call an XLib function that may
-- throw an error), attach an error handler before performing the action and
-- remove it after it completes. The error handler will ignore BadWindow errors
-- (which in this case are assumed to be benign since the _NET_ACTIVE_WINDOW
-- atom may refer to a non-existent window)
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
-- TODO also ignore badvalue errors?
handleError ErrorEvent { ev_error_code = t }
| fromIntegral t == badWindow = return ()
handleError _ = print "actual error"
-- | Given an app name, start or stop xcape if it matches any of the supplied
-- regular expressions in XMan
startOrKillXCape :: AppName -> XMan ()
startOrKillXCape name = do
rs <- asks regexps
if any (name =~) rs then stopXCape else startXCape
-- | Start xcape if it is not already running
startXCape :: XMan ()
startXCape = do
pID <- gets xcapeHandle
unless (isJust pID) $ do
cp <- asks xcapeProcess
h <- io $ createProcessNull cp
modify $ \s -> s { xcapeHandle = Just h }
io $ print "started xcape"
-- | Stop xcape if it is running
stopXCape :: XMan ()
stopXCape = do
pID <- gets xcapeHandle
forM_ pID $ \p -> do
io $ terminateProcess p
modify $ \s -> s { xcapeHandle = Nothing }
io $ print "stopped xcape"
-- | Given a createProcess record, start the process with stderr and stdout
-- redirected to the null device
-- NOTE: use the process module here rather than the unix module. The latter has
-- the 'forkProcess' function which may fail if multiple instances of xcape are
-- started and killed in quick succession (Resource unavailable error).
createProcessNull :: CreateProcess -> IO ProcessHandle
createProcessNull cp = do
fd <- openFd "/dev/null" ReadOnly Nothing defaultFileFlags
dn <- UseHandle <$> fdToHandle fd
(_, _, _, h) <- createProcess $ cp { std_err = dn, std_out = dn }
return h