packages feed

opt-env-conf-0.0.0.0: src/OptEnvConf/Doc.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module OptEnvConf.Doc
  ( renderVersionPage,
    parserDocs,
    renderHelpPage,
    renderManPage,
    renderReferenceDocumentation,
    parserOptDocs,
    renderLongOptDocs,
    renderShortOptDocs,
    parserEnvDocs,
    renderEnvDocs,
    parserConfDocs,
    renderConfDocs,

    -- * Internal
    AnyDocs (..),
    SetDoc (..),
    OptDoc (..),
    EnvDoc (..),
    ConfDoc (..),
    settingSetDoc,
    renderSetDoc,
    settingOptDoc,
    renderOptDocLong,
    settingEnvDoc,
    renderEnvDoc,
    settingConfDoc,
    renderConfDoc,
    helpLines,
  )
where

import Autodocodec.Schema
import Autodocodec.Yaml.Schema
import Control.Arrow
import Control.Monad
import Data.List (intercalate, intersperse)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T
import Data.Version
import OptEnvConf.Args (Dashed (..))
import qualified OptEnvConf.Args as Args
import OptEnvConf.Parser
import OptEnvConf.Setting
import Text.Colour
import Text.Colour.Layout

data SetDoc = SetDoc
  { setDocTryArgument :: !Bool,
    setDocTrySwitch :: !Bool,
    setDocTryOption :: !Bool,
    setDocDasheds :: ![Dashed],
    setDocEnvVars :: !(Maybe (NonEmpty String)),
    setDocConfKeys :: !(Maybe (NonEmpty (NonEmpty String, JSONSchema))),
    setDocDefault :: !(Maybe String),
    setDocMetavar :: !(Maybe Metavar),
    setDocHelp :: !(Maybe String)
  }
  deriving (Show)

data OptDoc = OptDoc
  { optDocTryArgument :: !Bool,
    optDocTrySwitch :: !Bool,
    optDocTryOption :: !Bool,
    optDocDasheds :: ![Dashed],
    optDocDefault :: !(Maybe String),
    optDocMetavar :: !(Maybe Metavar),
    optDocHelp :: !(Maybe String)
  }
  deriving (Show)

data EnvDoc = EnvDoc
  { envDocVars :: !(NonEmpty String),
    envDocDefault :: !(Maybe String),
    envDocMetavar :: !(Maybe Metavar),
    envDocHelp :: !(Maybe String)
  }
  deriving (Show)

data ConfDoc = ConfDoc
  { confDocKeys :: !(NonEmpty (NonEmpty String, JSONSchema)),
    confDocDefault :: !(Maybe String),
    confDocHelp :: !(Maybe String)
  }
  deriving (Show)

data AnyDocs a
  = AnyDocsCommands [CommandDoc a]
  | AnyDocsAnd ![AnyDocs a]
  | AnyDocsOr ![AnyDocs a]
  | AnyDocsSingle !a
  deriving (Show)

instance Functor AnyDocs where
  fmap f = \case
    AnyDocsCommands cs -> AnyDocsCommands $ map (fmap f) cs
    AnyDocsAnd as -> AnyDocsAnd $ fmap (fmap f) as
    AnyDocsOr as -> AnyDocsOr $ fmap (fmap f) as
    AnyDocsSingle a -> AnyDocsSingle $ f a

instance Foldable AnyDocs where
  foldMap f = \case
    AnyDocsCommands cs -> foldMap (foldMap f) cs
    AnyDocsAnd as -> foldMap (foldMap f) as
    AnyDocsOr as -> foldMap (foldMap f) as
    AnyDocsSingle a -> f a

instance Traversable AnyDocs where
  traverse f = \case
    AnyDocsCommands cs -> AnyDocsCommands <$> traverse (traverse f) cs
    AnyDocsAnd as -> AnyDocsAnd <$> traverse (traverse f) as
    AnyDocsOr as -> AnyDocsOr <$> traverse (traverse f) as
    AnyDocsSingle a -> AnyDocsSingle <$> f a

data CommandDoc a = CommandDoc
  { commandDocArgument :: String,
    commandDocHelp :: Help,
    commandDocs :: AnyDocs a
  }
  deriving (Show)

instance Functor CommandDoc where
  fmap f cd = cd {commandDocs = fmap f (commandDocs cd)}

