hwm-0.4.0: src/HWM/Core/Formatting.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
module HWM.Core.Formatting
( Color (..),
chalk,
formatStatus,
Status (..),
padDots,
genMaxLen,
separator,
deriveStatus,
statusFromSeverity,
statusIcon,
displayStatusM,
Format (..),
availableOptions,
renderSummaryStatus,
subPathSign,
slugify,
commonPrefix,
indentBlockNum,
indentBlock,
formatTable,
formatList,
monadStatus,
formatTemplate,
toCamelCase,
formatTableRow,
StatusM,
slugifyList,
andMore,
minRowSize,
)
where
import Data.Aeson (Value, encode)
import Data.ByteString.Lazy.Char8 (unpack)
import Data.Char (isAlphaNum)
import Data.Foldable (maximum)
import qualified Data.List as List
import Data.Text (pack)
import qualified Data.Text as T
import Distribution.PackageDescription (UnqualComponentName, unUnqualComponentName)
import HWM.Core.Common (Name)
import HWM.Core.Result (MonadIssue (catchIssues), Severity (..))
import Relude
data Color
= Red
| Green
| Yellow
| Gray
| Magenta
| Cyan
| Dim
| Bold
| White
| None
| RedBackground
| GreenBackground
| YellowBackground
| BrightRed
| BrightGreen
| BrightYellow
toColor :: Color -> Text
toColor c = "\x1b[" <> T.pack (show (colorCode c)) <> "m"
colorCode :: Color -> Int
colorCode Red = 31
colorCode Green = 32
colorCode Yellow = 33
colorCode Cyan = 36
colorCode Magenta = 95
colorCode Gray = 90
colorCode Dim = 2
colorCode None = 0
colorCode Bold = 1
colorCode RedBackground = 41
colorCode GreenBackground = 42
colorCode YellowBackground = 43
colorCode BrightRed = 91
colorCode BrightGreen = 92
colorCode BrightYellow = 93
colorCode White = 37
chalk :: Color -> Text -> Text
chalk c x = toColor c <> x <> toColor None
type StatusM m = [(Name, m Status)]
data Status = Checked | Updated | Warning | Invalid
deriving (Show, Eq, Ord)
instance Semigroup Status where
a <> b = max a b
deriveStatus :: [Status] -> Status
deriveStatus [] = Checked
deriveStatus statuses = maximum statuses
-- | Convert issue severity to status
statusFromSeverity :: Maybe Severity -> Status
statusFromSeverity (Just SeverityError) = Invalid
statusFromSeverity (Just SeverityWarning) = Warning
statusFromSeverity Nothing = Checked
monadStatus :: (Functor m, MonadIssue m) => m b -> m Status
monadStatus x = statusFromSeverity . fst <$> catchIssues x
displayStatusM :: (Monad m) => StatusM m -> m Text
displayStatusM ls = do
statuses <- mapM snd ls
let status = deriveStatus statuses
if status == Checked then return (statusIcon status) else return (formatStatus (zip (map fst ls) statuses))
formatTemplate :: [(Text, Text)] -> Text -> Text
formatTemplate vars template =
foldl' applyVar template vars
where
applyVar current (key, val) =
T.replace ("{{" <> key <> "}}") val current
padDots :: Int -> Text -> Text
padDots width s = s <> " " <> chalk Dim (T.replicate (max 0 (width - T.length s)) ".") <> " "
labelColor :: Status -> Color
labelColor Checked = Dim
labelColor Updated = Dim
labelColor Warning = Yellow
labelColor Invalid = Red
statusIcon :: Status -> Text
statusIcon s = case s of
Checked -> chalk Green "✓"
Updated -> chalk Cyan "⟳"
Invalid -> chalk Red "✖"
Warning -> chalk Yellow "!"
formatStatus :: [(Text, Status)] -> Text
formatStatus = T.intercalate (chalk Dim " ") . map formatItem . sortBy (comparing (Down . snd)) . filter ((/= Checked) . snd)
where
formatItem (label, s) = statusIcon s <> " " <> chalk (labelColor s) label
minRowSize :: Int
minRowSize = 16
genMaxLen :: [Text] -> Int
genMaxLen names = if null names then minRowSize else maximum (map T.length names) + 4
separator :: Int -> Text
separator size = chalk Gray $ T.replicate size "─"
class Format a where
format :: a -> Text
instance Format Int where
format = show
instance Format String where
format = pack
instance Format Text where
format = id
instance Format Value where
format = format . unpack . encode
instance Format UnqualComponentName where
format = pack . unUnqualComponentName
availableOptions :: (Format a) => [a] -> Text
availableOptions xs = "Available options: " <> T.intercalate ", " (map format xs)
boxed :: Color -> Text -> Text
boxed color text = chalk Bold "• " <> block <> " " <> chalk color text <> " " <> block
where
block = chalk color "▌"
renderSummaryStatus :: Status -> Text
renderSummaryStatus Warning = boxed BrightYellow "warning"
renderSummaryStatus Invalid = boxed Red "errors"
renderSummaryStatus _ = boxed BrightGreen "success"
subPathSign :: Text
subPathSign = chalk Dim "└── "
slugifyList :: [Text] -> Text
slugifyList = T.intercalate "-" . map slugify
slugify :: Text -> Text
slugify = T.map replaceChar . T.toLower
where
replaceChar c | isAlphaNum c = c | otherwise = '-'
commonPrefix :: [Text] -> (Maybe Text, [Text])
commonPrefix [] = (Nothing, [])
commonPrefix [name] = (Nothing, [name])
commonPrefix names =
refine names (T.dropWhileEnd (== '-') (List.foldl1' pairwisePrefix names))
where
pairwisePrefix a b = maybe "" (\(prefix, _, _) -> prefix) (T.commonPrefixes a b)
refine _ candidate | T.null candidate = (Nothing, names)
refine sources candidate
| all (matches candidate) sources = (Just candidate, map (dropPrefix candidate) sources)
| otherwise = refine sources (shrink candidate)
matches prefix name
| prefix == name = True
| prefix `T.isPrefixOf` name = startsWithHyphen (T.drop (T.length prefix) name)
| otherwise = False
startsWithHyphen remainder =
case T.uncons remainder of
Nothing -> True
Just ('-', _) -> True
_ -> False
shrink prefix =
case T.breakOnEnd "-" prefix of
(rest, _) | T.null rest -> ""
(rest, _) -> T.dropWhileEnd (== '-') rest
dropPrefix prefix text =
let name = fromMaybe "" (T.stripPrefix prefix text)
in if T.null name
then "."
else fromMaybe name (T.stripPrefix "-" name)
indentBlockNum :: Int -> Text -> Text
indentBlockNum i = indentBlock (T.replicate (i * 2) " ")
indentBlock :: Text -> Text -> Text
indentBlock prefix text
| T.null text = text
| otherwise =
let linesList = T.lines text
indented = map (prefix <>) linesList
joined = T.intercalate "\n" indented
in if T.isSuffixOf "\n" text
then joined <> "\n"
else joined
type Table = [Row]
type Row = [Text]
getSizes :: Table -> [Int]
getSizes xs = map size (transpose xs)
where
size :: Row -> Int
size = maximum . map T.length
printRow :: [Int] -> Row -> Text
printRow sizes ls =
T.strip
$ T.intercalate " "
$ zipWith (\item s -> T.justifyLeft s ' ' item) ls sizes
formatTableRow :: Table -> Row -> Text
formatTableRow table = printRow (getSizes table)
formatTable :: [Text] -> [Text]
formatTable deps = sort $ map (formatTableRow table) table
where
table = map words deps
formatList :: (Format a) => Text -> [a] -> Text
formatList x = T.intercalate x . map format
toCamelCase :: Text -> Text
toCamelCase txt = T.concat $ capitalizeLs $ T.splitOn "-" $ T.replace "_" "-" txt
where
capitalizeLs (x : xs) = x : map capitalize xs
capitalizeLs [] = []
capitalize t =
case T.uncons t of
Nothing -> ""
Just (c, rest) -> T.toUpper (T.singleton c) <> rest
andMore :: Text -> [Text] -> Text
andMore x xs = x <> if null xs then "" else " and " <> show (length xs) <> " more"