prettychart-0.3.0.0: src/Prettychart/Server.hs
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Serve a chart web page with a web socket in it, that accepts 'ChartOptions'.
module Prettychart.Server
( startChartServer,
startChartServerWith,
printChart,
chartSocketPage,
startFileServerWith,
watchE,
watchSvg,
svgEvent,
displayFile,
)
where
import Box
import Chart
import Control.Category ((>>>))
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Monad (forever, void, when)
import Data.Bool
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.ByteString.Char8 (pack)
import Data.Maybe
import Data.Text.Encoding
import MarkupParse
import Optics.Core hiding (element)
import Prettychart.Any
import System.FSNotify
import System.FilePath
import Web.Rep
-- | 'Page' containing a web socket and javascript needed to run it.
chartSocketPage :: Maybe ByteString -> Page
chartSocketPage title =
bootstrapPage
& #jsOnLoad
.~ mconcat
[ webSocket,
runScriptJs,
refreshJsbJs
]
& #htmlBody
.~ element "div" [Attr "class" "container"] (element "row" [Attr "class" "col"] (maybe mempty (elementc "h4" []) title) <> element_ "div" (pure $ Attr "id" "prettychart"))
& #cssBody
.~ cssColorScheme
-- | Print a chart supplying a 'ChartOptions' consumer, and a showable thing that may be chartable. The first argument flags whether to also print the item to stdout.
printChart :: (Show a) => Bool -> (ChartOptions -> IO Bool) -> a -> IO ()
printChart reprint send s = case anyChart (show s) of
Left _ -> print s
Right co -> do
b <- send co
when (not b || reprint) (print s)
-- | Start the chart server. Returns the chart consumer, and a server quit signal effect.
--
-- An iconic ghci session transcript:
--
-- >> import Chart.Examples
-- >> (sendChart, quitChartServer) <- startChartServer (Just "prettychart")
-- >> sendChart unitExample
--
-- ... point browser to localhost:9160 ...
--
-- >> quitChartServer
startChartServer :: Maybe String -> IO (ChartOptions -> IO Bool, IO ())
startChartServer title = startChartServerWith defaultSocketConfig (chartSocketPage $ pack <$> title)
-- | Start the chart server protocol with bespoke 'SocketConfig' and 'Page' configurations.
--
-- > startChartServerWith (defaultSocketConfig & #port .~ 4567) (defaultSocketPage & #htmlBody %~ divClass_ "row" "bespoke footnote")
startChartServerWith :: SocketConfig -> Page -> IO (ChartOptions -> IO Bool, IO ())
startChartServerWith scfg page = do
(Box c e, q) <- toBoxM Single
x <- async $ serveSocketBox scfg page (Box toStdout (decodeUtf8 . replace "prettychart" . encodeChartOptions <$> e))
pure (commit c, cancel x >> q)
-- | Start a file server protocol with bespoke 'SocketConfig' and 'Page' configurations.
--
-- > startFileServerWith (defaultSocketConfig & #port .~ 4567) (defaultSocketPage & #htmlBody %~ divClass_ "row" "bespoke footnote")
startFileServerWith :: SocketConfig -> Page -> IO (FilePath -> IO Bool, IO ())
startFileServerWith scfg page = do
(Box c e, q) <- toBoxM Single
x <- async $ serveSocketBox scfg page (Box toStdout (decodeUtf8 . replace "prettychart" <$> witherE (B.readFile >>> fmap Just) e))
pure (commit c, cancel x >> q)
-- | Emit from the fsnotify watch manager.
watchE :: [Char] -> Codensity IO (Emitter IO Event)
watchE fp =
emitQ
New
( \c -> withManager $ \mgr -> do
putStrLn "watchDir started"
_ <- watchDir mgr fp (const True) (void . commit c)
_ <- forever $ threadDelay 1000000
-- never gets to here:
putStrLn "watchDir stopped"
)
-- | Emit from the fsnotify watch manager.
-- > glue' toStdout <$|> fmap (Text.pack . show) <$> watchE "."
watchSvg :: FilePath -> CoEmitter IO FilePath
watchSvg fp =
emitQ
New
( \c -> withManager $ \mgr -> do
putStrLn "watchDir started"
_ <- watchDir mgr fp (isJust . svgEvent) (maybe (pure ()) (void . commit c) . svgEvent)
_ <- forever $ threadDelay 1000000
-- never gets to here:
putStrLn "watchDir stopped"
)
-- | Filter for svg file additions and modifications.
svgEvent :: Event -> Maybe FilePath
svgEvent (Added fp _ dir) = bool Nothing (Just fp) (takeExtension fp == ".svg" && dir == IsFile)
svgEvent (Modified fp _ dir) = bool Nothing (Just fp) (takeExtension fp == ".svg" && dir == IsFile)
svgEvent _ = Nothing
-- | Create a file committer to serve an SVG file.
displayFile :: SocketConfig -> Page -> IO (Committer IO FilePath, IO ())
displayFile scfg page = do
(Box c e, q) <- toBoxM Single
x <- async $ serveSocketBox scfg page (Box toStdout (decodeUtf8 . replace "prettychart" <$> e))
pure (witherC (B.readFile >>> fmap Just) c, cancel x >> q)