instance Foldable CommandDoc where
  foldMap f = foldMap f . commandDocs

instance Traversable CommandDoc where
  traverse f cd = (\d -> cd {commandDocs = d}) <$> traverse f (commandDocs cd)

mapMaybeDocs :: (a -> Maybe b) -> AnyDocs a -> AnyDocs b
mapMaybeDocs func = simplifyAnyDocs . go
  where
    go = \case
      AnyDocsCommands cs -> AnyDocsCommands $ map goCommandDoc cs
      AnyDocsAnd ds -> AnyDocsAnd $ map go ds
      AnyDocsOr ds -> AnyDocsOr $ map go ds
      AnyDocsSingle d -> maybe (AnyDocsAnd []) AnyDocsSingle $ func d

    goCommandDoc cd = cd {commandDocs = go (commandDocs cd)}

simplifyAnyDocs :: AnyDocs a -> AnyDocs a
simplifyAnyDocs = go
  where
    go = \case
      AnyDocsCommands cs -> AnyDocsCommands $ map goDoc cs
      AnyDocsAnd ds -> case concatMap goAnd ds of
        [a] -> a
        as -> AnyDocsAnd as
      AnyDocsOr ds -> case concatMap goOr ds of
        [a] -> a
        as -> AnyDocsOr as
      AnyDocsSingle v -> AnyDocsSingle v

    goDoc cd = cd {commandDocs = go (commandDocs cd)}

    goAnd = \case
      AnyDocsCommands c -> [AnyDocsCommands $ map goDoc c]
      AnyDocsAnd ds -> concatMap (goAnd . go) ds
      AnyDocsOr [] -> []
      ds -> [go ds]

    goOr = \case
      AnyDocsCommands c -> [AnyDocsCommands $ map goDoc c]
      AnyDocsOr ds -> concatMap (goOr . go) ds
      AnyDocsAnd [] -> []
      ds -> [go ds]

-- | Derive parser documentation
parserDocs :: Parser a -> AnyDocs SetDoc
parserDocs = simplifyAnyDocs . go
  where
    noDocs = AnyDocsAnd []
    go :: Parser a -> AnyDocs SetDoc
    go = \case
      ParserPure _ -> noDocs
      ParserAp pf pa -> AnyDocsAnd [go pf, go pa]
      ParserSelect p1 p2 -> AnyDocsAnd [go p1, go p2]
      ParserEmpty _ -> AnyDocsOr []
      ParserAlt p1 p2 -> AnyDocsOr [go p1, go p2]
      ParserMany p -> go p -- TODO: is this right?
      ParserAllOrNothing _ p -> go p
      ParserCheck _ _ _ p -> go p
      ParserCommands _ cs -> AnyDocsCommands $ map goCommand cs
      ParserWithConfig p1 p2 -> AnyDocsAnd [go p1, go p2] -- TODO: is this right? Maybe we want to document that it's not a pure parser?
      ParserSetting _ set -> maybe noDocs AnyDocsSingle $ settingSetDoc set
    goCommand :: Command a -> CommandDoc SetDoc
    goCommand Command {..} =
      CommandDoc
        { commandDocArgument = commandArg,
          commandDocHelp = commandHelp,
          commandDocs = go commandParser
        }

settingSetDoc :: Setting a -> Maybe SetDoc
settingSetDoc Setting {..} = do
  guard $ not settingHidden
  let setDocDasheds = settingDasheds
  let setDocTryArgument = settingTryArgument
  let setDocTrySwitch = isJust settingSwitchValue
  let setDocTryOption = settingTryOption
  let setDocEnvVars = settingEnvVars
  let setDocConfKeys = NE.map (second (\(DecodingCodec c) -> jsonSchemaVia c)) <$> settingConfigVals
  let setDocDefault = snd <$> settingDefaultValue
  let setDocMetavar = settingMetavar
  let setDocHelp = settingHelp
  pure SetDoc {..}

settingOptDoc :: Setting a -> Maybe OptDoc
settingOptDoc = settingSetDoc >=> setDocOptDoc

renderSetDoc :: SetDoc -> [[Chunk]]
renderSetDoc setDoc =
  concat
    [ renderSetDocHeader (setDocHelp setDoc),
      renderSetDocWithoutHeader setDoc,
      [[]]
    ]

