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 +3/−0
- LICENSE +27/−0
- README.md +17/−0
- Setup.lhs +6/−0
- rapid-term.cabal +57/−0
- src/Rapid/Term.hs +410/−0
+ 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!++-}