{-# LANGUAGE TupleSections #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  XMonad.Actions.WindowBringer
-- Description :  Dmenu operations to bring windows to you, and bring you to windows.
-- Copyright   :  Devin Mullins <me@twifkak.com>
-- License     :  BSD-style (see LICENSE)
--
-- Maintainer  :  Devin Mullins <me@twifkak.com>
-- Stability   :  stable
-- Portability :  unportable
--
-- dmenu operations to bring windows to you, and bring you to windows.
-- That is to say, it pops up a dmenu with window names, in case you forgot
-- where you left your XChat.
--
-----------------------------------------------------------------------------

module XMonad.Actions.WindowBringer (
                    -- * Usage
                    -- $usage
                    WindowBringerConfig(..),
                    gotoMenu, gotoMenuConfig, gotoMenu', gotoMenuArgs, gotoMenuArgs',
                    bringMenu, bringMenuConfig, bringMenu', bringMenuArgs, bringMenuArgs',
                    windowMap, windowAppMap, windowMap', bringWindow, actionMenu
                   ) where

import Control.Monad
import qualified Data.Map as M

import qualified XMonad.StackSet as W
import XMonad
import qualified XMonad as X
import XMonad.Util.Dmenu (menuMapArgs)
import XMonad.Util.NamedWindows (getName, getNameWMClass)

-- $usage
--
-- Import the module into your @xmonad.hs@:
--
-- > import XMonad.Actions.WindowBringer
--
-- and define appropriate key bindings:
--
-- > , ((modm .|. shiftMask, xK_g     ), gotoMenu)
-- > , ((modm .|. shiftMask, xK_b     ), bringMenu)
--
-- For detailed instructions on editing your key bindings, see
-- <https://xmonad.org/TUTORIAL.html#customizing-xmonad the tutorial>.

data WindowBringerConfig = WindowBringerConfig
    { WindowBringerConfig -> String
menuCommand :: String -- ^ The shell command that will handle window selection
    , WindowBringerConfig -> [String]
menuArgs :: [String] -- ^ Arguments to be passed to menuCommand
    , WindowBringerConfig -> WindowSpace -> Window -> X String
windowTitler :: X.WindowSpace -> Window -> X String -- ^ A function that produces window titles given a workspace and a window
    , WindowBringerConfig -> Window -> X Bool
windowFilter :: Window -> X Bool -- ^ Filter function to decide which windows to consider
    }

instance Default WindowBringerConfig where
    def :: WindowBringerConfig
def = WindowBringerConfig{ menuCommand :: String
menuCommand = String
"dmenu"
                             , menuArgs :: [String]
menuArgs = [String
"-i"]
                             , windowTitler :: WindowSpace -> Window -> X String
windowTitler = WindowSpace -> Window -> X String
decorateName
                             , windowFilter :: Window -> X Bool
windowFilter = \Window
_ -> Bool -> X Bool
forall a. a -> X a
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True
                             }

-- | Pops open a dmenu with window titles. Choose one, and you will be
--   taken to the corresponding workspace.
gotoMenu :: X ()
gotoMenu :: X ()
gotoMenu = WindowBringerConfig -> X ()
gotoMenuConfig WindowBringerConfig
forall a. Default a => a
def

-- | Pops open a dmenu with window titles. Choose one, and you will be
--   taken to the corresponding workspace. This version accepts a configuration
--   object.
gotoMenuConfig :: WindowBringerConfig -> X ()
gotoMenuConfig :: WindowBringerConfig -> X ()
gotoMenuConfig WindowBringerConfig
wbConfig = WindowBringerConfig -> (Window -> WindowSet -> WindowSet) -> X ()
actionMenu WindowBringerConfig
wbConfig Window -> WindowSet -> WindowSet
forall s a i l sd.
(Eq s, Eq a, Eq i) =>
a -> StackSet i l a s sd -> StackSet i l a s sd
W.focusWindow

-- | Pops open a dmenu with window titles. Choose one, and you will be
--   taken to the corresponding workspace. This version takes a list of
--   arguments to pass to dmenu.
gotoMenuArgs :: [String] -> X ()
gotoMenuArgs :: [String] -> X ()
gotoMenuArgs [String]
args = WindowBringerConfig -> X ()
gotoMenuConfig WindowBringerConfig
forall a. Default a => a
def { menuArgs = args }

-- | Pops open an application with window titles given over stdin. Choose one,
--   and you will be taken to the corresponding workspace.
gotoMenu' :: String -> X ()
gotoMenu' :: String -> X ()
gotoMenu' String
cmd = WindowBringerConfig -> X ()
gotoMenuConfig WindowBringerConfig
forall a. Default a => a
def { menuArgs = [], menuCommand = cmd }

-- | Pops open an application with window titles given over stdin. Choose one,
--   and you will be taken to the corresponding workspace. This version takes a
--   list of arguments to pass to dmenu.
gotoMenuArgs' :: String -> [String] -> X ()
gotoMenuArgs' :: String -> [String] -> X ()
gotoMenuArgs' String
cmd [String]
args = WindowBringerConfig -> X ()
gotoMenuConfig WindowBringerConfig
forall a. Default a => a
def { menuCommand = cmd, menuArgs = args }

-- | Pops open a dmenu with window titles. Choose one, and it will be
--   dragged, kicking and screaming, into your current workspace.
bringMenu :: X ()
bringMenu :: X ()
bringMenu = [String] -> X ()
bringMenuArgs [String]
forall a. Default a => a
def

-- | Pops open a dmenu with window titles. Choose one, and it will be
--   dragged, kicking and screaming, into your current workspace. This version
--   accepts a configuration object.
bringMenuConfig :: WindowBringerConfig -> X ()
bringMenuConfig :: WindowBringerConfig -> X ()
bringMenuConfig WindowBringerConfig
wbConfig = WindowBringerConfig -> (Window -> WindowSet -> WindowSet) -> X ()
actionMenu WindowBringerConfig
wbConfig Window -> WindowSet -> WindowSet
bringWindow

-- | Pops open a dmenu with window titles. Choose one, and it will be
--   dragged, kicking and screaming, into your current workspace. This version
--   takes a list of arguments to pass to dmenu.
bringMenuArgs :: [String] -> X ()
bringMenuArgs :: [String] -> X ()
bringMenuArgs [String]
args = WindowBringerConfig -> X ()
bringMenuConfig WindowBringerConfig
forall a. Default a => a
def { menuArgs = args }

-- | Pops open an application with window titles given over stdin. Choose one,
--   and it will be dragged, kicking and screaming, into your current
--   workspace.
bringMenu' :: String -> X ()
bringMenu' :: String -> X ()
bringMenu' String
cmd = WindowBringerConfig -> X ()
bringMenuConfig WindowBringerConfig
forall a. Default a => a
def { menuArgs = [], menuCommand = cmd }

-- | Pops open an application with window titles given over stdin. Choose one,
--   and it will be dragged, kicking and screaming, into your current
--   workspace. This version allows arguments to the chooser to be specified.
bringMenuArgs' :: String -> [String] -> X ()
bringMenuArgs' :: String -> [String] -> X ()
bringMenuArgs' String
cmd [String]
args = WindowBringerConfig -> X ()
bringMenuConfig WindowBringerConfig
forall a. Default a => a
def { menuArgs = args, menuCommand = cmd }

-- | Brings the specified window into the current workspace.
bringWindow :: Window -> X.WindowSet -> X.WindowSet
bringWindow :: Window -> WindowSet -> WindowSet
bringWindow Window
w WindowSet
ws = String -> Window -> WindowSet -> WindowSet
forall a s i l sd.
(Ord a, Eq s, Eq i) =>
i -> a -> StackSet i l a s sd -> StackSet i l a s sd
W.shiftWin (WindowSet -> String
forall i l a s sd. StackSet i l a s sd -> i
W.currentTag WindowSet
ws) Window
w WindowSet
ws

-- | Calls dmenuMap to grab the appropriate Window, and hands it off to action
--   if found.
actionMenu :: WindowBringerConfig -> (Window -> X.WindowSet -> X.WindowSet) -> X ()
actionMenu :: WindowBringerConfig -> (Window -> WindowSet -> WindowSet) -> X ()
actionMenu c :: WindowBringerConfig
c@WindowBringerConfig{ menuCommand :: WindowBringerConfig -> String
menuCommand = String
cmd, menuArgs :: WindowBringerConfig -> [String]
menuArgs = [String]
args } Window -> WindowSet -> WindowSet
action =
  WindowBringerConfig -> X (Map String Window)
windowMap' WindowBringerConfig
c X (Map String Window)
-> (Map String Window -> X (Maybe Window)) -> X (Maybe Window)
forall a b. X a -> (a -> X b) -> X b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Map String Window -> X (Maybe Window)
forall a. Map String a -> X (Maybe a)
menuMapFunction X (Maybe Window) -> (Maybe Window -> X ()) -> X ()
forall a b. X a -> (a -> X b) -> X b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= (Maybe Window -> (Window -> X ()) -> X ())
-> (Window -> X ()) -> Maybe Window -> X ()
forall a b c. (a -> b -> c) -> b -> a -> c
flip Maybe Window -> (Window -> X ()) -> X ()
forall (m :: * -> *) a. Monad m => Maybe a -> (a -> m ()) -> m ()
X.whenJust ((WindowSet -> WindowSet) -> X ()
windows ((WindowSet -> WindowSet) -> X ())
-> (Window -> WindowSet -> WindowSet) -> Window -> X ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Window -> WindowSet -> WindowSet
action)
    where
      menuMapFunction :: M.Map String a -> X (Maybe a)
      menuMapFunction :: forall a. Map String a -> X (Maybe a)
menuMapFunction = String -> [String] -> Map String a -> X (Maybe a)
forall (m :: * -> *) a.
MonadIO m =>
String -> [String] -> Map String a -> m (Maybe a)
menuMapArgs String
cmd [String]
args


-- | A map from window names to Windows.
windowMap :: X (M.Map String Window)
windowMap :: X (Map String Window)
windowMap = WindowBringerConfig -> X (Map String Window)
windowMap' WindowBringerConfig
forall a. Default a => a
def

-- | A map from application executable names to Windows.
windowAppMap :: X (M.Map String Window)
windowAppMap :: X (Map String Window)
windowAppMap = WindowBringerConfig -> X (Map String Window)
windowMap' WindowBringerConfig
forall a. Default a => a
def { windowTitler = decorateAppName }

-- | A map from window names to Windows, given a windowTitler function.
windowMap' :: WindowBringerConfig -> X (M.Map String Window)
windowMap' :: WindowBringerConfig -> X (Map String Window)
windowMap' WindowBringerConfig{ windowTitler :: WindowBringerConfig -> WindowSpace -> Window -> X String
windowTitler = WindowSpace -> Window -> X String
titler, windowFilter :: WindowBringerConfig -> Window -> X Bool
windowFilter = Window -> X Bool
include } = do
  WindowSet
