packages feed

chiasma-0.10.0.0: lib/Chiasma/Interpreter/TmuxClient.hs

module Chiasma.Interpreter.TmuxClient where

import Conc (interpretAtomic, interpretScopedResumableWith_, interpretScopedResumable_)
import Data.Sequence ((|>))
import qualified Data.Text as Text
import Exon (exon)
import qualified Log as Log
import Path (Abs, File, Path, relfile, toFilePath)
import Polysemy.Process.Interpreter.Process (ProcessQueues)
import qualified Process as Process
import Process (
  OutputPipe (Stderr, Stdout),
  Process,
  ProcessError,
  SystemProcess,
  SystemProcessError,
  SystemProcessScopeError,
  interpretProcessInputId,
  interpretProcessOutputLeft,
  interpretProcessOutputTextLines,
  interpretProcess_,
  interpretSystemProcessNative_,
  resolveExecutable,
  withProcess_,
  )
import System.Process.Typed (ProcessConfig, proc)

import qualified Chiasma.Data.TmuxError as TmuxError
import Chiasma.Data.TmuxError (TmuxError (NoExe))
import Chiasma.Data.TmuxNative (TmuxNative (TmuxNative))
import qualified Chiasma.Data.TmuxOutputBlock as TmuxOutputBlock
import Chiasma.Data.TmuxOutputBlock (TmuxOutputBlock)
import qualified Chiasma.Data.TmuxRequest as TmuxRequest
import Chiasma.Data.TmuxRequest (TmuxRequest (TmuxRequest))
import Chiasma.Data.TmuxResponse (TmuxResponse (TmuxResponse))
import qualified Chiasma.Effect.TmuxClient as TmuxClient
import Chiasma.Effect.TmuxClient (TmuxClient)
import Chiasma.Interpreter.ProcessOutput (interpretProcessOutputTmuxBlock)

type TmuxQueues =
  ProcessQueues (Either Text TmuxOutputBlock) Text

type TmuxProc =
  Process ByteString (Either Text TmuxOutputBlock)

validate :: TmuxRequest -> TmuxOutputBlock -> Either TmuxError TmuxResponse
validate request = \case
  TmuxOutputBlock.Success a ->
    Right (TmuxResponse a)
  TmuxOutputBlock.Error a ->
    Left (TmuxError.RequestFailed request a)

tmuxRequest ::
  Members [Process ByteString (Either Text TmuxOutputBlock), Log, Stop TmuxError] r =>
  TmuxRequest ->
  Sem r TmuxResponse
