packages feed

chiasma-0.2.0.0: lib/Chiasma/Test/Tmux.hs

module Chiasma.Test.Tmux where

import Chiasma.Data.TmuxError (TmuxError)
import Chiasma.Monad.Stream (runTmux)
import qualified Chiasma.Monad.Tmux as Tmux (write)
import Chiasma.Native.Api (TmuxNative(..))
import Control.Concurrent (threadDelay)
import System.Directory (doesFileExist)
import System.FilePath ((</>))
import System.Posix.IO (fdToHandle)
import System.Posix.Pty (Pty, createPty, resizePty)
import qualified System.Posix.Signals as Signal (killProcess, signalProcess)
import System.Posix.Terminal (openPseudoTerminal)
import System.Process (getPid)
import System.Process.Typed (
  Process,
  ProcessConfig,
  StreamSpec,
  proc,
  setStderr,
  setStdin,
  setStdout,
  unsafeProcessHandle,
  useHandleClose,
  withProcessWait,
  )
import UnliftIO (finally, throwString)
import UnliftIO.Exception (tryAny)
import UnliftIO.Temporary (withSystemTempDirectory)

import Chiasma.Test.File (fixture)

data Terminal = Terminal Handle Pty

data TmuxTestConf =
  TmuxTestConf {
    ttcWidth :: Int,
    ttcHeight :: Int,
    ttcFontSize :: Int,
    ttcGui :: Bool
  }
  deriving (Eq, Show)

instance Default TmuxTestConf where
  def = TmuxTestConf 240 61 18 True

usleep :: MonadIO f => Double -> f ()
usleep =
  liftIO . threadDelay . round

sleep :: MonadIO f => Double -> f ()
sleep seconds =
  usleep $ seconds * 1e6

unsafeTerminal :: IO Terminal
unsafeTerminal = do
  (_, slave) <- openPseudoTerminal
  mayPty <- createPty slave
  handle <- fdToHandle slave
  pty <- maybe (throwString "couldn't spawn pty") return mayPty
  return $ Terminal handle pty

urxvtArgs :: Int -> Int -> Int -> [Text]
urxvtArgs width height fontSize =
  ["-geometry", show width <> "x" <> show height, "-fn", "xft:monospace:size=" <> show fontSize, "-e", "tmux"]

testTmuxProcessConfig :: TmuxTestConf -> FilePath -> FilePath -> Terminal -> IO (ProcessConfig () () ())
testTmuxProcessConfig (TmuxTestConf width height fontSize gui) socket confFile (Terminal handle pty) = do
  confFileExists <- doesFileExist confFile
  resizePty pty (width, height)
  let
    stream :: StreamSpec st ()
    stream = useHandleClose handle
    stdio = setStdin stream . setStdout stream . setStderr stream
    tmuxArgs = ["-S", toText socket, "-f", toText confFileArg]
    confFileArg = if confFileExists then confFile else "/dev/null"
    prc =
      if gui
      then proc "urxvt" (toString <$> urxvtArgs width height fontSize ++ tmuxArgs)
      else proc "tmux" (toString <$> tmuxArgs)
  return $ stdio prc

killPid :: Integral a => a -> IO ()
killPid =
  void . tryAny . Signal.signalProcess Signal.killProcess . fromIntegral

killProcess :: TmuxNative -> Process () () () -> IO ()
killProcess api prc = do
  _ <- runExceptT @TmuxError $ runTmux api $ Tmux.write "kill-server" []
  let handle = unsafeProcessHandle prc
  mayPid <- getPid handle
  maybe (return ()) killPid mayPid

-- FIXME find a way to wait for tmux deterministically instead of sleeping
-- if the first tmux control mode process from a TmuxProg runs before urxvt has started the server,
-- it will not use the test tmux.conf
-- maybe start tmux first, then urxvt?
runAndKillTmux :: (TmuxNative -> IO a) -> TmuxNative -> Process () () () -> IO a
runAndKillTmux thunk api prc = do
  sleep 0.2
  finally (thunk api) (killProcess api prc)

withTestTmux :: TmuxTestConf -> (TmuxNative -> IO a) -> FilePath -> IO a
withTestTmux tConf thunk tempDir = do
  let socket = tempDir </> "tmux_socket"
  conf <- fixture "u" "tmux.conf"
  terminal <- unsafeTerminal
  pc <- testTmuxProcessConfig tConf socket conf terminal
  withProcessWait pc $ runAndKillTmux thunk (TmuxNative $ Just socket)

tmuxSpec' :: TmuxTestConf -> (TmuxNative -> IO a) -> IO a
tmuxSpec' conf thunk =
  withSystemTempDirectory "chiasma-test" $ withTestTmux conf thunk

tmuxSpec :: (TmuxNative -> IO a) -> IO a
tmuxSpec =
  tmuxSpec' def { ttcGui = False }

tmuxGuiSpec :: (TmuxNative -> IO a) -> IO a
tmuxGuiSpec =
  tmuxSpec' def