windowSet <- (XState -> WindowSet) -> X WindowSet
forall s (m :: * -> *) a. MonadState s m => (s -> a) -> m a
gets XState -> WindowSet
X.windowset
  [(String, Window)] -> Map String Window
forall k a. Ord k => [(k, a)] -> Map k a
M.fromList ([(String, Window)] -> Map String Window)
-> ([[(String, Window)]] -> [(String, Window)])
-> [[(String, Window)]]
-> Map String Window
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [[(String, Window)]] -> [(String, Window)]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat ([[(String, Window)]] -> Map String Window)
-> X [[(String, Window)]] -> X (Map String Window)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (WindowSpace -> X [(String, Window)])
-> [WindowSpace] -> X [[(String, Window)]]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> [a] -> m [b]
mapM WindowSpace -> X [(String, Window)]
keyValuePairs (WindowSet -> [WindowSpace]
forall i l a s sd. StackSet i l a s sd -> [Workspace i l a]
W.workspaces WindowSet
windowSet)
 where keyValuePairs :: WindowSpace -> X [(String, Window)]
keyValuePairs WindowSpace
ws = let wins :: [Window]
wins = Maybe (Stack Window) -> [Window]
forall a. Maybe (Stack a) -> [a]
W.integrate' (WindowSpace -> Maybe (Stack Window)
forall i l a. Workspace i l a -> Maybe (Stack a)
W.stack WindowSpace
ws)
                           in (Window -> X (String, Window)) -> [Window] -> X [(String, Window)]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> [a] -> m [b]