renderSetDocHeader :: Maybe Help -> [[Chunk]]
renderSetDocHeader = maybe [[fore red "undocumented"]] helpLines

renderSetDocWithoutHeader :: SetDoc -> [[Chunk]]
renderSetDocWithoutHeader SetDoc {..} =
  concat
    [ [ unwordsChunks
          [ ["argument:"],
            [mMetavarChunk setDocMetavar]
          ]
        | setDocTryArgument
      ],
      [ unwordsChunks
          [ ["switch:"],
            dashedChunksNE dasheds
          ]
        | setDocTrySwitch,
          dasheds <- maybeToList (NE.nonEmpty setDocDasheds)
      ],
      [ unwordsChunks
          [ ["option:"],
            dashedChunksNE dasheds
              ++ [" ", mMetavarChunk setDocMetavar]
          ]
        | setDocTryOption,
          dasheds <- maybeToList (NE.nonEmpty setDocDasheds)
      ],
      [ unwordsChunks
          [ ["env:"],
            envVarChunksNE vars
              ++ [" ", mMetavarChunk setDocMetavar]
          ]
        | vars <- maybeToList setDocEnvVars
      ],
      concat
        [ concatMap
            ( \(key, schema) ->
                case jsonSchemaChunkLines schema of
                  [line] ->
                    [["config: ", confValChunk key, ": "] ++ line]
                  ls ->
                    ["config:"]
                      : indent
                        ( case ls of
                            [] -> [["TODO"]]
                            (l : ll) ->
                              ([confValChunk key, ": "] ++ l)
                                : indent ll
                        )
            )
            (NE.toList confs)
          | confs <- maybeToList setDocConfKeys
        ]
    ]

helpLines :: Help -> [[Chunk]]
helpLines = map (map (fore blue)) . stringLines

progDescLines :: String -> [[Chunk]]
progDescLines = stringLines

stringLines :: String -> [[Chunk]]
stringLines s =
  let ls = T.lines (T.pack s)
   in map (pure . chunk) ls

-- | Render the output of `--render-man-page` for reading with @man@
renderManPage ::
  String ->
  Version ->
  String ->
  AnyDocs SetDoc ->
  [Chunk]
renderManPage progname version progDesc docs =
  let optDocs = docsToOptDocs docs
      envDocs = docsToEnvDocs docs
      confDocs = docsToConfDocs docs
   in unlinesChunks $
        -- See https://man.openbsd.org/mdoc#MACRO_OVERVIEW
        concat
          [ [ -- Document date
              [".Dd $Mdocdate$"],
              -- Document title
              [".Dt ", progNameChunk progname, " 1"],
              -- Operating system footer
              [".Os"],
              -- Section header
              [".Sh ", "NAME"],
              [".Nm ", progNameChunk progname],
              [".Nd ", chunk $ T.pack progDesc],
              [".Sh ", "VERSION"],
              [versionChunk version],
              [".Sh ", "SYNOPSIS"],
              renderShortOptDocs progname optDocs,
              [".Sh ", "SETTINGS"],
              renderSetDocs docs
            ],
            concat
              [ [ [".Sh ", "OPTIONS"],
                  renderLongOptDocs optDocs
                ]
                | not (nullDocs optDocs)
              ],
            concat
              [ [ [".Sh ", "ENVIRONMENT VARIABLES"],
                  renderEnvDocs envDocs
                ]
                | not (nullDocs envDocs)
              ],
            concat
              [ [ [".Sh ", "CONFIGURATION VALUES"],
                  renderConfDocs confDocs
                ]
                | not (nullDocs confDocs)
              ]
          ]

-- | Render reference documentation
renderReferenceDocumentation :: String -> AnyDocs SetDoc -> [Chunk]
renderReferenceDocumentation progname docs =
  let optDocs = docsToOptDocs docs
      envDocs = docsToEnvDocs docs
      confDocs = docsToConfDocs docs
   in unlinesChunks $
        concat
          [ [ usageChunk : renderShortOptDocs progname optDocs,
              [],
              headerChunks "All settings",
              renderSetDocs docs
            ],
            concat
              [ [ headerChunks "Options",
                  renderLongOptDocs optDocs
                ]
                | not (nullDocs optDocs)
              ],
            concat
              [ [ headerChunks "Environment Variables",
                  renderEnvDocs envDocs
                ]
                | not (nullDocs envDocs)
              ],
            concat
              [ [ headerChunks "Configuration Values",
                  renderConfDocs confDocs
                ]
                | not (nullDocs confDocs)
              ]
          ]

