kmonad-0.4.2: src/KMonad/App/Main.hs
{-# LANGUAGE CPP #-}
{-|
Module : KMonad.App.Main
Description : The entry-point to KMonad
Copyright : (c) David Janssen, 2021
License : MIT
Maintainer : janssen.dhj@gmail.com
Stability : experimental
Portability : non-portable
-}
module KMonad.App.Main
( -- * The entry-point to KMonad
main
)
where
import KMonad.Prelude
import KMonad.Args
import KMonad.App.Types
import KMonad.Keyboard
import KMonad.Util
import KMonad.Model
import qualified KMonad.Model.Dispatch as Dp
import qualified KMonad.Model.Hooks as Hs
import qualified KMonad.Model.Sluice as Sl
import qualified KMonad.Model.Keymap as Km
-- FIXME: This should live somewhere else
#ifdef linux_HOST_OS
import System.Posix.Signals (Handler(Ignore), installHandler, sigCHLD)
#endif
--------------------------------------------------------------------------------
-- $start
--
-- How to start KMonad
-- | The first command in KMonad
--
-- Get the invocation from the command-line, then do something with it.
main :: IO ()
main = getCmd >>= runCmd
-- | Execute the provided 'Cmd'
--
-- 1. Construct the log-func
-- 2. Parse the config-file
-- 3. Maybe start KMonad
runCmd :: Cmd -> IO ()
runCmd c = do
o <- logOptionsHandle stdout False <&> setLogMinLevel (c^.logLvl)
withLogFunc o $ \f -> runRIO f $ do
cfg <- loadConfig c
unless (c^.dryRun) $ startApp cfg
--------------------------------------------------------------------------------
-- $init
--
-- The steps required to turn a configuration into the initial KMonad env
-- | Initialize all the components of the KMonad app-loop
--
-- NOTE: This is written in 'ContT' over our normal RIO monad. This is just to
-- to simplify a bunch of nesting of calls. At no point do we make use of
-- 'callCC' or other 'ContT' functionality.
--
initAppEnv :: HasLogFunc e => AppCfg -> ContT r (RIO e) AppEnv
initAppEnv cfg = do
-- Get a reference to the logging function
lgf <- view logFuncL
-- Wait a bit for the user to release the 'Return' key with which they started KMonad
threadDelay $ fromIntegral (cfg^.startDelay) * 1000
-- Acquire the keysource and keysink
snk <- using $ cfg^.keySinkDev
src <- using $ cfg^.keySourceDev
-- Initialize the pull-chain components
dsp <- Dp.mkDispatch $ awaitKey src
ihk <- Hs.mkHooks $ Dp.pull dsp
slc <- Sl.mkSluice $ Hs.pull ihk
-- Initialize the button environments in the keymap
phl <- Km.mkKeymap (cfg^.firstLayer) (cfg^.keymapCfg)
-- Initialize output components
otv <- lift newEmptyTMVarIO
ohk <- Hs.mkHooks . atomically . takeTMVar $ otv
-- Setup thread to read from outHooks and emit to keysink
launch_ "emitter_proc" $ do
e <- atomically . takeTMVar $ otv
emitKey snk e
-- emit e = view keySink >>= flip emitKey e
pure $ AppEnv
{ _keAppCfg = cfg
, _keLogFunc = lgf
, _keySink = snk
, _keySource = src
, _dispatch = dsp
, _inHooks = ihk
, _sluice = slc
, _keymap = phl
, _outHooks = ohk
, _outVar = otv
}
--------------------------------------------------------------------------------
-- $emit
--
-- How to use a KMonad env to emit keys to the OS
--
-- FIXME: this needs to live somewhere else
-- | Trigger the button-action press currently registered to 'Keycode'
pressKey :: (HasAppEnv e, HasLogFunc e, HasAppCfg e) => Keycode -> RIO e ()
pressKey c =
view keymap >>= flip Km.lookupKey c >>= \case
-- If the keycode does not occur in our keymap
Nothing -> do
ft <- view fallThrough
when ft $ do
emit $ mkPress c
await (isReleaseOf c) $ \_ -> do
emit $ mkRelease c
pure Catch
-- If the keycode does occur in our keymap
Just b -> runBEnv b Press >>= \case
Nothing -> pure () -- If the previous action on this key was *not* a release
Just a -> do
-- Execute the press and register the release
app <- view appEnv
runRIO (KEnv app b) $ do
runAction a
awaitMy Release $ do
runBEnv b Release >>= \case
Nothing -> pure ()
Just a -> runAction a
pure Catch
--------------------------------------------------------------------------------
-- $loop
--
-- The app-loop of KMonad
-- | Perform 1 step of KMonad's app loop
--
-- We forever:
-- 1. Pull from the pull-chain until an unhandled event reaches us.
-- 2. If that event is a 'Press' we use our keymap to trigger an action.
loop :: RIO AppEnv ()
loop = forever $ view sluice >>= Sl.pull >>= \case
e | e^.switch == Press -> pressKey $ e^.keycode
_ -> pure ()
-- | Run KMonad using the provided configuration
startApp :: HasLogFunc e => AppCfg -> RIO e ()
startApp c = do
#ifdef linux_HOST_OS
-- Ignore SIGCHLD to avoid zombie processes.
liftIO . void $ installHandler sigCHLD Ignore Nothing
#endif
runContT (initAppEnv c) (`runRIO` loop)