{-# LANGUAGE ScopedTypeVariables #-} ----------------------------------------------------------------------------- -- | -- Module : System.Taffybar.WorkspaceSwitcher -- Copyright : (c) José A. Romero L. -- License : BSD3-style (see LICENSE) -- -- Maintainer : José A. Romero L. -- Stability : unstable -- Portability : unportable -- -- Composite widget that displays all currently configured workspaces and -- allows to switch to any of them by clicking on its label. Supports also -- urgency hints and (with an additional hook) display of other visible -- workspaces besides the active one (in Xinerama or XRandR installations). -- -- N.B. If you're just looking for a drop-in replacement for the -- "System.Taffybar.XMonadLog" widget that is clickable and doesn't require -- DBus, you may want to see first "System.Taffybar.TaffyPager". -- ----------------------------------------------------------------------------- module System.Taffybar.WorkspaceSwitcher ( -- * Usage -- $usage wspaceSwitcherNew ) where import Control.Monad import Control.Monad.IO.Class (MonadIO, liftIO) import Data.IORef import Data.List ((\\), findIndices) import Graphics.UI.Gtk hiding (get) import Graphics.X11.Xlib.Extras import System.Taffybar.Pager import System.Information.EWMHDesktopInfo type Desktop = [Workspace] data Workspace = Workspace { label :: Label , name :: String , urgent :: Bool } -- $usage -- -- This widget requires that the EwmhDesktops hook from the XMonadContrib -- project be installed in your @xmonad.hs@ file: -- -- > import XMonad.Hooks.EwmhDesktops (ewmh) -- > main = do -- > xmonad $ ewmh $ defaultConfig -- > ... -- -- Urgency hooks are not required for the urgency hints displaying to work -- (since it is also based on desktop events), but if you use @focusUrgent@ -- you may want to keep the \"@withUrgencyHook NoUrgencyHook@\" anyway. -- -- Unfortunately, in multiple monitor installations EWMH does not provide a -- way to determine what desktops are shown in secondary displays. Thus, if -- you have more than one monitor you may want to additionally install the -- "System.Taffybar.Hooks.PagerHints" hook in your @xmonad.hs@: -- -- > import System.Taffybar.Hooks.PagerHints (pagerHints) -- > main = do -- > xmonad $ ewmh $ pagerHints $ defaultConfig -- > ... -- -- Once you've properly configured @xmonad.hs@, you can use the widget in -- your @taffybar.hs@ file: -- -- > import System.Taffybar.WorkspaceSwitcher -- > main = do -- > pager <- pagerNew defaultPagerConfig -- > let wss = wspaceSwitcherNew pager -- -- now you can use @wss@ as any other Taffybar widget. -- | Create a new WorkspaceSwitcher widget that will use the given Pager as -- its source of events. wspaceSwitcherNew :: Pager -> IO Widget wspaceSwitcherNew pager = do switcher <- hBoxNew False 0 desktop <- getDesktop pager deskRef <- newIORef desktop populateSwitcher switcher deskRef let cfg = config pager activecb = activeCallback cfg deskRef redrawcb = redrawCallback pager deskRef switcher urgentcb = urgentCallback cfg deskRef subscribe pager activecb "_NET_CURRENT_DESKTOP" subscribe pager redrawcb "_NET_NUMBER_OF_DESKTOPS" subscribe pager urgentcb "WM_HINTS" return $ toWidget switcher -- | List of indices of all available workspaces. allWorkspaces :: Desktop -> [Int] allWorkspaces desktop = [0 .. length desktop - 1] -- | List of indices of all the workspaces that contain at least one window. nonEmptyWorkspaces :: IO [Int] nonEmptyWorkspaces = withDefaultCtx $ mapM getWorkspace =<< getWindows -- | Return a list of two-element tuples, one for every workspace, -- containing the Label widget used to display the name of that specific -- workspace and a String with its default (unmarked) representation. getDesktop :: Pager -> IO Desktop getDesktop pager = do names <- withDefaultCtx getWorkspaceNames labels <- toLabels $ map (hiddenWorkspace $ config pager) names return $ zipWith (\n l -> Workspace l n False) names labels -- | Take an existing Desktop IORef and update it if necessary, store the result -- in the IORef, then return True if the reference was actually updated, False -- otherwise. updateDesktop :: Pager -> IORef Desktop -> IO Bool updateDesktop pager deskRef = do wsnames <- withDefaultCtx getWorkspaceNames desktop <- readIORef deskRef if (length wsnames /= length desktop) then getDesktop pager >>= writeIORef deskRef >> return True else return False -- | Clean up the given box, then fill it up with the buttons for the current -- state of the desktop. populateSwitcher :: BoxClass box => box -> IORef Desktop -> IO () populateSwitcher switcher deskRef = do containerClear switcher desktop <- readIORef deskRef mapM_ (addButton switcher desktop) (allWorkspaces desktop) widgetShowAll switcher -- | Build a suitable callback function that can be registered as Listener -- of "_NET_CURRENT_DESKTOP" standard events. It will track the position of -- the active workspace in the desktop. activeCallback :: PagerConfig -> IORef Desktop -> Event -> IO () activeCallback cfg deskRef _ = do curr <- withDefaultCtx getVisibleWorkspaces desktop <- readIORef deskRef let visible = head curr when (urgent $ desktop !! visible) $ liftIO $ toggleUrgent deskRef visible False transition cfg desktop curr -- | Build a suitable callback function that can be registered as Listener -- of "WM_HINTS" standard events. It will display in a different color any -- workspace (other than the active one) containing one or more windows -- with its urgency hint set. urgentCallback :: PagerConfig -> IORef Desktop -> Event -> IO () urgentCallback cfg deskRef event = do desktop <- readIORef deskRef withDefaultCtx $ do let window = ev_window event isUrgent <- isWindowUrgent window when isUrgent $ do this <- getCurrentWorkspace that <- getWorkspace window when (this /= that) $ liftIO $ do toggleUrgent deskRef that True mark desktop (urgentWorkspace cfg) that -- | Build a suitable callback function that can be registered as Listener -- of "_NET_NUMBER_OF_DESKTOPS" standard events. It will handle dynamically -- adding and removing workspaces. redrawCallback :: BoxClass box => Pager -> IORef Desktop -> box -> Event -> IO () redrawCallback pager deskRef box _ = updateDesktop pager deskRef >>= \deskChanged -> when deskChanged $ postGUIAsync (populateSwitcher box deskRef) -- | Remove all children of a container. containerClear :: ContainerClass self => self -> IO () containerClear container = containerForeach container (containerRemove container) -- | Convert the given list of Strings to a list of Label widgets. toLabels :: [String] -> IO [Label] toLabels = mapM labelNewMarkup where labelNewMarkup markup = do lbl <- labelNew (Nothing :: Maybe String) labelSetMarkup lbl markup return lbl -- | Build a new clickable event box containing the Label widget that -- corresponds to the given index, and add it to the given container. addButton :: BoxClass self => self -- ^ Graphical container. -> Desktop -- ^ List of all workspace Labels available. -> Int -- ^ Index of the workspace to use. -> IO () addButton hbox desktop idx = do let index = desktop !! idx lbl = label index ebox <- eventBoxNew widgetSetName ebox $ name index eventBoxSetVisibleWindow ebox False _ <- on ebox buttonPressEvent $ switch idx containerAdd ebox lbl boxPackStart hbox ebox PackNatural 0 -- | Re-mark all workspace labels. transition :: PagerConfig -- ^ Configuration settings. -> Desktop -- ^ All available Labels with their default values. -> [Int] -- ^ Currently visible workspaces (first is active). -> IO () transition cfg desktop wss = do nonEmpty <- fmap (filter (>=0)) nonEmptyWorkspaces let urgentWs = findIndices urgent desktop allWs = (allWorkspaces desktop) \\ urgentWs nonEmptyWs = nonEmpty \\ urgentWs mapM_ (mark desktop $ hiddenWorkspace cfg) nonEmptyWs mapM_ (mark desktop $ emptyWorkspace cfg) (allWs \\ nonEmpty) mark desktop (activeWorkspace cfg) (head wss) mapM_ (mark desktop $ visibleWorkspace cfg) (tail wss) mapM_ (mark desktop $ urgentWorkspace cfg) urgentWs -- | Apply the given marking function to the Label of the workspace with -- the given index. mark :: Desktop -- ^ List of all available labels. -> (String -> String) -- ^ Marking function. -> Int -- ^ Index of the Label to modify. -> IO () mark desktop decorate idx = do let ws = desktop !! idx postGUIAsync $ labelSetMarkup (label ws) $ decorate' (name ws) where decorate' = pad . decorate pad m | m == [] = m | otherwise = ' ' : m -- | Switch to the workspace with the given index. switch :: (MonadIO m) => Int -> m Bool switch idx = do liftIO $ withDefaultCtx (switchToWorkspace idx) return True -- | Modify the Desktop inside the given IORef, so that the Workspace at the -- given index has its "urgent" flag set to the given value. toggleUrgent :: IORef Desktop -- ^ IORef to modify. -> Int -- ^ Index of the Workspace to replace. -> Bool -- ^ New value of the "urgent" flag. -> IO () toggleUrgent deskRef idx isUrgent = do desktop <- readIORef deskRef let ws = desktop !! idx unless (isUrgent == urgent ws) $ do let ws' = (desktop !! idx) { urgent = isUrgent } let (ys, zs) = splitAt idx desktop in writeIORef deskRef $ ys ++ (ws' : tail zs)