nullDocs :: AnyDocs a -> Bool
nullDocs = \case
  AnyDocsCommands cs -> all nullCommandDoc cs
  AnyDocsOr [] -> True
  AnyDocsOr _ -> False
  AnyDocsAnd [] -> True
  AnyDocsAnd _ -> False
  AnyDocsSingle _ -> False
  where
    nullCommandDoc :: CommandDoc a -> Bool
    nullCommandDoc = nullDocs . commandDocs

-- | Render the output of @--version@
renderVersionPage :: String -> Version -> [Chunk]
renderVersionPage progname version =
  unwordsChunks
    [ [progNameChunk progname],
      [versionChunk version],
      ["\n"]
    ]

-- | Render the output of @--help@
renderHelpPage :: String -> String -> AnyDocs SetDoc -> [Chunk]
renderHelpPage progname progDesc docs =
  unlinesChunks
    [ usageChunk : renderShortOptDocs progname (docsToOptDocs docs),
      [],
      unlinesChunks $ progDescLines progDesc,
      headerChunks "Available settings",
      renderSetDocs docs
    ]

renderSetDocs :: AnyDocs SetDoc -> [Chunk]
renderSetDocs = unlinesChunks . go
  where
    go :: AnyDocs SetDoc -> [[Chunk]]
    go = \case
      AnyDocsCommands cs -> concatMap goCommand cs
      AnyDocsAnd ds -> concatMap go ds
      AnyDocsOr ds -> goOr ds
      AnyDocsSingle d -> indent (renderSetDoc d)

    goCommand :: CommandDoc SetDoc -> [[Chunk]]
    goCommand CommandDoc {..} =
      indent $
        [helpChunk commandDocHelp]
          : ["command: ", commandChunk commandDocArgument]
          : go commandDocs
          ++ [[]]

    -- Group together settings with the same help (produced by combinators like enableDisableSwitch)
    goOr :: [AnyDocs SetDoc] -> [[Chunk]]
    goOr = \case
      [] -> []
      [d] -> go d
      (AnyDocsSingle d : ds) ->
        case setDocHelp d of
          Nothing -> go (AnyDocsSingle d) ++ goOr ds
          Just h ->
            let (sds, rest) = goSameHelp h ds
             in concat
                  [ indent $ renderSetDocHeader (Just h),
                    indent $ concatMap renderSetDocWithoutHeader $ d : sds,
                    [[]],
                    goOr rest
                  ]
      (d : ds) -> go d ++ goOr ds

    goSameHelp :: Help -> [AnyDocs SetDoc] -> ([SetDoc], [AnyDocs SetDoc])
    goSameHelp h = \case
      [] -> ([], [])
      (AnyDocsSingle d : ds) ->
        if setDocHelp d == Just h
          then
            let (sds, rest) = goSameHelp h ds
             in (d : sds, rest)
          else ([], AnyDocsSingle d : ds)
      ds -> ([], ds)

parserOptDocs :: Parser a -> AnyDocs OptDoc
parserOptDocs = docsToOptDocs . parserDocs

docsToOptDocs :: AnyDocs SetDoc -> AnyDocs OptDoc
docsToOptDocs = mapMaybeDocs setDocOptDoc

setDocOptDoc :: SetDoc -> Maybe OptDoc
setDocOptDoc SetDoc {..} = do
  guard $ setDocTryArgument || setDocTrySwitch || setDocTryOption
  let optDocTryArgument = setDocTryArgument
      optDocTrySwitch = setDocTrySwitch
      optDocTryOption = setDocTryOption
      optDocDasheds = setDocDasheds
      optDocDefault = setDocDefault
      optDocMetavar = setDocMetavar
      optDocHelp = setDocHelp
  pure OptDoc {..}

