-- | -- Module: I3IPC -- Copyright: (c) 2019 Evan Cameron -- License: BSD3 -- Maintainer: Evan Cameron -- -- Types and functions for interacting with i3's IPC mechanism -- module I3IPC ( -- ** Subscribe to events -- $sub -- ** Sending messages -- $msg -- ** Convenience functions -- $func getSocketPath , getSwaySocketPath , Response(..) , subscribe , subscribeM , receive , receive' , receiveMsg , receiveMsg' , getReply , connecti3 , connectsway , receiveEvent , receiveEvent' , runCommand , runCommand' , getWorkspaces , getWorkspaces' , getOutputs , getOutputs' , getTree , getTree' , getMarks , getMarks' , getVersion , getVersion' , getBarConfig , getBarConfig' , getBarIds , getBindingModes , getBindingModes' , getConfig , getConfig' , getTick , getTick' , getSync , getSync' ) where import qualified I3IPC.Message as Msg import qualified I3IPC.Subscribe as Sub import qualified I3IPC.Event as Evt import I3IPC.Reply import Control.Monad.IO.Class import Control.Exception ( Exception ) import Control.Monad.Catch ( MonadThrow , throwM ) import System.Environment ( lookupEnv ) import Data.Maybe ( isJust ) import Data.Semigroup ( (<>) ) import System.Process.Typed ( proc , readProcess ) import System.Exit ( ExitCode(..) ) import Network.Socket hiding ( send , sendTo , recv , recvFrom ) import Network.Socket.ByteString.Lazy import Data.Aeson ( encode ) import Data.Binary.Get import Data.Bifunctor ( second ) import qualified Data.ByteString.Lazy.Char8 as BL import Data.Typeable ( Typeable ) import qualified Data.Text as T import Data.Bits ( testBit , clearBit ) -- | Exception type data I3Exception = ConnectException T.Text | ProcessException deriving stock (Show, Eq, Typeable) instance Exception I3Exception -- | Get a new unix socket path from i3 getSocketPath :: MonadIO m => m (Maybe BL.ByteString) getSocketPath = do res <- liftIO $ lookupEnv "I3SOCK" if isJust res then pure $ fmap BL.pack res else do (exitCode, out, _) <- readProcess $ proc "i3" ["--get-socketpath"] if exitCode /= ExitSuccess then pure Nothing else pure $ Just (BL.filter (/= '\n') out) -- | Get a new unix socket path from sway getSwaySocketPath :: MonadIO m => m (Maybe BL.ByteString) getSwaySocketPath = fmap BL.pack <$> liftIO (lookupEnv "SWAYSOCK") -- | Subscribe with a list of 'I3IPC.Subscribe.Subscribe' types, and subscribe will to respond with specific 'I3IPC.Event.Event' subscribe :: (MonadThrow m, MonadIO m) => (Either String Evt.Event -> m ()) -> [Sub.Subscribe] -> m () subscribe handle subtypes = do soc <- connecti3 Msg.sendMsgPayload soc Msg.Subscribe (encode subtypes) >> receiveMsg soc >> handleSoc soc >> liftIO (close soc) where handleSoc soc = do r <- receiveEvent soc handle r handleSoc soc -- | A version of 'subscribe' that allows the use of any monad transformer on top of MonadIO (kept around for backwards compatibility) subscribeM :: (MonadThrow m, MonadIO m) => (Either String Evt.Event -> m ()) -> [Sub.Subscribe] -> m () subscribeM handle subtypes = do soc <- connecti3 Msg.sendMsgPayload soc Msg.Subscribe (encode subtypes) >> receiveMsg soc >> pure () handleSoc soc >> liftIO (close soc) where handleSoc soc = do r <- receiveEvent soc handle r handleSoc soc -- | Connect to an i3 socket and return it connecti3 :: (MonadThrow m, MonadIO m) => m Socket connecti3 = do soc <- liftIO $ socket AF_UNIX Stream 0 getSocketPath >>= \case Nothing -> throwM $ ConnectException "Failed to get i3 socket path" Just addr' -> do liftIO $ connect soc (SockAddrUnix $ BL.unpack addr') pure soc -- | Connect to SWAY socket and return it connectsway :: (MonadThrow m, MonadIO m) => m Socket connectsway = do soc <- liftIO $ socket AF_UNIX Stream 0 getSwaySocketPath >>= \case Nothing -> throwM $ ConnectException "Failed to get i3 socket path" Just addr' -> do liftIO $ connect soc (SockAddrUnix $ BL.unpack addr') pure soc -- | Useful for when you are receiving Events or Messages. data Response = Message MsgReply | Event Evt.Event deriving (Show, Eq) -- | Get and parse the response using i3's IPC getReply :: MonadIO m => Socket -> m (Either String (Int, BL.ByteString)) getReply soc = do magic <- liftIO $ recv soc 6 if magic == "i3-ipc" then do len <- getInt <$> liftIO (recv soc 4) ty <- getInt <$> liftIO (recv soc 4) body <- liftIO $ recv soc len pure $ Right (fromIntegral ty, body) else pure $ Left "Failed to get reply" where getInt = fromIntegral . runGet getWord32le test :: Int -> BL.ByteString -> IO Int test ty body = do putStrLn $ "type " <> show (ty `clearBit` 31) BL.putStrLn $ "body " <> body BL.putStrLn "" pure ty -- | Parse response from socket, returning either an error or a 'I3IPC.Response', representing a sum type of a 'I3IPC.Reply.MsgReply' or 'I3IPC.Event.Event' receive :: MonadIO m => Socket -> m (Either String Response) receive soc = do reply <- getReply soc case reply of Right (ty, body) -> pure $ if testBit ty 31 then Event `second` Evt.toEvent (ty `clearBit` 31) body else Message `second` toMsgReply ty body _ -> pure $ Left "Get Reply failed" -- | Like receive but strict-- will use eitherDecode' under the hood to parse receive' :: MonadIO m => Socket -> m (Either String Response) receive' soc = do reply <- getReply soc case reply of Right (ty, body) -> pure $ if testBit ty 31 then Event `second` Evt.toEvent' (ty `clearBit` 31) body else Message `second` toMsgReply' ty body _ -> pure $ Left "Get Reply failed" -- | Receive but specifically for msgs, for when you know the response won't include any Events receiveMsg :: MonadIO m => Socket -> m (Either String MsgReply) receiveMsg soc = do r <- getReply soc pure $ do (ty, body) <- r toMsgReply ty body -- | Like 'I3IPC.receiveMsg' but strict-- uses eitherDecode' receiveMsg' :: MonadIO m => Socket -> m (Either String MsgReply) receiveMsg' soc = do r <- getReply soc pure $ do (ty, body) <- r toMsgReply' ty body -- | 'I3IPC.receive' specifically for Event receiveEvent :: MonadIO m => Socket -> m (Either String Evt.Event) receiveEvent soc = do r <- getReply soc pure $ do (ty, body) <- r Evt.toEvent (ty `clearBit` 31) body -- | like 'receiveEvent' but strict-- uses eitherDecode' receiveEvent' :: MonadIO m => Socket -> m (Either String Evt.Event) receiveEvent' soc = do r <- getReply soc pure $ do (ty, body) <- r Evt.toEvent' (ty `clearBit` 31) body -- | Run a command represented as a ByteString, all the following functions are convenience wrappers around -- -- > Msg.sendMsgPayload soc Msg.X b >> receiveMsg soc -- -- Or, if there is no message body: -- -- > Msg.sendMsg soc Msg.X >> receiveMsg soc runCommand :: MonadIO m => Socket -> BL.ByteString -> m (Either String MsgReply) runCommand soc b = Msg.sendMsgPayload soc Msg.RunCommand b >> receiveMsg soc runCommand' :: MonadIO m => Socket -> BL.ByteString -> m (Either String MsgReply) runCommand' soc b = Msg.sendMsgPayload soc Msg.RunCommand b >> receiveMsg' soc getWorkspaces :: MonadIO m => Socket -> m (Either String MsgReply) getWorkspaces soc = Msg.sendMsg soc Msg.Workspaces >> receiveMsg soc getWorkspaces' :: MonadIO m => Socket -> m (Either String MsgReply) getWorkspaces' soc = Msg.sendMsg soc Msg.Workspaces >> receiveMsg' soc getOutputs :: MonadIO m => Socket -> m (Either String MsgReply) getOutputs soc = Msg.sendMsg soc Msg.Outputs >> receiveMsg soc getOutputs' :: MonadIO m => Socket -> m (Either String MsgReply) getOutputs' soc = Msg.sendMsg soc Msg.Outputs >> receiveMsg' soc getTree :: MonadIO m => Socket -> m (Either String MsgReply) getTree soc = Msg.sendMsg soc Msg.Tree >> receiveMsg soc getTree' :: MonadIO m => Socket -> m (Either String MsgReply) getTree' soc = Msg.sendMsg soc Msg.Tree >> receiveMsg' soc getMarks :: MonadIO m => Socket -> m (Either String MsgReply) getMarks soc = Msg.sendMsg soc Msg.Marks >> receiveMsg soc getMarks' :: MonadIO m => Socket -> m (Either String MsgReply) getMarks' soc = Msg.sendMsg soc Msg.Marks >> receiveMsg' soc getBarIds :: MonadIO m => Socket -> m (Either String BarIds) getBarIds soc = do _ <- Msg.sendMsg soc Msg.BarConfig r <- getReply soc pure $ do body <- r decodeBarIds (snd body) -- | Get a bar's config based on it's id getBarConfig :: MonadIO m => Socket -> BL.ByteString -> m (Either String MsgReply) getBarConfig soc b = Msg.sendMsgPayload soc Msg.BarConfig b >> receiveMsg' soc -- | Like 'I3IPC.getBarConfig' but strict getBarConfig' :: MonadIO m => Socket -> BL.ByteString -> m (Either String MsgReply) getBarConfig' soc b = Msg.sendMsgPayload soc Msg.BarConfig b >> receiveMsg' soc getVersion :: MonadIO m => Socket -> m (Either String MsgReply) getVersion soc = Msg.sendMsg soc Msg.Version >> receiveMsg soc getVersion' :: MonadIO m => Socket -> m (Either String MsgReply) getVersion' soc = Msg.sendMsg soc Msg.Version >> receiveMsg' soc getBindingModes :: MonadIO m => Socket -> m (Either String MsgReply) getBindingModes soc = Msg.sendMsg soc Msg.BindingModes >> receiveMsg soc getBindingModes' :: MonadIO m => Socket -> m (Either String MsgReply) getBindingModes' soc = Msg.sendMsg soc Msg.BindingModes >> receiveMsg' soc getConfig :: MonadIO m => Socket -> m (Either String MsgReply) getConfig soc = Msg.sendMsg soc Msg.Config >> receiveMsg soc getConfig' :: MonadIO m => Socket -> m (Either String MsgReply) getConfig' soc = Msg.sendMsg soc Msg.Config >> receiveMsg' soc getTick :: MonadIO m => Socket -> m (Either String MsgReply) getTick soc = Msg.sendMsg soc Msg.Tick >> receiveMsg soc getTick' :: MonadIO m => Socket -> m (Either String MsgReply) getTick' soc = Msg.sendMsg soc Msg.Tick >> receiveMsg' soc getSync :: MonadIO m => Socket -> m (Either String MsgReply) getSync soc = Msg.sendMsg soc Msg.Sync >> receiveMsg soc getSync' :: MonadIO m => Socket -> m (Either String MsgReply) getSync' soc = Msg.sendMsg soc Msg.Sync >> receiveMsg' soc -- $sub -- -- Commonly, you just want to subscribe to a set of event types and do something with the response: -- -- > import qualified I3IPC.Subscribe as Sub -- > import I3IPC.Event -- > import I3IPC ( subscribe ) -- > import Control.Monad.IO.Class -- > -- > main :: IO () -- > main = liftIO $ subscribe handle [Sub.Workspace, Sub.Window] -- > where -- > handle :: Either String Event -> IO () -- > handle (Right evt) = case evt of -- > Workspace WorkspaceEvent { wrk_current } -> print wrk_current -- > Window WindowEvent { win_container } -> print win_container -- > _ -> error "No other event types" -- > handle (Left err) = error err -- -- $msg -- -- Other times, you want to send some kind of command to i3, or get a specific response as a one-time action. -- -- > import I3IPC ( connecti3 -- > , getWorkspaces -- > ) -- > import Control.Monad.IO.Class -- > -- > main :: IO () -- > main = do -- > soc <- liftIO $ connecti3 -- > print getWorkspaces -- -- $func -- -- All of the "getX" functions are provided for convenience, but also exported are the building blocks to write whatever you like. -- There are strict and non-strict variants provided, the tick (') implies strict. -- For instance, the above could be written as: -- -- > import qualified I3IPC.Message as Msg -- > import I3IPC ( connecti3 -- > , receiveMsg -- > ) -- > import Control.Monad.IO.Class -- > -- > main :: IO () -- > main = do -- > soc <- liftIO $ connecti3 -- > print $ Msg.sendMsg soc Msg.Workspaces >> receiveMsg soc