packages feed

taskell 1.5.0.0 → 1.7.0.0

raw patch · 62 files changed

+1389/−814 lines, 62 filesdep ~aesondep ~attoparsecdep ~fold-debouncePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, attoparsec, fold-debounce, http-client, http-conduit, tasty, tasty-expected-failure, tasty-hunit

API changes (from Hackage documentation)

- Data.Taskell.Date: currentDay :: IO Day
- Events.State.Types: type Pointer = (Int, Int)
- IO.Markdown.Internal: trimTilde :: Text -> Text
- UI.Field: Field :: Text -> Int -> Field
- UI.Field: [_cursor] :: Field -> Int
- UI.Field: [_text] :: Field -> Text
- UI.Field: append :: Text -> [Text] -> [Text]
- UI.Field: backspace :: Field -> Field
- UI.Field: blankField :: Field
- UI.Field: combine :: Int -> ([Text], Int) -> Text -> ([Text], Int)
- UI.Field: cursorPosition :: [Text] -> Int -> Int -> (Int, Int)
- UI.Field: data Field
- UI.Field: event :: Event -> Field -> Field
- UI.Field: field :: Field -> Widget ResourceName
- UI.Field: getText :: Field -> Text
- UI.Field: insertCharacter :: Char -> Field -> Field
- UI.Field: insertText :: Text -> Field -> Field
- UI.Field: instance GHC.Classes.Eq UI.Field.Field
- UI.Field: instance GHC.Show.Show UI.Field.Field
- UI.Field: spl :: Text -> [Text]
- UI.Field: spl' :: [Text] -> Char -> [Text]
- UI.Field: textField :: Text -> Widget ResourceName
- UI.Field: textToField :: Text -> Field
- UI.Field: updateCursor :: Int -> Field -> Field
- UI.Field: widgetFromMaybe :: Widget ResourceName -> Maybe Field -> Widget ResourceName
- UI.Field: wrap :: Int -> Text -> ([Text], Int)
+ Data.Taskell.List: clearDue :: TaskIndex -> Update
+ Data.Taskell.List: due :: List -> Seq (TaskIndex, Task)
+ Data.Taskell.List: duplicate :: Int -> List -> Maybe List
+ Data.Taskell.List: type Update = List -> List
+ Data.Taskell.List.Internal: clearDue :: TaskIndex -> Update
+ Data.Taskell.List.Internal: due :: List -> Seq (TaskIndex, Task)
+ Data.Taskell.List.Internal: duplicate :: Int -> List -> Maybe List
+ Data.Taskell.Lists: clearDue :: Pointer -> Update
+ Data.Taskell.Lists: due :: Lists -> Seq (Pointer, Task)
+ Data.Taskell.Lists.Internal: clearDue :: Pointer -> Update
+ Data.Taskell.Lists.Internal: due :: Lists -> Seq (Pointer, Task)
+ Data.Taskell.Lists.Internal: updateFn :: ListIndex -> Update -> Update
+ Data.Taskell.Seq: (<#>) :: (Int -> a -> b) -> Seq a -> Seq b
+ Data.Taskell.Seq: bound :: Seq a -> Int -> Int
+ Data.Taskell.Subtask: duplicate :: Subtask -> Subtask
+ Data.Taskell.Subtask.Internal: duplicate :: Subtask -> Subtask
+ Data.Taskell.Task: clearDue :: Update
+ Data.Taskell.Task: duplicate :: Task -> Task
+ Data.Taskell.Task.Internal: clearDue :: Update
+ Data.Taskell.Task.Internal: duplicate :: Task -> Task
+ Events.Actions.Types: ClearDate :: ActionType
+ Events.Actions.Types: Complete :: ActionType
+ Events.Actions.Types: Due :: ActionType
+ Events.Actions.Types: Duplicate :: ActionType
+ Events.State: clearDate :: Stateful
+ Events.State: duplicate :: Stateful
+ Events.State: moveToLast :: Stateful
+ Events.State: setTime :: UTCTime -> State -> State
+ Events.State.Types: [_time] :: State -> UTCTime
+ Events.State.Types: time :: Lens' State UTCTime
+ Events.State.Types.Mode: Due :: Seq (Pointer, Task) -> Int -> ModalType
+ IO.Config: debugging :: Config -> Bool
+ Types: ListIndex :: Int -> ListIndex
+ Types: TaskIndex :: Int -> TaskIndex
+ Types: [showListIndex] :: ListIndex -> Int
+ Types: [showTaskIndex] :: TaskIndex -> Int
+ Types: instance GHC.Classes.Eq Types.ListIndex
+ Types: instance GHC.Classes.Eq Types.TaskIndex
+ Types: instance GHC.Classes.Ord Types.ListIndex
+ Types: instance GHC.Classes.Ord Types.TaskIndex
+ Types: instance GHC.Show.Show Types.ListIndex
+ Types: instance GHC.Show.Show Types.TaskIndex
+ Types: newtype ListIndex
+ Types: newtype TaskIndex
+ Types: type Pointer = (ListIndex, TaskIndex)
+ UI.Draw.Field: Field :: Text -> Int -> Field
+ UI.Draw.Field: [_cursor] :: Field -> Int
+ UI.Draw.Field: [_text] :: Field -> Text
+ UI.Draw.Field: append :: Text -> [Text] -> [Text]
+ UI.Draw.Field: backspace :: Field -> Field
+ UI.Draw.Field: blankField :: Field
+ UI.Draw.Field: combine :: Int -> ([Text], Int) -> Text -> ([Text], Int)
+ UI.Draw.Field: cursorPosition :: [Text] -> Int -> Int -> (Int, Int)
+ UI.Draw.Field: data Field
+ UI.Draw.Field: event :: Event -> Field -> Field
+ UI.Draw.Field: field :: Field -> Widget ResourceName
+ UI.Draw.Field: getText :: Field -> Text
+ UI.Draw.Field: insertCharacter :: Char -> Field -> Field
+ UI.Draw.Field: insertText :: Text -> Field -> Field
+ UI.Draw.Field: instance GHC.Classes.Eq UI.Draw.Field.Field
+ UI.Draw.Field: instance GHC.Show.Show UI.Draw.Field.Field
+ UI.Draw.Field: spl :: Text -> [Text]
+ UI.Draw.Field: spl' :: [Text] -> Char -> [Text]
+ UI.Draw.Field: textField :: Text -> Widget ResourceName
+ UI.Draw.Field: textToField :: Text -> Field
+ UI.Draw.Field: updateCursor :: Int -> Field -> Field
+ UI.Draw.Field: widgetFromMaybe :: Widget ResourceName -> Maybe Field -> Widget ResourceName
+ UI.Draw.Field: widthFold :: [Int] -> Char -> [Int]
+ UI.Draw.Field: wrap :: Int -> Text -> ([Text], Int)
- Data.Taskell.List: move :: Int -> Int -> List -> Maybe List
+ Data.Taskell.List: move :: Int -> Int -> Maybe Text -> List -> Maybe (List, Int)
- Data.Taskell.List.Internal: bound :: Int -> List -> Int
+ Data.Taskell.List.Internal: bound :: List -> Int -> Int
- Data.Taskell.List.Internal: move :: Int -> Int -> List -> Maybe List
+ Data.Taskell.List.Internal: move :: Int -> Int -> Maybe Text -> List -> Maybe (List, Int)
- Data.Taskell.Lists: changeList :: (Int, Int) -> Lists -> Int -> Maybe Lists
+ Data.Taskell.Lists: changeList :: Pointer -> Lists -> Int -> Maybe Lists
- Data.Taskell.Lists.Internal: changeList :: (Int, Int) -> Lists -> Int -> Maybe Lists
+ Data.Taskell.Lists.Internal: changeList :: Pointer -> Lists -> Int -> Maybe Lists
- Events.State: create :: FilePath -> Lists -> State
+ Events.State: create :: UTCTime -> FilePath -> Lists -> State
- Events.State.Types: State :: Mode -> Lists -> [(Pointer, Lists)] -> Pointer -> FilePath -> Maybe Lists -> Int -> Maybe Field -> State
+ Events.State.Types: State :: Mode -> Lists -> [(Pointer, Lists)] -> Pointer -> FilePath -> Maybe Lists -> Int -> Maybe Field -> UTCTime -> State

Files

app/Main.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoImplicitPrelude #-}  module Main where@@ -14,7 +13,8 @@ main = do     config <- setup     next <- runReaderT load config+    time <- getCurrentTime     case next of         Exit            -> pure ()         Output text     -> putStrLn text-        Load path lists -> go config $ create path lists+        Load path lists -> go config $ create time path lists
src/App.hs view
@@ -6,25 +6,28 @@  import ClassyPrelude +import Control.Concurrent (forkIO, threadDelay)+ import Control.Lens ((^.))  import Brick-import Graphics.Vty              (Mode (BracketedPaste), displayBounds, outputIface, setMode,-                                  supportsMode)+import Brick.BChan               (BChan, newBChan, writeBChan)+import Graphics.Vty              (Mode (BracketedPaste), defaultConfig, displayBounds, mkVty,+                                  outputIface, setMode, supportsMode) import Graphics.Vty.Input.Events (Event (..))  import qualified Control.FoldDebounce as Debounce -import Data.Taskell.Date       (currentDay) import Data.Taskell.Lists      (Lists) import Events.Actions          (ActionSets, event, generateActions)-import Events.State            (continue, countCurrent, setHeight)+import Events.State            (continue, countCurrent, setHeight, setTime) import Events.State.Types      (State, current, io, lists, mode, path, searchTerm) import Events.State.Types.Mode (InsertMode (..), InsertType (..), ModalType (..), Mode (..))-import IO.Config               (Config, generateAttrMap, getBindings, layout)+import IO.Config               (Config, debugging, generateAttrMap, getBindings, layout) import IO.Taskell              (writeData)+import Types                   (ListIndex (..), TaskIndex (..)) import UI.Draw                 (chooseCursor, draw)-import UI.Types                (ListIndex (..), ResourceName (..), TaskIndex (..))+import UI.Types                (ResourceName (..))  type DebouncedMessage = (Lists, FilePath) @@ -32,6 +35,22 @@  type Trigger = Debounce.Trigger DebouncedMessage DebouncedMessage +-- tick+data TaskellEvent =+    Tick++oneSecond :: Int+oneSecond = 1000000++frequency :: Int+frequency = 60 * oneSecond++timer :: BChan TaskellEvent -> IO ()+timer chan =+    void . forkIO . forever $ do+        writeBChan chan Tick+        threadDelay frequency+ -- store store :: Config -> DebouncedMessage -> IO () store config (ls, pth) = writeData config ls pth@@ -62,7 +81,7 @@ -- cache clearing clearCache :: State -> EventM ResourceName () clearCache state = do-    let (li, ti) = state ^. current+    let (ListIndex li, TaskIndex ti) = state ^. current     invalidateCacheEntry (RNList li)     invalidateCacheEntry (RNTask (ListIndex li, TaskIndex ti)) @@ -75,12 +94,20 @@  clearList :: State -> EventM ResourceName () clearList state = do-    let (list, _) = state ^. current+    let (ListIndex list, _) = state ^. current     let count = countCurrent state     let range = [0 .. (count - 1)]     invalidateCacheEntry $ RNList list     traverse_ (invalidateCacheEntry . RNTask . (,) (ListIndex list) . TaskIndex) range +clearDue :: State -> EventM ResourceName ()+clearDue state =+    case state ^. mode of+        Modal (Due dues _) -> do+            let range = [0 .. (length dues + 1)]+            traverse_ (invalidateCacheEntry . RNDue) range+        _ -> pure ()+ -- event handling handleVtyEvent ::        (DebouncedWrite, Trigger) -> ActionSets -> State -> Event -> EventM ResourceName (Next State)@@ -93,6 +120,7 @@         _                        -> pure ()     case state ^. mode of         Shutdown -> liftIO (Debounce.close trigger) *> Brick.halt state+        (Modal Due {}) -> clearDue state *> next send state         (Modal MoveTo) -> clearAllTitles state *> next send state         (Insert ITask ICreate _) -> clearList state *> next send state         _ -> clearCache previousState *> clearCache state *> next send state@@ -104,8 +132,11 @@        (DebouncedWrite, Trigger)     -> ActionSets     -> State-    -> BrickEvent ResourceName e+    -> BrickEvent ResourceName TaskellEvent     -> EventM ResourceName (Next State)+handleEvent _ _ state (AppEvent Tick) = do+    t <- liftIO getCurrentTime+    Brick.continue $ setTime t state handleEvent _ _ state (VtyEvent (EvResize _ _)) = do     invalidateCache     h <- getHeight@@ -126,15 +157,23 @@ go :: Config -> State -> IO () go config initial = do     attrMap' <- const <$> generateAttrMap-    today <- currentDay+    -- setup debouncing     db <- debounce config initial+    -- get bindings     bindings <- getBindings+    -- setup timer channel+    timerChan <- newBChan 1+    timer timerChan+    -- create app     let app =             App-            { appDraw = draw (layout config) bindings today+            { appDraw = draw (layout config) bindings (debugging config)             , appChooseCursor = chooseCursor             , appHandleEvent = handleEvent db (generateActions bindings)             , appStartEvent = appStart             , appAttrMap = attrMap'             }-    void (defaultMain app initial)+    -- start+    let builder = mkVty defaultConfig+    initialVty <- builder+    void $ customMain initialVty builder (Just timerChan) app initial
src/Config.hs view
@@ -9,7 +9,7 @@ import Data.FileEmbed (embedFile)  version :: Text-version = "1.5.0"+version = "1.7.0"  usage :: Text usage = decodeUtf8 $(embedFile "templates/usage.txt")
src/Data/Taskell/Date.hs view
@@ -9,7 +9,6 @@     , dayToOutput     , textToDay     , utcToLocalDay-    , currentDay     , deadline     ) where @@ -52,9 +51,6 @@  textToDay :: Text -> Maybe Day textToDay = (utctDay <$>) . textToTime--currentDay :: IO Day-currentDay = utctDay <$> getCurrentTime  -- work out the deadline deadline :: Day -> Day -> Deadline
src/Data/Taskell/List.hs view
@@ -2,13 +2,17 @@  module Data.Taskell.List     ( List+    , Update     , title     , tasks     , create     , empty+    , due+    , clearDue     , new     , count     , newAt+    , duplicate     , append     , extract     , updateFn
src/Data/Taskell/List/Internal.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}  module Data.Taskell.List.Internal where @@ -10,7 +11,8 @@ import Data.Sequence as S (adjust', deleteAt, insertAt, update, (|>))  import qualified Data.Taskell.Seq  as S-import qualified Data.Taskell.Task as T (Task, Update, blank, contains)+import qualified Data.Taskell.Task as T (Task, Update, blank, clearDue, contains, due, duplicate)+import           Types             (TaskIndex (TaskIndex))  data List = List     { _title :: Text@@ -35,9 +37,22 @@ 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 %~ insertAt idx T.blank+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 %~ (|> task) @@ -52,8 +67,13 @@ update :: Int -> T.Task -> Update update idx task = tasks %~ S.update idx task -move :: Int -> Int -> List -> Maybe List-move from dir = tasks %%~ S.shiftBy from dir+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@@ -87,11 +107,8 @@         then next         else previous -bound :: Int -> List -> Int-bound idx lst-    | idx < 0 = 0-    | idx > count lst = count lst - 1-    | otherwise = idx+bound :: List -> Int -> Int+bound lst = S.bound (lst ^. tasks)  nearest' :: Int -> Maybe Text -> List -> Maybe Int nearest' current term lst = do@@ -106,5 +123,5 @@     near = fromMaybe (-1) $ nearest' current term lst     idx =         case term of-            Nothing  -> bound current lst+            Nothing  -> bound lst current             Just txt -> maybe near (bool near current . T.contains txt) $ getTask current lst
src/Data/Taskell/Lists.hs view
@@ -5,6 +5,8 @@     , initial     , updateLists     , count+    , due+    , clearDue     , get     , changeList     , newList
src/Data/Taskell/Lists/Internal.hs view
@@ -1,41 +1,57 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}  module Data.Taskell.Lists.Internal where -import ClassyPrelude hiding (empty)+import ClassyPrelude -import Data.Sequence as S (deleteAt, update, (!?), (|>))+import Control.Lens  ((^.))+import Data.Sequence as S (adjust', deleteAt, update, (!?), (|>)) -import           Data.Taskell.List as L (List, append, count, empty, extract, searchFor)+import qualified Data.Taskell.List as L (List, Update, append, clearDue, count, due, empty, extract,+                                         searchFor) import qualified Data.Taskell.Seq  as S-import           Data.Taskell.Task (Task)+import qualified Data.Taskell.Task as T (Task, due)+import           Types             (ListIndex (ListIndex), Pointer, TaskIndex (TaskIndex)) -type Lists = Seq List+type Lists = Seq L.List  type Update = Lists -> Lists  initial :: Lists-initial = fromList [empty "To Do", empty "Done"]+initial = fromList [L.empty "To Do", L.empty "Done"] -updateLists :: Int -> List -> Update+updateLists :: Int -> L.List -> Update updateLists = S.update  count :: Int -> Lists -> Int count idx tasks = maybe 0 L.count (tasks !? idx) -get :: Lists -> Int -> Maybe List+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 :: (Int, Int) -> Lists -> Int -> Maybe Lists-changeList (list, idx) tasks dir = do+changeList :: Pointer -> Lists -> Int -> Maybe Lists+changeList (ListIndex list, TaskIndex idx) tasks dir = do     let next = list + dir-    (from, task) <- extract idx =<< tasks !? list -- extract current task-    to <- append task <$> tasks !? next -- get next list and append task+    (from, task) <- L.extract idx =<< tasks !? list -- extract current task+    to <- L.append task <$> tasks !? next -- get next list and append task     pure . updateLists next to $ updateLists list from tasks -- update lists  newList :: Text -> Update-newList title = (|> empty title)+newList title = (|> L.empty title)  delete :: Int -> Update delete = deleteAt@@ -47,13 +63,13 @@ shiftBy = S.shiftBy  search :: Text -> Update-search text = (searchFor text <$>)+search text = (L.searchFor text <$>) -appendToLast :: Task -> Update+appendToLast :: T.Task -> Update appendToLast task lists =     fromMaybe lists $ do         let idx = length lists - 1-        list <- append task <$> lists !? idx+        list <- L.append task <$> lists !? idx         pure $ updateLists idx list lists  analyse :: Text -> Lists -> Text
src/Data/Taskell/Seq.hs view
@@ -4,8 +4,11 @@  import ClassyPrelude -import Data.Sequence (deleteAt, insertAt, (!?))+import Data.Sequence (deleteAt, insertAt, mapWithIndex, (!?)) +(<#>) :: (Int -> a -> b) -> Seq a -> Seq b+(<#>) = mapWithIndex+ extract :: Int -> Seq a -> Maybe (Seq a, a) extract idx xs = (,) (deleteAt idx xs) <$> xs !? idx @@ -13,3 +16,9 @@ shiftBy idx dir xs = do     (a, current) <- extract idx xs     pure $ insertAt (idx + dir) current a++bound :: Seq a -> Int -> Int+bound s i+    | i < 0 = 0+    | i >= length s = pred (length s)+    | otherwise = i
src/Data/Taskell/Subtask.hs view
@@ -8,6 +8,7 @@     , name     , complete     , toggle+    , duplicate     ) where  import Data.Taskell.Subtask.Internal
src/Data/Taskell/Subtask/Internal.hs view
@@ -26,3 +26,6 @@  toggle :: Update toggle = complete %~ not++duplicate :: Subtask -> Subtask+duplicate (Subtask n c) = Subtask n c
src/Data/Taskell/Task.hs view
@@ -9,9 +9,11 @@     , subtasks     , blank     , new+    , duplicate     , setDescription     , appendDescription     , setDue+    , clearDue     , getSubtask     , addSubtask     , hasSubtasks
src/Data/Taskell/Task/Internal.hs view
@@ -10,7 +10,7 @@  import           Data.Sequence        as S (adjust', deleteAt, (|>)) import           Data.Taskell.Date    (Day, textToDay)-import qualified Data.Taskell.Subtask as ST (Subtask, Update, complete, name)+import qualified Data.Taskell.Subtask as ST (Subtask, Update, complete, duplicate, name) import           Data.Text            (strip)  data Task = Task@@ -57,6 +57,9 @@                  Just day -> task & due .~ Just day                  Nothing  -> task +clearDue :: Update+clearDue task = task & due .~ Nothing+ getSubtask :: Int -> Task -> Maybe ST.Subtask getSubtask idx = (^? subtasks . ix idx) @@ -89,3 +92,6 @@ 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/Events/Actions.hs view
@@ -21,6 +21,7 @@ import qualified Events.Actions.Insert       as Insert import qualified Events.Actions.Modal        as Modal import qualified Events.Actions.Modal.Detail as Detail+import qualified Events.Actions.Modal.Due    as Due import qualified Events.Actions.Modal.Help   as Help import qualified Events.Actions.Normal       as Normal import qualified Events.Actions.Search       as Search@@ -43,6 +44,7 @@             case state ^. mode of                 Normal                        -> lookup e $ normal actions                 Modal (Detail _ DetailNormal) -> lookup e $ detail actions+                Modal Due {}                  -> lookup e $ due actions                 Modal Help                    -> lookup e $ help actions                 _                             -> Nothing     fromMaybe state $@@ -54,6 +56,7 @@     { normal :: BoundActions     , detail :: BoundActions     , help   :: BoundActions+    , due    :: BoundActions     }  generateActions :: Bindings -> ActionSets@@ -62,4 +65,5 @@     { normal = generate bindings Normal.events     , detail = generate bindings Detail.events     , help = generate bindings Help.events+    , due = generate bindings Due.events     }
src/Events/Actions/Insert.hs view
@@ -12,7 +12,7 @@ import           Events.State.Types import           Events.State.Types.Mode   (InsertMode (..), InsertType (..), Mode (Insert)) import           Graphics.Vty.Input.Events (Event (EvKey), Key (KEnter, KEsc))-import qualified UI.Field                  as F (event)+import qualified UI.Draw.Field             as F (event)  event :: Event -> Stateful event (EvKey KEnter _) state =
src/Events/Actions/Modal.hs view
@@ -13,13 +13,15 @@ import Graphics.Vty.Input.Events  import qualified Events.Actions.Modal.Detail as Detail+import qualified Events.Actions.Modal.Due    as Due import qualified Events.Actions.Modal.Help   as Help import qualified Events.Actions.Modal.MoveTo as MoveTo  event :: Event -> Stateful event e s =     case s ^. mode of-        Modal Help         -> Help.event e s-        Modal (Detail _ _) -> Detail.event e s-        Modal MoveTo       -> MoveTo.event e s-        _                  -> pure s+        Modal Help      -> Help.event e s+        Modal Detail {} -> Detail.event e s+        Modal MoveTo    -> MoveTo.event e s+        Modal Due {}    -> Due.event e s+        _               -> pure s
src/Events/Actions/Modal/Detail.hs view
@@ -16,7 +16,7 @@ import           Events.State.Types.Mode   (DetailItem (..), DetailMode (..)) import           Graphics.Vty.Input.Events import           IO.Keyboard.Types         (Actions)-import qualified UI.Field                  as F (event)+import qualified UI.Draw.Field             as F (event)  events :: Actions events@@ -28,9 +28,10 @@     , (A.Next, nextSubtask)     , (A.New, (Detail.insertMode =<<) . (Detail.lastSubtask =<<) . (Detail.newItem =<<) . store)     , (A.Edit, (Detail.insertMode =<<) . store)-    , (A.MoveRight, (write =<<) . (setComplete =<<) . store)+    , (A.Complete, (write =<<) . (setComplete =<<) . store)     , (A.Delete, (write =<<) . (Detail.remove =<<) . store)     , (A.DueDate, (editDue =<<) . store)+    , (A.ClearDate, (write =<<) . (clearDate =<<) . store)     , (A.Detail, (editDescription =<<) . store)     ] 
+ src/Events/Actions/Modal/Due.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedLists #-}++module Events.Actions.Modal.Due+    ( event+    , events+    ) where++import ClassyPrelude++import Events.Actions.Types      as A (ActionType (..))+import Events.State              (normalMode, store, write)+import Events.State.Modal.Detail (showDetail)+import Events.State.Modal.Due    (clearDate, goto, next, previous)+import Events.State.Types        (Stateful)+import Graphics.Vty.Input.Events (Event (EvKey))+import IO.Keyboard.Types         (Actions)++events :: Actions+events =+    [ (A.Previous, previous)+    , (A.Next, next)+    , (A.Detail, (showDetail =<<) . goto)+    , (A.ClearDate, (write =<<) . (clearDate =<<) . store)+    ]++event :: Event -> Stateful+event (EvKey _ _) = normalMode+event _           = pure
src/Events/Actions/Normal.hs view
@@ -12,6 +12,7 @@ import Events.Actions.Types      as A (ActionType (..)) import Events.State import Events.State.Modal.Detail (editDue, showDetail)+import Events.State.Modal.Due    (showDue) import Events.State.Types        (Stateful) import Graphics.Vty.Input.Events import IO.Keyboard.Types         (Actions)@@ -24,6 +25,7 @@     , (A.Undo, (write =<<) . undo)     , (A.Search, searchMode)     , (A.Help, showHelp)+    , (A.Due, showDue)         -- navigation     , (A.Previous, previous)     , (A.Next, next)@@ -34,17 +36,20 @@     , (A.New, (startCreate =<<) . (newItem =<<) . store)     , (A.NewAbove, (startCreate =<<) . (above =<<) . store)     , (A.NewBelow, (startCreate =<<) . (below =<<) . store)+    , (A.Duplicate, (next =<<) . (write =<<) . (duplicate =<<) . store)     -- editing tasks     , (A.Edit, (startEdit =<<) . store)     , (A.Clear, (startEdit =<<) . (clearItem =<<) . store)     , (A.Delete, (write =<<) . (delete =<<) . store)     , (A.Detail, showDetail)     , (A.DueDate, (editDue =<<) . (store =<<) . showDetail)+    , (A.ClearDate, (write =<<) . (clearDate =<<) . store)     -- moving tasks     , (A.MoveUp, (write =<<) . (up =<<) . store)     , (A.MoveDown, (write =<<) . (down =<<) . store)     , (A.MoveLeft, (write =<<) . (bottom =<<) . (left =<<) . (moveLeft =<<) . store)     , (A.MoveRight, (write =<<) . (bottom =<<) . (right =<<) . (moveRight =<<) . store)+    , (A.Complete, (write =<<) . (moveToLast =<<) . (clearDate =<<) . store)     , (A.MoveMenu, showMoveTo)     -- lists     , (A.ListNew, (createListStart =<<) . store)
src/Events/Actions/Search.hs view
@@ -12,7 +12,7 @@ import           Events.State.Types        (Stateful, mode) import           Events.State.Types.Mode   (Mode (Search)) import           Graphics.Vty.Input.Events-import qualified UI.Field                  as F (event)+import qualified UI.Draw.Field             as F (event)  event :: Event -> Stateful event (EvKey KEsc _) s = clearSearch =<< normalMode s
src/Events/Actions/Types.hs view
@@ -9,6 +9,7 @@     = Quit     | Undo     | Search+    | Due     | Help     | Previous     | Next@@ -18,15 +19,18 @@     | New     | NewAbove     | NewBelow+    | Duplicate     | Edit     | Clear     | Delete     | Detail     | DueDate+    | ClearDate     | MoveUp     | MoveDown     | MoveLeft     | MoveRight+    | Complete     | MoveMenu     | ListNew     | ListEdit@@ -43,6 +47,7 @@ read "quit"       = Quit read "undo"       = Undo read "search"     = Search+read "due"        = Due read "help"       = Help read "previous"   = Previous read "next"       = Next@@ -52,15 +57,18 @@ read "new"        = New read "newAbove"   = NewAbove read "newBelow"   = NewBelow+read "duplicate"  = Duplicate read "edit"       = Edit read "clear"      = Clear read "delete"     = Delete read "detail"     = Detail read "dueDate"    = DueDate+read "clearDate"  = ClearDate read "moveUp"     = MoveUp read "moveDown"   = MoveDown read "moveLeft"   = MoveLeft read "moveRight"  = MoveRight+read "complete"   = Complete read "moveMenu"   = MoveMenu read "listNew"    = ListNew read "listEdit"   = ListEdit
src/Events/State.hs view
@@ -5,6 +5,7 @@     -- App     ( continue     , write+    , setTime     , countCurrent     , setHeight     -- UI.Main@@ -19,10 +20,12 @@     , editListStart     , deleteCurrentList     , clearItem+    , clearDate     , above     , below     , bottom     , previous+    , duplicate     , next     , left     , right@@ -30,6 +33,7 @@     , down     , moveLeft     , moveRight+    , moveToLast     , delete     , selectList     , listLeft@@ -61,46 +65,51 @@  import Data.Char (digitToInt, ord) -import           Data.Taskell.List  (List, deleteTask, getTask, move, nearest, new, newAt, nextTask,-                                     prevTask, title, update)+import qualified Data.Taskell.List  as L (List, deleteTask, duplicate, getTask, move, nearest, new,+                                          newAt, nextTask, prevTask, title, update) import qualified Data.Taskell.Lists as Lists import           Data.Taskell.Task  (Task, isBlank, name)+import           Types  import Events.State.Types import Events.State.Types.Mode (InsertMode (..), InsertType (..), ModalType (..), Mode (..))-import UI.Field                (Field, blankField, getText, textToField)+import UI.Draw.Field           (Field, blankField, getText, textToField)  type InternalStateful = State -> State -create :: FilePath -> Lists.Lists -> State-create p ls =+create :: UTCTime -> FilePath -> Lists.Lists -> State+create t p ls =     State     { _mode = Normal     , _lists = ls     , _history = []-    , _current = (0, 0)+    , _current = (ListIndex 0, TaskIndex 0)     , _path = p     , _io = Nothing     , _height = 0     , _searchTerm = Nothing+    , _time = t     }  -- app state quit :: Stateful-quit = Just . (mode .~ Shutdown)+quit = pure . (mode .~ Shutdown)  continue :: State -> State continue = io .~ Nothing +setTime :: UTCTime -> State -> State+setTime t = time .~ t+ write :: Stateful-write state = Just $ state & io .~ Just (state ^. lists)+write state = pure $ state & io .~ Just (state ^. lists)  store :: Stateful-store state = Just $ state & history .~ (state ^. current, state ^. lists) : state ^. history+store state = pure $ state & history .~ (state ^. current, state ^. lists) : state ^. history  undo :: Stateful undo state =-    Just $+    pure $     case state ^. history of         []          -> state         ((c, l):xs) -> state & current .~ c & lists .~ l & history .~ xs@@ -108,7 +117,7 @@ -- createList createList :: Stateful createList state =-    Just $+    pure $     case state ^. mode of         Insert IList ICreate f ->             updateListToLast . setLists state $ Lists.newList (getText f) $ state ^. lists@@ -118,31 +127,31 @@ updateListToLast state = setCurrentList state (length (state ^. lists) - 1)  createListStart :: Stateful-createListStart = Just . (mode .~ Insert IList ICreate blankField)+createListStart = pure . (mode .~ Insert IList ICreate blankField)  -- editList editListStart :: Stateful editListStart state = do-    f <- textToField . (^. title) <$> getList state+    f <- textToField . (^. L.title) <$> getList state     pure $ state & mode .~ Insert IList IEdit f  deleteCurrentList :: Stateful deleteCurrentList state =-    Just . fixIndex . setLists state $ Lists.delete (getCurrentList state) (state ^. lists)+    pure . fixIndex . setLists state $ Lists.delete (getCurrentList state) (state ^. lists)  -- insert getCurrentTask :: State -> Maybe Task-getCurrentTask state = getList state >>= getTask (getIndex state)+getCurrentTask state = getList state >>= L.getTask (getIndex state)  setCurrentTask :: Task -> Stateful-setCurrentTask task state = setList state . update (getIndex state) task <$> getList state+setCurrentTask task state = setList state . L.update (getIndex state) task <$> getList state  setCurrentTaskText :: Text -> Stateful setCurrentTaskText text state =     flip setCurrentTask state =<< (name .~ text) <$> getCurrentTask state  startCreate :: Stateful-startCreate = Just . (mode .~ Insert ITask ICreate blankField)+startCreate = pure . (mode .~ Insert ITask ICreate blankField)  startEdit :: Stateful startEdit state = do@@ -154,22 +163,22 @@     case state ^. mode of         Insert ITask iMode f ->             setCurrentTaskText (getText f) $ state & (mode .~ Insert ITask iMode blankField)-        _ -> Just state+        _ -> pure state  finishListTitle :: Stateful finishListTitle state =     case state ^. mode of         Insert IList iMode f ->             setCurrentListTitle (getText f) $ state & (mode .~ Insert IList iMode blankField)-        _ -> Just state+        _ -> pure state  normalMode :: Stateful-normalMode = Just . (mode .~ Normal)+normalMode = pure . (mode .~ Normal)  addToListAt :: Int -> Stateful addToListAt offset state = do     let idx = getIndex state + offset-    fixIndex . setList (setIndex state idx) . newAt idx <$> getList state+    fixIndex . setList (setIndex state idx) . L.newAt idx <$> getList state  above :: Stateful above = addToListAt 0@@ -178,11 +187,17 @@ below = addToListAt 1  newItem :: Stateful-newItem state = selectLast . setList state . new <$> getList state+newItem state = selectLast . setList state . L.new <$> getList state +duplicate :: Stateful+duplicate state = setList state <$> (L.duplicate (getIndex state) =<< getList state)+ clearItem :: Stateful clearItem = setCurrentTaskText "" +clearDate :: Stateful+clearDate state = pure $ state & lists .~ Lists.clearDue (state ^. current) (state ^. lists)+ bottom :: Stateful bottom = pure . selectLast @@ -198,27 +213,42 @@         state  -- moving+--+moveVertical :: Int -> Stateful+moveVertical dir state = do+    (lst, idx) <- L.move (getIndex state) dir (getText <$> state ^. searchTerm) =<< getList state+    pure $ setIndex (setList state lst) idx+ up :: Stateful-up state = previous =<< setList state <$> (move (getIndex state) (-1) =<< getList state)+up = moveVertical (-1)  down :: Stateful-down state = next =<< setList state <$> (move (getIndex state) 1 =<< getList state)+down = moveVertical 1 -move' :: Int -> State -> Maybe State-move' idx state =+moveHorizontal :: Int -> State -> Maybe State+moveHorizontal idx state =     fixIndex . setLists state <$> Lists.changeList (state ^. current) (state ^. lists) idx  moveLeft :: Stateful-moveLeft = move' (-1)+moveLeft = moveHorizontal (-1)  moveRight :: Stateful-moveRight = move' 1+moveRight = moveHorizontal 1 +moveToLast :: Stateful+moveToLast state =+    if idx == cur+        then pure state+        else moveHorizontal (idx - cur) state+  where+    idx = length (state ^. lists) - 1+    cur = getCurrentList state+ selectList :: Char -> Stateful selectList idx state =-    Just $+    pure $     (if exists-         then current .~ (list, 0)+         then current .~ (ListIndex list, TaskIndex 0)          else id)         state   where@@ -227,37 +257,37 @@  -- removing delete :: Stateful-delete state = fixIndex . setList state . deleteTask (getIndex state) <$> getList state+delete state = fixIndex . setList state . L.deleteTask (getIndex state) <$> getList state  -- list and index countCurrent :: State -> Int countCurrent state = Lists.count (getCurrentList state) (state ^. lists)  setIndex :: State -> Int -> State-setIndex state idx = state & current .~ (getCurrentList state, idx)+setIndex state idx = state & current .~ (ListIndex (getCurrentList state), TaskIndex idx)  setCurrentList :: State -> Int -> State-setCurrentList state idx = state & current .~ (idx, getIndex state)+setCurrentList state idx = state & current .~ (ListIndex idx, TaskIndex (getIndex state))  getIndex :: State -> Int-getIndex = snd . (^. current)+getIndex = showTaskIndex . snd . (^. current) -changeTask :: (Int -> Maybe Text -> List -> Int) -> Stateful+changeTask :: (Int -> Maybe Text -> L.List -> Int) -> Stateful changeTask fn state = do     list <- getList state     let idx = getIndex state     let term = getText <$> state ^. searchTerm-    Just $ setIndex state (fn idx term list)+    pure $ setIndex state (fn idx term list)  next :: Stateful-next = changeTask nextTask+next = changeTask L.nextTask  previous :: Stateful-previous = changeTask prevTask+previous = changeTask L.prevTask  left :: Stateful left state =-    Just . fixIndex . setCurrentList state $+    pure . fixIndex . setCurrentList state $     if list > 0         then pred list         else 0@@ -266,7 +296,7 @@  right :: Stateful right state =-    Just . fixIndex . setCurrentList state $+    pure . fixIndex . setCurrentList state $     if list < (count - 1)         then succ list         else list@@ -274,41 +304,52 @@     list = getCurrentList state     count = length (state ^. lists) +fixListIndex :: InternalStateful+fixListIndex state =+    if listIdx+        then state+        else setCurrentList state (length lists' - 1)+  where+    lists' = state ^. lists+    listIdx = Lists.exists (getCurrentList state) lists'+ fixIndex :: InternalStateful fixIndex state =     case getList state of-        Just list -> setIndex state (nearest idx trm list)-        Nothing   -> state+        Just list -> setIndex state (L.nearest idx trm list)+        Nothing   -> fixListIndex state   where     trm = getText <$> state ^. searchTerm     idx = getIndex state  -- tasks getCurrentList :: State -> Int-getCurrentList = fst . (^. current)+getCurrentList = showListIndex . fst . (^. current) -getList :: State -> Maybe List+getList :: State -> Maybe L.List getList state = Lists.get (state ^. lists) (getCurrentList state) -setList :: State -> List -> State+setList :: State -> L.List -> State setList state list = setLists state (Lists.updateLists (getCurrentList state) list (state ^. lists))  setCurrentListTitle :: Text -> Stateful-setCurrentListTitle text state = setList state . (title .~ text) <$> getList state+setCurrentListTitle text state = setList state . (L.title .~ text) <$> getList state  setLists :: State -> Lists.Lists -> State setLists state lists' = state & lists .~ lists' -moveTo :: Char -> Stateful-moveTo char state = do-    let li = ord char - ord 'a'-        cur = getCurrentList state+moveTo' :: Int -> Stateful+moveTo' li state = do+    let cur = getCurrentList state     if li == cur || li < 0 || li >= length (state ^. lists)         then Nothing         else do-            s <- move' (li - cur) state+            s <- moveHorizontal (li - cur) state             pure . selectLast $ setCurrentList s li +moveTo :: Char -> Stateful+moveTo char = moveTo' (ord char - ord 'a')+ -- move lists listMove :: Int -> Stateful listMove offset state = do@@ -330,7 +371,7 @@ searchMode :: Stateful searchMode state = pure . fixIndex $ (state & mode .~ Search) & searchTerm .~ sTerm   where-    sTerm = Just (fromMaybe blankField (state ^. searchTerm))+    sTerm = pure (fromMaybe blankField (state ^. searchTerm))  clearSearch :: Stateful clearSearch state = pure $ state & searchTerm .~ Nothing@@ -338,18 +379,18 @@ appendSearch :: (Field -> Field) -> Stateful appendSearch genField state = do     let field = fromMaybe blankField (state ^. searchTerm)-    pure . fixIndex $ state & searchTerm .~ Just (genField field)+    pure . fixIndex $ state & searchTerm .~ pure (genField field)  -- help showHelp :: Stateful-showHelp = Just . (mode .~ Modal Help)+showHelp = pure . (mode .~ Modal Help)  showMoveTo :: Stateful-showMoveTo = Just . (mode .~ Modal MoveTo)+showMoveTo state = const (state & mode .~ Modal MoveTo) <$> getCurrentTask state  -- view setHeight :: Int -> State -> State-setHeight i = height .~ i+setHeight = (.~) height  -- more view - maybe shouldn't be in here... newList :: State -> State
src/Events/State/Modal/Detail.hs view
@@ -34,7 +34,7 @@ import           Events.State.Types      (State, Stateful, mode) import           Events.State.Types.Mode (DetailItem (..), DetailMode (..), ModalType (Detail),                                           Mode (Modal))-import           UI.Field                (Field, blankField, getText, textToField)+import           UI.Draw.Field           (Field, blankField, getText, textToField)  updateField :: (Field -> Field) -> Stateful updateField fieldEvent s =@@ -159,4 +159,4 @@             | i > lst = lst             | i < 0 = 0             | otherwise = i-    pure $ state & mode .~ Modal (Detail (DetailItem newIndex) m)+    return $ state & mode .~ Modal (Detail (DetailItem newIndex) m)
+ src/Events/State/Modal/Due.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE NoImplicitPrelude #-}++module Events.State.Modal.Due+    ( showDue+    , clearDate+    , previous+    , next+    , goto+    ) where++import ClassyPrelude++import Control.Lens  ((&), (.~), (^.))+import Data.Sequence ((!?))++import qualified Data.Taskell.Lists      as L (clearDue, due)+import           Data.Taskell.Seq        (bound)+import           Data.Taskell.Task       (Task)+import           Events.State.Types      (Stateful, current, lists, mode)+import           Events.State.Types.Mode (ModalType (Due), Mode (..))+import           Types                   (Pointer)++type DueStateful = Seq (Pointer, Task) -> Int -> Stateful++λfilter :: DueStateful -> Stateful+λfilter fn state =+    case state ^. mode of+        Modal (Due due cur) -> fn due cur state+        _                   -> pure state++λsetMode :: DueStateful+λsetMode due pos state = pure $ state & mode .~ Modal (Due due pos)++λprevious :: DueStateful+λprevious due cur = λsetMode due (bound due (cur - 1))++λnext :: DueStateful+λnext due cur = λsetMode due (bound due (cur + 1))++λgoto :: DueStateful+λgoto due cur state =+    case due !? cur of+        Just (pointer, _) -> pure $ state & current .~ pointer+        Nothing           -> Nothing++λclearDate :: DueStateful+λclearDate due cur state =+    case due !? cur of+        Just (pointer, _) -> do+            let new = L.clearDue pointer (state ^. lists)+            let dues = L.due new+            λsetMode dues (bound dues cur) (state & lists .~ new)+        Nothing -> Nothing++showDue :: Stateful+showDue state = λsetMode (L.due (state ^. lists)) 0 state++previous :: Stateful+previous = λfilter λprevious++next :: Stateful+next = λfilter λnext++goto :: Stateful+goto = λfilter λgoto++clearDate :: Stateful+clearDate = λfilter λclearDate
src/Events/State/Types.hs view
@@ -8,12 +8,11 @@ import Control.Lens (makeLenses)  import Data.Taskell.Lists (Lists)-import UI.Field           (Field)+import Types              (Pointer)+import UI.Draw.Field      (Field)  import qualified Events.State.Types.Mode as M (Mode) -type Pointer = (Int, Int)- data State = State     { _mode       :: M.Mode     , _lists      :: Lists@@ -23,6 +22,7 @@     , _io         :: Maybe Lists     , _height     :: Int     , _searchTerm :: Maybe Field+    , _time       :: UTCTime     } deriving (Eq, Show)  -- create lenses
src/Events/State/Types/Mode.hs view
@@ -4,7 +4,9 @@  import ClassyPrelude -import UI.Field (Field)+import Data.Taskell.Task (Task)+import Types             (Pointer)+import UI.Draw.Field     (Field)  data DetailMode     = DetailNormal@@ -20,6 +22,8 @@ data ModalType     = Help     | MoveTo+    | Due (Seq (Pointer, Task))+          Int     | Detail DetailItem              DetailMode     deriving (Eq, Show)
src/IO/Config.hs view
@@ -36,6 +36,9 @@     , github   :: GitHub.Config     } +debugging :: Config -> Bool+debugging config = General.debug (general config)+ defaultConfig :: Config defaultConfig =     Config
src/IO/Config/General.hs view
@@ -8,10 +8,11 @@  data Config = Config     { filename :: FilePath+    , debug    :: Bool     }  defaultConfig :: Config-defaultConfig = Config {filename = "taskell.md"}+defaultConfig = Config {filename = "taskell.md", debug = False}  parser :: IniParser Config parser =@@ -20,4 +21,5 @@         "general"         (do filenameCf <-                 maybe (filename defaultConfig) unpack . (noEmpty =<<) <$> fieldMb "filename"-            pure Config {filename = filenameCf})+            debugCf <- fieldFlagDef "debug" False+            pure Config {filename = filenameCf, debug = debugCf})
src/IO/Keyboard.hs view
@@ -43,6 +43,7 @@     [ (BChar 'q', A.Quit)     , (BChar 'u', A.Undo)     , (BChar '/', A.Search)+    , (BChar '!', A.Due)     , (BChar '?', A.Help)     , (BChar 'k', A.Previous)     , (BChar 'j', A.Next)@@ -52,6 +53,7 @@     , (BChar 'a', A.New)     , (BChar 'O', A.NewAbove)     , (BChar 'o', A.NewBelow)+    , (BChar '+', A.Duplicate)     , (BChar 'e', A.Edit)     , (BChar 'A', A.Edit)     , (BChar 'i', A.Edit)@@ -59,11 +61,12 @@     , (BChar 'D', A.Delete)     , (BKey "Enter", A.Detail)     , (BChar '@', A.DueDate)+    , (BKey "Backspace", A.ClearDate)     , (BChar 'K', A.MoveUp)     , (BChar 'J', A.MoveDown)     , (BChar 'H', A.MoveLeft)     , (BChar 'L', A.MoveRight)-    , (BKey "Space", A.MoveRight)+    , (BKey "Space", A.Complete)     , (BChar 'm', A.MoveMenu)     , (BChar 'N', A.ListNew)     , (BChar 'E', A.ListEdit)
src/IO/Markdown/Internal.hs view
@@ -8,7 +8,7 @@ import Control.Lens ((^.))  import Data.Sequence      (adjust')-import Data.Text          as T (dropAround, splitOn, strip)+import Data.Text          as T (splitOn, strip) import Data.Text.Encoding (decodeUtf8With)  import           Data.Taskell.Date    (dayToOutput)@@ -23,9 +23,6 @@                                      taskOutput, titleOutput)  -- parse code-trimTilde :: Text -> Text-trimTilde = strip . T.dropAround (== '~')- addSubItem :: Text -> Lists -> Lists addSubItem t ls = adjust' updateList i ls   where@@ -33,7 +30,6 @@     st         | "[ ] " `isPrefixOf` t = ST.new (drop 4 t) False         | "[x] " `isPrefixOf` t = ST.new (drop 4 t) True-        | "~" `isPrefixOf` t = ST.new (trimTilde t) True         | otherwise = ST.new t False     updateList l = updateFn j (T.addSubtask st) l       where
+ src/Types.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE NoImplicitPrelude #-}++module Types where++import ClassyPrelude++newtype ListIndex = ListIndex+    { showListIndex :: Int+    } deriving (Show, Eq, Ord)++newtype TaskIndex = TaskIndex+    { showTaskIndex :: Int+    } deriving (Show, Eq, Ord)++type Pointer = (ListIndex, TaskIndex)
src/UI/Draw.hs view
@@ -1,6 +1,4 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE LambdaCase #-}  module UI.Draw     ( draw@@ -12,272 +10,26 @@ import Control.Lens ((^.))  import Control.Monad.Reader (runReader)-import Data.Char            (chr, ord)-import Data.Sequence        (mapWithIndex)  import Brick -import           Data.Taskell.Date       (Day, dayToText, deadline)-import           Data.Taskell.List       (List, tasks, title)-import           Data.Taskell.Lists      (Lists, count)-import qualified Data.Taskell.Task       as T (Task, contains, countCompleteSubtasks, countSubtasks,-                                               description, due, hasSubtasks, name)-import           Events.State            (normalise)-import           Events.State.Types      (Pointer, State, current, height, lists, mode, path,-                                          searchTerm)-import           Events.State.Types.Mode (DetailMode (..), InsertType (..), ModalType (..),-                                          Mode (..))-import           IO.Config.Layout        (Config, columnPadding, columnWidth, descriptionIndicator)-import           IO.Keyboard.Types       (Bindings)-import           UI.Field                (Field, field, getText, textField, widgetFromMaybe)-import           UI.Modal                (showModal)-import           UI.Theme-import           UI.Types                (ListIndex (..), ResourceName (..), TaskIndex (..))---- | Draw needs to know various pieces of information, so keep track of them in a record-data DrawState = DrawState-    { dsLists        :: Lists-    , dsMode         :: Mode-    , dsLayout       :: Config-    , dsPath         :: FilePath-    , dsToday        :: Day-    , dsCurrent      :: Pointer-    , dsField        :: Maybe Field-    , dsEditingTitle :: Bool-    , dsSearchTerm   :: Maybe Field-    }---- | Use a Reader to pass around DrawState-type ReaderDrawState = ReaderT DrawState Identity---- | Takes a task's 'due' property and renders a date with appropriate styling (e.g. red if overdue)-renderDate :: Maybe Day -> ReaderDrawState (Maybe (Widget ResourceName))-renderDate dueDay = do-    today <- dsToday <$> ask -- get the value of `today` from DrawState-    let attr = withAttr . dlToAttr . deadline today <$> dueDay -- create a `Maybe (Widget -> Widget)` attribute function-        widget = txt . dayToText today <$> dueDay -- get the formatted due date `Maybe Text`-    pure $ attr <*> widget---- | Renders the appropriate completed sub task count e.g. "[2/3]"-renderSubtaskCount :: T.Task -> Widget ResourceName-renderSubtaskCount task =-    txt $ concat ["[", tshow $ T.countCompleteSubtasks task, "/", tshow $ T.countSubtasks task, "]"]---- | Renders the appropriate indicators: description, sub task count, and due date-indicators :: T.Task -> ReaderDrawState (Widget ResourceName)-indicators task = do-    dateWidget <- renderDate (task ^. T.due) -- get the due date widget-    descIndicator <- descriptionIndicator . dsLayout <$> ask-    pure . hBox $-        padRight (Pad 1) <$>-        catMaybes-            [ const (txt descIndicator) <$> task ^. T.description -- show the description indicator if one is set-            , bool Nothing (Just (renderSubtaskCount task)) (T.hasSubtasks task) -- if it has subtasks, render the sub task count-            , dateWidget-            ]---- | Renders an individual task-renderTask' :: Int -> Int -> T.Task -> ReaderDrawState (Widget ResourceName)-renderTask' listIndex taskIndex task = do-    eTitle <- dsEditingTitle <$> ask -- is the title being edited? (for visibility)-    selected <- (== (listIndex, taskIndex)) . dsCurrent <$> ask -- is the current task selected?-    taskField <- dsField <$> ask -- get the field, if it's being edited-    after <- indicators task -- get the indicators widget-    let text = task ^. T.name-        name = RNTask (ListIndex listIndex, TaskIndex taskIndex)-        widget = textField text-        widget' = widgetFromMaybe widget taskField-    pure $-        cached name .-        (if selected && not eTitle-             then visible-             else id) .-        padBottom (Pad 1) .-        (<=> withAttr disabledAttr after) .-        withAttr-            (if selected-                 then taskCurrentAttr-                 else taskAttr) $-        if selected && not eTitle-            then widget'-            else widget--renderTask :: Int -> Int -> T.Task -> ReaderDrawState (Widget ResourceName)-renderTask listIndex taskIndex task = do-    searchT <- asks dsSearchTerm-    case searchT of-        Nothing -> renderTask' listIndex taskIndex task-        Just term ->-            if T.contains (getText term) task-                then renderTask' listIndex taskIndex task-                else pure emptyWidget---- | Gets the relevant column prefix - number in normal mode, letter in moveTo-columnPrefix :: Int -> Int -> ReaderDrawState Text-columnPrefix selectedList i = do-    m <- dsMode <$> ask-    if moveTo m-        then do-            let col = chr (i + ord 'a')-            pure $-                if i /= selectedList && i >= 0 && i <= 26-                    then singleton col <> ". "-                    else ""-        else do-            let col = i + 1-            pure $-                if col >= 1 && col <= 9-                    then tshow col <> ". "-                    else ""---- | Renders the title for a list-renderTitle :: Int -> List -> ReaderDrawState (Widget ResourceName)-renderTitle listIndex list = do-    (selectedList, selectedTask) <- dsCurrent <$> ask-    editing <- (selectedList == listIndex &&) . dsEditingTitle <$> ask-    titleField <- dsField <$> ask-    col <- txt <$> columnPrefix selectedList listIndex-    let text = list ^. title-        attr =-            if selectedList == listIndex-                then titleCurrentAttr-                else titleAttr-        widget = textField text-        widget' = widgetFromMaybe widget titleField-        title' =-            padBottom (Pad 1) . withAttr attr . (col <+>) $-            if editing-                then widget'-                else widget-    pure $-        if editing || selectedList /= listIndex || selectedTask == 0-            then visible title'-            else title'---- | Renders a list-renderList :: Int -> List -> ReaderDrawState (Widget ResourceName)-renderList listIndex list = do-    layout <- dsLayout <$> ask-    eTitle <- dsEditingTitle <$> ask-    titleWidget <- renderTitle listIndex list-    (currentList, _) <- dsCurrent <$> ask-    taskWidgets <- sequence $ renderTask listIndex `mapWithIndex` (list ^. tasks)-    let widget =-            (if not eTitle-                 then cached (RNList listIndex)-                 else id) .-            padLeftRight (columnPadding layout) .-            hLimit (columnWidth layout) .-            viewport (RNList listIndex) Vertical . vBox . (titleWidget :) $-            toList taskWidgets-    pure $-        if currentList == listIndex-            then visible widget-            else widget---- | Renders the search area-renderSearch :: Widget ResourceName -> ReaderDrawState (Widget ResourceName)-renderSearch mainWidget = do-    m <- asks dsMode-    term <- asks dsSearchTerm-    case term of-        Just searchField -> do-            colPad <- columnPadding . dsLayout <$> ask-            let attr =-                    withAttr $-                    case m of-                        Search -> taskCurrentAttr-                        _      -> taskAttr-            let widget = attr . padLeftRight colPad $ txt "/" <+> field searchField-            pure $ mainWidget <=> widget-        _ -> pure mainWidget---- | Render the status bar-getPosition :: ReaderDrawState Text-getPosition = do-    (col, pos) <- asks dsCurrent-    len <- count col <$> asks dsLists-    let posNorm =-            if len > 0-                then pos + 1-                else 0-    pure $ tshow posNorm <> "/" <> tshow len--modeToText :: Maybe Field -> Mode -> Text-modeToText fld =-    \case-        Normal ->-            case fld of-                Nothing -> "NORMAL"-                Just _  -> "NORMAL + SEARCH"-        Insert {} -> "INSERT"-        Modal Help -> "HELP"-        Modal MoveTo -> "MOVE"-        Modal Detail {} -> "DETAIL"-        Search {} -> "SEARCH"-        _ -> ""--getMode :: ReaderDrawState Text-getMode = modeToText <$> asks dsSearchTerm <*> asks dsMode--renderStatusBar :: ReaderDrawState (Widget ResourceName)-renderStatusBar = do-    topPath <- pack <$> asks dsPath-    colPad <- columnPadding <$> asks dsLayout-    posTxt <- getPosition-    modeTxt <- getMode-    let titl = padLeftRight colPad $ txt topPath-    let pos = padRight (Pad colPad) $ txt posTxt-    let md = txt modeTxt-    let bar = padRight Max (titl <+> md) <+> pos-    pure . padTop (Pad 1) $ withAttr statusBarAttr bar---- | Renders the main widget-main :: ReaderDrawState (Widget ResourceName)-main = do-    ls <- dsLists <$> ask-    listWidgets <- toList <$> sequence (renderList `mapWithIndex` ls)-    let mainWidget = viewport RNLists Horizontal . padTopBottom 1 $ hBox listWidgets-    statusBar <- renderStatusBar-    renderSearch (mainWidget <=> statusBar)--getField :: Mode -> Maybe Field-getField (Insert _ _ f) = Just f-getField _              = Nothing--editingTitle :: Mode -> Bool-editingTitle (Insert IList _ _) = True-editingTitle _                  = False--moveTo :: Mode -> Bool-moveTo (Modal MoveTo) = True-moveTo _              = False+import Events.State            (normalise)+import Events.State.Types      (State, mode)+import Events.State.Types.Mode (DetailMode (..), ModalType (..), Mode (..))+import IO.Config.Layout        (Config)+import IO.Keyboard.Types       (Bindings)+import UI.Draw.Main            (renderMain)+import UI.Draw.Modal           (renderModal)+import UI.Draw.Types           (DrawState (DrawState), ReaderDrawState, TWidget)+import UI.Types                (ResourceName (..))  -- draw-drawR :: Int -> State -> Bindings -> ReaderDrawState [Widget ResourceName]-drawR ht normalisedState bindings = do-    modal <- showModal ht bindings normalisedState <$> asks dsToday-    mn <- main-    pure [modal, mn]+renderApp :: ReaderDrawState [TWidget]+renderApp = sequence [renderModal, renderMain] -draw :: Config -> Bindings -> Day -> State -> [Widget ResourceName]-draw layout bindings today state = runReader (drawR ht normalisedState bindings) drawState-  where-    normalisedState = normalise state-    stateMode = state ^. mode-    ht = state ^. height-    drawState =-        DrawState-        { dsLists = normalisedState ^. lists-        , dsMode = stateMode-        , dsLayout = layout-        , dsPath = normalisedState ^. path-        , dsToday = today-        , dsField = getField stateMode-        , dsCurrent = normalisedState ^. current-        , dsEditingTitle = editingTitle stateMode-        , dsSearchTerm = normalisedState ^. searchTerm-        }+draw :: Config -> Bindings -> Bool -> State -> [TWidget]+draw layout bindings debug state =+    runReader renderApp (DrawState layout bindings debug (normalise state))  -- cursors chooseCursor :: State -> [CursorLocation ResourceName] -> Maybe (CursorLocation ResourceName)
+ src/UI/Draw/Field.hs view
@@ -0,0 +1,146 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module UI.Draw.Field where++import ClassyPrelude++import qualified Data.List as L (scanl1)+import qualified Data.Text as T (splitAt, takeEnd)++import qualified Brick                     as B (Location (Location), Size (Fixed), Widget (Widget),+                                                 availWidth, getContext, render, showCursor, txt,+                                                 vBox)+import qualified Brick.Widgets.Core        as B (textWidth)+import qualified Graphics.Text.Width       as V (safeWcwidth)+import qualified Graphics.Vty.Input.Events as V (Event (..), Key (..))++import qualified UI.Types as UI (ResourceName (RNCursor))++data Field = Field+    { _text   :: Text+    , _cursor :: Int+    } deriving (Eq, Show)++blankField :: Field+blankField = Field "" 0++event :: V.Event -> Field -> Field+event (V.EvKey (V.KChar '\t') _) f = f+event (V.EvPaste bs) f             = insertText (decodeUtf8 bs) f+event (V.EvKey V.KBS _) f          = backspace f+event (V.EvKey V.KLeft _) f        = updateCursor (-1) f+event (V.EvKey V.KRight _) f       = updateCursor 1 f+event (V.EvKey (V.KChar char) _) f = insertCharacter char f+event _ f                          = f++updateCursor :: Int -> Field -> Field+updateCursor dir (Field text cursor) = Field text newCursor+  where+    next = cursor + dir+    limit = length text+    newCursor+        | next <= 0 = 0+        | next > limit = limit+        | otherwise = next++backspace :: Field -> Field+backspace (Field text cursor) =+    let (start, end) = T.splitAt cursor text+    in case fromNullable start of+           Nothing     -> Field end cursor+           Just start' -> Field (init start' <> end) (cursor - 1)++insertCharacter :: Char -> Field -> Field+insertCharacter char (Field text cursor) = Field newText newCursor+  where+    (start, end) = T.splitAt cursor text+    newText = snoc start char <> end+    newCursor = cursor + 1++insertText :: Text -> Field -> Field+insertText insert (Field text cursor) = Field newText newCursor+  where+    (start, end) = T.splitAt cursor text+    newText = concat [start, insert, end]+    newCursor = cursor + length insert++widthFold :: [Int] -> Char -> [Int]+widthFold l t = l <> [V.safeWcwidth t]++cursorPosition :: [Text] -> Int -> Int -> (Int, Int)+cursorPosition lns width cursor =+    if x == width+        then (0, y + 1) -- go to next line if at end of line+        else (x, y)+  where+    parts = foldl' widthFold [] <$> lns -- list of list of individual character lengths+    lengths = length <$> parts -- list of line lengths+    cumulative = L.scanl1 (+) lengths -- cumulative total of line lengths+    above = takeWhile (< cursor) cumulative -- lines above the cursor+    y = length above -- number of lines above+    adjustedCursor = cursor - fromMaybe 0 (lastMay above) -- subtract lines above from cursor position+    cumulativeWidths = L.scanl1 (+) <$> index parts y -- get cumulative widths for current line+    x = fromMaybe 0 $ flip index (adjustedCursor - 1) =<< cumulativeWidths++getText :: Field -> Text+getText (Field text _) = text++textToField :: Text -> Field+textToField text = Field text (length text)++field :: Field -> B.Widget UI.ResourceName+field (Field text cursor) =+    B.Widget B.Fixed B.Fixed $ do+        width <- B.availWidth <$> B.getContext+        let (wrapped, offset) = wrap width text+            location = cursorPosition wrapped width (cursor - offset)+        B.render $+            if null text+                then B.showCursor UI.RNCursor (B.Location (0, 0)) $ B.txt " "+                else B.showCursor UI.RNCursor (B.Location location) . B.vBox $ B.txt <$> wrapped++widgetFromMaybe :: B.Widget UI.ResourceName -> Maybe Field -> B.Widget UI.ResourceName+widgetFromMaybe _ (Just f) = field f+widgetFromMaybe w Nothing  = w++textField :: Text -> B.Widget UI.ResourceName+textField text =+    B.Widget B.Fixed B.Fixed $ do+        width <- B.availWidth <$> B.getContext+        let (wrapped, _) = wrap width text+        B.render $+            if null text+                then B.txt "---"+                else B.vBox $ B.txt <$> wrapped++-- wrap+wrap :: Int -> Text -> ([Text], Int)+wrap width = foldl' (combine width) ([""], 0) . spl++spl' :: [Text] -> Char -> [Text]+spl' ts c+    | c == ' ' = ts <> [" "] <> [""]+    | otherwise =+        case fromNullable ts of+            Just ts' -> init ts' <> [snoc (last ts') c]+            Nothing  -> [singleton c]++spl :: Text -> [Text]+spl = foldl' spl' [""]++combine :: Int -> ([Text], Int) -> Text -> ([Text], Int)+combine width (acc, offset) s+    | newline && s == " " = (acc, offset + 1)+    | T.takeEnd 1 l == " " && s == " " = (acc, offset + 1)+    | newline = (acc <> [s], offset)+    | otherwise = (append (l <> s) acc, offset)+  where+    l = maybe "" last (fromNullable acc)+    newline = B.textWidth l + B.textWidth s > width++append :: Text -> [Text] -> [Text]+append s l =+    case fromNullable l of+        Just l' -> init l' <> [s]+        Nothing -> l <> [s]
+ src/UI/Draw/Main.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE NoImplicitPrelude #-}++module UI.Draw.Main+    ( renderMain+    ) where++import ClassyPrelude++import Brick+import Control.Lens  ((^.))+import Data.Sequence (mapWithIndex)++import Events.State.Types     (lists)+import UI.Draw.Main.List      (renderList)+import UI.Draw.Main.Search    (renderSearch)+import UI.Draw.Main.StatusBar (renderStatusBar)+import UI.Draw.Types          (DSWidget, DrawState (..))+import UI.Types               (ResourceName (..))++renderMain :: DSWidget+renderMain = do+    ls <- (^. lists) <$> asks dsState+    listWidgets <- toList <$> sequence (renderList `mapWithIndex` ls)+    let mainWidget = viewport RNLists Horizontal . padTopBottom 1 $ hBox listWidgets+    statusBar <- renderStatusBar+    renderSearch (mainWidget <=> statusBar)
+ src/UI/Draw/Main/List.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module UI.Draw.Main.List+    ( renderList+    ) where++import ClassyPrelude++import Control.Lens ((^.))++import Data.Char     (chr, ord)+import Data.Sequence (mapWithIndex)++import Brick++import Data.Taskell.List  (List, tasks, title)+import Events.State.Types (current, mode)+import IO.Config.Layout   (columnPadding, columnWidth)+import Types              (ListIndex (ListIndex), TaskIndex (TaskIndex))+import UI.Draw.Field      (textField, widgetFromMaybe)+import UI.Draw.Mode+import UI.Draw.Task       (renderTask)+import UI.Draw.Types      (DSWidget, DrawState (..), ReaderDrawState)+import UI.Theme+import UI.Types           (ResourceName (..))++-- | Gets the relevant column prefix - number in normal mode, letter in moveTo+columnPrefix :: Int -> Int -> ReaderDrawState Text+columnPrefix selectedList i = do+    m <- (^. mode) <$> asks dsState+    if moveTo m+        then do+            let col = chr (i + ord 'a')+            pure $+                if i /= selectedList && i >= 0 && i <= 26+                    then singleton col <> ". "+                    else ""+        else do+            let col = i + 1+            pure $+                if col >= 1 && col <= 9+                    then tshow col <> ". "+                    else ""++-- | Renders the title for a list+renderTitle :: Int -> List -> DSWidget+renderTitle listIndex list = do+    (ListIndex selectedList, TaskIndex selectedTask) <- (^. current) <$> asks dsState+    editing <- (selectedList == listIndex &&) . editingTitle . (^. mode) <$> asks dsState+    titleField <- getField . (^. mode) <$> asks dsState+    col <- txt <$> columnPrefix selectedList listIndex+    let text = list ^. title+        attr =+            if selectedList == listIndex+                then titleCurrentAttr+                else titleAttr+        widget = textField text+        widget' = widgetFromMaybe widget titleField+        title' =+            padBottom (Pad 1) . withAttr attr . (col <+>) $+            if editing+                then widget'+                else widget+    pure $+        if editing || selectedList /= listIndex || selectedTask == 0+            then visible title'+            else title'++-- | Renders a list+renderList :: Int -> List -> DSWidget+renderList listIndex list = do+    layout <- dsLayout <$> ask+    eTitle <- editingTitle . (^. mode) <$> asks dsState+    titleWidget <- renderTitle listIndex list+    (ListIndex currentList, _) <- (^. current) <$> asks dsState+    taskWidgets <-+        sequence $+        renderTask (RNTask . (ListIndex listIndex, ) . TaskIndex) listIndex `mapWithIndex`+        (list ^. tasks)+    let widget =+            (if not eTitle+                 then cached (RNList listIndex)+                 else id) .+            padLeftRight (columnPadding layout) .+            hLimit (columnWidth layout) .+            viewport (RNList listIndex) Vertical . vBox . (titleWidget :) $+            toList taskWidgets+    pure $+        if currentList == listIndex+            then visible widget+            else widget
+ src/UI/Draw/Main/Search.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module UI.Draw.Main.Search+    ( renderSearch+    ) where++import Brick+import ClassyPrelude+import Control.Lens  ((^.))++import Events.State.Types      (mode, searchTerm)+import Events.State.Types.Mode (Mode (..))+import IO.Config.Layout        (columnPadding)+import UI.Draw.Field           (field)+import UI.Draw.Types           (DSWidget, DrawState (..))+import UI.Theme+import UI.Types                (ResourceName (..))++renderSearch :: Widget ResourceName -> DSWidget+renderSearch mainWidget = do+    m <- (^. mode) <$> asks dsState+    term <- (^. searchTerm) <$> asks dsState+    case term of+        Just searchField -> do+            colPad <- columnPadding . dsLayout <$> ask+            let attr =+                    withAttr $+                    case m of+                        Search -> taskCurrentAttr+                        _      -> taskAttr+            let widget = attr . padLeftRight colPad $ txt "/" <+> field searchField+            pure $ mainWidget <=> widget+        _ -> pure mainWidget
+ src/UI/Draw/Main/StatusBar.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE LambdaCase #-}++module UI.Draw.Main.StatusBar+    ( renderStatusBar+    ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick++import Data.Taskell.Lists (count)+import Events.State.Types (current, lists, mode, path, searchTerm)++import Events.State.Types.Mode (ModalType (..), Mode (..))+import IO.Config.Layout        (columnPadding)+import Types                   (ListIndex (ListIndex), TaskIndex (TaskIndex))+import UI.Draw.Field           (Field)+import UI.Draw.Types           (DSWidget, DrawState (..), ReaderDrawState)+import UI.Theme++getPosition :: ReaderDrawState Text+getPosition = do+    (ListIndex col, TaskIndex pos) <- (^. current) <$> asks dsState+    len <- count col . (^. lists) <$> asks dsState+    let posNorm =+            if len > 0+                then pos + 1+                else 0+    pure $ tshow posNorm <> "/" <> tshow len++modeToText :: Maybe Field -> Mode -> ReaderDrawState Text+modeToText fld md = do+    debug <- asks dsDebug+    pure $+        if debug+            then tshow md+            else case md of+                     Normal ->+                         case fld of+                             Nothing -> "NORMAL"+                             Just _  -> "NORMAL + SEARCH"+                     Insert {} -> "INSERT"+                     Modal Help -> "HELP"+                     Modal MoveTo -> "MOVE"+                     Modal Detail {} -> "DETAIL"+                     Modal Due {} -> "DUE"+                     Search {} -> "SEARCH"+                     _ -> ""++getMode :: ReaderDrawState Text+getMode = do+    state <- asks dsState+    modeToText (state ^. searchTerm) (state ^. mode)++renderStatusBar :: DSWidget+renderStatusBar = do+    topPath <- pack . (^. path) <$> asks dsState+    colPad <- columnPadding <$> asks dsLayout+    posTxt <- getPosition+    modeTxt <- getMode+    let titl = padLeftRight colPad $ txt topPath+    let pos = padRight (Pad colPad) $ txt posTxt+    let md = txt modeTxt+    let bar = padRight Max (titl <+> md) <+> pos+    pure . padTop (Pad 1) $ withAttr statusBarAttr bar
+ src/UI/Draw/Modal.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE NoImplicitPrelude #-}++module UI.Draw.Modal+    ( renderModal+    ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick+import Brick.Widgets.Border+import Brick.Widgets.Center++import Events.State.Types      (height, mode)+import Events.State.Types.Mode (ModalType (..), Mode (..))+import UI.Draw.Field           (textField)+import UI.Draw.Modal.Detail    (detail)+import UI.Draw.Modal.Due       (due)+import UI.Draw.Modal.Help      (help)+import UI.Draw.Modal.MoveTo    (moveTo)+import UI.Draw.Types           (DSWidget, DrawState (dsState), TWidget)+import UI.Theme                (titleAttr)+import UI.Types                (ResourceName (..))++surround :: (Text, TWidget) -> DSWidget+surround (title, widget) = do+    ht <- (^. height) <$> asks dsState+    let t = padBottom (Pad 1) . withAttr titleAttr $ textField title+    pure .+        padTopBottom 1 .+        centerLayer .+        border .+        padTopBottom 1 .+        padLeftRight 4 . vLimit (ht - 9) . hLimit 50 . (t <=>) . viewport RNModal Vertical $+        widget++renderModal :: DSWidget+renderModal = do+    md <- (^. mode) <$> asks dsState+    case md of+        Modal Help                 -> surround =<< help+        Modal Detail {}            -> surround =<< detail+        Modal MoveTo               -> surround =<< moveTo+        Modal (Due tasks selected) -> surround =<< due tasks selected+        _                          -> pure emptyWidget
+ src/UI/Draw/Modal/Detail.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module UI.Draw.Modal.Detail+    ( detail+    ) where++import ClassyPrelude++import Control.Lens ((^.))++import Data.Sequence (mapWithIndex)++import Brick++import           Data.Taskell.Date         (Day, dayToOutput, deadline)+import qualified Data.Taskell.Subtask      as ST (Subtask, complete, name)+import           Data.Taskell.Task         (Task, description, due, name, subtasks)+import           Events.State              (getCurrentTask)+import           Events.State.Modal.Detail (getCurrentItem, getField)+import           Events.State.Types        (time)+import           Events.State.Types.Mode   (DetailItem (..))+import           UI.Draw.Field             (Field, textField, widgetFromMaybe)+import           UI.Draw.Types             (DrawState (..), ModalWidget, TWidget)+import           UI.Theme                  (disabledAttr, dlToAttr, taskCurrentAttr,+                                            titleCurrentAttr)++renderSubtask :: Maybe Field -> DetailItem -> Int -> ST.Subtask -> TWidget+renderSubtask f current i subtask = padBottom (Pad 1) $ prefix <+> final+  where+    cur =+        case current of+            DetailItem c -> i == c+            _            -> False+    done = subtask ^. ST.complete+    attr =+        withAttr+            (if cur+                 then taskCurrentAttr+                 else titleCurrentAttr)+    prefix =+        attr . txt $+        if done+            then "[x] "+            else "[ ] "+    widget = textField (subtask ^. ST.name)+    final+        | cur = visible . attr $ widgetFromMaybe widget f+        | not done = attr widget+        | otherwise = widget++renderSummary :: Maybe Field -> DetailItem -> Task -> TWidget+renderSummary f i task = padTop (Pad 1) $ padBottom (Pad 2) w'+  where+    w = textField $ fromMaybe "No description" (task ^. description)+    w' =+        case i of+            DetailDescription -> visible $ widgetFromMaybe w f+            _                 -> w++renderDate :: Day -> Maybe Field -> DetailItem -> Task -> TWidget+renderDate today field item task =+    case item of+        DetailDate -> visible $ prefix <+> widgetFromMaybe widget field+        _ ->+            case day of+                Just d  -> prefix <+> withAttr (dlToAttr (deadline today d)) widget+                Nothing -> emptyWidget+  where+    day = task ^. due+    prefix = txt "Due: "+    widget = textField $ maybe "" dayToOutput day++detail :: ModalWidget+detail = do+    state <- asks dsState+    let today = utctDay (state ^. time)+    pure $+        fromMaybe ("Error", txt "Oops") $ do+            task <- getCurrentTask state+            i <- getCurrentItem state+            let f = getField state+            let sts = task ^. subtasks+                w+                    | null sts = withAttr disabledAttr $ txt "No sub-tasks"+                    | otherwise = vBox . toList $ renderSubtask f i `mapWithIndex` sts+            pure (task ^. name, renderDate today f i task <=> renderSummary f i task <=> w)
+ src/UI/Draw/Modal/Due.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module UI.Draw.Modal.Due+    ( due+    ) where++import ClassyPrelude++import Brick+import Data.Taskell.Seq ((<#>))++import qualified Data.Taskell.Task as T (Task)+import           Types             (Pointer)+import           UI.Draw.Task      (TaskWidget (..), parts)+import           UI.Draw.Types     (DSWidget, ModalWidget)+import           UI.Theme          (taskAttr, taskCurrentAttr)+import           UI.Types          (ResourceName (RNDue))++renderTask :: Int -> Int -> T.Task -> DSWidget+renderTask current position task = do+    (TaskWidget text date _ _) <- parts task+    let selected = current == position+    let attr =+            if selected+                then taskCurrentAttr+                else taskAttr+    let shw =+            if selected+                then visible+                else id+    pure . shw . cached (RNDue position) . padBottom (Pad 1) . withAttr attr $ vBox [date, text]++due :: Seq (Pointer, T.Task) -> Int -> ModalWidget+due tasks selected = do+    let items = snd <$> tasks+    widgets <- sequence $ renderTask selected <#> items+    pure+        ( "Due Tasks"+        , if null items+              then txt "No due tasks"+              else vBox $ toList widgets)
+ src/UI/Draw/Modal/Help.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module UI.Draw.Modal.Help+    ( help+    ) where++import ClassyPrelude++import Brick+import Data.Text as T (justifyRight)++import Events.Actions.Types as A (ActionType (..))+import IO.Keyboard.Types    (bindingsToText)++import IO.Keyboard.Types (Bindings)+import UI.Draw.Field     (textField)+import UI.Draw.Types     (DrawState (dsBindings), ModalWidget, TWidget)+import UI.Theme          (taskCurrentAttr)++descriptions :: [([ActionType], Text)]+descriptions =+    [ ([A.Help], "Show this list of controls")+    , ([A.Due], "Show tasks with due dates")+    , ([A.Previous, A.Next, A.Left, A.Right], "Move down / up / left / right")+    , ([A.Bottom], "Go to bottom of list")+    , ([A.New], "Add a task")+    , ([A.NewAbove, A.NewBelow], "Add a task above / below")+    , ([A.Duplicate], "Duplicate a task")+    , ([A.Edit], "Edit a task")+    , ([A.Clear], "Change task")+    , ([A.Detail], "Show task details / Edit task description")+    , ([A.DueDate], "Add/edit due date (yyyy-mm-dd)")+    , ([A.ClearDate], "Removes due date")+    , ([A.MoveUp, A.MoveDown], "Shift task down / up")+    , ([A.MoveLeft, A.MoveRight], "Shift task left / right")+    , ([A.Complete], "Move task to last list and remove any due dates")+    , ([A.MoveMenu], "Move task to specific list")+    , ([A.Delete], "Delete task")+    , ([A.Undo], "Undo")+    , ([A.ListNew], "New list")+    , ([A.ListEdit], "Edit list title")+    , ([A.ListDelete], "Delete list")+    , ([A.ListLeft, A.ListRight], "Move list left / right")+    , ([A.Search], "Search")+    , ([A.Quit], "Quit")+    ]++generate :: Bindings -> [([Text], Text)]+generate bindings = first (intercalate ", " . bindingsToText bindings <$>) <$> descriptions++format :: ([Text], Text) -> (Text, Text)+format = first (intercalate " / ")++line :: Int -> (Text, Text) -> TWidget+line m (l, r) = left <+> right+  where+    left = padRight (Pad 2) . withAttr taskCurrentAttr . txt $ justifyRight m ' ' l+    right = textField r++help :: ModalWidget+help = do+    bindings <- asks dsBindings+    let ls = format <$> generate bindings+    let m = foldl' max 0 $ length . fst <$> ls+    let w = vBox $ line m <$> ls+    pure ("Controls", w)
+ src/UI/Draw/Modal/MoveTo.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module UI.Draw.Modal.MoveTo+    ( moveTo+    ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick++import Data.Taskell.List  (title)+import Events.State.Types (current, lists)+import Types              (showListIndex)+import UI.Draw.Field      (textField)+import UI.Draw.Types      (DrawState (dsState), ModalWidget)+import UI.Theme           (taskCurrentAttr)++moveTo :: ModalWidget+moveTo = do+    skip <- showListIndex . fst . (^. current) <$> asks dsState+    ls <- toList . (^. lists) <$> asks dsState+    let titles = textField . (^. title) <$> ls+    let letter a =+            padRight (Pad 1) . hBox $+            [txt "[", withAttr taskCurrentAttr $ txt (singleton a), txt "]"]+    let letters = letter <$> ['a' ..]+    let remove i l = take i l <> drop (i + 1) l+    let output (l, t) = l <+> t+    let widget = vBox $ output <$> remove skip (zip letters titles)+    pure ("Move To:", widget)
+ src/UI/Draw/Mode.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE NoImplicitPrelude #-}++module UI.Draw.Mode where++import ClassyPrelude++import Events.State.Types.Mode (InsertType (..), ModalType (..), Mode (..))+import UI.Draw.Field           (Field)++getField :: Mode -> Maybe Field+getField (Insert _ _ f) = Just f+getField _              = Nothing++editingTitle :: Mode -> Bool+editingTitle (Insert IList _ _) = True+editingTitle _                  = False++moveTo :: Mode -> Bool+moveTo (Modal MoveTo) = True+moveTo _              = False
+ src/UI/Draw/Task.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module UI.Draw.Task+    ( TaskWidget(..)+    , renderTask+    , parts+    ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick++import           Data.Taskell.Date  (dayToText, deadline)+import qualified Data.Taskell.Task  as T (Task, contains, countCompleteSubtasks, countSubtasks,+                                          description, due, hasSubtasks, name)+import           Events.State.Types (current, mode, searchTerm, time)+import           IO.Config.Layout   (descriptionIndicator)+import           Types              (ListIndex (..), TaskIndex (..))+import           UI.Draw.Field      (getText, textField, widgetFromMaybe)+import           UI.Draw.Mode+import           UI.Draw.Types      (DSWidget, DrawState (..), ReaderDrawState, TWidget)+import           UI.Theme+import           UI.Types           (ResourceName)++data TaskWidget = TaskWidget+    { textW     :: TWidget+    , dateW     :: TWidget+    , summaryW  :: TWidget+    , subtasksW :: TWidget+    }++-- | Takes a task's 'due' property and renders a date with appropriate styling (e.g. red if overdue)+renderDate :: T.Task -> DSWidget+renderDate task = do+    today <- utctDay . (^. time) <$> asks dsState+    pure . fromMaybe emptyWidget $+        (\day -> withAttr (dlToAttr $ deadline today day) (txt $ dayToText today day)) <$>+        task ^. T.due++-- | Renders the appropriate completed sub task count e.g. "[2/3]"+renderSubtaskCount :: T.Task -> DSWidget+renderSubtaskCount task =+    pure . fromMaybe emptyWidget $ bool Nothing (Just indicator) (T.hasSubtasks task)+  where+    complete = tshow $ T.countCompleteSubtasks task+    total = tshow $ T.countSubtasks task+    indicator = txt $ concat ["[", complete, "/", total, "]"]++-- | Renders the description indicator+renderDescIndicator :: T.Task -> DSWidget+renderDescIndicator task = do+    indicator <- descriptionIndicator <$> asks dsLayout+    pure . fromMaybe emptyWidget $ const (txt indicator) <$> task ^. T.description -- show the description indicator if one is set++-- | Renders the task text+renderText :: T.Task -> DSWidget+renderText task = pure $ textField (task ^. T.name)++-- | Renders the appropriate indicators: description, sub task count, and due date+indicators :: T.Task -> DSWidget+indicators task = do+    widgets <- sequence (($ task) <$> [renderDescIndicator, renderSubtaskCount, renderDate])+    pure . hBox $ padRight (Pad 1) <$> widgets++-- | The individual parts of a task widget+parts :: T.Task -> ReaderDrawState TaskWidget+parts task =+    TaskWidget <$> renderText task <*> renderDate task <*> renderDescIndicator task <*>+    renderSubtaskCount task++-- | Renders an individual task+renderTask' :: (Int -> ResourceName) -> Int -> Int -> T.Task -> DSWidget+renderTask' rn listIndex taskIndex task = do+    eTitle <- editingTitle . (^. mode) <$> asks dsState -- is the title being edited? (for visibility)+    selected <- (== (ListIndex listIndex, TaskIndex taskIndex)) . (^. current) <$> asks dsState -- is the current task selected?+    taskField <- getField . (^. mode) <$> asks dsState -- get the field, if it's being edited+    after <- indicators task -- get the indicators widget+    widget <- renderText task+    let name = rn taskIndex+        widget' = widgetFromMaybe widget taskField+    pure $+        cached name .+        (if selected && not eTitle+             then visible+             else id) .+        padBottom (Pad 1) .+        (<=> withAttr disabledAttr after) .+        withAttr+            (if selected+                 then taskCurrentAttr+                 else taskAttr) $+        if selected && not eTitle+            then widget'+            else widget++renderTask :: (Int -> ResourceName) -> Int -> Int -> T.Task -> DSWidget+renderTask rn listIndex taskIndex task = do+    searchT <- (getText <$>) . (^. searchTerm) <$> asks dsState+    let taskWidget = renderTask' rn listIndex taskIndex task+    case searchT of+        Nothing -> taskWidget+        Just term ->+            if T.contains term task+                then taskWidget+                else pure emptyWidget
+ src/UI/Draw/Types.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE NoImplicitPrelude #-}++module UI.Draw.Types where++import ClassyPrelude++import Brick              (Widget)+import Events.State.Types (State)+import IO.Config.Layout   (Config)+import IO.Keyboard.Types  (Bindings)+import UI.Types           (ResourceName)++data DrawState = DrawState+    { dsLayout   :: Config+    , dsBindings :: Bindings+    , dsDebug    :: Bool+    , dsState    :: State+    }++-- | Use a Reader to pass around DrawState+type ReaderDrawState = ReaderT DrawState Identity++-- | Aliases for common combinations+type TWidget = Widget ResourceName++type ModalWidget = ReaderDrawState (Text, TWidget)++type DSWidget = ReaderDrawState TWidget
− src/UI/Field.hs
@@ -1,138 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.Field where--import ClassyPrelude--import qualified Data.List as L (scanl1)-import qualified Data.Text as T (splitAt, takeEnd)--import qualified Brick                     as B (Location (Location), Size (Fixed), Widget (Widget),-                                                 availWidth, getContext, render, showCursor, txt,-                                                 vBox)-import qualified Brick.Widgets.Core        as B (textWidth)-import qualified Graphics.Vty.Input.Events as V (Event (..), Key (..))--import qualified UI.Types as UI (ResourceName (RNCursor))--data Field = Field-    { _text   :: Text-    , _cursor :: Int-    } deriving (Eq, Show)--blankField :: Field-blankField = Field "" 0--event :: V.Event -> Field -> Field-event (V.EvKey (V.KChar '\t') _) f = f-event (V.EvPaste bs) f             = insertText (decodeUtf8 bs) f-event (V.EvKey V.KBS _) f          = backspace f-event (V.EvKey V.KLeft _) f        = updateCursor (-1) f-event (V.EvKey V.KRight _) f       = updateCursor 1 f-event (V.EvKey (V.KChar char) _) f = insertCharacter char f-event _ f                          = f--updateCursor :: Int -> Field -> Field-updateCursor dir (Field text cursor) = Field text newCursor-  where-    next = cursor + dir-    limit = length text-    newCursor-        | next <= 0 = 0-        | next > limit = limit-        | otherwise = next--backspace :: Field -> Field-backspace (Field text cursor) =-    let (start, end) = T.splitAt cursor text-    in case fromNullable start of-           Nothing     -> Field end cursor-           Just start' -> Field (init start' <> end) (cursor - 1)--insertCharacter :: Char -> Field -> Field-insertCharacter char (Field text cursor) = Field newText newCursor-  where-    (start, end) = T.splitAt cursor text-    newText = snoc start char <> end-    newCursor = cursor + 1--insertText :: Text -> Field -> Field-insertText insert (Field text cursor) = Field newText newCursor-  where-    (start, end) = T.splitAt cursor text-    newText = concat [start, insert, end]-    newCursor = cursor + length insert--cursorPosition :: [Text] -> Int -> Int -> (Int, Int)-cursorPosition text width cursor =-    if x == width-        then (0, y + 1)-        else (x, y)-  where-    scanned = L.scanl1 (+) $ length <$> text-    below = takeWhile (< cursor) scanned-    x = cursor - maybe 0 last (fromNullable below)-    y = length below--getText :: Field -> Text-getText (Field text _) = text--textToField :: Text -> Field-textToField text = Field text (length text)--field :: Field -> B.Widget UI.ResourceName-field (Field text cursor) =-    B.Widget B.Fixed B.Fixed $ do-        width <- B.availWidth <$> B.getContext-        let (wrapped, offset) = wrap width text-            location = cursorPosition wrapped width (cursor - offset)-        B.render $-            if null text-                then B.showCursor UI.RNCursor (B.Location (0, 0)) $ B.txt " "-                else B.showCursor UI.RNCursor (B.Location location) . B.vBox $ B.txt <$> wrapped--widgetFromMaybe :: B.Widget UI.ResourceName -> Maybe Field -> B.Widget UI.ResourceName-widgetFromMaybe _ (Just f) = field f-widgetFromMaybe w Nothing  = w--textField :: Text -> B.Widget UI.ResourceName-textField text =-    B.Widget B.Fixed B.Fixed $ do-        width <- B.availWidth <$> B.getContext-        let (wrapped, _) = wrap width text-        B.render $-            if null text-                then B.txt "---"-                else B.vBox $ B.txt <$> wrapped---- wrap-wrap :: Int -> Text -> ([Text], Int)-wrap width = foldl' (combine width) ([""], 0) . spl--spl' :: [Text] -> Char -> [Text]-spl' ts c-    | c == ' ' = ts <> [" "] <> [""]-    | otherwise =-        case fromNullable ts of-            Just ts' -> init ts' <> [snoc (last ts') c]-            Nothing  -> [singleton c]--spl :: Text -> [Text]-spl = foldl' spl' [""]--combine :: Int -> ([Text], Int) -> Text -> ([Text], Int)-combine width (acc, offset) s-    | newline && s == " " = (acc, offset + 1)-    | T.takeEnd 1 l == " " && s == " " = (acc, offset + 1)-    | newline = (acc <> [s], offset)-    | otherwise = (append (l <> s) acc, offset)-  where-    l = maybe "" last (fromNullable acc)-    newline = B.textWidth l + B.textWidth s > width--append :: Text -> [Text] -> [Text]-append s l =-    case fromNullable l of-        Just l' -> init l' <> [s]-        Nothing -> l <> [s]
− src/UI/Modal.hs
@@ -1,43 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module UI.Modal-    ( showModal-    ) where--import ClassyPrelude--import Control.Lens ((^.))--import Brick-import Brick.Widgets.Border-import Brick.Widgets.Center--import Data.Taskell.Date       (Day)-import Events.State.Types      (State, mode)-import Events.State.Types.Mode (ModalType (..), Mode (..))-import IO.Keyboard.Types       (Bindings)-import UI.Field                (textField)-import UI.Modal.Detail         (detail)-import UI.Modal.Help           (help)-import UI.Modal.MoveTo         (moveTo)-import UI.Theme                (titleAttr)-import UI.Types                (ResourceName (..))--surround :: Int -> (Text, Widget ResourceName) -> Widget ResourceName-surround ht (title, widget) =-    padTopBottom 1 .-    centerLayer .-    border .-    padTopBottom 1 .-    padLeftRight 4 . vLimit (ht - 9) . hLimit 50 . (t <=>) . viewport RNModal Vertical $-    widget-  where-    t = padBottom (Pad 1) . withAttr titleAttr $ textField title--showModal :: Int -> Bindings -> State -> Day -> Widget ResourceName-showModal ht bindings state today =-    case state ^. mode of-        Modal Help      -> surround ht (help bindings)-        Modal Detail {} -> surround ht (detail state today)-        Modal MoveTo    -> surround ht (moveTo state)-        _               -> emptyWidget
− src/UI/Modal/Detail.hs
@@ -1,84 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.Modal.Detail-    ( detail-    ) where--import ClassyPrelude--import Control.Lens ((^.))--import Data.Sequence (mapWithIndex)--import Brick--import           Data.Taskell.Date         (Day, dayToOutput, deadline)-import qualified Data.Taskell.Subtask      as ST (Subtask, complete, name)-import           Data.Taskell.Task         (Task, description, due, name, subtasks)-import           Events.State              (getCurrentTask)-import           Events.State.Modal.Detail (getCurrentItem, getField)-import           Events.State.Types        (State)-import           Events.State.Types.Mode   (DetailItem (..))-import           UI.Field                  (Field, textField, widgetFromMaybe)-import           UI.Theme                  (disabledAttr, dlToAttr, taskCurrentAttr,-                                            titleCurrentAttr)-import           UI.Types                  (ResourceName (..))--renderSubtask :: Maybe Field -> DetailItem -> Int -> ST.Subtask -> Widget ResourceName-renderSubtask f current i subtask = padBottom (Pad 1) $ prefix <+> final-  where-    cur =-        case current of-            DetailItem c -> i == c-            _            -> False-    done = subtask ^. ST.complete-    attr =-        withAttr-            (if cur-                 then taskCurrentAttr-                 else titleCurrentAttr)-    prefix =-        attr . txt $-        if done-            then "[x] "-            else "[ ] "-    widget = textField (subtask ^. ST.name)-    final-        | cur = visible . attr $ widgetFromMaybe widget f-        | not done = attr widget-        | otherwise = widget--renderSummary :: Maybe Field -> DetailItem -> Task -> Widget ResourceName-renderSummary f i task = padTop (Pad 1) $ padBottom (Pad 2) w'-  where-    w = textField $ fromMaybe "No description" (task ^. description)-    w' =-        case i of-            DetailDescription -> visible $ widgetFromMaybe w f-            _                 -> w--renderDate :: Day -> Maybe Field -> DetailItem -> Task -> Widget ResourceName-renderDate today field item task =-    case item of-        DetailDate -> visible $ prefix <+> widgetFromMaybe widget field-        _ ->-            case day of-                Just d  -> prefix <+> withAttr (dlToAttr (deadline today d)) widget-                Nothing -> emptyWidget-  where-    day = task ^. due-    prefix = txt "Due: "-    widget = textField $ maybe "" dayToOutput day--detail :: State -> Day -> (Text, Widget ResourceName)-detail state today =-    fromMaybe ("Error", txt "Oops") $ do-        task <- getCurrentTask state-        i <- getCurrentItem state-        let f = getField state-        let sts = task ^. subtasks-            w-                | null sts = withAttr disabledAttr $ txt "No sub-tasks"-                | otherwise = vBox . toList $ renderSubtask f i `mapWithIndex` sts-        pure (task ^. name, renderDate today f i task <=> renderSummary f i task <=> w)
− src/UI/Modal/Help.hs
@@ -1,62 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.Modal.Help-    ( help-    ) where--import ClassyPrelude--import Brick-import Data.Text as T (justifyRight)--import Events.Actions.Types as A (ActionType (..))-import IO.Keyboard.Types    (Bindings, bindingsToText)--import UI.Field (textField)-import UI.Theme (taskCurrentAttr)-import UI.Types (ResourceName)--descriptions :: [([ActionType], Text)]-descriptions =-    [ ([A.Help], "Show this list of controls")-    , ([A.Previous, A.Next, A.Left, A.Right], "Move down / up / left / right")-    , ([A.Bottom], "Go to bottom of list")-    , ([A.New], "Add a task")-    , ([A.NewAbove, A.NewBelow], "Add a task above / below")-    , ([A.Edit], "Edit a task")-    , ([A.Clear], "Change task")-    , ([A.Detail], "Show task details / Edit task description")-    , ([A.DueDate], "Add/edit due date (yyyy-mm-dd)")-    , ([A.MoveUp, A.MoveDown], "Shift task down / up")-    , ([A.MoveLeft, A.MoveRight], "Shift task left / right")-    , ([A.MoveMenu], "Move task to specific list")-    , ([A.Delete], "Delete task")-    , ([A.Undo], "Undo")-    , ([A.ListNew], "New list")-    , ([A.ListEdit], "Edit list title")-    , ([A.ListDelete], "Delete list")-    , ([A.ListLeft, A.ListRight], "Move list left / right")-    , ([A.Search], "Search")-    , ([A.Quit], "Quit")-    ]--generate :: Bindings -> [([Text], Text)]-generate bindings = first (intercalate ", " . bindingsToText bindings <$>) <$> descriptions--format :: ([Text], Text) -> (Text, Text)-format = first (intercalate " / ")--line :: Int -> (Text, Text) -> Widget ResourceName-line m (l, r) = left <+> right-  where-    left = padRight (Pad 2) . withAttr taskCurrentAttr . txt $ justifyRight m ' ' l-    right = textField r--help :: Bindings -> (Text, Widget ResourceName)-help bindings = ("Controls", w)-  where-    ls = format <$> generate bindings-    m = foldl' max 0 $ length . fst <$> ls-    w = vBox $ line m <$> ls
− src/UI/Modal/MoveTo.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.Modal.MoveTo-    ( moveTo-    ) where--import ClassyPrelude--import Control.Lens ((^.))--import Brick--import Data.Taskell.List  (title)-import Events.State       (getCurrentList)-import Events.State.Types (State, lists)-import UI.Field           (textField)-import UI.Theme           (taskCurrentAttr)-import UI.Types           (ResourceName)--moveTo :: State -> (Text, Widget ResourceName)-moveTo state = ("Move To:", widget)-  where-    skip = getCurrentList state-    ls = toList $ state ^. lists-    titles = textField . (^. title) <$> ls-    letter a =-        padRight (Pad 1) . hBox $ [txt "[", withAttr taskCurrentAttr $ txt (singleton a), txt "]"]-    letters = letter <$> ['a' ..]-    remove i l = take i l <> drop (i + 1) l-    output (l, t) = l <+> t-    widget = vBox $ output <$> remove skip (zip letters titles)
src/UI/Types.hs view
@@ -4,13 +4,7 @@  import ClassyPrelude (Eq, Int, Ord, Show) -newtype ListIndex = ListIndex-    { showListIndex :: Int-    } deriving (Show, Eq, Ord)--newtype TaskIndex = TaskIndex-    { showTaskIndex :: Int-    } deriving (Show, Eq, Ord)+import Types (ListIndex, TaskIndex)  data ResourceName     = RNCursor@@ -18,4 +12,5 @@     | RNList Int     | RNLists     | RNModal+    | RNDue Int     deriving (Show, Eq, Ord)
taskell.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.12 name: taskell-version: 1.5.0.0+version: 1.7.0.0 license: BSD3 license-file: LICENSE copyright: 2019 Mark Wales@@ -54,7 +54,8 @@         IO.Keyboard         IO.Keyboard.Parser         IO.Keyboard.Types-        UI.Field+        UI.Draw.Field+        Types     hs-source-dirs: src     other-modules:         Config@@ -62,11 +63,13 @@         Events.Actions.Insert         Events.Actions.Modal         Events.Actions.Modal.Detail+        Events.Actions.Modal.Due         Events.Actions.Modal.Help         Events.Actions.Modal.MoveTo         Events.Actions.Normal         Events.Actions.Search         Events.State.Modal.Detail+        Events.State.Modal.Due         IO.Config.General         IO.Config.GitHub         IO.Config.Layout@@ -81,18 +84,26 @@         IO.Markdown         UI.CLI         UI.Draw-        UI.Modal-        UI.Modal.Detail-        UI.Modal.Help-        UI.Modal.MoveTo+        UI.Draw.Main+        UI.Draw.Main.List+        UI.Draw.Main.Search+        UI.Draw.Main.StatusBar+        UI.Draw.Modal+        UI.Draw.Modal.Detail+        UI.Draw.Modal.Due+        UI.Draw.Modal.Help+        UI.Draw.Modal.MoveTo+        UI.Draw.Mode+        UI.Draw.Task+        UI.Draw.Types         UI.Theme         UI.Types         Paths_taskell     default-language: Haskell2010     default-extensions: OverloadedStrings NoImplicitPrelude     build-depends:-        aeson >=1.4.2.0 && <1.5,-        attoparsec >=0.13.2.2 && <0.14,+        aeson >=1.4.5.0 && <1.5,+        attoparsec >=0.13.2.3 && <0.14,         base >=4.12.0.0 && <=5,         brick >=0.47.1 && <0.48,         bytestring >=0.10.8.2 && <0.11,@@ -101,9 +112,9 @@         containers >=0.6.0.1 && <0.7,         directory >=1.3.3.0 && <1.4,         file-embed >=0.0.11 && <0.1,-        fold-debounce >=0.2.0.8 && <0.3,-        http-client >=0.5.14 && <0.6,-        http-conduit >=2.3.7.1 && <2.4,+        fold-debounce >=0.2.0.9 && <0.3,+        http-client >=0.6.4 && <0.7,+        http-conduit >=2.3.7.3 && <2.4,         http-types >=0.12.3 && <0.13,         lens >=4.17.1 && <4.18,         mtl >=2.2.2 && <2.3,@@ -150,7 +161,7 @@     default-extensions: OverloadedStrings NoImplicitPrelude     ghc-options: -threaded -rtsopts -with-rtsopts=-N     build-depends:-        aeson >=1.4.2.0 && <1.5,+        aeson >=1.4.5.0 && <1.5,         base >=4.12.0.0 && <4.13,         classy-prelude >=1.5.0 && <1.6,         containers >=0.6.0.1 && <0.7,@@ -158,10 +169,10 @@         lens >=4.17.1 && <4.18,         raw-strings-qq ==1.1.*,         taskell -any,-        tasty ==1.2.*,+        tasty >=1.2.3 && <1.3,         tasty-discover >=4.2.1 && <4.3,-        tasty-expected-failure >=0.11.1.1 && <0.12,-        tasty-hunit >=0.10.0.1 && <0.11,+        tasty-expected-failure >=0.11.1.2 && <0.12,+        tasty-hunit >=0.10.0.2 && <0.11,         text >=1.2.3.1 && <1.3,         time >=1.8.0.2 && <1.9,         vty >=5.25.1 && <5.26
templates/bindings.ini view
@@ -3,6 +3,7 @@ undo = u search = / help = ?+due = !  # navigation previous = k@@ -15,6 +16,7 @@ new = a newAbove = O newBelow = o+duplicate = +  # editing tasks edit = e, A, i@@ -22,12 +24,14 @@ delete = D detail = <Enter> dueDate = @+clearDate = <Backspace>  # moving tasks moveUp = K moveDown = J moveLeft = H-moveRight = L, <Space>+moveRight = L+complete = <Space> moveMenu = m  # lists
test/Data/Taskell/ListNavigationTest.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedStrings #-}  module Data.Taskell.ListNavigationTest-    ( test_list+    ( test_listNav     ) where  import ClassyPrelude as CP@@ -28,8 +28,8 @@ list = List "Populated" taskSeq  -- tests-test_list :: TestTree-test_list =+test_listNav :: TestTree+test_listNav =     testGroup         "Data.Taskell.List Navigation"         [ testGroup@@ -74,5 +74,8 @@               , testCase                     "out of bounds with no term"                     (assertEqual "nothing" 5 (L.nearest 50 Nothing list))+              , testCase+                    "out of bounds by 1 with no term"+                    (assertEqual "nothing" 5 (L.nearest 6 Nothing list))               ]         ]
test/Data/Taskell/ListTest.hs view
@@ -92,37 +92,61 @@                     (assertEqual                          "List with moved item"                          (Just-                              (List-                                   "Populated"-                                   (fromList [T.new "Hello", T.new "Fish", T.new "Blah"])))-                         (move 1 1 populatedList))+                              ( List+                                    "Populated"+                                    (fromList [T.new "Hello", T.new "Fish", T.new "Blah"])+                              , 2))+                         (move 1 1 Nothing populatedList))               , testCase                     "down"                     (assertEqual                          "List with moved item"                          (Just-                              (List-                                   "Populated"-                                   (fromList [T.new "Blah", T.new "Hello", T.new "Fish"])))-                         (move 1 (-1) populatedList))+                              ( List+                                    "Populated"+                                    (fromList [T.new "Blah", T.new "Hello", T.new "Fish"])+                              , 0))+                         (move 1 (-1) Nothing populatedList))               , testCase                     "up - out of bounds"                     (assertEqual                          "List with moved item"                          (Just-                              (List-                                   "Populated"-                                   (fromList [T.new "Hello", T.new "Fish", T.new "Blah"])))-                         (move 1 10 populatedList))+                              ( List+                                    "Populated"+                                    (fromList [T.new "Hello", T.new "Fish", T.new "Blah"])+                              , 2))+                         (move 1 10 Nothing populatedList))               , testCase                     "down - out of bounds"                     (assertEqual                          "List with moved item"                          (Just-                              (List-                                   "Populated"-                                   (fromList [T.new "Blah", T.new "Hello", T.new "Fish"])))-                         (move 1 (-10) populatedList))+                              ( List+                                    "Populated"+                                    (fromList [T.new "Blah", T.new "Hello", T.new "Fish"])+                              , 0))+                         (move 1 (-10) Nothing populatedList))+              , testCase+                    "up - with search"+                    (assertEqual+                         "List with moved item"+                         (Just+                              ( List+                                    "Populated"+                                    (fromList [T.new "Blah", T.new "Fish", T.new "Hello"])+                              , 2))+                         (move 0 1 (Just "Fish") populatedList))+              , testCase+                    "down - with search"+                    (assertEqual+                         "List with moved item"+                         (Just+                              ( List+                                    "Populated"+                                    (fromList [T.new "Fish", T.new "Hello", T.new "Blah"])+                              , 0))+                         (move 2 (-1) (Just "Hello") populatedList))               ]         , testCase               "deleteTask"
test/Data/Taskell/ListsTest.hs view
@@ -13,14 +13,27 @@ import qualified Data.Taskell.List           as L import           Data.Taskell.Lists.Internal import qualified Data.Taskell.Task           as T+import           Types                       (ListIndex (ListIndex), TaskIndex (TaskIndex))  -- test data list1, list2, list3 :: L.List-list1 = foldl' (flip L.append) (L.empty "List 1") [T.new "One", T.new "Two", T.new "Three"]+list1 =+    foldl'+        (flip L.append)+        (L.empty "List 1")+        [T.new "One", T.setDue "2019-08-14" (T.new "Two"), T.new "Three"] -list2 = foldl' (flip L.append) (L.empty "List 2") [T.new "1", T.new "2", T.new "3"]+list2 =+    foldl'+        (flip L.append)+        (L.empty "List 2")+        [T.new "1", T.new "2", T.setDue "2018-12-03" (T.new "3")] -list3 = foldl' (flip L.append) (L.empty "List 3") [T.new "01", T.new "10", T.new "11"]+list3 =+    foldl'+        (flip L.append)+        (L.empty "List 3")+        [T.setDue "2019-04-05" (T.new "01"), T.new "10", T.new "11"]  testLists :: Lists testLists = fromList [list1, list2, list3]@@ -68,13 +81,17 @@                                    , foldl'                                          (flip L.append)                                          (L.empty "List 2")-                                         [T.new "1", T.new "3"]+                                         [T.new "1", T.setDue "2018-12-03" (T.new "3")]                                    , foldl'                                          (flip L.append)                                          (L.empty "List 3")-                                         [T.new "01", T.new "10", T.new "11", T.new "2"]+                                         [ T.setDue "2019-04-05" (T.new "01")+                                         , T.new "10"+                                         , T.new "11"+                                         , T.new "2"+                                         ]                                    ]))-                         (changeList (1, 1) testLists 1))+                         (changeList (ListIndex 1, TaskIndex 1) testLists 1))               , testCase                     "left"                     (assertEqual@@ -84,20 +101,30 @@                                    [ foldl'                                          (flip L.append)                                          (L.empty "List 1")-                                         [T.new "One", T.new "Two", T.new "Three", T.new "2"]+                                         [ T.new "One"+                                         , T.setDue "2019-08-14" (T.new "Two")+                                         , T.new "Three"+                                         , T.new "2"+                                         ]                                    , foldl'                                          (flip L.append)                                          (L.empty "List 2")-                                         [T.new "1", T.new "3"]+                                         [T.new "1", T.setDue "2018-12-03" (T.new "3")]                                    , list3                                    ]))-                         (changeList (1, 1) testLists (-1)))+                         (changeList (ListIndex 1, TaskIndex 1) testLists (-1)))               , testCase                     "out of bounds list"-                    (assertEqual "Nothing" Nothing (changeList (5, 1) testLists 1))+                    (assertEqual+                         "Nothing"+                         Nothing+                         (changeList (ListIndex 5, TaskIndex 1) testLists 1))               , testCase                     "out of bounds task"-                    (assertEqual "Nothing" Nothing (changeList (1, 10) testLists 1))+                    (assertEqual+                         "Nothing"+                         Nothing+                         (changeList (ListIndex 1, TaskIndex 10) testLists 1))               ]         , testCase               "newList"@@ -173,7 +200,11 @@                               , foldl'                                     (flip L.append)                                     (L.empty "List 3")-                                    [T.new "01", T.new "10", T.new "11", T.new "Blah"]+                                    [ T.setDue "2019-04-05" (T.new "01")+                                    , T.new "10"+                                    , T.new "11"+                                    , T.new "Blah"+                                    ]                               ])                          (appendToLast (T.new "Blah") testLists))               , testCase@@ -186,4 +217,14 @@                    "Returns an analysis"                    "test.md\nLists: 3\nTasks: 9"                    (analyse "test.md" testLists))+        , testCase+              "due"+              (assertEqual+                   "returns just due list"+                   (fromList+                        [ ((ListIndex 1, TaskIndex 2), T.setDue "2018-12-03" (T.new "3"))+                        , ((ListIndex 2, TaskIndex 0), T.setDue "2019-04-05" (T.new "01"))+                        , ((ListIndex 0, TaskIndex 1), T.setDue "2019-08-14" (T.new "Two"))+                        ])+                   (due testLists))         ]
test/Events/StateTest.hs view
@@ -12,24 +12,31 @@  import Control.Lens ((&), (.~), (^.)) +import Data.Time.Clock (secondsToDiffTime)+ import qualified Data.Sequence           as S (lookup) import qualified Data.Taskell.List       as L (append, empty) import qualified Data.Taskell.Task       as T (new) import           Events.State import           Events.State.Types import           Events.State.Types.Mode+import           Types                   (ListIndex (..), TaskIndex (..)) +mockTime :: UTCTime+mockTime = UTCTime (ModifiedJulianDay 20) (secondsToDiffTime 0)+ testState :: State testState =     State     { _mode = Normal     , _lists = empty     , _history = []-    , _current = (0, 0)+    , _current = (ListIndex 0, TaskIndex 0)     , _path = "test.md"     , _io = Nothing     , _height = 0     , _searchTerm = Nothing+    , _time = mockTime     }  moveToState :: State@@ -49,11 +56,12 @@               , L.empty "List 9"               ]     , _history = []-    , _current = (4, 0)+    , _current = (ListIndex 4, TaskIndex 0)     , _path = "test.md"     , _io = Nothing     , _height = 0     , _searchTerm = Nothing+    , _time = mockTime     }  -- tests
test/IO/Keyboard/ParserTest.hs view
@@ -44,6 +44,7 @@     , (BChar 'u', A.Undo)     , (BChar '/', A.Search)     , (BChar '?', A.Help)+    , (BChar '!', A.Due)     , (BChar 'k', A.Previous)     , (BChar 'j', A.Next)     , (BChar 'h', A.Left)@@ -52,6 +53,7 @@     , (BChar 'a', A.New)     , (BChar 'O', A.NewAbove)     , (BChar 'o', A.NewBelow)+    , (BChar '+', A.Duplicate)     , (BChar 'e', A.Edit)     , (BChar 'A', A.Edit)     , (BChar 'i', A.Edit)
test/IO/Keyboard/TypesTest.hs view
@@ -22,6 +22,7 @@     [ (BChar 'œ', A.Quit)     , (BChar 'U', A.Undo)     , (BChar '/', A.Search)+    , (BChar '!', A.Due)     , (BChar '?', A.Help)     , (BChar 'k', A.Previous)     , (BChar 'j', A.Next)@@ -31,6 +32,7 @@     , (BChar 'a', A.New)     , (BChar 'O', A.NewAbove)     , (BChar 'o', A.NewBelow)+    , (BChar '+', A.Duplicate)     , (BChar 'e', A.Edit)     , (BChar 'A', A.Edit)     , (BChar 'i', A.Edit)@@ -38,11 +40,12 @@     , (BChar 'D', A.Delete)     , (BKey "Enter", A.Detail)     , (BChar '@', A.DueDate)+    , (BKey "Backspace", A.ClearDate)     , (BChar 'K', A.MoveUp)     , (BChar 'J', A.MoveDown)     , (BChar 'H', A.MoveLeft)     , (BChar 'L', A.MoveRight)-    , (BKey "Space", A.MoveRight)+    , (BKey "Space", A.Complete)     , (BChar 'm', A.MoveMenu)     , (BChar 'N', A.ListNew)     , (BChar 'E', A.ListEdit)
test/IO/KeyboardTest.hs view
@@ -13,6 +13,8 @@  import Control.Lens ((.~)) +import Data.Time.Clock (secondsToDiffTime)+ import Data.Taskell.Lists.Internal (initial) import Events.Actions.Types        as A import Events.State                (create, quit)@@ -22,11 +24,14 @@ import IO.Keyboard                 (generate) import IO.Keyboard.Types +mockTime :: UTCTime+mockTime = UTCTime (ModifiedJulianDay 20) (secondsToDiffTime 0)+ tester :: BoundActions -> Event -> Stateful tester actions ev state = lookup ev actions >>= ($ state)  cleanState :: State-cleanState = create "taskell.md" initial+cleanState = create mockTime "taskell.md" initial  basicBindings :: Bindings basicBindings = [(BChar 'q', A.Quit)]
test/IO/MarkdownTest.hs view
@@ -122,7 +122,7 @@                           (assertEqual                                "List item with Sub-Task"                                (makeSubTask "Blah" True, [])-                               (start defaultConfig (listWithItem, []) ("    * ~Blah~", 1)))+                               (start defaultConfig (listWithItem, []) ("    * [x] Blah", 1)))                     , ignoreTest $                       testCase                           "List item without list"@@ -194,12 +194,6 @@                                "List item with Sub-Task"                                (makeSubTask "Blah" False, [])                                (start alternativeConfig (listWithItem, []) ("- Blah", 1)))-                    , testCase-                          "Complete Sub-Task (old style)"-                          (assertEqual-                               "List item with Sub-Task"-                               (makeSubTask "Blah" True, [])-                               (start alternativeConfig (listWithItem, []) ("- ~Blah~", 1)))                     ]               ]         , testGroup
test/UI/FieldTest.hs view
@@ -7,8 +7,8 @@  import ClassyPrelude -import UI.Field (Field (Field), backspace, cursorPosition, insertCharacter, insertText,-                 updateCursor, wrap)+import UI.Draw.Field (Field (Field), backspace, cursorPosition, insertCharacter, insertText,+                      updateCursor, wrap)  import Test.Tasty import Test.Tasty.HUnit@@ -24,7 +24,7 @@ test_field :: TestTree test_field =     testGroup-        "UI.Field"+        "UI.Draw.Field"         [ testGroup               "Field cursor left"               [ testCase@@ -148,10 +148,27 @@                          (6, 1)                          (cursorPosition' "Artichoke penguin astronaut wombat" 34))               , testCase+                    "Triple line"+                    (assertEqual+                         "Should wrap to correct position"+                         (11, 2)+                         (cursorPosition'+                              "Artichoke penguin astronaut wombat artichoke penguin astronaut wombat"+                              64))+              , testCase                     "Long words with space"                     (assertEqual                          "Should ignore the space when counting"                          (16, 1)                          (cursorPosition' "Blah fish wombat monkey sponge catpult arsonist" 47))+              , testCase+                    "Multibyte string single line"+                    (assertEqual "Should move in twos" (4, 0) (cursorPosition' "乤乭亍乫" 2))+              , testCase+                    "Multibyte string multi-line"+                    (assertEqual+                         "Should move in twos"+                         (4, 1)+                         (cursorPosition' "乤乭亍乫 乤乭亍乫 乤乭亍乫 乤乭亍乫" 17))               ]         ]