-- | Render short-form documentation of options
renderShortOptDocs :: String -> AnyDocs OptDoc -> [Chunk]
renderShortOptDocs progname = unwordsChunks . (\cs -> [[progNameChunk progname], cs]) . go
  where
    go :: AnyDocs OptDoc -> [Chunk]
    go = \case
      AnyDocsCommands cs ->
        unwordsChunks $
          intersperse [orChunk] $
            map
              ( \CommandDoc {..} ->
                  if nullDocs commandDocs
                    then [commandChunk commandDocArgument]
                    else
                      commandChunk commandDocArgument
                        : " "
                        : go commandDocs
              )
              cs
      AnyDocsAnd ds -> unwordsChunks $ map go ds
      AnyDocsOr ds -> renderOrChunks (map go ds)
      AnyDocsSingle OptDoc {..} ->
        unwordsChunks $
          concat
            [ [ [mMetavarChunk optDocMetavar]
                | optDocTryArgument
              ],
              [ concat $ maybeToList $ dashedChunks optDocDasheds
                | optDocTrySwitch
              ],
              [ concat
                  [ concat $ maybeToList $ dashedChunks optDocDasheds,
                    [" ", mMetavarChunk optDocMetavar]
                  ]
                | optDocTryOption
              ]
            ]

renderOrChunks :: [[Chunk]] -> [Chunk]
renderOrChunks os =
  unwordsChunks $
    intersperse [orChunk] $
      map parenthesise os
  where
    parenthesise :: [Chunk] -> [Chunk]
    parenthesise [c] = [c]
    parenthesise cs = fore cyan "(" : cs ++ [fore cyan ")"]

orChunk :: Chunk
orChunk = fore cyan "|"

-- | Render long-form documentation of options
renderLongOptDocs :: AnyDocs OptDoc -> [Chunk]
renderLongOptDocs = unlinesChunks . go
  where
    go :: AnyDocs OptDoc -> [[Chunk]]
    go = \case
      AnyDocsCommands cs ->
        concatMap
          ( \CommandDoc {..} ->
              indent $
                unwordsChunks [[commandChunk commandDocArgument], [helpChunk commandDocHelp]]
                  : indent (go commandDocs)
          )
          cs
      AnyDocsAnd ds -> case goTable (AnyDocsAnd ds) of
        Nothing -> concatMap go ds
        Just csss -> indent $ layoutAsTableLines csss
      AnyDocsOr ds -> case goTable (AnyDocsOr ds) of
        Nothing -> concatMap go ds
        Just csss -> indent $ layoutAsTableLines csss
      AnyDocsSingle vs -> indent $ layoutAsTableLines [renderOptDocLong vs]

    goTable :: AnyDocs OptDoc -> Maybe [[[Chunk]]]
    goTable = \case
      AnyDocsCommands _ -> Nothing
      AnyDocsAnd ds -> concat <$> mapM goTable ds
      AnyDocsOr ds -> concat <$> mapM goTable ds
      AnyDocsSingle od -> Just [renderOptDocLong od]

renderOptDocLong :: OptDoc -> [[Chunk]]
renderOptDocLong OptDoc {..} =
  [ unwordsChunks $
      concat
        [ maybeToList $ dashedChunks optDocDasheds,
          [ [ mMetavarChunk optDocMetavar
            ]
            | optDocTryArgument
          ]
        ],
    [mHelpChunk optDocHelp],
    unwordsChunks [defaultValueChunks d | d <- maybeToList optDocDefault]
  ]

parserEnvDocs :: Parser a -> AnyDocs EnvDoc
parserEnvDocs = docsToEnvDocs . parserDocs

docsToEnvDocs :: AnyDocs SetDoc -> AnyDocs EnvDoc
docsToEnvDocs = mapMaybeDocs setDocEnvDoc

setDocEnvDoc :: SetDoc -> Maybe EnvDoc
setDocEnvDoc SetDoc {..} = do
  envDocVars <- setDocEnvVars
  let envDocDefault = setDocDefault
  let envDocMetavar = setDocMetavar
  let envDocHelp = setDocHelp
  pure EnvDoc {..}

settingEnvDoc :: Setting a -> Maybe EnvDoc
settingEnvDoc = settingSetDoc >=> setDocEnvDoc

