Files
dotfiles/.config/xmonad/xmonad.hs
T
Daniele Fucini 3e5824a590 Keep the games workspace on the HDMI screen
Wine games lose the mouse when their window moves between screens. safeView replaces greedyView for mod-N, xmobar clicks and mpv so workspace 3 always opens on screen 0 and is never swapped away; safeRescreen keeps a hidden workspace 3 off the laptop screen when screens change.
2026-10-24 20:27:52 +02:00

346 lines
14 KiB
Haskell

-- Import
import Data.Monoid
import Graphics.X11.ExtraTypes.XF86
import Control.Monad (forM_, liftM2, when)
import XMonad
import XMonad.Actions.OnScreen (greedyViewOnScreen)
import XMonad.Actions.PhysicalScreens
import XMonad.Hooks.EwmhDesktops
import XMonad.Hooks.ManageDocks
import XMonad.Hooks.ManageHelpers
import XMonad.Hooks.Rescreen
import XMonad.Hooks.StatusBar
import XMonad.Hooks.StatusBar.PP
import XMonad.Layout.Grid
import XMonad.Layout.Magnifier
import XMonad.Layout.NoBorders
import XMonad.Layout.PerWorkspace
import XMonad.Layout.Renamed
import XMonad.Layout.Spacing
import XMonad.Layout.Spiral
import XMonad.Layout.Tabbed
import XMonad.Layout.ThreeColumns
import qualified XMonad.StackSet as W
import XMonad.Util.ClickableWorkspaces
import XMonad.Util.EZConfig
import qualified XMonad.Util.Hacks as Hacks
import XMonad.Util.Loggers
import XMonad.Util.NamedScratchpad
import XMonad.Util.Run (runProcessWithInput)
import XMonad.Util.SpawnOnce
-- Main
main :: IO ()
main =
xmonad
. ewmhFullscreen
. setEwmhSwitchDesktopHook safeView
. ewmh
. docks
. rescreenHook def {afterRescreenHook = restartStatusBars, rescreenWorkspacesHook = Last (Just safeRescreen)}
. addRandrChangeHook (spawn myScreenHotplug)
$ myConfig
-- Variables
myModMask :: KeyMask
myModMask = mod4Mask
myTerminal :: String
myTerminal = "kitty"
myBrowser :: String
myBrowser = "zen-browser"
myNormBorderColor :: String
myNormBorderColor = "#1f2335"
myFocusBorderColor :: String
myFocusBorderColor = "#41a6b5"
myKeyboardLayoutIT :: String
myKeyboardLayoutIT = " -layout it"
myKeyboardLayoutUS :: String
myKeyboardLayoutUS = " -layout us"
myKeyboardLayoutSK :: String
myKeyboardLayoutSK = " -layout sk -variant qwerty"
myScreenshotDir :: String
myScreenshotDir = "/home/fuxino/Pictures/Screenshots/"
myScreenshotName :: String
myScreenshotName = "Screenshot-%Y-%m-%d-%H%M%S.png"
-- Sets up the outputs, the wallpaper and trayer; shared with ~/.local/bin/movie
myScreenLayout :: String
myScreenLayout = "/home/fuxino/.local/bin/screen-layout"
-- Safe to re-run (e.g. spawnOnce after this string changes and xmonad restarts)
myScreenSetup :: String
myScreenSetup =
"sleep 1; " ++ myScreenLayout ++ "; pgrep -x picom > /dev/null || picom -b -c"
-- Run when a monitor is (dis)connected; restarts trayer so it moves to the right screen
myScreenHotplug :: String
myScreenHotplug = myScreenLayout
myScratchpads :: [NamedScratchpad]
myScratchpads =
[ NS "Riseup-vpn" "riseup-vpn" (className =? "RiseupVPN") (customFloating $ W.RationalRect 0.15 0.4 0.15 0.4),
NS "Terminal" spawnTerminal (className =? "scratch-terminal") (customFloating $ W.RationalRect 0.2 0.2 0.6 0.6),
NS "Music" spawnMusic (className =? "scratch-music") (customFloating $ W.RationalRect 0.2 0.2 0.6 0.6),
NS "Mail" spawnMail (className =? "scratch-mail") (customFloating $ W.RationalRect 0.2 0.2 0.6 0.6),
NS "Cal" spawnCal (className =? "scratch-cal") (customFloating $ W.RationalRect 0.2 0.2 0.6 0.6),
NS "Btop" spawnBtop (className =? "scratch-btop") (customFloating $ W.RationalRect 0.2 0.2 0.6 0.6)
]
where
spawnTerminal = myTerminal ++ " --class scratch-terminal -T kitty-float"
spawnMusic = myTerminal ++ " --class scratch-music -T ncmpcpp ncmpcpp"
spawnMail = myTerminal ++ " --class scratch-mail -T mutt bash -c 'pushd ~/Downloads > /dev/null; mutt; popd > /dev/null'"
spawnCal = myTerminal ++ " --class scratch-cal -T calcurse calcurse"
spawnBtop = myTerminal ++ " --class scratch-btop -T btop btop"
-- Config
myConfig =
def
{ modMask = myModMask,
terminal = myTerminal,
workspaces = myWorkspaces,
focusedBorderColor = myFocusBorderColor,
normalBorderColor = myNormBorderColor,
layoutHook = myLayoutHook,
startupHook = myStartupHook,
manageHook = myManageHook,
handleEventHook = myHandleEventHook,
logHook = myLogHook
}
`additionalKeysP` [ ("M-b", sendMessage ToggleStruts),
("M-S-l", spawn "slock"),
("<Print>", unGrab *> spawn ("scrot " ++ myScreenshotDir ++ myScreenshotName)),
("S-<Print>", unGrab *> spawn ("scrot -s " ++ myScreenshotDir ++ myScreenshotName)),
("M-S-<Print>", unGrab *> spawn ("scrot -u " ++ myScreenshotDir ++ myScreenshotName)),
("M-d", spawn "dmenu_run"),
("M-p", spawn "passmenu -i"),
("M-f", sendMessage $ JumpToLayout "Tabbed"),
("M-i", spawn $ "setxkbmap" ++ myKeyboardLayoutIT),
("M-u", spawn $ "setxkbmap" ++ myKeyboardLayoutUS),
("M-s", spawn $ "setxkbmap" ++ myKeyboardLayoutSK),
("M-S-v", namedScratchpadAction myScratchpads "Riseup-vpn"),
("M-S-s", namedScratchpadAction myScratchpads "Terminal"),
("M-S-a", namedScratchpadAction myScratchpads "Cal"),
("M-o", namedScratchpadAction myScratchpads "Btop"),
("M-x", spawn "bluetoothctl connect F4:9D:8A:B9:8C:EE"),
("M-S-x", spawn "bluetoothctl disconnect F4:9D:8A:B9:8C:EE")
]
`additionalKeys` [ ((0, xF86XK_AudioMute), spawn "pactl set-sink-mute $(pactl get-default-sink) toggle"),
((0, xF86XK_AudioLowerVolume), spawn "pactl set-sink-volume $(pactl get-default-sink) -10%"),
((0, xF86XK_AudioRaiseVolume), spawn "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 10%+"),
((0, xF86XK_Calculator), spawn "qalculate-gtk"),
((0, xF86XK_HomePage), spawn myBrowser),
((0, xF86XK_Mail), namedScratchpadAction myScratchpads "Mail"),
((0, xF86XK_Tools), namedScratchpadAction myScratchpads "Music")
]
++ [ ((myModMask .|. mask, key), f sc)
| (key, sc) <- zip [xK_w, xK_e] [0 ..],
(f, mask) <- [(viewScreen def, 0), (sendToScreen def, shiftMask)]
]
++ [((myModMask, key), windows $ safeView ws) | (ws, key) <- zip myWorkspaces [xK_1 .. xK_9]]
-- greedyView that keeps wsGames on screen 0 (HDMI): Wine games lose the mouse when moved between screens
safeView :: WorkspaceId -> WindowSet -> WindowSet
safeView i ws
| i == wsGames = greedyViewOnScreen 0 i ws
| W.currentTag ws == wsGames && i `elem` map (W.tag . W.workspace) (W.visible ws) = W.view i ws
| otherwise = W.greedyView i ws
-- xmonad's rescreen, except that a hidden wsGames is never put on a secondary screen
safeRescreen :: X ()
safeRescreen = do
screens <- withDisplay getCleanedScreenInfo
case screens of
[] -> return ()
xinesc : xinescs ->
windows $ \ws ->
let notGames = (/= wsGames) . W.tag
pool = map W.workspace (W.visible ws) ++ filter notGames (W.hidden ws)
(xs, _) = splitAt (length xinescs) pool
shown = map W.tag (W.workspace (W.current ws) : xs)
in ws
{ W.current = W.Screen (W.workspace (W.current ws)) 0 (SD xinesc),
W.visible = zipWith3 W.Screen xs [1 ..] (map SD xinescs),
W.hidden = filter ((`notElem` shown) . W.tag) (map W.workspace (W.visible ws) ++ W.hidden ws)
}
-- Workspaces
myWorkspaces :: [String]
myWorkspaces = [wsTerm, wsWeb, wsGames, wsChat, wsMedia, wsDocs, wsVM] ++ map show [8, 9]
wsTerm, wsWeb, wsGames, wsChat, wsMedia, wsDocs, wsVM :: String
wsTerm = "1:\xf489 "
wsWeb = "2:\xe743 "
wsGames = "3:\xf1b6 "
wsChat = "4:\xf10b "
wsMedia = "5:\xead9 "
wsDocs = "6:\xeb69 "
wsVM = "7:\xea7a "
-- Layout
myLayoutHook = onWorkspace wsWeb myWebLayout $ onWorkspace wsGames myGamesLayout $ onWorkspace wsMedia myVideoLayout myDefaultLayout
where
myWebLayout = avoidStruts $ renamed [CutWordsLeft 1] $ spacingWithEdge 3 $ myTabbed ||| tiled ||| Mirror tiled ||| threeCol ||| Grid ||| spiral 0.856
myGamesLayout = avoidStruts $ smartBorders Full
myVideoLayout = avoidStruts $ smartBorders Full
myDefaultLayout = avoidStruts $ renamed [CutWordsLeft 1] $ spacingWithEdge 3 $ tiled ||| Mirror tiled ||| myTabbed ||| threeCol ||| Grid ||| spiral 0.856
threeCol =
renamed [Replace "ThreeCol"] $
magnifiercz' 1.3 $
ThreeColMid nmaster delta ratio
myTabbed =
renamed [Replace "Tabbed"] $
tabbed shrinkText myTabConfig
myTabConfig =
def
{ activeColor = "#24283b",
inactiveColor = "#737aa2"
}
tiled = Tall nmaster delta ratio
nmaster = 1
ratio = 1 / 2
delta = 3 / 100
-- Xmobar
-- The laptop config leaves room for trayer, so it goes on the laptop screen,
-- which is screen 1 with HDMI and screen 0 without.
myStatusBar :: Int -> Int -> String
myStatusBar nScreens i = "xmobar -x " ++ show i ++ " ~/.config/xmobar/" ++ rc
where
rc = if nScreens > 1 && i /= 1 then "xmobarrc_hdmi" else "xmobarrc_laptop"
-- One bar per screen, all restarted on every screen change: the right config for a
-- screen id depends on the screen count, and dynamicSBs would keep the old bar.
-- Kills by name (pkill waits) because killAllStatusBars' PID list can lose bars.
restartStatusBars :: X ()
restartStatusBars = do
_ <- runProcessWithInput "pkill" ["-x", "xmobar"] ""
screens <- gets (W.screens . windowset)
forM_ screens $ \s -> let S i = W.screen s in spawn (myStatusBar (length screens) i)
myXmobarPP :: PP
myXmobarPP =
def
{ ppSep = pink " • ",
ppTitleSanitize = xmobarStrip,
ppCurrent = wrap " " "" . xmobarBorder "Top" "#ff79c6" 2,
ppHidden = cultured . wrap " " "",
ppHiddenNoWindows = outerspace . wrap " " "",
ppUrgent = sunsetorange . wrap (keylime "!") (keylime "!"),
ppOrder = \[ws, l, _, wins] -> [ws, l, wins],
ppExtras = [logTitles formatFocused formatUnfocused]
}
where
formatFocused = wrap (cultured "[") (cultured "]") . pink . ppWindow
formatUnfocused = wrap (outerspace "[") (outerspace "]") . cultured . ppWindow
ppWindow :: String -> String
ppWindow = xmobarRaw . (\w -> if null w then "untitled" else w) . shorten 20
outerspace, pink, sunsetorange, cultured, keylime :: String -> String
pink = xmobarColor "#ff79c6" ""
cultured = xmobarColor "#f8f8f2" ""
keylime = xmobarColor "#f1fa8c" ""
sunsetorange = xmobarColor "#ff5555" ""
outerspace = xmobarColor "#44475a" ""
-- Autostart
myStartupHook :: X ()
myStartupHook = do
restartStatusBars
spawnOnce myScreenSetup
spawnOnce "systemctl --user start fehbg.timer"
spawnOnce "xsetroot -cursor_name left_ptr"
spawnOnce "xset s 590 10"
spawnOnce "xset dpms 0 0 900"
spawnOnce "xss-lock -n xidle-dim -l -- screen-lock"
spawnOnce "systemctl --user start xidle-inhibit"
spawnOnce "redshift-gtk"
spawnOnce "systemctl --user start udiskie"
spawnOnce "nm-applet"
spawnOnce "arch-audit-gtk"
spawnOnce "calcurse --daemon"
spawnOnce "systemctl --user start dunst"
spawnOnce "lxpolkit"
spawnOnce "numlockx"
spawnOnce "blueman-applet"
spawnOnce "adb start-server"
spawnOnce "/home/fuxino/.local/bin/discover-overlay-wait"
-- Manage hook
myManageHook :: ManageHook
myManageHook =
composeAll
[ className =? "Gpodder" --> doShift wsMedia,
className =? "An Anime Game Launcher" --> doShift wsGames,
className =? "Qalculate-gtk" --> doFloat,
title =? "Genshin Impact" --> doShift wsGames,
className =? "Signal" --> doShift wsChat,
className =? "Spotify" --> doShift wsMedia,
className =? "Transmission-gtk" --> doShift wsWeb,
className =? "Viewnior" --> doFloat,
className =? "Vivaldi-stable" --> doShift wsWeb,
className =? "Xmessage" --> doFloat,
className =? "Xreader" --> doShift wsDocs,
className =? "calibre" --> doShift wsDocs,
className =? "discord" --> doShift wsChat,
className =? "explorer.exe" --> doShift wsGames,
className =? "feh" --> doFloat,
title =? "glxgears" --> doFloat,
className =? "library_manager" --> doShift wsDocs,
className =? "libreoffice-calc" --> doShift wsDocs,
className =? "libreoffice-writer" --> doShift wsDocs,
className =? "mpv" --> doFullFloat,
className =? "mpv" --> viewShift wsMedia,
className =? "freetube" --> doShift wsMedia,
className =? myTerminal --> doShift wsTerm,
className =? "scrcpy" --> doShift wsMedia,
className =? "steam" --> doShift wsGames,
className =? "steam_app_109600" --> doFloat,
className =? "steam_app_109600" --> doShift wsGames,
className =? "steam_app_289070" --> doFloat,
className =? "steam_app_289070" --> doShift wsGames,
className =? "VirtualBox Machine" --> doShift wsVM,
className =? "VirtualBox Machine" --> doFullFloat,
className =? "VirtualBox Manager" --> doShift wsVM,
className =? "zen" --> doShift wsWeb,
isDialog --> doFloat,
-- Windows that open already fullscreen; ewmhFullscreen only handles later requests
isFullscreen --> doFullFloat
]
<+> namedScratchpadManageHook myScratchpads
where viewShift = doF . liftM2 (.) safeView W.shift
-- Handle event hook
myHandleEventHook :: Event -> X All
myHandleEventHook =
handleEventHook def
<> Hacks.trayerAboveXmobarEventHook
<> Hacks.trayerPaddingXmobarEventHook
<> Hacks.fixSteamFlicker
-- Log hook
myLogHook :: X ()
myLogHook = do
raiseDiscoverOverlay
xmonadPropLog =<< dynamicLogString =<< clickablePP myXmobarPP
-- discover-overlay's windows are override-redirect, so xmonad doesn't manage them and
-- raising a floating window (e.g. a game) covers them; put them back on top after every change
raiseDiscoverOverlay :: X ()
raiseDiscoverOverlay = withDisplay $ \d -> do
root <- asks theRoot
(_, _, ws) <- io $ queryTree d root
forM_ ws $ \w -> do
cls <- io $ getClassHint d w
when (resClass cls == "Discover-overlay") $ io $ raiseWindow d w