-----------------------------------------------------------------------------
-- |
-- Module      :  XMonad.Hooks.OnPropertyChange
-- Description :  Apply a manageHook on a property (e.g., @WM_CLASS@) change
-- Copyright   :  (c) Brandon S Allbery, 2015
-- License     :  BSD3-style (see LICENSE)
--
-- Maintainer  :  allbery.b@gmail.com
-- Stability   :  unstable
-- Portability :  not portable
--
-- Module to apply a ManageHook to an already-mapped window when a property
-- changes. This would commonly be used to match browser windows by title,
-- since the final title will only be set after (a) the window is mapped,
-- (b) its document has been loaded, (c) all load-time scripts have run.
-- (Don't blame browsers for this; it's inherent in HTML and the DOM. And
-- changing title dynamically is explicitly permitted by ICCCM and EWMH;
-- you don't really want to have your editor window umapped/remapped to
-- show the current document and modified state in the titlebar, do you?)
--
-- This is a handleEventHook that triggers on a PropertyChange event. It
-- currently ignores properties being removed, in part because you can't
-- do anything useful in a ManageHook involving nonexistence of a property.
--
-- This module could also be useful for Electron applications like Spotify
-- which sets its WM_CLASS too late for window manager to map it properly.
--
-----------------------------------------------------------------------------

module XMonad.Hooks.OnPropertyChange (
  -- * Usage
  -- $usage
  onXPropertyChange,
  onTitleChange,
  onClassChange,
) where

import XMonad
import XMonad.Prelude

-- $usage
-- You can use this module with the following in your @xmonad.hs@:
--
-- > import XMonad.Hooks.OnPropertyChange
--
-- Enable it by including in you handleEventHook definition:
--
-- >  main = xmonad $ def
-- >      { ...
-- >      , handleEventHook = onXPropertyChange "WM_NAME" (title =? "Spotify" --> doShift "5")
-- >      , ...
-- >      }
--
-- Or you could create a dynamicManageHook as below:
--
-- > myDynamicManageHook :: ManageHook
-- > myDynamicManageHook =
-- >  composeAll
-- >    [ className =? "Spotify" --> doShift (myWorkspaces !! 4),
-- >      title =? "maybe_special_terminal" <||> title =? "special_terminal" --> doCenterFloat,
-- >      className =? "dynamicApp" <&&> title =? "dynamic_app" --> doCenterFloat
-- >    ]
--
-- And then use it in your handleEventHookDefinition:
--
-- >  main = xmonad $ def
-- >      { ...
-- >      , handleEventHook = onXPropertyChange "WM_NAME" myDynamicManageHook
-- >      , ...
-- >      }
--

-- |
-- Run a 'ManageHook' when a specific property is changed on a window. Note
-- that this will run on any window which changes the property, so you should
-- be very specific in your 'ManageHook' matching (lots of windows change
-- their titles on the fly!):
--
-- > onXPropertyChange "WM_NAME" (className =? "Iceweasel" <&&> title =? "whatever" --> doShift "2")
--
-- Note that the fixity of (-->) won't allow it to be mixed with ($), so you
-- can't use the obvious $ shorthand.
--
-- > onXPropertyChange "WM_NAME" $ title =? "Foo" --> doFloat -- won't work!
--
-- Consider instead phrasing it like any
-- other 'ManageHook':
--
-- >  main = xmonad $ def
-- >      { ...
-- >      , handleEventHook = onXPropertyChange "WM_NAME" myDynHook
-- >      , ...
-- >      }
-- >
-- >    myDynHook = composeAll [...]
--
onXPropertyChange :: String -> ManageHook -> Event -> X All
onXPropertyChange :: String -> ManageHook -> Event -> X All
onXPropertyChange String
prop ManageHook
hook PropertyEvent { ev_window :: Event -> Atom
ev_window = Atom
w, ev_atom :: Event -> Atom
ev_atom = Atom
a, ev_propstate :: Event -> CInt
ev_propstate = CInt
ps } = do
  Atom
pa <- String -> X Atom
getAtom String
prop
  Bool -> X () -> X ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (CInt
ps CInt -> CInt -> Bool
forall a. Eq a => a -> a -> Bool
== CInt
propertyNewValue Bool -> Bool -> Bool
&& Atom
a Atom -> Atom -> Bool
forall a. Eq a => a -> a -> Bool
== Atom
pa) (X () -> X ()) -> X () -> X ()
forall a b. (a -> b) -> a -> b
$ do
    WindowSet -> WindowSet
g <- Endo WindowSet -> WindowSet -> WindowSet
forall a. Endo a -> a -> a
appEndo (Endo WindowSet -> WindowSet -> WindowSet)
-> X (Endo WindowSet) -> X (WindowSet -> WindowSet)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Endo WindowSet -> X (Endo WindowSet) -> X (Endo WindowSet)
forall a. a -> X a -> X a
userCodeDef ((WindowSet -> WindowSet) -> Endo WindowSet
forall a. (a -> a) -> Endo a
Endo WindowSet -> WindowSet
forall a. a -> a
id) (ManageHook -> Atom -> X (Endo WindowSet)
forall a. Query a -> Atom -> X a
runQuery ManageHook
hook Atom
w)
    (WindowSet -> WindowSet) -> X ()
windows WindowSet -> WindowSet
g
  All -> X All
forall a. a -> X a
forall (m :: * -> *) a. Monad m => a -> m a
return All
forall a. Monoid a => a
mempty -- so anything else also processes it
onXPropertyChange String
_ ManageHook
_ Event
_ = All -> X All
forall a. a -> X a
forall (m :: * -> *) a. Monad m => a -> m a
return All
forall a. Monoid a => a
mempty

-- | A shorthand for dynamic titles; i.e., applications changing their
-- @WM_NAME@ property.
onTitleChange :: ManageHook -> Event -> X All
-- strictly, this should also check _NET_WM_NAME. practically, both will
-- change and each gets its own PropertyEvent, so we'd need to record that
-- we saw the event for that window and ignore the second one. Instead, just
-- trust that nobody sets only _NET_WM_NAME. (I'm sure this will prove false,
-- since there's always someone who can't bother being compliant.)
onTitleChange :: ManageHook -> Event -> X All
onTitleChange = String -> ManageHook -> Event -> X All
onXPropertyChange String
"WM_NAME"

-- | A shorthand for dynamic resource and class names; i.e.,
-- applications changing their @WM_CLASS@ property.
onClassChange :: ManageHook -> Event -> X All
onClassChange :: ManageHook -> Event -> X All
onClassChange = String -> ManageHook -> Event -> X All
onXPropertyChange String
"WM_CLASS"