-- | Render documentation of envionment variables
renderEnvDocs :: AnyDocs EnvDoc -> [Chunk]
renderEnvDocs = layoutAsTable . go
  where
    go :: AnyDocs EnvDoc -> [[[Chunk]]]
    go = \case
      AnyDocsCommands cs -> concatMap (go . commandDocs) cs
      AnyDocsAnd ds -> concatMap go ds
      AnyDocsOr ds -> concatMap go ds
      AnyDocsSingle ed -> [indent $ renderEnvDoc ed]

renderEnvDoc :: EnvDoc -> [[Chunk]]
renderEnvDoc EnvDoc {..} =
  [ unwordsChunks
      [ envVarChunksNE envDocVars,
        [ mMetavarChunk envDocMetavar
        ]
      ],
    [mHelpChunk envDocHelp]
  ]

parserConfDocs :: Parser a -> AnyDocs ConfDoc
parserConfDocs = docsToConfDocs . parserDocs

docsToConfDocs :: AnyDocs SetDoc -> AnyDocs ConfDoc
docsToConfDocs = mapMaybeDocs setDocConfDoc

setDocConfDoc :: SetDoc -> Maybe ConfDoc
setDocConfDoc SetDoc {..} = do
  confDocKeys <- setDocConfKeys
  let confDocDefault = setDocDefault
  let confDocHelp = setDocHelp
  pure ConfDoc {..}

settingConfDoc :: Setting a -> Maybe ConfDoc
settingConfDoc = settingSetDoc >=> setDocConfDoc

-- | Render documentation of configuration values
renderConfDocs :: AnyDocs ConfDoc -> [Chunk]
renderConfDocs = unlinesChunks . go
  where
    go :: AnyDocs ConfDoc -> [[Chunk]]
    go = \case
      AnyDocsCommands cs -> concatMap (go . commandDocs) cs
      AnyDocsAnd ds -> concatMap go ds
      AnyDocsOr ds -> concatMap go ds
      AnyDocsSingle ed -> indent (renderConfDoc ed)

renderConfDoc :: ConfDoc -> [[Chunk]]
renderConfDoc ConfDoc {..} =
  [mHelpChunk confDocHelp]
    : concatMap
      ( \(key, schema) ->
          case jsonSchemaChunkLines schema of
            [line] ->
              [[confValChunk key, ": "] ++ line]
            ls ->
              [confValChunk key, ":"] : indent ls
      )
      (NE.toList confDocKeys)

progNameChunk :: String -> Chunk
progNameChunk = fore yellow . chunk . T.pack

versionChunk :: Version -> Chunk
versionChunk = chunk . T.pack . showVersion

usageChunk :: Chunk
usageChunk = fore cyan "Usage: "

commandChunk :: String -> Chunk
commandChunk = fore magenta . chunk . T.pack

mMetavarChunk :: Maybe Metavar -> Chunk
mMetavarChunk = metavarChunk . fromMaybe "METAVAR"

metavarChunk :: Metavar -> Chunk
metavarChunk = fore yellow . chunk . T.pack

dashedChunks :: [Dashed] -> Maybe [Chunk]
dashedChunks = fmap dashedChunksNE . NE.nonEmpty

dashedChunksNE :: NonEmpty Dashed -> [Chunk]
dashedChunksNE = intersperse (fore cyan "|") . map dashedChunk . NE.toList

dashedChunk :: Dashed -> Chunk
dashedChunk = fore white . chunk . T.pack . Args.renderDashed

envVarChunksNE :: NonEmpty String -> [Chunk]
envVarChunksNE = intersperse (fore cyan "|") . map envVarChunk . NE.toList

envVarChunk :: String -> Chunk
envVarChunk = fore white . chunk . T.pack

confValChunk :: NonEmpty String -> Chunk
confValChunk = fore white . chunk . T.pack . intercalate "." . NE.toList

defaultValueChunks :: String -> [Chunk]
defaultValueChunks val = ["default: ", fore yellow $ chunk $ T.pack val]

mHelpChunk :: Maybe Help -> Chunk
mHelpChunk = maybe (fore red "undocumented") helpChunk

helpChunk :: Help -> Chunk
helpChunk = fore blue . chunk . T.pack

headerChunks :: Text -> [Chunk]
headerChunks t = [fore cyan (chunk t), ":"]

indent :: [[Chunk]] -> [[Chunk]]
indent = map ("  " :)