{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Life.Configuration
( LifePath (..)
, LifeConfiguration (..)
, singleDirConfig
, singleFileConfig
, lifeConfigMinus
, files
, directories
, parseHomeLife
, parseRepoLife
, parseLifeConfiguration
, renderLifeConfiguration
, writeGlobalLife
) where
import Control.Monad.Catch (MonadThrow (..))
import Fmt (indentF, unlinesF, (+|), (|+))
import Lens.Micro.Platform (makeFields, (.~), (^.))
import Path (Dir, File, Path, Rel, fromAbsFile, parseRelDir, parseRelFile, toFilePath, (</>))
import Toml (AnyValue (..), BiToml, Prism (..), (.=))
import Life.Shell (lifePath, relativeToHome, repoName)
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Text.Show as Show
import qualified Toml
data LifePath = File FilePath | Dir FilePath
deriving (Show)
data LifeConfiguration = LifeConfiguration
{ lifeConfigurationFiles :: Set (Path Rel File)
, lifeConfigurationDirectories :: Set (Path Rel Dir)
} deriving (Show, Eq)
makeFields ''LifeConfiguration
instance Semigroup LifeConfiguration where
life1 <> life2 = LifeConfiguration
{ lifeConfigurationFiles = life1^.files <> life2^.files
, lifeConfigurationDirectories = life1^.directories <> life2^.directories
}
instance Monoid LifeConfiguration where
mempty = LifeConfiguration mempty mempty
mappend = (<>)
singleFileConfig :: Path Rel File -> LifeConfiguration
singleFileConfig file = mempty & files .~ one file
singleDirConfig :: Path Rel Dir -> LifeConfiguration
singleDirConfig dir = mempty & directories .~ one dir
lifeConfigMinus :: LifeConfiguration
-> LifeConfiguration
-> LifeConfiguration
lifeConfigMinus dotfiles global = LifeConfiguration
(Set.difference (dotfiles ^. files) (global ^. files))
(Set.difference (dotfiles ^. directories) (global ^. directories))
data CorpseConfiguration = CorpseConfiguration
{ corpseFiles :: [FilePath]
, corpseDirectories :: [FilePath]
}
corpseConfiguationT :: BiToml CorpseConfiguration
corpseConfiguationT = CorpseConfiguration
<$> Toml.arrayOf _String "files" .= corpseFiles
<*> Toml.arrayOf _String "directories" .= corpseDirectories
where
_String :: Prism AnyValue String
_String = Prism
{ preview = \(AnyValue t) -> Toml.matchText t >>= pure . toString
, review = AnyValue . Toml.Text . toText
}
resurrect :: MonadThrow m => CorpseConfiguration -> m LifeConfiguration
resurrect CorpseConfiguration{..} = do
filePaths <- mapM parseRelFile corpseFiles
dirPaths <- mapM parseRelDir corpseDirectories
pure $ LifeConfiguration
{ lifeConfigurationFiles = Set.fromList filePaths
, lifeConfigurationDirectories = Set.fromList dirPaths
}
renderLifeConfiguration :: Bool
-> LifeConfiguration
-> Text
renderLifeConfiguration printIfEmpty LifeConfiguration{..} = mconcat $
maybeToList (render "directories" lifeConfigurationDirectories)
++ [ "\n" ]
++ maybeToList (render "files" lifeConfigurationFiles)
where
render :: Text -> Set (Path b t) -> Maybe Text
render key paths = do
let prefix = key <> " = "
let array = renderStringArray (T.length prefix) (map show $ toList paths)
if not printIfEmpty && null paths
then Nothing
else Just $ prefix <> array
renderStringArray :: Int -> [String] -> Text
renderStringArray _ [] = "[]"
renderStringArray n (x:xs) = "[ " +| x |+ "\n"
+| indentF n (unlinesF (map (", " ++) xs ++ ["]"]))
|+ ""
writeGlobalLife :: LifeConfiguration -> IO ()
writeGlobalLife config = do
lifeFilePath <- relativeToHome lifePath
writeFile (fromAbsFile lifeFilePath) (renderLifeConfiguration True config)
parseLifeConfiguration :: MonadThrow m => Text -> m LifeConfiguration
parseLifeConfiguration tomlText = case Toml.decode corpseConfiguationT tomlText of
Left err -> throwM $ LoadTomlException (toFilePath lifePath) $ Toml.prettyException err
Right cfg -> resurrect cfg
parseLife :: Path Rel File -> IO LifeConfiguration
parseLife path = relativeToHome path
>>= readFile . fromAbsFile
>>= parseLifeConfiguration
parseHomeLife :: IO LifeConfiguration
parseHomeLife = parseLife lifePath
parseRepoLife :: IO LifeConfiguration
parseRepoLife = parseLife (repoName </> lifePath)
data LoadTomlException = LoadTomlException FilePath Text
instance Show.Show LoadTomlException where
show (LoadTomlException filePath msg) = "Couldnt parse file " ++ filePath ++ ": " ++ show msg
instance Exception LoadTomlException