{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE PatternSynonyms #-} module Main (main) where import System.Exit (exitFailure) import XMonad.Hooks.EwmhDesktops (ewmh) import XMonad.Hooks.EwmhDesktops.Extra (ewmhExtra) import XMonad.Hooks.RefocusLast (refocusLastLayoutHook, toggleFocus) import Control.Monad (void) import Control.Monad.Extra (whenJustM) import qualified Data.Aeson import qualified Data.ByteString.Char8 import qualified Data.List import qualified Data.Maybe import Graphics.X11.ExtraTypes.XF86 import XMonad import XMonad.Extra (isFloatingX) import System.IO (hPutStrLn, stderr) import System.Environment (getArgs, getEnv, getEnvironment, lookupEnv) import System.Posix.Process (executeFile) import XMonad.Actions.DynamicWorkspaces (removeEmptyWorkspace) import XMonad.Actions.CycleWS (toggleWS) import XMonad.Layout.Gaps (Direction2D(U,R,D,L), gaps) import XMonad.Layout.FocusTracking (focusTracking) import XMonad.Layout.NoBorders ( smartBorders ) import XMonad.Layout.ResizableTile (ResizableTall(ResizableTall)) import XMonad.Layout.ResizableTile (MirrorResize(MirrorExpand,MirrorShrink)) import qualified XMonad.StackSet as W import Data.Map (Map) import qualified Data.Map as Map import XMonad.Hooks.UrgencyHook ( BorderUrgencyHook(BorderUrgencyHook,urgencyBorderColor) , RemindWhen(Dont) , SuppressWhen(Never) , UrgencyConfig(UrgencyConfig,remindWhen,suppressWhen) , clearUrgents' , withUrgencyHookC ) import XMonad.Hooks.ManageHelpers (doCenterFloat,doRectFloat) import Data.Ratio import XMonad.Hooks.Place (placeHook, smart) import XMonad.Actions.PerWorkspaceKeys (chooseAction) import Shutdown (shutdown, newShutdownEventHandler) import System.IO.Unsafe (unsafePerformIO) sliceEnvPath :: FilePath sliceEnvPath = unsafePerformIO (getEnv "XMONAD_CACHE_DIR") <> "/slice.env" main :: IO () main = getArgs >>= \case [] -> mainNoArgs ["--shutdown"] -> shutdown args -> hPutStrLn stderr ("bad arguments: " <> show args) >> exitFailure (=??) :: Query a -> (a -> Bool) -> Query Bool (=??) x p = fmap p x readEnv :: Data.Aeson.FromJSON b => String -> IO b readEnv name = readEnv' (error $ "could not get environment variable: " <> name) name readEnv' :: Data.Aeson.FromJSON b => b -> String -> IO b readEnv' defaultValue name = Data.Maybe.fromMaybe defaultValue . Data.Aeson.decodeStrict' . Data.ByteString.Char8.pack . Data.Maybe.fromMaybe mempty <$> lookupEnv name mainNoArgs :: IO () mainNoArgs = do getEnvironment >>= \env -> writeFile sliceEnvPath $ unlines [ k <> "=" <> v | (k, v) <- env] myMasterDelta <- readEnv' (1 / 20) "XMONAD_MASTER_DELTA" :: IO Rational myMasterWidth <- readEnv' (1 / 2) "XMONAD_MASTER_WIDTH" :: IO Rational myScreenGaps <- readEnv' [] "XMONAD_SCREEN_GAPS" :: IO [Int] myScreenWidth <- bracket (getEnv "DISPLAY" >>= openDisplay) closeDisplay (return . widthOfScreen . defaultScreenOfDisplay) handleShutdownEvent <- newShutdownEventHandler config <- ewmhExtra $ ewmh $ withUrgencyHookC BorderUrgencyHook { urgencyBorderColor = "#ff0000" } UrgencyConfig { remindWhen = Dont , suppressWhen = Never } $ def { terminal = {-pkg:alacritty-tv-}"alacritty" , clientMask = clientMask def .|. focusChangeMask , modMask = mod4Mask , keys = myKeys , layoutHook = refocusLastLayoutHook $ gaps (zip [U,R,D,L] myScreenGaps) $ smartBorders $ ResizableTall 1 myMasterDelta (myMasterWidth + 2 * fromIntegral (borderWidth def) / fromIntegral myScreenWidth) [] ||| focusTracking Full , manageHook = composeAll [ appName =? "fzmenu-urxvt" --> doCenterFloat , appName =?? Data.List.isPrefixOf "pinentry" --> doCenterFloat , appName =?? Data.List.isInfixOf "Float" --> doCenterFloat , title =? "Upload to Imgur" --> doRectFloat (W.RationalRect 0 0 (1 % 8) (1 % 8)) , placeHook (smart (1,0)) ] , startupHook = whenJustM (io (lookupEnv "XMONAD_STARTUP_HOOK")) (\path -> forkFile path [] Nothing) , normalBorderColor = "#1c1c1c" , focusedBorderColor = "#f000b0" , handleEventHook = handleFocusChangeEvent <> handleShutdownEvent } directories <- getDirectories launch config directories handleFocusChangeEvent :: Event -> X All handleFocusChangeEvent = \case FocusChangeEvent{ev_event_type,ev_window} | ev_event_type == focusIn -> do clearUrgents' [ev_window] return (All True) _ -> return (All True) forkFile :: FilePath -> [String] -> Maybe [(String, String)] -> X () forkFile path args env = void . xfork $ do environment <- getEnvironment executeFile path True args (env <> Just environment) forkFileInSlice :: String -> FilePath -> [String] -> X () forkFileInSlice sliceName path args = void . xfork $ executeFile {-pkg:systemd-}"systemd-run" True ( "--collect" : "--user" : "--slice=" <> sliceName : "--property=EnvironmentFile=" <> sliceEnvPath : path : args ) Nothing myKeys :: XConfig Layout -> Map (KeyMask, KeySym) (X ()) myKeys conf = Map.fromList $ [ ((_4 , xK_Escape ), forkFile {-pkg-}"slock" [] Nothing) , ((_4S , xK_c ), kill) , ((_4 , xK_o ), forkFile {-pkg:fzmenu-}"otpmenu" [] Nothing) , ((_4C , xK_o ), forkFile {-pkg:fzmenu-}"otpmenu" ["--phase2-method=copy"] Nothing) , ((_4 , xK_p ), forkFile {-pkg:fzmenu-}"passmenu" [] Nothing) , ((_4C , xK_p ), forkFile {-pkg:fzmenu-}"passmenu" ["--phase2-method=copy"] Nothing) , ((_4 , xK_x ), forkFileInSlice "alacritty" {-pkg:alacritty-tv-}"alacritty" ["--dtach", "--singleton"]) , ((_4C , xK_x ), forkFileInSlice "alacritty" {-pkg:alacritty-tv-}"alacritty" ["--profile=root", "-e", "/run/wrappers/bin/su", "-"]) , ((_C , xK_Menu ), toggleWS) , ((_4 , xK_space ), withFocused $ \w -> ifM (isFloatingX w) xdeny $ sendMessage NextLayout) , ((_4M , xK_space ), withFocused $ \w -> ifM (isFloatingX w) xdeny $ resetLayout) , ((_4 , xK_l ), toggleFocus) , ((_4 , xK_m ), windows W.focusMaster) , ((_4 , xK_j ), windows W.focusDown) , ((_4 , xK_k ), windows W.focusUp) , ((_4S , xK_m ), windows W.swapMaster) , ((_4S , xK_j ), windows W.swapDown) , ((_4S , xK_k ), windows W.swapUp) , ((_4M , xK_h ), sendMessage Shrink) , ((_4M , xK_l ), sendMessage Expand) , ((_4M , xK_j ), sendMessage MirrorShrink) , ((_4M , xK_k ), sendMessage MirrorExpand) , ((_4 , xK_t ), withFocused $ windows . W.sink) , ((_4 , xK_comma ), sendMessage $ IncMasterN 1) , ((_4 , xK_period ), sendMessage $ IncMasterN (-1)) , ((_4 , xK_Delete ), removeEmptyWorkspace) , ((_4 , xK_Return ), toggleWS) , ((0, xF86XK_AudioLowerVolume), audioLowerVolume) , ((0, xF86XK_AudioRaiseVolume), audioRaiseVolume) , ((0, xF86XK_AudioMute), audioMute) , ((0, xF86XK_AudioMicMute), audioMicMute) , ((_4, xF86XK_AudioMute), pavucontrol []) , ((_4, xK_Prior), forkFile {-pkg-}"xcalib" ["-invert", "-alter"] Nothing) , ((0, xK_Print), forkFile {-pkg:flameshot-once-tv-}"flameshot-once" [] Nothing) , ((_C, xF86XK_Forward), forkFile {-pkg:xdpytools-}"xdpychvt" ["next"] Nothing) , ((_C, xF86XK_Back), forkFile {-pkg:xdpytools-}"xdpychvt" ["prev"] Nothing) ] where _4 = mod4Mask _C = controlMask _S = shiftMask _M = mod1Mask _4C = _4 .|. _C _4S = _4 .|. _S _4M = _4 .|. _M _4CM = _4 .|. _C .|. _M _4SM = _4 .|. _S .|. _M amixer args = forkFile {-pkg:alsa-utils-}"amixer" args Nothing pavucontrol args = forkFile {-pkg-}"pavucontrol" args Nothing audioLowerVolume = amixer ["-q", "sset", "Master", "5%-"] audioRaiseVolume = amixer ["-q", "sset", "Master", "5%+"] audioMute = amixer ["-q", "sset", "Master", "toggle"] audioMicMute = amixer ["-q", "sset", "Capture", "toggle"] resetLayout = setLayout $ XMonad.layoutHook conf xdeny = forkFile {-pkg-}"xterm" [ "-geometry", "300x100" , "-name", "AlertFloat" , "-bg", "#E4002B" , "-e", "sleep", "0.05" ] Nothing