|
| 1 | +-- | |
| 2 | +-- Module : XMonad.Util.StickyWindows |
| 3 | +-- Description : Make windows sticky to a screen across workspace changes. |
| 4 | +-- Copyright : (c) Yecine Megdiche <[email protected]> |
| 5 | +-- License : BSD3-style (see LICENSE) |
| 6 | +-- |
| 7 | +-- Maintainer : Yecine Megdiche <[email protected]> |
| 8 | +-- Stability : unstable |
| 9 | +-- Portability : unportable |
| 10 | +-- |
| 11 | +-- This module provides functionality to make windows \"sticky\" to a particular |
| 12 | +-- screen. When a window is marked as sticky on a screen, it will automatically |
| 13 | +-- follow that screen across workspace changes, staying visible even when you |
| 14 | +-- switch to a different workspace. |
| 15 | +-- |
| 16 | +-- This is particularly useful for windows you want to keep visible at all times |
| 17 | +-- on a specific monitor, such as Picture-in-Picture videos, music players, |
| 18 | +-- communication apps, or reference documentation. |
| 19 | +module XMonad.Util.StickyWindows ( |
| 20 | + -- * Usage |
| 21 | + -- $usage |
| 22 | + sticky, |
| 23 | + stick, |
| 24 | + unstick, |
| 25 | +) where |
| 26 | + |
| 27 | +import qualified Data.Map as M |
| 28 | +import qualified Data.Set as S |
| 29 | +import XMonad |
| 30 | +import XMonad.Prelude |
| 31 | +import qualified XMonad.StackSet as W |
| 32 | +import qualified XMonad.Util.ExtensibleState as XS |
| 33 | + |
| 34 | +-- $usage |
| 35 | +-- You can use this module with the following in your @xmonad.hs@: |
| 36 | +-- |
| 37 | +-- > import XMonad.Util.StickyWindows |
| 38 | +-- |
| 39 | +-- To enable sticky windows, wrap your config with 'sticky': |
| 40 | +-- |
| 41 | +-- > main = xmonad $ … . sticky . … $ def { ... } |
| 42 | +-- |
| 43 | +-- This adds the necessary hooks to manage sticky windows. Next, add keybindings |
| 44 | +-- to stick and unstick windows: |
| 45 | +-- |
| 46 | +-- > , ((modMask, xK_s), withFocused stick) |
| 47 | +-- > , ((modMask .|. shiftMask, xK_s), withFocused unstick) |
| 48 | +-- |
| 49 | +-- Now you can: |
| 50 | +-- |
| 51 | +-- 1. Focus a window and press @Mod-s@ to make it sticky to the current screen |
| 52 | +-- 2. Switch workspaces on that screen, and the sticky window will follow |
| 53 | +-- 3. Press @Mod-Shift-s@ to unstick the window |
| 54 | +-- |
| 55 | +-- Note that windows are sticky to a /specific screen/, not to all screens. If you |
| 56 | +-- have multiple monitors, a window marked sticky on screen 0 will only follow |
| 57 | +-- workspace changes on screen 0, not on other screens. |
| 58 | +-- |
| 59 | +-- The sticky state persists across XMonad restarts. |
| 60 | + |
| 61 | +data StickyState = SS |
| 62 | + { lastWs :: !(M.Map ScreenId WorkspaceId) |
| 63 | + , stickies :: !(M.Map ScreenId (S.Set Window)) |
| 64 | + } |
| 65 | + deriving (Show, Read) |
| 66 | + |
| 67 | +instance ExtensionClass StickyState where |
| 68 | + initialValue = SS mempty mempty |
| 69 | + extensionType = PersistentExtension |
| 70 | + |
| 71 | +modifySticky :: |
| 72 | + (S.Set Window -> S.Set Window) -> ScreenId -> StickyState -> StickyState |
| 73 | +modifySticky f sid (SS ws ss) = |
| 74 | + SS ws $ M.alter (Just . f . fromMaybe S.empty) sid ss |
| 75 | + |
| 76 | +modifyStickyM :: (S.Set Window -> S.Set Window) -> ScreenId -> X () |
| 77 | +modifyStickyM f sid = XS.modify (modifySticky f sid) |
| 78 | + |
| 79 | +stick' :: Window -> ScreenId -> X () |
| 80 | +stick' = modifyStickyM . S.insert |
| 81 | + |
| 82 | +unstick' :: Window -> ScreenId -> X () |
| 83 | +unstick' = modifyStickyM . S.delete |
| 84 | + |
| 85 | +-- | Remove the sticky status from the given window on the current screen. |
| 86 | +-- The window will no longer automatically follow workspace changes. |
| 87 | +-- |
| 88 | +-- Typically used with 'withFocused': |
| 89 | +-- |
| 90 | +-- > , ((modMask .|. shiftMask, xK_s), withFocused unstick) |
| 91 | +unstick :: Window -> X () |
| 92 | +unstick w = unstick' w =<< currentScreen |
| 93 | + |
| 94 | +-- | Mark the given window as sticky to the current screen. The window will |
| 95 | +-- automatically follow this screen across workspace changes until explicitly |
| 96 | +-- unstuck with 'unstick' or until the window is destroyed. |
| 97 | +-- |
| 98 | +-- Typically used with 'withFocused': |
| 99 | +-- |
| 100 | +-- > , ((modMask, xK_s), withFocused stick) |
| 101 | +stick :: Window -> X () |
| 102 | +stick w = stick' w =<< currentScreen |
| 103 | + |
| 104 | +currentScreen :: X ScreenId |
| 105 | +currentScreen = gets $ W.screen . W.current . windowset |
| 106 | + |
| 107 | +-- | Incorporates sticky window functionality into an 'XConfig'. This adds |
| 108 | +-- the necessary log hook and event hook to: |
| 109 | +-- |
| 110 | +-- * Automatically move sticky windows when workspaces change on their screen |
| 111 | +-- * Clean up sticky state when windows are destroyed |
| 112 | +-- |
| 113 | +-- Example usage: |
| 114 | +-- |
| 115 | +-- > main = xmonad $ … . sticky . … $ def { ... } |
| 116 | +sticky :: XConfig l -> XConfig l |
| 117 | +sticky xconf = |
| 118 | + xconf |
| 119 | + { logHook = logHook xconf >> stickyLogHook |
| 120 | + , handleEventHook = handleEventHook xconf <> stickyEventHook |
| 121 | + } |
| 122 | + |
| 123 | +stickyLogHook :: X () |
| 124 | +stickyLogHook = do |
| 125 | + lastWS_ <- XS.gets lastWs |
| 126 | + screens <- withWindowSet $ return . map (\s -> (W.screen s, W.tag . W.workspace $ s)) . W.screens |
| 127 | + for_ screens $ \(sid, wsTag) -> do |
| 128 | + unless (M.lookup sid lastWS_ == Just wsTag) $ |
| 129 | + -- We need to update the last workspace before moving windows to avoid |
| 130 | + -- getting stuck in a loop: This is a log hook, and calling moveWindows |
| 131 | + -- (which in turn calls 'windows') would trigger another log hook. |
| 132 | + XS.modify (\(SS ws ss) -> SS (M.insert sid wsTag ws) ss) |
| 133 | + >> XS.gets (M.lookup sid . stickies) |
| 134 | + >>= maybe mempty (moveWindows wsTag) |
| 135 | + |
| 136 | +moveWindows :: WorkspaceId -> S.Set Window -> X () |
| 137 | +moveWindows wsTag = traverse_ (\w -> windows $ W.focusDown . W.shiftWin wsTag w) |
| 138 | + |
| 139 | +stickyEventHook :: Event -> X All |
| 140 | +stickyEventHook DestroyWindowEvent{ev_window = w} = |
| 141 | + XS.modify (\(SS ws ss) -> SS ws (M.map (S.delete w) ss)) $> All True |
| 142 | +stickyEventHook _ = return (All True) |
0 commit comments