packages feed

hinit-0.1.0: src/Hinit/Config.hs

{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE QuasiQuotes #-}

-- | Global configuration
module Hinit.Config where

import Control.Algebra
import Control.Effect.Lift
import Control.Effect.Terminal
import Control.Effect.Throw
import Control.Effect.Time as T
import qualified Data.Map.Strict as M
import Data.String.Interpolate
import Data.Text (Text, pack, unpack)
import qualified Data.Text.IO as T
import Data.Time.Calendar
import Data.Time.Format.ISO8601
import Data.Time.LocalTime
import Distribution.Parsec
import Distribution.Pretty
import Distribution.SPDX.Extra
import GHC.Generics
import Hinit.Errors
import Hinit.Types
import Path
import Path.IO
import System.IO
import Toml hiding (Bool, Text, day)
import qualified Toml as T

data Config = Config
  { name :: Text,
    email :: Text,
    ghUserName :: Text,
    license :: Maybe LicenseId,
    vcs :: Maybe VCS,
    defAttrs :: Context
  }
  deriving (Show, Eq, Generic)

configCodec :: TomlCodec Config
configCodec =
  Config
    <$> text "name" .= name
    <*> text "email" .= email
    <*> text "github_username" .= ghUserName
    <*> dioptional (licenseIdCodec "license") .= license
    <*> vcsCodec "vcs" .= vcs
    <*> contextCodec .= defAttrs

licenseToToml :: LicenseId -> AnyValue
licenseToToml license = AnyValue $ T.Text $ pack $ prettyShow license

tomlToLicense :: AnyValue -> Either TomlBiMapError LicenseId
tomlToLicense (AnyValue v)
  | T.Text t <- v =
    case eitherParsec (unpack t) of
      Left err ->
        Left $
          ArbitraryError [i|Failed to parse license: #{err}|]
      Right l -> pure l
  | otherwise = Left $ WrongValue $ MatchError TText (AnyValue v)

_LicenseId :: TomlBiMap LicenseId AnyValue
_LicenseId = invert $ prism licenseToToml tomlToLicense

licenseIdCodec :: Key -> TomlCodec LicenseId
licenseIdCodec = match _LicenseId

askConfig :: forall sig m. Has Terminal sig m => m Config
askConfig = do
  prettyPrint stdout "No config has been found, creating a new config (~/.config/hi/config.toml)"
  prettyPrint stdout "Please enter your name:"
  name <- askText
  prettyPrint stdout "Your email:"
  email <- askText
  prettyPrint stdout "Your github username:"
  ghUserName <- askText
  let vcs = Just Git
  let defAttrs = mempty
  let license = Nothing
  pure Config {..}
  where
    askText :: m Text
    askText = do
      resp <- prompt "> "
      maybe askText pure resp

getConfigFile :: (Has (Lift IO) sig m) => m (Path Abs File)
getConfigFile = do
  configDir <- sendIO $ getXdgDir XdgConfig $ Just [reldir|hi|]
  sendIO $ ensureDir configDir
  let configFile = configDir </> [relfile|config.toml|]
  pure configFile

readConfig :: (Has (Lift IO) sig m, Has (Throw ConfigParseError) sig m) => Path Abs File -> m (Maybe Config)
readConfig configFile = do
  exists <- sendIO $ doesFileExist configFile
  if exists
    then do
      f <- sendIO $ T.readFile $ fromAbsFile configFile
      case decode configCodec f of
        Left e -> throwError $ ConfigParseError Global e
        Right config -> pure (Just config)
    else pure Nothing

getConfig ::
  ( Has Terminal sig m,
    Has (Lift IO) sig m,
    Has (Throw ConfigParseError) sig m
  ) =>
  m Config
getConfig = do
  configFile <- getConfigFile
  mConfig <- readConfig configFile
  case mConfig of
    Just c -> pure c
    Nothing -> do
      config <- askConfig
      sendIO $ T.writeFile (fromAbsFile configFile) $ encode configCodec config
      pure config

-- | Build two 'Context's from a program config
buildContextFromConfig :: Has Time sig m => Text -> Config -> m (Context, Context)
buildContextFromConfig project Config {..} = do
  ZonedTime {..} <- T.getZonedTime
  let lday = localDay zonedTimeToLocalTime
  let (show' -> year, show' -> month, show' -> day) = toGregorian lday
  let iso8601 = pack $ iso8601Show lday
  let overrides =
        [ ("year", Text year),
          ("month", Text month),
          ("day", Text day),
          ("iso8601", Text iso8601),
          ("name", Text name),
          ("email", Text email),
          ("github_username", Text ghUserName),
          ("project", Text project)
        ]
  pure (M.fromList overrides, defAttrs)
  where
    show' :: Show a => a -> Text
    show' = pack . show