{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE CPP #-} ----------------------------------------------------------------------------- -- | -- Module : Diagrams.Backend.SVG.CmdLine -- Copyright : (c) 2013 Diagrams team (see LICENSE) -- License : BSD-style (see LICENSE) -- Maintainer : diagrams-discuss@googlegroups.com -- -- Convenient creation of command-line-driven executables for -- rendering diagrams using the SVG backend. -- -- * 'defaultMain' creates an executable which can render a single -- diagram at various options. -- -- * 'multiMain' is like 'defaultMain' but allows for a list of -- diagrams from which the user can choose one to render. -- -- * 'mainWith' is a generic form that does all of the above but with -- a slightly scarier type. See "Diagrams.Backend.CmdLine". This -- form can also take a function type that has a subtable final result -- (any of arguments to the above types) and 'Parseable' arguments. -- -- If you want to generate diagrams programmatically---/i.e./ if you -- want to do anything more complex than what the below functions -- provide---you have several options. -- -- * Use a function with 'mainWith'. This may require making -- 'Parseable' instances for custom argument types. -- -- * Make a new 'Mainable' instance. This may require a newtype -- wrapper on your diagram type to avoid the existing instances. -- This gives you more control over argument parsing, intervening -- steps, and diagram creation. -- -- * Build option records and pass them along with a diagram to 'mainRender' -- from "Diagrams.Backend.CmdLine". -- -- * You can use 'Diagrams.Backend.SVG.renderSVG' to render a diagram -- to a file directly; see "Diagrams.Backend.SVG". -- -- * A more flexible approach is to directly call 'renderDia'; see -- "Diagrams.Backend.SVG" for more information. -- -- For a tutorial on command-line diagram creation see -- . -- ----------------------------------------------------------------------------- module Diagrams.Backend.SVG.CmdLine ( -- * General form of @main@ -- $mainwith mainWith -- * Supported forms of @main@ , defaultMain , multiMain -- * Backend tokens , SVG , B ) where import Diagrams.Prelude hiding (width, height, interval) import Diagrams.Backend.SVG import Diagrams.Backend.CmdLine import Control.Lens import Text.Blaze.Svg.Renderer.Utf8 (renderSvg) import qualified Data.ByteString.Lazy as BS import Data.List.Split #ifdef CMDLINELOOP import Data.Maybe (fromMaybe) import Control.Monad (when) import System.Directory (getModificationTime) import System.Process (runProcess, waitForProcess) import System.IO (openFile, hClose, IOMode(..), hSetBuffering, BufferMode(..), stdout) import System.Exit (ExitCode(..)) import Control.Concurrent (threadDelay) import qualified Control.Exception as Exc (catch, bracket) import Control.Exception (SomeException(..)) import System.Environment (getProgName,getArgs) import System.Posix.Process (executeFile) # if MIN_VERSION_directory(1,2,0) import Data.Time.Clock (UTCTime,getCurrentTime) type ModuleTime = UTCTime getModuleTime :: IO ModuleTime getModuleTime = getCurrentTime #else import System.Time (ClockTime, getClockTime) type ModuleTime = ClockTime getModuleTime :: IO ModuleTime getModuleTime = getClockTime #endif #endif -- $mainwith -- The 'mainWith' method unifies all of the other forms of @main@ and is -- now the recommended way to build a command-line diagrams program. It -- works as a direct replacement for 'defaultMain' or 'multiMain' as well -- as allowing more general arguments. For example, given a function that -- produces a diagram when given an @Int@ and a @'Colour' Double@, 'mainWith' -- will produce a program that looks for additional number and color arguments. -- -- > ... definitions ... -- > f :: Int -> Colour Double -> Diagram SVG R2 -- > f i c = ... -- > -- > main = mainWith f -- -- We can run this program as follows: -- -- > $ ghc --make MyDiagram -- > -- > # output image.svg built by `f 20 red` -- > $ ./MyDiagram -o image.svg -w 200 20 red -- | This is the simplest way to render diagrams, and is intended to -- be used like so: -- -- > ... definitions ... -- > -- > main = defaultMain myDiagram -- -- Compiling this file will result in an executable which takes -- various command-line options for setting the size, output file, -- and so on, and renders @myDiagram@ with the specified options. -- -- Pass @--help@ to the generated executable to see all available -- options. Currently it looks something like -- -- @ -- ./Program -- -- Usage: ./Program [-w|--width WIDTH] [-h|--height HEIGHT] [-o|--output OUTPUT] [--loop] [-s|--src ARG] [-i|--interval INTERVAL] -- Command-line diagram generation. -- -- Available options: -- -?,--help Show this help text -- -w,--width WIDTH Desired WIDTH of the output image -- -h,--height HEIGHT Desired HEIGHT of the output image -- -o,--output OUTPUT OUTPUT file -- -l,--loop Run in a self-recompiling loop -- -s,--src ARG Source file to watch -- -i,--interval INTERVAL When running in a loop, check for changes every INTERVAL seconds. -- @ -- -- For example, a common scenario is -- -- @ -- $ ghc --make MyDiagram -- -- # output image.svg with a width of 400pt (and auto-determined height) -- $ ./MyDiagram -o image.svg -w 400 -- @ defaultMain :: Diagram SVG R2 -> IO () defaultMain = mainWith instance Mainable (Diagram SVG R2) where #ifdef CMDLINELOOP type MainOpts (Diagram SVG R2) = (DiagramOpts, DiagramLoopOpts) mainRender (opts,loopOpts) d = do chooseRender opts d when (loopOpts^.loop) (waitForChange Nothing loopOpts) #else type MainOpts (Diagram SVG R2) = DiagramOpts mainRender opts d = chooseRender opts d #endif chooseRender :: DiagramOpts -> Diagram SVG R2 -> IO () chooseRender opts d = case splitOn "." (opts^.output) of [""] -> putStrLn "No output file given." ps | last ps `elem` ["svg"] -> do let sizeSpec = case (opts^.width, opts^.height) of (Nothing, Nothing) -> Absolute (Just w, Nothing) -> Width (fromIntegral w) (Nothing, Just h) -> Height (fromIntegral h) (Just w, Just h) -> Dims (fromIntegral w) (fromIntegral h) build = renderDia SVG (SVGOptions sizeSpec Nothing) d BS.writeFile (opts^.output) (renderSvg build) | otherwise -> putStrLn $ "Unknown file type: " ++ last ps -- | @multiMain@ is like 'defaultMain', except instead of a single -- diagram it takes a list of diagrams paired with names as input. -- The generated executable then takes a @--selection@ option -- specifying the name of the diagram that should be rendered. The -- list of available diagrams may also be printed by passing the -- option @--list@. -- -- Example usage: -- -- @ -- $ ghc --make MultiTest -- [1 of 1] Compiling Main ( MultiTest.hs, MultiTest.o ) -- Linking MultiTest ... -- $ ./MultiTest --list -- Available diagrams: -- foo bar -- $ ./MultiTest --selection bar -o Bar.eps -w 200 -- @ multiMain :: [(String, Diagram SVG R2)] -> IO () multiMain = mainWith instance Mainable [(String,Diagram SVG R2)] where type MainOpts [(String,Diagram SVG R2)] = (MainOpts (Diagram SVG R2), DiagramMultiOpts) mainRender = defaultMultiMainRender #ifdef CMDLINELOOP waitForChange :: Maybe ModuleTime -> DiagramLoopOpts -> IO () waitForChange lastAttempt opts = do prog <- getProgName args <- getArgs hSetBuffering stdout NoBuffering go prog args lastAttempt where go prog args lastAtt = do threadDelay (1000000 * opts^.interval) -- putStrLn $ "Checking... (last attempt = " ++ show lastAttempt ++ ")" (newBin, newAttempt) <- recompile lastAtt prog (opts^.src) if newBin then executeFile prog False args Nothing else go prog args $ getFirst (First newAttempt <> First lastAtt) -- | @recompile t prog@ attempts to recompile @prog@, assuming the -- last attempt was made at time @t@. If @t@ is @Nothing@ assume -- the last attempt time is the same as the modification time of the -- binary. If the source file modification time is later than the -- last attempt time, then attempt to recompile, and return the time -- of this attempt. Otherwise (if nothing has changed since the -- last attempt), return @Nothing@. Also return a Bool saying -- whether a successful recompilation happened. recompile :: Maybe ModuleTime -> String -> Maybe String -> IO (Bool, Maybe ModuleTime) recompile lastAttempt prog mSrc = do let errFile = prog ++ ".errors" srcFile = fromMaybe (prog ++ ".hs") mSrc binT <- maybe (getModTime prog) (return . Just) lastAttempt srcT <- getModTime srcFile if (srcT > binT) then do putStr "Recompiling..." status <- Exc.bracket (openFile errFile WriteMode) hClose $ \h -> waitForProcess =<< runProcess "ghc" ["--make", srcFile] Nothing Nothing Nothing Nothing (Just h) if (status /= ExitSuccess) then putStrLn "" >> putStrLn (replicate 75 '-') >> readFile errFile >>= putStr else putStrLn "done." curTime <- getModuleTime return (status == ExitSuccess, Just curTime) else return (False, Nothing) where getModTime f = Exc.catch (Just <$> getModificationTime f) (\(SomeException _) -> return Nothing) #endif