Adam Plaice ca866229f6 Fix most remaining broken inter-module docs links
The links were broken due to:

1. Incorrect quotes (' instead of " for module links and occasionally
vice-versa).

2. Changes in the name of the "target" module not reflected in the
"source" docs.

3. Typos to begin with.

4. Use of `<foo>` in the docs is rendered as just `foo` with a link to
`/foo`.

5. Similarly for `"Foo"` if it starts with a capital letter (and hence
could be a module).

6. Markup inside `@` code blocks still being applied.

e.g. `@M-<arrow-keys>@` is rendered as `M-arrow-keys` with a spurious
hyperlink from arrow-keys to `/arrow-keys`, which is confusing.

Three links from XMonad.Util.Run have been removed outright, since
they're no longer examples of the usage of 'runProcessWithInput'.
WmiiActions has been gone since 2008, while XMonad.Prompt.Directory
and XMonad.Layout.WorkspaceDir haven't been using
'runProcessWithInput' since 2020 and 2012, respectively.

In some cases the `<foo>` were surrounded with @, especially in the
case of key definitions, for consistency.  (This wasn't done
everywhere, because it looks ugly in the source.)

MoreManageHelpers has never been in xmonad-contrib.  ManageHelpers
seems to fill the expected role.

In the case of the module description for X.H.ManageDebug the quotes
were simply removed because none of the likely options to make the
link work were successful.
2022-11-01 19:35:55 +01:00

