kmonad-0.4.5: src/KMonad/Args/Joiner.hs
{-# LANGUAGE CPP #-}
{-|
Module : KMonad.Args.Joiner
Description : The code that turns tokens into a DaemonCfg
Copyright : (c) David Janssen, 2019
License : MIT
Maintainer : janssen.dhj@gmail.com
Stability : experimental
Portability : non-portable (MPTC with FD, FFI to Linux-only c-code)
We perform configuration parsing in 2 steps:
- 1. We turn the text-file into a token representation
- 2. We check the tokens and turn them into an AppCfg
This module covers step 2.
NOTE: This is where we make a distinction between operating systems.
-}
module KMonad.Args.Joiner
( joinConfigIO
, joinConfig
)
where
import KMonad.Prelude hiding (uncons)
import KMonad.Util
import KMonad.Args.Types
import KMonad.Model.Action
import KMonad.Model.Button
import KMonad.Keyboard
import KMonad.Keyboard.IO
#ifdef linux_HOST_OS
import KMonad.Keyboard.IO.Linux.DeviceSource
import KMonad.Keyboard.IO.Linux.UinputSink
#endif
#ifdef mingw32_HOST_OS
import KMonad.Keyboard.IO.Windows.LowLevelHookSource
import KMonad.Keyboard.IO.Windows.SendEventSink
#endif
#ifdef darwin_HOST_OS
import KMonad.Keyboard.IO.Mac.IOKitSource
import KMonad.Keyboard.IO.Mac.KextSink
#endif
import Control.Monad.Except
import RIO.List (headMaybe, intersperse, uncons, sort, group, find)
import RIO.Partial (fromJust)
import qualified KMonad.Util.LayerStack as L
import qualified RIO.NonEmpty as NE
import qualified RIO.HashMap as M
import qualified RIO.Text as T
--------------------------------------------------------------------------------
-- $err
-- | All the things that can go wrong with a joining attempt
data JoinError
= DuplicateBlock Text
| MissingBlock Text
| DuplicateAlias Text
| CyclicAlias Text
| DuplicateLayer Text
| DuplicateSource (Maybe Text)
| DuplicateKeyInSource (Maybe Text) [Keycode]
| MissingAlias Text
| MissingLayer Text
| MissingSource (Maybe Text)
| MissingSetting Text
| DuplicateSetting Text
| DuplicateLayerSetting Text Text
| InvalidOS Text
| ImplArndDisabled
| NestedTrans
| InvalidComposeKey
| LengthMismatch Text Int Int
| NotAKeycode DefButton
instance Show JoinError where
show e = case e of
DuplicateBlock t -> "Encountered duplicate block of type: " <> T.unpack t
MissingBlock t -> "Missing at least 1 block of type: " <> T.unpack t
DuplicateAlias t -> "Multiple aliases of the same name: " <> T.unpack t
CyclicAlias t -> "Aliases references itself in a loop: " <> T.unpack t
DuplicateLayer t -> "Multiple layers of the same name: " <> T.unpack t
DuplicateSource t -> case t of
Just t -> "Multiple sources of the same name: " <> T.unpack t
Nothing -> "Multiple default sources"
DuplicateKeyInSource t ks -> case t of
Just t -> "Keycodes appear multiple times in source `" <> T.unpack t <> "`:" <> ((' ' :) . show =<< ks)
Nothing -> "Keycodes appear multiple times in default source: " <> ((' ' :) . show =<< ks)
MissingAlias t -> "Reference to non-existent alias: " <> T.unpack t
MissingLayer t -> "Reference to non-existent layer: " <> T.unpack t
MissingSource t -> case t of
Just t -> "Reference to non-existent source: " <> T.unpack t
Nothing -> "Reference to non-existent default source"
MissingSetting t -> "Missing setting in 'defcfg': " <> T.unpack t
DuplicateSetting t -> "Duplicate setting in 'defcfg': " <> T.unpack t
DuplicateLayerSetting t s -> "Duplicate setting in 'deflayer '" <> T.unpack t <> "': " <> T.unpack s
InvalidOS t -> "Not available under this OS: " <> T.unpack t
ImplArndDisabled -> "Implicit around via `A` or `S-a` are disabled in your config"
NestedTrans -> "Encountered 'Transparent' outside of top-level layer"
InvalidComposeKey -> "Encountered invalid button as Compose key"
LengthMismatch t l s -> mconcat
[ "Mismatch between length of 'defsrc' and deflayer <", T.unpack t, ">\n"
, "Source length: ", show s, "\n"
, "Layer length: ", show l ]
NotAKeycode b -> "Encountered non keycode button in 'defsrc': " <> show b
instance Exception JoinError
-- | Joining Config
data JCfg = JCfg
{ _cmpKey :: Button -- ^ How to prefix compose-sequences
, _implArnd :: ImplArnd -- ^ How to handle implicit `around`s
, _kes :: [KExpr] -- ^ The source expresions we operate on
}
makeLenses ''JCfg
defJCfg :: [KExpr] ->JCfg
defJCfg = JCfg
(emitB KeyRightAlt)
IAAround
-- | Monad in which we join, just Except over Reader
newtype J a = J { unJ :: ExceptT JoinError (Reader JCfg) a }
deriving ( Functor, Applicative, Monad
, MonadError JoinError , MonadReader JCfg)
-- | Perform a joining computation
runJ :: J a -> JCfg -> Either JoinError a
runJ j = runReader (runExceptT $ unJ j)
--------------------------------------------------------------------------------
-- $full
-- | Turn a list of KExpr into a CfgToken, throwing errors when encountered.
--
-- NOTE: We start joinConfig with the default JCfg, but joinConfig might locally
-- override settings by things it reads from the config itself.
joinConfigIO :: HasLogFunc e => [KExpr] -> RIO e CfgToken
joinConfigIO es = case runJ joinConfig $ defJCfg es of
Left e -> throwM e
Right c -> pure c
-- | Extract anything matching a particular prism from a list
extract :: Prism' a b -> [a] -> [b]
extract p = mapMaybe (preview p)
data SingletonError
= None
| Duplicate
-- | Take the head of a list, or else throw the appropriate error
onlyOne :: [a] -> Either SingletonError a
onlyOne xs = case uncons xs of
Just (x, []) -> Right x
Just _ -> Left Duplicate
Nothing -> Left None
-- | Take the one and only block matching the prism from the expressions
oneBlock :: Text -> Prism' KExpr a -> J a
oneBlock t l = (view kes <&> (extract l >>> onlyOne)) >>= \case
Right x -> pure x
Left None -> throwError $ MissingBlock t
Left Duplicate -> throwError $ DuplicateBlock t
-- | Update the JCfg and then run the entire joining process
joinConfig :: J CfgToken
joinConfig = getOverride >>= \cfg -> local (const cfg) joinConfig'
-- | Join an entire 'CfgToken' from the current list of 'KExpr'.
joinConfig' :: J CfgToken
joinConfig' = do
es <- view kes
-- Extract the IO settings
i <- getI
o <- getO
ft <- getFT
al <- getAllow
ksd <- getKeySeqDelay
-- Extract the other blocks and join them into a keymap
let als = extract _KDefAlias es
let lys = extract _KDefLayer es
let srcs = extract _KDefSrc es
(km, fl) <- joinKeymap srcs als lys
pure $ CfgToken
{ _snk = o
, _src = i
, _km = km
, _fstL = fl
, _flt = ft
, _allow = al
, _ksd = ksd
}
--------------------------------------------------------------------------------
-- $settings
--
-- TODO: This needs to be seriously refactored: all this code duplication is a
-- sign that something is amiss.
-- | Return a JCfg with all settings from defcfg applied to the env's JCfg
getOverride :: J JCfg
getOverride = do
-- FIXME: duplicates don't throw errors
env <- ask
cfg <- oneBlock "defcfg" _KDefCfg
let getB = joinButton [] M.empty
let go e v = case v of
SCmpSeq b -> getB b >>= maybe (throwError InvalidComposeKey)
(\b' -> pure $ set cmpKey b' e)
SImplArnd ia -> pure $ set implArnd ia e
_ -> pure e
foldM go env cfg
-- | Turn a 'HasLogFunc'-only RIO into a function from LogFunc to IO
runLF :: HasLogFunc lf => RIO lf a -> lf -> IO a
runLF = flip runRIO
-- | Extract the KeySource-loader from the 'KExpr's
getI :: J (LogFunc -> IO (Acquire KeySource))
getI = do
cfg <- oneBlock "defcfg" _KDefCfg
case onlyOne . extract _SIToken $ cfg of
Right i -> pickInput i
Left None -> throwError $ MissingSetting "input"
Left Duplicate -> throwError $ DuplicateSetting "input"
-- | Extract the KeySource-loader from a 'KExpr's
getO :: J (LogFunc -> IO (Acquire KeySink))
getO = do
cfg <- oneBlock "defcfg" _KDefCfg
case onlyOne . extract _SOToken $ cfg of
Right o -> pickOutput o
Left None -> throwError $ MissingSetting "output"
Left Duplicate -> throwError $ DuplicateSetting "output"
-- | Extract the fallthrough setting
getFT :: J Bool
getFT = do
cfg <- oneBlock "defcfg" _KDefCfg
case onlyOne . extract _SFallThrough $ cfg of
Right b -> pure b
Left None -> pure False
Left Duplicate -> throwError $ DuplicateSetting "fallthrough"
-- | Extract the allow-cmd setting
getAllow :: J Bool
getAllow = do
cfg <- oneBlock "defcfg" _KDefCfg
case onlyOne . extract _SAllowCmd $ cfg of
Right b -> pure b
Left None -> pure False
Left Duplicate -> throwError $ DuplicateSetting "allow-cmd"
-- | Extract the cmp-seq-delay setting
getCmpSeqDelay :: J (Maybe Int)
getCmpSeqDelay = do
cfg <- oneBlock "defcfg" _KDefCfg
case onlyOne . extract _SCmpSeqDelay $ cfg of
Right 0 -> pure Nothing
Right b -> pure (Just b)
Left None -> pure Nothing
Left Duplicate -> throwError $ DuplicateSetting "cmp-seq-delay"
-- | Extract the key-seq-delay setting
getKeySeqDelay :: J (Maybe Int)
getKeySeqDelay = do
cfg <- oneBlock "defcfg" _KDefCfg
case onlyOne . extract _SKeySeqDelay $ cfg of
Right 0 -> pure Nothing
Right b -> pure (Just b)
Left None -> pure (Just 1)
Left Duplicate -> throwError $ DuplicateSetting "key-seq-delay"
#ifdef linux_HOST_OS
-- | The Linux correspondence between IToken and actual code
pickInput :: IToken -> J (LogFunc -> IO (Acquire KeySource))
pickInput (KDeviceSource f im) = pure $ runLF (deviceSource64 f im)
pickInput KLowLevelHookSource = throwError $ InvalidOS "LowLevelHookSource"
pickInput (KIOKitSource _) = throwError $ InvalidOS "IOKitSource"
-- | The Linux correspondence between OToken and actual code
pickOutput :: OToken -> J (LogFunc -> IO (Acquire KeySink))
pickOutput (KUinputSink t init) = pure $ runLF (uinputSink cfg)
where cfg = defUinputCfg { _keyboardName = T.unpack t
, _postInit = T.unpack <$> init }
pickOutput (KSendEventSink _) = throwError $ InvalidOS "SendEventSink"
pickOutput KKextSink = throwError $ InvalidOS "KextSink"
#endif
#ifdef mingw32_HOST_OS
-- | The Windows correspondence between IToken and actual code
pickInput :: IToken -> J (LogFunc -> IO (Acquire KeySource))
pickInput KLowLevelHookSource = pure $ runLF llHook
pickInput (KDeviceSource _ _) = throwError $ InvalidOS "DeviceSource"
pickInput (KIOKitSource _) = throwError $ InvalidOS "IOKitSource"
-- | The Windows correspondence between OToken and actual code
pickOutput :: OToken -> J (LogFunc -> IO (Acquire KeySink))
pickOutput (KSendEventSink di) = pure $ runLF (sendEventKeySink di)
pickOutput (KUinputSink _ _) = throwError $ InvalidOS "UinputSink"
pickOutput KKextSink = throwError $ InvalidOS "KextSink"
#endif
#ifdef darwin_HOST_OS
-- | The Mac correspondence between IToken and actual code
pickInput :: IToken -> J (LogFunc -> IO (Acquire KeySource))
pickInput (KIOKitSource name) = pure $ runLF (iokitSource (T.unpack <$> name))
pickInput (KDeviceSource _ _) = throwError $ InvalidOS "DeviceSource"
pickInput KLowLevelHookSource = throwError $ InvalidOS "LowLevelHookSource"
-- | The Mac correspondence between OToken and actual code
pickOutput :: OToken -> J (LogFunc -> IO (Acquire KeySink))
pickOutput KKextSink = pure $ runLF kextSink
pickOutput (KUinputSink _ _) = throwError $ InvalidOS "UinputSink"
pickOutput (KSendEventSink _) = throwError $ InvalidOS "SendEventSink"
#endif
--------------------------------------------------------------------------------
-- $als
type Aliases = M.HashMap Text DefButton
type LNames = [Text]
-- | Build up a hashmap of text to button mappings
--
-- Aliases can refer back to other aliases.
joinAliases :: [DefAlias] -> J Aliases
joinAliases als = do
let als' = concat als
case find (not . null . NE.tail) $ NE.groupAllWith fst als' of
Nothing -> pure ()
Just ((t, _) :| _) -> throwError $ DuplicateAlias t
M.traverseWithKey =<< go $ M.fromList als'
where
go mp t b@(KRef t')
| t == t' = throwError $ CyclicAlias t
| otherwise = withDerefAlias mp (go mp t) b
go _ _ b = pure b
withDerefAlias :: Aliases -> (DefButton -> J a) -> DefButton -> J a
withDerefAlias als f (KRef t) = case M.lookup t als of
Just b -> f b
Nothing -> throwError $ MissingAlias t
withDerefAlias _ f b = f b
--------------------------------------------------------------------------------
-- $but
-- | Turn 'Nothing's (caused by joining a KTrans) into the appropriate error.
-- KTrans buttons may only occur in 'DefLayer' definitions.
unnest :: J (Maybe Button) -> J Button
unnest = (maybe (throwError NestedTrans) pure =<<)
fromImplArnd :: DefButton -> DefButton -> ImplArnd -> J DefButton
fromImplArnd _ _ IADisabled = throwError ImplArndDisabled
fromImplArnd o i IAAround = pure $ KAround o i
fromImplArnd o i IAAroundOnly = pure $ KAroundOnly o i
fromImplArnd o i IAAroundWhenAlone = pure $ KAroundWhenAlone o i
-- | Turn a button token into an actual KMonad `Button` value
joinButton :: LNames -> Aliases -> DefButton -> J (Maybe Button)
joinButton ns als =
-- Define some utility functions
let ret = pure . Just
go = unnest . joinButton ns als
jst = fmap Just
fi = fromIntegral
isps l = traverse go . maybe l ((`intersperse` l) . KPause . fi)
in \case
-- Variable dereference
b@(KRef _) -> withDerefAlias als (joinButton ns als) b
-- Various simple buttons
KEmit c -> ret $ emitB c
KPressOnly c -> ret $ pressOnly c
KReleaseOnly c -> ret $ releaseOnly c
KCommand pr mbR -> ret $ cmdButton pr mbR
KLayerToggle t -> if t `elem` ns
then ret $ layerToggle t
else throwError $ MissingLayer t
KLayerSwitch t -> if t `elem` ns
then ret $ layerSwitch t
else throwError $ MissingLayer t
KLayerAdd t -> if t `elem` ns
then ret $ layerAdd t
else throwError $ MissingLayer t
KLayerRem t -> if t `elem` ns
then ret $ layerRem t
else throwError $ MissingLayer t
KLayerDelay s t -> if t `elem` ns
then ret $ layerDelay (fi s) t
else throwError $ MissingLayer t
KLayerNext t -> if t `elem` ns
then ret $ layerNext t
else throwError $ MissingLayer t
-- Various compound buttons
KComposeSeq bs -> do csd <- getCmpSeqDelay
c <- view cmpKey
csd' <- for csd $ go . KPause . fi
jst $ tapMacro . (c:) . maybe id (:) csd' <$> isps bs csd
KTapMacro bs mbD -> jst $ tapMacro <$> isps bs mbD
KBeforeAfterNext b a -> jst $ beforeAfterNext <$> go b <*> go a
KTapMacroRelease bs mbD ->
jst $ tapMacroRelease <$> isps bs mbD
KAround o i -> jst $ around <$> go o <*> go i
KTapNext t h -> jst $ tapNext <$> go t <*> go h
KTapHold s t h -> jst $ tapHold (fi s) <$> go t <*> go h
KTapHoldNext s t h mtb
-> jst $ tapHoldNext (fi s) <$> go t <*> go h <*> traverse go mtb
KTapNextRelease t h -> jst $ tapNextRelease <$> go t <*> go h
KTapHoldNextRelease ms t h mtb
-> jst $ tapHoldNextRelease (fi ms) <$> go t <*> go h <*> traverse go mtb
KTapNextPress t h -> jst $ tapNextPress <$> go t <*> go h
KTapHoldNextPress ms t h mtb
-> jst $ tapHoldNextPress (fi ms) <$> go t <*> go h <*> traverse go mtb
KAroundOnly o i -> jst $ aroundOnly <$> go o <*> go i
KAroundWhenAlone o i -> jst $ aroundWhenAlone <$> go o <*> go i
KAroundImplicit o i -> joinButton ns als =<< fromImplArnd o i =<< view implArnd
KAroundNext b -> jst $ aroundNext <$> go b
KAroundNextSingle b -> jst $ aroundNextSingle <$> go b
KAroundNextTimeout ms b t -> jst $ aroundNextTimeout (fi ms) <$> go b <*> go t
KPause ms -> jst . pure $ onPress (pause ms)
KMultiTap bs d -> jst $ multiTap <$> go d <*> mapM f bs
where f (ms, b) = (fi ms,) <$> go b
KStepped bs -> jst $ steppedButton <$> mapM go bs
KStickyKey s d -> jst $ stickyKey (fi s) <$> go d
-- Non-action buttons
KTrans -> pure Nothing
KBlock -> ret pass
--------------------------------------------------------------------------------
-- $src
type Sources = M.HashMap (Maybe Text) [Maybe Keycode]
-- | Build up a hashmap of text to source mappings.
joinSources :: Aliases -> [DefSrc] -> J Sources
joinSources als = foldM joiner mempty
where
joiner :: Sources -> DefSrc -> J Sources
joiner sources DefSrc{ _srcName = n, _keycodes = ks }
| n `M.member` sources = throwError $ DuplicateSource n
| otherwise = do
ks' <- for ks $ withDerefAlias als joinKeycode
let dups = NE.head <$> duplicatesWith id (toList =<< ks')
unless (null dups) $ throwError $ DuplicateKeyInSource n dups
pure $ M.insert n ks' sources
where
joinKeycode (KEmit k) = pure $ Just k
joinKeycode KBlock = pure Nothing
joinKeycode b = throwError $ NotAKeycode b
--------------------------------------------------------------------------------
-- $kmap
-- | Join the defsrc, defalias, and deflayer layers into a Keymap of buttons and
-- the name signifying the initial layer to load.
joinKeymap :: [DefSrc] -> [DefAlias] -> [DefLayer] -> J (LMap Button, LayerTag)
joinKeymap [] _ _ = throwError $ MissingBlock "defsrc"
joinKeymap _ _ [] = throwError $ MissingBlock "deflayer"
joinKeymap srcs als lys = do
let f acc x = if x `elem` acc then throwError $ DuplicateLayer x else pure (x:acc)
nms <- foldM f [] $ map _layerName lys -- Extract all names
als' <- joinAliases als -- Join aliases into 1 hashmap
srcs' <- joinSources als' srcs -- Join all sources into 1 hashmap
lys' <- mapM (joinLayer als' nms srcs') lys -- Join all layers
-- Return the layerstack and the name of the first layer
pure (L.mkLayerStack lys', _layerName . fromJust . headMaybe $ lys)
-- | Check and join 1 deflayer.
joinLayer ::
Aliases -- ^ Mapping of names to buttons
-> LNames -- ^ List of valid layer names
-> Sources -- ^ Mapping of names to source layer
-> DefLayer -- ^ The layer token to join
-> J (Text, [(Keycode, Button)]) -- ^ The resulting tuple
joinLayer als ns srcs l@(DefLayer n settings) = do
let bs = settings ^.. each . _LButton
assocSrc <- getAssocSrc l
implAround <- getImplAround l
src <- case M.lookup assocSrc srcs of
Just src -> pure src
Nothing -> throwError $ MissingSource assocSrc
-- Ensure length-match between src and buttons
when (length bs /= length src) $
throwError $ LengthMismatch n (length bs) (length src)
-- Join each button and add it (filtering out KTrans)
let
f acc (Nothing, b) = joinButton ns als b $> acc
f acc (Just kc, b) = joinButton ns als b >>= \case
Nothing -> pure acc
Just b' -> pure $ (kc, b') : acc
maybe id (local . set implArnd) implAround $
(n,) <$> foldM f [] (zip src bs)
getAssocSrc :: DefLayer -> J (Maybe Text)
getAssocSrc (DefLayer n settings) = case onlyOne (settings ^.. each . _LSrcName) of
Right x -> pure $ Just x
Left None -> pure Nothing
Left Duplicate -> throwError $ DuplicateLayerSetting n "source"
getImplAround :: DefLayer -> J (Maybe ImplArnd)
getImplAround (DefLayer n settings) = case onlyOne (settings ^.. each . _LImplArnd) of
Right x -> pure $ Just x
Left None -> pure Nothing
Left Duplicate -> throwError $ DuplicateLayerSetting n "implicit-around"
--------------------------------------------------------------------------------
-- $test
-- fname :: String
-- fname = "/home/david/prj/hask/kmonad/doc/example.kbd"
-- test :: IO (J DefCfg)
-- test = runRIO () . fmap joinConfig $ loadTokens fname