registry-options-0.1.0.0: src/Data/Registry/Options/DisplayHelpBox.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
-- | Support for displaying a full help message as a Box (https://hackage.haskell.org/package/boxes)
--
-- The main display function uses a registry to register various
-- @Display "name" Data Box@ values which depend on each other
--
-- Display "name" Data Box is responsible for the display of "Data" in the section "name" as a Box,
-- which can then be rendered as Text with 'renderBox'.
--
-- For example there is a @Display "command-options" Help Box@ to display the options of a given command
-- (represented as a 'Help' value) in 2 columns: option flags / option help text.
--
-- This 'Display' value depends on 2 other 'Display' values:
--
-- - @Display "option-flag" OptionDescription Box@ to display the flag of an option
-- - @Display "option-help" OptionDescription Box@ to display the help of an option
--
-- It is possible to modify the display of the overall help of a command by adding a different display on top of
-- the registry of displays. For example
-- @
-- myDisplayBoxRegistry =
-- fun myDisplayOptionFlagBox <: displayBoxRegistry
--
-- myDisplayOptionFlagBox :: Display "option-flag" OptionDescription Box
-- myDisplayOptionFlagBox = Display $ fromMaybe "" . _name -- just use the option long name
--
-- myDisplayHelp :: Help -> Box
-- myDisplayHelp = renderBox . display (make @(Display "any" Help Box) myDisplayBoxRegistry)
-- @
module Data.Registry.Options.DisplayHelpBox where
import Data.Coerce (coerce)
import Data.Registry hiding ((<+>))
import Data.Registry.Options.Display
import Data.Registry.Options.Help
import Data.Registry.Options.OptionDescription hiding (help)
import Data.Text qualified as T
import Protolude hiding (Any, list)
import Text.PrettyPrint.Boxes hiding ((<>))
-- | Default display for a Help Text
displayHelp :: Help -> Text
displayHelp = renderBox . displayHelpBox
-- | Default display for a Help Box
displayHelpBox :: Help -> Box
displayHelpBox = display (make @(Display "any" Help Box) displayBoxRegistry)
-- | This registry provides overridable functions for displaying various parts of
-- a help text.
--
-- It can be overridden to display the help differently
displayBoxRegistry :: Registry _ _
displayBoxRegistry =
fun displayAllBox
<: fun displayHelpTitleBox
<: fun displayUsageBox
<: fun displayOptionsBox
<: fun displayCommandsBox
<: fun displayCommandSummaryBox
<: fun displayCommandDetailBox
<: fun displayCommandTitleBox
<: fun displayCommandUsageBox
<: fun displayCommandOptionsBox
<: fun displayOptionBox
<: fun displayOptionBoxes
<: fun displayOptionUsageBox
<: fun displayOptionFlagBox
<: fun displayOptionHelpBox
<: fun displayMetavarUsageBox
<: fun displayMetavarBox
<: val (TableParameters left top 10)
<: val (ParagraphWidth 50)
-- | *Template*
--
-- Display "title" Help Box
--
-- Display "usage" Help Box
--
-- Display "commands" Help Box
--
-- *Example*
--
-- fs - a utility to copy and move files",
--
-- USAGE
--
-- fs [-h|--help] [-v|--version] [copy] [move]
--
-- OPTIONS
--
-- -h,--help BOOL Display this help message
-- -v,--version BOOL Display the version
--
-- COMMANDS
--
-- copy [OPTIONS] copy a file from SOURCE to TARGET
-- move [OPTIONS] move a file from SOURCE to TARGET
--
-- fs copy - copy a file from SOURCE to TARGET
--
-- fs copy [-h|--help] [-f|--force] [-r|--retries INT] [SOURCE] [TARGET]
--
-- -h,--help BOOL Display this help message
-- -f,--force BOOL Force the action even if a file already exists with the same name
-- -r,--retries INT number of retries in case of an error
-- SOURCE Source path
-- TARGET Target path
--
-- fs move - move a file from SOURCE to TARGET
--
-- fs move [-h|--help] [-f|--force] [SOURCE] [TARGET]
--
-- -h,--help BOOL Display this help message
-- -f,--force BOOL Force the action even if a file already exists with the same name
-- SOURCE Source path
-- TARGET Target path
displayAllBox :: Display "title" Help Box -> Display "usage" Help Box -> Display "options" Help Box -> Display "commands" Help Box -> Display "any" Help Box
displayAllBox dt du dos dcs = Display $ \help ->
vsepNonEmpty
[ display dt help,
display du help,
display dos help,
display dcs help
]
-- | Example
--
-- fs - a utility to copy and move files
--
-- We reused the display for a command title, which should work for either a top-level or a sub command
displayHelpTitleBox :: Display "command-title" Help Box -> Display "title" Help Box
displayHelpTitleBox = coerce
-- | Example
--
-- USAGE
--
-- fs [-h|--help] [-v|--version] [copy] [move]
displayUsageBox :: Display "option-usage" OptionDescription Box -> Display "usage" Help Box
displayUsageBox dou = Display $ \(Help n _ _ _ os cs _) -> do
let options = display dou <$> os
let commands = mText . helpCommandName <$> cs
vsep
1
top
[ "USAGE",
moveRight 2 $ hsepNonEmpty (mText n : (options <> (brackets <$> commands)))
]
-- | Example
--
-- OPTIONS
--
-- -h,--help BOOL Display this help message
-- -v,--version BOOL Display the version
displayOptionsBox :: Display "command-options" [OptionDescription] Box -> Display "options" Help Box
displayOptionsBox dos = Display $ \help -> do
let os = helpCommandFields help
if null os
then nullBox
else
vsepNonEmpty
[ "OPTIONS",
nullBox,
moveRight 2 (display dos os)
]
-- | Example
--
-- COMMANDS
--
-- copy [OPTIONS] copy a file from SOURCE to TARGET
-- move [OPTIONS] move a file from SOURCE to TARGET
--
-- fs copy - copy a file from SOURCE to TARGET
--
-- fs copy [-h|--help] [-f|--force] [-r|--retries INT] [SOURCE] [TARGET]
--
-- -h,--help BOOL Display this help message
-- -f,--force BOOL Force the action even if a file already exists with the same name
-- -r,--retries INT number of retries in case of an error
-- SOURCE Source path
-- TARGET Target path
--
-- fs move - move a file from SOURCE to TARGET
--
-- fs move [-h|--help] [-f|--force] [SOURCE] [TARGET]
--
-- -h,--help BOOL Display this help message
-- -f,--force BOOL Force the action even if a file already exists with the same name
-- SOURCE Source path
-- TARGET Target path
displayCommandsBox :: TableParameters -> Display "command-summary" Help [Box] -> Display "command-detail" Help Box -> Display "commands" Help Box
displayCommandsBox tps commandsSummary commandDetail = Display $ \help -> do
let cs = helpCommands help
if null cs
then nullBox
else
vsepNonEmpty
[ "COMMANDS",
moveRight 2 $ table tps $ display commandsSummary <$> cs,
vsepNonEmpty $ display commandDetail <$> cs
]
-- | Example
--
-- copy [OPTIONS] copy a file from SOURCE to TARGET"
displayCommandSummaryBox :: Display "command-summary" Help [Box]
displayCommandSummaryBox = Display $ \(Help n _ s _ os _ isDefault) -> do
let withOptions = if null os then nullBox else "[OPTIONS]"
let withDefault = tText $ fromMaybe "" s <> if isDefault then " (default)" else ""
[mText n <+> withOptions, withDefault]
-- | Example
--
-- fs move - move a file from SOURCE to TARGET
--
-- fs move [-h|--help] [-f|--force] [SOURCE] [TARGET]
--
-- -h,--help BOOL Display this help message
-- -f,--force BOOL Force the action even if a file already exists with the same name
-- SOURCE Source path
-- TARGET Target path
displayCommandDetailBox :: TableParameters -> Display "command-title" Help Box -> Display "command-usage" Help Box -> Display "option" OptionDescription [Box] -> Display "command-detail" Help Box
displayCommandDetailBox tp dct dcu dco = Display $ \h ->
vsepNonEmpty $
[ display dct h,
moveRight 2 $ display dcu h
]
<> [moveRight 2 $ table tp $ display dco <$> helpCommandFields h]
-- | Example
--
-- fs move - move a file from SOURCE to TARGET
--
-- - the parent command name is appended to the command name if the parent is defined
-- - if the command is a default subcommand the name is parenthesized
displayCommandTitleBox :: ParagraphWidth -> Display "command-title" Help Box
displayCommandTitleBox w = Display $ \(Help n p s l _ _ isDefault) -> do
vsepNonEmpty
[ hsepNonEmpty $
catMaybes
[ tText <$> p,
(if isDefault then parens else identity) . tText <$> n,
tText . ("- " <>) <$> s
],
maybe nullBox (moveRight 2) $ paragraph w <$> l
]
-- | Example
--
-- fs move [-h|--help] [-f|--force] [SOURCE] [TARGET]
displayCommandUsageBox :: Display "option-usage" OptionDescription Box -> Display "command-usage" Help Box
displayCommandUsageBox dou = Display $ \(Help n p _ _ os _ isDefault) ->
hsepNonEmpty $
catMaybes
[ tText <$> p,
(if isDefault then parens else identity) . tText <$> n
]
<> (display dou <$> os)
-- | Example
--
-- -h,--help BOOL Display this help message
-- -f,--force BOOL Force the action even if a file already exists with the same name
displayCommandOptionsBox :: TableParameters -> Display "option" OptionDescription [Box] -> Display "command-options" [OptionDescription] Box
displayCommandOptionsBox t d = Display $ \os -> table t (display d <$> os)
-- | Example
--
-- -h,--help BOOL Display this help message
displayOptionBox :: TableParameters -> Display "option" OptionDescription [Box] -> Display "option" OptionDescription Box
displayOptionBox t dos = Display $ \o -> table t [filter (not . isEmpty) (display dos o)]
-- | Example
--
-- -h,--help BOOL Display this help message
displayOptionBoxes :: Display "option-flag" OptionDescription Box -> Display "option-help" OptionDescription Box -> Display "option" OptionDescription [Box]
displayOptionBoxes df dh = Display $ \o -> [display df o, display dh o]
-- | Example
--
-- [-h|--help]
-- [-f|--file FILE]
displayOptionUsageBox :: Display "metavar-usage" OptionDescription Box -> Display "option-usage" OptionDescription Box
displayOptionUsageBox dmu = Display $ \case
o@(OptionDescription (Just n) _ (Just s) _ _) ->
brackets $ hsepNonEmpty [piped [tText $ "-" <> T.singleton s, tText $ "--" <> n], display dmu o]
o@(OptionDescription _ _ (Just s) _ _) ->
brackets $ hsepNonEmpty [tText $ "-" <> T.singleton s, display dmu o]
o@(OptionDescription (Just n) _ _ _ _) ->
brackets $ hsepNonEmpty [tText $ "--" <> n, display dmu o]
o@(OptionDescription _ _ _ (Just _) _) ->
brackets $ display dmu o
_ -> nullBox
-- | Example
--
-- -h,--help BOOL
displayOptionFlagBox :: Display "metavar" OptionDescription Box -> Display "option-flag" OptionDescription Box
displayOptionFlagBox dm = Display $ \o@(OptionDescription n as s _ _) ->
hsepNonEmpty
[ commaed
( [ -- short flag
mText $ ("-" <>) . T.singleton <$> s,
-- long flag
mText $ ("--" <>) <$> n
]
-- aliases
<> (text . ("--" <>) . toS <$> as)
),
display dm o
]
-- | Example
--
-- Display this help message
displayOptionHelpBox :: ParagraphWidth -> Display "option-help" OptionDescription Box
displayOptionHelpBox w = Display (mParagraph w . _help)
-- | Display a metavar, except for a switch because it is obvious that it is a boolean
-- or for a String flag
--
-- Example
--
-- FILE
displayMetavarUsageBox :: Display "metavar-usage" OptionDescription Box
displayMetavarUsageBox = Display $ \(OptionDescription _ _ _ m _) ->
case m of
Nothing -> ""
(Just "BOOL") -> ""
(Just "[CHAR]") -> ""
Just s -> tText $ s
-- | Display a metavar in a full help text
--
-- [Char] is transformed to String
displayMetavarBox :: Display "metavar" OptionDescription Box
displayMetavarBox = Display $ \(OptionDescription _ _ _ m _) ->
case m of
Nothing -> ""
Just "[CHAR]" -> "STRING"
Just s -> tText $ s
-- | Return True if a Box is Empty
-- The best we can do is to render the box and compare it to the empty text
isEmpty :: Box -> Bool
isEmpty b = renderBox b == ""
-- | Separate a list of Boxes with a separator
separate :: Box -> [Box] -> Box
separate _ [] = nullBox
separate s ds = punctuateH left s (filter (not . isEmpty) ds)
-- | Separate a list of boxes with a pipe
piped :: [Box] -> Box
piped = separate $ char '|'
-- | Separate a list of boxes with a comma
commaed :: [Box] -> Box
commaed = separate $ char ','
-- | Add brackets to a Box
brackets :: Box -> Box
brackets b = char '[' <:> b <:> char ']'
-- | Add parens to a Box
parens :: Box -> Box
parens b = char '(' <:> b <:> char ')'
-- | Remove empty docs and use hsep
hsepNonEmpty :: [Box] -> Box
hsepNonEmpty = hsep 1 left . filter (not . isEmpty)
-- | Remove empty docs and use hcat
hcatNonEmpty :: [Box] -> Box
hcatNonEmpty = hcat left . filter (not . isEmpty)
-- | Remove empty docs and use vsep
vsepNonEmpty :: [Box] -> Box
vsepNonEmpty = vsep 1 top . filter (not . isEmpty)
-- | Remove empty docs and use vcat
vcatNonEmpty :: [Box] -> Box
vcatNonEmpty = vcat top . filter (not . isEmpty)
-- | Create a box for non empty text
mText :: Maybe Text -> Box
mText Nothing = nullBox
mText (Just t) = text (toS t)
-- | Create a box for a Text value instead of a String
tText :: Text -> Box
tText = text . toS
-- | Non-clashing append operator for boxes
(<:>) :: Box -> Box -> Box
l <:> r = hcat left [l, r]
-- | Render a Box as Text
-- The render function for boxes adds one last newline which we want to avoid
renderBox :: Box -> Text
renderBox = T.intercalate "\n" . T.lines . toS . render
-- | Display a table given a list of rows containing boxes
table :: TableParameters -> [[Box]] -> Box
table _ [] = nullBox
table (TableParameters h v i) rs = do
-- compute the max height of a row
let maxCellRows cells = maximum (rows <$> cells)
-- adjust all the rows so that they have the same height
let rs' = (\row -> let m = maxCellRows row in alignVert v m <$> row) <$> rs
-- transpose the rows to get the columns
-- display them vertically then concatenate the columns horizontally
hsep i h $ vcat v <$> transpose rs'
-- | Create a paragraph for some Text, wrapping the text at paragraph width
paragraph :: ParagraphWidth -> Text -> Box
paragraph (ParagraphWidth w) = para left w . toS
-- | Create a paragraph for an option piece of text
mParagraph :: ParagraphWidth -> Maybe Text -> Box
mParagraph _ Nothing = nullBox
mParagraph w (Just t) = paragraph w t
-- | Width of paragraph, used in conjunction with the 'paragraph' function
newtype ParagraphWidth = ParagraphWidth Int deriving (Eq, Show, Num)
-- | Those parameters are used when creating a table with the
-- 'table' function
data TableParameters = TableParameters
{ horizontalAlignment :: Alignment,
verticalAlignment :: Alignment,
intercolumn :: Int
}
deriving (Eq, Show)