mapM (WindowSpace -> Window -> X (String, Window)
keyValuePair WindowSpace
ws) ([Window] -> X [(String, Window)])
-> X [Window] -> X [(String, Window)]
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< (Window -> X Bool) -> [Window] -> X [Window]
forall (m :: * -> *) a.
Applicative m =>
(a -> m Bool) -> [a] -> m [a]
filterM Window -> X Bool
include [Window]
wins
       keyValuePair :: WindowSpace -> Window -> X (String, Window)
keyValuePair WindowSpace
ws Window
w = (, Window
w) (String -> (String, Window)) -> X String -> X (String, Window)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> WindowSpace -> Window -> X String
titler WindowSpace
ws Window
w

-- | Returns the window name as will be listed in dmenu.
--   Tagged with the workspace ID, to guarantee uniqueness, and to let the user
--   know where he's going.
decorateName :: X.WindowSpace -> Window -> X String
decorateName :: WindowSpace -> Window -> X String
decorateName WindowSpace
ws Window
w = do
  String
name <- NamedWindow -> String
forall a. Show a => a -> String
show (NamedWindow -> String) -> X NamedWindow -> X String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Window -> X NamedWindow
getName Window
w
  String -> X String
forall a. a -> X a
forall (m :: * -> *) a. Monad m => a -> m a
return (String -> X String) -> String -> X String
forall a b. (a -> b) -> a -> b
$ String
name String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
" [" String -> String -> String
forall a. [a] -> [a] -> [a]
++ WindowSpace -> String
forall i l a. Workspace i l a -> i
W.tag WindowSpace
ws String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"]"

-- | Returns the window name as will be listed in dmenu.  This will
-- return the executable name of the window along with it's workspace
-- ID.
decorateAppName :: X.WindowSpace -> Window -> X String
decorateAppName :: WindowSpace -> Window -> X String
decorateAppName WindowSpace
ws Window
w = do
  String
name <- NamedWindow -> String
forall a. Show a => a -> String
show (NamedWindow -> String) -> X NamedWindow -> X String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Window -> X NamedWindow
getNameWMClass Window
w
  String -> X String
forall a. a -> X a
forall (m :: * -> *) a. Monad m => a -> m a
return (String -> X String) -> String -> X String
forall a b. (a -> b) -> a -> b
$ String
name String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
" [" String -> String -> String
forall a. [a] -> [a] -> [a]
++ WindowSpace -> String
forall i l a. Workspace i l a -> i
W.tag WindowSpace
ws String -> String -> String
forall a. [a] -> [a] -> [a]
++ String
"]"