Skip to content

Commit 803bc3d

Browse files
authored
Merge pull request #807 from TheMC47/sticky
X.H.StickyWindows: Stick windows on screens
2 parents 4c6f3be + c18aea3 commit 803bc3d

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
* Drop support for GHC 8.6
88

9+
### New Modules
10+
11+
* `XMonad.Util.StickyWindows`
12+
13+
- Stick windows on screens so they follow you across desktops.
14+
915
### Bug Fixes and Minor Changes
1016

1117
* `XMonad.Actions.WindowBringer`

XMonad/Util/StickyWindows.hs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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)

xmonad-contrib.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ library
406406
XMonad.Util.SpawnNamedPipe
407407
XMonad.Util.SpawnOnce
408408
XMonad.Util.Stack
409+
XMonad.Util.StickyWindows
409410
XMonad.Util.StringProp
410411
XMonad.Util.Themes
411412
XMonad.Util.Timer

0 commit comments

Comments
 (0)