-- | Specify your key-bindings with a description and zero or more tags, -- then add a key-binding to search through them. module XMonad.Config.DescriptiveKeys ( -- * Usage -- $usage -- * Description Description(..) , DescriptiveKey(..) , defaultDescriptiveKey -- * `DescriptiveKeys` data structure , DescriptiveKeys , descriptiveKeys , wKeys , setDescriptiveKeys -- * Tags , Tag(..) , Tags , allTags , SearchTags(..) , defaultSearchTags , filterTags -- * Pretty-printing the key description , DescriptiveKeysPP(..) , defaultDescriptiveKeysPP -- * The prompt text when searching , SearchTextPrompt(..) , defaultSearchTextPrompt -- * The action to take to describe the keys , DescribeKeys(..) , defaultDescribeKeys -- * Configuration , HelpPromptConfig(..) , helpPrompt , helpPromptAndSet , defaultHelpPromptAndSet ) where import qualified Data.Set as S import qualified Data.Map as M import qualified Data.Foldable as F import XMonad import XMonad.Prompt import XMonad.Prompt.Input import Data.Bits import Data.List -- $usage -- -- You can use this module with the following in your @~\/.xmonad\/xmonad.hs@: -- -- > import XMonad.Config.DescriptiveKeys -- -- -- Create an instance of `DescriptiveKeys` by annotating key-bindings with a description and list of tags. -- -- > myDescriptiveKeys = wKeys $ \c -> [(modMask c, xk_z, spawn "xclock"), "opens xclock", ["open", "xclock"] -- -- Modify your `XConfig` with a configuration combinator. Following is the simplest. -- -- > myXConfig = defaultHelpPromptAndSet myDescriptiveKeys myXPConfig xConfig -- -- This will set the keys property of the `XConfig` and a key-binding of mod-F1 to search. -- | Wraps a string to create a tag. newtype Tag = Tag String deriving (Eq, Ord, Show) -- | A set of tags. type Tags = S.Set Tag -- | Wraps an optional string to denote a description for a key-binding. newtype Description = Description (Maybe String) deriving (Eq, Ord, Show) -- | The data structure that denotes an annotated key-binding. data DescriptiveKey = DescriptiveKey { mask :: ButtonMask , sym :: KeySym , action :: X () , description :: Description , tags :: Tags } -- | A default key-binding that has no description or tags. defaultDescriptiveKey :: ButtonMask -> KeySym -> X () -> DescriptiveKey defaultDescriptiveKey m s a = DescriptiveKey m s a (Description Nothing) S.empty -- | A list of descriptive key-bindings that have access to the `XConfig`. newtype DescriptiveKeys = DescriptiveKeys (XConfig Layout -> [DescriptiveKey]) -- | Construct a list of descriptive key-bindings. descriptiveKeys :: (XConfig Layout -> [DescriptiveKey]) -> DescriptiveKeys descriptiveKeys = DescriptiveKeys -- | Construct a list of descriptive key-bindings by specifying the description as a string -- and the tags as a list of strings. wKeys :: (XConfig Layout -> [(KeyMask, KeySym, X(), String, [String])]) -> DescriptiveKeys wKeys z = descriptiveKeys (fmap (\(m, s, a, d, t) -> DescriptiveKey m s a (Description (Just d)) (S.fromList (fmap Tag t))) . z) -- | Sets the `keys` property of the given `XConfig` with the given descriptive key-bindings. setDescriptiveKeys :: DescriptiveKeys -> XConfig l -> XConfig l setDescriptiveKeys k l = let rawKeys (DescriptiveKeys d) = F.foldl' (\p (DescriptiveKey m s a _ _) -> M.insert (m, s) a p) M.empty . d in l { keys = rawKeys k } -- | Returns all the tags for a given list of key-bindings. allTags :: XConfig Layout -> DescriptiveKeys -> Tags allTags l (DescriptiveKeys k) = S.unions (fmap tags (k l)) -- | How to produce a set of tags from a string, which will likely come from user-input. newtype SearchTags = SearchTags { searchTags :: String -> Tags } -- | Splits a string by spaces to produce a set of tags. defaultSearchTags :: SearchTags defaultSearchTags = SearchTags (S.fromList . fmap Tag . words) -- | Removes all descriptive key-bindings that are not in the given set of tags. filterTags :: Tags -> DescriptiveKeys -> DescriptiveKeys filterTags t z@(DescriptiveKeys k) = if S.null t then z else DescriptiveKeys (\l -> filter (\(DescriptiveKey _ _ _ _ u) -> not (S.null (S.intersection t u))) $ k l) -- | A pretty-printer for descriptive key-bindings. data DescriptiveKeysPP = DescriptiveKeysPP ([DescriptiveKey] -> String) -- | A plain-text pretty-printer that takes particular care of mod/mask keys and spacing. defaultDescriptiveKeysPP :: DescriptiveKeysPP defaultDescriptiveKeysPP = DescriptiveKeysPP (unlines . fmap (\(DescriptiveKey m s _ d _) -> let pick n str = if n .&. complement m == 0 then str else "" mk = concatMap (++"-") . filter (not . null) . map (uncurry pick) $ [ (mod1Mask, "mod") , (mod2Mask, "mod") , (mod3Mask, "mod") , (mod4Mask, "mod") , (mod5Mask, "mod") , (controlMask, "cntrl") , (shiftMask, "shift") ] space g = g ++ replicate (16 - length g) ' ' in space (mk ++ keysymToString s) ++ case d of Description Nothing -> "" Description (Just e) -> " " ++ e)) -- | The prompt text when searching newtype SearchTextPrompt = SearchTextPrompt String deriving (Eq, Ord, Show) -- | The default search prompt, @Search key-bindings@ defaultSearchTextPrompt :: SearchTextPrompt defaultSearchTextPrompt = SearchTextPrompt "Search key-bindings" -- | The action to take to describe key-bindings from a string user-input. newtype DescribeKeys = DescribeKeys { describeKeys :: String -> X () } -- | A default that opens @xmessage@ and uses the default pretty-printer. defaultDescribeKeys :: DescriptiveKeys -> XConfig Layout -> DescribeKeys defaultDescribeKeys k l = let dk (DescriptiveKeys g) = g pp (DescriptiveKeysPP p) = p j s = dk (filterTags (searchTags defaultSearchTags s) k) l in DescribeKeys (\z -> spawn ("xmessage \"" ++ pp defaultDescriptiveKeysPP (j z) ++ "\"")) -- | The attributes required to do the final configuration of the descriptive key-bindings. data HelpPromptConfig = HelpPromptConfig { descriptiveHelp :: DescriptiveKeys -- ^ The descriptive key-bindings. , xpConfigHelp :: XPConfig -- ^ The `XPConfig` that is used. , keyHelp :: (ButtonMask, KeySym) -- ^ The key-binding to prompt the user to search. , searchTextHelp :: SearchTextPrompt -- ^ The search text prompt. , describeHelp :: DescribeKeys -- ^ The action to take after string user-input. } -- | Sets the help prompt on the given `XPConfig`. helpPrompt :: (XConfig Layout -> HelpPromptConfig) -> XConfig l -> XConfig l helpPrompt f c = c { keys = \d -> let HelpPromptConfig ks xpc ms (SearchTextPrompt stp) (DescribeKeys describek) = f d compl s = return $ filter (isPrefixOf s) . fmap (\(Tag t) -> t) $ S.toList (allTags d ks) in M.insert ms (inputPromptWithCompl xpc stp compl ?+ describek) (keys c d) } -- | Sets the help prompt on the given `XPConfig` and sets the `keys` attribute. helpPromptAndSet :: DescriptiveKeys -> XPConfig -> (ButtonMask, KeySym) -> SearchTextPrompt -> (XConfig Layout -> DescribeKeys) -> XConfig l -> XConfig l helpPromptAndSet k c m s d = helpPrompt (\l -> HelpPromptConfig { descriptiveHelp = k , xpConfigHelp = c , keyHelp = m , searchTextHelp = s , describeHelp = d l }) . setDescriptiveKeys k -- | Sets the help prompt on the given `XPConfig` and sets the `keys` attribute with a default -- key-binding of mod-F1, default search text prompt and using @xmessage@ to provide the descriptive response. defaultHelpPromptAndSet :: DescriptiveKeys -> XPConfig -> XConfig l -> XConfig l defaultHelpPromptAndSet k c = helpPromptAndSet k c (mod4Mask, xK_F1) defaultSearchTextPrompt (defaultDescribeKeys k)