{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances #-}
{-# LANGUAGE PatternGuards #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  XMonad.Layout.Stoppable
-- Description :  A layout modifier to stop all non-visible processes.
-- 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 special kind of layout modifier, which when
-- applied to a layout, causes xmonad to stop all non-visible processes.
-- In a way, this is a sledge-hammer for applications that drain power.
-- For example, given a web browser on a stoppable workspace, once the
-- workspace is hidden the web browser will be stopped.
--
-- Note that the stopped application won't be able to communicate with X11
-- clipboard. For this, the module actually stops applications after a
-- certain delay, giving a chance for a user to complete copy-paste
-- sequence. By default, the delay equals to 15 seconds, it is
-- configurable via 'Stoppable' constructor.
--
-- The stoppable modifier prepends a mark (by default equals to
-- \"Stoppable\") to the layout description (alternatively, you can choose
-- your own mark and use it with 'Stoppable' constructor). The stoppable
-- layout (identified by a mark) spans to multiple workspaces, letting you
-- to create groups of stoppable workspaces that only stop processes when
-- none of the workspaces are visible, and conversely, unfreezing all
-- processes even if one of the stoppable workspaces are visible.
--
-- To stop the process we use signals, which works for most cases. For
-- processes that tinker with signal handling (debuggers), another
-- (Linux-centric) approach may be used. See
-- <https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt>
--
-- * Note
-- This module doesn't work on programs that do fancy things with processes
-- (such as Chromium) and programs that do not set _NET_WM_PID.
-----------------------------------------------------------------------------

module XMonad.Layout.Stoppable
    ( -- $usage
      Stoppable(..)
    , stoppable
    ) where

import XMonad
import XMonad.Prelude
import XMonad.Actions.WithAll
import XMonad.Util.WindowProperties
import XMonad.Util.RemoteWindows
import XMonad.Util.Timer
import XMonad.StackSet hiding (filter)
import XMonad.Layout.LayoutModifier
import System.Posix.Signals

-- $usage
-- You can use this module with the following in your @xmonad.hs@:
--
-- > import XMonad
-- > import XMonad.Layout.Stoppable
-- >
-- > main = xmonad def
-- >    { layoutHook = layoutHook def ||| stoppable (layoutHook def) }
--
-- Note that the module has to distinguish between local and remote
-- proccesses, which means that it needs to know the hostname, so it looks
-- for environment variables (e.g. HOST).
--
-- Environment variables will work for most cases, but won't work if the
-- hostname changes. To cover dynamic hostnames case, in addition to
-- layoutHook you have to provide manageHook from
-- "XMonad.Util.RemoteWindows" module.
--
-- For more detailed instructions on editing the layoutHook see
-- <https://xmonad.org/TUTORIAL.html#customizing-xmonad the tutorial> and
-- "XMonad.Doc.Extending#Editing_the_layout_hook".

signalWindow :: Signal -> Window -> X ()
signalWindow :: Signal -> Window -> X ()
signalWindow Signal
s Window
w = do
    Maybe [CLong]
pid <- String -> Window -> X (Maybe [CLong])
getProp32s String
"_NET_WM_PID" Window
w
    forall (m :: * -> *) a. MonadIO m => IO a -> m a
io forall a b. (a -> b) -> a -> b
$ (Signal -> ProcessID -> IO ()
signalProcess Signal
s forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (Integral a, Num b) => a -> b
fromIntegral) forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
`mapM_` forall a. a -> Maybe a -> a
fromMaybe [] Maybe [CLong]
pid

signalLocalWindow :: Signal -> Window -> X ()
signalLocalWindow :: Signal -> Window -> X ()
signalLocalWindow Signal
s Window
w  = Window -> X Bool
isLocalWindow Window
w forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= forall a b c. (a -> b -> c) -> b -> a -> c
flip forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Signal -> Window -> X ()
signalWindow Signal
s Window
w)

withAllOn :: (a -> X ()) -> Workspace i l a -> X ()
withAllOn :: forall a i l. (a -> X ()) -> Workspace i l a -> X ()
withAllOn a -> X ()
f Workspace i l a
wspc = a -> X ()
f forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
`mapM_` forall a. Maybe (Stack a) -> [a]
integrate' (forall i l a. Workspace i l a -> Maybe (Stack a)
stack Workspace i l a
wspc)

withAllFiltered :: (Workspace i l a -> Bool)
                -> [Workspace i l a]
                -> (a -> X ()) -> X ()
withAllFiltered :: forall i l a.
(Workspace i l a -> Bool)
-> [Workspace i l a] -> (a -> X ()) -> X ()
withAllFiltered Workspace i l a -> Bool
p [Workspace i l a]
wspcs a -> X ()
f = forall a i l. (a -> X ()) -> Workspace i l a -> X ()
withAllOn a -> X ()
f forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
`mapM_` forall a. (a -> Bool) -> [a] -> [a]
filter Workspace i l a -> Bool
p [Workspace i l a]
wspcs

sigStoppableWorkspacesHook :: String -> X ()
sigStoppableWorkspacesHook :: String -> X ()
sigStoppableWorkspacesHook String
k = do
    WindowSet
