{-# LANGUAGE LambdaCase #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  XMonad.Util.RemoteWindows
-- Description :  A module to find out whether the window is remote or local.
-- Copyright   :  (c) Anton Vorontsov <anton@enomsg.org> 2014
-- License     :  BSD-style (as xmonad)
--
-- Maintainer  :  Anton Vorontsov <anton@enomsg.org>
-- Stability   :  unstable
-- Portability :  unportable
--
-- This module implements a proper way of finding out whether the window
-- is remote or local.
--
-- Just checking for a hostname and WM_CLIENT_MACHINE being equal is often
-- not enough because the hostname is a changing subject (without any
-- established notification mechanisms), and thus WM_CLIENT_MACHINE and
-- the hostname can diverge even for a local window.
--
-- This module solves the problem. As soon as there is a new window
-- created, we check the hostname and WM_CLIENT_MACHINE, and then we cache
-- the result into the XMONAD_REMOTE property.
--
-- Notice that XMonad itself does not know anything about hostnames, nor
-- does it have any dependency on Network.* modules. For this module it is
-- not a problem: you can provide a mean to get the hostname through your
-- config file (see usage). Or, if you don't like the hassle of handling
-- dynamic hostnames (suppose your hostname never changes), it is also
-- fine: this module will fallback to using environment variables.
--
-----------------------------------------------------------------------------

module XMonad.Util.RemoteWindows
    ( -- $usage
      isLocalWindow
    , manageRemote
    , manageRemoteG
    ) where

import XMonad
import XMonad.Util.WindowProperties
import XMonad.Prelude
import System.Posix.Env

-- $usage
-- You can use this module with the following in your @~\/.xmonad\/xmonad.hs@:
--
-- > import XMonad
-- > import XMonad.Util.RemoteWindows
-- > import Network.BSD
-- >
-- > main = xmonad def
-- >    { manageHook = manageRemote =<< io getHostName }

guessHostName :: IO String
guessHostName :: IO String
guessHostName = [Maybe String] -> String
forall a. [Maybe [a]] -> [a]
pickOneMaybe ([Maybe String] -> String) -> IO [Maybe String] -> IO String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (String -> IO (Maybe String)
getEnv (String -> IO (Maybe String)) -> [String] -> IO [Maybe String]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
`mapM` [String]
vars)
  where
    pickOneMaybe :: [Maybe [a]] -> [a]
pickOneMaybe = [[a]] -> [a]
forall a. [a] -> a
last ([[a]] -> [a]) -> ([Maybe [a]] -> [[a]]) -> [Maybe [a]] -> [a]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([a]
forall (m :: * -> *) a. MonadPlus m => m a
mzero[a] -> [[a]] -> [[a]]
forall a. a -> [a] -> [a]
:) ([[a]] -> [[a]]) -> ([Maybe [a]] -> [[a]]) -> [Maybe [a]] -> [[a]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> [[a]] -> [[a]]
forall a. Int -> [a] -> [a]
take Int
1 ([[a]] -> [[a]]) -> ([Maybe [a]] -> [[a]]) -> [Maybe [a]] -> [[a]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Maybe [a]] -> [[a]]
forall a. [Maybe a] -> [a]
catMaybes
    vars :: [String]
vars = [String
"XAUTHLOCALHOSTNAME",String
"HOST",String
"HOSTNAME"]

setRemoteProp :: Window -> String -> X ()
setRemoteProp :: Window -> String -> X ()
setRemoteProp Window
w String
host = do
    Display
d <- (XConf -> Display) -> X Display
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks XConf -> Display
display
    Window
p <- String -> X Window
getAtom String
"XMONAD_REMOTE"
    Bool
v <- Property -> Window -> X Bool
hasProperty (String -> Property
Machine String
host) Window
w
    IO () -> X ()
forall (m :: * -> *) a. MonadIO m => IO a -> m a
io (IO () -> X ()) -> IO () -> X ()
forall a b. (a -> b) -> a -> b
$ Display -> Window -> Window -> Window -> CInt -> [CLong] -> IO ()
changeProperty32 Display
d Window
w Window
p Window
cARDINAL CInt
propModeReplace
                          [Int -> CLong
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int -> CLong) -> (Bool -> Int) -> Bool -> CLong
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Bool -> Int
forall a. Enum a => a -> Int
fromEnum (Bool -> CLong) -> Bool -> CLong
forall a b. (a -> b) -> a -> b
$ Bool -> Bool
not Bool
v]

-- | Given a window, tell if it is a local or a remote process. Normally,
-- it checks XMONAD_REMOTE property. If it does not exist (i.e. the
-- manageRemote hook was not deployed in user's config), it falls back to
-- checking environment variables and assuming that hostname never
-- changes.
isLocalWindow :: Window -> X Bool
isLocalWindow :: Window -> X Bool
isLocalWindow Window
w = String -> Window -> X (Maybe [CLong])
getProp32s String
"XMONAD_REMOTE" Window
w X (Maybe [CLong]) -> (Maybe [CLong] -> X Bool) -> X Bool
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
    Just [CLong
y] -> Bool -> X Bool
forall (m :: * -> *) a. Monad m => a -> m a
return (Bool -> X Bool) -> Bool -> X Bool
forall a b. (a -> b) -> a -> b
$ CLong
y CLong -> CLong -> Bool
forall a. Eq a => a -> a -> Bool
== CLong
0
    Maybe [CLong]
_ -> IO String -> X String
forall (m :: * -> *) a. MonadIO m => IO a -> m a
io IO String
guessHostName X String -> (String -> X Bool) -> X Bool
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \String
host -> Property -> Window -> X Bool
hasProperty (String -> Property
Machine String
host) Window
w

-- | Use this hook to let XMonad properly track remote/local windows. For
-- example, @manageHook = manageRemote =<< io getHostName@.
manageRemote :: String -> ManageHook
manageRemote :: String -> ManageHook
manageRemote String
host = Query Window
forall r (m :: * -> *). MonadReader r m => m r
ask Query Window -> (Window -> ManageHook) -> ManageHook
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \Window
w -> X () -> Query ()
forall a. X a -> Query a
liftX (Window -> String -> X ()
setRemoteProp Window
w String
host) Query () -> ManageHook -> ManageHook
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Endo WindowSet -> ManageHook
forall (m :: * -> *) a. Monad m => a -> m a
return Endo WindowSet
forall a. Monoid a => a
mempty

-- | Use this hook if you want to manage XMONAD_REMOTE properties, but
-- don't want to use an external getHostName in your config. That way you
-- are retreating to environment variables.
manageRemoteG :: ManageHook
manageRemoteG :: ManageHook
manageRemoteG = String -> ManageHook
manageRemote (String -> ManageHook) -> Query String -> ManageHook
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< IO String -> Query String
forall (m :: * -> *) a. MonadIO m => IO a -> m a
io IO String
guessHostName