Potatoes.hs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. -- Tn - a simple journal program
  2. -- Copyright (C) 2015 Peter Harpending
  3. --
  4. -- === License disclaimer
  5. --
  6. -- This program is free software: you can redistribute it and/or modify
  7. -- it under the terms of the GNU General Public License as published by
  8. -- the Free Software Foundation, either version 3 of the License, or (at
  9. -- your option) any later version.
  10. --
  11. -- This program is distributed in the hope that it will be useful, but
  12. -- WITHOUT ANY WARRANTY; without even the implied warranty of
  13. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. -- General Public License for more details.
  15. --
  16. -- You should have received a copy of the GNU General Public License
  17. -- along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. -- |
  19. -- Module : Tn.Potatoes
  20. -- Description : The slightly more interesting stuff in tn
  21. -- Copyright : Copyright (C) 2015 Peter Harpending
  22. -- License : GPL-3
  23. -- Maintainer : Peter Harpending <peter@harpending.org>
  24. -- Stability : experimental
  25. -- Portability : UNIX/GHC
  26. --
  27. -- This is the slightly more interesting stuff in @tn@. The name is a
  28. -- pun on the idiom \"meat & potatoes\"; this is the potatoes,
  29. -- "Tn.Meat" is the meat. The meat is obviously better than the
  30. -- potatoes.
  31. module Tn.Potatoes where
  32. import Control.Applicative
  33. import Control.Monad
  34. import qualified Data.ByteString as B
  35. import Data.Char
  36. import Data.List
  37. import Data.Monoid
  38. import Data.Text (Text)
  39. import qualified Data.Text as T
  40. import Data.Time
  41. import Data.Yaml
  42. import Prelude hiding (getContents)
  43. import System.Directory
  44. import System.IO hiding (getContents)
  45. import System.IO.Error
  46. import Tn.Static
  47. -- |==== Technically not variables
  48. --
  49. -- Okay, these aren't really variables, but they sort of serve the
  50. -- same purpose
  51. --
  52. -- |This is the text that will show up in the editor
  53. dailyTemplate :: Day -> IO String
  54. dailyTemplate dy = do
  55. return $ mconcat
  56. [ "\n"
  57. , "\n;; Date: "
  58. , show dy
  59. , "\n;;"
  60. , "\n;; By the way, that date is in UTC time, so it might be different than the date"
  61. , "\n;; in your local time."
  62. , "\n;; "
  63. , "\n;; Lines starting with ';;' will be ignored."
  64. , "\n"
  65. ]
  66. -- |Given some file name, get the canonical path
  67. getHypotheticalDataFileName :: String -> IO FilePath
  68. getHypotheticalDataFileName s = do
  69. dir <- tnDir
  70. return $ dir <> s
  71. -- |Create the relevant directories
  72. initialize :: IO ()
  73. initialize = do
  74. createDirectoryIfMissing False =<< tnDir
  75. jfp <- journalFilePath
  76. jfpExists <- doesFileExist jfp
  77. cfp <- configFilePath
  78. cfpExists <- doesFileExist cfp
  79. let writeBlank q = openFile q WriteMode >>= \h -> B.hPut h "{}" >> hClose h
  80. if not jfpExists
  81. then do
  82. putStrLn $ "Creating empty journal file: " <> jfp
  83. writeBlank jfp
  84. else return ()
  85. if not cfpExists
  86. then do
  87. putStrLn $ "Creating empty configuration file: " <> cfp
  88. writeBlank cfp
  89. else return ()
  90. putStrLn "Initialized!"
  91. -- |Read the config file and the journal
  92. getTheTn :: IO Tn
  93. getTheTn = do
  94. -- Read the journal
  95. journalStr <- getContents =<< journalFilePath
  96. jnl <- case decodeEither journalStr of
  97. Left err -> fail err
  98. Right j -> return j
  99. -- Read the configuration
  100. configStr <- getContents =<< configFilePath :: IO B.ByteString
  101. let ioEitherConfig = decodeEither configStr :: Either String (IO TnConfig)
  102. cfg <- case ioEitherConfig of
  103. Left err -> fail err
  104. Right c -> c
  105. return $ Tn jnl cfg
  106. getContents :: FilePath -> IO B.ByteString
  107. getContents jfp = do
  108. let handleError err
  109. | isDoesNotExistError err = ioError . annotateIOError
  110. err
  111. (mconcat
  112. [ "File does not exist: "
  113. , jfp
  114. , "\nYou may need to run `tn initialize` first."
  115. ])
  116. Nothing $ Just jfp
  117. | otherwise = ioError . annotateIOError
  118. err
  119. (mconcat
  120. [ "\n"
  121. , "While tn was trying to read:"
  122. , "\n "
  123. , jfp
  124. , "\n"
  125. , "It came across this error. Tn doesn't know what to do with it."
  126. , "\n"
  127. , "It may be a bug. If so, please report it at"
  128. , "\n "
  129. , "https://notabug.org/pharpend/tn/issues/new"
  130. , "\n"
  131. , "or email the developer at"
  132. , "\n "
  133. , "peter@harpending.org"
  134. , "\n"
  135. , "Thank you!"
  136. , "\n"
  137. ])
  138. Nothing $ Just jfp
  139. hdl <- flip catchIOError handleError $ openFile jfp ReadMode
  140. hSetBinaryMode hdl True
  141. B.hGetContents hdl
  142. -- |Today
  143. today :: IO Day
  144. today = utctDay <$> getCurrentTime
  145. -- |Subtract some number of days from 'today'. So, yesterday would be
  146. -- @todayMinus 1@.
  147. todayMinus :: Integer -> IO Day
  148. todayMinus i = addDays (-1 * i) <$> today
  149. -- |Get rid of the comments
  150. filterComments :: Text -> Text
  151. filterComments = T.unlines . filter notAComment . T.lines
  152. where
  153. notAComment :: Text -> Bool
  154. notAComment s = T.take 2 (noLeadingWhitespace s) /= ";;"
  155. noLeadingWhitespace :: Text -> Text
  156. noLeadingWhitespace = T.dropWhile isSpace
  157. -- |Delete trailing whitespace, but make sure it ends with a newline
  158. deleteTrailingWhitespace :: Text -> Text
  159. deleteTrailingWhitespace = T.unlines . composeChain . T.lines
  160. where
  161. composeChain :: [Text] -> [Text]
  162. composeChain = getRidOfTrailingBlanks . map deleteAllTrails
  163. deleteAllTrails :: Text -> Text
  164. deleteAllTrails = T.dropWhileEnd isSpace
  165. getRidOfTrailingBlanks :: [Text] -> [Text]
  166. getRidOfTrailingBlanks = dropWhileEnd T.null