ws <- forall s (m :: * -> *) a. MonadState s m => (s -> a) -> m a
gets XState -> WindowSet
windowset
    forall i l a.
(Workspace i l a -> Bool)
-> [Workspace i l a] -> (a -> X ()) -> X ()
withAllFiltered forall {layout :: * -> *} {a} {i} {a}.
LayoutClass layout a =>
Workspace i (layout a) a -> Bool
isStoppable (forall i l a sid sd. StackSet i l a sid sd -> [Workspace i l a]
hidden WindowSet
ws) (Signal -> Window -> X ()
signalLocalWindow Signal
sigSTOP)
  where
    isStoppable :: Workspace i (layout a) a -> Bool
isStoppable Workspace i (layout a) a
ws = String
k forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` String -> [String]
words (forall (layout :: * -> *) a.
LayoutClass layout a =>
layout a -> String
description forall a b. (a -> b) -> a -> b
$ forall i l a. Workspace i l a -> l
layout Workspace i (layout a) a
ws)

-- | Data type for ModifiedLayout. The constructor lets you to specify a
-- custom mark/description modifier and a delay. You can also use
-- 'stoppable' helper function.
data Stoppable a = Stoppable
    { forall a. Stoppable a -> String
mark :: String
    , forall a. Stoppable a -> Rational
delay :: Rational
    , forall a. Stoppable a -> Maybe TimerId
timer :: Maybe TimerId
    } deriving (TimerId -> Stoppable a -> ShowS
forall a. TimerId -> Stoppable a -> ShowS
forall a. [Stoppable a] -> ShowS
forall a. Stoppable a -> String
forall a.
(TimerId -> a -> ShowS)
-> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Stoppable a] -> ShowS
$cshowList :: forall a. [Stoppable a] -> ShowS
show :: Stoppable a -> String
$cshow :: forall a. Stoppable a -> String
showsPrec :: TimerId -> Stoppable a -> ShowS
$cshowsPrec :: forall a. TimerId -> Stoppable a -> ShowS
Show,ReadPrec [Stoppable a]
ReadPrec (Stoppable a)
ReadS [Stoppable a]
forall a. ReadPrec [Stoppable a]
forall a. ReadPrec (Stoppable a)
forall a. TimerId -> ReadS (Stoppable a)
forall a. ReadS [Stoppable a]
forall a.
(TimerId -> ReadS a)
-> ReadS [a] -> ReadPrec a -> ReadPrec [a] -> Read a
readListPrec :: ReadPrec [Stoppable a]
$creadListPrec :: forall a. ReadPrec [Stoppable a]
readPrec :: ReadPrec (Stoppable a)
$creadPrec :: forall a. ReadPrec (Stoppable a)
readList :: ReadS [Stoppable a]
$creadList :: forall a. ReadS [Stoppable a]
readsPrec :: TimerId -> ReadS (Stoppable a)
$creadsPrec :: forall a. TimerId -> ReadS (Stoppable a)
Read)

instance LayoutModifier Stoppable Window where
    modifierDescription :: Stoppable Window -> String
modifierDescription = forall a. Stoppable a -> String
mark

    hook :: Stoppable Window -> X ()
hook Stoppable Window
_   = (Window -> X ()) -> X ()
withAll forall a b. (a -> b) -> a -> b
$ Signal -> Window -> X ()
signalLocalWindow Signal
sigCONT

    handleMess :: Stoppable Window -> SomeMessage -> X (Maybe (Stoppable Window))
handleMess (Stoppable String
m Rational
_ (Just TimerId
tid)) SomeMessage
msg
        | Just Event
ev <- forall m. Message m => SomeMessage -> Maybe m
fromMessage SomeMessage
msg = forall a. TimerId -> Event -> X (Maybe a) -> X (Maybe a)
handleTimer TimerId
tid Event
ev forall {a}. X (Maybe a)
run
          where run :: X (Maybe a)
run = String -> X ()
sigStoppableWorkspacesHook String
m forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall (m :: * -> *) a. Monad m => a -> m a
return forall a. Maybe a
Nothing
    handleMess (Stoppable String
m Rational
d Maybe TimerId
_) SomeMessage
msg
        | Just LayoutMessages
Hide <- forall m. Message m => SomeMessage -> Maybe m
fromMessage SomeMessage
msg =
            forall a. a -> Maybe a
Just forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. String -> Rational -> Maybe TimerId -> Stoppable a
Stoppable String
m Rational
d forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. a -> Maybe a
Just forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Rational -> X TimerId
startTimer Rational
d
        | Bool
otherwise = forall (m :: * -> *) a. Monad m => a -> m a
return forall a. Maybe a
Nothing

-- | Convert a layout to a stoppable layout using the default mark
-- (\"Stoppable\") and a delay of 15 seconds.
stoppable :: l a -> ModifiedLayout Stoppable l a
stoppable :: forall (l :: * -> *) a. l a -> ModifiedLayout Stoppable l a
stoppable = forall (m :: * -> *) (l :: * -> *) a.
m a -> l a -> ModifiedLayout m l a
ModifiedLayout (forall a. String -> Rational -> Maybe TimerId -> Stoppable a
Stoppable String
"Stoppable" Rational
15 forall a. Maybe a
Nothing)