560 lines
21 KiB
Haskell

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# OPTIONS_HADDOCK show-extensions #-}
-- |
-- Module: XMonad.Hooks.Focus
-- Description: Extends ManageHook EDSL to work on focused windows and current workspace.
-- Copyright: sgf-dma, 2016
-- Maintainer: sgf.dma@gmail.com
--
-- Extends "XMonad.ManageHook" EDSL to work on focused windows and current
-- workspace.
--
module XMonad.Hooks.Focus
(
-- $main
-- * FocusQuery.
--
-- $focusquery
Focus (..)
, FocusLock (..)
, toggleLock
, focusLockOn
, focusLockOff
, FocusQuery
, runFocusQuery
, FocusHook
-- * Lifting into FocusQuery.
--
-- $lift
, liftQuery
, new
, focused
, focused'
, focusedOn
, focusedOn'
, focusedCur
, focusedCur'
, newOn
, newOnCur
, unlessFocusLock
-- * Commonly used actions for modifying focus.
--
-- $common
, keepFocus
, switchFocus
, keepWorkspace
, switchWorkspace
-- * Running FocusQuery.
--
-- $running
, manageFocus
-- * Example configurations.
--
-- $examples
, activateSwitchWs
, activateOnCurrentWs
, activateOnCurrentKeepFocus
)
where
import Control.Arrow ((&&&))
import Control.Monad.Reader
import XMonad
import XMonad.Prelude
import qualified XMonad.StackSet as W
import qualified XMonad.Util.ExtensibleState as XS
import XMonad.Hooks.ManageHelpers (currentWs)
-- $main
--
-- This module provides monad on top of Query monad providing additional
-- information about new window:
--
-- - workspace, where new window will appear;
-- - focused window on workspace, where new window will appear;
-- - current workspace;
--
-- And a property in extensible state:
--
-- - is focus lock enabled? Focus lock instructs all library's 'FocusHook'
-- functions to not move focus or switch workspace.
--
-- Lifting operations for standard 'ManageHook' EDSL combinators into
-- 'FocusQuery' monad allowing to run these combinators on focused window and
-- common actions for keeping focus and\/or workspace, switching focus and\/or
-- workspace are also provided.
--
-- == Quick start.
--
-- I may use one of predefined configurations.
--
-- 1. The default window activation behavior (switch to workspace with
-- activated window and switch focus to it) expressed using this module:
--
-- > import XMonad
-- >
-- > import XMonad.Hooks.EwmhDesktops
-- > import XMonad.Hooks.Focus
-- >
-- > main :: IO ()
-- > main = do
-- > let ah :: ManageHook
-- > ah = activateSwitchWs
-- > xcf = setEwmhActivateHook ah
-- > . ewmh $ def{ modMask = mod4Mask }
-- > xmonad xcf
--
-- 2. Or i may move activated window to current workspace and switch focus to
-- it:
--
-- > let ah :: ManageHook
-- > ah = activateOnCurrentWs
--
-- 3. Or move activated window to current workspace, but keep focus unchanged:
--
-- > let ah :: ManageHook
-- > ah = activateOnCurrentKeepFocus
--
-- 4. I may use regular 'ManageHook' combinators for filtering, which windows
-- may activate. E.g. activate all windows, except firefox:
--
-- > let ah :: ManageHook
-- > ah = not <$> (className =? "Firefox" <||> className =? "Firefox-esr" <||> className =? "Iceweasel")
-- > --> activateSwitchWs
--
-- 5. Or even use 'FocusHook' combinators. E.g. activate all windows, unless
-- xterm is focused on /current/ workspace:
--
-- > let ah :: ManageHook
-- > ah = manageFocus (not <$> focusedCur (className =? "XTerm")
-- > --> liftQuery activateSwitchWs)
--
-- or activate all windows, unless focused window on the workspace,
-- /where activated window is/, is not a xterm:
--
-- > let ah :: ManageHook
-- > ah = manageFocus (not <$> focused (className =? "XTerm")
-- > --> liftQuery activateSwitchWs)
--
-- == Defining FocusHook.
--
-- I may define my own 'FocusHook' like:
--
-- > activateFocusHook :: FocusHook
-- > activateFocusHook = composeAll
-- > -- If 'gmrun' is focused on workspace, on which
-- > -- /activated window/ is, keep focus unchanged. But i
-- > -- may still switch workspace (thus, i use 'composeAll').
-- > -- See 'keepFocus' properties in the docs below.
-- > [ focused (className =? "Gmrun")
-- > --> keepFocus
-- > -- Default behavior for activated windows: switch
-- > -- workspace and focus.
-- > , return True --> switchWorkspace <> switchFocus
-- > ]
-- >
-- > newFocusHook :: FocusHook
-- > newFocusHook = composeOne
-- > -- Always switch focus to 'gmrun'.
-- > [ new (className =? "Gmrun") -?> switchFocus
-- > -- And always keep focus on 'gmrun'. Note, that
-- > -- another 'gmrun' will steal focus from already
-- > -- running one.
-- > , focused (className =? "Gmrun") -?> keepFocus
-- > -- If firefox dialog prompt (e.g. master password
-- > -- prompt) is focused on current workspace and new
-- > -- window appears here too, keep focus unchanged
-- > -- (note, used predicates: @newOnCur <&&> focused@ is
-- > -- the same as @newOnCur <&&> focusedCur@, but is
-- > -- /not/ the same as just 'focusedCur' )
-- > , newOnCur <&&> focused
-- > ((className =? "Firefox" <||> className =? "Firefox-esr" <||> className =? "Iceweasel") <&&> isDialog)
-- > -?> keepFocus
-- > -- Default behavior for new windows: switch focus.
-- > , return True -?> switchFocus
-- > ]
--
-- And then use it:
--
-- > import XMonad
-- > import XMonad.Util.EZConfig
-- >
-- > import XMonad.Hooks.EwmhDesktops
-- > import XMonad.Hooks.ManageHelpers
-- > import XMonad.Hooks.Focus
-- >
-- >
-- > main :: IO ()
-- > main = do
-- > let newFh :: ManageHook
-- > newFh = manageFocus newFocusHook
-- > acFh :: ManageHook
-- > acFh = manageFocus activateFocusHook
-- > xcf = setEwmhActivateHook acFh
-- > . ewmh $ def
-- > { manageHook = newFh <> manageHook def
-- > , modMask = mod4Mask
-- > }
-- > `additionalKeys` [((mod4Mask, xK_v), toggleLock)]
-- > xmonad xcf
--
-- Note:
--
-- - /mod4Mask+v/ key toggles focus lock (when enabled, neither focus nor
-- workspace won't be switched).
-- - I need "XMonad.Hooks.EwmhDesktops" module for enabling window
-- activation.
-- - 'FocusHook' in 'manageHook' will be called /only/ for new windows.
-- - 'FocusHook' in 'setEwmhActivateHook' will be called /only/ for activated windows.
--
-- Alternatively, i may construct a single 'FocusHook' for both new and
-- activated windows and then just add it to both 'manageHook' and 'setEwmhActivateHook':
--
-- > let fh :: Bool -> ManageHook
-- > fh activated = manageFocus $ composeOne
-- > [ pure activated -?> activateFocusHook
-- > , pure True -?> newFocusHook
-- > ]
-- > xcf = setEwmhActivateHook (fh True)
-- > . ewmh $ def
-- > { manageHook = fh False <> manageHook def
-- > , modMask = mod4Mask
-- > }
-- > `additionalKeys` [((mod4Mask, xK_v), toggleLock)]
--
-- And more technical notes:
--
-- - 'FocusHook' will run /many/ times, so it usually should not keep state
-- or save results. Precisely, it may do anything, but it must be idempotent
-- to operate properly.
-- - 'FocusHook' will see new window at workspace, where functions on the
-- /right/ from it in 'ManageHook' monoid place it. In other words, in
-- @(Endo WindowSet)@ monoid i may see changes only from functions applied
-- /before/ (more to the right in function composition). Thus, it's better to
-- add 'FocusHook' the last.
-- - 'FocusHook' functions won't see window shift to another workspace made
-- by function from 'FocusHook' itself: new window workspace is determined
-- /before/ running 'FocusHook' and even if later one of 'FocusHook'
-- functions moves window to another workspace, predicates ('focused',
-- 'newOn', etc) will still think new window is at workspace it was before.
-- This can be worked around by splitting 'FocusHook' into several different
-- values and evaluating each one separately, like:
--
-- > (FH2 -- manageFocus --> MH2) <> (FH1 -- manageFocus --> MH1) <> ..
--
-- E.g.
--
-- > manageFocus FH2 <> manageFocus FH1 <> ..
--
-- now @FH2@ will see window shift made by @FH1@.
--
-- Another interesting example is moving all activated windows to current
-- workspace by default, and applying 'FocusHook' after:
--
-- > import XMonad
-- > import XMonad.Util.EZConfig
-- >
-- > import XMonad.Hooks.EwmhDesktops
-- > import XMonad.Hooks.ManageHelpers
-- > import XMonad.Hooks.Focus
-- >
-- > main :: IO ()
-- > main = do
-- > let fh :: Bool -> ManageHook
-- > fh activated = manageFocus $ composeOne
-- > [ pure activated -?> (newOnCur --> keepFocus)
-- > , pure True -?> newFocusHook
-- > ]
-- > xcf = setEwmhActivateHook (fh True <> activateOnCurrentWs)
-- > . ewmh $ def
-- > { manageHook = fh False <> manageHook def
-- > , modMask = mod4Mask
-- > }
-- > `additionalKeys` [((mod4Mask, xK_v), toggleLock)]
-- > xmonad xcf
-- >
-- > newFocusHook :: FocusHook
-- > newFocusHook = composeOne
-- > -- Always switch focus to 'gmrun'.
-- > [ new (className =? "Gmrun") -?> switchFocus
-- > -- And always keep focus on 'gmrun'. Note, that
-- > -- another 'gmrun' will steal focus from already
-- > -- running one.
-- > , focused (className =? "Gmrun") -?> keepFocus
-- > -- If firefox dialog prompt (e.g. master password
-- > -- prompt) is focused on current workspace and new
-- > -- window appears here too, keep focus unchanged
-- > -- (note, used predicates: @newOnCur <&&> focused@ is
-- > -- the same as @newOnCur <&&> focusedCur@, but is
-- > -- /not/ the same as just 'focusedCur' )
-- > , newOnCur <&&> focused
-- > ((className =? "Firefox" <||> className =? "Firefox-esr" <||> className =? "Iceweasel") <&&> isDialog)
-- > -?> keepFocus
-- > -- Default behavior for new windows: switch focus.
-- > , return True -?> switchFocus
-- > ]
--
-- Note here:
--
-- - i keep focus, when activated window appears on current workspace, in
-- this example.
-- - when @pure activated -?> (newOnCur --> keepFocus)@ runs, activated
-- window will be /already/ on current workspace, thus, if i do not want to
-- move some activated windows, i should filter them out before applying
-- @activateOnCurrentWs@ 'FocusHook'.
-- FocusQuery.
-- $focusquery
-- | Information about current workspace and focus.
data Focus = Focus
{ -- | Workspace, where new window appears.
newWorkspace :: WorkspaceId
-- | Focused window on workspace, where new window
-- appears.
, focusedWindow :: Maybe Window
-- | Current workspace.
, currentWorkspace :: WorkspaceId
}
deriving (Show)
instance Default Focus where
def = Focus
{ focusedWindow = Nothing
, newWorkspace = ""
, currentWorkspace = ""
}
newtype FocusLock = FocusLock {getFocusLock :: Bool}
deriving (Show)
instance ExtensionClass FocusLock where
initialValue = FocusLock False
-- | Toggle stored focus lock state.
toggleLock :: X ()
toggleLock = XS.modify (\(FocusLock b) -> FocusLock (not b))
-- | Lock focus.
focusLockOn :: X ()
focusLockOn = XS.modify (const (FocusLock True))
-- | Unlock focus.
focusLockOff :: X ()
focusLockOff = XS.modify (const (FocusLock False))
-- | Monad on top of 'Query' providing additional information about new
-- window.
newtype FocusQuery a = FocusQuery (ReaderT Focus Query a)
instance Functor FocusQuery where
fmap f (FocusQuery x) = FocusQuery (fmap f x)
instance Applicative FocusQuery where
pure x = FocusQuery (pure x)
(FocusQuery f) <*> (FocusQuery mx) = FocusQuery (f <*> mx)
instance Monad FocusQuery where
(FocusQuery mx) >>= f = FocusQuery $ mx >>= \x ->
let FocusQuery y = f x in y
instance MonadReader Focus FocusQuery where
ask = FocusQuery ask
local f (FocusQuery mx) = FocusQuery (local f mx)
instance MonadIO FocusQuery where
liftIO mx = FocusQuery (liftIO mx)
instance Semigroup a => Semigroup (FocusQuery a) where
(<>) = liftM2 (<>)
instance Monoid a => Monoid (FocusQuery a) where
mempty = return mempty
runFocusQuery :: FocusQuery a -> Focus -> Query a
runFocusQuery (FocusQuery m) = runReaderT m
type FocusHook = FocusQuery (Endo WindowSet)
-- Lifting into 'FocusQuery'.
-- $lift
-- | Lift 'Query' into 'FocusQuery' monad. The same as 'new'.
liftQuery :: Query a -> FocusQuery a
liftQuery = FocusQuery . lift
-- | Run 'Query' on new window.
new :: Query a -> FocusQuery a
new = liftQuery
-- | Run 'Query' on focused window on workspace, where new window appears. If
-- there is no focused window, return 'False'.
focused :: Query Bool -> FocusQuery Bool
focused m = getAny <$> focused' (Any <$> m)
-- | More general version of 'focused'.
focused' :: Monoid a => Query a -> FocusQuery a
focused' m = do
mw <- asks focusedWindow
liftQuery (maybe mempty (flip local m . const) mw)
-- | Run 'Query' on window focused at particular workspace. If there is no
-- focused window, return 'False'.
focusedOn :: WorkspaceId -> Query Bool -> FocusQuery Bool
focusedOn i m = getAny <$> focusedOn' i (Any <$> m)
-- | More general version of 'focusedOn'.
focusedOn' :: Monoid a => WorkspaceId -> Query a -> FocusQuery a
focusedOn' i m = liftQuery $ do
mw <- liftX $ withWindowSet (return . W.peek . W.view i)
maybe mempty (flip local m . const) mw
-- | Run 'Query' on focused window on current workspace. If there is no
-- focused window, return 'False'. Note,
--
-- > focused <&&> newOnCur != focusedCur
--
-- The first will affect only new or activated window appearing on current
-- workspace, while the last will affect any window: focus even for windows
-- appearing on other workpsaces will depend on focus on /current/ workspace.
focusedCur :: Query Bool -> FocusQuery Bool
focusedCur m = getAny <$> focusedCur' (Any <$> m)
-- | More general version of 'focusedCur'.
focusedCur' :: Monoid a => Query a -> FocusQuery a
focusedCur' m = asks currentWorkspace >>= \i -> focusedOn' i m
-- | Does new window appear at particular workspace?
newOn :: WorkspaceId -> FocusQuery Bool
newOn i = asks ((i ==) . newWorkspace)
-- | Does new window appear at current workspace?
newOnCur :: FocusQuery Bool
newOnCur = asks currentWorkspace >>= newOn
-- | Execute 'Query', unless focus is locked.
unlessFocusLock :: Monoid a => Query a -> Query a
unlessFocusLock m = do
FocusLock b <- liftX XS.get
when' (not b) m
-- Commonly used actions for modifying focus.
--
-- $common
-- Operations in each pair 'keepFocus' and 'switchFocus', 'keepWorkspace' and
-- 'switchWorkspace' overwrite each other (the letftmost will determine what
-- happened):
--
-- prop> keepFocus <> switchFocus = keepFocus
-- prop> switchFocus <> keepFocus = switchFocus
-- prop> keepWorkspace <> switchWorkspace = keepWorkspace
-- prop> switchWorkspace <> keepWorkspace = switchWorkspace
--
-- and operations from different pairs are commutative:
--
-- prop> keepFocus <> switchWorkspace = switchWorkspace <> keepFocus
-- prop> switchFocus <> switchWorkspace = switchWorkspace <> switchFocus
--
-- etc.
-- | Keep focus on workspace (may not be current), where new window appears.
-- Workspace will not be switched. This operation is idempotent and
-- effectively returns focus to window focused on that workspace before
-- applying @(Endo WindowSet)@ function.
keepFocus :: FocusHook
keepFocus = focused' $ ask >>= \w -> doF $ \ws ->
W.view (W.currentTag ws) . W.focusWindow w $ ws
-- | Switch focus to new window on workspace (may not be current), where new
-- window appears. Workspace will not be switched. This operation is
-- idempotent.
switchFocus :: FocusHook
switchFocus = do
FocusLock b <- liftQuery . liftX $ XS.get
if b
-- When focus lock is enabled, call 'keepFocus' explicitly (still no
-- 'keepWorkspace') to overwrite default behavior.
then keepFocus
else new $ ask >>= \w -> doF $ \ws ->
W.view (W.currentTag ws) . W.focusWindow w $ ws
-- | Keep current workspace. Focus will not be changed at either current or
-- new window's workspace. This operation is idempotent and effectively
-- switches to workspace, which was current before applying @(Endo WindowSet)@
-- function.
keepWorkspace :: FocusHook
keepWorkspace = do
ws <- asks currentWorkspace
liftQuery . doF $ W.view ws
-- | Switch workspace to one, where new window appears. Focus will not be
-- changed at either current or new window's workspace. This operation is
-- idempotent.
switchWorkspace :: FocusHook
switchWorkspace = do
FocusLock b <- liftQuery . liftX $ XS.get
if b
-- When focus lock is enabled, call 'keepWorkspace' explicitly (still no
-- 'keepFocus') to overwrite default behavior.
then keepWorkspace
else do
ws <- asks newWorkspace
liftQuery . doF $ W.view ws
-- Running FocusQuery.
-- $running
-- | I don't know at which workspace new window will appear until @(Endo
-- WindowSet)@ function from 'windows' in "XMonad.Operations" actually run,
-- but in @(Endo WindowSet)@ function i can't already execute monadic actions,
-- because it's pure. So, i compute result for every workspace here and just
-- use it later in @(Endo WindowSet)@ function. Note, though, that this will
-- execute monadic actions many times, and therefore assume, that result of
-- 'FocusHook' does not depend on the number of times it was executed.
manageFocus :: FocusHook -> ManageHook
manageFocus m = do
fws <- liftX . withWindowSet $ return
. map (W.tag &&& fmap W.focus . W.stack) . W.workspaces
ct <- currentWs
let r = def {currentWorkspace = ct}
hs <- forM fws $ \(i, mw) -> do
f <- runFocusQuery m (r {focusedWindow = mw, newWorkspace = i})
return (i, f)
reader (selectHook hs) >>= doF
where
-- | Select and apply @(Endo WindowSet)@ function depending on which
-- workspace new window appeared now.
selectHook :: [(WorkspaceId, Endo WindowSet)] -> Window -> WindowSet -> WindowSet
selectHook cfs nw ws = fromMaybe ws $ do
i <- W.findTag nw ws
f <- lookup i cfs
return (appEndo f ws)
when' :: (Monad m, Monoid a) => Bool -> m a -> m a
when' b mx
| b = mx
| otherwise = return mempty
-- Exmaple configurations.
-- $examples
-- | Default EWMH window activation behavior: switch to workspace with
-- activated window and switch focus to it. Not to be used in a 'manageHook'.
activateSwitchWs :: ManageHook
activateSwitchWs = manageFocus (switchWorkspace <> switchFocus)
-- | Move activated window to current workspace. Not to be used in a 'manageHook'.
activateOnCurrent' :: ManageHook
activateOnCurrent' = currentWs >>= unlessFocusLock . doShift
-- | Move activated window to current workspace and switch focus to it. Note,
-- that i need to explicitly call 'switchFocus' here, because otherwise, when
-- activated window is /already/ on current workspace, focus won't be
-- switched. Not to be used in a 'manageHook'.
activateOnCurrentWs :: ManageHook
activateOnCurrentWs = manageFocus (newOnCur --> switchFocus) <> activateOnCurrent'
-- | Move activated window to current workspace, but keep focus unchanged.
-- Not to be used in a 'manageHook'.
activateOnCurrentKeepFocus :: ManageHook
activateOnCurrentKeepFocus = manageFocus (newOnCur --> keepFocus) <> activateOnCurrent'