1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE TemplateHaskell #-}
3 {-# LANGUAGE DuplicateRecordFields #-}
7 import Data.List (intercalate, zipWith)
8 import Data.Text hiding (map, init, length, splitAt, foldl, zipWith)
9 import Data.Text.IO (putStrLn, putStr, writeFile)
10 import Data.Text.Lazy (toStrict)
11 import Data.Text.Template
12 import Data.Time.Clock.POSIX
14 import Prelude hiding (putStrLn, putStr, init, unlines, writeFile, zipWith)
16 import System.Directory
17 import System.Environment
21 import Data.Aeson.Text
23 import Data.Bool (bool)
24 import Data.Tuple.Curry (uncurryN)
25 import qualified Data.ByteString.Lazy as BL
27 import Control.Monad.Except (throwError)
28 import Control.Monad.Trans.Class (lift)
29 import Control.Monad.Trans.Except
30 import Control.Error.Safe
32 import qualified Data.ByteString.Lazy.UTF8 as BLU
34 getGlobalProjectDir :: IO FilePath
35 getGlobalProjectDir = (++ "/.xyz") <$> getHomeDirectory
37 getGlobalProjectConfigPath :: IO FilePath
38 getGlobalProjectConfigPath = getGlobalProjectDir >>= return . (++ "/xyz.json")
40 safeReadFile :: FilePath -> ExceptT Text IO Text
41 safeReadFile p = (lift $ doesFileExist p) >>= bool (throwError $ pack p +++ ": File does not exist") (lift $ pack <$> readFile p)
43 safeDecodeJSONFile :: FromJSON a => FilePath -> ExceptT Text IO a
44 safeDecodeJSONFile f = safeReadFile f >>= (tryRight . first appendFileName . eitherDecode . BLU.fromString . unpack)
45 where appendFileName s = pack $ f ++ ": " ++ s
47 getGlobalProjectConfig :: ExceptT Text IO GlobalConfig
48 getGlobalProjectConfig = lift path >>= safeDecodeJSONFile
49 where path = (++ "/xyz.json") <$> getGlobalProjectDir
51 getLocalProjectConfig :: ExceptT Text IO LocalConfig
52 getLocalProjectConfig = safeDecodeJSONFile "xyz.json"
54 getEntryTemplate :: Text -> ExceptT Text IO Template
55 getEntryTemplate t = template <$> (lift path >>= safeReadFile)
56 where path = (++ "/entry-" ++ (unpack t) ++ ".txt") <$> getGlobalProjectDir
58 resolveProjectConfig :: ExceptT Text IO ProjectConfig
59 resolveProjectConfig = getProjectConfig <$> getGlobalProjectConfig <*> getLocalProjectConfig
61 init :: ExceptT Text IO LocalConfig -> ExceptT Text IO GlobalConfig -> ExceptT Text IO Text
62 init local global = do
63 l <- lift $ runExceptT local
64 g <- lift $ runExceptT global
66 (_, Right _) -> pure "Project already initialised"
67 (Left _, Left _) -> globalInit >> localInit
68 (Right _, Left _) -> localInit
71 lift $ putStrLn "\nWelcome to xyz! Looks like this is your first time here. Let's configure a few things...\n"
72 editor <- lift editorPrompt
74 let pushCmdAndRemote = case vcs of
75 Just (True, x) -> [("pushCommand", "git add . && git commit -m \"$title\""), ("remote", x)]
78 lift $ getGlobalProjectConfigPath >>= flip writeFile (toStrict . encodeToLazyText $ GlobalConfig editor (lookup "pushCommand" pushCmdAndRemote) (lookup "remote" pushCmdAndRemote))
79 lift $ putStrLn "\nThanks! Now let's continue with initialising your project...\n"
82 home <- lift $ getHomeDirectory
83 themes <- lift $ allFilesIn $ home ++ "/.xyz/themes"
84 projectSpec <- lift $ (\[x,y,z] -> (x, y, z, Nothing, Nothing, Nothing)) <$> fmap snd <$> initPrompt (map pack themes)
85 let localConfig = uncurryN LocalConfig projectSpec
86 lift $ writeFile "xyz.json" (toStrict . encodeToLazyText $ localConfig)
87 config <- resolveProjectConfig
88 lift $ createDirectory "entries"
90 case (commitCommand (config :: ProjectConfig), gitRemote (config :: ProjectConfig)) of
91 (Just cmd, Just remote) -> do
92 lift $ putStrLn "Initialising project with git"
93 lift $ mapM_ (callCommand . unpack . (+++ " > /dev/null 2>&1")) ["git init", "git remote add origin " +++ remote, "git add .", "git commit -m \"Initial commit\""]
96 pure "Initialised empty project"
98 build :: ProjectConfig -> ExceptT Text IO Text
99 build config = (lift $ allFilesIn "entries" >>= writeOutProject config) >> pure "Build successful"
101 entry :: ProjectConfig -> ExceptT Text IO Text
103 homeDir <- lift getHomeDirectory
104 entrySpec <- lift entryPrompt
105 entryNum <- lift $ fmap (show . (+1) . length) $ allFilesIn "entries"
106 stamp <- lift $ fmap (pack . show . round) getPOSIXTime
107 videoTemplate <- getEntryTemplate $ "video"
108 normalTemplate <- getEntryTemplate $ "normal"
110 let rawEntry = case entrySpec of
111 Just (title, videoSpec) -> do
112 let baseContext = [("timestamp", stamp), ("title", title)]
114 -- TODO: Properly handle the Maybes for video and normal templates
115 Just (videoId, videoFilename) -> (title, toStrict . render videoTemplate $ context $ baseContext ++ [("videoid", videoId), ("videofilename", videoFilename)])
116 Nothing -> (title, toStrict . render normalTemplate $ context baseContext)
118 let fileName = "entries/" ++ entryNum ++ "-" ++ (unpack . slugify $ fst rawEntry) ++ ".txt"
119 lift $ writeFile fileName $ snd rawEntry
121 lift $ callCommand $ (unpack (editor (config :: ProjectConfig))) ++ " " ++ fileName
124 push <- lift pushPrompt
127 lift $ callCommand . unpack $ "git add . && git commit -m \"" +++ (fst rawEntry) +++ "\" && git push"
130 pure "\nThanks for using xyz!"
132 config :: ProjectConfig -> ExceptT Text IO Text
133 config config = pure $ "\nProject details:\n" +++ (unlines $ zipWith (+++) ["Name:\t\t", "Description:\t", "Theme:\t\t", "Editor:\t\t", "Commit command:\t"] [n, d, t, e, ccc]) where
134 n = name (config :: ProjectConfig)
135 d = description (config :: ProjectConfig)
136 t = theme (config :: ProjectConfig)
137 e = editor (config :: ProjectConfig)
138 cc = commitCommand (config :: ProjectConfig)
144 usage = putStr . ununlines $ [
145 "usage: xyz <command>",
146 "Commands:", unlines [
147 "\tinit\t\tInitialise a new site",
148 "\tbuild\t\tBuild the site",
149 "\tentry\t\tInitialise an entry"
153 runExceptTAndOutput :: ExceptT Text IO Text -> IO ()
154 runExceptTAndOutput e = do
155 result <- runExceptT e
157 Left error -> putStrLn error >> (exitWith $ ExitFailure 1)
158 Right output -> putStrLn output >> exitWith ExitSuccess
161 main = getArgs >>= parse
163 parse ["init"] = runExceptTAndOutput $ init getLocalProjectConfig getGlobalProjectConfig
164 parse ["build"] = runExceptTAndOutput $ resolveProjectConfig >>= build
165 parse ["entry"] = runExceptTAndOutput $ resolveProjectConfig >>= entry
166 parse ["config"] = runExceptTAndOutput $ resolveProjectConfig >>= config