packages feed

rapid-term (empty) → 0.1.0

raw patch · 6 files changed

+520/−0 lines, 6 filesdep +ansi-terminaldep +basedep +clocksetup-changed

Dependencies added: ansi-terminal, base, clock, kan-extensions, process, rapid, unix

Files

+ CHANGELOG.md view
@@ -0,0 +1,3 @@+# 0.1.0++  * Initial version.
+ LICENSE view
@@ -0,0 +1,27 @@+Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++    (1) Redistributions of source code must retain the above copyright+    notice, this list of conditions and the following disclaimer.++    (2) Redistributions in binary form must reproduce the above+    copyright notice, this list of conditions and the following+    disclaimer in the documentation and/or other materials provided with+    the distribution.++    (3)The name of the author may not be used to endorse or promote+    products derived from this software without specific prior written+    permission.++THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,17 @@+Rapid-term+==========++When developing interactive command line applications in an editor like+Emacs GHCi typically has no access to an actual terminal.  This is good+enough for applications that only read lines from stdin and print+diagnostics to stdout, but as soon as terminal functionality is needed,+the application has to be tested elsewhere.++This package provides functionality that, when used together with the+[rapid library][], can open a persistent terminal that the application+can access directly, such that terminal applications can be tested with+the main GHCi instance.++You can find a tutorial in the documentation of the `Rapid.Term` module.++[rapid library]: https://hackage.haskell.org/package/rapid
+ Setup.lhs view
@@ -0,0 +1,6 @@+> module Main (main) where+>+> import Distribution.Simple+>+> main :: IO ()+> main = defaultMain
+ rapid-term.cabal view
@@ -0,0 +1,57 @@+name:     rapid-term+version:  0.1.0+category: Development+synopsis: External terminal support for rapid++maintainer:   Ertugrul Söylemez <esz@posteo.de>+author:       Ertugrul Söylemez <esz@posteo.de>+copyright:    Copyright 2016 Ertugrul Söylemez+homepage:     https://github.com/esoeylemez/rapid-term+bug-reports:  https://github.com/esoeylemez/rapid-term/issues+license:      BSD3+license-file: LICENSE++description:  When developing interactive command line applications in+    an editor like Emacs GHCi typically has no access to an actual+    terminal.  This is good enough for applications that only read lines+    from stdin and print diagnostics to stdout, but as soon as terminal+    functionality is needed, the application has to be tested elsewhere.+    .+    This package provides functionality that, when used together with+    the <https://hackage.haskell.org/package/rapid rapid library>, can+    open a persistent terminal that the application can access directly,+    such that terminal applications can be tested with the main GHCi+    instance.+    .+    You can find a tutorial in the documentation of the @Rapid.Term@+    module.++build-type:         Simple+cabal-version:      >= 1.10+extra-source-files: CHANGELOG.md README.md++source-repository head+    type:     git+    location: https://github.com/esoeylemez/rapid-term.git++flag Devel+    default: False+    description: Development dependencies+    manual: True+++library+    build-depends:+        ansi-terminal >= 0.6 && < 1,+        base >= 4.8 && < 5,+        clock >= 0.7 && < 1,+        kan-extensions >= 5.0 && < 6,+        process >= 1.4 && < 2,+        unix >= 2.7 && < 3+    if flag(devel)+        build-depends: rapid+    default-language: Haskell2010+    ghc-options: -W -fdefer-typed-holes+    hs-source-dirs: src+    exposed-modules:+        Rapid.Term
+ src/Rapid/Term.hs view
@@ -0,0 +1,410 @@+-- |+-- Copyright:  (c) 2016 Ertugrul Söylemez+-- License:    BSD3+-- Maintainer: Ertugrul Söylemez <esz@posteo.de>+--+-- When developing interactive command line applications in an editor+-- like Emacs GHCi typically has no access to an actual terminal.  This+-- is good enough for applications that only read lines from stdin and+-- print diagnostics to stdout, but as soon as terminal functionality is+-- needed, the application has to be tested elsewhere.+--+-- This package provides functionality that, when used together with the+-- <https://hackage.haskell.org/package/rapid rapid library>, can open a+-- persistent terminal that the application can access directly, such+-- that terminal applications can be tested with the main GHCi instance.++module Rapid.Term+    ( -- * Tutorial+      -- $tutorial++      -- ** Vty+      -- $vty++      -- * Terminal support for Rapid+      Term,+      newTermRef,+      runTerm,+      termFd,+      terminal,+      -- ** Low-level+      termFdPure,+      terminalPure,+      waitTerm,+      withTerm,++      -- * Supported terminal emulators+      -- ** rxvt-unicode+      urxvt,+      urxvtc,+      urxvtAt,++      -- * Helper functions+      stats+    )+    where++import Control.Concurrent+import Control.Exception+import Control.Monad.Codensity+import Control.Monad.IO.Class+import Data.IORef+import System.Clock+import System.IO+import System.Mem.Weak+import System.Posix.IO+import System.Posix.Terminal+import System.Posix.Types+import System.Process+import Text.Printf+++-- | Handle to a terminal++data Term =+    Term {+      _process  :: ProcessHandle,  -- ^ Process handle+      _ttySlave :: Fd              -- ^ File descriptor+    }+++-- | Create a new terminal reference.++newTermRef :: IO (MVar Term)+newTermRef = newEmptyMVar+++-- | Start a terminal and update the given terminal reference for use+-- from other threads.++runTerm :: (Fd -> IO ProcessHandle) -> MVar Term -> IO ()+runTerm start var =+    withTerm start $ \t ->+        mask $ \unmask ->+            bracket_ (unmask (putMVar var t))+                     (takeMVar var)+                     (unmask (waitTerm t))+++-- | Write execution diagnostics for the given action to the given+-- terminal++stats :: MVar Term -> IO a -> IO ()+stats tRef c =+    terminal tRef $ \h -> do+        hSetBuffering h NoBuffering+        hPutStrLn h "\n--- App start"+        rt0 <- getTime Monotonic+        ct0 <- getTime ProcessCPUTime+        mx <- try c+        ct1 <- getTime ProcessCPUTime+        rt1 <- getTime Monotonic+        case mx of+          Left (SomeException ex) -> do+              hPutStrLn h "*** Unhandled exception:"+              hPutStr h . unlines . map ("      " ++) . lines . show $ ex+          Right _ -> hPutStrLn h "--- App stop"+        let dt t0 t1 = fromInteger (toNanoSecs (t1 - t0)) / 1e9 :: Double+        hPrintf h "Real time: %8.4f secs\n" (dt rt0 rt1)+        hPrintf h "CPU time:  %8.4f secs\n" (dt ct0 ct1)+++-- | Provide a file descriptor to the given terminal+--+-- Given a terminal, this function duplicates its file descriptor and+-- passes it to the given continuation.  It is closed after the+-- continuation returns.+--+-- If you need separate file descriptors for input and output, you can+-- cascade this function in the same way as 'terminal'.+--+-- You can use this function as often as you want, in sequence or+-- concurrently.++termFd :: MVar Term -> (Fd -> IO r) -> IO r+termFd tRef k = readMVar tRef >>= \t -> termFdPure t k+++-- | Variant of 'termFd' that works on a pure terminal handle++termFdPure :: Term -> (Fd -> IO r) -> IO r+termFdPure t = bracket (dup (_ttySlave t)) closeFd+++-- | Provide a handle to the given terminal+--+-- Given a terminal, this function creates a handle (by duplicating the+-- underlying file descriptor) and passes it to the given continuation.+-- It is closed after the continuation returns.+--+-- If you need separate handles for input and output (for example to+-- select different buffering modes), just cascade this function:+--+-- > terminal t (\hI -> terminal t (\hO -> k hI hO))+--+-- You can use this function as often as you want, in sequence or+-- concurrently.++terminal :: MVar Term -> (Handle -> IO r) -> IO r+terminal tRef k = readMVar tRef >>= \t -> terminalPure t k+++-- | Variant of 'terminal' that works on a pure terminal handle++terminalPure :: Term -> (Handle -> IO r) -> IO r+terminalPure t k =+    mask $ \unmask ->+        let mkTtyHandle = unmask (dup (_ttySlave t)) >>= fdToHandle+        in bracket mkTtyHandle hClose $ \h ->+               unmask $ do+                   hSetBinaryMode h False+                   hSetBuffering h LineBuffering+                   hSetEcho h True+                   hSetEncoding h localeEncoding+                   hSetNewlineMode h nativeNewlineMode+                   k h+++-- | Spawns rxvt-unicode using the @urxvt@ executable++urxvt :: Fd -> IO ProcessHandle+urxvt = urxvtAt "urxvt"+++-- | Spawns rxvt-unicode using the @urxvtc@ executable++urxvtc :: Fd -> IO ProcessHandle+urxvtc = urxvtAt "urxvtc"+++-- | Spawns rxvt-unicode using the given executable++urxvtAt :: FilePath -> Fd -> IO ProcessHandle+urxvtAt p fd = spawnProcess p ["-pty-fd", show fd]+++-- | Wait for the given terminal subprocess to exit++waitTerm :: Term -> IO ()+waitTerm = (() <$) . waitForProcess . _process+++-- | Create a terminal using the given spawn function and pass its+-- terminal handle to the given continuation+--+-- The subprocess is terminated and resources are cleaned up once the+-- continuation returns.++withTerm+    :: (Fd -> IO ProcessHandle)  -- ^ Spawn function+    -> (Term -> IO r)            -- ^ Continuation+    -> IO r+withTerm start k =+    mask $ \unmask -> lowerCodensity $ do+        (master, slave) <- liftIO (unmask openPseudoTerminal)++        masterRef <- liftIO (newIORef master)+        masterWeak <- liftIO (mkWeakIORef masterRef (closeFd master))+        cOnException (finalize masterWeak)+        cFinally (closeFd slave)++        liftIO (unmask (setFdOption master CloseOnExec False))++        ph <- cBracket (unmask (start master))+                       (\ph -> unmask (terminateProcess ph >> waitForProcess ph))+        liftIO (unmask (finalize masterWeak))++        liftIO (unmask (k (Term {+                             _process  = ph,+                             _ttySlave = slave+                           })))++    where+    cBracket :: IO a -> (a -> IO b) -> Codensity IO a+    cBracket c o = Codensity (bracket c o)++    cFinally :: IO a -> Codensity IO ()+    cFinally c = Codensity (\k -> k () `finally` c)++    cOnException :: IO a -> Codensity IO ()+    cOnException c = Codensity (\k -> k () `onException` c)+++{- $tutorial++This tutorial assumes that you are already familiar with the+<https://hackage.haskell.org/package/rapid rapid library>, and that you+use <http://software.schmorp.de/pkg/rxvt-unicode.html rxvt-unicode> (or+at least have it installed).++Say you are writing a terminal application that requires an actual+terminal that you would like to test during development.  For example+you are using ANSI control sequences, or perhaps you're even using a+text UI based on <https://hackage.haskell.org/package/vty Vty>.  Ideally+you could use the running GHCi instance, but if you're using an editor+like Emacs and haskell-interactive-mode, then that's not possible+/directly/, because it's not attached to a terminal.++This library provides a way to fire up a separate, potentially+persistent terminal as a subprocess and communicate with it through one+or more 'Handle's.++The first step to using this library is to abstract over the 'Handle's+you want to use:++> module Main (main) where+>+> import System.IO+>+> mainWith :: Handle -> Handle -> Handle -> IO ()+> mainWith hI hO hE = {- ... -}+>+> main :: IO ()+> main = mainWith stdin stdout stderr++In other words: you no longer use the built-in handles, but do all your+input and output in @mainWith@ by reading from and writing to the+handles explicitly passed to it.  Let's use an example program that+reads a line from the input handle and writes it to the output handle:++> import Control.Concurrent+> import System.IO+>+> mainWith :: Handle -> Handle -> Handle -> IO ()+> mainWith hI hO _ = do+>     hPutStr hO "Type something: "+>     hFlush hO+>     line <- hGetLine hI+>+>     hPutStrLn hO "Wait for it..."+>     threadDelay 2000000+>     hPutStrLn hO ("You typed: " ++ line)++Now in your @DevelMain@ module you need three things:++  * a terminal reference,++  * a terminal thread,++  * a thread that calls your application.++This amounts to the following @update@ action:++> module DevelMain (update) where+>+> import Main (mainWith)+> import Rapid+> import Rapid.Term+>+> update :: IO ()+> update =+>     rapid 0 $ \r -> do+>         -- Create the terminal reference+>         t <- createRef r "term-ref" newTermRef+>+>         -- Thread for the terminal+>         start r "term" (runTerm urxvt t)+>+>         -- Thread for your application+>         restart r "my-app" . terminal t $ \h ->+>             mainWith h h h++Now if you use @update@ an rxvt-unicode terminal will pop up and run+@mainWith@, which will prompt you to type something.  Once you type a+line into that terminal, @mainWith@ will finish.  When you @update@+again, it will start over in the same terminal.  If you actually want to+open a new terminal every invocation, just use @restart@ instead of+@start@ for the terminal thread.++You can have as many application threads using the terminal concurrently+as you want.  Also you can request multiple handles to the terminal+e.g. to have different buffering modes for each:++> restart r "my-app" . terminal t $ \hI ->+>     terminal t $ \hO ->+>         mainWith hI hO hO++If you would like to see a few diagnostics after each application run,+just wrap your terminal action by 'stats':++> restart r "test-app" . stats t . terminal t $ \h ->+>     mainWith h h h++This also makes it easier to see when the application is finished,+because otherwise there would be no indication.++Note: While we have abstracted over three handles above there is no+technical reason to do that.  If you don't actually use, say, stderr in+your application, there is no reason to abstract over it:++> mainWith :: Handle -> Handle -> IO ()+> mainWith hI hO = {- ... -}++-}+++{- $vty++Running Vty applications requires some minor setup to work properly.+First of all instead of abstracting over input/output handles you should+abstract over the @Vty@ handle instead.  Let's write a very simple+example application:++> module Main (main) where+>+> import Graphics.Vty+>+> mainWith :: Vty -> IO ()+> mainWith vty = go ""+>     where+>     go inp = do+>         let pic = picForImage $+>                   string defAttr "Type some text:" <->+>                   string defAttr inp+>+>         update vty pic+>         ev <- nextEvent vty+>         case ev of+>           EvKey (KChar c) _ -> go (inp ++ [c])+>           EvKey KEsc _ -> pure ()+>           _ -> go inp+>+> main :: IO ()+> main = do+>     cfg <- standardIOConfig+>     bracket (mkVty cfg) shutdown mainWith++Now in your @DevelMain@ module you create the terminal reference and+thread as usual, but in your application thread you use 'termFd' to get+a file descriptor instead of a 'Handle', which is exactly what Vty+needs:++> module DevelMain (update) where+>+> import Control.Exception+> import qualified Graphics.Vty as Vty+> import Rapid+> import Rapid.Term+>+> update :: IO ()+> update =+>     rapid 0 $ \r -> do+>         t <- createRef r "term-ref" newTermRef+>         start r "term" (runTerm urxvt t)+>         restart r "test-app" . stats t . termFd t $ \fd -> do+>             cfg' <- Vty.standardIOConfig+>             let cfg = cfg' {+>                         Vty.inputFd = Just fd,+>                         Vty.outputFd = Just fd,+>                         Vty.termName = Just "rxvt-unicode-256color"+>                       }+>             bracket (Vty.mkVty cfg) Vty.shutdown mainWith++So the main differences are that you need to tell Vty explicitly which+handles it should use, and that you should probably also set the name of+the terminal explicitly (@termName@) so that Vty can find its terminfo+database.++Now you can use Rapid to develop your Vty applications!++-}