{-# LANGUAGE NamedFieldPuns, GeneralizedNewtypeDeriving #-} ----------------------------------------------------------------------------- -- | -- Module : XMonad.Actions.MostRecentlyUsed -- Description : Tab through windows by recency of use. -- Copyright : (c) 2022 L. S. Leary -- License : BSD3-style (see LICENSE) -- -- Maintainer : @LSLeary (on github) -- Stability : unstable -- Portability : unportable -- -- Based on the Alt+Tab behaviour common outside of xmonad. -- ----------------------------------------------------------------------------- -- --< Imports & Exports >-- {{{ module XMonad.Actions.MostRecentlyUsed ( -- * Usage -- $usage -- * Interface configureMRU, mostRecentlyUsed, withMostRecentlyUsed, Location(..), ) where -- base import Data.List.NonEmpty (nonEmpty) import Data.IORef (newIORef, readIORef, writeIORef, modifyIORef) import Control.Monad.IO.Class (MonadIO) -- mtl import Control.Monad.Trans (lift) import Control.Monad.State (get, put, gets) -- containers import qualified Data.Map.Strict as M -- xmonad import XMonad ( Window, KeySym, keyPress, io , Event (DestroyWindowEvent, UnmapEvent, ev_send_event, ev_window) ) import XMonad.Core ( X, XConfig(..), windowset, WorkspaceId, ScreenId , ExtensionClass(..), StateExtension(..) , waitingUnmap ) import XMonad.Operations (screenWorkspace) import qualified XMonad.StackSet as W -- xmonad-contrib import qualified XMonad.Util.ExtensibleConf as XC import qualified XMonad.Util.ExtensibleState as XS import XMonad.Util.PureX (handlingRefresh, curScreenId, curTag, greedyView, view, peek, focusWindow) import XMonad.Util.History (History, origin, event, erase, ledger) import XMonad.Actions.Repeatable (repeatableSt) import XMonad.Prelude -- }}} -- --< Core Data Types: WindowHistory & Location >-- {{{ data WindowHistory = WinHist { busy :: !Bool , hist :: !(History Window Location) } deriving (Show, Read) instance ExtensionClass WindowHistory where initialValue = WinHist { busy = False , hist = origin } extensionType = PersistentExtension data Location = Location { workspace :: !WorkspaceId , screen :: !ScreenId } deriving (Show, Read, Eq, Ord) -- }}} -- --< Interface >-- {{{ -- $usage -- -- 'configureMRU' must be applied to your config in order for 'mostRecentlyUsed' -- to work. -- -- > main :: IO () -- > main = xmonad . configureMRU . ... $ def -- > { ... -- > } -- -- Once that's done, it can be used normally in keybinds: -- -- > , ((mod1Mask, xK_Tab), mostRecentlyUsed [xK_Alt_L, xK_Alt_R] xK_Tab) -- -- N.B.: This example assumes that 'mod1Mask' corresponds to alt, which is not -- always the case, depending on how your system is configured. -- | Configure xmonad to support 'mostRecentlyUsed'. configureMRU :: XConfig l -> XConfig l configureMRU = XC.once f (MRU ()) where f cnf = cnf { logHook = logHook cnf <> logWinHist , handleEventHook = handleEventHook cnf <> winHistEH } newtype MRU = MRU () deriving Semigroup -- | An action to browse through the history of focused windows, taking -- another step back with each tap of the key. mostRecentlyUsed :: [KeySym] -- ^ The 'KeySym's corresponding to the modifier to which the -- action is bound. -> KeySym -- ^ The 'KeySym' corresponding to the key to which the action -- is bound. -> X () mostRecentlyUsed mods key = do (toUndo, undo) <- undoer let undoably curThing withThing thing = curThing >>= \cur -> when (cur /= thing) $ withThing thing >> toUndo (withThing cur) withMostRecentlyUsed mods key $ \win Location{workspace,screen} -> handlingRefresh $ do undo undoably curScreenId viewScreen screen undoably curTag greedyView workspace mi <- gets (W.findTag win . windowset) for_ mi $ \i -> do undoably curTag greedyView i mfw <- peek for_ mfw $ \fw -> do undoably (pure fw) focusWindow win where undoer :: (MonadIO m, Monoid a) => m (m a -> m (), m a) undoer = do ref <- io . newIORef $ pure mempty let toUndo = io . modifyIORef ref . liftA2 (<>) undo = join (io $ readIORef ref) <* io (writeIORef ref $ pure mempty) pure (toUndo, undo) viewScreen :: ScreenId -> X Any viewScreen scr = screenWorkspace scr >>= foldMap view -- | A version of 'mostRecentlyUsed' that allows you to customise exactly what -- is done with each window you tab through (the default being to visit its -- previous 'Location' and give it focus). withMostRecentlyUsed :: [KeySym] -- ^ The 'KeySym's corresponding to the -- modifier to which the action is bound. -> KeySym -- ^ The 'KeySym' corresponding to the key to -- which the action is bound. -> (Window -> Location -> X ()) -- ^ The function applied to each window. -> X () withMostRecentlyUsed mods tab preview = do wh@WinHist{busy,hist} <- XS.get unless busy $ do XS.put wh{ busy = True } for_ (nonEmpty $ ledger hist) $ \ne -> do mfw <- gets (W.peek . windowset) let iSt = case cycleS ne of (w, _) :~ s | mfw == Just w -> s s -> s repeatableSt iSt mods tab $ \t s -> when (t == keyPress && s == tab) (pop >>= lift . uncurry preview) XS.modify $ \ws@WinHist{} -> ws{ busy = False } logWinHist where pop = do h :~ t <- get put t $> h -- }}} -- --< Raw Config >-- {{{ logWinHist :: X () logWinHist = do wh@WinHist{busy,hist} <- XS.get unless busy $ do cs <- gets (W.current . windowset) let cws = W.workspace cs for_ (W.stack cws) $ \st -> do let location = Location{ workspace = W.tag cws, screen = W.screen cs } XS.put wh{ hist = event (W.focus st) location hist } winHistEH :: Event -> X All winHistEH ev = All True <$ case ev of UnmapEvent{ ev_send_event = synth, ev_window = w } -> do e <- gets (fromMaybe 0 . M.lookup w . waitingUnmap) when (synth || e == 0) (collect w) DestroyWindowEvent{ ev_window = w } -> collect w _ -> pure () where collect w = XS.modify $ \wh@WinHist{hist} -> wh{ hist = erase w hist } -- }}}