taskell 1.9.3.0 → 1.10.0
raw patch · 23 files changed
+811/−512 lines, 23 filesdep ~aesondep ~attoparsecdep ~brick
Dependency ranges changed: aeson, attoparsec, brick, directory, file-embed, http-client, vty
Files
- README.md +4/−0
- src/Taskell/Config.hs +11/−2
- src/Taskell/Data/List.hs +127/−27
- src/Taskell/Data/List/Internal.hs +0/−128
- src/Taskell/Data/Lists.hs +88/−19
- src/Taskell/Data/Lists/Internal.hs +0/−89
- src/Taskell/Data/Subtask.hs +28/−11
- src/Taskell/Data/Subtask/Internal.hs +0/−29
- src/Taskell/Data/Task.hs +96/−26
- src/Taskell/Data/Task/Internal.hs +0/−97
- src/Taskell/IO.hs +8/−16
- src/Taskell/IO/Config.hs +4/−0
- src/Taskell/IO/Markdown/Parser.hs +1/−1
- taskell.cabal +54/−47
- templates/template.md +8/−0
- test/Taskell/Data/ListNavigationTest.hs +2/−2
- test/Taskell/Data/ListTest.hs +2/−2
- test/Taskell/Data/ListsTest.hs +6/−11
- test/Taskell/Data/SubtaskTest.hs +1/−1
- test/Taskell/Data/TaskTest.hs +3/−3
- test/Taskell/IO/KeyboardTest.hs +1/−1
- test/Taskell/IO/Markdown/ParserTest.hs +6/−0
- test/Taskell/IO/data/roadmap.md +361/−0
README.md view
@@ -257,6 +257,10 @@ **Warning**: currently if you change your `[markdown]` settings any older files stored with different settings will not be readable. +### Template++You can edit the `template.md` config file to change what a new taskell file looks like.+ ### Controls You can edit keyboard bindings in the `bindings.ini` config file.
src/Taskell/Config.hs view
@@ -4,10 +4,19 @@ import ClassyPrelude -import Data.FileEmbed (embedFile)+import Data.FileEmbed (embedFile)+import Data.Version (showVersion)+import Language.Haskell.TH.Syntax (liftString)+import qualified Paths_taskell (version) version :: Text-version = "1.9.3"+version = $(liftString $ showVersion Paths_taskell.version)++trelloUsage :: Text+trelloUsage = decodeUtf8 $(embedFile "templates/trello-token.txt")++githubUsage :: Text+githubUsage = decodeUtf8 $(embedFile "templates/github-token.txt") usage :: Text usage = decodeUtf8 $(embedFile "templates/usage.txt")
src/Taskell/Data/List.hs view
@@ -1,28 +1,128 @@-module Taskell.Data.List- ( List- , Update- , title- , tasks- , create- , empty- , due- , clearDue- , new- , count- , newAt- , duplicate- , append- , prepend- , extract- , updateFn- , update- , move- , deleteTask- , getTask- , searchFor- , nextTask- , prevTask- , nearest- ) where+{-# LANGUAGE TemplateHaskell #-} -import Taskell.Data.List.Internal+module Taskell.Data.List where++import ClassyPrelude++import Control.Lens (element, makeLenses, (%%~), (%~), (&), (.~), (^.), (^?))++import Data.Sequence as S (adjust', deleteAt, insertAt, update, (<|), (|>))++import qualified Taskell.Data.Seq as S+import qualified Taskell.Data.Task as T (Task, Update, blank, clearDue, contains, due, duplicate)+import Taskell.Types (TaskIndex (TaskIndex))++data List = List+ { _title :: Text+ , _tasks :: Seq T.Task+ } deriving (Show, Eq)++type Update = List -> List++-- create lenses+$(makeLenses ''List)++-- operations+create :: Text -> Seq T.Task -> List+create = List++empty :: Text -> List+empty text = List text ClassyPrelude.empty++new :: Update+new = append T.blank++count :: List -> Int+count = length . (^. tasks)++due :: List -> Seq (TaskIndex, T.Task)+due list = catMaybes (filt S.<#> (list ^. tasks))+ where+ filt int task = const (TaskIndex int, task) <$> task ^. T.due++clearDue :: TaskIndex -> Update+clearDue (TaskIndex int) = updateFn int T.clearDue++newAt :: Int -> Update+newAt idx = tasks %~ S.insertAt idx T.blank++duplicate :: Int -> List -> Maybe List+duplicate idx list = do+ task <- T.duplicate <$> getTask idx list+ pure $ list & tasks %~ S.insertAt idx task++append :: T.Task -> Update+append task = tasks %~ (S.|> task)++prepend :: T.Task -> Update+prepend task = tasks %~ (task S.<|)++extract :: Int -> List -> Maybe (List, T.Task)+extract idx list = do+ (xs, x) <- S.extract idx (list ^. tasks)+ pure (list & tasks .~ xs, x)++updateFn :: Int -> T.Update -> Update+updateFn idx fn = tasks %~ adjust' fn idx++update :: Int -> T.Task -> Update+update idx task = tasks %~ S.update idx task++move :: Int -> Int -> Maybe Text -> List -> Maybe (List, Int)+move current dir term list =+ case term of+ Nothing -> (, bound list (current + dir)) <$> (list & tasks %%~ S.shiftBy current dir)+ Just _ -> do+ idx <- changeTask dir current term list+ (, idx) <$> (list & tasks %%~ S.shiftBy current (idx - current))++deleteTask :: Int -> Update+deleteTask idx = tasks %~ deleteAt idx++getTask :: Int -> List -> Maybe T.Task+getTask idx = (^? tasks . element idx)++searchFor :: Text -> Update+searchFor text = tasks %~ filter (T.contains text)++changeTask :: Int -> Int -> Maybe Text -> List -> Maybe Int+changeTask dir current term list = do+ let next = current + dir+ tsk <- getTask next list+ case term of+ Nothing -> Just next+ Just trm ->+ if T.contains trm tsk+ then Just next+ else changeTask dir next term list++nextTask :: Int -> Maybe Text -> List -> Int+nextTask idx text lst = fromMaybe idx $ changeTask 1 idx text lst++prevTask :: Int -> Maybe Text -> List -> Int+prevTask idx text lst = fromMaybe idx $ changeTask (-1) idx text lst++closest :: Int -> Int -> Int -> Int+closest current previous next =+ if (next - current) < (current - previous)+ then next+ else previous++bound :: List -> Int -> Int+bound lst = S.bound (lst ^. tasks)++nearest' :: Int -> Maybe Text -> List -> Maybe Int+nearest' current term lst = do+ let prev = changeTask (-1) current term lst+ let nxt = changeTask 1 current term lst+ let comp idx = Just $ maybe idx (closest current idx) nxt+ maybe nxt comp prev++nearest :: Int -> Maybe Text -> List -> Int+nearest current term lst = idx+ where+ near = fromMaybe (-1) $ nearest' current term lst+ idx =+ case term of+ Nothing -> bound lst current+ Just txt -> maybe near (bool near current . T.contains txt) $ getTask current lst
− src/Taskell/Data/List/Internal.hs
@@ -1,128 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--module Taskell.Data.List.Internal where--import ClassyPrelude--import Control.Lens (element, makeLenses, (%%~), (%~), (&), (.~), (^.), (^?))--import Data.Sequence as S (adjust', deleteAt, insertAt, update, (<|), (|>))--import qualified Taskell.Data.Seq as S-import qualified Taskell.Data.Task as T (Task, Update, blank, clearDue, contains, due, duplicate)-import Taskell.Types (TaskIndex (TaskIndex))--data List = List- { _title :: Text- , _tasks :: Seq T.Task- } deriving (Show, Eq)--type Update = List -> List---- create lenses-$(makeLenses ''List)---- operations-create :: Text -> Seq T.Task -> List-create = List--empty :: Text -> List-empty text = List text ClassyPrelude.empty--new :: Update-new = append T.blank--count :: List -> Int-count = length . (^. tasks)--due :: List -> Seq (TaskIndex, T.Task)-due list = catMaybes (filt S.<#> (list ^. tasks))- where- filt int task = const (TaskIndex int, task) <$> task ^. T.due--clearDue :: TaskIndex -> Update-clearDue (TaskIndex int) = updateFn int T.clearDue--newAt :: Int -> Update-newAt idx = tasks %~ S.insertAt idx T.blank--duplicate :: Int -> List -> Maybe List-duplicate idx list = do- task <- T.duplicate <$> getTask idx list- pure $ list & tasks %~ S.insertAt idx task--append :: T.Task -> Update-append task = tasks %~ (S.|> task)--prepend :: T.Task -> Update-prepend task = tasks %~ (task S.<|)--extract :: Int -> List -> Maybe (List, T.Task)-extract idx list = do- (xs, x) <- S.extract idx (list ^. tasks)- pure (list & tasks .~ xs, x)--updateFn :: Int -> T.Update -> Update-updateFn idx fn = tasks %~ adjust' fn idx--update :: Int -> T.Task -> Update-update idx task = tasks %~ S.update idx task--move :: Int -> Int -> Maybe Text -> List -> Maybe (List, Int)-move current dir term list =- case term of- Nothing -> (, bound list (current + dir)) <$> (list & tasks %%~ S.shiftBy current dir)- Just _ -> do- idx <- changeTask dir current term list- (, idx) <$> (list & tasks %%~ S.shiftBy current (idx - current))--deleteTask :: Int -> Update-deleteTask idx = tasks %~ deleteAt idx--getTask :: Int -> List -> Maybe T.Task-getTask idx = (^? tasks . element idx)--searchFor :: Text -> Update-searchFor text = tasks %~ filter (T.contains text)--changeTask :: Int -> Int -> Maybe Text -> List -> Maybe Int-changeTask dir current term list = do- let next = current + dir- tsk <- getTask next list- case term of- Nothing -> Just next- Just trm ->- if T.contains trm tsk- then Just next- else changeTask dir next term list--nextTask :: Int -> Maybe Text -> List -> Int-nextTask idx text lst = fromMaybe idx $ changeTask 1 idx text lst--prevTask :: Int -> Maybe Text -> List -> Int-prevTask idx text lst = fromMaybe idx $ changeTask (-1) idx text lst--closest :: Int -> Int -> Int -> Int-closest current previous next =- if (next - current) < (current - previous)- then next- else previous--bound :: List -> Int -> Int-bound lst = S.bound (lst ^. tasks)--nearest' :: Int -> Maybe Text -> List -> Maybe Int-nearest' current term lst = do- let prev = changeTask (-1) current term lst- let nxt = changeTask 1 current term lst- let comp idx = Just $ maybe idx (closest current idx) nxt- maybe nxt comp prev--nearest :: Int -> Maybe Text -> List -> Int-nearest current term lst = idx- where- near = fromMaybe (-1) $ nearest' current term lst- idx =- case term of- Nothing -> bound lst current- Just txt -> maybe near (bool near current . T.contains txt) $ getTask current lst
src/Taskell/Data/Lists.hs view
@@ -1,20 +1,89 @@-module Taskell.Data.Lists- ( Lists- , ListPosition(..)- , initial- , updateLists- , count- , due- , clearDue- , get- , changeList- , newList- , delete- , exists- , shiftBy- , search- , appendToLast- , analyse- ) where+module Taskell.Data.Lists where -import Taskell.Data.Lists.Internal+import ClassyPrelude++import Control.Lens ((^.))+import Data.Sequence as S (adjust', deleteAt, update, (!?), (|>))++import qualified Taskell.Data.List as L (List, Update, append, clearDue, count, due, empty, extract,+ prepend, searchFor)+import qualified Taskell.Data.Seq as S+import qualified Taskell.Data.Task as T (Task, due)+import Taskell.Types (ListIndex (ListIndex), Pointer, TaskIndex (TaskIndex))++type Lists = Seq L.List++type Update = Lists -> Lists++data ListPosition+ = Top+ | Bottom++initial :: Lists+initial = fromList []++updateLists :: Int -> L.List -> Update+updateLists = S.update++count :: Int -> Lists -> Int+count idx tasks = maybe 0 L.count (tasks !? idx)++due :: Lists -> Seq (Pointer, T.Task)+due lists = sortOn ((^. T.due) . snd) dues+ where+ format x lst = (\(y, t) -> ((ListIndex x, y), t)) <$> L.due lst+ dues = concat $ format S.<#> lists++clearDue :: Pointer -> Update+clearDue (idx, tsk) = updateFn idx (L.clearDue tsk)++updateFn :: ListIndex -> L.Update -> Update+updateFn (ListIndex idx) fn = adjust' fn idx++get :: Lists -> Int -> Maybe L.List+get = (!?)++changeList :: ListPosition -> Pointer -> Lists -> Int -> Maybe Lists+changeList pos (ListIndex list, TaskIndex idx) tasks dir = do+ let next = list + dir+ let fn =+ case pos of+ Top -> L.prepend+ Bottom -> L.append+ (from, task) <- L.extract idx =<< tasks !? list -- extract current task+ to <- fn task <$> tasks !? next -- get next list and append task+ pure . updateLists next to $ updateLists list from tasks -- update lists++newList :: Text -> Update+newList title = (|> L.empty title)++delete :: Int -> Update+delete = deleteAt++exists :: Int -> Lists -> Bool+exists idx tasks = isJust $ tasks !? idx++shiftBy :: Int -> Int -> Lists -> Maybe Lists+shiftBy = S.shiftBy++search :: Text -> Update+search text = (L.searchFor text <$>)++appendToLast :: T.Task -> Update+appendToLast task lists =+ fromMaybe lists $ do+ let idx = length lists - 1+ list <- L.append task <$> lists !? idx+ pure $ updateLists idx list lists++analyse :: Text -> Lists -> Text+analyse filepath lists =+ concat+ [ filepath+ , "\n"+ , "Lists: "+ , tshow $ length lists+ , "\n"+ , "Tasks: "+ , tshow $ foldl' (+) 0 (L.count <$> lists)+ ]
− src/Taskell/Data/Lists/Internal.hs
@@ -1,89 +0,0 @@-module Taskell.Data.Lists.Internal where--import ClassyPrelude--import Control.Lens ((^.))-import Data.Sequence as S (adjust', deleteAt, update, (!?), (|>))--import qualified Taskell.Data.List as L (List, Update, append, clearDue, count, due, empty, extract,- prepend, searchFor)-import qualified Taskell.Data.Seq as S-import qualified Taskell.Data.Task as T (Task, due)-import Taskell.Types (ListIndex (ListIndex), Pointer, TaskIndex (TaskIndex))--type Lists = Seq L.List--type Update = Lists -> Lists--data ListPosition- = Top- | Bottom--initial :: Lists-initial = fromList [L.empty "To Do", L.empty "Done"]--updateLists :: Int -> L.List -> Update-updateLists = S.update--count :: Int -> Lists -> Int-count idx tasks = maybe 0 L.count (tasks !? idx)--due :: Lists -> Seq (Pointer, T.Task)-due lists = sortOn ((^. T.due) . snd) dues- where- format x lst = (\(y, t) -> ((ListIndex x, y), t)) <$> L.due lst- dues = concat $ format S.<#> lists--clearDue :: Pointer -> Update-clearDue (idx, tsk) = updateFn idx (L.clearDue tsk)--updateFn :: ListIndex -> L.Update -> Update-updateFn (ListIndex idx) fn = adjust' fn idx--get :: Lists -> Int -> Maybe L.List-get = (!?)--changeList :: ListPosition -> Pointer -> Lists -> Int -> Maybe Lists-changeList pos (ListIndex list, TaskIndex idx) tasks dir = do- let next = list + dir- let fn =- case pos of- Top -> L.prepend- Bottom -> L.append- (from, task) <- L.extract idx =<< tasks !? list -- extract current task- to <- fn task <$> tasks !? next -- get next list and append task- pure . updateLists next to $ updateLists list from tasks -- update lists--newList :: Text -> Update-newList title = (|> L.empty title)--delete :: Int -> Update-delete = deleteAt--exists :: Int -> Lists -> Bool-exists idx tasks = isJust $ tasks !? idx--shiftBy :: Int -> Int -> Lists -> Maybe Lists-shiftBy = S.shiftBy--search :: Text -> Update-search text = (L.searchFor text <$>)--appendToLast :: T.Task -> Update-appendToLast task lists =- fromMaybe lists $ do- let idx = length lists - 1- list <- L.append task <$> lists !? idx- pure $ updateLists idx list lists--analyse :: Text -> Lists -> Text-analyse filepath lists =- concat- [ filepath- , "\n"- , "Lists: "- , tshow $ length lists- , "\n"- , "Tasks: "- , tshow $ foldl' (+) 0 (L.count <$> lists)- ]
src/Taskell/Data/Subtask.hs view
@@ -1,12 +1,29 @@-module Taskell.Data.Subtask- ( Subtask- , Update- , new- , blank- , name- , complete- , toggle- , duplicate- ) where+{-# LANGUAGE TemplateHaskell #-} -import Taskell.Data.Subtask.Internal+module Taskell.Data.Subtask where++import ClassyPrelude+import Control.Lens (makeLenses, (%~))++data Subtask = Subtask+ { _name :: Text+ , _complete :: Bool+ } deriving (Show, Eq)++type Update = Subtask -> Subtask++-- create lenses+$(makeLenses ''Subtask)++-- operations+blank :: Subtask+blank = Subtask "" False++new :: Text -> Bool -> Subtask+new = Subtask++toggle :: Update+toggle = complete %~ not++duplicate :: Subtask -> Subtask+duplicate (Subtask n c) = Subtask n c
− src/Taskell/Data/Subtask/Internal.hs
@@ -1,29 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--module Taskell.Data.Subtask.Internal where--import ClassyPrelude-import Control.Lens (makeLenses, (%~))--data Subtask = Subtask- { _name :: Text- , _complete :: Bool- } deriving (Show, Eq)--type Update = Subtask -> Subtask---- create lenses-$(makeLenses ''Subtask)---- operations-blank :: Subtask-blank = Subtask "" False--new :: Text -> Bool -> Subtask-new = Subtask--toggle :: Update-toggle = complete %~ not--duplicate :: Subtask -> Subtask-duplicate (Subtask n c) = Subtask n c
src/Taskell/Data/Task.hs view
@@ -1,27 +1,97 @@-module Taskell.Data.Task- ( Task- , Update- , name- , description- , due- , subtasks- , blank- , new- , create- , duplicate- , setDescription- , appendDescription- , setDue- , clearDue- , getSubtask- , addSubtask- , hasSubtasks- , updateSubtask- , removeSubtask- , countSubtasks- , countCompleteSubtasks- , contains- , isBlank- ) where+{-# LANGUAGE TemplateHaskell #-} -import Taskell.Data.Task.Internal+module Taskell.Data.Task where++import ClassyPrelude++import Control.Lens (ix, makeLenses, (%~), (&), (.~), (?~), (^.), (^?))++import Data.Sequence as S (adjust', deleteAt, (|>))+import Data.Text (strip)+import Data.Time.Zones (TZ)+import Taskell.Data.Date (Due (..), inputToTime)+import qualified Taskell.Data.Subtask as ST (Subtask, Update, complete, duplicate, name)++data Task = Task+ { _name :: Text+ , _description :: Maybe Text+ , _subtasks :: Seq ST.Subtask+ , _due :: Maybe Due+ } deriving (Show, Eq)++type Update = Task -> Task++-- create lenses+$(makeLenses ''Task)++-- operations+create :: Text -> Maybe Due -> Maybe Text -> Seq ST.Subtask -> Task+create name' due' description' subtasks' = Task name' description' subtasks' due'++blank :: Task+blank = Task "" Nothing empty Nothing++new :: Text -> Task+new text = blank & (name .~ text)++setDescription :: Text -> Update+setDescription text =+ description .~+ if null (strip text)+ then Nothing+ else Just text++maybeAppend :: Text -> Maybe Text -> Maybe Text+maybeAppend text (Just current) = Just (concat [current, "\n", text])+maybeAppend text Nothing = Just text++appendDescription :: Text -> Update+appendDescription text =+ if null (strip text)+ then id+ else description %~ maybeAppend text++setDue :: TZ -> UTCTime -> Text -> Task -> Maybe Task+setDue tz now date task =+ if null date+ then Just (task & due .~ Nothing)+ else (\d -> task & due ?~ d) <$> inputToTime tz now (strip date)++clearDue :: Update+clearDue task = task & due .~ Nothing++getSubtask :: Int -> Task -> Maybe ST.Subtask+getSubtask idx = (^? subtasks . ix idx)++addSubtask :: ST.Subtask -> Update+addSubtask subtask = subtasks %~ (|> subtask)++hasSubtasks :: Task -> Bool+hasSubtasks = not . null . (^. subtasks)++updateSubtask :: Int -> ST.Update -> Update+updateSubtask idx fn = subtasks %~ adjust' fn idx++removeSubtask :: Int -> Update+removeSubtask idx = subtasks %~ S.deleteAt idx++countSubtasks :: Task -> Int+countSubtasks = length . (^. subtasks)++countCompleteSubtasks :: Task -> Int+countCompleteSubtasks = length . filter (^. ST.complete) . (^. subtasks)++contains :: Text -> Task -> Bool+contains text task =+ check (task ^. name) || maybe False check (task ^. description) || not (null sts)+ where+ check = isInfixOf (toLower text) . toLower+ sts = filter check $ (^. ST.name) <$> (task ^. subtasks)++isBlank :: Task -> Bool+isBlank task =+ null (task ^. name) &&+ isNothing (task ^. description) && null (task ^. subtasks) && isNothing (task ^. due)++duplicate :: Task -> Task+duplicate (Task n d st du) = Task n d (ST.duplicate <$> st) du
− src/Taskell/Data/Task/Internal.hs
@@ -1,97 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--module Taskell.Data.Task.Internal where--import ClassyPrelude--import Control.Lens (ix, makeLenses, (%~), (&), (.~), (?~), (^.), (^?))--import Data.Sequence as S (adjust', deleteAt, (|>))-import Data.Text (strip)-import Data.Time.Zones (TZ)-import Taskell.Data.Date (Due (..), inputToTime)-import qualified Taskell.Data.Subtask as ST (Subtask, Update, complete, duplicate, name)--data Task = Task- { _name :: Text- , _description :: Maybe Text- , _subtasks :: Seq ST.Subtask- , _due :: Maybe Due- } deriving (Show, Eq)--type Update = Task -> Task---- create lenses-$(makeLenses ''Task)---- operations-create :: Text -> Maybe Due -> Maybe Text -> Seq ST.Subtask -> Task-create name' due' description' subtasks' = Task name' description' subtasks' due'--blank :: Task-blank = Task "" Nothing empty Nothing--new :: Text -> Task-new text = blank & (name .~ text)--setDescription :: Text -> Update-setDescription text =- description .~- if null (strip text)- then Nothing- else Just text--maybeAppend :: Text -> Maybe Text -> Maybe Text-maybeAppend text (Just current) = Just (concat [current, "\n", text])-maybeAppend text Nothing = Just text--appendDescription :: Text -> Update-appendDescription text =- if null (strip text)- then id- else description %~ maybeAppend text--setDue :: TZ -> UTCTime -> Text -> Task -> Maybe Task-setDue tz now date task =- if null date- then Just (task & due .~ Nothing)- else (\d -> task & due ?~ d) <$> inputToTime tz now (strip date)--clearDue :: Update-clearDue task = task & due .~ Nothing--getSubtask :: Int -> Task -> Maybe ST.Subtask-getSubtask idx = (^? subtasks . ix idx)--addSubtask :: ST.Subtask -> Update-addSubtask subtask = subtasks %~ (|> subtask)--hasSubtasks :: Task -> Bool-hasSubtasks = not . null . (^. subtasks)--updateSubtask :: Int -> ST.Update -> Update-updateSubtask idx fn = subtasks %~ adjust' fn idx--removeSubtask :: Int -> Update-removeSubtask idx = subtasks %~ S.deleteAt idx--countSubtasks :: Task -> Int-countSubtasks = length . (^. subtasks)--countCompleteSubtasks :: Task -> Int-countCompleteSubtasks = length . filter (^. ST.complete) . (^. subtasks)--contains :: Text -> Task -> Bool-contains text task =- check (task ^. name) || maybe False check (task ^. description) || not (null sts)- where- check = isInfixOf (toLower text) . toLower- sts = filter check $ (^. ST.name) <$> (task ^. subtasks)--isBlank :: Task -> Bool-isBlank task =- null (task ^. name) &&- isNothing (task ^. description) && null (task ^. subtasks) && isNothing (task ^. due)--duplicate :: Task -> Task-duplicate (Task n d st du) = Task n d (ST.duplicate <$> st) du
src/Taskell/IO.hs view
@@ -1,20 +1,18 @@-{-# LANGUAGE TemplateHaskell #-}- module Taskell.IO where import ClassyPrelude import Control.Monad.Reader (runReader)-import Data.FileEmbed (embedFile) import Data.Text.Encoding (decodeUtf8With) import System.Directory (doesFileExist, getCurrentDirectory) import Data.Time.Zones (TZ) -import Taskell.Config (usage, version)+import Taskell.Config (githubUsage, trelloUsage, usage, version) import Taskell.Data.Lists (Lists, analyse, initial) -import Taskell.IO.Config (Config, general, github, markdown, trello)+import Taskell.IO.Config (Config, general, getDir, github, markdown, templatePath,+ trello) import Taskell.IO.Config.General (filename) import qualified Taskell.IO.Config.GitHub as GitHub (token) import qualified Taskell.IO.Config.Trello as Trello (token)@@ -106,18 +104,10 @@ bool (pure Exit) (Load path ls <$ lift (writeData tz config ls path)) createTrello :: Trello.TrelloBoardID -> FilePath -> ReaderConfig Next-createTrello =- createRemote- (Trello.token . trello)- (decodeUtf8 $(embedFile "templates/trello-token.txt"))- Trello.getLists+createTrello = createRemote (Trello.token . trello) trelloUsage Trello.getLists createGitHub :: GitHub.GitHubIdentifier -> FilePath -> ReaderConfig Next-createGitHub =- createRemote- (GitHub.token . github)- (decodeUtf8 $(embedFile "templates/github-token.txt"))- GitHub.getLists+createGitHub = createRemote (GitHub.token . github) githubUsage GitHub.getLists exists :: Text -> ReaderConfig (Maybe FilePath) exists filepath = do@@ -140,7 +130,9 @@ createPath path = do config <- asks ioConfig tz <- asks ioTZ- lift (writeData tz config initial path)+ template <- readData =<< templatePath <$> lift getDir+ let ls = either (const initial) id template+ lift (writeData tz config ls path) -- writes Tasks to json file writeData :: TZ -> Config -> Lists -> FilePath -> IO ()
src/Taskell/IO/Config.hs view
@@ -68,6 +68,9 @@ configPath :: FilePath -> FilePath configPath = (</> "config.ini") +templatePath :: FilePath -> FilePath+templatePath = (</> "template.md")+ bindingsPath :: FilePath -> FilePath bindingsPath = (</> "bindings.ini") @@ -80,6 +83,7 @@ -- create config files create (configPath dir) $(embedFile "templates/config.ini") create (themePath dir) $(embedFile "templates/theme.ini")+ create (templatePath dir) $(embedFile "templates/template.md") create (bindingsPath dir) $(embedFile "templates/bindings.ini") -- get config getConfig
src/Taskell/IO/Markdown/Parser.hs view
@@ -57,7 +57,7 @@ listP sym = L.create <$> listTitleP sym <*> (fromList <$> many' (taskP sym)) markdownP :: Symbol -> Parser LS.Lists-markdownP sym = fromList <$> many1 (listP sym) <* endOfInput+markdownP sym = fromList <$> many1 (listP sym) <* skipSpace <* endOfInput -- parse parse :: Config -> Text -> Either Text LS.Lists
taskell.cabal view
@@ -1,33 +1,36 @@-cabal-version: 1.12-name: taskell-version: 1.9.3.0-license: BSD3-license-file: LICENSE-copyright: 2019 Mark Wales-maintainer: mark@smallhadroncollider.com-author: Mark Wales-homepage: https://github.com/smallhadroncollider/taskell#readme-bug-reports: https://github.com/smallhadroncollider/taskell/issues-synopsis: A command-line kanban board/task manager+cabal-version: 1.12+name: taskell+version: 1.10.0+license: BSD3+license-file: LICENSE+copyright: 2019 Mark Wales+maintainer: mark@smallhadroncollider.com+author: Mark Wales+homepage: https://github.com/smallhadroncollider/taskell#readme+bug-reports: https://github.com/smallhadroncollider/taskell/issues+synopsis: A command-line kanban board/task manager description: Please see the README on GitHub at <https://github.com/smallhadroncollider/taskell#readme>-category: Command Line Tools-build-type: Simple++category: Command Line Tools+build-type: Simple extra-source-files: README.md templates/api-error.txt templates/bindings.ini templates/config.ini templates/github-token.txt+ templates/template.md templates/theme.ini templates/trello-token.txt templates/usage.txt test/Taskell/IO/data/trello-checklists.json test/Taskell/IO/data/trello.json+ test/Taskell/IO/data/roadmap.md test/Taskell/IO/Keyboard/data/bindings.ini source-repository head- type: git+ type: git location: https://github.com/smallhadroncollider/taskell library@@ -39,14 +42,10 @@ Taskell.Data.Date Taskell.Data.Date.RelativeParser Taskell.Data.List- Taskell.Data.List.Internal Taskell.Data.Lists- Taskell.Data.Lists.Internal Taskell.Data.Seq Taskell.Data.Subtask- Taskell.Data.Subtask.Internal Taskell.Data.Task- Taskell.Data.Task.Internal Taskell.Events.Actions.Types Taskell.Events.State.History Taskell.Events.State.Types@@ -63,7 +62,8 @@ Taskell.IO.Keyboard.Types Taskell.UI.Draw.Field Taskell.Types- hs-source-dirs: src++ hs-source-dirs: src other-modules: Taskell.Config Taskell.Data.Date.Types@@ -109,22 +109,25 @@ Taskell.UI.Types Taskell.Utility.Parser Paths_taskell- default-language: Haskell2010- default-extensions: OverloadedStrings NoImplicitPrelude- TupleSections LambdaCase RankNTypes++ default-language: Haskell2010+ default-extensions:+ OverloadedStrings NoImplicitPrelude TupleSections LambdaCase+ RankNTypes+ build-depends:- aeson >=1.4.6.0 && <1.5,- attoparsec >=0.13.2.3 && <0.14,+ aeson >=1.4.7.1 && <1.5,+ attoparsec >=0.13.2.4 && <0.14, base >=4.13.0.0 && <=5,- brick ==0.52.*,+ brick >=0.52.1 && <0.53, bytestring >=0.10.10.0 && <0.11, classy-prelude >=1.5.0 && <1.6, config-ini >=0.2.4.0 && <0.3, containers >=0.6.2.1 && <0.7,- directory >=1.3.4.0 && <1.4,- file-embed >=0.0.11.1 && <0.1,+ directory >=1.3.6.0 && <1.4,+ file-embed >=0.0.11.2 && <0.1, fold-debounce >=0.2.0.9 && <0.3,- http-client >=0.6.4 && <0.7,+ http-client >=0.6.4.1 && <0.7, http-conduit >=2.3.7.3 && <2.4, http-types >=0.12.3 && <0.13, lens >=4.18.1 && <4.19,@@ -133,17 +136,18 @@ text >=1.2.4.0 && <1.3, time >=1.9.3 && <1.10, tz >=0.1.3.3 && <0.2,- vty ==5.26.*+ vty >=5.28.2 && <5.29 executable taskell- main-is: Main.hs- hs-source-dirs: app- other-modules:- Paths_taskell- default-language: Haskell2010- default-extensions: OverloadedStrings NoImplicitPrelude- TupleSections LambdaCase RankNTypes- ghc-options: -threaded -rtsopts -with-rtsopts=-N+ main-is: Main.hs+ hs-source-dirs: app+ other-modules: Paths_taskell+ default-language: Haskell2010+ default-extensions:+ OverloadedStrings NoImplicitPrelude TupleSections LambdaCase+ RankNTypes++ ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: base >=4.13.0.0 && <4.14, classy-prelude >=1.5.0 && <1.6,@@ -151,9 +155,9 @@ tz >=0.1.3.3 && <0.2 test-suite taskell-test- type: exitcode-stdio-1.0- main-is: Spec.hs- hs-source-dirs: test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test other-modules: Taskell.Data.Date.RelativeParserTest Taskell.Data.DateTest@@ -175,16 +179,19 @@ Taskell.IO.TrelloTest Taskell.UI.FieldTest Paths_taskell- default-language: Haskell2010- default-extensions: OverloadedStrings NoImplicitPrelude- TupleSections LambdaCase RankNTypes- ghc-options: -threaded -rtsopts -with-rtsopts=-N++ default-language: Haskell2010+ default-extensions:+ OverloadedStrings NoImplicitPrelude TupleSections LambdaCase+ RankNTypes++ ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends:- aeson >=1.4.6.0 && <1.5,+ aeson >=1.4.7.1 && <1.5, base >=4.13.0.0 && <4.14, classy-prelude >=1.5.0 && <1.6, containers >=0.6.2.1 && <0.7,- file-embed >=0.0.11.1 && <0.1,+ file-embed >=0.0.11.2 && <0.1, lens >=4.18.1 && <4.19, mtl >=2.2.2 && <2.3, raw-strings-qq ==1.1.*,@@ -196,4 +203,4 @@ text >=1.2.4.0 && <1.3, time >=1.9.3 && <1.10, tz >=0.1.3.3 && <0.2,- vty ==5.26.*+ vty >=5.28.2 && <5.29
+ templates/template.md view
@@ -0,0 +1,8 @@+## To Do+++## Doing+++## Done+
@@ -7,8 +7,8 @@ import Test.Tasty import Test.Tasty.HUnit -import Taskell.Data.List.Internal as L-import qualified Taskell.Data.Task as T (Task, new)+import Taskell.Data.List as L+import qualified Taskell.Data.Task as T (Task, new) taskSeq :: Seq T.Task taskSeq =
test/Taskell/Data/ListTest.hs view
@@ -9,8 +9,8 @@ import Test.Tasty import Test.Tasty.HUnit -import Taskell.Data.List.Internal as L-import qualified Taskell.Data.Task as T (Task, blank, name, new)+import Taskell.Data.List as L+import qualified Taskell.Data.Task as T (Task, blank, name, new) emptyList :: List emptyList = L.empty "Test"
test/Taskell/Data/ListsTest.hs view
@@ -9,11 +9,11 @@ import Control.Lens ((.~)) -import Taskell.Data.Date (textToTime)-import qualified Taskell.Data.List as L-import Taskell.Data.Lists.Internal-import qualified Taskell.Data.Task as T-import Taskell.Types (ListIndex (ListIndex), TaskIndex (TaskIndex))+import Taskell.Data.Date (textToTime)+import qualified Taskell.Data.List as L+import Taskell.Data.Lists+import qualified Taskell.Data.Task as T+import Taskell.Types (ListIndex (ListIndex), TaskIndex (TaskIndex)) -- test data list1, list2, list3 :: L.List@@ -43,12 +43,7 @@ test_lists = testGroup "Data.Taskell.Lists"- [ testCase- "initial"- (assertEqual- "Returns To Do and Done lists"- (fromList [L.empty "To Do", L.empty "Done"])- initial)+ [ testCase "initial" (assertEqual "Returns empty" (fromList []) initial) , testCase "updateLists" (assertEqual
test/Taskell/Data/SubtaskTest.hs view
@@ -7,7 +7,7 @@ import Test.Tasty import Test.Tasty.HUnit -import Taskell.Data.Subtask.Internal+import Taskell.Data.Subtask -- tests test_subtask :: TestTree
test/Taskell/Data/TaskTest.hs view
@@ -11,9 +11,9 @@ import Data.Time.Calendar (fromGregorianValid) -import Taskell.Data.Date (Due (DueDate))-import qualified Taskell.Data.Subtask as ST (name, new)-import Taskell.Data.Task.Internal+import Taskell.Data.Date (Due (DueDate))+import qualified Taskell.Data.Subtask as ST (name, new)+import Taskell.Data.Task desc :: Maybe Text desc = Just "A very boring description"
test/Taskell/IO/KeyboardTest.hs view
@@ -15,7 +15,7 @@ import Data.Time.Zones (utcTZ) import Graphics.Vty.Input.Events (Event (..), Key (..))-import Taskell.Data.Lists.Internal (initial)+import Taskell.Data.Lists (initial) import Taskell.Events.Actions.Types as A import Taskell.Events.State (create, quit) import Taskell.Events.State.Types (State, Stateful, mode)
test/Taskell/IO/Markdown/ParserTest.hs view
@@ -134,6 +134,12 @@ "List item with Sub-Task" (Right (makeSubTask "Blah" True)) (parse defaultConfig "## Test\n\n- Test Item\n * [x] Blah"))+ , testCase+ "Line break at end"+ (assertEqual+ "List item"+ (Right listWithItem)+ (parse defaultConfig "## Test\n\n- Test Item\n\n")) ] , testGroup "Alternative Format"
+ test/Taskell/IO/data/roadmap.md view
@@ -0,0 +1,361 @@+## Misc.++- Add more information to taskell.app+ > Update taskell.app to have more than just README.md contents. Use cases, more images, examples, etc.+ * [ ] Use case example: checklist+ * [ ] Use case example: Git controlled tasks+ * [ ] Blog posts for updates+- Add to Flatpack+ > https://opensource.com/article/19/10/how-build-flatpak-packaging++## Refactoring++- 乤乭 乤亍 乤乭乫 乤乭亍乫 乤乭 乤乭亍+- Refactor Task `Update` to be `Task -> Maybe Task`?+- Use Attoparsec for parsing+- Add tests for Taskell.IO.GitHub+- Break up State module+ * [ ] More of logic should go into Task, List, and Lists+- Parse checkItems Trello JSON using Aeson FromJSON rather than needing extra record type+- Remove duplication of config - currently using ini and hard-coded defaults+- Use Shake instead of bash script++## Bugs++- Empty subtasks create "---" task+ > Feel like I added this for some reason, can't think why...+- Pressing Esc while editing a task should go back to previous version?+- Selected item can still get a bit lost in SEARCH mode when going between lists+ > Sometimes defaults to 0, when there is something to be found+- Very long words should get hyphenated+ > The cursor gets lost if a word is longer than the line - URLs in particular can cause issues+- Help modal needs to wrap and scroll+- Limit modal height based on content+- Multiple spaces in a line don't show up as more than one, but are saved as more than one+- Task description should be visible by default in task detail+ > Visibility should be on the description by default?+- No obvious way to know if there are more items in a list off-screen+ > Lowest item should be "..." if more items+- Blank trello token should show info about setting it up rather than auth error+ > Auth error should show setup info too?+- A single issue with config.ini reverts to defaultConfig?++## Features++- Add Cabal test to build process+ > Use `stack sdist .` to generate cabal file+- Define key binding info in one place+ > Currently all over the place+ * [ ] Bindings+ * [ ] Default key+ * [ ] Description+ * [ ] Generate bindings.ini from it+- Somehow merge `event` and `events` in Actions that use bindings+ > Can one event trigger multiple separate state changes?+- Some way to just see tasks with due dates+ * [x] Sort by date+ * [x] Scrollable+ * [x] Pressing Enter on one takes you to it+ * [x] Use renderTask from Draw+ * [ ] Filter by: overdue, this week, later+ * [ ] Number of due items in status bar?+ * [x] Descriptions need to wrap+ * [ ] Search should filter+ * [ ] Show list each task belongs to?+ * [x] Backspace (customisable) removes date+- Configuration options+ > See #54+ * [ ] Padding+ * [ ] Move to top of list - different key binding?+ * [ ] Show toggle bar+- Edit task text in DETAIL mode+ > Need to be able to select different parts of the DETAIL modal. Probably need to rethink Mode constructors.+- Pressing Undo in DUE mode should undo without leaving view+ > Fallthrough to NORMAL events more generally? Might require rethinking modes to separate behaviour and things being tracked?+- Add a List widget for common actions between tasks and sub-tasks+ > Or use a typeclass... somehow?+ * [ ] Moving around+ * [ ] Re-ordering subtasks+ * [ ] New items above/below+ * [ ] Duplicating+ * [ ] Marking complete+ * [ ] Deleting+- Add custom key support+ * [x] Create bindings.ini+ * [x] Update events to use Map from bindings.ini+ * [ ] Check for key conflicts: include keys not explicitly mapped (e.g. 1-9, Esc, Enter)+ * [x] Check for bits of functionality missing a mapping+ * [x] Update Help dialogue with key mappings+ * [x] Needs to support merging with default options so that it's easy to add new default keys in the future+ * [ ] Add keys to Help which aren't in bindings+ * [ ] More detailed error messages for missing/invalid mappings+- Add tags/labels with `t`+ * [ ] Way to filter by tag+ * [ ] Hide/show tags with key press+- Performance with large files+ > Becomes unusable with large files+ * [x] Initially use debouncing to avoid writing too often+ * [ ] Cache formatting results+ * [ ] Invalidate layout cache less frequently+ * [ ] Benchmarking tests+ * [ ] Allow cancelling write to avoid trying to write the same file at the same time+- Should be able to have new-lines in task descriptions+ * [x] Trello import+ * [ ] Regular input (Shift + Enter for new line?)+ * [x] Markdown parsing+ * [ ] Text line breaks go a bit funny with multi-line descriptions+- Always show list title+ > Floating list titles - so you can always see what list you're in+- Make token UX better+ * [ ] Open link automatically?+ * [ ] Ask for token and save to ini file automatically+- URL field - plus config to run specific command when selected (e.g. `open -a Chrome.app #{url}`)+- Import Issues from GitHub using labels+- Readline support?+ > Using Haskline: https://rootmos.github.io/main/2017/08/31/combining-brick-and-haskeline.html+- Editable title?+ > Use a `# Title` at top of file and display title somewhere in taskell+- Keep undo between sessions?+- Ability to load a taskell file with custom config.ini settings+ > Either command line arguments for settings or just a `-c other.ini` command+- Inifinite task depth?+ > No reason, other than UX, that sub-tasks can't have sub-tasks.+- Add Trello syncing++## In Progress++- Relative dates days and weeks shouldn't include time?+- Show remaining time on near dates++## Done++- `a` to add+- `e` to edit+- `Space` to mark complete+- `j`/`k`/`up`/`down` to move up/down list+- `h`/`l`/`Left`/`Right` to move between lists+- `q` to quit+- Create taskell.json if doesn't exist+- Move tasks up/down+- Delete with `D`+- No padding on lists+- Order of lists is wrong+- Can't switch lists+- Add doesn't go to bottom of list+- foldr1 in UI/Main will break if no lists+- Cursor support+- Move tasks between lists with `H`/`K`+- Move to next list with `Space`+- Create new list with `N`+- Delete lists with `X`+- Enter while in add mode creates new/Esc leaves add mode+- Rename Tasks to List+- Rename AllTasks to Lists+- Horizontal scrolling+- Scrolling long lists+- If no lists crashes on up/down - using index in AllTasks+- State should return Maybe?+- Select lists with `1-9`+- Run with any correctly formatted json file+- Wrap lines+- UI modules need refactoring+- Horizontal scrolling stops working if a word longer than the column width is entered+- Cursor no longer in the right place+- Space in edit/add mode doesn't move cursor along - bit disconcerting+- If no items in current list, returns a Nothing, so everything dissappears+- CreateList mode doesn't display anything+- Cursor doesn't show on CreateList mode+- Reordering lists with `>` and `<`+- `o` to add on line below+- `O` to add on line above+- `G` take to bottom of file+- Undo with `u`+- Should only save after adding a new item or finished editing an item - should save when pressing Enter while adding new items+- `C` to change task - i.e. deletes text and goes into edit mode+- Space moves to next and stays in list / H & L move but keep current+- Cursor position assumes single line list title+- `E` edits list title+- Pressing enter on Edit creates new task+- Reordering leftmost list left takes you to last list+- Linux binary+- Homebrew support+- Tabs in tasks throw off cursor and wrapping+- Cursor vertical offset is wrong when adding a new list+- Search using `/`+- Change foldl to foldl`+- Debian package+- Empty tasks aren't obviously selectable+- Pressing `e` on a blank list breaks things+- Add support for Markdown+- Fixed Unicode support+- Scrolling+- New list outside view doesn't scroll+- New item outside view doesn't scroll+- Vertical scrolling falls behind+- Use concurrency for IO+- Search UI+- Vertical scrolling hides list titles+- Use Brick for UI+- `C` doesn't work properly+- Custom colours+- `.taskell` config file in home directory+- Rename Persistence to Taskell.IO+- List titles sometimes go missing+- Use Template Haskell to import in config file templates+- On `?` show keyboard commands+- Remove size from state+- Sub-lists+ * [x] Scrolling in sub-tasks+ * [x] Press Enter to create next+ * [x] Word wrapping+ * [x] Searching+ * [x] Delete items+- No cursor in sub-task view+ * [x] Single line+ * [x] Multi-line+- Customisable Markdown format+ * [x] Change top level headers+ * [x] Change top level list item: e.g. to H3 instead of li+ * [x] Change sub-list: e.g. from " *" to "-"+- Feels sluggish in sub-task view - cache main view?+- Leaving search only refreshes current list+- Display a warning if any line of the file could not be parsed - otherwise could lead to data loss+- One bad config line stops all config from working - needs to merge with defaultConfig+- Split up Draw/Modal code into more logical chunks+- Move between lists with `m` - shows possible lists+- Caching issue when using `m` to move lists - doesn't update previous list+- Copy and paste?+- Pressing Enter on empty list shows an subtasks box with an error+- Cursor goes missing on the left hand side at the end of a line - needs to wrap+- Sub-task count not visible on last item in a list longer than the vertical height+- Vertical spacing doesn't work if the current item is blank+- Empty tasks - i.e. just a space - don't show up+- Editing list title doesn't always have visibility+- Left/Right arrow keys in insert mode+- Share code between tasks and sub-tasks lists?+ * [x] Move wrap into Field widget+ * [x] Use Field for search+ * [x] Use Field for sub-tasks+ * [x] Use Field for titles+ * [x] Use Field for tasks+ * [x] Make sure `C` works+- Copy and paste+ * [x] List titles+ * [x] Search+- Multiple spaces at the beginning of a line can break cursor positioning+- Task body - e.g. as well as sub lists, have a longer description+- Indicator for when a task has a description+ > Use ≡?+- Pressing `Esc` when entering task description shouldn't reset it+- Better Trello import errors - e.g. auth vs. parsing issues+ * [x] Error on parse issues+ * [x] Error on Auth issues+- Add due dates to tasks with `@`+ * [x] Render due dates+ * [x] Editable due dates+- Trello dates need to take current timezone into account+ > Trello gives dates in UTC, but need to display them in the current timezone. Deadlines should also take timezones into account if necessary.+- Move to column only works for columns before the one you're in+- Add Trello import+ * [x] Basic trello import+ * [x] Add due date support+ * [x] Add sub-tasks support+ * [x] Add card summary support+- Improve Trello checklist import+ * [x] Take checklist fetch errors into account+ * [x] Refactor code+ * [x] Use Reader to pass around trello token?+- Should change list numbering to letters when in move list mode+- GitHub checklist support - []/[x]+- Caching doesn't clear properly when using `o` and `O`+- Add description status indicator option to config.ini+ > Part of the themeing should allow changing to different icon - might not work in all fonts+- Add Twitter links+ * [x] Website+ * [x] Github+- Move image to taskell.app+ > The demo image should live on the taskell.app server, rather than being on an orphan branch on GitHub+ * [x] Move image+ * [x] Update site+ * [x] Update GitHub README.md+ * [x] Remove img branch+- Sort out Homebrew forumula+ > Make the necessary changes so that taskell can be put on the homebrew-core repository+ * [x] Find someone to submit it+ * [x] Use `install_cabal_package`+ * [x] Use `depends_on "cabal-install" => :build`+- Update Task field naming+ * [x] Task: description -> name/title+ * [x] Task: summary -> description+ * [x] Taskell.UI.Modal.SubTasks -> Taskell.UI.Modal.Detail+- The isBlank check on tasks could potentially delete a task with no description but which does have sub-tasks+- Blank task names should appear as something+- Use lenses for nested data+- Add more tests+ * [x] Trello response parsing+- Add GitHub Project support+- Refactor Taskell.IO+ > Avoid repeating basically the same code for Trello and GitHub fetching+- Add ability to list GitHub projects+ > Give an organisation or username and repo, list the possible projects to fetch - avoid having to look up the project ID manually first+- GitHub import should take pagination into account+- Use XDG spec for storing config files+- Automate website publishing when doing a new build+ > Should automatically update the `_config.yml` file, build the website, then deploy it+- Can't remove a description+- Title bar for extra info+ * [x] File path+ * [x] Current position+- Search should be case insensitive+- Add Mode to status bar+- Modals interfere with status bar+- Showing a specific task in search mode shows wrong task+ > Based on the index in the full list, rather than the filtered one. So will show the task from the full list if the indexes don't happen to match.+- Can't remove dates+- Tidy up load functions in Taskell.IO+- Getting stuck in INSERT mode when blank item+- If an item isn't created, then selection gets lost+- Search navigation issues+ > Issues with navigation when in NORMAL + SEARCH mode+ * [x] Navigating up and down+ * [x] Navigating between lists+ * [x] Moving task up and down+ * [x] Often nothing is selected when first entering search mode+- Duplicate task with `+`+- Use Reader throughout Draw/Modal modules+- Simplify DrawState+ * [x] Bindings+ * [x] Today+ * [x] Normalised State+ * [x] Layout+- Add a debug option+ * [x] Shows full Mode print out+- Add a "complete" action+ > Moves to last column and removes date. Space bar by default.+- Remove `~` style sub-task complete parsing+- MoveTo shouldn't show if nothing selected+- Deleting a list can screw up current position+- Remove date+ * [x] NORMAL mode+ * [x] DETAIL mode+ * [x] DUE mode+- Update screenshot+- Date should update if taskell is left open+- Use proper error codes+- Redo functionality+- Modifier keys?+- Use relative times for due dates+ > e.g. 1w, 2d, 1w 2d (see `man sleep` options for ideas)+ * [x] Markdown input+ * [x] Markdown output+ * [x] Markdown needs to use actual timezone+ * [x] Need to use parser+ * [x] Trello time parsing+- Use ReaderConfig in Taskell.IO.Markdown.Internal stringify functions+- Add test for subtask linebreaks to MarkdownTest+- Config option to always use UTC for markdown output+- Check times work no matter what timezone+- Show time on short dates+- Date validation+- Check order of due items+- Trim spaces for date input