packages feed

tasklite-core-0.3.0.0: source/Config.hs

{-|
The default config primarily defines the styling and formatting
-}
module Config where

import Protolude as P (
  Applicative (pure),
  Char,
  Eq ((==)),
  FilePath,
  Foldable (foldl),
  Functor (fmap),
  Generic,
  Int,
  Maybe (..),
  Semigroup ((<>)),
  Show,
  Text,
  decodeUtf8,
  fromMaybe,
  lines,
  show,
  unlines,
  ($),
  (&),
  (.),
 )

import Data.Aeson (
  FromJSON (parseJSON),
  ToJSON (toJSON),
  Value (String),
  withObject,
  withText,
  (.!=),
  (.:?),
 )
import Data.Hourglass (TimeFormat (toFormat), TimeFormatString)
import Data.Text as T (
  dropEnd,
  isInfixOf,
  pack,
  replace,
  split,
  strip,
  stripPrefix,
 )
import Data.Yaml (encode)
import Prettyprinter.Internal (Pretty (pretty))
import Prettyprinter.Render.Terminal (
  AnsiStyle,
  Color (..),
  color,
  colorDull,
 )
import Prettyprinter.Render.Terminal.Internal (ansiForeground)
import System.FilePath (takeBaseName)


data Hook = Hook
  { filePath :: Maybe FilePath
  , interpreter :: Text
  , body :: Text
  }
  deriving (Generic, Show)


instance ToJSON Hook
instance FromJSON Hook


emptyHook :: Hook
emptyHook =
  Hook
    { filePath = Nothing
    , interpreter = ""
    , body = ""
    }


data HookSet = HookSet
  { pre :: [Hook]
  , post :: [Hook]
  }
  deriving (Generic, Show)


instance ToJSON HookSet


-- | Necessary to make fields optional without using a Maybe type
instance FromJSON HookSet where
  parseJSON = withObject "hookSet" $ \o -> do
    pre <- o .:? "pre" .!= pre emptyHookSet
    post <- o .:? "post" .!= post emptyHookSet

    pure $ HookSet{..}


emptyHookSet :: HookSet
emptyHookSet =
  HookSet
    { pre = []
    , post = []
    }


data HooksConfig = HooksConfig
  { directory :: FilePath
  , launch :: HookSet
  , add :: HookSet
  , modify :: HookSet
  , exit :: HookSet
  -- TODO: , delete :: HookSet
  }
  deriving (Generic, Show)


instance ToJSON HooksConfig


-- | Necessary to make fields optional without using a Maybe type
instance FromJSON HooksConfig where
  parseJSON = withObject "hooksConfig" $ \o -> do
    directory <- o .:? "directory" .!= directory defaultHooksConfig
    launch <- o .:? "launch" .!= launch defaultHooksConfig
    add <- o .:? "add" .!= add defaultHooksConfig
    modify <- o .:? "modify" .!= modify defaultHooksConfig
    exit <- o .:? "exit" .!= exit defaultHooksConfig

    pure $ HooksConfig{..}


defaultHooksConfig :: HooksConfig
defaultHooksConfig =
  HooksConfig
    { directory = ""
    , launch = emptyHookSet
    , add = emptyHookSet
    , modify = emptyHookSet
    , exit = emptyHookSet
    }


addHookFilesToConfig :: Config -> [(FilePath, b, Text)] -> Config
addHookFilesToConfig =
  let
    buildHook :: FilePath -> Text -> Hook
    buildHook filePath content =
      case lines content of
        firstLine : rest ->
          Hook
            { filePath = Just filePath
            , interpreter =
                firstLine
                  & T.replace "#!" ""
                  & T.strip
            , body =
                rest
                  & unlines
                  & T.strip
            }
        _ -> emptyHook

    addToHookSet :: Hook -> Text -> HookSet -> HookSet
    addToHookSet hook stage hookSet =
      case stage of
        "pre" -> hookSet{pre = hookSet.pre <> [hook]}
        "post" -> hookSet{post = hookSet.post <> [hook]}
        _ -> hookSet

    addToHooksConfig :: Text -> Text -> Hook -> HooksConfig -> HooksConfig
    addToHooksConfig event stage hook hConfig =
      case event of
        "launch" ->
          hConfig
            { launch = addToHookSet hook stage (hConfig & launch)
            }
        "add" ->
          hConfig
            { add = addToHookSet hook stage (hConfig & add)
            }
        "modify" ->
          hConfig
            { modify = addToHookSet hook stage (hConfig & modify)
            }
        "exit" ->
          hConfig
            { exit = addToHookSet hook stage (hConfig & exit)
            }
        _ -> hConfig
  in
    P.foldl $ \conf (filePath, _, fileContent) ->
      case split (== '-') $ pack $ takeBaseName filePath of
        [stage, event] ->
          conf
            { hooks =
                addToHooksConfig
                  event
                  stage
                  (buildHook filePath fileContent)
                  conf.hooks
            }
        _ -> conf


