taskwarrior-0.3.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 Control.Monad (when)
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS hiding (
putStrLn,
)
import qualified Data.ByteString.Lazy.Char8 as LBS
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Time (getCurrentTime)
import Data.UUID (UUID)
import qualified Data.UUID as UUID
import System.Exit (ExitCode (..))
import System.IO (hClose)
import System.Process (
CreateProcess (..),
StdStream (..),
proc,
waitForProcess,
withCreateProcess,
)
import System.Random (
getStdRandom,
random,
)
import Taskwarrior.Task (
Task,
makeTask,
)
-- | 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."