packages feed

ymonad-0.1.0.0: src/YMonad/Run.hs

{-# LANGUAGE BangPatterns #-}

module YMonad.Run (
  ymonad,
) where

import Control.Concurrent (forkIO, killThread)
import Control.Concurrent.STM (TQueue, newTQueueIO, tryReadTQueue, writeTQueue)
import Control.Exception (AsyncException (ThreadKilled), bracket, try)
import Control.Monad (foldM)
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as B8
import Data.Map.Strict qualified as M
import Data.Text qualified as T
import Data.Text.IO qualified as TIO
import Network.Socket
import Network.Socket.ByteString qualified as NSB
import System.Directory (removePathForcibly)
import System.Environment (getEnvironment, getExecutablePath)
import System.FilePath ((</>))
import System.IO.Error (catchIOError)
import System.Process (createProcess, env, proc, shell)
import System.Timeout (timeout)

import YMonad.Backend.River
import YMonad.Config
import YMonad.ControlCommand
import YMonad.Core
import YMonad.Keys
import YMonad.Prompt.Internal (choosePrompt, getCommands, getMans, getSshHosts, runInConfiguredTerminal)

ymonad :: YConfig -> IO ()
ymonad conf = do
  controlPath <- getControlPath
  queue <- newTQueueIO
  withControlServer controlPath queue $ do
    logTrace ("control socket ready: " <> toText controlPath)
    runtimeResult <- loadRiverRuntime
    riverRt0 <- case runtimeResult of
      Left err -> die ("Failed to load river protocol: " <> err)
      Right rt -> pure rt
    socketResult <- connectRiver Nothing
    sock <- case socketResult of
      Left fault -> die ("Failed to connect to river: " <> toString (faultMessage fault))
      Right connected -> pure connected
    bracket
      (pure sock)
      close
      ( \activeSock -> do
          startupResult <- acquireRiverManager activeSock riverRt0
          (runtimeSt0, riverRt1) <- case startupResult of
            Left fault -> die ("Failed to acquire river window manager: " <> toString (faultMessage fault))
            Right (startupEvents, riverRt') -> foldM (\(st, rt) event -> applyEvent st rt event) (runtimeStateFromConfig conf, riverRt') startupEvents
          riverRt2 <- maybeInstallKeyBindings conf riverRt1
          loop conf activeSock queue runtimeSt0 riverRt2
      )

runtimeStateFromConfig :: YConfig -> RuntimeState
runtimeStateFromConfig conf =
  runtimeSt0
    { rtWindowSet =
        windowSet0
          { current = (current windowSet0) {screenWorkspace = seedLayout (screenWorkspace (current windowSet0))}
          , hidden = map seedLayout (hidden windowSet0)
          }
    , rtTheme =
        RuntimeTheme
          { themeBorderWidth = borderWidth conf
          , themeNormalBorderColor = fromMaybe (opaqueRgb8 0xdd 0xdd 0xdd) (parseColorString (normalBorderColor conf))
          , themeFocusedBorderColor = fromMaybe (opaqueRgb8 0xff 0x00 0x00) (parseColorString (focusedBorderColor conf))
          , themeTitleBarMode = titleBarMode conf
          }
    }
  where
    runtimeSt0 = defaultRuntimeStateWithWorkspaces (Rect 0 0 1920 1080) workspaceIds
    windowSet0 = rtWindowSet runtimeSt0

    seedLayout workspace = workspace {workspaceLayout = layoutHook conf}

    workspaceIds = case map (WorkspaceId . T.pack) (workspaces conf) of
      [] -> WorkspaceId "1" :| map (WorkspaceId . show) [(2 :: Int) .. 9]
      firstWorkspace : remainingWorkspaces -> firstWorkspace :| remainingWorkspaces

data RiverPoll = RiverPoll
  { riverPollRuntime :: RiverRuntime
  , riverPollCallbacks :: [RiverCallback]
  , riverPollSteps :: [RiverPollStep]
  }

data RiverPollStep
  = RiverPollAction YAction
  | RiverPollEvent YEvent

loop :: YConfig -> Socket -> TQueue ControlCommand -> RuntimeState -> RiverRuntime -> IO ()
loop conf sock queue runtimeSt riverRt = do
  polled <- pollRiverEvents sock riverRt
  case polled of
    Left fault -> stopAfterFault runtimeSt riverRt fault
    Right poll -> do
      logCallbacks (riverPollCallbacks poll)
      (runtimeSt0, riverRt0) <- foldM (applyPollStep conf) (runtimeSt, riverPollRuntime poll) (riverPollSteps poll)

      pendingCommands <- atomically (drainQueue queue)
      (runtimeSt1, riverRt1) <- foldM applyControlCommand (runtimeSt0, riverRt0) pendingCommands
      riverRt2 <- maybeInstallKeyBindings conf riverRt1

      flushed <- flushTransportLogged sock riverRt2
      case flushed of
        Left fault -> stopAfterFault runtimeSt1 riverRt2 fault
        Right riverRt3 -> loop conf sock queue runtimeSt1 riverRt3
  where
    stopAfterFault runtimeSt' riverRt' fault = do
      _ <- applyEvent runtimeSt' riverRt' (EvBackendFault fault)
      logTrace "stopping river event loop after transport failure"

applyPollStep :: YConfig -> (RuntimeState, RiverRuntime) -> RiverPollStep -> IO (RuntimeState, RiverRuntime)
applyPollStep conf state' step =
  case step of
    RiverPollAction action -> applyAction conf state' action
    RiverPollEvent event -> let (runtimeSt, riverRt) = state' in applyEvent runtimeSt riverRt event

applyEvent :: RuntimeState -> RiverRuntime -> YEvent -> IO (RuntimeState, RiverRuntime)
applyEvent runtimeSt riverRt event = do
  case event of
    EvBackendFault fault -> logBackendFault fault
    _otherEvent -> pass
  let transition = reduce runtimeSt event
      requests = trRequests transition
      !requestCount = length requests
      !runtimeSt' = trState transition
      !riverRt' = pumpPending (stageRequests riverRt requests)
  requestCount `seq` pass
  pure (runtimeSt', riverRt')

applyAction :: YConfig -> (RuntimeState, RiverRuntime) -> YAction -> IO (RuntimeState, RiverRuntime)
applyAction conf (runtimeSt, riverRt) action =
  case action of
    Spawn cmd -> do
      _ <- createProcess (shell cmd)
      pure (runtimeSt, riverRt)
    ToggleFullscreen ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdToggleFullscreen)
    ViewWorkspace workspaceName ->
      applyEvent runtimeSt riverRt (EvUserCommand (CmdViewWorkspace (WorkspaceId (T.pack workspaceName))))
    MoveFocusedTo workspaceName ->
      applyEvent runtimeSt riverRt (EvUserCommand (CmdMoveFocusedTo (WorkspaceId (T.pack workspaceName))))
    FocusNext ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdFocusNext)
    FocusPrev ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdFocusPrev)
    SwapNext ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdSwapNext)
    SwapPrev ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdSwapPrev)
    KillFocused ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdKillFocused)
    CycleLayout ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdCycleLayout)
    ShrinkMaster ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdShrink)
    ExpandMaster ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdExpand)
    IncMasterNBy delta ->
      applyEvent runtimeSt riverRt (EvUserCommand (CmdIncMasterN delta))
    ExitSession ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdExitSession)
    RecompileRestart -> do
      restartYMonad
      pure (runtimeSt, riverRt)
    ShellPrompt xpConfig -> do
      commands <- getCommands
      selection <- choosePrompt xpConfig "Run: " commands
      case selection of
        Nothing -> pure (runtimeSt, riverRt)
        Just commandText -> do
          _ <- createProcess (shell commandText)
          pure (runtimeSt, riverRt)
    SafePrompt commandPath xpConfig -> do
      selection <- choosePrompt xpConfig "Run: " []
      case selection of
        Nothing -> pure (runtimeSt, riverRt)
        Just argument -> do
          _ <- createProcess (proc commandPath [argument])
          pure (runtimeSt, riverRt)
    UnsafePrompt commandPrefix xpConfig -> do
      selection <- choosePrompt xpConfig "Run: " []
      case selection of
        Nothing -> pure (runtimeSt, riverRt)
        Just argument -> do
          _ <- createProcess (shell (commandPrefix <> " " <> argument))
          pure (runtimeSt, riverRt)
    SshPrompt xpConfig -> do
      hosts <- getSshHosts
      selection <- choosePrompt xpConfig "SSH to: " hosts
      case selection of
        Nothing -> pure (runtimeSt, riverRt)
        Just host -> do
          runInConfiguredTerminal conf xpConfig ("ssh " <> host)
          pure (runtimeSt, riverRt)
    ManPrompt xpConfig -> do
      pages <- getMans
      selection <- choosePrompt xpConfig "Manual page: " pages
      case selection of
        Nothing -> pure (runtimeSt, riverRt)
        Just page -> do
          runInConfiguredTerminal conf xpConfig ("man " <> page)
          pure (runtimeSt, riverRt)

