puffytools-0.0.0.1: src/PuffyTools/Journal.hs
{- |
Module : PuffyTools.Journal
Description : A Journal-keeping thing
Copyright : 2014, Peter Harpending
License : BSD3
Maintainer : Peter Harpending <pharpend2@gmail.com>
Stability : experimental
Portability : Linux
-}
module PuffyTools.Journal where
import Control.Applicative
import Data.Aeson
import Data.Aeson.Encode.Pretty
import Data.Monoid
import qualified Data.ByteString as Bs
import qualified Data.ByteString.Lazy as B
import Data.List.Utils
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
import Data.Vector (Vector)
import qualified Data.Vector as V
import PuffyTools.Slug
import System.Directory
import System.IO
programName :: String
programName = "puffytools"
journalExt :: String
journalExt = ".journal"
-- |A Journal is really a wrapper around a list of entries
data Journal = Journal { journalSlug :: Slug
, journalTitle :: Text
, journalLastEdited :: UTCTime
, journalCreated :: UTCTime
, journalDescription :: Maybe Text
, journalEntries :: Vector Entry
}
deriving (Show, Eq)
-- |Entries
data Entry = Entry { entrySummary :: Text
, entryCreated :: UTCTime
, entryLastEdited :: UTCTime
}
deriving (Show, Eq)
instance FromJSON Journal where
parseJSON (Object v) = Journal <$> v .: "slug"
<*> v .: "title"
<*> v .: "last-edited"
<*> v .: "created"
<*> v .: "description"
<*> v .: "entries"
parseJSON _ = fail "Must be an object"
instance ToJSON Journal where
toJSON (Journal s t le cr des ent) = object
[ "slug" .= s
, "title" .= t
, "last-edited" .= le
, "created" .= cr
, "description" .= des
, "entries" .= ent
]
instance FromJSON Entry where
parseJSON (Object v) = Entry <$> v .: "summary"
<*> v .: "created"
<*> v .: "last-edited"
parseJSON _ = fail "Not an object"
instance ToJSON Entry where
toJSON e = object [ "summary" .= entrySummary e
, "created" .= entryCreated e
, "last-edited" .= entryLastEdited e
]
addEntry :: Journal -> Entry -> Journal
addEntry j e = j { journalEntries = newEntries }
where newEntries = journalEntries j `V.snoc` e
mkEntry :: Text -> IO Entry
mkEntry entryText = Entry entryText <$> getCurrentTime <*> getCurrentTime
-- |Makes a journal, given a slug
mkJournal :: Slug -> IO Journal
mkJournal s = getCurrentTime >>= \t -> return $ Journal s mempty t t mempty mempty
-- |Makes a journal, given a slug
mkJournal' :: Text -> IO Journal
mkJournal' s = mkSlugIO s >>= mkJournal
-- |Figures out the file path for a journal
generateJournalPath :: Journal -> IO FilePath
generateJournalPath j = do
dataDir <- getAppUserDataDirectory programName
return $ mconcat [dataDir, "/", (T.unpack . unSlug . journalSlug) j, journalExt]
generateSlugPath :: Slug -> IO FilePath
generateSlugPath slg = do
ddir <- getAppUserDataDirectory programName
let fullPath = mconcat [ddir, "/", T.unpack $ unSlug slg, journalExt]
return fullPath
-- |Writes a journal to a file path
writeJournal :: Journal -> IO ()
writeJournal j = do pth <- generateJournalPath j; B.writeFile pth $ encodePretty j
-- |Reads a journal from the default file path (~/.puffytools/journal-title.json)
readJournalName :: Text -> IO Journal
readJournalName name = do
slg <- case mkSlugEither name of
Left err -> fail err
Right s -> return s
generateSlugPath slg >>= readJournalFromFile
-- |Reads a journal from the default file path (~/.puffytools/journal-title.json)
readJournalDef :: Slug -> IO Journal
readJournalDef slg = generateSlugPath slg >>= readJournalFromFile
-- |Reads a journal, given a file path
readJournalFromFile :: FilePath -> IO Journal
readJournalFromFile fp = openFile fp ReadMode >>= readJournalFromHandle
-- |Reads a journal from a handle, close handle
readJournalFromHandle :: Handle -> IO Journal
readJournalFromHandle h = do
handleBytes <- Bs.hGetContents h
case eitherDecodeStrict' handleBytes of
Left err -> fail err
Right j -> return j
{-# DEPRECATED listJournals "use listJournalFiles instead"#-}
listJournals :: IO [FilePath]
listJournals = listJournalFiles
-- |List all of the journal file paths
listJournalFiles :: IO [FilePath]
listJournalFiles = do
ddr <- ddir
fps <- filter (endswith ".journal") <$> allDataFiles
return $ map (\fp -> mconcat [ddr, "/", fp]) fps
where
ddir = getAppUserDataDirectory programName
allDataFiles = do
d <- ddir
d `seq` createDirectoryIfMissing True d
getDirectoryContents d
-- |List all of the Journal slugs
listJournalSlugs :: IO [Text]
listJournalSlugs = do
fps <- listJournalFiles
journals <- mapM readJournalFromFile fps
let slugs = map (unSlug . journalSlug) journals
pure slugs
-- |Perform some action if a given journal exists
ifJournal :: Text -> IO () -> IO ()
ifJournal slg doStuff = do
jss <- listJournalSlugs
if slg `elem` jss
then doStuff
else return ()
-- |Perform some action if a given journal does not exist
unlessJournal :: Text -> IO () -> IO ()
unlessJournal slg doStuff = do
jss <- listJournalSlugs
if not (slg `elem` jss)
then doStuff
else return ()