xmonad-config/bin/xmonad.hs

510 lines
17 KiB
Haskell
Raw Normal View History

2020-03-28 19:58:26 -04:00
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | XMonad binary
module Main (main) where
2020-03-22 23:23:02 -04:00
import Control.Concurrent
import Control.Monad (unless)
2020-04-01 20:17:47 -04:00
import Data.List
( isPrefixOf
, sortBy
, sortOn
)
import Data.Maybe (isJust)
import Data.Monoid (All (..))
2020-03-22 23:23:02 -04:00
import Graphics.X11.Types
import Graphics.X11.Xlib.Atom
import Graphics.X11.Xlib.Extras
2020-03-22 23:23:02 -04:00
import System.IO
import System.Process
2020-03-22 23:23:02 -04:00
import XMonad
import XMonad.Actions.CopyWindow
import XMonad.Actions.CycleWS
import XMonad.Actions.PhysicalScreens
2020-03-27 21:09:40 -04:00
import XMonad.Actions.Warp
2020-04-01 20:17:47 -04:00
import XMonad.Hooks.DynamicLog
( pad
, wrap
, xmobarColor
)
2020-03-22 23:23:02 -04:00
import XMonad.Hooks.EwmhDesktops
import XMonad.Hooks.ManageDocks
import XMonad.Hooks.ManageHelpers
2020-04-01 20:17:47 -04:00
import XMonad.Internal.Command.Desktop
import XMonad.Internal.Command.DMenu
import XMonad.Internal.Command.Power
import XMonad.Internal.Concurrent.ACPIEvent
import XMonad.Internal.Concurrent.ClientMessage
import XMonad.Internal.Concurrent.DynamicWorkspaces
import XMonad.Internal.Concurrent.Removable
2020-04-01 20:17:47 -04:00
import XMonad.Internal.DBus.Control
import XMonad.Internal.Process
import XMonad.Internal.Shell
import qualified XMonad.Internal.Theme as T
2020-03-28 19:58:26 -04:00
import XMonad.Layout.MultiToggle
2020-03-22 23:23:02 -04:00
import XMonad.Layout.NoBorders
import XMonad.Layout.NoFrillsDecoration
import XMonad.Layout.PerWorkspace
import XMonad.Layout.Renamed
2020-03-22 23:23:02 -04:00
import XMonad.Layout.Tabbed
2020-04-01 20:17:47 -04:00
import qualified XMonad.StackSet as W
2020-04-05 00:48:44 -04:00
import XMonad.Util.Cursor
import qualified XMonad.Util.ExtensibleState as E
2020-03-22 23:23:02 -04:00
import XMonad.Util.EZConfig
import XMonad.Util.NamedActions
import XMonad.Util.WorkspaceCompare
2020-03-20 01:15:22 -04:00
main :: IO ()
main = do
cl <- startXMonadService
(h, p) <- spawnPipe "xmobar"
2020-03-22 23:20:10 -04:00
_ <- forkIO runPowermon
_ <- forkIO runRemovableMon
_ <- forkIO $ runWorkspaceMon allDWs
let ts = ThreadState
{ client = cl
, childPIDs = [p]
, childHandles = [h]
}
launch
$ ewmh
$ addKeymap ts
$ def { terminal = myTerm
, modMask = myModMask
, layoutHook = myLayouts
, manageHook = myManageHook
, handleEventHook = myEventHook
, startupHook = myStartupHook
, workspaces = myWorkspaces
, logHook = myLoghook h
, clickJustFocuses = False
, focusFollowsMouse = False
2020-03-22 17:59:49 -04:00
, normalBorderColor = T.bordersColor
, focusedBorderColor = T.selectedBordersColor
}
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Concurrency configuration
2020-03-28 18:38:38 -04:00
data ThreadState = ThreadState
{ client :: Client
, childPIDs :: [ProcessHandle]
, childHandles :: [Handle]
}
2020-03-28 18:38:38 -04:00
-- TODO shouldn't this be run by a signal handler?
runCleanup :: ThreadState -> X ()
runCleanup ts = io $ do
mapM_ killHandle $ childPIDs ts
2020-03-28 18:38:38 -04:00
stopXMonadService $ client ts
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Startuphook configuration
-- TODO add _NET_DESKTOP_VIEWPORTS to _NET_SUPPORTED?
myStartupHook :: X ()
2020-04-05 00:48:44 -04:00
myStartupHook = setDefaultCursor xC_left_ptr <+> docksStartupHook
<+> startupHook def
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Workspace configuration
myWorkspaces :: [WorkspaceId]
myWorkspaces = map show [1..10 :: Int]
gimpDynamicWorkspace :: DynWorkspace
gimpDynamicWorkspace = DynWorkspace
{ dwName = "Gimp"
, dwTag = t
, dwClass = c
, dwHook =
[ matchGimpRole "gimp-image-window" -?> appendViewShift t
2020-07-03 10:54:57 -04:00
, matchGimpRole "gimp-dock" -?> doF W.swapDown
, matchGimpRole "gimp-toolbox" -?> doF W.swapDown
, className =? c -?> appendViewShift t
]
2020-03-28 23:15:41 -04:00
, dwKey = 'g'
, dwCmd = Just $ spawnCmd "gimp-2.10" []
}
where
matchGimpRole role = isPrefixOf role <$> stringProperty "WM_WINDOW_ROLE"
<&&> className =? c
t = "GIMP"
c = "Gimp-2.10" -- TODO I don't feel like changing the version long term
wmDynamicWorkspace :: DynWorkspace
wmDynamicWorkspace = DynWorkspace
{ dwName = "Windows VirtualBox"
, dwTag = t
, dwClass = c
, dwHook = [ className =? c -?> appendViewShift t ]
2020-03-28 23:15:41 -04:00
, dwKey = 'v'
, dwCmd = Just $ spawnCmd "vbox-start" ["win8raw"]
}
where
t = "VM"
c = "VirtualBoxVM"
xsaneDynamicWorkspace :: DynWorkspace
xsaneDynamicWorkspace = DynWorkspace
{ dwName = "XSane"
, dwTag = t
, dwClass = c
, dwHook = [ className =? c -?> appendViewShift t >> doFloat ]
2020-03-28 23:15:41 -04:00
, dwKey = 'x'
, dwCmd = Just $ spawnCmd "xsane" []
}
where
t = "XSANE"
c = "Xsane"
2020-03-28 23:15:41 -04:00
steamDynamicWorkspace :: DynWorkspace
steamDynamicWorkspace = DynWorkspace
{ dwName = "Steam Game"
, dwTag = t
, dwClass = c
-- TODO not sure why the doSink is needed, windows should tile be default
-- since dynamic workspaces are first in the managehook
, dwHook = [ className =? c -?> appendViewShift t >> doSink ]
, dwKey = 'z'
, dwCmd = Nothing
}
where
t = "STEAM"
-- TODO this actually needs to be a list to match all games we care about
c = "portal2_linux"
allDWs :: [DynWorkspace]
2020-03-28 23:15:41 -04:00
allDWs = [ xsaneDynamicWorkspace
, wmDynamicWorkspace
, gimpDynamicWorkspace
, steamDynamicWorkspace
]
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Layout configuration
myLayouts = onWorkspace (dwTag wmDynamicWorkspace) vmLayout
$ onWorkspace (dwTag gimpDynamicWorkspace) gimpLayout
2020-03-28 23:15:41 -04:00
$ onWorkspace (dwTag steamDynamicWorkspace) steamLayout
2020-03-28 19:58:26 -04:00
$ mkToggle (single HIDE)
$ tall ||| fulltab ||| full
where
addTopBar = noFrillsDeco shrinkText T.tabbedTheme
tall = renamed [Replace "Tall"]
$ avoidStruts
$ addTopBar
$ noBorders
$ Tall 1 0.03 0.5
2020-03-28 19:58:26 -04:00
fulltab = renamed [Replace "Tabbed"]
$ avoidStruts
$ noBorders
$ tabbedAlways shrinkText T.tabbedTheme
full = renamed [Replace "Full"]
$ noBorders Full
vmLayout = noBorders Full
2020-03-28 23:15:41 -04:00
steamLayout = vmLayout
-- TODO use a tabbed layout for multiple master windows
gimpLayout = renamed [Replace "Gimp Layout"]
$ avoidStruts
$ noBorders
$ addTopBar
$ Tall 1 0.025 0.8
2020-04-01 22:06:00 -04:00
-- | Make a new empty layout and add a message to show/hide it. This is useful
-- for quickly showing conky.
2020-03-28 19:58:26 -04:00
data EmptyLayout a = EmptyLayout
deriving (Show, Read)
instance LayoutClass EmptyLayout a where
doLayout a b _ = emptyLayout a b
description _ = "Desktop"
data HIDE = HIDE
deriving (Read, Show, Eq, Typeable)
instance Transformer HIDE Window where
transform _ x k = k EmptyLayout (\EmptyLayout -> x)
-- TODO toggle back to normal when a new window is opened
2020-03-28 19:58:26 -04:00
runHide :: X ()
runHide = sendMessage $ Toggle HIDE
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Loghook configuration
2020-04-01 22:06:00 -04:00
--
myLoghook :: Handle -> X ()
myLoghook h = do
logXinerama h
logViewports
-- | Viewports loghook
-- This is all stuff that should probably be added to the EVMH contrib module.
-- Basically, this will send the workspace "viewport" positions to
-- _NET_DESKTOP_VIEWPORT which can be further processed by tools such as
-- 'wmctrl' to figure out which workspaces are on what monitor outside of
-- xmomad. This is more or less the way i3 does this, where the current
-- workspace has a valid position and everything else is just (0, 0). Also, I
-- probably should set the _NET_SUPPORT atom to reflect the existance of
-- _NET_DESKTOP_VIEWPORT, but for now there seems to be no ill effects so why
-- bother...(if that were necessary it would go in the startup hook)
newtype DesktopViewports = DesktopViewports [Int]
deriving Eq
instance ExtensionClass DesktopViewports where
initialValue = DesktopViewports []
logViewports :: X ()
logViewports = withWindowSet $ \s -> do
sort' <- getSortByIndex
let ws = sort' $ W.workspaces s
let desktopViewports = concatMap (wsToViewports s) ws
whenChanged (DesktopViewports desktopViewports) $
setDesktopViewports desktopViewports
where
wsToViewports s w = let cur = W.current s in
if W.tag w == currentTag cur then currentPos cur else [0, 0]
currentTag = W.tag . W.workspace
currentPos = rectXY . screenRect . W.screenDetail
rectXY (Rectangle x y _ _) = [fromIntegral x, fromIntegral y]
setDesktopViewports :: [Int] -> X ()
setDesktopViewports vps = withDisplay $ \dpy -> do
r <- asks theRoot
a <- getAtom "_NET_DESKTOP_VIEWPORT"
c <- getAtom "CARDINAL"
io $ changeProperty32 dpy r a c propModeReplace $ map fromIntegral vps
-- stolen from XMonad.Hooks.EwmhDesktops
whenChanged :: (Eq a, ExtensionClass a) => a -> X () -> X ()
whenChanged v action = do
v0 <- E.get
unless (v == v0) $ do
action
E.put v
-- | Xinerama loghook (for xmobar)
2020-04-01 22:06:00 -04:00
-- The format will be like "[<1> 2 3] 4 5 | LAYOUT (N)" where each digit is the
-- workspace and LAYOUT is the current layout. Each workspace in the brackets is
-- currently visible and the order reflects the physical location of each
-- screen. The "<>" is the workspace that currently has focus. N is the number
-- of windows on the current workspace.
logXinerama :: Handle -> X ()
logXinerama h = withWindowSet $ \ws -> io
$ hPutStrLn h
$ unwords
$ filter (not . null) [onScreen ws, offScreen ws, sep, layout ws, nWindows ws]
where
onScreen ws = xmobarColor hilightFgColor hilightBgColor
2020-04-01 20:17:47 -04:00
$ pad
$ unwords
$ map (fmtTags ws . W.tag . W.workspace)
2020-04-01 18:51:30 -04:00
$ sortBy compareXCoord
$ W.current ws : W.visible ws
offScreen ws = xmobarColor T.backdropFgColor ""
2020-04-01 18:51:30 -04:00
$ unwords
$ map W.tag
2020-04-01 18:51:30 -04:00
$ filter (isJust . W.stack)
$ sortOn W.tag
$ W.hidden ws
2020-04-01 20:17:47 -04:00
sep = xmobarColor T.backdropFgColor "" ":"
layout ws = description $ W.layout $ W.workspace $ W.current ws
nWindows ws = wrap "(" ")"
2020-04-01 18:51:30 -04:00
$ show
$ length
$ W.integrate'
$ W.stack
$ W.workspace
$ W.current ws
hilightBgColor = "#8fc7ff"
hilightFgColor = T.blend' 0.5 hilightBgColor T.fgColor
fmtTags ws t = if t == W.currentTag ws
2020-04-01 20:17:47 -04:00
then xmobarColor T.fgColor hilightBgColor t
2020-04-01 18:51:30 -04:00
else t
compareXCoord
:: W.Screen i1 l1 a1 ScreenId ScreenDetail
-> W.Screen i2 l2 a2 ScreenId ScreenDetail -> Ordering
compareXCoord s0 s1 = compare x0 x1
where
(_, Rectangle x0 _ _ _) = getScreenIdAndRectangle s0
(_, Rectangle x1 _ _ _) = getScreenIdAndRectangle s1
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Managehook configuration
2020-03-20 01:15:22 -04:00
myManageHook :: ManageHook
myManageHook = manageApps <+> manageDocks <+> manageHook def
manageApps :: ManageHook
manageApps = composeOne $ concatMap dwHook allDWs ++
[ isDialog -?> doCenterFloat
-- the seafile applet
, className =? "Seafile Client" -?> doFloat
-- gnucash
, (className =? "Gnucash" <&&> title =? "Transaction Import Assistant") -?> doFloat
2020-04-08 23:16:34 -04:00
-- plots and graphics
, className =? "R_x11" -?> doFloat
2020-04-08 23:16:34 -04:00
, className =? "Matplotlib" -?> doFloat
, className =? "mpv" -?> doFloat
-- the floating windows created by the brave browser
, stringProperty "WM_NAME" =? "Brave" -?> doFloat
2020-05-02 00:19:54 -04:00
-- , (stringProperty "WM_WINDOW_ROLE" =? "pop-up"
-- <&&> className =? "Brave-browser") -?> doFloat
-- the dialog windows created by the zotero addon in Google Docs
, (className =? "Zotero" <&&> resource =? "Toplevel") -?> doFloat
]
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Eventhook configuration
2020-03-20 01:15:22 -04:00
myEventHook :: Event -> X All
2020-03-28 18:38:38 -04:00
myEventHook = xMsgEventHook <+> docksEventHook <+> handleEventHook def
2020-04-01 22:06:00 -04:00
-- | React to ClientMessage events from concurrent threads
2020-03-28 18:38:38 -04:00
xMsgEventHook :: Event -> X All
xMsgEventHook ClientMessageEvent { ev_message_type = t, ev_data = d }
| t == bITMAP = do
2020-03-25 14:45:21 -04:00
let (xtype, tag) = splitXMsg d
case xtype of
Workspace -> removeDynamicWorkspace tag
2020-03-28 18:38:38 -04:00
ACPI -> handleACPI tag
return (All True)
2020-03-28 18:38:38 -04:00
xMsgEventHook _ = return (All True)
2020-04-01 22:06:00 -04:00
--------------------------------------------------------------------------------
-- | Keymap configuration
myModMask :: KeyMask
myModMask = mod4Mask
addKeymap :: ThreadState -> XConfig l -> XConfig l
addKeymap ts = addDescrKeys' ((myModMask, xK_F1), runShowKeys) (mkKeys ts)
mkKeys :: ThreadState -> XConfig Layout -> [((KeyMask, KeySym), NamedAction)]
mkKeys ts c =
mkNamedSubmap "Window Layouts"
2020-03-26 21:03:34 -04:00
[ ("M-j", "focus down", windows W.focusDown)
, ("M-k", "focus up", windows W.focusUp)
, ("M-m", "focus master", windows W.focusMaster)
2020-03-28 19:58:26 -04:00
, ("M-d", "focus master", runHide)
2020-03-26 21:03:34 -04:00
, ("M-S-j", "swap down", windows W.swapDown)
, ("M-S-k", "swap up", windows W.swapUp)
, ("M-S-m", "swap master", windows W.swapMaster)
, ("M-<Return>", "next layout", sendMessage NextLayout)
2020-03-26 21:06:38 -04:00
, ("M-S-<Return>", "reset layout", setLayout $ layoutHook c)
2020-03-26 21:03:34 -04:00
, ("M-t", "sink tiling", withFocused $ windows . W.sink)
, ("M--", "shrink", sendMessage Shrink)
, ("M-=", "expand", sendMessage Expand)
2020-03-26 21:06:38 -04:00
, ("M-S--", "remove master window", sendMessage $ IncMasterN (-1))
, ("M-S-=", "add master window", sendMessage $ IncMasterN 1)
] ++
mkNamedSubmap "Workspaces"
2020-03-26 21:06:38 -04:00
-- ASSUME standard workspaces only use numbers 0-9 (otherwise we won't get
2020-03-26 21:03:34 -04:00
-- valid keysyms)
2020-03-26 21:41:56 -04:00
([ (mods ++ n, msg ++ n, f n) | n <- myWorkspaces
, (mods, msg, f) <-
[ ("M-", "switch to workspace ", windows . W.view)
, ("M-S-", "move client to workspace ", windows . W.shift)
, ("M-C-", "follow client to workspace ", \n' -> do
windows $ W.shift n'
windows $ W.view n')
]
] ++
[ ("M-M1-l", "move up workspace", moveTo Next HiddenNonEmptyWS)
, ("M-M1-h", "move down workspace", moveTo Prev HiddenNonEmptyWS)
]) ++
mkNamedSubmap "Dynamic Workspaces"
2020-03-28 23:15:41 -04:00
[ ("M-C-" ++ [k], "launch/switch to " ++ n, cmd)
| DynWorkspace { dwTag = t, dwKey = k, dwCmd = a, dwName = n } <- allDWs,
let cmd = case a of
Just a' -> spawnOrSwitch t a'
Nothing -> windows $ W.view t
] ++
mkNamedSubmap "Screens"
2020-03-26 09:48:02 -04:00
[ ("M-l", "move up screen", nextScreen)
, ("M-h", "move down screen", prevScreen)
2020-03-26 21:41:56 -04:00
, ("M-C-l", "follow client up screen", shiftNextScreen >> nextScreen)
, ("M-C-h", "follow client down screen", shiftPrevScreen >> prevScreen)
, ("M-S-l", "shift workspace up screen", swapNextScreen >> nextScreen)
, ("M-S-h", "shift workspace down screen", swapPrevScreen >> prevScreen)
] ++
mkNamedSubmap "Actions"
2020-03-26 09:48:02 -04:00
[ ("M-q", "close window", kill1)
, ("M-r", "run program", runCmdMenu)
2020-03-27 21:09:40 -04:00
, ("M-<Space>", "warp pointer", warpToWindow 0.5 0.5)
2020-03-26 09:48:02 -04:00
, ("M-C-s", "capture area", runAreaCapture)
, ("M-C-S-s", "capture screen", runScreenCapture)
, ("M-C-d", "capture desktop", runDesktopCapture)
, ("M-C-b", "browse captures", runCaptureBrowser)
2020-03-26 09:48:02 -04:00
-- , ("M-C-S-s", "capture focused window", spawn myWindowCap)
] ++
mkNamedSubmap "Launchers"
2020-03-26 09:48:02 -04:00
[ ("<XF86Search>", "select/launch app", runAppMenu)
, ("M-g", "launch clipboard manager", runClipMenu)
, ("M-a", "launch network selector", runNetMenu)
, ("M-w", "launch window selector", runWinMenu)
, ("M-u", "launch device selector", runDevMenu)
2020-05-02 00:02:29 -04:00
, ("M-b", "launch bitwarden selector", runBwMenu)
2020-03-26 09:48:02 -04:00
, ("M-C-e", "launch editor", runEditor)
, ("M-C-w", "launch browser", runBrowser)
, ("M-C-t", "launch terminal", runTerm)
, ("M-C-q", "launch calc", runCalc)
, ("M-C-f", "launch file manager", runFileManager)
] ++
mkNamedSubmap "Multimedia"
2020-03-26 09:48:02 -04:00
[ ("<XF86AudioPlay>", "toggle play/pause", runTogglePlay)
, ("<XF86AudioPrev>", "previous track", runPrevTrack)
, ("<XF86AudioNext>", "next track", runNextTrack)
, ("<XF86AudioStop>", "stop", runStopPlay)
, ("<XF86AudioLowerVolume>", "volume down", runVolumeDown)
, ("<XF86AudioRaiseVolume>", "volume up", runVolumeUp)
, ("<XF86AudioMute>", "volume mute", runVolumeMute)
] ++
2020-03-26 22:02:20 -04:00
-- dummy map for dunst commands (defined separately but this makes them show
-- up in the help menu)
mkNamedSubmap "Dunst"
2020-03-26 22:02:20 -04:00
[ ("M-`", "dunst history", return ())
, ("M-S-`", "dunst close", return ())
, ("M-M1-`", "dunst context menu", return ())
, ("M-C-`", "dunst close all", return ())
] ++
mkNamedSubmap "System"
2020-03-26 09:48:02 -04:00
[ ("M-.", "backlight up", runIncBacklight)
, ("M-,", "backlight down", runDecBacklight)
, ("M-M1-,", "backlight min", runMinBacklight)
, ("M-M1-.", "backlight max", runMaxBacklight)
, ("M-<End>", "power menu", runPowerPrompt)
, ("M-<Home>", "quit xmonad", runQuitPrompt)
, ("M-<Delete>", "lock screen", runScreenLock)
-- M-<F1> reserved for showing the keymap
, ("M-<F2>", "restart xmonad", runCleanup ts >> runRestart)
, ("M-<F3>", "recompile xmonad", runRecompile)
2020-08-17 18:46:02 -04:00
, ("M-<F8>", "select autorandr profile", runAutorandrMenu)
2020-05-28 23:17:17 -04:00
, ("M-<F9>", "toggle ethernet", runToggleEthernet)
, ("M-<F10>", "toggle bluetooth", runToggleBluetooth)
2020-03-26 22:06:38 -04:00
, ("M-<F11>", "toggle screensaver", runToggleDPMS)
, ("M-<F12>", "switch gpu", runOptimusPrompt)
]
where
mkNamedSubmap header bindings = (subtitle header:) $ mkNamedKeymap c
$ map (\(key, name, cmd) -> (key, addName name cmd)) bindings