mirror of
https://github.com/xmonad/xmonad-contrib.git
synced 2025-08-01 20:51:52 -07:00
529 lines
16 KiB
Haskell
529 lines
16 KiB
Haskell
{-# LANGUAGE
|
|
FlexibleInstances,
|
|
FlexibleContexts,
|
|
MultiParamTypeClasses,
|
|
ExistentialQuantification
|
|
#-}
|
|
|
|
-------------------------------------------------------------------------
|
|
-- |
|
|
-- Module : XMonad.Config.PlainConfig
|
|
-- Copyright : Braden Shepherdson <Braden.Shepherdson@gmail.com>
|
|
-- License : BSD3
|
|
--
|
|
-- Maintainer : Braden Shepherdson <Braden.Shepherdson@gmail.com>
|
|
--
|
|
-- Proof-of-concept (but usable) plain-text configuration file
|
|
-- parser, for use instead of xmonad.hs. Does not require recompilation,
|
|
-- allowing xmonad to be free of the GHC dependency.
|
|
--
|
|
-------------------------------------------------------------------------
|
|
|
|
|
|
module XMonad.Config.PlainConfig
|
|
(
|
|
-- * Introduction
|
|
-- $usage
|
|
|
|
-- * Supported Layouts
|
|
-- $layouts
|
|
|
|
-- * Support Key Bindings
|
|
-- $keys
|
|
|
|
-- * Other Notes
|
|
-- $notes
|
|
|
|
-- * Example Config File
|
|
-- $example
|
|
|
|
plainConfig ,readConfig, checkConfig
|
|
)
|
|
where
|
|
|
|
|
|
import XMonad
|
|
import System.Exit
|
|
|
|
import qualified XMonad.StackSet as W
|
|
import qualified Data.Map as M
|
|
import Data.List
|
|
import Data.Maybe (isJust,fromJust)
|
|
import Data.Char (isSpace)
|
|
|
|
|
|
--import Control.Monad
|
|
import Control.Monad.Error
|
|
import Control.Monad.Identity
|
|
|
|
import Control.Arrow ((&&&))
|
|
|
|
import Text.ParserCombinators.ReadP
|
|
|
|
import System.IO
|
|
import Control.Exception (bracket)
|
|
|
|
import XMonad.Util.EZConfig (mkKeymap)
|
|
|
|
|
|
|
|
-- $usage
|
|
-- The @xmonad.hs@ file is very minimal when used with PlainConfig.
|
|
-- It typically contains only the following:
|
|
--
|
|
-- > module Main where
|
|
-- > import XMonad
|
|
-- > import XMonad.Config.PlainConfig (plainConfig)
|
|
-- > main = plainConfig
|
|
--
|
|
-- The 'plainConfig' function parses @~\/.xmonad\/xmonad.conf@,
|
|
-- the format of which is described below.
|
|
|
|
|
|
-- $layouts
|
|
-- Only 'Tall', 'Wide' and 'Full' are supported at present.
|
|
|
|
|
|
|
|
-- $keys
|
|
--
|
|
-- Key bindings are specified as a pair of an arbitrary EZConfig and
|
|
-- one of the following:
|
|
--
|
|
-- @ Name Haskell equivalent Default binding(s)@
|
|
--
|
|
-- * @spawn \<cmd\> spawn \"\<cmd\>\" none@
|
|
--
|
|
-- * @kill kill M-S-c@
|
|
--
|
|
-- * @nextLayout sendMessage NextLayout M-\<Space\>@
|
|
--
|
|
-- * @refresh refresh M-S-\<Space\>@
|
|
--
|
|
-- * @focusDown windows W.focusDown M-\<Tab\>, M-j@
|
|
--
|
|
-- * @focusUp windows W.focusUp M-k@
|
|
--
|
|
-- * @focusMaster windows W.focusMaster M-m@
|
|
--
|
|
-- * @swapDown windows W.swapDown M-S-j@
|
|
--
|
|
-- * @swapUp windows W.swapUp M-S-k@
|
|
--
|
|
-- * @swapMaster windows W.swapMaster M-\<Return\>@
|
|
--
|
|
-- * @shrink sendMessage Shrink M-h@
|
|
--
|
|
-- * @expand sendMessage Expand M-l@
|
|
--
|
|
-- * @sink withFocused $ windows . W.sink M-t@
|
|
--
|
|
-- * @incMaster sendMessage (IncMasterN 1) M-,@
|
|
--
|
|
-- * @decMaster sendMessage (IncMasterN (-1)) M-.@
|
|
--
|
|
-- * @quit io $ exitWith ExitSuccess M-S-q@
|
|
--
|
|
-- * @restart broadcastMessageReleaseResources >> restart \"xmonad\" True M-q@
|
|
--
|
|
|
|
|
|
-- $notes
|
|
-- Submaps are allowed.
|
|
-- These settings override the defaults. Changes made here will be used over
|
|
-- the default bindings for those keys.
|
|
|
|
|
|
-- $example
|
|
-- An example @~\/.xmonad\/xmonad.conf@ file follows:
|
|
--
|
|
-- @modMask = 3@
|
|
--
|
|
-- @numlockMask = 2@
|
|
--
|
|
-- @borderWidth = 1@
|
|
--
|
|
-- @normalBorderColor = #dddddd@
|
|
--
|
|
-- @focusedBorderColor = #00ff00@
|
|
--
|
|
-- @terminal=urxvt@
|
|
--
|
|
-- @workspaces=[\"1: IRC\",\"2: Web\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"]@
|
|
--
|
|
-- @focusFollowsMouse=True@
|
|
--
|
|
-- @layouts=[\"Tall\",\"Full\",\"Wide\"]@
|
|
--
|
|
-- @key=(\"M-x t\", \"spawn xmessage Test\")@
|
|
--
|
|
-- @manageHook=(ClassName \"MPlayer\" , \"float\" )@
|
|
--
|
|
-- @manageHook=(ClassName \"Gimp\" , \"float\" )@
|
|
--
|
|
-- @manageHook=(Resource \"desktop_window\", \"ignore\" )@
|
|
--
|
|
-- @manageHook=(Resource \"kdesktop\" , \"ignore\" )@
|
|
--
|
|
-- @manageHook=(Resource \"gnome-panel\" , \"ignore\" )@
|
|
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------
|
|
------ Several functions for parsing the key-value file. -------
|
|
----------------------------------------------------------------
|
|
|
|
parseKVBy :: Char -> ReadP (String,String)
|
|
parseKVBy sep = do
|
|
skipSpaces
|
|
k <- munch1 (\x -> x /= ' ' && x /= sep)
|
|
skipSpaces
|
|
char kvSep
|
|
skipSpaces
|
|
v <- munch1 (\x -> x /= ' ') --or EOS
|
|
return (k,v)
|
|
|
|
parseKVVBy :: Char -> ReadP (String,String)
|
|
parseKVVBy sep = do
|
|
skipSpaces
|
|
k <- munch1 (\x -> x /= ' ' && x /= sep)
|
|
skipSpaces
|
|
char kvSep
|
|
skipSpaces
|
|
v <- munch1 (const True) -- until EOS
|
|
return (k,v)
|
|
|
|
|
|
kvSep :: Char
|
|
kvSep = '='
|
|
|
|
parseKV, parseKVV :: ReadP (String,String)
|
|
parseKV = parseKVBy kvSep
|
|
parseKVV = parseKVVBy kvSep
|
|
|
|
|
|
|
|
readKV :: String -> Integer -> RC (String,String)
|
|
readKV s ln = case readP_to_S parseKV s of
|
|
[((k,v),"")] -> return (k,v) --single, correct parse
|
|
[] -> throwError [(ln,"No parse")]
|
|
_ -> do
|
|
case readP_to_S parseKVV s of
|
|
[((k,v),"")] -> return (k,v) --single, correct parse
|
|
[] -> throwError [(ln,"No parse")]
|
|
xs -> throwError [(ln,"Ambiguous parse: "
|
|
++ show xs)]
|
|
|
|
|
|
|
|
isComment :: String -> Bool
|
|
isComment = not . null . readP_to_S parseComment
|
|
where parseComment = skipSpaces >> char '#' >> return ()
|
|
-- null means failed parse, so _not_ a comment.
|
|
|
|
|
|
isBlank :: String -> Bool
|
|
isBlank = null . filter (not . isSpace)
|
|
|
|
|
|
type RC = ErrorT [(Integer,String)] Identity
|
|
|
|
instance Error [(Integer,String)] where
|
|
noMsg = [(-1, "Unknown error.")]
|
|
strMsg s = [(-1, s)]
|
|
|
|
|
|
parseFile :: [String] -> RC (XConfig Layout)
|
|
parseFile ss = parseLines baseConfig theLines
|
|
where theLines = filter (not . liftM2 (||) isComment isBlank . snd)
|
|
$ zip [1..] ss
|
|
|
|
|
|
|
|
parseLines :: XConfig Layout -> [(Integer,String)] -> RC (XConfig Layout)
|
|
parseLines = foldM parse
|
|
|
|
|
|
parse :: XConfig Layout -> (Integer, String) -> RC (XConfig Layout)
|
|
parse xc (ln,s) = do
|
|
(k,v) <- readKV s ln
|
|
case M.lookup k commands of
|
|
Nothing -> throwError [(ln,"Unknown command: "++k)]
|
|
Just f -> f v ln xc
|
|
|
|
|
|
|
|
|
|
----------------------------------------------------------------
|
|
-- Now the semantic parts, that convert from the relevant --
|
|
-- key-value entries to values in an XConfig --
|
|
----------------------------------------------------------------
|
|
|
|
|
|
|
|
type Command = String -> Integer -> XConfig Layout -> RC (XConfig Layout)
|
|
|
|
commands :: M.Map String Command
|
|
commands = M.fromList $
|
|
[("modMask" , cmd_modMask )
|
|
,("numlockMask" , cmd_numlockMask )
|
|
,("normalBorderColor" , cmd_normalBorderColor )
|
|
,("focusedBorderColor" , cmd_focusedBorderColor)
|
|
,("terminal" , cmd_terminal )
|
|
,("workspaces" , cmd_workspaces )
|
|
,("focusFollowsMouse" , cmd_focusFollowsMouse )
|
|
,("layouts" , cmd_layouts )
|
|
,("key" , cmd_key )
|
|
,("manageHook" , cmd_manageHook )
|
|
,("borderWidth" , cmd_borderWidth )
|
|
]
|
|
|
|
|
|
-- | Behind-the-scenes helper for both 'cmd_modMask' and 'cmd_numlockMask'.
|
|
genericModKey :: (KeyMask -> XConfig Layout) -> Command
|
|
genericModKey f s ln _ = do
|
|
x <- rcRead s ln :: RC Integer
|
|
case lookup x (zip [1..] [mod1Mask,mod2Mask,mod3Mask,mod4Mask,mod5Mask]) of
|
|
Just y -> return $ f y
|
|
Nothing -> throwError [(ln,"Invalid mod key number: "++ show x)]
|
|
|
|
|
|
-- | Reads the mod key modifier number.
|
|
cmd_modMask :: Command
|
|
cmd_modMask s ln xc = genericModKey (\k -> xc{modMask = k}) s ln xc
|
|
|
|
-- | Reads the numlock key modifier number.
|
|
cmd_numlockMask :: Command
|
|
cmd_numlockMask s ln xc = genericModKey (\k -> xc{numlockMask = k}) s ln xc
|
|
|
|
|
|
-- | Reads the border width.
|
|
cmd_borderWidth :: Command
|
|
cmd_borderWidth s ln xc = do
|
|
w <- rcRead s ln
|
|
return $ xc { borderWidth = w }
|
|
|
|
|
|
-- | Reads the colors but just keeps them as RRGGBB Strings.
|
|
cmd_normalBorderColor, cmd_focusedBorderColor :: Command
|
|
cmd_normalBorderColor s _ xc = return $ xc{ normalBorderColor = s }
|
|
cmd_focusedBorderColor s _ xc = return $ xc{ focusedBorderColor = s }
|
|
|
|
|
|
-- | Reads the terminal. It is just a String, no parsing.
|
|
cmd_terminal :: Command
|
|
cmd_terminal s _ xc = return $ xc{ terminal = s }
|
|
|
|
|
|
-- | Reads the workspace tag list. This is given as a Haskell [String].
|
|
cmd_workspaces :: Command
|
|
cmd_workspaces s ln xc = rcRead s ln >>= \x -> return xc{ workspaces = x }
|
|
|
|
|
|
-- | Reads the focusFollowsMouse, as a Haskell Bool.
|
|
cmd_focusFollowsMouse :: Command
|
|
cmd_focusFollowsMouse s ln xc = rcRead s ln >>=
|
|
\x -> return xc{focusFollowsMouse = x}
|
|
|
|
|
|
-- | The list known layouts, mapped by name.
|
|
-- An easy location for improvement is to add more contrib layouts here.
|
|
layouts :: M.Map String (Layout Window)
|
|
layouts = M.fromList
|
|
[("Tall", Layout (Tall 1 (3/100) (1/2)))
|
|
,("Wide", Layout (Mirror (Tall 1 (3/100) (1/2))))
|
|
,("Full", Layout Full)
|
|
]
|
|
|
|
|
|
-- | Expects a [String], the strings being layout names. Quotes required.
|
|
-- Draws from the `layouts' list above.
|
|
cmd_layouts :: Command
|
|
cmd_layouts s ln xc = do
|
|
xs <- rcRead s ln -- read the list of strings
|
|
let ls = map (id &&& (flip M.lookup) layouts) xs
|
|
when (null ls) $ throwError [(ln,"Empty layout list")]
|
|
case filter (not . isJust . snd) ls of
|
|
[] -> return $ xc{ layoutHook = foldr1
|
|
(\(Layout l) (Layout r) ->
|
|
Layout (l ||| r)) (map (fromJust . snd) ls)
|
|
}
|
|
ys -> throwError $ map (\(x,_) -> (ln, "Unknown layout: "++ x)) ys
|
|
|
|
|
|
|
|
-- | A Map from names to key binding actions.
|
|
key_actions :: M.Map String (X ())
|
|
key_actions = M.fromList
|
|
[("kill" , kill )
|
|
,("nextLayout" , sendMessage NextLayout )
|
|
--,("prevLayout" , sendMessage PrevLayout )
|
|
--,("resetLayout" , setLayout $ XMonad.layoutHook conf)
|
|
,("refresh" , refresh )
|
|
,("focusDown" , windows W.focusDown )
|
|
,("focusUp" , windows W.focusUp )
|
|
,("focusMaster" , windows W.focusMaster )
|
|
,("swapMaster" , windows W.swapMaster )
|
|
,("swapDown" , windows W.swapDown )
|
|
,("swapUp" , windows W.swapUp )
|
|
,("shrink" , sendMessage Shrink )
|
|
,("expand" , sendMessage Expand )
|
|
,("sink" , withFocused $ windows . W.sink)
|
|
,("incMaster" , sendMessage (IncMasterN 1))
|
|
,("decMaster" , sendMessage (IncMasterN (-1)))
|
|
,("quit" , io $ exitWith ExitSuccess)
|
|
,("restart" , broadcastMessage ReleaseResources
|
|
>> restart "xmonad" True)
|
|
]
|
|
|
|
|
|
-- | Expects keys as described in the preamble, as
|
|
-- (\"EZConfig key name\", \"action name\"),
|
|
-- eg. (\"M-S-t\", \"spawn thunderbird\")
|
|
-- One key per "key=" line.
|
|
cmd_key :: Command
|
|
cmd_key s ln xc = do
|
|
(k,v) <- rcRead s ln
|
|
if "spawn " `isPrefixOf` v
|
|
then return $ xc {
|
|
keys = \c -> M.union (mkKeymap c
|
|
[(k, spawn (drop 6 v))]
|
|
) ((keys xc) c)
|
|
}
|
|
else do
|
|
case M.lookup v key_actions of
|
|
Nothing -> throwError [(ln, "Unknown key action \"" ++ v ++ "\"")]
|
|
Just ac -> return $
|
|
xc { keys = \c -> M.union (mkKeymap c [(k, ac)])
|
|
((keys xc) c)
|
|
}
|
|
|
|
|
|
|
|
-- | Map of names to actions for 'ManageHook's.
|
|
manageHook_actions :: M.Map String ManageHook
|
|
manageHook_actions = M.fromList
|
|
[("float" , doFloat )
|
|
,("ignore" , doIgnore )
|
|
]
|
|
|
|
|
|
-- | Parses 'ManageHook's in the form given in the preamble.
|
|
-- eg. (ClassName \"MPlayer\", \"float\")
|
|
cmd_manageHook :: Command
|
|
cmd_manageHook s ln xc = do
|
|
(k,v) <- rcRead s ln
|
|
let q = parseQuery k
|
|
if "toWorkspace " `isPrefixOf` v
|
|
then return $ xc { manageHook = manageHook xc <+>
|
|
(q --> doShift (drop 12 v))
|
|
}
|
|
else case M.lookup v manageHook_actions of
|
|
Nothing -> throwError [(ln, "Unknown ManageHook action \""
|
|
++ v ++ "\"")]
|
|
Just ac -> return $ xc { manageHook = manageHook xc <+> (q --> ac) }
|
|
|
|
|
|
|
|
-- | Core of the ManageHook expression parser.
|
|
-- Taken from Roman Cheplyaka's WindowProperties
|
|
parseQuery :: Property -> Query Bool
|
|
parseQuery (Title s) = title =? s
|
|
parseQuery (ClassName s) = className =? s
|
|
parseQuery (Resource s) = resource =? s
|
|
parseQuery (And p q) = parseQuery p <&&> parseQuery q
|
|
parseQuery (Or p q) = parseQuery p <&&> parseQuery q
|
|
parseQuery (Not p) = not `fmap` parseQuery p
|
|
parseQuery (Const b) = return b
|
|
|
|
|
|
-- | Property constructors are quite self-explaining.
|
|
-- Taken from Roman Cheplyaka's WindowProperties
|
|
data Property = Title String
|
|
| ClassName String
|
|
| Resource String
|
|
| And Property Property
|
|
| Or Property Property
|
|
| Not Property
|
|
| Const Bool
|
|
deriving (Read, Show)
|
|
|
|
|
|
|
|
-- | A wrapping of the read function into the RC monad.
|
|
rcRead :: (Read a) => String -> Integer -> RC a
|
|
rcRead s ln = case reads s of
|
|
[(x,"")] -> return x
|
|
_ -> throwError [(ln, "Failed to parse value")]
|
|
|
|
|
|
|
|
-- | The standard Config.hs 'defaultConfig', with the layout wrapped.
|
|
baseConfig :: XConfig Layout
|
|
baseConfig = defaultConfig{ layoutHook = Layout (layoutHook defaultConfig) }
|
|
|
|
|
|
|
|
-- | Core function that attempts to parse @~\/.xmonad\/xmonad.conf@
|
|
readConfig :: IO (Maybe (XConfig Layout))
|
|
readConfig = do
|
|
dir <- getXMonadDir
|
|
cs <- bracket (openFile (dir++"/xmonad.conf") ReadMode)
|
|
(\h -> hClose h) -- vv force the lazy IO
|
|
(\h -> (lines `fmap` hGetContents h) >>= \ss ->
|
|
length ss `seq` return ss)
|
|
let xce = runIdentity $ runErrorT $ parseFile cs
|
|
case xce of
|
|
Left es -> mapM_ (\(ln,e) ->
|
|
putStrLn $ "readConfig error: line "++show ln++
|
|
": "++ e) es
|
|
>> return Nothing
|
|
Right xc -> return $ Just xc
|
|
|
|
|
|
-- | Attempts to run readConfig, and checks if it failed.
|
|
checkConfig :: IO Bool
|
|
checkConfig = isJust `fmap` readConfig
|
|
|
|
|
|
|
|
{- REMOVED: It was for debugging, and causes an 'orphaned instances'
|
|
warning to boot.
|
|
|
|
|
|
|
|
-- | Reads in the config, and then prints the resulting XConfig
|
|
dumpConfig :: IO ()
|
|
dumpConfig = readConfig >>= print
|
|
|
|
|
|
instance Show (XConfig Layout) where
|
|
show x = "XConfig { "
|
|
++ "normalBorderColor = "++ normalBorderColor x ++", "
|
|
++ "focusedBorderColor = "++ focusedBorderColor x++", "
|
|
++ "terminal = "++ terminal x ++", "
|
|
++ "workspaces = "++ show (workspaces x) ++", "
|
|
++ "numlockMask = "++ show (numlockMask x) ++", "
|
|
++ "modMask = "++ show (modMask x) ++", "
|
|
++ "borderWidth = "++ show (borderWidth x) ++", "
|
|
++ "focusFollowsMouse = "++ show (focusFollowsMouse x) ++", "
|
|
++ "layouts = "++ show (layoutHook x) ++" }"
|
|
|
|
-}
|
|
|
|
-- | Handles the unwrapping of the Layout. Intended for use as
|
|
-- @main = plainConfig@
|
|
plainConfig :: IO ()
|
|
plainConfig = do
|
|
conf <- readConfig
|
|
case conf of
|
|
(Just xc@XConfig{layoutHook= (Layout l)}) ->
|
|
xmonad (xc{ layoutHook = l })
|
|
Nothing ->
|
|
spawn $ "xmessage Failed to read xmonad.conf. See xmonad.errors."
|
|
|