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.Exception (catch, IOException)
35 main = getArgs >>= parse
37 getGlobalProjectDir :: IO FilePath
38 getGlobalProjectDir = getHomeDirectory >>= return . (++ "/.xyz")
40 getGlobalProjectConfigPath :: IO FilePath
41 getGlobalProjectConfigPath = getGlobalProjectDir >>= return . (++ "/xyz.json")
43 getGlobalProjectConfig :: IO (Maybe GlobalConfig)
44 getGlobalProjectConfig = getGlobalProjectConfigPath >>= (\path -> doesFileExist path >>= bool (return Nothing) (BL.readFile path >>= return . decode))
46 getLocalProjectConfig :: IO (Maybe LocalConfig)
47 getLocalProjectConfig = doesFileExist "xyz.json" >>= bool (return Nothing) (BL.readFile "xyz.json" >>= return . decode)
49 safeReadFile :: FilePath -> IO (Maybe Text)
50 safeReadFile p = (Just . pack <$> readFile p) `catch` handler
52 handler :: IOException -> IO (Maybe Text)
53 handler = const $ pure Nothing
55 getEntryTemplate :: Text -> IO (Maybe Template)
56 getEntryTemplate t = (pure $ fmap template) <*> (safeReadFile =<< ((++ "/entry-" ++ (unpack t) ++ ".txt")) <$> getGlobalProjectDir)
58 resolveProjectConfig :: IO (Either Text ProjectConfig)
59 resolveProjectConfig = do
60 l <- getLocalProjectConfig
61 g <- getGlobalProjectConfig
63 return $ case (l, g) of
64 (Just l, Just g) -> Right $ getProjectConfig g l
65 (Just _, Nothing) -> Left "Busted global config"
66 (Nothing, Just _) -> Left "Busted local config"
67 _ -> Left "All configs are busted"
69 continueIfInvalidProject :: IO () -> IO ()
70 continueIfInvalidProject next = doesFileExist "xyz.json" >>= bool next (putStrLn "Project already initialised")
72 initGlobalIfNeeded :: IO () -> IO ()
73 initGlobalIfNeeded next = getGlobalProjectConfigPath >>= doesFileExist >>= bool globalInit (return ()) >> next
77 home <- getHomeDirectory
78 putStrLn "\nWelcome to xyz! Looks like this is your first time here. Let's configure a few things...\n"
79 editor <- editorPrompt
81 let pushCmdAndRemote = case vcs of
82 Just (True, x) -> [("pushCommand", "git add . && git commit -m \"$title\""), ("remote", x)]
85 getGlobalProjectConfigPath >>= flip writeFile (toStrict . encodeToLazyText $ GlobalConfig editor (lookup "pushCommand" pushCmdAndRemote) (lookup "remote" pushCmdAndRemote))
86 putStrLn "\nThanks! Now let's continue with initialising your project...\n"
90 home <- getHomeDirectory
91 themes <- allFilesIn $ home ++ "/.xyz/themes"
92 projectSpec <- (\[x,y,z] -> (x, y, z, Nothing, Nothing, Nothing)) <$> fmap snd <$> initPrompt (map pack themes)
93 let localConfig = uncurryN LocalConfig projectSpec
94 writeFile "xyz.json" (toStrict . encodeToLazyText $ localConfig)
95 config <- resolveProjectConfig
98 Right projectConfig -> do
99 createDirectory "entries"
101 case (commitCommand (projectConfig :: ProjectConfig), gitRemote (projectConfig :: ProjectConfig)) of
102 (Just cmd, Just remote) -> do
103 putStrLn "Initialising project with git"
104 mapM_ (callCommand . unpack . (+++ " > /dev/null 2>&1")) ["git init", "git remote add origin " +++ remote, "git add .", "git commit -m \"Initial commit\""]
107 putStrLn "Initialised empty project"
111 build :: ProjectConfig -> IO ()
112 build config = allFilesIn "entries" >>= (writeOutProject config) >> putStrLn "Build successful"
114 entry :: ProjectConfig -> IO ()
116 homeDir <- getHomeDirectory
117 entrySpec <- entryPrompt
118 entryNum <- fmap (show . (+1) . length) $ allFilesIn "entries"
119 stamp <- fmap (pack . show . round) getPOSIXTime
120 videoTemplate <- getEntryTemplate "video"
121 normalTemplate <- getEntryTemplate "normal"
123 let rawEntry = case entrySpec of
124 Just (title, videoSpec) -> do
125 let baseContext = [("timestamp", stamp), ("title", title)]
127 -- TODO: Properly handle the Maybes for video and normal templates
128 Just (videoId, videoFilename) -> (title, toStrict . render (fromJust videoTemplate) $ context $ baseContext ++ [("videoid", videoId), ("videofilename", videoFilename)])
129 Nothing -> (title, toStrict . render (fromJust normalTemplate) $ context baseContext)
131 let fileName = "entries/" ++ entryNum ++ "-" ++ (unpack . slugify $ fst rawEntry) ++ ".txt"
132 writeFile fileName $ snd rawEntry
134 callCommand $ (unpack (editor (config :: ProjectConfig))) ++ " " ++ fileName
140 callCommand . unpack $ "git add . && git commit -m \"" +++ (fst rawEntry) +++ "\" && git push"
143 putStrLn "\nThanks for using xyz!"
145 config :: ProjectConfig -> IO ()
146 config config = (putStrLn "\nProject details:\n") >> (putStr . unlines $ zipWith (+++) ["Name:\t\t", "Description:\t", "Theme:\t\t", "Editor:\t\t", "Commit command:\t"] [n, d, t, e, ccc]) where
147 n = name (config :: ProjectConfig)
148 d = description (config :: ProjectConfig)
149 t = theme (config :: ProjectConfig)
150 e = editor (config :: ProjectConfig)
151 cc = commitCommand (config :: ProjectConfig)
157 usage = putStr . ununlines $ [
158 "usage: xyz <command>",
159 "Commands:", unlines [
160 "\tinit\t\tInitialise a new site",
161 "\tbuild\t\tBuild the site",
162 "\tentry\t\tInitialise an entry"
166 continueIfValidProject :: (ProjectConfig -> IO ()) -> Either Text ProjectConfig -> IO ()
167 continueIfValidProject next projectConfig = case projectConfig of
168 Right config -> next config
169 Left error -> putStrLn $ "This does not appear to be a vailid project directory: " +++ error
171 parse ["init"] = initGlobalIfNeeded (continueIfInvalidProject init) >> exitSuccess
172 parse ["build"] = resolveProjectConfig >>= (continueIfValidProject build) >> exitSuccess
173 parse ["entry"] = resolveProjectConfig >>= (continueIfValidProject entry) >> exitSuccess
174 parse ["config"] = resolveProjectConfig >>= (continueIfValidProject config) >> exitSuccess
175 parse [_] = usage >> exit
176 parse [] = usage >> exit
178 exit = exitWith ExitSuccess