1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE TemplateHaskell #-}
3 {-# LANGUAGE DuplicateRecordFields #-}
7 import Data.List (intercalate, zipWith)
8 import Data.String hiding (unlines)
9 import Data.Text hiding (map, init, length, splitAt, foldl, zipWith)
10 import Data.Text.IO (putStrLn, putStr, writeFile)
11 import Data.Text.Lazy (toStrict)
12 import Data.Text.Template
13 import Data.Time.Clock.POSIX
15 import Prelude hiding (putStrLn, putStr, init, unlines, writeFile, zipWith)
17 import System.Directory
18 import System.Directory (doesFileExist)
19 import System.Environment
21 import System.File.Tree hiding (map, mapM, mapM_)
25 import Data.Aeson.Text
28 import Data.Bool (bool)
29 import Data.Tuple.Curry (uncurryN)
30 import qualified Data.ByteString.Lazy as BL
32 import Control.Monad.Trans.Maybe
33 import Control.Monad.Except (throwError)
34 import Control.Monad.Trans.Class (lift)
35 import Control.Monad.Trans.Except
36 import Control.Error.Safe
38 import qualified Data.ByteString.Lazy.UTF8 as BLU
40 getGlobalProjectDir :: IO FilePath
41 getGlobalProjectDir = (++ "/.xyz") <$> getHomeDirectory
43 getGlobalProjectConfigPath :: IO FilePath
44 getGlobalProjectConfigPath = getGlobalProjectDir >>= return . (++ "/xyz.json")
46 safeReadFile :: FilePath -> ExceptT Text IO Text
47 safeReadFile p = (lift $ doesFileExist p) >>= bool (throwError $ pack p +++ ": File does not exist") (lift $ pack <$> readFile p)
49 safeDecodeJSONFile :: FromJSON a => FilePath -> ExceptT Text IO a
50 safeDecodeJSONFile f = safeReadFile f >>= (tryRight . first appendFileName . eitherDecode . BLU.fromString . unpack)
51 where appendFileName s = pack $ f ++ ": " ++ s
53 getGlobalProjectConfig :: ExceptT Text IO GlobalConfig
54 getGlobalProjectConfig = lift path >>= safeDecodeJSONFile
55 where path = (++ "/xyz.json") <$> getGlobalProjectDir
57 getLocalProjectConfig :: ExceptT Text IO LocalConfig
58 getLocalProjectConfig = safeDecodeJSONFile "xyz.json"
60 getEntryTemplate :: Text -> ExceptT Text IO Template
61 getEntryTemplate t = template <$> (lift path >>= safeReadFile)
62 where path = (++ "/entry-" ++ (unpack t) ++ ".txt") <$> getGlobalProjectDir
64 resolveProjectConfig :: ExceptT Text IO ProjectConfig
65 resolveProjectConfig = getProjectConfig <$> getGlobalProjectConfig <*> getLocalProjectConfig
67 init :: ExceptT Text IO LocalConfig -> ExceptT Text IO GlobalConfig -> ExceptT Text IO Text
68 init local global = do
69 l <- lift $ runExceptT local
70 g <- lift $ runExceptT global
72 (_, Right _) -> pure "Project already initialised"
73 (Left _, Left _) -> globalInit >> localInit
74 (Right _, Left _) -> localInit
77 lift $ putStrLn "\nWelcome to xyz! Looks like this is your first time here. Let's configure a few things...\n"
78 editor <- lift editorPrompt
80 let pushCmdAndRemote = case vcs of
81 Just (True, x) -> [("pushCommand", "git add . && git commit -m \"$title\""), ("remote", x)]
84 lift $ getGlobalProjectConfigPath >>= flip writeFile (toStrict . encodeToLazyText $ GlobalConfig editor (lookup "pushCommand" pushCmdAndRemote) (lookup "remote" pushCmdAndRemote))
85 lift $ putStrLn "\nThanks! Now let's continue with initialising your project...\n"
88 home <- lift $ getHomeDirectory
89 themes <- lift $ allFilesIn $ home ++ "/.xyz/themes"
90 projectSpec <- lift $ (\[x,y,z] -> (x, y, z, Nothing, Nothing, Nothing)) <$> fmap snd <$> initPrompt (map pack themes)
91 let localConfig = uncurryN LocalConfig projectSpec
92 lift $ writeFile "xyz.json" (toStrict . encodeToLazyText $ localConfig)
93 config <- resolveProjectConfig
94 lift $ createDirectory "entries"
96 case (commitCommand (config :: ProjectConfig), gitRemote (config :: ProjectConfig)) of
97 (Just cmd, Just remote) -> do
98 lift $ putStrLn "Initialising project with git"
99 lift $ mapM_ (callCommand . unpack . (+++ " > /dev/null 2>&1")) ["git init", "git remote add origin " +++ remote, "git add .", "git commit -m \"Initial commit\""]
102 pure "Initialised empty project"
104 build :: ProjectConfig -> ExceptT Text IO Text
105 build config = (lift $ allFilesIn "entries" >>= writeOutProject config) >> pure "Build successful"
107 entry :: ProjectConfig -> ExceptT Text IO Text
109 homeDir <- lift getHomeDirectory
110 entrySpec <- lift entryPrompt
111 entryNum <- lift $ fmap (show . (+1) . length) $ allFilesIn "entries"
112 stamp <- lift $ fmap (pack . show . round) getPOSIXTime
113 videoTemplate <- getEntryTemplate $ "video"
114 normalTemplate <- getEntryTemplate $ "normal"
116 let rawEntry = case entrySpec of
117 Just (title, videoSpec) -> do
118 let baseContext = [("timestamp", stamp), ("title", title)]
120 -- TODO: Properly handle the Maybes for video and normal templates
121 Just (videoId, videoFilename) -> (title, toStrict . render videoTemplate $ context $ baseContext ++ [("videoid", videoId), ("videofilename", videoFilename)])
122 Nothing -> (title, toStrict . render normalTemplate $ context baseContext)
124 let fileName = "entries/" ++ entryNum ++ "-" ++ (unpack . slugify $ fst rawEntry) ++ ".txt"
125 lift $ writeFile fileName $ snd rawEntry
127 lift $ callCommand $ (unpack (editor (config :: ProjectConfig))) ++ " " ++ fileName
130 push <- lift pushPrompt
133 lift $ callCommand . unpack $ "git add . && git commit -m \"" +++ (fst rawEntry) +++ "\" && git push"
136 pure "\nThanks for using xyz!"
138 config :: ProjectConfig -> ExceptT Text IO Text
139 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
140 n = name (config :: ProjectConfig)
141 d = description (config :: ProjectConfig)
142 t = theme (config :: ProjectConfig)
143 e = editor (config :: ProjectConfig)
144 cc = commitCommand (config :: ProjectConfig)
150 usage = putStr . ununlines $ [
151 "usage: xyz <command>",
152 "Commands:", unlines [
153 "\tinit\t\tInitialise a new site",
154 "\tbuild\t\tBuild the site",
155 "\tentry\t\tInitialise an entry"
159 runExceptTAndOutput :: ExceptT Text IO Text -> IO ()
160 runExceptTAndOutput e = do
161 result <- runExceptT e
163 Left error -> putStrLn error >> (exitWith $ ExitFailure 1)
164 Right output -> putStrLn output >> exitWith ExitSuccess
167 main = getArgs >>= parse
169 parse ["init"] = runExceptTAndOutput $ init getLocalProjectConfig getGlobalProjectConfig
170 parse ["build"] = runExceptTAndOutput $ resolveProjectConfig >>= build
171 parse ["entry"] = runExceptTAndOutput $ resolveProjectConfig >>= entry
172 parse ["config"] = runExceptTAndOutput $ resolveProjectConfig >>= config