tmuxRequest request = do
  Log.trace [exon|tmux request: #{Text.stripEnd (decodeUtf8 cmdline)}|]
  Process.send cmdline
  Process.recv >>= \case
    Left err -> stop (TmuxError.RequestFailed request [err])
    Right block -> do
      Log.trace [exon|tmux response: #{show block}|]
      stopEither (validate request block)
  where
    cmdline =
      TmuxRequest.encode request

socketArg :: Path Abs File -> [String]
socketArg socket =
  ["-S", toFilePath socket]

tmuxProc ::
  TmuxNative ->
  ProcessConfig () () ()
tmuxProc (TmuxNative exe socket) =
  proc (toFilePath exe) (foldMap socketArg socket <> ["-C", "-u", "attach-session", "-f", "ignore-size"])

interpretSystemProcessTmux ::
  Members [Reader TmuxNative, Resource, Race, Async, Embed IO] r =>
  InterpreterFor (Scoped_ (SystemProcess !! SystemProcessError) !! SystemProcessScopeError) r
interpretSystemProcessTmux sem = do
  conf <- tmuxProc <$> ask
  interpretSystemProcessNative_ conf sem

interpretProcessTmux ::
  Member (Scoped_ (SystemProcess !! SystemProcessError) !! SystemProcessScopeError) r =>
  Members [Resource, Race, Async, Embed IO] r =>
  InterpreterFor (Scoped_ TmuxProc !! ProcessError) r
interpretProcessTmux sem = do
  interpretProcessOutputTmuxBlock @'Stdout $
    interpretProcessOutputTextLines @'Stderr $
    interpretProcessOutputLeft @'Stderr $
    interpretProcessInputId $
    interpretProcess_ def $
    insertAt @1 sem
{-# inline interpretProcessTmux #-}

flush ::
  Members [TmuxProc, AtomicState (Seq TmuxRequest), Log, Stop TmuxError] r =>
  Sem r ()
flush =
  traverse_ tmuxRequest =<< atomicState' (mempty,)

tmuxSession ::
  ∀ r a .
  Members [Scoped_ TmuxProc !! ProcessError, AtomicState (Seq TmuxRequest), Log, Stop TmuxError] r =>
  Sem (TmuxProc : r) a ->
  Sem r a
tmuxSession action =
  resumeHoist @ProcessError @(Scoped_ TmuxProc) TmuxError.ProcessFailed $ withProcess_ do
    void Process.recv
    tmuxRequest (TmuxRequest "refresh-client" ["-C", "10000x10000"] Nothing)
    raiseUnder action <* flush

interpretTmuxProcessBuffered ::
  Members [AtomicState (Seq TmuxRequest), Scoped_ TmuxProc !! ProcessError, Log, Embed IO] r =>
  InterpreterFor (Scoped_ (TmuxClient TmuxRequest TmuxResponse) !! TmuxError) r
interpretTmuxProcessBuffered =
  interpretScopedResumableWith_ @'[TmuxProc] (const tmuxSession) \case
    TmuxClient.Schedule request ->
      atomicModify' (|> request)
    TmuxClient.Send cmd -> do
      flush
      tmuxRequest cmd
{-# inline interpretTmuxProcessBuffered #-}

interpretTmuxWithProcess ::
  Members [Scoped_ TmuxProc !! ProcessError, Log, Embed IO] r =>
  InterpreterFor (Scoped_ (TmuxClient TmuxRequest TmuxResponse) !! TmuxError) r
interpretTmuxWithProcess =
  interpretAtomic mempty .
  interpretTmuxProcessBuffered .
  raiseUnder
{-# inline interpretTmuxWithProcess #-}

interpretTmuxNative ::
  ∀ r .
  Members [Reader TmuxNative, Log, Resource, Race, Async, Embed IO] r =>
  InterpreterFor (Scoped_ (TmuxClient TmuxRequest TmuxResponse) !! TmuxError) r
interpretTmuxNative =
  interpretSystemProcessTmux .
  interpretProcessTmux .
  interpretTmuxWithProcess .
  raiseUnder2
{-# inline interpretTmuxNative #-}

interpretTmuxFailing ::
  TmuxError ->
  InterpreterFor (Scoped_ (TmuxClient TmuxRequest TmuxResponse) !! TmuxError) r
interpretTmuxFailing err =
  interpretScopedResumable_ mempty \ () -> \case
    TmuxClient.Schedule _ ->
      stop err
    TmuxClient.Send _ ->
      stop err

withTmuxNativeEnv ::
  Member (Embed IO) r =>
  Maybe (Path Abs File) ->
  (Maybe TmuxNative -> Sem r a) ->
  Sem r a
withTmuxNativeEnv socket use =
  use . fmap (flip TmuxNative socket) . rightToMaybe =<< resolveExecutable [relfile|tmux|] Nothing

runReaderTmuxNativeEnv ::
  Members [Error TmuxError, Embed IO] r =>
  Maybe (Path Abs File) ->
  InterpreterFor (Reader TmuxNative) r
runReaderTmuxNativeEnv socket sem = do
  tn <- withTmuxNativeEnv socket (note NoExe)
  runReader tn sem
{-# inline runReaderTmuxNativeEnv #-}

interpretTmuxNativeEnv ::
  Members [Error TmuxError, Log, Resource, Race, Async, Embed IO] r =>
  Maybe (Path Abs File) ->
  InterpreterFor (Scoped_ (TmuxClient TmuxRequest TmuxResponse) !! TmuxError) r
interpretTmuxNativeEnv socket =
  runReaderTmuxNativeEnv socket . interpretTmuxNative . raiseUnder
{-# inline interpretTmuxNativeEnv #-}

interpretTmuxNativeEnvGraceful ::
  Members [Log, Resource, Race, Async, Embed IO] r =>
  Maybe (Path Abs File) ->
  InterpreterFor (Scoped_ (TmuxClient TmuxRequest TmuxResponse) !! TmuxError) r
interpretTmuxNativeEnvGraceful socket sem =
  withTmuxNativeEnv socket \case
    Just tn -> runReader tn (interpretTmuxNative (raiseUnder sem))
    Nothing -> interpretTmuxFailing NoExe sem
{-# inline interpretTmuxNativeEnvGraceful #-}

interpretTmuxClientNull ::
  InterpreterFor (Scoped_ (TmuxClient i ()) !! TmuxError) r
interpretTmuxClientNull =
  interpretScopedResumable_ mempty \ () -> \case
    TmuxClient.Schedule _ ->
      unit
    TmuxClient.Send _ ->
      unit
{-# inline interpretTmuxClientNull #-}