taskwarrior-0.2.1.0: src/Taskwarrior/IO.hs
-- | This modules contains IO actions to interact with the taskwarrior application.
-- The taskwarrior documentation very explicitly disallows accessing the files by itself.
-- So all functions here work via calling the @task@ binary which needs to be in the PATH.
module Taskwarrior.IO
( getTasks
, saveTasks
, createTask
, getUUIDs
, onAdd
, onAddPure
, onModify
, onModifyPure
)
where
import Taskwarrior.Task ( Task
, makeTask
)
import Data.Text ( Text )
import qualified Data.Text as Text
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
hiding ( putStrLn )
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Aeson as Aeson
import System.Process ( withCreateProcess
, CreateProcess(..)
, proc
, StdStream(..)
, waitForProcess
)
import System.IO ( hClose )
import System.Exit ( ExitCode(..) )
import Control.Monad ( when )
import System.Random ( getStdRandom
, random
)
import Data.Time ( getCurrentTime )
import Data.UUID ( UUID )
import qualified Data.UUID as UUID
-- | Uses @task export@ with a given filter like @["description:Milk", "+PENDING"]@.
getTasks :: [Text] -> IO [Task]
getTasks args =
withCreateProcess
((proc "task" (fmap Text.unpack . (++ ["export"]) $ args))
{ std_out = CreatePipe
}
)
$ \_ stdoutMay _ _ -> do
stdout <- maybe
(fail "Couldn‘t create stdout handle for `task export`")
pure
stdoutMay
input <- LBS.hGetContents stdout
either fail return . Aeson.eitherDecode $ input
-- | Gives all uuids matching the given filter (e.g. @["description:Milk", "+PENDING"]@). This calls the @task@ binary.
getUUIDs :: [Text] -> IO [UUID]
getUUIDs args =
withCreateProcess
((proc "task" (fmap Text.unpack . (++ ["_uuid"]) $ args)) { std_out = CreatePipe
}
)
$ \_ stdoutMay _ _ -> do
stdout <- maybe
(fail "Couldn‘t create stdout handle for `task _uuid`")
pure
stdoutMay
input <- LBS.hGetContents stdout
maybe (fail "Couldn't parse UUIDs") return
. traverse UUID.fromLazyASCIIBytes
. LBS.lines
$ input
-- | Uses @task import@ to save the given tasks.
saveTasks :: [Task] -> IO ()
saveTasks tasks =
withCreateProcess ((proc "task" ["import"]) { std_in = CreatePipe })
$ \stdinMay _ _ process -> do
stdin <- maybe (fail "Couldn‘t create stdin handle for `task import`")
pure
stdinMay
LBS.hPut stdin . Aeson.encode $ tasks
hClose stdin
exitCode <- waitForProcess process
when (exitCode /= ExitSuccess) $ fail . show $ exitCode
-- | This will create a @'Task'@. I runs in @'IO'@ to create a @'UUID'@ and get the current time. This will not save the @'Task'@ to taskwarrior.
-- If you want to create a task with certain fields and save it you could do that like this:
--
-- @
-- newTask <- 'createTask' "Buy Milk"
-- 'saveTasks' [newTask { 'Taskwarrior.Task.tags' = ["groceries"] }]
-- @
createTask :: Text -> IO Task
createTask description = do
uuid <- getStdRandom random
entry <- getCurrentTime
pure $ makeTask uuid entry description
-- | Takes a function @f originalTask modifiedTask = taskToSave@.
-- The resulting IO action can be run as the `main :: IO ()` of a taskwarrior on-modify hook.
onModifyPure :: (Task -> Task -> Task) -> IO ()
onModifyPure f = onModify (\x y -> pure (f x y))
onModifyError :: String
onModifyError = "OnModify hook couldn‘t parse task."
-- | Like onModifyPure but with side effects.
onModify :: (Task -> Task -> IO Task) -> IO ()
onModify f = do
original <- readTaskLine onModifyError
modified <- readTaskLine onModifyError
LBS.putStrLn . Aeson.encode =<< f original modified
readTaskLine :: String -> IO Task
readTaskLine errorMsg =
maybe (fail errorMsg) pure =<< Aeson.decode' . LBS.fromStrict <$> BS.getLine
-- | Like onModifyPure but for the onAdd hook.
onAddPure :: (Task -> Task) -> IO ()
onAddPure f = onAdd (pure . f)
-- | Like onAddPure with side effects.
onAdd :: (Task -> IO Task) -> IO ()
onAdd f = LBS.putStrLn . Aeson.encode =<< f =<< readTaskLine
"OnAdd hook couldn‘t parse task."