data Config = Config
  { tableName :: Text
  , idStyle :: AnsiStyle
  , priorityStyle :: AnsiStyle
  , dateStyle :: AnsiStyle
  , bodyStyle :: AnsiStyle
  , bodyClosedStyle :: AnsiStyle
  , closedStyle :: AnsiStyle
  , dueStyle :: AnsiStyle
  , overdueStyle :: AnsiStyle
  , tagStyle :: AnsiStyle
  , utcFormat :: TimeFormatString
  , utcFormatShort :: TimeFormatString
  , dataDir :: FilePath
  , dbName :: FilePath
  , dateWidth :: Int
  , bodyWidth :: Int
  , prioWidth :: Int
  , headCount :: Int
  , maxWidth :: Int
  , progressBarWidth :: Int
  , hooks :: HooksConfig
  }
  deriving (Generic, Show)


-- | Necessary to make fields optional without using a Maybe type
{- FOURMOLU_DISABLE -}
instance FromJSON Config where
  parseJSON = withObject "config" $ \o -> do
    idStyle         <- o .:? "idStyle" .!= defaultConfig.idStyle
    priorityStyle   <- o .:? "priorityStyle".!= defaultConfig.priorityStyle
    tableName       <- o .:? "tableName" .!= defaultConfig.tableName
    dateStyle       <- o .:? "dateStyle" .!= defaultConfig.dateStyle
    bodyStyle       <- o .:? "bodyStyle" .!= defaultConfig.bodyStyle
    bodyClosedStyle <- o .:? "bodyClosedStyle".!= defaultConfig.bodyClosedStyle
    closedStyle     <- o .:? "closedStyle" .!= defaultConfig.closedStyle
    dueStyle        <- o .:? "dueStyle" .!= defaultConfig.dueStyle
    overdueStyle    <- o .:? "overdueStyle" .!= defaultConfig.overdueStyle
    tagStyle        <- o .:? "tagStyle" .!= defaultConfig.tagStyle
    utcFormat       <- o .:? "utcFormat" .!= defaultConfig.utcFormat
    utcFormatShort  <- o .:? "utcFormatShort" .!= defaultConfig.utcFormatShort
    dataDir         <- o .:? "dataDir" .!= defaultConfig.dataDir
    dbName          <- o .:? "dbName" .!= defaultConfig.dbName
    dateWidth       <- o .:? "dateWidth" .!= defaultConfig.dateWidth
    bodyWidth       <- o .:? "bodyWidth" .!= defaultConfig.bodyWidth
    prioWidth       <- o .:? "prioWidth" .!= defaultConfig.prioWidth
    headCount       <- o .:? "headCount" .!= defaultConfig.headCount
    maxWidth        <- o .:? "maxWidth" .!= defaultConfig.maxWidth
    progressBarWidth <- o .:? "progressBarWidth"
                                .!= defaultConfig.progressBarWidth
    hooks           <- o .:? "hooks" .!= defaultConfig.hooks

    pure $ Config{..}

{- FOURMOLU_ENABLE -}


instance ToJSON Config


instance Pretty Config where
  pretty =
    pretty
      . dropEnd 1 -- Drop trailing newline to maybe add it later
      . decodeUtf8
      . Data.Yaml.encode


parseColor :: Text -> Maybe Color
parseColor colorTxt =
  let
    colorOnly = fromMaybe colorTxt $ stripPrefix "dull " colorTxt
    colorToType = \case
      "black" -> Just Black
      "red" -> Just Red
      "green" -> Just Green
      "yellow" -> Just Yellow
      "blue" -> Just Blue
      "magenta" -> Just Magenta
      "cyan" -> Just Cyan
      "white" -> Just White
      _ -> Nothing
  in
    colorToType colorOnly


parseAnsiStyle :: Text -> Maybe AnsiStyle
parseAnsiStyle colorTxt =
  let
    func =
      if "dull" `T.isInfixOf` colorTxt
        then colorDull
        else color
    colorMaybe = parseColor colorTxt
  in
    fmap func colorMaybe


instance FromJSON AnsiStyle where
  parseJSON = withText "AnsiStyle" $ \value -> do
    pure $ fromMaybe (color Black) $ parseAnsiStyle value


instance ToJSON AnsiStyle where
  toJSON style = String $ show $ ansiForeground style


instance FromJSON TimeFormatString where
  parseJSON = withText "TimeFormatString" $ \_ -> do
    pure (toFormat ("YYYY-MM-DD H:MI:S" :: [Char]))


instance ToJSON TimeFormatString where
  toJSON = pure "YYYY-MM-DD H:MI:S"


-- instance Pretty AnsiStyle where
--   pretty = pretty . (\_ -> ". " :: Text)

defaultConfig :: Config
defaultConfig =
  Config
    { tableName = "tasks"
    , idStyle = color Green
    , priorityStyle = color Magenta
    , dateStyle = color Black
    , bodyStyle = color White
    , bodyClosedStyle = color Black
    , closedStyle = colorDull Black
    , dueStyle = color Yellow
    , overdueStyle = color Red
    , tagStyle = color Blue
    , utcFormat = toFormat ("YYYY-MM-DD H:MI:S" :: [Char])
    , utcFormatShort = toFormat ("YYYY-MM-DD H:MI" :: [Char])
    , dataDir = ""
    , dbName = "main.db"
    , dateWidth = 10
    , bodyWidth = 10
    , prioWidth = 4
    , headCount = 20
    , maxWidth = 120
    , progressBarWidth = 24
    , hooks =
        HooksConfig
          { directory = ""
          , launch = emptyHookSet
          , add = emptyHookSet
          , modify = emptyHookSet
          , exit = emptyHookSet
          }
    }