-- ockcal - the simplest possible calendar that can work
-- Copyright (C) 2015 Lukas Epple aka sternenseemann
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see .
import Data.Time (getCurrentTime, LocalTime(..), utcToLocalTime, getCurrentTimeZone)
import Data.List (sort, insert, findIndex)
import Data.Maybe (fromJust)
import System.Command (runCommand, waitForProcess, isSuccess)
import System.Environment (getArgs, getProgName, getEnv)
import System.Directory (removeFile, doesFileExist)
import Control.Monad (when, unless)
-- config
-- location of the files in $HOME
calendarFile :: IO String
calendarFile = getEnv "HOME" >>= \x -> return (x ++ ".ockcal")
temporaryFile :: IO String
temporaryFile = getEnv "HOME" >>= \x -> return (x ++ ".ockcal.tmp")
-- the file format of the calendarFile is as follows:
-- YYYY-MM-DD HH:MM:SS |
--
-- it currently gets parsed using read
-- I will evaluate if there's any problem doing this
-- internal stuff
data CalendarEvent = CalendarEvent LocalTime String
deriving (Show, Read, Eq, Ord)
-- construct a CalendarEvent from a line in the calendarFile
parseCalendarEvent :: String -> CalendarEvent
parseCalendarEvent line = CalendarEvent (LocalTime (read $ head dateAndTime) (read $ (head . tail) dateAndTime)) eventText
where dateAndTime = words $ takeWhile (/= '|') line
eventText = dropWhile (== ' ') $ tail $ dropWhile (/= '|') line
-- opposite of parseCalendarEvent
showCalendarEvent :: CalendarEvent -> String
showCalendarEvent (CalendarEvent (LocalTime day time) text) = show day ++ " " ++ show time ++ " | " ++ text
-- creates a string showing a CalendarEvent in a human readable form
prettyCalendarEvent :: CalendarEvent -> String
prettyCalendarEvent (CalendarEvent (LocalTime day time) string) = "On " ++ show day ++ " at " ++ show time ++ ": " ++ string
prettyCalendarEventListing :: [CalendarEvent] -> Int -> String
prettyCalendarEventListing events enumstart = unlines $ map (\x -> fst x ++ ". " ++ snd x) $
zip (map show [enumstart .. enumstart + length events]) $ map prettyCalendarEvent events
-- helper IO functions
readCalendarEventsFromFile :: FilePath -> IO [CalendarEvent]
readCalendarEventsFromFile file = do
calendar <- readFile file
return $ sort $ map parseCalendarEvent $ lines calendar
copyFile :: FilePath -> FilePath -> IO ()
copyFile from to = writeFile to =<< readFile from
-- IO actions for the subcommands
listCalendarEntries :: (LocalTime -> LocalTime -> Bool) -> IO ()
listCalendarEntries timeRelation = do
localTimeZone <- getCurrentTimeZone
now <- getCurrentTime
calendarFile <- calendarFile
calendar <- readCalendarEventsFromFile calendarFile
-- use the coreutil cal to print a nice calendar
prettyCalendar <- runCommand "cal"
exitCode <- waitForProcess prettyCalendar
unless (isSuccess exitCode) (error "Unable to call the coreutil 'cal'")
-- generate a simple listing of the upcoming events noted in the calendar
-- Note: we asume the input list is sorted. this is done by readCalendarEventsFromFile
let filterFun (CalendarEvent time _) = time `timeRelation` utcToLocalTime localTimeZone now
matchingEvents = filter filterFun calendar
enumStart = if null matchingEvents then 0 else (+) 1 $ fromJust $ findIndex filterFun calendar
putStr $ prettyCalendarEventListing matchingEvents enumStart
addCalendarEntry :: [String] -> IO ()
addCalendarEntry [day, time, text] = do
-- copy the file to a temporary location
-- see README#FAQ
calendarFile <- calendarFile
temporaryFile <- temporaryFile
copyFile calendarFile temporaryFile
calendar <- readCalendarEventsFromFile temporaryFile
writeFile calendarFile $ unlines $ map showCalendarEvent $ insert (CalendarEvent (LocalTime (read day) (read time)) text) calendar
addCalendarEntry _ = error "Incorrect number of arguments"
deleteCalendarEntry :: [String] -> IO ()
deleteCalendarEntry [num] = do
calendarFile <- calendarFile
temporaryFile <- temporaryFile
copyFile calendarFile temporaryFile
calendar <- readCalendarEventsFromFile temporaryFile
writeFile calendarFile $ unlines $ map showCalendarEvent $ take (read num - 1) calendar ++ drop (read num) calendar
deleteCalendarEntry _ = error "Incorrect number of arguments"
printUsage :: IO ()
printUsage = do
prog <- getProgName
putStrLn $ prog ++ " [command]\n\n\tlist - list all upcoming calendar items\n\tpast - list all past calendar events\n\tadd YYYY-MM-DD HH:MM:SS title - add a event to the calendar"
main :: IO ()
main = do
args <- getArgs
calendarFile <- calendarFile
temporaryFile <- temporaryFile
-- create calendarFile if it doesn't exist
calendarFileExists <- doesFileExist calendarFile
unless calendarFileExists $ writeFile calendarFile ""
case args of
[] -> listCalendarEntries (>=)
("list":_) -> listCalendarEntries (>=)
("past":_) -> listCalendarEntries (<=)
("add":info) -> addCalendarEntry info
("del":info) -> deleteCalendarEntry info
_ -> printUsage
-- delete the temporary file if it exists
temporaryFileExists <- doesFileExist temporaryFile
when temporaryFileExists $ removeFile temporaryFile