restartYMonad :: IO ()
restartYMonad = do
  launcherPath <- getLauncherPath
  env0 <- getEnvironment
  let env1 =
        ("YMONAD_FORCE_REBUILD", "1")
          : filter ((/= "YMONAD_FORCE_REBUILD") . fst) env0
  _ <- createProcess ((proc launcherPath []) {env = Just env1})
  logTrace ("spawned restart via " <> toText launcherPath)
  exitSuccess

getLauncherPath :: IO FilePath
getLauncherPath = do
  explicit <- lookupEnv "YMONAD_LAUNCHER_PATH"
  maybe getExecutablePath pure explicit

applyControlCommand :: (RuntimeState, RiverRuntime) -> ControlCommand -> IO (RuntimeState, RiverRuntime)
applyControlCommand (runtimeSt, riverRt) command =
  case command of
    ControlSpawn cmd -> do
      _ <- createProcess (shell cmd)
      pure (runtimeSt, riverRt)
    ControlToggleFullscreen ->
      applyEvent runtimeSt riverRt (EvUserCommand CmdToggleFullscreen)
    ControlUserCommand userCmd ->
      applyEvent runtimeSt riverRt (EvUserCommand userCmd)

pollRiverEvents :: Socket -> RiverRuntime -> IO (Either BackendFault RiverPoll)
pollRiverEvents sock riverRt = do
  recvResult <- (try (timeout 10000 (NSB.recv sock 65535)) :: IO (Either SomeException (Maybe BS.ByteString)))
  case recvResult of
    Left ex -> pure (Left (BackendFault ("Wayland recv failed: " <> toText (displayException ex))))
    Right mChunk ->
      case mChunk of
        Nothing -> pure (Right (RiverPoll riverRt [] []))
        Just chunk
          | BS.null chunk -> pure (Left (BackendFault "Wayland socket closed by compositor"))
          | otherwise ->
              pure $ do
                (riverRt', callbacks) <- ingestBytes riverRt chunk
                steps <- fmap concat (mapM callbackToSteps callbacks)
                let !callbackCount = length callbacks
                    !stepCount = length steps
                callbackCount `seq` stepCount `seq` Right (RiverPoll riverRt' callbacks steps)

pollEventsFromSteps :: [RiverPollStep] -> [YEvent]
pollEventsFromSteps steps = [event | RiverPollEvent event <- steps]

callbackToSteps :: RiverCallback -> Either BackendFault [RiverPollStep]
callbackToSteps callback =
  case callback of
    RiverActionTriggered action -> Right [RiverPollAction action]
    _otherCallback -> map RiverPollEvent <$> decodeCallback callback

acquireRiverManager :: Socket -> RiverRuntime -> IO (Either BackendFault ([YEvent], RiverRuntime))
acquireRiverManager sock riverRt0 = do
  flushed <- flushTransportLogged sock riverRt0
  case flushed of
    Left fault -> pure (Left fault)
    Right riverRt1 -> waitForRegistryRoundtrip 200 [] riverRt1
  where
    waitForRegistryRoundtrip :: Int -> [YEvent] -> RiverRuntime -> IO (Either BackendFault ([YEvent], RiverRuntime))
    waitForRegistryRoundtrip remaining acc riverRt
      | startupReady riverRt =
          case rrManagerObject riverRt of
            Nothing ->
              pure
                ( Left
                    ( BackendFault
                        "Compositor did not advertise river_window_manager_v1 during startup; YMonad requires River's window management protocol"
                    )
                )
            Just _managerObject -> do
              logTrace "river_window_manager_v1 acquired"
              pure (Right (acc, riverRt))
      | remaining <= 0 =
          pure
            ( Left
                ( BackendFault
                    "Timed out waiting for wl_registry roundtrip while acquiring river_window_manager_v1"
                )
            )
      | otherwise = do
          polled <- pollRiverEvents sock riverRt
          case polled of
            Left fault -> pure (Left fault)
            Right poll -> do
              logCallbacks (riverPollCallbacks poll)
              let pollEvents = pollEventsFromSteps (riverPollSteps poll)
              case firstBackendFault pollEvents of
                Just fault -> pure (Left fault)
                Nothing -> do
                  flushed' <- flushTransportLogged sock (riverPollRuntime poll)
                  case flushed' of
                    Left fault -> pure (Left fault)
                    Right riverRt' -> waitForRegistryRoundtrip (remaining - 1) (acc ++ pollEvents) riverRt'

    startupReady :: RiverRuntime -> Bool
    startupReady riverRt =
      rrRegistryReady riverRt
        && isJust (rrManagerObject riverRt)
        && case rrXkbBindingsObject riverRt of
          Nothing -> True
          Just _ -> isJust (rrSeat riverRt)

flushTransportLogged :: Socket -> RiverRuntime -> IO (Either BackendFault RiverRuntime)
flushTransportLogged sock riverRt = do
  let prepared = pumpPending riverRt
  logOutgoingMessages (rrOutgoing prepared)
  flushTransportIO sock riverRt

maybeInstallKeyBindings :: YConfig -> RiverRuntime -> IO RiverRuntime
maybeInstallKeyBindings conf riverRt
  | rrBindingsInstalled riverRt = pure riverRt
  | otherwise =
      case (rrXkbBindingsObject riverRt, rrSeat riverRt) of
        (Nothing, _)
          | rrRegistryReady riverRt -> do
              logTrace "river_xkb_bindings_v1 was not advertised; key bindings are unavailable"
              pure (riverRt {rrBindingsInstalled = True})
        (Just _bindingsObject, Just _seat) -> do
          let (warnings, riverRt') = stageConfiguredBindings conf riverRt
          mapM_ logTrace warnings
          logTrace "installed river xkb key bindings"
          pure riverRt'
        _notReadyYet -> pure riverRt

stageConfiguredBindings :: YConfig -> RiverRuntime -> ([Text], RiverRuntime)
stageConfiguredBindings conf riverRt0 =
  let (warnings, riverRt1) = foldl' registerBinding ([], riverRt0) (M.toList (keys conf conf))
      riverRt2 = riverRt1 {rrBindingsInstalled = True}
   in (warnings, riverRt2)
  where
    bindingsObjectId = fromMaybe 0 (rrXkbBindingsObject riverRt0)
    seatObjectId = maybe 0 riverSeatObjectId (rrSeat riverRt0)

    registerBinding :: ([Text], RiverRuntime) -> ((KeyMask, KeySym), YAction) -> ([Text], RiverRuntime)
    registerBinding (warnings, riverRt) ((mask, keySym), action) =
      case keySymToXkb keySym of
        Nothing ->
          ( warnings ++ ["skipping unsupported key symbol for river xkb bindings: " <> renderKeySym keySym]
          , riverRt
          )
        Just keysym ->
          let (bindingObjectId, riverRt1) = allocateObjectId riverRt
              createBinding =
                WireMessage
                  { wireObjectId = bindingsObjectId
                  , wireInterface = "river_xkb_bindings_v1"
                  , wireMessageName = "get_xkb_binding"
                  , wireOpcode = requestOpcode riverRt1 "river_xkb_bindings_v1" "get_xkb_binding"
                  , wireArgs =
                      [ ArgObjectValue (Just seatObjectId)
                      , ArgNewIdValue bindingObjectId
                      , ArgUintValue keysym
                      , ArgUintValue (unKeyMask mask)
                      ]
                  }
              enableBinding =
                WireMessage
                  { wireObjectId = bindingObjectId
                  , wireInterface = "river_xkb_binding_v1"
                  , wireMessageName = "enable"
                  , wireOpcode = requestOpcode riverRt1 "river_xkb_binding_v1" "enable"
                  , wireArgs = []
                  }
              riverRt2 =
                riverRt1
                  { rrBindingActions = M.insert bindingObjectId action (rrBindingActions riverRt1)
                  , rrObjectInterfaces = M.insert bindingObjectId "river_xkb_binding_v1" (rrObjectInterfaces riverRt1)
                  , rrOutgoing = rrOutgoing riverRt1 ++ [createBinding]
                  , rrPendingManage = rrPendingManage riverRt1 ++ [enableBinding]
                  }
           in (warnings, riverRt2)

requestOpcode :: RiverRuntime -> Text -> Text -> Word16
requestOpcode riverRt ifaceName name = maybe 0 messageSpecOpcode (lookupRequest (rrProtocol riverRt) ifaceName name)

logCallbacks :: [RiverCallback] -> IO ()
logCallbacks = mapM_ logCallback

logCallback :: RiverCallback -> IO ()
logCallback callback = case callback of
  RiverTrace msg -> logTrace msg
  RiverProtocolFault _msg -> pass
  RiverViewCreated vid _meta -> logTrace ("view created: " <> renderText vid)
  RiverViewDestroyed vid -> logTrace ("view destroyed: " <> renderText vid)
  RiverOutputDiscovered oid info ->
    logTrace ("output discovered: " <> renderText oid <> " " <> T.pack (show (outputRect info)))
  RiverOutputLost oid -> logTrace ("output removed: " <> renderText oid)
  RiverSeatFocus mVid -> logTrace ("seat focus: " <> maybe "none" renderText mVid)
  RiverManageStart -> pass
  RiverRenderStart -> pass
  RiverUserCommand userCommand -> logTrace ("user command: " <> renderText userCommand)
  RiverActionTriggered action -> logTrace ("binding pressed: " <> renderText action)

logOutgoingMessages :: [WireMessage] -> IO ()
logOutgoingMessages = mapM_ (logTrace . ("-> " <>) . renderWireMessage)

renderWireMessage :: WireMessage -> Text
renderWireMessage msg =
  wireInterface msg
    <> "."
    <> wireMessageName msg
    <> "(object="
    <> renderText (wireObjectId msg)
    <> ", args=["
    <> T.intercalate ", " (map renderArgValue (wireArgs msg))
    <> "])"

renderArgValue :: ArgValue -> Text
renderArgValue arg = case arg of
  ArgIntValue n -> renderText n
  ArgUintValue n -> renderText n
  ArgStringValue Nothing -> "null"
  ArgStringValue (Just txt) -> txt
  ArgObjectValue Nothing -> "object:null"
  ArgObjectValue (Just oid) -> "object:" <> renderText oid
  ArgNewIdValue oid -> "new_id:" <> renderText oid

renderText :: (Show a) => a -> Text
renderText = T.pack . show

logBackendFault :: BackendFault -> IO ()
logBackendFault fault = logTrace ("backend fault: " <> faultMessage fault)

logTrace :: Text -> IO ()
logTrace msg = TIO.hPutStrLn stderr ("[ymonad] " <> msg)

firstBackendFault :: [YEvent] -> Maybe BackendFault
firstBackendFault [] = Nothing
firstBackendFault (event : rest) = case event of
  EvBackendFault fault -> Just fault
  _otherEvent -> firstBackendFault rest

getControlPath :: IO FilePath
getControlPath = do
  explicit <- lookupEnv "YMONAD_SOCKET"
  case explicit of
    Just path -> pure path
    Nothing -> do
      runtimeDir <- lookupEnv "XDG_RUNTIME_DIR"
      case runtimeDir of
        Just dir -> pure (dir </> "ymonad.sock")
        Nothing -> pure "/tmp/ymonad.sock"

withControlServer :: FilePath -> TQueue ControlCommand -> IO a -> IO a
withControlServer path queue action = bracket acquire releaseResource (const action)
  where
    acquire = do
      removePathForcibly path `catchIOError` const pass
      server <- socket AF_UNIX Stream defaultProtocol
      bind server (SockAddrUnix path)
      listen server 5
      tid <- forkIO (acceptLoop server)
      pure (server, tid)

    releaseResource (server, tid) = do
      killThread tid
      close server
      removePathForcibly path `catchIOError` const pass

    acceptLoop server = forever $ do
      accepted <- (try (accept server) :: IO (Either SomeException (Socket, SockAddr)))
      case accepted of
        Left ex
          | isControlServerShutdown ex -> pass
          | otherwise -> logTrace ("control socket accept failed: " <> toText (displayException ex))
        Right (conn, _) -> do
          commandResult <- (try (recvAll conn) :: IO (Either SomeException BS.ByteString))
          close conn
          case commandResult of
            Left ex -> logTrace ("control socket recv failed: " <> toText (displayException ex))
            Right commandBytes ->
              case readMaybe (B8.unpack commandBytes) of
                Nothing -> logTrace ("ignored malformed control command: " <> decodeUtf8With lenientDecode commandBytes)
                Just command -> do
                  _ <- evaluateWHNF command
                  atomically (writeTQueue queue command)

isControlServerShutdown :: SomeException -> Bool
isControlServerShutdown ex =
  case fromException ex of
    Just ThreadKilled -> True
    _ -> "Bad file descriptor" `T.isInfixOf` toText (displayException ex)

recvAll :: Socket -> IO BS.ByteString
recvAll conn = go id
  where
    go chunks = do
      chunk <- NSB.recv conn 4096
      if BS.null chunk
        then pure (BS.concat (chunks []))
        else go (chunks . (chunk :))

drainQueue :: TQueue a -> STM [a]
drainQueue queue = go id
  where
    go acc = do
      next <- tryReadTQueue queue
      case next of
        Nothing -> pure (acc [])
        Just value -> go (acc . (value :))