packages feed

hwm-0.0.1: 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,
    isOk,
    displayStatus,
    Format (..),
    availableOptions,
    renderSummaryStatus,
    subPathSign,
    slugify,
    commonPrefix,
    indentBlockNum,
    indentBlock,
    formatTable,
    formatList,
    monadStatus,
  )
where

import Data.Aeson (Value, encode)
import Data.ByteString.Lazy.Char8 (unpack)
import Data.Foldable (maximum)
import qualified Data.List as List
import Data.Text (pack)
import qualified Data.Text as T
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

data Status = Checked | Updated | Warning | Invalid
  deriving (Show, Eq, Ord)

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

displayStatus :: [(Text, Status)] -> Text
displayStatus ls =
  let status = deriveStatus (map snd ls)
   in if isOk status then statusIcon status else formatStatus ls

padDots :: Int -> Text -> Text
padDots width s = s <> " " <> chalk Dim (T.replicate (max 0 (width - T.length s)) ".") <> " "

isOk :: Status -> Bool
isOk Checked = True
isOk Updated = True
isOk _ = False

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

genMaxLen :: [Text] -> Int
genMaxLen names = if null names then 16 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

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 "└─- "

slugify :: Text -> Text
slugify = T.map replaceChar . T.toLower
  where
    replaceChar c
      | c == ' ' = '-'
      | c == '_' = '-'
      | c == '.' = '-'
      | otherwise = c

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 " ")

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

formatTable :: [Text] -> [Text]
formatTable deps = sort $ map (printRow (getSizes table)) table
  where
    table = map words deps

formatList :: (Format a) => Text -> [a] -> Text
formatList x = T.intercalate x . map format