taskell 1.9.2.0 → 1.9.3.0
raw patch · 194 files changed
+7015/−6961 lines, 194 filesdep ~aesondep ~basedep ~brick
Dependency ranges changed: aeson, base, brick, bytestring, containers, directory, file-embed, lens, template-haskell, text, time, tz
Files
- README.md +12/−1
- app/Main.hs +4/−6
- src/App.hs +0/−182
- src/Config.hs +0/−15
- src/Data/Taskell/Date.hs +0/−115
- src/Data/Taskell/Date/RelativeParser.hs +0/−67
- src/Data/Taskell/Date/Types.hs +0/−27
- src/Data/Taskell/List.hs +0/−29
- src/Data/Taskell/List/Internal.hs +0/−127
- src/Data/Taskell/Lists.hs +0/−21
- src/Data/Taskell/Lists/Internal.hs +0/−85
- src/Data/Taskell/Seq.hs +0/−24
- src/Data/Taskell/Subtask.hs +0/−14
- src/Data/Taskell/Subtask/Internal.hs +0/−31
- src/Data/Taskell/Task.hs +0/−29
- src/Data/Taskell/Task/Internal.hs +0/−99
- src/Events/Actions.hs +0/−69
- src/Events/Actions/Insert.hs +0/−38
- src/Events/Actions/Modal.hs +0/−27
- src/Events/Actions/Modal/Detail.hs +0/−66
- src/Events/Actions/Modal/Due.hs +0/−29
- src/Events/Actions/Modal/Help.hs +0/−22
- src/Events/Actions/Modal/MoveTo.hs +0/−16
- src/Events/Actions/Normal.hs +0/−72
- src/Events/Actions/Search.hs +0/−23
- src/Events/Actions/Types.hs +0/−82
- src/Events/State.hs +0/−414
- src/Events/State/History.hs +0/−32
- src/Events/State/Modal/Detail.hs +0/−187
- src/Events/State/Modal/Due.hs +0/−68
- src/Events/State/Types.hs +0/−52
- src/Events/State/Types/Mode.hs +0/−49
- src/IO/Config.hs +0/−119
- src/IO/Config/General.hs +0/−30
- src/IO/Config/GitHub.hs +0/−25
- src/IO/Config/Layout.hs +0/−54
- src/IO/Config/Markdown.hs +0/−66
- src/IO/Config/Parser.hs +0/−12
- src/IO/Config/Trello.hs +0/−25
- src/IO/HTTP/Aeson.hs +0/−18
- src/IO/HTTP/GitHub.hs +0/−171
- src/IO/HTTP/GitHub/AutomatedCard.hs +0/−33
- src/IO/HTTP/GitHub/Card.hs +0/−32
- src/IO/HTTP/GitHub/Column.hs +0/−35
- src/IO/HTTP/GitHub/Project.hs +0/−29
- src/IO/HTTP/GitHub/Utility.hs +0/−13
- src/IO/HTTP/Trello.hs +0/−105
- src/IO/HTTP/Trello/Card.hs +0/−45
- src/IO/HTTP/Trello/ChecklistItem.hs +0/−41
- src/IO/HTTP/Trello/List.hs +0/−36
- src/IO/Keyboard.hs +0/−78
- src/IO/Keyboard/Parser.hs +0/−46
- src/IO/Keyboard/Types.hs +0/−45
- src/IO/Markdown.hs +0/−8
- src/IO/Markdown/Parser.hs +0/−67
- src/IO/Markdown/Serializer.hs +0/−92
- src/IO/Taskell.hs +0/−159
- src/Taskell.hs +179/−0
- src/Taskell/Config.hs +13/−0
- src/Taskell/Data/Date.hs +112/−0
- src/Taskell/Data/Date/RelativeParser.hs +64/−0
- src/Taskell/Data/Date/Types.hs +25/−0
- src/Taskell/Data/List.hs +28/−0
- src/Taskell/Data/List/Internal.hs +128/−0
- src/Taskell/Data/Lists.hs +20/−0
- src/Taskell/Data/Lists/Internal.hs +89/−0
- src/Taskell/Data/Seq.hs +22/−0
- src/Taskell/Data/Subtask.hs +12/−0
- src/Taskell/Data/Subtask/Internal.hs +29/−0
- src/Taskell/Data/Task.hs +27/−0
- src/Taskell/Data/Task/Internal.hs +97/−0
- src/Taskell/Events/Actions.hs +67/−0
- src/Taskell/Events/Actions/Insert.hs +36/−0
- src/Taskell/Events/Actions/Modal.hs +25/−0
- src/Taskell/Events/Actions/Modal/Detail.hs +64/−0
- src/Taskell/Events/Actions/Modal/Due.hs +28/−0
- src/Taskell/Events/Actions/Modal/Help.hs +20/−0
- src/Taskell/Events/Actions/Modal/MoveTo.hs +14/−0
- src/Taskell/Events/Actions/Normal.hs +73/−0
- src/Taskell/Events/Actions/Search.hs +21/−0
- src/Taskell/Events/Actions/Types.hs +85/−0
- src/Taskell/Events/State.hs +419/−0
- src/Taskell/Events/State/History.hs +29/−0
- src/Taskell/Events/State/Modal/Detail.hs +184/−0
- src/Taskell/Events/State/Modal/Due.hs +66/−0
- src/Taskell/Events/State/Types.hs +51/−0
- src/Taskell/Events/State/Types/Mode.hs +47/−0
- src/Taskell/IO.hs +157/−0
- src/Taskell/IO/Config.hs +117/−0
- src/Taskell/IO/Config/General.hs +30/−0
- src/Taskell/IO/Config/GitHub.hs +25/−0
- src/Taskell/IO/Config/Layout.hs +54/−0
- src/Taskell/IO/Config/Markdown.hs +66/−0
- src/Taskell/IO/Config/Parser.hs +12/−0
- src/Taskell/IO/Config/Trello.hs +25/−0
- src/Taskell/IO/HTTP/Aeson.hs +17/−0
- src/Taskell/IO/HTTP/GitHub.hs +161/−0
- src/Taskell/IO/HTTP/GitHub/AutomatedCard.hs +31/−0
- src/Taskell/IO/HTTP/GitHub/Card.hs +30/−0
- src/Taskell/IO/HTTP/GitHub/Column.hs +34/−0
- src/Taskell/IO/HTTP/GitHub/Project.hs +28/−0
- src/Taskell/IO/HTTP/GitHub/Utility.hs +10/−0
- src/Taskell/IO/HTTP/Trello.hs +102/−0
- src/Taskell/IO/HTTP/Trello/Card.hs +43/−0
- src/Taskell/IO/HTTP/Trello/ChecklistItem.hs +39/−0
- src/Taskell/IO/HTTP/Trello/List.hs +35/−0
- src/Taskell/IO/Keyboard.hs +77/−0
- src/Taskell/IO/Keyboard/Parser.hs +42/−0
- src/Taskell/IO/Keyboard/Types.hs +41/−0
- src/Taskell/IO/Markdown.hs +8/−0
- src/Taskell/IO/Markdown/Parser.hs +64/−0
- src/Taskell/IO/Markdown/Serializer.hs +89/−0
- src/Taskell/Types.hs +16/−0
- src/Taskell/UI/CLI.hs +21/−0
- src/Taskell/UI/Draw.hs +39/−0
- src/Taskell/UI/Draw/Field.hs +143/−0
- src/Taskell/UI/Draw/Main.hs +26/−0
- src/Taskell/UI/Draw/Main/List.hs +89/−0
- src/Taskell/UI/Draw/Main/Search.hs +31/−0
- src/Taskell/UI/Draw/Main/StatusBar.hs +65/−0
- src/Taskell/UI/Draw/Modal.hs +44/−0
- src/Taskell/UI/Draw/Modal/Detail.hs +88/−0
- src/Taskell/UI/Draw/Modal/Due.hs +39/−0
- src/Taskell/UI/Draw/Modal/Help.hs +68/−0
- src/Taskell/UI/Draw/Modal/MoveTo.hs +30/−0
- src/Taskell/UI/Draw/Mode.hs +18/−0
- src/Taskell/UI/Draw/Task.hs +107/−0
- src/Taskell/UI/Draw/Types.hs +27/−0
- src/Taskell/UI/Theme.hs +84/−0
- src/Taskell/UI/Types.hs +14/−0
- src/Taskell/Utility/Parser.hs +17/−0
- src/Types.hs +0/−18
- src/UI/CLI.hs +0/−24
- src/UI/Draw.hs +0/−41
- src/UI/Draw/Field.hs +0/−146
- src/UI/Draw/Main.hs +0/−28
- src/UI/Draw/Main/List.hs +0/−93
- src/UI/Draw/Main/Search.hs +0/−34
- src/UI/Draw/Main/StatusBar.hs +0/−69
- src/UI/Draw/Modal.hs +0/−46
- src/UI/Draw/Modal/Detail.hs +0/−90
- src/UI/Draw/Modal/Due.hs +0/−42
- src/UI/Draw/Modal/Help.hs +0/−70
- src/UI/Draw/Modal/MoveTo.hs +0/−33
- src/UI/Draw/Mode.hs +0/−20
- src/UI/Draw/Task.hs +0/−109
- src/UI/Draw/Types.hs +0/−29
- src/UI/Theme.hs +0/−70
- src/UI/Types.hs +0/−16
- src/Utility/Parser.hs +0/−19
- taskell.cabal +122/−116
- templates/bindings.ini +10/−2
- templates/theme.ini +8/−0
- test/Data/Taskell/Date/RelativeParserTest.hs +0/−79
- test/Data/Taskell/DateTest.hs +0/−246
- test/Data/Taskell/ListNavigationTest.hs +0/−81
- test/Data/Taskell/ListTest.hs +0/−191
- test/Data/Taskell/ListsTest.hs +0/−236
- test/Data/Taskell/SeqTest.hs +0/−95
- test/Data/Taskell/SubtaskTest.hs +0/−45
- test/Data/Taskell/TaskTest.hs +0/−203
- test/Events/State/HistoryTest.hs +0/−98
- test/Events/StateTest.hs +0/−132
- test/IO/GitHub/CardsTest.hs +0/−39
- test/IO/GitHubTest.hs +0/−48
- test/IO/Keyboard/ParserTest.hs +0/−95
- test/IO/Keyboard/TypesTest.hs +0/−76
- test/IO/KeyboardTest.hs +0/−56
- test/IO/Markdown/ParserTest.hs +0/−214
- test/IO/Markdown/SerializerTest.hs +0/−194
- test/IO/TrelloTest.hs +0/−42
- test/Taskell/Data/Date/RelativeParserTest.hs +75/−0
- test/Taskell/Data/DateTest.hs +243/−0
- test/Taskell/Data/ListNavigationTest.hs +78/−0
- test/Taskell/Data/ListTest.hs +188/−0
- test/Taskell/Data/ListsTest.hs +233/−0
- test/Taskell/Data/SeqTest.hs +92/−0
- test/Taskell/Data/SubtaskTest.hs +42/−0
- test/Taskell/Data/TaskTest.hs +200/−0
- test/Taskell/Events/State/HistoryTest.hs +95/−0
- test/Taskell/Events/StateTest.hs +130/−0
- test/Taskell/IO/GitHub/CardsTest.hs +36/−0
- test/Taskell/IO/GitHubTest.hs +45/−0
- test/Taskell/IO/Keyboard/ParserTest.hs +93/−0
- test/Taskell/IO/Keyboard/TypesTest.hs +37/−0
- test/Taskell/IO/Keyboard/data/bindings.ini +43/−0
- test/Taskell/IO/KeyboardTest.hs +54/−0
- test/Taskell/IO/Markdown/ParserTest.hs +213/−0
- test/Taskell/IO/Markdown/SerializerTest.hs +192/−0
- test/Taskell/IO/TrelloTest.hs +41/−0
- test/Taskell/IO/data/trello-checklists.json +35/−0
- test/Taskell/IO/data/trello.json +146/−0
- test/Taskell/UI/FieldTest.hs +171/−0
- test/UI/FieldTest.hs +0/−174
README.md view
@@ -297,7 +297,7 @@ title.fg = green ; status bar-statusBar.bg = magenta+statusBar.bg = blue statusBar.fg = black ; current list title@@ -305,6 +305,17 @@ ; current task taskCurrent.fg = magenta++; subtasks+; selected+subtaskCurrent.fg = magenta+; incomplete+subtaskIncomplete.fg = blue+; complete+subtaskComplete.fg = yellow++; disabled+disabled.fg = yellow ``` You can also change the background and default text colour:
app/Main.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE NoImplicitPrelude #-}- module Main where import ClassyPrelude@@ -8,10 +6,10 @@ import Data.Time.Zones (loadLocalTZ) -import App (go)-import Events.State (create)-import IO.Config (setup)-import IO.Taskell (IOInfo (IOInfo), Next (..), load)+import Taskell (go)+import Taskell.Events.State (create)+import Taskell.IO (IOInfo (IOInfo), Next (..), load)+import Taskell.IO.Config (setup) main :: IO () main = do
− src/App.hs
@@ -1,182 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TupleSections #-}--module App- ( go- ) where--import ClassyPrelude--import Control.Concurrent (forkIO, threadDelay)--import Control.Lens ((^.))--import Data.Time.Zones (TZ)--import Brick-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.Lists (Lists)-import Events.Actions (ActionSets, event, generateActions)-import Events.State (continue, countCurrent, setHeight, setTime)-import Events.State.Types (State, current, io, lists, mode, path, searchTerm, timeZone)-import Events.State.Types.Mode (InsertMode (..), InsertType (..), ModalType (..), Mode (..))-import IO.Config (Config, debugging, generateAttrMap, getBindings, layout)-import IO.Taskell (writeData)-import Types (ListIndex (..), TaskIndex (..))-import UI.Draw (chooseCursor, draw)-import UI.Types (ResourceName (..))--type DebouncedMessage = (Lists, FilePath, TZ)--type DebouncedWrite = DebouncedMessage -> IO ()--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, tz) = writeData tz config ls pth--next :: DebouncedWrite -> State -> EventM ResourceName (Next State)-next send state =- case state ^. io of- Just ls -> do- invalidateCache- liftIO $ send (ls, state ^. path, state ^. timeZone)- Brick.continue $ Events.State.continue state- Nothing -> Brick.continue state---- debouncing-debounce :: Config -> State -> IO (DebouncedWrite, Trigger)-debounce config initial = do- trigger <-- Debounce.new- Debounce.Args- { Debounce.cb = store config- , Debounce.fold = flip const- , Debounce.init = (initial ^. lists, initial ^. path, initial ^. timeZone)- }- Debounce.def- let send = Debounce.send trigger- pure (send, trigger)---- cache clearing-clearCache :: State -> EventM ResourceName ()-clearCache state = do- let (ListIndex li, TaskIndex ti) = state ^. current- invalidateCacheEntry (RNList li)- invalidateCacheEntry (RNTask (ListIndex li, TaskIndex ti))--clearAllTitles :: State -> EventM ResourceName ()-clearAllTitles state = do- let count = length (state ^. lists)- let range = [0 .. (count - 1)]- traverse_ (invalidateCacheEntry . RNList) range- traverse_ (invalidateCacheEntry . RNTask . (, TaskIndex (-1)) . ListIndex) range--clearList :: State -> EventM ResourceName ()-clearList state = do- 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)-handleVtyEvent (send, trigger) actions previousState e = do- let state = event actions e previousState- when (previousState ^. searchTerm /= state ^. searchTerm) invalidateCache- case previousState ^. mode of- (Modal MoveTo) -> clearAllTitles previousState- (Insert ITask ICreate _) -> clearList previousState- _ -> 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--getHeight :: EventM ResourceName Int-getHeight = snd <$> (liftIO . displayBounds =<< outputIface <$> getVtyHandle)--handleEvent ::- (DebouncedWrite, Trigger)- -> ActionSets- -> State- -> 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- Brick.continue (setHeight h state)-handleEvent db actions state (VtyEvent ev) = handleVtyEvent db actions state ev-handleEvent _ _ state _ = Brick.continue state---- | Runs when the app starts--- Adds paste support-appStart :: State -> EventM ResourceName State-appStart state = do- output <- outputIface <$> getVtyHandle- when (supportsMode output BracketedPaste) . liftIO $ setMode output BracketedPaste True- h <- getHeight- pure (setHeight h state)---- | Sets up Brick-go :: Config -> State -> IO ()-go config initial = do- attrMap' <- const <$> generateAttrMap- -- 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 (debugging config)- , appChooseCursor = chooseCursor- , appHandleEvent = handleEvent db (generateActions bindings)- , appStartEvent = appStart- , appAttrMap = attrMap'- }- -- start- let builder = mkVty defaultConfig- initialVty <- builder- void $ customMain initialVty builder (Just timerChan) app initial
− src/Config.hs
@@ -1,15 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module Config where--import ClassyPrelude--import Data.FileEmbed (embedFile)--version :: Text-version = "1.9.2"--usage :: Text-usage = decodeUtf8 $(embedFile "templates/usage.txt")
− src/Data/Taskell/Date.hs
@@ -1,115 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.Date- ( Day- , Deadline(..)- , Due(..)- , timeToText- , timeToDisplay- , timeToOutput- , timeToOutputLocal- , textToTime- , inputToTime- , isoToTime- , deadline- ) where--import ClassyPrelude--import Control.Monad.Fail (MonadFail)--import Data.Time.LocalTime (ZonedTime (ZonedTime))-import Data.Time.Zones (TZ, localTimeToUTCTZ, timeZoneForUTCTime, utcToLocalTimeTZ)--import Data.Time.Calendar (diffDays)-import Data.Time.Format (FormatTime, ParseTime, formatTime, iso8601DateFormat, parseTimeM)--import Data.Taskell.Date.RelativeParser (parseRelative)-import Data.Taskell.Date.Types (Deadline (..), Due (..))---- formats-dateFormat :: String-dateFormat = "%Y-%m-%d"--timeDisplayFormat :: String-timeDisplayFormat = "%Y-%m-%d %H:%M"--timeFormat :: String-timeFormat = "%Y-%m-%d %H:%M %Z"--isoFormat :: String-isoFormat = iso8601DateFormat (Just "%H:%M:%S%Q%Z")---- utility functions-utcToZonedTime :: TZ -> UTCTime -> ZonedTime-utcToZonedTime tz time = ZonedTime (utcToLocalTimeTZ tz time) (timeZoneForUTCTime tz time)--appendYear :: (FormatTime t, FormatTime s) => String -> t -> s -> String-appendYear txt t1 t2 =- if format "%Y" t1 == format "%Y" t2- then txt- else txt <> " %Y"---- output-format :: (FormatTime t) => String -> t -> Text-format fmt = pack . formatTime defaultTimeLocale fmt--timeToText :: TZ -> UTCTime -> Due -> Text-timeToText _ now (DueDate day) = format fmt day- where- fmt = appendYear "%d-%b" now day-timeToText tz now (DueTime time) = format fmt local- where- local = utcToLocalTimeTZ tz time- fmt = appendYear "%H:%M %d-%b" now local--timeToDisplay :: TZ -> Due -> Text-timeToDisplay _ (DueDate day) = format dateFormat day-timeToDisplay tz (DueTime time) = format timeDisplayFormat (utcToLocalTimeTZ tz time)--timeToOutput :: Due -> Text-timeToOutput (DueDate day) = format dateFormat day-timeToOutput (DueTime time) = format timeFormat time--timeToOutputLocal :: TZ -> Due -> Text-timeToOutputLocal _ (DueDate day) = format dateFormat day-timeToOutputLocal tz (DueTime time) = format timeFormat (utcToZonedTime tz time)---- input-parseT :: (Monad m, MonadFail m, ParseTime t) => String -> Text -> m t-parseT fmt txt = parseTimeM False defaultTimeLocale fmt (unpack txt)--parseDate :: Text -> Maybe Due-parseDate txt = DueDate <$> parseT dateFormat txt--(<?>) :: Maybe a -> Maybe a -> Maybe a-(<?>) Nothing b = b-(<?>) a _ = a--textToTime :: Text -> Maybe Due-textToTime txt = parseDate txt <?> (DueTime <$> parseT timeFormat txt)--inputToTime :: TZ -> UTCTime -> Text -> Maybe Due-inputToTime tz now txt =- parseDate txt <?> (DueTime . localTimeToUTCTZ tz <$> parseT timeDisplayFormat txt) <?>- case parseRelative now txt of- Right due -> Just due- Left _ -> Nothing--isoToTime :: Text -> Maybe Due-isoToTime txt = DueTime <$> parseT isoFormat txt---- deadlines-deadline :: UTCTime -> Due -> Deadline-deadline now date- | days < 0 = Passed- | days == 0 = Today- | days == 1 = Tomorrow- | days < 7 = ThisWeek- | otherwise = Plenty- where- days =- case date of- DueTime t -> diffDays (utctDay t) (utctDay now)- DueDate d -> diffDays d (utctDay now)
− src/Data/Taskell/Date/RelativeParser.hs
@@ -1,67 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.Date.RelativeParser- ( parseRelative- ) where--import ClassyPrelude--import Data.Attoparsec.Text--import Data.Time.Clock (addUTCTime)--import Data.Taskell.Date.Types (Due (DueDate, DueTime))-import Utility.Parser (lexeme, only)---- utility functions-addP :: (Integral a) => Parser a -> UTCTime -> Parser UTCTime-addP p now = ($ now) . addUTCTime . fromIntegral . sum <$> many1 p---- relative time parsing-minute :: Int-minute = 60--hour :: Int-hour = minute * 60--day :: Int-day = hour * 24--week :: Int-week = day * 7--timePeriodP :: Char -> Parser Int-timePeriodP c = lexeme decimal <* char c--wP :: Parser Int-wP = (* week) <$> timePeriodP 'w'--dP :: Parser Int-dP = (* day) <$> timePeriodP 'd'--hP :: Parser Int-hP = (* hour) <$> timePeriodP 'h'--mP :: Parser Int-mP = (* minute) <$> timePeriodP 'm'--sP :: Parser Int-sP = timePeriodP 's'--timeP :: UTCTime -> Parser (Maybe Due)-timeP now = only . lexeme $ Just . DueTime <$> addP (sP <|> mP <|> hP <|> dP <|> wP) now---- relative date parsing-dateP :: UTCTime -> Parser (Maybe Due)-dateP now = only . lexeme $ Just . DueDate . utctDay <$> addP (dP <|> wP) now---- relative parser-relativeP :: UTCTime -> Parser (Maybe Due)-relativeP now = dateP now <|> timeP now--parseRelative :: UTCTime -> Text -> Either Text Due-parseRelative now text =- case parseOnly (relativeP now) text of- Right (Just due) -> Right due- _ -> Left "Could not parse date."
− src/Data/Taskell/Date/Types.hs
@@ -1,27 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Data.Taskell.Date.Types- ( Deadline(..)- , Due(..)- ) where--import ClassyPrelude--data Due- = DueTime UTCTime- | DueDate Day- deriving (Show, Eq)--instance Ord Due where- compare (DueTime t) (DueDate d) = t `compare` UTCTime d 0- compare (DueDate d) (DueTime t) = UTCTime d 0 `compare` t- compare (DueDate d1) (DueDate d2) = d1 `compare` d2- compare (DueTime t1) (DueTime t2) = t1 `compare` t2--data Deadline- = Passed- | Today- | Tomorrow- | ThisWeek- | Plenty- deriving (Show, Eq)
− src/Data/Taskell/List.hs
@@ -1,29 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Data.Taskell.List- ( List- , Update- , title- , tasks- , create- , empty- , due- , clearDue- , new- , count- , newAt- , duplicate- , append- , extract- , updateFn- , update- , move- , deleteTask- , getTask- , searchFor- , nextTask- , prevTask- , nearest- ) where--import Data.Taskell.List.Internal
− src/Data/Taskell/List/Internal.hs
@@ -1,127 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TupleSections #-}--module Data.Taskell.List.Internal where--import ClassyPrelude--import Control.Lens (element, makeLenses, (%%~), (%~), (&), (.~), (^.), (^?))--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, clearDue, contains, due, duplicate)-import Types (TaskIndex (TaskIndex))--data List = List- { _title :: Text- , _tasks :: Seq T.Task- } deriving (Show, Eq)--type Update = List -> List---- create lenses-$(makeLenses ''List)---- operations-create :: Text -> Seq T.Task -> List-create = List--empty :: Text -> List-empty text = List text ClassyPrelude.empty--new :: Update-new = append T.blank--count :: List -> Int-count = length . (^. tasks)--due :: List -> Seq (TaskIndex, T.Task)-due list = catMaybes (filt S.<#> (list ^. tasks))- where- filt int task = const (TaskIndex int, task) <$> task ^. T.due--clearDue :: TaskIndex -> Update-clearDue (TaskIndex int) = updateFn int T.clearDue--newAt :: Int -> Update-newAt idx = tasks %~ S.insertAt idx T.blank--duplicate :: Int -> List -> Maybe List-duplicate idx list = do- task <- T.duplicate <$> getTask idx list- pure $ list & tasks %~ S.insertAt idx task--append :: T.Task -> Update-append task = tasks %~ (|> task)--extract :: Int -> List -> Maybe (List, T.Task)-extract idx list = do- (xs, x) <- S.extract idx (list ^. tasks)- pure (list & tasks .~ xs, x)--updateFn :: Int -> T.Update -> Update-updateFn idx fn = tasks %~ adjust' fn idx--update :: Int -> T.Task -> Update-update idx task = tasks %~ S.update idx task--move :: Int -> Int -> Maybe Text -> List -> Maybe (List, Int)-move current dir term list =- case term of- Nothing -> (, bound list (current + dir)) <$> (list & tasks %%~ S.shiftBy current dir)- Just _ -> do- idx <- changeTask dir current term list- (, idx) <$> (list & tasks %%~ S.shiftBy current (idx - current))--deleteTask :: Int -> Update-deleteTask idx = tasks %~ deleteAt idx--getTask :: Int -> List -> Maybe T.Task-getTask idx = (^? tasks . element idx)--searchFor :: Text -> Update-searchFor text = tasks %~ filter (T.contains text)--changeTask :: Int -> Int -> Maybe Text -> List -> Maybe Int-changeTask dir current term list = do- let next = current + dir- tsk <- getTask next list- case term of- Nothing -> Just next- Just trm ->- if T.contains trm tsk- then Just next- else changeTask dir next term list--nextTask :: Int -> Maybe Text -> List -> Int-nextTask idx text lst = fromMaybe idx $ changeTask 1 idx text lst--prevTask :: Int -> Maybe Text -> List -> Int-prevTask idx text lst = fromMaybe idx $ changeTask (-1) idx text lst--closest :: Int -> Int -> Int -> Int-closest current previous next =- if (next - current) < (current - previous)- then next- else previous--bound :: List -> Int -> Int-bound lst = S.bound (lst ^. tasks)--nearest' :: Int -> Maybe Text -> List -> Maybe Int-nearest' current term lst = do- let prev = changeTask (-1) current term lst- let nxt = changeTask 1 current term lst- let comp idx = Just $ maybe idx (closest current idx) nxt- maybe nxt comp prev--nearest :: Int -> Maybe Text -> List -> Int-nearest current term lst = idx- where- near = fromMaybe (-1) $ nearest' current term lst- idx =- case term of- Nothing -> bound lst current- Just txt -> maybe near (bool near current . T.contains txt) $ getTask current lst
− src/Data/Taskell/Lists.hs
@@ -1,21 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Data.Taskell.Lists- ( Lists- , initial- , updateLists- , count- , due- , clearDue- , get- , changeList- , newList- , delete- , exists- , shiftBy- , search- , appendToLast- , analyse- ) where--import Data.Taskell.Lists.Internal
− src/Data/Taskell/Lists/Internal.hs
@@ -1,85 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TupleSections #-}--module Data.Taskell.Lists.Internal where--import ClassyPrelude--import Control.Lens ((^.))-import Data.Sequence as S (adjust', deleteAt, update, (!?), (|>))--import qualified Data.Taskell.List as L (List, Update, append, clearDue, count, due, empty, extract,- searchFor)-import qualified Data.Taskell.Seq as S-import qualified Data.Taskell.Task as T (Task, due)-import Types (ListIndex (ListIndex), Pointer, TaskIndex (TaskIndex))--type Lists = Seq L.List--type Update = Lists -> Lists--initial :: Lists-initial = fromList [L.empty "To Do", L.empty "Done"]--updateLists :: Int -> L.List -> Update-updateLists = S.update--count :: Int -> Lists -> Int-count idx tasks = maybe 0 L.count (tasks !? idx)--due :: Lists -> Seq (Pointer, T.Task)-due lists = sortOn ((^. T.due) . snd) dues- where- format x lst = (\(y, t) -> ((ListIndex x, y), t)) <$> L.due lst- dues = concat $ format S.<#> lists--clearDue :: Pointer -> Update-clearDue (idx, tsk) = updateFn idx (L.clearDue tsk)--updateFn :: ListIndex -> L.Update -> Update-updateFn (ListIndex idx) fn = adjust' fn idx--get :: Lists -> Int -> Maybe L.List-get = (!?)--changeList :: Pointer -> Lists -> Int -> Maybe Lists-changeList (ListIndex list, TaskIndex idx) tasks dir = do- let next = list + dir- (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 = (|> L.empty title)--delete :: Int -> Update-delete = deleteAt--exists :: Int -> Lists -> Bool-exists idx tasks = isJust $ tasks !? idx--shiftBy :: Int -> Int -> Lists -> Maybe Lists-shiftBy = S.shiftBy--search :: Text -> Update-search text = (L.searchFor text <$>)--appendToLast :: T.Task -> Update-appendToLast task lists =- fromMaybe lists $ do- let idx = length lists - 1- list <- L.append task <$> lists !? idx- pure $ updateLists idx list lists--analyse :: Text -> Lists -> Text-analyse filepath lists =- concat- [ filepath- , "\n"- , "Lists: "- , tshow $ length lists- , "\n"- , "Tasks: "- , tshow $ foldl' (+) 0 (L.count <$> lists)- ]
− src/Data/Taskell/Seq.hs
@@ -1,24 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Data.Taskell.Seq where--import ClassyPrelude--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--shiftBy :: Int -> Int -> Seq a -> Maybe (Seq a)-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
@@ -1,14 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Data.Taskell.Subtask- ( Subtask- , Update- , new- , blank- , name- , complete- , toggle- , duplicate- ) where--import Data.Taskell.Subtask.Internal
− src/Data/Taskell/Subtask/Internal.hs
@@ -1,31 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.Subtask.Internal where--import ClassyPrelude-import Control.Lens (makeLenses, (%~))--data Subtask = Subtask- { _name :: Text- , _complete :: Bool- } deriving (Show, Eq)--type Update = Subtask -> Subtask---- create lenses-$(makeLenses ''Subtask)---- operations-blank :: Subtask-blank = Subtask "" False--new :: Text -> Bool -> Subtask-new = Subtask--toggle :: Update-toggle = complete %~ not--duplicate :: Subtask -> Subtask-duplicate (Subtask n c) = Subtask n c
− src/Data/Taskell/Task.hs
@@ -1,29 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Data.Taskell.Task- ( Task- , Update- , name- , description- , due- , subtasks- , blank- , new- , create- , duplicate- , setDescription- , appendDescription- , setDue- , clearDue- , getSubtask- , addSubtask- , hasSubtasks- , updateSubtask- , removeSubtask- , countSubtasks- , countCompleteSubtasks- , contains- , isBlank- ) where--import Data.Taskell.Task.Internal
− src/Data/Taskell/Task/Internal.hs
@@ -1,99 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.Task.Internal where--import ClassyPrelude--import Control.Lens (ix, makeLenses, (%~), (&), (.~), (?~), (^.), (^?))--import Data.Sequence as S (adjust', deleteAt, (|>))-import Data.Taskell.Date (Due (..), inputToTime)-import qualified Data.Taskell.Subtask as ST (Subtask, Update, complete, duplicate, name)-import Data.Text (strip)-import Data.Time.Zones (TZ)--data Task = Task- { _name :: Text- , _description :: Maybe Text- , _subtasks :: Seq ST.Subtask- , _due :: Maybe Due- } deriving (Show, Eq)--type Update = Task -> Task---- create lenses-$(makeLenses ''Task)---- operations-create :: Text -> Maybe Due -> Maybe Text -> Seq ST.Subtask -> Task-create name' due' description' subtasks' = Task name' description' subtasks' due'--blank :: Task-blank = Task "" Nothing empty Nothing--new :: Text -> Task-new text = blank & (name .~ text)--setDescription :: Text -> Update-setDescription text =- description .~- if null (strip text)- then Nothing- else Just text--maybeAppend :: Text -> Maybe Text -> Maybe Text-maybeAppend text (Just current) = Just (concat [current, "\n", text])-maybeAppend text Nothing = Just text--appendDescription :: Text -> Update-appendDescription text =- if null (strip text)- then id- else description %~ maybeAppend text--setDue :: TZ -> UTCTime -> Text -> Task -> Maybe Task-setDue tz now date task =- if null date- then Just (task & due .~ Nothing)- else (\d -> task & due ?~ d) <$> inputToTime tz now (strip date)--clearDue :: Update-clearDue task = task & due .~ Nothing--getSubtask :: Int -> Task -> Maybe ST.Subtask-getSubtask idx = (^? subtasks . ix idx)--addSubtask :: ST.Subtask -> Update-addSubtask subtask = subtasks %~ (|> subtask)--hasSubtasks :: Task -> Bool-hasSubtasks = not . null . (^. subtasks)--updateSubtask :: Int -> ST.Update -> Update-updateSubtask idx fn = subtasks %~ adjust' fn idx--removeSubtask :: Int -> Update-removeSubtask idx = subtasks %~ S.deleteAt idx--countSubtasks :: Task -> Int-countSubtasks = length . (^. subtasks)--countCompleteSubtasks :: Task -> Int-countCompleteSubtasks = length . filter (^. ST.complete) . (^. subtasks)--contains :: Text -> Task -> Bool-contains text task =- check (task ^. name) || maybe False check (task ^. description) || not (null sts)- where- check = isInfixOf (toLower text) . toLower- sts = filter check $ (^. ST.name) <$> (task ^. subtasks)--isBlank :: Task -> Bool-isBlank task =- null (task ^. name) &&- isNothing (task ^. description) && null (task ^. subtasks) && isNothing (task ^. due)--duplicate :: Task -> Task-duplicate (Task n d st du) = Task n d (ST.duplicate <$> st) du
− src/Events/Actions.hs
@@ -1,69 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Events.Actions- ( event- , generateActions- , ActionSets- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Graphics.Vty.Input.Events (Event (..))--import Events.State.Types (State, Stateful, mode)-import Events.State.Types.Mode (DetailMode (..), ModalType (..), Mode (..))--import IO.Keyboard (generate)-import IO.Keyboard.Types (Bindings, BoundActions)--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---- takes an event and returns a Maybe State-event' :: Event -> Stateful--- for other events pass through to relevant modules-event' e state =- case state ^. mode of- Normal -> Normal.event e state- Search -> Search.event e state- Insert {} -> Insert.event e state- Modal {} -> Modal.event e state- _ -> pure state---- returns new state if successful-event :: ActionSets -> Event -> State -> State-event actions e state = do- let mEv =- 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 $- case mEv of- Nothing -> event' e state- Just ev -> ev state--data ActionSets = ActionSets- { normal :: BoundActions- , detail :: BoundActions- , help :: BoundActions- , due :: BoundActions- }--generateActions :: Bindings -> ActionSets-generateActions bindings =- ActionSets- { 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
@@ -1,38 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Events.Actions.Insert- ( event- ) where--import ClassyPrelude--import Control.Lens ((&), (.~), (^.))--import Events.State-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.Draw.Field as F (event)--event :: Event -> Stateful-event (EvKey KEnter _) state =- case state ^. mode of- Insert IList ICreate _ ->- (write =<<) . (startCreate =<<) . (newItem =<<) . (store =<<) $ createList state- Insert IList IEdit _ -> (write =<<) . (normalMode =<<) $ finishListTitle state- Insert ITask ICreate _ ->- (write =<<) . (below =<<) . (removeBlank =<<) . (store =<<) $ finishTask state- Insert ITask IEdit _ ->- (write =<<) . (removeBlank =<<) . (normalMode =<<) $ finishTask state- _ -> pure state-event (EvKey KEsc _) state =- case state ^. mode of- Insert IList ICreate _ -> (normalMode =<<) . (write =<<) $ createList state- Insert IList IEdit _ -> (write =<<) . (normalMode =<<) $ finishListTitle state- Insert ITask _ _ -> (write =<<) . (removeBlank =<<) . (normalMode =<<) $ finishTask state- _ -> pure state-event e state =- pure $- case state ^. mode of- Insert iType iMode field -> state & mode .~ Insert iType iMode (F.event e field)- _ -> state
− src/Events/Actions/Modal.hs
@@ -1,27 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Events.Actions.Modal- ( event- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Events.State.Types (Stateful, mode)-import Events.State.Types.Mode (ModalType (..), Mode (Modal))-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- Modal Due {} -> Due.event e s- _ -> pure s
− src/Events/Actions/Modal/Detail.hs
@@ -1,66 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}--module Events.Actions.Modal.Detail- ( event- , events- ) where--import ClassyPrelude--import Events.Actions.Types as A (ActionType (..))-import Events.State (clearDate, normalMode, quit, store, undo, write)-import Events.State.Modal.Detail as Detail-import Events.State.Types-import Events.State.Types.Mode (DetailItem (..), DetailMode (..))-import Graphics.Vty.Input.Events-import IO.Keyboard.Types (Actions)-import qualified UI.Draw.Field as F (event)--events :: Actions-events- -- general- =- [ (A.Quit, quit)- , (A.Undo, (write =<<) . undo)- , (A.Previous, previousSubtask)- , (A.Next, nextSubtask)- , (A.MoveUp, (write =<<) . (up =<<) . store)- , (A.MoveDown, (write =<<) . (down =<<) . store)- , (A.New, (Detail.insertMode =<<) . (Detail.lastSubtask =<<) . (Detail.newItem =<<) . store)- , (A.Edit, (Detail.insertMode =<<) . 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)- ]--normal :: Event -> Stateful-normal (EvKey KEsc _) = normalMode-normal _ = pure--insert :: Event -> Stateful-insert (EvKey KEsc _) s = do- item <- getCurrentItem s- case item of- DetailDescription -> (write =<<) $ finishDescription s- DetailDate -> showDetail s- (DetailItem _) -> (write =<<) . (showDetail =<<) $ finishSubtask s-insert (EvKey KEnter _) s = do- item <- getCurrentItem s- case item of- DetailDescription -> (write =<<) $ finishDescription s- DetailDate -> (write =<<) $ finishDue s- (DetailItem _) ->- (Detail.lastSubtask =<<) . (Detail.newItem =<<) . (store =<<) . (write =<<) $- finishSubtask s-insert e s = updateField (F.event e) s--event :: Event -> Stateful-event e s = do- m <- getCurrentMode s- case m of- DetailNormal -> normal e s- (DetailInsert _) -> insert e s
− src/Events/Actions/Modal/Due.hs
@@ -1,29 +0,0 @@-{-# 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/Modal/Help.hs
@@ -1,22 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}--module Events.Actions.Modal.Help- ( event- , events- ) where--import ClassyPrelude-import Events.Actions.Types as A (ActionType (..))-import Events.State-import Events.State.Types (Stateful)-import Graphics.Vty.Input.Events-import IO.Keyboard.Types (Actions)--events :: Actions-events = [(A.Quit, quit)]--event :: Event -> Stateful-event (EvKey _ _) = normalMode-event _ = pure
− src/Events/Actions/Modal/MoveTo.hs
@@ -1,16 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Events.Actions.Modal.MoveTo- ( event- ) where--import ClassyPrelude-import Events.State-import Events.State.Types (Stateful)-import Graphics.Vty.Input.Events--event :: Event -> Stateful-event (EvKey KEsc _) = normalMode-event (EvKey KEnter _) = normalMode-event (EvKey (KChar c) _) = (normalMode =<<) . (write =<<) . (moveTo c =<<) . store-event _ = pure
− src/Events/Actions/Normal.hs
@@ -1,72 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedLists #-}--module Events.Actions.Normal- ( event- , events- ) where--import ClassyPrelude hiding (delete)--import Data.Char (isDigit)-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)--events :: Actions-events- -- general- =- [ (A.Quit, quit)- , (A.Undo, (write =<<) . undo)- , (A.Redo, (write =<<) . redo)- , (A.Search, searchMode)- , (A.Help, showHelp)- , (A.Due, showDue)- -- navigation- , (A.Previous, previous)- , (A.Next, next)- , (A.Left, left)- , (A.Right, right)- , (A.Bottom, bottom)- , (A.Top, top)- -- new tasks- , (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)- , (A.ListEdit, (editListStart =<<) . store)- , (A.ListDelete, (write =<<) . (deleteCurrentList =<<) . store)- , (A.ListRight, (write =<<) . (listRight =<<) . store)- , (A.ListLeft, (write =<<) . (listLeft =<<) . store)- ]---- Normal-event :: Event -> Stateful--- selecting lists-event (EvKey (KChar n) _)- | isDigit n = selectList n- | otherwise = pure-event (EvKey KEsc _) = clearSearch--- fallback-event _ = pure
− src/Events/Actions/Search.hs
@@ -1,23 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Events.Actions.Search- ( event- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Events.State-import Events.State.Types (Stateful, mode)-import Events.State.Types.Mode (Mode (Search))-import Graphics.Vty.Input.Events-import qualified UI.Draw.Field as F (event)--event :: Event -> Stateful-event (EvKey KEsc _) s = clearSearch =<< normalMode s-event (EvKey KEnter _) s = normalMode s-event e s =- case s ^. mode of- Search -> appendSearch (F.event e) s- _ -> pure s
− src/Events/Actions/Types.hs
@@ -1,82 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Events.Actions.Types where--import ClassyPrelude hiding (Left, Nothing, Right)--data ActionType- = Quit- | Undo- | Redo- | Search- | Due- | Help- | Previous- | Next- | Left- | Right- | Bottom- | Top- | New- | NewAbove- | NewBelow- | Duplicate- | Edit- | Clear- | Delete- | Detail- | DueDate- | ClearDate- | MoveUp- | MoveDown- | MoveLeft- | MoveRight- | Complete- | MoveMenu- | ListNew- | ListEdit- | ListDelete- | ListRight- | ListLeft- | Nothing- deriving (Show, Eq, Ord, Enum)--allActions :: [ActionType]-allActions = [toEnum 0 ..]--read :: Text -> ActionType-read "quit" = Quit-read "undo" = Undo-read "redo" = Redo-read "search" = Search-read "due" = Due-read "help" = Help-read "previous" = Previous-read "next" = Next-read "left" = Left-read "right" = Right-read "bottom" = Bottom-read "top" = Top-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-read "listDelete" = ListDelete-read "listRight" = ListRight-read "listLeft" = ListLeft-read _ = Nothing
− src/Events/State.hs
@@ -1,414 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Events.State- -- App- ( continue- , write- , setTime- , countCurrent- , setHeight- -- UI.Main- , normalise- -- Main- , create- -- Events.Actions.Normal- , quit- , startEdit- , startCreate- , createListStart- , editListStart- , deleteCurrentList- , clearItem- , clearDate- , above- , below- , bottom- , top- , previous- , duplicate- , next- , left- , right- , up- , down- , moveLeft- , moveRight- , moveToLast- , delete- , selectList- , listLeft- , listRight- , undo- , redo- , store- , searchMode- , clearSearch- , appendSearch- -- Events.Actions.Insert- , createList- , removeBlank- , newItem- , normalMode- , finishTask- , finishListTitle- -- Events.Actions.Modal- , showHelp- , showMoveTo- , moveTo- , getCurrentList- , getCurrentTask- , setCurrentTask- ) where--import ClassyPrelude hiding (delete)--import Control.Lens ((%~), (&), (.~), (?~), (^.))--import Data.Char (digitToInt, ord)-import Data.Time.Zones (TZ)--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 qualified Events.State.History as History (redo, store, undo)-import Events.State.Types-import Events.State.Types.Mode (InsertMode (..), InsertType (..), ModalType (..),- Mode (..))-import UI.Draw.Field (Field, blankField, getText, textToField)--type InternalStateful = State -> State--create :: TZ -> UTCTime -> FilePath -> Lists.Lists -> State-create tz t p ls =- State- { _mode = Normal- , _history = fresh ls- , _path = p- , _io = Nothing- , _height = 0- , _searchTerm = Nothing- , _time = t- , _timeZone = tz- }---- app state-quit :: Stateful-quit = pure . (mode .~ Shutdown)--continue :: State -> State-continue = io .~ Nothing--store :: Stateful-store state = pure $ state & history %~ History.store--undo :: Stateful-undo state = pure $ state & history %~ History.undo--redo :: Stateful-redo state = pure $ state & history %~ History.redo--setTime :: UTCTime -> State -> State-setTime t = time .~ t--write :: Stateful-write state = pure $ state & (io ?~ (state ^. lists))---- createList-createList :: Stateful-createList state =- pure $- case state ^. mode of- Insert IList ICreate f ->- updateListToLast . setLists state $ Lists.newList (getText f) $ state ^. lists- _ -> state--updateListToLast :: InternalStateful-updateListToLast state = setCurrentList state (length (state ^. lists) - 1)--createListStart :: Stateful-createListStart = pure . (mode .~ Insert IList ICreate blankField)---- editList-editListStart :: Stateful-editListStart state = do- f <- textToField . (^. L.title) <$> getList state- pure $ state & mode .~ Insert IList IEdit f--deleteCurrentList :: Stateful-deleteCurrentList state =- pure . fixIndex . setLists state $ Lists.delete (getCurrentList state) (state ^. lists)---- insert-getCurrentTask :: State -> Maybe Task-getCurrentTask state = getList state >>= L.getTask (getIndex state)--setCurrentTask :: Task -> Stateful-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 = pure . (mode .~ Insert ITask ICreate blankField)--startEdit :: Stateful-startEdit state = do- field <- textToField . (^. name) <$> getCurrentTask state- pure $ state & mode .~ Insert ITask IEdit field--finishTask :: Stateful-finishTask state =- case state ^. mode of- Insert ITask iMode f ->- setCurrentTaskText (getText f) $ state & (mode .~ Insert ITask iMode blankField)- _ -> pure state--finishListTitle :: Stateful-finishListTitle state =- case state ^. mode of- Insert IList iMode f ->- setCurrentListTitle (getText f) $ state & (mode .~ Insert IList iMode blankField)- _ -> pure state--normalMode :: Stateful-normalMode = pure . (mode .~ Normal)--addToListAt :: Int -> Stateful-addToListAt offset state = do- let idx = getIndex state + offset- fixIndex . setList (setIndex state idx) . L.newAt idx <$> getList state--above :: Stateful-above = addToListAt 0--below :: Stateful-below = addToListAt 1--newItem :: Stateful-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--selectLast :: InternalStateful-selectLast state = setIndex state (countCurrent state - 1)--top :: Stateful-top = pure . selectFirst--selectFirst :: InternalStateful-selectFirst state = setIndex state (0)--removeBlank :: Stateful-removeBlank state = do- currentTask <- getCurrentTask state- (if isBlank currentTask- then delete- else pure)- 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 = moveVertical (-1)--down :: Stateful-down = moveVertical 1--moveHorizontal :: Int -> State -> Maybe State-moveHorizontal idx state =- fixIndex . setLists state <$> Lists.changeList (state ^. current) (state ^. lists) idx--moveLeft :: Stateful-moveLeft = moveHorizontal (-1)--moveRight :: Stateful-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 =- pure $- (if exists- then current .~ (ListIndex list, TaskIndex 0)- else id)- state- where- list = digitToInt idx - 1- exists = Lists.exists list (state ^. lists)---- removing-delete :: Stateful-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 .~ (ListIndex (getCurrentList state), TaskIndex idx)--setCurrentList :: State -> Int -> State-setCurrentList state idx = state & current .~ (ListIndex idx, TaskIndex (getIndex state))--getIndex :: State -> Int-getIndex = showTaskIndex . snd . (^. current)--changeTask :: (Int -> Maybe Text -> L.List -> Int) -> Stateful-changeTask fn state = do- list <- getList state- let idx = getIndex state- let term = getText <$> state ^. searchTerm- pure $ setIndex state (fn idx term list)--next :: Stateful-next = changeTask L.nextTask--previous :: Stateful-previous = changeTask L.prevTask--left :: Stateful-left state =- pure . fixIndex . setCurrentList state $- if list > 0- then pred list- else 0- where- list = getCurrentList state--right :: Stateful-right state =- pure . fixIndex . setCurrentList state $- if list < (count - 1)- then succ list- else list- where- 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 (L.nearest idx trm list)- Nothing -> fixListIndex state- where- trm = getText <$> state ^. searchTerm- idx = getIndex state---- tasks-getCurrentList :: State -> Int-getCurrentList = showListIndex . fst . (^. current)--getList :: State -> Maybe L.List-getList state = Lists.get (state ^. lists) (getCurrentList 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 . (L.title .~ text) <$> getList state--setLists :: State -> Lists.Lists -> State-setLists state lists' = state & lists .~ lists'--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 <- 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- let currentList = getCurrentList state- let lists' = state ^. lists- if currentList + offset < 0 || currentList + offset >= length lists'- then Nothing- else do- let state' = fixIndex $ setCurrentList state (currentList + offset)- setLists state' <$> Lists.shiftBy currentList offset lists'--listLeft :: Stateful-listLeft = listMove (-1)--listRight :: Stateful-listRight = listMove 1---- search-searchMode :: Stateful-searchMode state = pure . fixIndex $ (state & mode .~ Search) & searchTerm .~ sTerm- where- sTerm = pure (fromMaybe blankField (state ^. searchTerm))--clearSearch :: Stateful-clearSearch state = pure $ state & searchTerm .~ Nothing--appendSearch :: (Field -> Field) -> Stateful-appendSearch genField state = do- let field = fromMaybe blankField (state ^. searchTerm)- pure . fixIndex $ state & searchTerm .~ pure (genField field)---- help-showHelp :: Stateful-showHelp = pure . (mode .~ Modal Help)--showMoveTo :: Stateful-showMoveTo state = const (state & mode .~ Modal MoveTo) <$> getCurrentTask state---- view-setHeight :: Int -> State -> State-setHeight = (.~) height---- more view - maybe shouldn't be in here...-newList :: State -> State-newList state =- case state ^. mode of- Insert IList ICreate f ->- let ls = state ^. lists- in fixIndex $ setCurrentList (setLists state (Lists.newList (getText f) ls)) (length ls)- _ -> state--normalise :: State -> State-normalise = newList
− src/Events/State/History.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE RankNTypes #-}--module Events.State.History- ( undo- , redo- , store- ) where--import ClassyPrelude--import Control.Lens (Lens', (&), (.~), (^.))--import Events.State.Types (History (History), future, past, present)--λstack :: Lens' (History a) [a] -> History a -> [a]-λstack fn history = history ^. present : (history ^. fn)--store :: History a -> History a-store history = history & past .~ λstack past history & future .~ empty--undo :: History a -> History a-undo history =- case history ^. past of- [] -> history- (moment:xs) -> History xs moment (λstack future history)--redo :: History a -> History a-redo history =- case history ^. future of- [] -> history- (moment:xs) -> History (λstack past history) moment xs
− src/Events/State/Modal/Detail.hs
@@ -1,187 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Events.State.Modal.Detail- ( updateField- , finishSubtask- , finishDescription- , finishDue- , showDetail- , getCurrentItem- , getCurrentMode- , getField- , setComplete- , remove- , insertMode- , editDescription- , editDue- , newItem- , nextSubtask- , previousSubtask- , lastSubtask- , up- , down- ) where--import ClassyPrelude--import Control.Lens ((&), (.~), (^.))--import Data.Taskell.Date (timeToDisplay)-import qualified Data.Taskell.Seq as S-import qualified Data.Taskell.Subtask as ST (blank, name, toggle)-import Data.Taskell.Task (Task, addSubtask, countSubtasks, description, due,- getSubtask, removeSubtask, setDescription, setDue,- subtasks, updateSubtask)-import Events.State (getCurrentTask, setCurrentTask)-import Events.State.Types (State, Stateful, mode, time, timeZone)-import Events.State.Types.Mode (DetailItem (..), DetailMode (..), ModalType (Detail),- Mode (Modal))-import UI.Draw.Field (Field, blankField, getText, textToField)--updateField :: (Field -> Field) -> Stateful-updateField fieldEvent s =- pure $- case s ^. mode of- Modal (Detail detailItem (DetailInsert field)) ->- s & mode .~ Modal (Detail detailItem (DetailInsert (fieldEvent field)))- _ -> s--finishSubtask :: Stateful-finishSubtask state = do- text <- getText <$> getField state- i <- getCurrentSubtask state- task <- updateSubtask i (ST.name .~ text) <$> getCurrentTask state- setCurrentTask task $ state & mode .~ Modal (Detail (DetailItem i) (DetailInsert blankField))--finish :: (Text -> Task -> Task) -> Stateful-finish fn state = do- text <- getText <$> getField state- task <- fn text <$> getCurrentTask state- setCurrentTask task $ state & mode .~ Modal (Detail (DetailItem 0) DetailNormal)--finishDescription :: Stateful-finishDescription = finish setDescription--finishDue :: Stateful-finishDue state = do- let tz = state ^. timeZone- let now = state ^. time- text <- getText <$> getField state- task <- setDue tz now text =<< getCurrentTask state- setCurrentTask task $ state & mode .~ Modal (Detail (DetailItem 0) DetailNormal)--showDetail :: Stateful-showDetail s = do- _ <- getCurrentTask s- let i = fromMaybe 0 $ getCurrentSubtask s- pure $ s & mode .~ Modal (Detail (DetailItem i) DetailNormal)--getCurrentSubtask :: State -> Maybe Int-getCurrentSubtask state =- case state ^. mode of- Modal (Detail (DetailItem i) _) -> Just i- _ -> Nothing--getCurrentItem :: State -> Maybe DetailItem-getCurrentItem state =- case state ^. mode of- Modal (Detail item _) -> Just item- _ -> Nothing--getCurrentMode :: State -> Maybe DetailMode-getCurrentMode state =- case state ^. mode of- Modal (Detail _ m) -> Just m- _ -> Nothing--getField :: State -> Maybe Field-getField state =- case state ^. mode of- Modal (Detail _ (DetailInsert f)) -> Just f- _ -> Nothing--setComplete :: Stateful-setComplete state = do- i <- getCurrentSubtask state- task <- updateSubtask i ST.toggle <$> getCurrentTask state- setCurrentTask task state--remove :: Stateful-remove state = do- i <- getCurrentSubtask state- task <- removeSubtask i <$> getCurrentTask state- state' <- setCurrentTask task state- setIndex state' i--insertMode :: Stateful-insertMode state = do- i <- getCurrentSubtask state- task <- getCurrentTask state- n <- (^. ST.name) <$> getSubtask i task- case state ^. mode of- Modal (Detail (DetailItem i') _) ->- Just (state & mode .~ Modal (Detail (DetailItem i') (DetailInsert (textToField n))))- _ -> Nothing--editDescription :: Stateful-editDescription state = do- summ <- (^. description) <$> getCurrentTask state- let summ' = fromMaybe "" summ- pure $ state & mode .~ Modal (Detail DetailDescription (DetailInsert (textToField summ')))--editDue :: Stateful-editDue state = do- day <- (^. due) <$> getCurrentTask state- let tz = state ^. timeZone- let day' = maybe "" (timeToDisplay tz) day- pure $ state & mode .~ Modal (Detail DetailDate (DetailInsert (textToField day')))--newItem :: Stateful-newItem state = do- task <- addSubtask ST.blank <$> getCurrentTask state- setCurrentTask task state---- list navigation-changeSubtask :: Int -> Stateful-changeSubtask inc state = do- i <- (+ inc) <$> getCurrentSubtask state- setIndex state i--nextSubtask :: Stateful-nextSubtask = changeSubtask 1--previousSubtask :: Stateful-previousSubtask = changeSubtask (-1)--lastSubtask :: Stateful-lastSubtask state = lastIndex state >>= setIndex state--lastIndex :: State -> Maybe Int-lastIndex state = (+ (-1)) . countSubtasks <$> getCurrentTask state--setIndex :: State -> Int -> Maybe State-setIndex state i = do- lst <- lastIndex state- m <- getCurrentMode state- let newIndex- | i > lst = lst- | i < 0 = 0- | otherwise = i- return $ state & mode .~ Modal (Detail (DetailItem newIndex) m)---- moving tasks-moveVertical :: Int -> Stateful-moveVertical dir state = do- task <- getCurrentTask state- let tasks = task ^. subtasks- i <- getCurrentSubtask state- shifted <- S.shiftBy i dir tasks- state' <- setCurrentTask (task & subtasks .~ shifted) state- changeSubtask dir state'--up :: Stateful-up = moveVertical (-1)--down :: Stateful-down = moveVertical 1
− src/Events/State/Modal/Due.hs
@@ -1,68 +0,0 @@-{-# 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
@@ -1,52 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}--module Events.State.Types where--import ClassyPrelude--import Control.Lens (Lens', makeLenses)-import Control.Lens.Combinators (_1, _2)--import Data.Time.Zones (TZ)--import Data.Taskell.Lists (Lists)-import Types (Pointer, startPointer)-import UI.Draw.Field (Field)--import qualified Events.State.Types.Mode as M (Mode)--type Moment = (Pointer, Lists)--data History a = History- { _past :: [a]- , _present :: a- , _future :: [a]- } deriving (Eq, Show)--fresh :: Lists -> History Moment-fresh ls = History empty (startPointer, ls) empty--data State = State- { _mode :: M.Mode- , _history :: History Moment- , _path :: FilePath- , _io :: Maybe Lists- , _height :: Int- , _searchTerm :: Maybe Field- , _time :: UTCTime- , _timeZone :: TZ- } deriving (Eq, Show)---- create lenses-$(makeLenses ''State)--$(makeLenses ''History)--type Stateful = State -> Maybe State--current :: Lens' State Pointer-current = history . present . _1--lists :: Lens' State Lists-lists = history . present . _2
− src/Events/State/Types/Mode.hs
@@ -1,49 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Events.State.Types.Mode where--import ClassyPrelude--import Data.Taskell.Task (Task)-import Types (Pointer)-import UI.Draw.Field (Field)--data DetailMode- = DetailNormal- | DetailInsert Field- deriving (Eq, Show)--data DetailItem- = DetailItem Int- | DetailDescription- | DetailDate- deriving (Eq, Show)--data ModalType- = Help- | MoveTo- | Due (Seq (Pointer, Task))- Int- | Detail DetailItem- DetailMode- deriving (Eq, Show)--data InsertType- = ITask- | IList- deriving (Eq, Show)--data InsertMode- = IEdit- | ICreate- deriving (Eq, Show)--data Mode- = Normal- | Insert InsertType- InsertMode- Field- | Modal ModalType- | Search- | Shutdown- deriving (Eq, Show)
− src/IO/Config.hs
@@ -1,119 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}--module IO.Config where--import ClassyPrelude--import qualified Data.Text.IO as T (readFile)-import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist,- getHomeDirectory)-import System.Environment (lookupEnv)--import Brick (AttrMap)-import Brick.Themes (loadCustomizations, themeToAttrMap)-import Data.FileEmbed (embedFile)-import Data.Ini.Config--import IO.Keyboard (addMissing, badMapping, defaultBindings)-import IO.Keyboard.Parser (bindings)-import IO.Keyboard.Types (Bindings)-import UI.Theme (defaultTheme)--import qualified IO.Config.General as General-import qualified IO.Config.GitHub as GitHub-import qualified IO.Config.Layout as Layout-import qualified IO.Config.Markdown as Markdown-import qualified IO.Config.Trello as Trello--data Config = Config- { general :: General.Config- , layout :: Layout.Config- , markdown :: Markdown.Config- , trello :: Trello.Config- , github :: GitHub.Config- }--debugging :: Config -> Bool-debugging config = General.debug (general config)--defaultConfig :: Config-defaultConfig =- Config- General.defaultConfig- Layout.defaultConfig- Markdown.defaultConfig- Trello.defaultConfig- GitHub.defaultConfig--directoryName :: FilePath-directoryName = "taskell"--legacyConfigPath :: IO FilePath-legacyConfigPath = (</> "." <> directoryName) <$> getHomeDirectory--xdgDefaultConfig :: IO FilePath-xdgDefaultConfig = (</> ".config") <$> getHomeDirectory--xdgConfigPath :: IO FilePath-xdgConfigPath =- (</> directoryName) <$> (fromMaybe <$> xdgDefaultConfig <*> lookupEnv "XDG_CONFIG_HOME")--getDir :: IO FilePath-getDir = legacyConfigPath >>= doesDirectoryExist >>= bool xdgConfigPath legacyConfigPath--themePath :: FilePath -> FilePath-themePath = (</> "theme.ini")--configPath :: FilePath -> FilePath-configPath = (</> "config.ini")--bindingsPath :: FilePath -> FilePath-bindingsPath = (</> "bindings.ini")--setup :: IO Config-setup- -- create config dir- = do- dir <- getDir- createDirectoryIfMissing True dir- -- create config files- create (configPath dir) $(embedFile "templates/config.ini")- create (themePath dir) $(embedFile "templates/theme.ini")- create (bindingsPath dir) $(embedFile "templates/bindings.ini")- -- get config- getConfig--create :: FilePath -> ByteString -> IO ()-create path contents = doesFileExist path >>= flip unless (writeFile path contents)--configParser :: IniParser Config-configParser =- Config <$> General.parser <*> Layout.parser <*> Markdown.parser <*> Trello.parser <*>- GitHub.parser--getConfig :: IO Config-getConfig = do- content <- T.readFile =<< (configPath <$> getDir)- case parseIniFile content configParser of- Right config -> pure config- Left s -> putStrLn (pack $ "config.ini: " <> s) $> defaultConfig--getBindings :: IO Bindings-getBindings = do- bnds <- bindings <$> (T.readFile =<< (bindingsPath <$> getDir))- case addMissing <$> (badMapping =<< bnds) of- Right b -> pure b- Left err ->- putStrLn ("bindings.ini: " <> err <> " - using default bindings") $> defaultBindings---- generate theme-generateAttrMap :: IO AttrMap-generateAttrMap = do- dir <- getDir- customizedTheme <- loadCustomizations (themePath dir) defaultTheme- case customizedTheme of- Right theme -> pure $ themeToAttrMap theme- Left s -> putStrLn (pack $ "theme.ini: " <> s) $> themeToAttrMap defaultTheme
− src/IO/Config/General.hs
@@ -1,30 +0,0 @@-module IO.Config.General- ( Config- , defaultConfig- , parser- , filename- , debug- ) where--import ClassyPrelude--import Data.Ini.Config--import IO.Config.Parser (noEmpty)--data Config = Config- { filename :: FilePath- , debug :: Bool- }--defaultConfig :: Config-defaultConfig = Config {filename = "taskell.md", debug = False}--filenameP :: SectionParser String-filenameP = maybe (filename defaultConfig) unpack . (noEmpty =<<) <$> fieldMb "filename"--debugP :: SectionParser Bool-debugP = fieldFlagDef "debug" False--parser :: IniParser Config-parser = fromMaybe defaultConfig <$> sectionMb "general" (Config <$> filenameP <*> debugP)
− src/IO/Config/GitHub.hs
@@ -1,25 +0,0 @@-module IO.Config.GitHub- ( Config- , defaultConfig- , parser- , token- ) where--import ClassyPrelude--import Data.Ini.Config--import IO.HTTP.GitHub (GitHubToken)--data Config = Config- { token :: Maybe GitHubToken- }--defaultConfig :: Config-defaultConfig = Config {token = Nothing}--tokenP :: SectionParser (Maybe GitHubToken)-tokenP = fieldMb "token"--parser :: IniParser Config-parser = fromMaybe defaultConfig <$> sectionMb "github" (Config <$> tokenP)
− src/IO/Config/Layout.hs
@@ -1,54 +0,0 @@-module IO.Config.Layout- ( Config- , defaultConfig- , parser- , padding- , columnWidth- , columnPadding- , descriptionIndicator- , statusBar- ) where--import ClassyPrelude--import Data.Ini.Config--import IO.Config.Parser (noEmpty, parseText)--data Config = Config- { padding :: Int- , columnWidth :: Int- , columnPadding :: Int- , descriptionIndicator :: Text- , statusBar :: Bool- }--defaultConfig :: Config-defaultConfig =- Config- {padding = 1, columnWidth = 30, columnPadding = 3, descriptionIndicator = "≡", statusBar = True}--paddingP :: SectionParser Int-paddingP = fromMaybe (padding defaultConfig) <$> fieldMbOf "padding" number--columnWidthP :: SectionParser Int-columnWidthP = fromMaybe (columnWidth defaultConfig) <$> fieldMbOf "column_width" number--columnPaddingP :: SectionParser Int-columnPaddingP = fromMaybe (columnPadding defaultConfig) <$> fieldMbOf "column_padding" number--descriptionIndicatorP :: SectionParser Text-descriptionIndicatorP =- fromMaybe (descriptionIndicator defaultConfig) . (noEmpty . parseText =<<) <$>- fieldMb "description_indicator"--statusBarP :: SectionParser Bool-statusBarP = fieldFlagDef "statusbar" (statusBar defaultConfig)--parser :: IniParser Config-parser =- fromMaybe defaultConfig <$>- sectionMb- "layout"- (Config <$> paddingP <*> columnWidthP <*> columnPaddingP <*> descriptionIndicatorP <*>- statusBarP)
− src/IO/Config/Markdown.hs
@@ -1,66 +0,0 @@-module IO.Config.Markdown- ( Config(Config)- , defaultConfig- , parser- , titleOutput- , taskOutput- , descriptionOutput- , dueOutput- , subtaskOutput- , localTimes- ) where--import ClassyPrelude--import Data.Ini.Config--import IO.Config.Parser (noEmpty, parseText)--data Config = Config- { titleOutput :: Text- , taskOutput :: Text- , descriptionOutput :: Text- , dueOutput :: Text- , subtaskOutput :: Text- , localTimes :: Bool- }--defaultConfig :: Config-defaultConfig =- Config- { titleOutput = "##"- , taskOutput = "-"- , descriptionOutput = " >"- , dueOutput = " @"- , subtaskOutput = " *"- , localTimes = False- }--titleOutputP :: SectionParser Text-titleOutputP = fromMaybe (titleOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "title"--taskOutputP :: SectionParser Text-taskOutputP = fromMaybe (taskOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "task"--descriptionOutputP :: SectionParser Text-descriptionOutputP =- fromMaybe (descriptionOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "summary"--dueOutputP :: SectionParser Text-dueOutputP = fromMaybe (dueOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "due"--subtaskOutputP :: SectionParser Text-subtaskOutputP =- fromMaybe (subtaskOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "subtask"--localTimesP :: SectionParser Bool-localTimesP = fieldFlagDef "localTimes" (localTimes defaultConfig)--parser :: IniParser Config-parser =- fromMaybe defaultConfig <$>- sectionMb- "markdown"- (Config <$> titleOutputP <*> taskOutputP <*> descriptionOutputP <*> dueOutputP <*>- subtaskOutputP <*>- localTimesP)
− src/IO/Config/Parser.hs
@@ -1,12 +0,0 @@-module IO.Config.Parser where--import ClassyPrelude--import Data.Text as T (dropAround, strip)--noEmpty :: Text -> Maybe Text-noEmpty "" = Nothing-noEmpty txt = Just txt--parseText :: Text -> Text-parseText = dropAround (== '"') . strip
− src/IO/Config/Trello.hs
@@ -1,25 +0,0 @@-module IO.Config.Trello- ( Config- , defaultConfig- , parser- , token- ) where--import ClassyPrelude--import Data.Ini.Config--import IO.HTTP.Trello (TrelloToken)--data Config = Config- { token :: Maybe TrelloToken- }--defaultConfig :: Config-defaultConfig = Config {token = Nothing}--tokenP :: SectionParser (Maybe TrelloToken)-tokenP = fieldMb "token"--parser :: IniParser Config-parser = fromMaybe defaultConfig <$> sectionMb "trello" (Config <$> tokenP)
− src/IO/HTTP/Aeson.hs
@@ -1,18 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE NoImplicitPrelude #-}--module IO.HTTP.Aeson where--import ClassyPrelude--import Data.Aeson (defaultOptions, fieldLabelModifier)-import qualified Data.Aeson.TH as TH (deriveFromJSON)-import Language.Haskell.TH (Dec, Name, Q)--import Data.FileEmbed (embedFile)--deriveFromJSON :: Name -> Q [Dec]-deriveFromJSON = TH.deriveFromJSON defaultOptions {fieldLabelModifier = drop 1}--parseError :: String -> Text-parseError err = decodeUtf8 $(embedFile "templates/api-error.txt") <> "\n\n" <> pack err
− src/IO/HTTP/GitHub.hs
@@ -1,171 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.HTTP.GitHub- ( GitHubToken- , GitHubIdentifier- , getNextLink- , getLists- ) where--import ClassyPrelude--import Control.Lens ((^.))-import Data.Either (lefts, rights)-import Data.Sequence (mapWithIndex, (!?))-import Data.Text (splitOn, strip)--import Data.Aeson-import UI.CLI (prompt)--import Network.HTTP.Client (requestHeaders)-import Network.HTTP.Simple (getResponseBody, getResponseHeader, getResponseStatusCode, httpBS,- parseRequest)-import Network.HTTP.Types.Header (HeaderName)--import IO.HTTP.Aeson (parseError)-import IO.HTTP.GitHub.AutomatedCard (automatedCardToTask)-import IO.HTTP.GitHub.Card (MaybeCard, content_url, maybeCardToTask)-import IO.HTTP.GitHub.Column (Column, cardsURL, columnToList)-import IO.HTTP.GitHub.Project (Project, columnsURL, name)--import Data.Taskell.List (List)-import Data.Taskell.Lists (Lists)-import Data.Taskell.Task (Task)--type GitHubToken = Text--type GitHubIdentifier = Text--type ReaderGitHubToken a = ReaderT GitHubToken IO a--concatEithers :: [Either String [a]] -> Either String [a]-concatEithers vals =- if null errors- then Right $ concat (rights vals)- else Left $ unlines errors- where- errors = lefts vals--root :: Text-root = "https://api.github.com/"--headers :: ReaderGitHubToken [(HeaderName, ByteString)]-headers = do- token <- ask- pure- [ ("User-Agent", "smallhadroncollider/taskell")- , ("Accept", "application/vnd.github.inertia-preview+json")- , ("Authorization", encodeUtf8 ("token " <> token))- ]--getNextLink :: [ByteString] -> Maybe Text-getNextLink bs = do- lnks <- splitOn "," . decodeUtf8 <$> headMay bs- let rel = "rel=\"next\""- next <- find (isSuffixOf rel) lnks- stripPrefix "<" =<< stripSuffix (">; " <> rel) (strip next)--fetch' :: [ByteString] -> Text -> ReaderGitHubToken (Int, [ByteString])-fetch' bs url = do- initialRequest <- lift $ parseRequest (unpack url)- rHeaders <- headers- let request = initialRequest {requestHeaders = rHeaders}- response <- lift $ httpBS request- let responses = bs <> [getResponseBody response]- case getNextLink (getResponseHeader "Link" response) of- Nothing -> pure (getResponseStatusCode response, responses)- Just lnk -> fetch' responses lnk--fetch :: Text -> ReaderGitHubToken (Int, [ByteString])-fetch = fetch' []--fetchContent :: MaybeCard -> ReaderGitHubToken (Either Text Task)-fetchContent card =- case maybeCardToTask card of- Just tsk -> pure $ Right tsk- Nothing ->- case card ^. content_url of- Nothing -> pure $ Left "Could not parse card"- Just url -> do- (_, body) <- fetch url- case headMay body of- Nothing -> pure $ Left "Could not find card content"- Just is ->- pure . first parseError $ automatedCardToTask <$> eitherDecodeStrict is--getCards :: Text -> ReaderGitHubToken (Either Text [Task])-getCards url = do- (status, body) <- fetch url- case status of- 200 ->- case concatEithers (eitherDecodeStrict <$> body) of- Right cards -> do- cds <- sequence (fetchContent <$> cards)- let (ls, rs) = partitionEithers cds- pure $- if null ls- then Right rs- else Left (unlines ls)- Left err -> pure $ Left (parseError err)- 429 -> pure $ Left "Too many cards"- _ -> pure . Left $ tshow status <> " error while fetching " <> url--addCard :: Column -> ReaderGitHubToken (Either Text List)-addCard column = do- cards <- getCards $ column ^. cardsURL- pure $ columnToList column <$> cards--addCards :: [Column] -> ReaderGitHubToken (Either Text Lists)-addCards columns = (fromList <$>) . sequence <$> traverse addCard columns--getColumns :: Text -> ReaderGitHubToken (Either Text Lists)-getColumns url = do- putStrLn "Fetching project from GitHub..."- (status, body) <- fetch url- case status of- 200 ->- case concatEithers (eitherDecodeStrict <$> body) of- Right columns -> addCards columns- Left err -> pure $ Left (parseError err)- 404 -> pure . Left $ "Could not find GitHub project ."- 401 -> pure . Left $ "You do not have permission to view GitHub project " <> url- _ -> pure . Left $ tshow status <> " error. Cannot fetch columns from GitHub."--printProjects :: Seq Project -> Text-printProjects projects = unlines $ toList display- where- names = (^. name) <$> projects- line i nm = concat ["[", tshow (i + 1), "] ", nm]- display = line `mapWithIndex` names--chooseProject :: [Project] -> ReaderGitHubToken (Either Text Lists)-chooseProject projects = do- let projects' = fromList projects- putStrLn $ printProjects projects'- chosen <- lift $ prompt "Import project"- let project = (projects' !?) =<< (-) 1 <$> readMay chosen- case project of- Nothing -> pure $ Left "Invalid project selected"- Just proj -> getColumns (proj ^. columnsURL)--getLists :: GitHubIdentifier -> ReaderGitHubToken (Either Text Lists)-getLists identifier = do- putStrLn "Fetching project list from GitHub...\n"- let url = concat [root, identifier, "/projects"]- (status, body) <- fetch url- case status of- 200 ->- case concatEithers (eitherDecodeStrict <$> body) of- Right projects ->- if null projects- then pure . Left $ concat ["\nNo projects found for ", identifier, "\n"]- else chooseProject projects- Left err -> pure $ Left (parseError err)- 404 ->- pure . Left $- "Could not find GitHub org/repo. For organisation make sure you use 'orgs/<org-name>' and for repos use 'repos/<username>/<repo-name>'"- 401 ->- pure . Left $- "You do not have permission to view the GitHub projects for " <> identifier- _ -> pure . Left $ tshow status <> " error. Cannot fetch projects from GitHub."
− src/IO/HTTP/GitHub/AutomatedCard.hs
@@ -1,33 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.HTTP.GitHub.AutomatedCard- ( AutomatedCard(AutomatedCard)- , automatedCardToTask- ) where--import ClassyPrelude--import Control.Lens (makeLenses, (^.))--import qualified Data.Taskell.Task as T (Task, new, setDescription)-import IO.HTTP.Aeson (deriveFromJSON)-import IO.HTTP.GitHub.Utility (cleanUp)--data AutomatedCard = AutomatedCard- { _title :: Text- , _body :: Text- } deriving (Eq, Show)---- strip underscores from field labels-$(deriveFromJSON ''AutomatedCard)---- create lenses-$(makeLenses ''AutomatedCard)---- operations-automatedCardToTask :: AutomatedCard -> T.Task-automatedCardToTask automatedCard = T.setDescription (cleanUp (automatedCard ^. body)) task- where- task = T.new $ cleanUp (automatedCard ^. title)
− src/IO/HTTP/GitHub/Card.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.HTTP.GitHub.Card- ( MaybeCard(MaybeCard)- , maybeCardToTask- , content_url- ) where--import ClassyPrelude--import Control.Lens (makeLenses, (^.))--import qualified Data.Taskell.Task as T (Task, new)-import IO.HTTP.Aeson (deriveFromJSON)-import IO.HTTP.GitHub.Utility (cleanUp)--data MaybeCard = MaybeCard- { _note :: Maybe Text- , _content_url :: Maybe Text- } deriving (Eq, Show)---- strip underscores from field labels-$(deriveFromJSON ''MaybeCard)---- create lenses-$(makeLenses ''MaybeCard)---- operations-maybeCardToTask :: MaybeCard -> Maybe T.Task-maybeCardToTask card = T.new . cleanUp <$> card ^. note
− src/IO/HTTP/GitHub/Column.hs
@@ -1,35 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.HTTP.GitHub.Column- ( Column- , columnToList- , cardsURL- ) where--import ClassyPrelude--import Control.Lens (Lens', makeLenses, (^.))--import IO.HTTP.Aeson (deriveFromJSON)--import qualified Data.Taskell.List as L (List, create)-import qualified Data.Taskell.Task as T (Task)--data Column = Column- { _name :: Text- , _cards_url :: Text- } deriving (Eq, Show)---- create Aeson code-$(deriveFromJSON ''Column)---- create lenses-$(makeLenses ''Column)---- operations-cardsURL :: Lens' Column Text-cardsURL = cards_url--columnToList :: Column -> [T.Task] -> L.List-columnToList ls tasks = L.create (ls ^. name) (fromList tasks)
− src/IO/HTTP/GitHub/Project.hs
@@ -1,29 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.HTTP.GitHub.Project- ( Project- , columnsURL- , name- ) where--import ClassyPrelude--import Control.Lens (Lens', makeLenses)--import IO.HTTP.Aeson (deriveFromJSON)--data Project = Project- { _name :: Text- , _columns_url :: Text- } deriving (Eq, Show)---- create Aeson code-$(deriveFromJSON ''Project)---- create lenses-$(makeLenses ''Project)---- operations-columnsURL :: Lens' Project Text-columnsURL = columns_url
− src/IO/HTTP/GitHub/Utility.hs
@@ -1,13 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.HTTP.GitHub.Utility- ( cleanUp- ) where--import ClassyPrelude--import Data.Text (replace)--cleanUp :: Text -> Text-cleanUp txt = replace "\r" "" $ replace "\n" " " txt
− src/IO/HTTP/Trello.hs
@@ -1,105 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.HTTP.Trello- ( TrelloToken- , TrelloBoardID- , getLists- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Data.Aeson-import Network.HTTP.Simple (getResponseBody, getResponseStatusCode, httpBS, parseRequest)--import IO.HTTP.Aeson (parseError)-import IO.HTTP.Trello.Card (Card, idChecklists, setChecklists)-import IO.HTTP.Trello.ChecklistItem (ChecklistItem, checkItems)-import IO.HTTP.Trello.List (List, cards, listToList, setCards)--import Data.Taskell.Lists (Lists)--type ReaderTrelloToken a = ReaderT TrelloToken IO a--type TrelloToken = Text--type TrelloBoardID = Text--type TrelloChecklistID = Text--key :: Text-key = "80dbcf6f88f62cc5639774e13342c20b"--root :: Text-root = "https://api.trello.com/1/"--fullURL :: Text -> ReaderTrelloToken String-fullURL uri = do- token <- ask- pure . unpack $ concat [root, uri, "&key=", key, "&token=", token]--boardURL :: TrelloBoardID -> ReaderTrelloToken String-boardURL board =- fullURL $- concat- [ "boards/"- , board- , "/lists"- , "?cards=open"- , "&card_fields=name,due,desc,idChecklists"- , "&fields=name,cards"- ]--checklistURL :: TrelloChecklistID -> ReaderTrelloToken String-checklistURL checklist =- fullURL $ concat ["checklists/", checklist, "?fields=id", "&checkItem_fields=name,state"]--trelloListsToLists :: [List] -> Lists-trelloListsToLists ls = fromList $ listToList <$> ls--fetch :: String -> IO (Int, ByteString)-fetch url = do- request <- parseRequest url- response <- httpBS request- pure (getResponseStatusCode response, getResponseBody response)--getChecklist :: TrelloChecklistID -> ReaderTrelloToken (Either Text [ChecklistItem])-getChecklist checklist = do- url <- checklistURL checklist- (status, body) <- lift $ fetch url- pure $- case status of- 200 ->- case (^. checkItems) <$> eitherDecodeStrict body of- Right ls -> Right ls- Left err -> Left $ parseError err- 429 -> Left "Too many checklists"- _ -> Left $ tshow status <> " error while fetching checklist " <> checklist--updateCard :: Card -> ReaderTrelloToken (Either Text Card)-updateCard card = (setChecklists card . concat <$>) . sequence <$> checklists- where- checklists = traverse getChecklist (card ^. idChecklists)--updateList :: List -> ReaderTrelloToken (Either Text List)-updateList l = (setCards l <$>) . sequence <$> traverse updateCard (l ^. cards)--getChecklists :: [List] -> ReaderTrelloToken (Either Text [List])-getChecklists ls = sequence <$> traverse updateList ls--getLists :: TrelloBoardID -> ReaderTrelloToken (Either Text Lists)-getLists board = do- putStrLn "Fetching from Trello..."- url <- boardURL board- (status, body) <- lift $ fetch url- case status of- 200 ->- case eitherDecodeStrict body of- Right raw -> fmap trelloListsToLists <$> getChecklists raw- Left err -> pure . Left $ parseError err- 404 ->- pure . Left $ "Could not find Trello board " <> board <> ". Make sure the ID is correct"- 401 -> pure . Left $ "You do not have permission to view Trello board " <> board- _ -> pure . Left $ tshow status <> " error. Cannot fetch from Trello."
− src/IO/HTTP/Trello/Card.hs
@@ -1,45 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.HTTP.Trello.Card- ( Card- , idChecklists- , cardToTask- , setChecklists- ) where--import ClassyPrelude--import Control.Lens (makeLenses, (&), (.~), (^.))--import IO.HTTP.Aeson (deriveFromJSON)-import IO.HTTP.Trello.ChecklistItem (ChecklistItem, checklistItemToSubTask)--import Data.Taskell.Date (isoToTime)-import qualified Data.Taskell.Task as T (Task, due, new, setDescription, subtasks)--data Card = Card- { _name :: Text- , _desc :: Text- , _due :: Maybe Text- , _idChecklists :: [Text]- , _checklists :: Maybe [ChecklistItem]- } deriving (Eq, Show)---- strip underscores from field labels-$(deriveFromJSON ''Card)---- create lenses-$(makeLenses ''Card)---- operations-cardToTask :: Card -> T.Task-cardToTask card =- task & T.due .~ isoToTime (fromMaybe "" (card ^. due)) & T.subtasks .~- fromList (checklistItemToSubTask <$> fromMaybe [] (card ^. checklists))- where- task = T.setDescription (card ^. desc) $ T.new (card ^. name)--setChecklists :: Card -> [ChecklistItem] -> Card-setChecklists card cls = card & checklists .~ Just cls
− src/IO/HTTP/Trello/ChecklistItem.hs
@@ -1,41 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.HTTP.Trello.ChecklistItem- ( ChecklistItem- , checklistItemToSubTask- , checkItems- ) where--import ClassyPrelude--import Control.Lens (makeLenses, (^.))-import qualified Data.Taskell.Subtask as ST (Subtask, new)-import IO.HTTP.Aeson (deriveFromJSON)--data ChecklistItem = ChecklistItem- { _name :: Text- , _state :: Text- } deriving (Eq, Show)---- create Aeson code-$(deriveFromJSON ''ChecklistItem)---- create lenses-$(makeLenses ''ChecklistItem)---- operations-checklistItemToSubTask :: ChecklistItem -> ST.Subtask-checklistItemToSubTask cl = ST.new (cl ^. name) ((cl ^. state) == "complete")---- check list wrapper-newtype ChecklistWrapper = ChecklistWrapper- { _checkItems :: [ChecklistItem]- } deriving (Eq, Show)---- create Aeson code-$(deriveFromJSON ''ChecklistWrapper)---- create lenses-$(makeLenses ''ChecklistWrapper)
− src/IO/HTTP/Trello/List.hs
@@ -1,36 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.HTTP.Trello.List- ( List- , cards- , setCards- , listToList- ) where--import ClassyPrelude--import Control.Lens (makeLenses, (&), (.~), (^.))--import IO.HTTP.Aeson (deriveFromJSON)-import IO.HTTP.Trello.Card (Card, cardToTask)--import qualified Data.Taskell.List as L (List, create)--data List = List- { _name :: Text- , _cards :: [Card]- } deriving (Eq, Show)---- create Aeson code-$(deriveFromJSON ''List)---- create lenses-$(makeLenses ''List)---- operations-setCards :: List -> [Card] -> List-setCards list cs = list & cards .~ cs--listToList :: List -> L.List-listToList ls = L.create (ls ^. name) (fromList $ cardToTask <$> (ls ^. cards))
− src/IO/Keyboard.hs
@@ -1,78 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.Keyboard- ( generate- , defaultBindings- , badMapping- , addMissing- ) where--import ClassyPrelude hiding ((\\))--import Data.Bitraversable (bitraverse)-import Data.List ((\\))--import qualified Events.Actions.Types as A-import IO.Keyboard.Types--generate :: Bindings -> Actions -> BoundActions-generate bindings actions =- mapFromList . catMaybes $ bitraverse bindingToEvent (`lookup` actions) <$> bindings--badMapping :: Bindings -> Either Text Bindings-badMapping bindings =- if null result- then Right bindings- else Left "invalid mapping"- where- result = filter ((== A.Nothing) . snd) bindings--addMissing :: Bindings -> Bindings-addMissing bindings = bindings <> replaced- where- bnd = A.Nothing : (snd <$> bindings)- result = A.allActions \\ bnd- replaced = concat $ replace <$> result--replace :: A.ActionType -> Bindings-replace action = filter ((==) action . snd) defaultBindings--defaultBindings :: Bindings-defaultBindings =- [ (BChar 'q', A.Quit)- , (BChar 'u', A.Undo)- , (BChar 'r', A.Redo)- , (BChar '/', A.Search)- , (BChar '!', A.Due)- , (BChar '?', A.Help)- , (BChar 'k', A.Previous)- , (BChar 'j', A.Next)- , (BChar 'h', A.Left)- , (BChar 'l', A.Right)- , (BChar 'G', A.Bottom)- , (BChar 'g', A.Top)- , (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)- , (BChar 'C', A.Clear)- , (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.Complete)- , (BChar 'm', A.MoveMenu)- , (BChar 'N', A.ListNew)- , (BChar 'E', A.ListEdit)- , (BChar 'X', A.ListDelete)- , (BChar '>', A.ListRight)- , (BChar '<', A.ListLeft)- ]
− src/IO/Keyboard/Parser.hs
@@ -1,46 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TupleSections #-}--module IO.Keyboard.Parser where--import ClassyPrelude--import Data.Attoparsec.Text--import Utility.Parser--import Events.Actions.Types (ActionType, read)-import IO.Keyboard.Types---- utility functions-commentP :: Parser ()-commentP = lexeme $ skipMany ((char '#' <|> char ';') *> manyTill anyChar endOfLine)--stripComments :: Parser a -> Parser a-stripComments p = lexeme $ commentP *> p <* commentP---- ini parser-keyP :: Parser Binding-keyP = lexeme $ BKey <$> (char '<' *> word <* char '>')--charP :: Parser Binding-charP = lexeme $ BChar <$> anyChar--bindingP :: Parser [Binding]-bindingP = lexeme $ (keyP <|> charP) `sepBy` char ','--lineP :: Parser [(Binding, ActionType)]-lineP =- stripComments $ do- name <- read <$> word- _ <- lexeme $ char '='- binds <- bindingP- pure $ (, name) <$> binds--bindingsP :: Parser Bindings-bindingsP = stripComments $ concat <$> many' lineP---- run parser-bindings :: Text -> Either Text Bindings-bindings ini = first (const "Could not parse keyboard bindings.") (parseOnly bindingsP ini)
− src/IO/Keyboard/Types.hs
@@ -1,45 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.Keyboard.Types where--import ClassyPrelude--import Data.Map.Strict (Map)-import Graphics.Vty.Input.Events (Event (..), Key (..))--import qualified Events.Actions.Types as A (ActionType)-import Events.State.Types (Stateful)--data Binding- = BChar Char- | BKey Text- deriving (Eq, Ord)--type Bindings = [(Binding, A.ActionType)]--type Actions = Map A.ActionType Stateful--type BoundActions = Map Event Stateful--instance Show Binding where- show (BChar c) = singleton c- show (BKey "Up") = "↑"- show (BKey "Down") = "↓"- show (BKey "Left") = "←"- show (BKey "Right") = "→"- show (BKey name) = "<" <> unpack name <> ">"--bindingsToText :: Bindings -> A.ActionType -> [Text]-bindingsToText bindings key = tshow . fst <$> toList (filterMap (== key) bindings)--bindingToEvent :: Binding -> Maybe Event-bindingToEvent (BChar char) = pure $ EvKey (KChar char) []-bindingToEvent (BKey "Space") = pure $ EvKey (KChar ' ') []-bindingToEvent (BKey "Backspace") = pure $ EvKey KBS []-bindingToEvent (BKey "Enter") = pure $ EvKey KEnter []-bindingToEvent (BKey "Left") = pure $ EvKey KLeft []-bindingToEvent (BKey "Right") = pure $ EvKey KRight []-bindingToEvent (BKey "Up") = pure $ EvKey KUp []-bindingToEvent (BKey "Down") = pure $ EvKey KDown []-bindingToEvent _ = Nothing
− src/IO/Markdown.hs
@@ -1,8 +0,0 @@-module IO.Markdown- ( parse- , serialize- , MarkdownInfo(MarkdownInfo)- ) where--import IO.Markdown.Parser (parse)-import IO.Markdown.Serializer (MarkdownInfo (MarkdownInfo), serialize)
− src/IO/Markdown/Parser.hs
@@ -1,67 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.Markdown.Parser- ( parse- ) where--import ClassyPrelude--import Data.Attoparsec.Text hiding (parse)--import qualified Data.Taskell.Date as D (Due, textToTime)-import qualified Data.Taskell.List as L (List, create)-import qualified Data.Taskell.Lists as LS (Lists)-import qualified Data.Taskell.Subtask as ST (Subtask, new)-import qualified Data.Taskell.Task as T (Task, create)--import IO.Config.Markdown (Config, descriptionOutput, dueOutput, subtaskOutput, taskOutput,- titleOutput)-import Utility.Parser (lexeme, line)---- config symbol parsing-type Symbol = (Config -> Text) -> Parser ()--symP :: Config -> Symbol-symP config fn = string (fn config) *> char ' ' $> ()---- utility functions-emptyMay :: (MonoFoldable a) => a -> Maybe a-emptyMay a =- if null a- then Nothing- else Just a---- parsers-subtaskCompleteP :: Parser Bool-subtaskCompleteP = (== 'x') <$> (char '[' *> (char 'x' <|> char ' ') <* char ']' <* char ' ')--subtaskP :: Symbol -> Parser ST.Subtask-subtaskP sym = flip ST.new <$> (sym subtaskOutput *> subtaskCompleteP) <*> line--taskDescriptionP :: Symbol -> Parser (Maybe Text)-taskDescriptionP sym = emptyMay <$> (intercalate "\n" <$> many' (sym descriptionOutput *> line))--dueP :: Symbol -> Parser (Maybe D.Due)-dueP sym = (D.textToTime =<<) <$> optional (sym dueOutput *> line)--taskNameP :: Symbol -> Parser Text-taskNameP sym = sym taskOutput *> line--taskP :: Symbol -> Parser T.Task-taskP sym =- T.create <$> taskNameP sym <*> dueP sym <*> taskDescriptionP sym <*>- (fromList <$> many' (subtaskP sym))--listTitleP :: Symbol -> Parser Text-listTitleP sym = lexeme $ sym titleOutput *> line--listP :: Symbol -> Parser L.List-listP sym = L.create <$> listTitleP sym <*> (fromList <$> many' (taskP sym))--markdownP :: Symbol -> Parser LS.Lists-markdownP sym = fromList <$> many1 (listP sym) <* endOfInput---- parse-parse :: Config -> Text -> Either Text LS.Lists-parse config txt = first (const "Could not parse file.") (parseOnly (markdownP (symP config)) txt)
− src/IO/Markdown/Serializer.hs
@@ -1,92 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.Markdown.Serializer- ( serialize- , MarkdownInfo(MarkdownInfo)- ) where--import ClassyPrelude--import Control.Lens ((^.))--import qualified Data.Text as T (splitOn)--import Data.Time.Zones (TZ)--import Data.Taskell.Date (Due, timeToOutput, timeToOutputLocal)-import Data.Taskell.List (List, tasks, title)-import Data.Taskell.Lists (Lists)-import qualified Data.Taskell.Subtask as ST (Subtask, complete, name)-import qualified Data.Taskell.Task as T (Task, description, due, name, subtasks)--import IO.Config.Markdown (Config, descriptionOutput, dueOutput, localTimes, subtaskOutput,- taskOutput, titleOutput)--data MarkdownInfo = MarkdownInfo- { mdTZ :: TZ- , mdConfig :: Config- }--type ReaderMarkdown = Reader MarkdownInfo---- utility functions-askConf :: (Config -> a) -> ReaderMarkdown a-askConf fn = fn <$> asks mdConfig--strMay :: (Applicative m) => (a -> m Text) -> Maybe a -> m Text-strMay _ Nothing = pure ""-strMay fn (Just a) = fn a--space :: Text -> Text -> Text-space symbol txt = symbol <> " " <> txt--timeFn :: ReaderMarkdown (Due -> Text)-timeFn = bool timeToOutput <$> (timeToOutputLocal <$> asks mdTZ) <*> askConf localTimes---- serializers-subtaskCompleteS :: Bool -> Text-subtaskCompleteS True = "[x]"-subtaskCompleteS False = "[ ]"--subtaskS :: ST.Subtask -> ReaderMarkdown Text-subtaskS st = do- symbol <- askConf subtaskOutput- pure $ unwords [symbol, subtaskCompleteS (st ^. ST.complete), st ^. ST.name]--subtasksS :: Seq ST.Subtask -> ReaderMarkdown Text-subtasksS sts = intercalate "\n" <$> sequence (subtaskS <$> sts)--descriptionS :: Text -> ReaderMarkdown Text-descriptionS desc = do- symbol <- askConf descriptionOutput- pure . intercalate "\n" $ space symbol <$> T.splitOn "\n" desc--dueS :: Due -> ReaderMarkdown Text-dueS due = do- symbol <- askConf dueOutput- fn <- timeFn- pure $ space symbol (fn due)--nameS :: Text -> ReaderMarkdown Text-nameS desc = space <$> askConf taskOutput <*> pure desc--taskS :: T.Task -> ReaderMarkdown Text-taskS t =- unlines . filter (/= "") <$>- sequence- [ nameS (t ^. T.name)- , strMay dueS (t ^. T.due)- , strMay descriptionS (t ^. T.description)- , subtasksS (t ^. T.subtasks)- ]--listS :: List -> ReaderMarkdown Text-listS list = do- symbol <- askConf titleOutput- taskString <- concat <$> sequence (taskS <$> list ^. tasks)- pure . space symbol $ concat [list ^. title, "\n\n", taskString]---- serialize-serialize :: Lists -> ReaderMarkdown Text-serialize ls = intercalate "\n" <$> sequence (listS <$> ls)
− src/IO/Taskell.hs
@@ -1,159 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.Taskell where--import ClassyPrelude--import Control.Monad.Reader (runReader)-import Data.FileEmbed (embedFile)-import Data.Text.Encoding (decodeUtf8With)-import System.Directory (doesFileExist, getCurrentDirectory)--import Data.Time.Zones (TZ)--import Config (usage, version)-import Data.Taskell.Lists (Lists, analyse, initial)--import IO.Config (Config, general, github, markdown, trello)-import IO.Config.General (filename)-import qualified IO.Config.GitHub as GitHub (token)-import qualified IO.Config.Trello as Trello (token)--import IO.Markdown (MarkdownInfo (MarkdownInfo), parse, serialize)--import qualified IO.HTTP.GitHub as GitHub (GitHubIdentifier, getLists)-import qualified IO.HTTP.Trello as Trello (TrelloBoardID, getLists)--import UI.CLI (PromptYN (PromptYes), promptYN)--data IOInfo = IOInfo- { ioTZ :: TZ- , ioConfig :: Config- }--type ReaderConfig a = ReaderT IOInfo IO a--data Next- = Output Text- | Error Text- | Load FilePath- Lists- | Exit--parseArgs :: [Text] -> ReaderConfig Next-parseArgs ["-v"] = pure $ Output version-parseArgs ["-h"] = pure $ Output usage-parseArgs ["-t", boardID, file] = loadTrello boardID file-parseArgs ["-g", identifier, file] = loadGitHub identifier file-parseArgs ["-i", file] = fileInfo file-parseArgs [file] = loadFile file-parseArgs [] = (pack . filename . general <$> asks ioConfig) >>= loadFile-parseArgs _ = pure $ Error (unlines ["Invalid options", "", usage])--load :: ReaderConfig Next-load = getArgs >>= parseArgs--colonic :: FilePath -> Text -> Text-colonic path = ((pack path <> ": ") <>)--loadFile :: Text -> ReaderConfig Next-loadFile filepath = do- mPath <- exists filepath- case mPath of- Nothing -> pure Exit- Just path -> either (Error . colonic path) (Load path) <$> readData path--loadRemote :: (token -> FilePath -> ReaderConfig Next) -> token -> Text -> ReaderConfig Next-loadRemote createFn identifier filepath = do- let path = unpack filepath- exists' <- fileExists path- if exists'- then pure $ Error (filepath <> " already exists")- else createFn identifier path--loadTrello :: Trello.TrelloBoardID -> Text -> ReaderConfig Next-loadTrello = loadRemote createTrello--loadGitHub :: GitHub.GitHubIdentifier -> Text -> ReaderConfig Next-loadGitHub = loadRemote createGitHub--fileInfo :: Text -> ReaderConfig Next-fileInfo filepath = do- let path = unpack filepath- exists' <- fileExists path- if exists'- then either (Error . colonic path) (Output . analyse filepath) <$> readData path- else pure $ Error (filepath <> " does not exist")--createRemote ::- (Config -> Maybe token)- -> Text- -> (token -> ReaderT token IO (Either Text Lists))- -> token- -> FilePath- -> ReaderConfig Next-createRemote tokenFn missingToken getFn identifier path = do- config <- asks ioConfig- tz <- asks ioTZ- case tokenFn config of- Nothing -> pure $ Error missingToken- Just token -> do- lists <- lift $ runReaderT (getFn identifier) token- case lists of- Left txt -> pure $ Error txt- Right ls ->- promptCreate path >>=- bool (pure Exit) (Load path ls <$ lift (writeData tz config ls path))--createTrello :: Trello.TrelloBoardID -> FilePath -> ReaderConfig Next-createTrello =- createRemote- (Trello.token . trello)- (decodeUtf8 $(embedFile "templates/trello-token.txt"))- Trello.getLists--createGitHub :: GitHub.GitHubIdentifier -> FilePath -> ReaderConfig Next-createGitHub =- createRemote- (GitHub.token . github)- (decodeUtf8 $(embedFile "templates/github-token.txt"))- GitHub.getLists--exists :: Text -> ReaderConfig (Maybe FilePath)-exists filepath = do- let path = unpack filepath- exists' <- fileExists path- if exists'- then pure $ Just path- else promptCreate path >>= bool (pure Nothing) (Just path <$ createPath path)--fileExists :: FilePath -> ReaderConfig Bool-fileExists path = lift $ doesFileExist path--promptCreate :: FilePath -> ReaderConfig Bool-promptCreate path = do- cwd <- lift $ pack <$> getCurrentDirectory- lift $ promptYN PromptYes $ concat ["Create ", cwd, "/", pack path, "?"]---- creates taskell file-createPath :: FilePath -> ReaderConfig ()-createPath path = do- config <- asks ioConfig- tz <- asks ioTZ- lift (writeData tz config initial path)---- writes Tasks to json file-writeData :: TZ -> Config -> Lists -> FilePath -> IO ()-writeData tz config tasks path = void (writeFile path output)- where- output = encodeUtf8 $ runReader (serialize tasks) (MarkdownInfo tz (markdown config))---- reads json file-decodeError :: String -> Maybe Word8 -> Maybe Char-decodeError _ _ = Just '\65533'--readData :: FilePath -> ReaderConfig (Either Text Lists)-readData path =- parse <$> (markdown <$> asks ioConfig) <*> (decodeUtf8With decodeError <$> readFile path)
+ src/Taskell.hs view
@@ -0,0 +1,179 @@+module Taskell+ ( go+ ) where++import ClassyPrelude++import Control.Concurrent (forkIO, threadDelay)++import Control.Lens ((^.))++import Data.Time.Zones (TZ)++import Brick+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 Taskell.Data.Lists (Lists)+import Taskell.Events.Actions (ActionSets, event, generateActions)+import Taskell.Events.State (continue, countCurrent, setHeight, setTime)+import Taskell.Events.State.Types (State, current, io, lists, mode, path, searchTerm, timeZone)+import Taskell.Events.State.Types.Mode (InsertMode (..), InsertType (..), ModalType (..), Mode (..))+import Taskell.IO (writeData)+import Taskell.IO.Config (Config, debugging, generateAttrMap, getBindings, layout)+import Taskell.Types (ListIndex (..), TaskIndex (..))+import Taskell.UI.Draw (chooseCursor, draw)+import Taskell.UI.Types (ResourceName (..))++type DebouncedMessage = (Lists, FilePath, TZ)++type DebouncedWrite = DebouncedMessage -> IO ()++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, tz) = writeData tz config ls pth++next :: DebouncedWrite -> State -> EventM ResourceName (Next State)+next send state =+ case state ^. io of+ Just ls -> do+ invalidateCache+ liftIO $ send (ls, state ^. path, state ^. timeZone)+ Brick.continue $ Taskell.Events.State.continue state+ Nothing -> Brick.continue state++-- debouncing+debounce :: Config -> State -> IO (DebouncedWrite, Trigger)+debounce config initial = do+ trigger <-+ Debounce.new+ Debounce.Args+ { Debounce.cb = store config+ , Debounce.fold = flip const+ , Debounce.init = (initial ^. lists, initial ^. path, initial ^. timeZone)+ }+ Debounce.def+ let send = Debounce.send trigger+ pure (send, trigger)++-- cache clearing+clearCache :: State -> EventM ResourceName ()+clearCache state = do+ let (ListIndex li, TaskIndex ti) = state ^. current+ invalidateCacheEntry (RNList li)+ invalidateCacheEntry (RNTask (ListIndex li, TaskIndex ti))++clearAllTitles :: State -> EventM ResourceName ()+clearAllTitles state = do+ let count = length (state ^. lists)+ let range = [0 .. (count - 1)]+ traverse_ (invalidateCacheEntry . RNList) range+ traverse_ (invalidateCacheEntry . RNTask . (, TaskIndex (-1)) . ListIndex) range++clearList :: State -> EventM ResourceName ()+clearList state = do+ 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)+handleVtyEvent (send, trigger) actions previousState e = do+ let state = event actions e previousState+ when (previousState ^. searchTerm /= state ^. searchTerm) invalidateCache+ case previousState ^. mode of+ (Modal MoveTo) -> clearAllTitles previousState+ (Insert ITask ICreate _) -> clearList previousState+ _ -> 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++getHeight :: EventM ResourceName Int+getHeight = snd <$> (liftIO . displayBounds =<< outputIface <$> getVtyHandle)++handleEvent ::+ (DebouncedWrite, Trigger)+ -> ActionSets+ -> State+ -> 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+ Brick.continue (setHeight h state)+handleEvent db actions state (VtyEvent ev) = handleVtyEvent db actions state ev+handleEvent _ _ state _ = Brick.continue state++-- | Runs when the app starts+-- Adds paste support+appStart :: State -> EventM ResourceName State+appStart state = do+ output <- outputIface <$> getVtyHandle+ when (supportsMode output BracketedPaste) . liftIO $ setMode output BracketedPaste True+ h <- getHeight+ pure (setHeight h state)++-- | Sets up Brick+go :: Config -> State -> IO ()+go config initial = do+ attrMap' <- const <$> generateAttrMap+ -- 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 (debugging config)+ , appChooseCursor = chooseCursor+ , appHandleEvent = handleEvent db (generateActions bindings)+ , appStartEvent = appStart+ , appAttrMap = attrMap'+ }+ -- start+ let builder = mkVty defaultConfig+ initialVty <- builder+ void $ customMain initialVty builder (Just timerChan) app initial
+ src/Taskell/Config.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.Config where++import ClassyPrelude++import Data.FileEmbed (embedFile)++version :: Text+version = "1.9.3"++usage :: Text+usage = decodeUtf8 $(embedFile "templates/usage.txt")
+ src/Taskell/Data/Date.hs view
@@ -0,0 +1,112 @@+module Taskell.Data.Date+ ( Day+ , Deadline(..)+ , Due(..)+ , timeToText+ , timeToDisplay+ , timeToOutput+ , timeToOutputLocal+ , textToTime+ , inputToTime+ , isoToTime+ , deadline+ ) where++import ClassyPrelude++import Control.Monad.Fail (MonadFail)++import Data.Time.LocalTime (ZonedTime (ZonedTime))+import Data.Time.Zones (TZ, localTimeToUTCTZ, timeZoneForUTCTime, utcToLocalTimeTZ)++import Data.Time.Calendar (diffDays)+import Data.Time.Format (FormatTime, ParseTime, iso8601DateFormat)++import Taskell.Data.Date.RelativeParser (parseRelative)+import Taskell.Data.Date.Types (Deadline (..), Due (..))++-- formats+dateFormat :: String+dateFormat = "%Y-%m-%d"++timeDisplayFormat :: String+timeDisplayFormat = "%Y-%m-%d %H:%M"++timeFormat :: String+timeFormat = "%Y-%m-%d %H:%M %Z"++isoFormat :: String+isoFormat = iso8601DateFormat (Just "%H:%M:%S%Q%Z")++-- utility functions+utcToZonedTime :: TZ -> UTCTime -> ZonedTime+utcToZonedTime tz time = ZonedTime (utcToLocalTimeTZ tz time) (timeZoneForUTCTime tz time)++appendYear :: (FormatTime t, FormatTime s) => String -> t -> s -> String+appendYear txt t1 t2 =+ if format "%Y" t1 == format "%Y" t2+ then txt+ else txt <> " %Y"++-- output+format :: (FormatTime t) => String -> t -> Text+format fmt = pack . formatTime defaultTimeLocale fmt++timeToText :: TZ -> UTCTime -> Due -> Text+timeToText _ now (DueDate day) = format fmt day+ where+ fmt = appendYear "%d-%b" now day+timeToText tz now (DueTime time) = format fmt local+ where+ local = utcToLocalTimeTZ tz time+ fmt = appendYear "%H:%M %d-%b" now local++timeToDisplay :: TZ -> Due -> Text+timeToDisplay _ (DueDate day) = format dateFormat day+timeToDisplay tz (DueTime time) = format timeDisplayFormat (utcToLocalTimeTZ tz time)++timeToOutput :: Due -> Text+timeToOutput (DueDate day) = format dateFormat day+timeToOutput (DueTime time) = format timeFormat time++timeToOutputLocal :: TZ -> Due -> Text+timeToOutputLocal _ (DueDate day) = format dateFormat day+timeToOutputLocal tz (DueTime time) = format timeFormat (utcToZonedTime tz time)++-- input+parseT :: (Monad m, MonadFail m, ParseTime t) => String -> Text -> m t+parseT fmt txt = parseTimeM False defaultTimeLocale fmt (unpack txt)++parseDate :: Text -> Maybe Due+parseDate txt = DueDate <$> parseT dateFormat txt++(<?>) :: Maybe a -> Maybe a -> Maybe a+(<?>) Nothing b = b+(<?>) a _ = a++textToTime :: Text -> Maybe Due+textToTime txt = parseDate txt <?> (DueTime <$> parseT timeFormat txt)++inputToTime :: TZ -> UTCTime -> Text -> Maybe Due+inputToTime tz now txt =+ parseDate txt <?> (DueTime . localTimeToUTCTZ tz <$> parseT timeDisplayFormat txt) <?>+ case parseRelative now txt of+ Right due -> Just due+ Left _ -> Nothing++isoToTime :: Text -> Maybe Due+isoToTime txt = DueTime <$> parseT isoFormat txt++-- deadlines+deadline :: UTCTime -> Due -> Deadline+deadline now date+ | days < 0 = Passed+ | days == 0 = Today+ | days == 1 = Tomorrow+ | days < 7 = ThisWeek+ | otherwise = Plenty+ where+ days =+ case date of+ DueTime t -> diffDays (utctDay t) (utctDay now)+ DueDate d -> diffDays d (utctDay now)
+ src/Taskell/Data/Date/RelativeParser.hs view
@@ -0,0 +1,64 @@+module Taskell.Data.Date.RelativeParser+ ( parseRelative+ ) where++import ClassyPrelude++import Data.Attoparsec.Text++import Data.Time.Clock (addUTCTime)++import Taskell.Data.Date.Types (Due (DueDate, DueTime))+import Taskell.Utility.Parser (lexeme, only)++-- utility functions+addP :: (Integral a) => Parser a -> UTCTime -> Parser UTCTime+addP p now = ($ now) . addUTCTime . fromIntegral . sum <$> many1 p++-- relative time parsing+minute :: Int+minute = 60++hour :: Int+hour = minute * 60++day :: Int+day = hour * 24++week :: Int+week = day * 7++timePeriodP :: Char -> Parser Int+timePeriodP c = lexeme decimal <* char c++wP :: Parser Int+wP = (* week) <$> timePeriodP 'w'++dP :: Parser Int+dP = (* day) <$> timePeriodP 'd'++hP :: Parser Int+hP = (* hour) <$> timePeriodP 'h'++mP :: Parser Int+mP = (* minute) <$> timePeriodP 'm'++sP :: Parser Int+sP = timePeriodP 's'++timeP :: UTCTime -> Parser (Maybe Due)+timeP now = only . lexeme $ Just . DueTime <$> addP (sP <|> mP <|> hP <|> dP <|> wP) now++-- relative date parsing+dateP :: UTCTime -> Parser (Maybe Due)+dateP now = only . lexeme $ Just . DueDate . utctDay <$> addP (dP <|> wP) now++-- relative parser+relativeP :: UTCTime -> Parser (Maybe Due)+relativeP now = dateP now <|> timeP now++parseRelative :: UTCTime -> Text -> Either Text Due+parseRelative now text =+ case parseOnly (relativeP now) text of+ Right (Just due) -> Right due+ _ -> Left "Could not parse date."
+ src/Taskell/Data/Date/Types.hs view
@@ -0,0 +1,25 @@+module Taskell.Data.Date.Types+ ( Deadline(..)+ , Due(..)+ ) where++import ClassyPrelude++data Due+ = DueTime UTCTime+ | DueDate Day+ deriving (Show, Eq)++instance Ord Due where+ compare (DueTime t) (DueDate d) = t `compare` UTCTime d 0+ compare (DueDate d) (DueTime t) = UTCTime d 0 `compare` t+ compare (DueDate d1) (DueDate d2) = d1 `compare` d2+ compare (DueTime t1) (DueTime t2) = t1 `compare` t2++data Deadline+ = Passed+ | Today+ | Tomorrow+ | ThisWeek+ | Plenty+ deriving (Show, Eq)
+ src/Taskell/Data/List.hs view
@@ -0,0 +1,28 @@+module Taskell.Data.List+ ( List+ , Update+ , title+ , tasks+ , create+ , empty+ , due+ , clearDue+ , new+ , count+ , newAt+ , duplicate+ , append+ , prepend+ , extract+ , updateFn+ , update+ , move+ , deleteTask+ , getTask+ , searchFor+ , nextTask+ , prevTask+ , nearest+ ) where++import Taskell.Data.List.Internal
+ src/Taskell/Data/List/Internal.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.Data.List.Internal where++import ClassyPrelude++import Control.Lens (element, makeLenses, (%%~), (%~), (&), (.~), (^.), (^?))++import Data.Sequence as S (adjust', deleteAt, insertAt, update, (<|), (|>))++import qualified Taskell.Data.Seq as S+import qualified Taskell.Data.Task as T (Task, Update, blank, clearDue, contains, due, duplicate)+import Taskell.Types (TaskIndex (TaskIndex))++data List = List+ { _title :: Text+ , _tasks :: Seq T.Task+ } deriving (Show, Eq)++type Update = List -> List++-- create lenses+$(makeLenses ''List)++-- operations+create :: Text -> Seq T.Task -> List+create = List++empty :: Text -> List+empty text = List text ClassyPrelude.empty++new :: Update+new = append T.blank++count :: List -> Int+count = length . (^. tasks)++due :: List -> Seq (TaskIndex, T.Task)+due list = catMaybes (filt S.<#> (list ^. tasks))+ where+ filt int task = const (TaskIndex int, task) <$> task ^. T.due++clearDue :: TaskIndex -> Update+clearDue (TaskIndex int) = updateFn int T.clearDue++newAt :: Int -> Update+newAt idx = tasks %~ S.insertAt idx T.blank++duplicate :: Int -> List -> Maybe List+duplicate idx list = do+ task <- T.duplicate <$> getTask idx list+ pure $ list & tasks %~ S.insertAt idx task++append :: T.Task -> Update+append task = tasks %~ (S.|> task)++prepend :: T.Task -> Update+prepend task = tasks %~ (task S.<|)++extract :: Int -> List -> Maybe (List, T.Task)+extract idx list = do+ (xs, x) <- S.extract idx (list ^. tasks)+ pure (list & tasks .~ xs, x)++updateFn :: Int -> T.Update -> Update+updateFn idx fn = tasks %~ adjust' fn idx++update :: Int -> T.Task -> Update+update idx task = tasks %~ S.update idx task++move :: Int -> Int -> Maybe Text -> List -> Maybe (List, Int)+move current dir term list =+ case term of+ Nothing -> (, bound list (current + dir)) <$> (list & tasks %%~ S.shiftBy current dir)+ Just _ -> do+ idx <- changeTask dir current term list+ (, idx) <$> (list & tasks %%~ S.shiftBy current (idx - current))++deleteTask :: Int -> Update+deleteTask idx = tasks %~ deleteAt idx++getTask :: Int -> List -> Maybe T.Task+getTask idx = (^? tasks . element idx)++searchFor :: Text -> Update+searchFor text = tasks %~ filter (T.contains text)++changeTask :: Int -> Int -> Maybe Text -> List -> Maybe Int+changeTask dir current term list = do+ let next = current + dir+ tsk <- getTask next list+ case term of+ Nothing -> Just next+ Just trm ->+ if T.contains trm tsk+ then Just next+ else changeTask dir next term list++nextTask :: Int -> Maybe Text -> List -> Int+nextTask idx text lst = fromMaybe idx $ changeTask 1 idx text lst++prevTask :: Int -> Maybe Text -> List -> Int+prevTask idx text lst = fromMaybe idx $ changeTask (-1) idx text lst++closest :: Int -> Int -> Int -> Int+closest current previous next =+ if (next - current) < (current - previous)+ then next+ else previous++bound :: List -> Int -> Int+bound lst = S.bound (lst ^. tasks)++nearest' :: Int -> Maybe Text -> List -> Maybe Int+nearest' current term lst = do+ let prev = changeTask (-1) current term lst+ let nxt = changeTask 1 current term lst+ let comp idx = Just $ maybe idx (closest current idx) nxt+ maybe nxt comp prev++nearest :: Int -> Maybe Text -> List -> Int+nearest current term lst = idx+ where+ near = fromMaybe (-1) $ nearest' current term lst+ idx =+ case term of+ Nothing -> bound lst current+ Just txt -> maybe near (bool near current . T.contains txt) $ getTask current lst
+ src/Taskell/Data/Lists.hs view
@@ -0,0 +1,20 @@+module Taskell.Data.Lists+ ( Lists+ , ListPosition(..)+ , initial+ , updateLists+ , count+ , due+ , clearDue+ , get+ , changeList+ , newList+ , delete+ , exists+ , shiftBy+ , search+ , appendToLast+ , analyse+ ) where++import Taskell.Data.Lists.Internal
+ src/Taskell/Data/Lists/Internal.hs view
@@ -0,0 +1,89 @@+module Taskell.Data.Lists.Internal where++import ClassyPrelude++import Control.Lens ((^.))+import Data.Sequence as S (adjust', deleteAt, update, (!?), (|>))++import qualified Taskell.Data.List as L (List, Update, append, clearDue, count, due, empty, extract,+ prepend, searchFor)+import qualified Taskell.Data.Seq as S+import qualified Taskell.Data.Task as T (Task, due)+import Taskell.Types (ListIndex (ListIndex), Pointer, TaskIndex (TaskIndex))++type Lists = Seq L.List++type Update = Lists -> Lists++data ListPosition+ = Top+ | Bottom++initial :: Lists+initial = fromList [L.empty "To Do", L.empty "Done"]++updateLists :: Int -> L.List -> Update+updateLists = S.update++count :: Int -> Lists -> Int+count idx tasks = maybe 0 L.count (tasks !? idx)++due :: Lists -> Seq (Pointer, T.Task)+due lists = sortOn ((^. T.due) . snd) dues+ where+ format x lst = (\(y, t) -> ((ListIndex x, y), t)) <$> L.due lst+ dues = concat $ format S.<#> lists++clearDue :: Pointer -> Update+clearDue (idx, tsk) = updateFn idx (L.clearDue tsk)++updateFn :: ListIndex -> L.Update -> Update+updateFn (ListIndex idx) fn = adjust' fn idx++get :: Lists -> Int -> Maybe L.List+get = (!?)++changeList :: ListPosition -> Pointer -> Lists -> Int -> Maybe Lists+changeList pos (ListIndex list, TaskIndex idx) tasks dir = do+ let next = list + dir+ let fn =+ case pos of+ Top -> L.prepend+ Bottom -> L.append+ (from, task) <- L.extract idx =<< tasks !? list -- extract current task+ to <- fn task <$> tasks !? next -- get next list and append task+ pure . updateLists next to $ updateLists list from tasks -- update lists++newList :: Text -> Update+newList title = (|> L.empty title)++delete :: Int -> Update+delete = deleteAt++exists :: Int -> Lists -> Bool+exists idx tasks = isJust $ tasks !? idx++shiftBy :: Int -> Int -> Lists -> Maybe Lists+shiftBy = S.shiftBy++search :: Text -> Update+search text = (L.searchFor text <$>)++appendToLast :: T.Task -> Update+appendToLast task lists =+ fromMaybe lists $ do+ let idx = length lists - 1+ list <- L.append task <$> lists !? idx+ pure $ updateLists idx list lists++analyse :: Text -> Lists -> Text+analyse filepath lists =+ concat+ [ filepath+ , "\n"+ , "Lists: "+ , tshow $ length lists+ , "\n"+ , "Tasks: "+ , tshow $ foldl' (+) 0 (L.count <$> lists)+ ]
+ src/Taskell/Data/Seq.hs view
@@ -0,0 +1,22 @@+module Taskell.Data.Seq where++import ClassyPrelude++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++shiftBy :: Int -> Int -> Seq a -> Maybe (Seq a)+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/Taskell/Data/Subtask.hs view
@@ -0,0 +1,12 @@+module Taskell.Data.Subtask+ ( Subtask+ , Update+ , new+ , blank+ , name+ , complete+ , toggle+ , duplicate+ ) where++import Taskell.Data.Subtask.Internal
+ src/Taskell/Data/Subtask/Internal.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.Data.Subtask.Internal where++import ClassyPrelude+import Control.Lens (makeLenses, (%~))++data Subtask = Subtask+ { _name :: Text+ , _complete :: Bool+ } deriving (Show, Eq)++type Update = Subtask -> Subtask++-- create lenses+$(makeLenses ''Subtask)++-- operations+blank :: Subtask+blank = Subtask "" False++new :: Text -> Bool -> Subtask+new = Subtask++toggle :: Update+toggle = complete %~ not++duplicate :: Subtask -> Subtask+duplicate (Subtask n c) = Subtask n c
+ src/Taskell/Data/Task.hs view
@@ -0,0 +1,27 @@+module Taskell.Data.Task+ ( Task+ , Update+ , name+ , description+ , due+ , subtasks+ , blank+ , new+ , create+ , duplicate+ , setDescription+ , appendDescription+ , setDue+ , clearDue+ , getSubtask+ , addSubtask+ , hasSubtasks+ , updateSubtask+ , removeSubtask+ , countSubtasks+ , countCompleteSubtasks+ , contains+ , isBlank+ ) where++import Taskell.Data.Task.Internal
+ src/Taskell/Data/Task/Internal.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.Data.Task.Internal where++import ClassyPrelude++import Control.Lens (ix, makeLenses, (%~), (&), (.~), (?~), (^.), (^?))++import Data.Sequence as S (adjust', deleteAt, (|>))+import Data.Text (strip)+import Data.Time.Zones (TZ)+import Taskell.Data.Date (Due (..), inputToTime)+import qualified Taskell.Data.Subtask as ST (Subtask, Update, complete, duplicate, name)++data Task = Task+ { _name :: Text+ , _description :: Maybe Text+ , _subtasks :: Seq ST.Subtask+ , _due :: Maybe Due+ } deriving (Show, Eq)++type Update = Task -> Task++-- create lenses+$(makeLenses ''Task)++-- operations+create :: Text -> Maybe Due -> Maybe Text -> Seq ST.Subtask -> Task+create name' due' description' subtasks' = Task name' description' subtasks' due'++blank :: Task+blank = Task "" Nothing empty Nothing++new :: Text -> Task+new text = blank & (name .~ text)++setDescription :: Text -> Update+setDescription text =+ description .~+ if null (strip text)+ then Nothing+ else Just text++maybeAppend :: Text -> Maybe Text -> Maybe Text+maybeAppend text (Just current) = Just (concat [current, "\n", text])+maybeAppend text Nothing = Just text++appendDescription :: Text -> Update+appendDescription text =+ if null (strip text)+ then id+ else description %~ maybeAppend text++setDue :: TZ -> UTCTime -> Text -> Task -> Maybe Task+setDue tz now date task =+ if null date+ then Just (task & due .~ Nothing)+ else (\d -> task & due ?~ d) <$> inputToTime tz now (strip date)++clearDue :: Update+clearDue task = task & due .~ Nothing++getSubtask :: Int -> Task -> Maybe ST.Subtask+getSubtask idx = (^? subtasks . ix idx)++addSubtask :: ST.Subtask -> Update+addSubtask subtask = subtasks %~ (|> subtask)++hasSubtasks :: Task -> Bool+hasSubtasks = not . null . (^. subtasks)++updateSubtask :: Int -> ST.Update -> Update+updateSubtask idx fn = subtasks %~ adjust' fn idx++removeSubtask :: Int -> Update+removeSubtask idx = subtasks %~ S.deleteAt idx++countSubtasks :: Task -> Int+countSubtasks = length . (^. subtasks)++countCompleteSubtasks :: Task -> Int+countCompleteSubtasks = length . filter (^. ST.complete) . (^. subtasks)++contains :: Text -> Task -> Bool+contains text task =+ check (task ^. name) || maybe False check (task ^. description) || not (null sts)+ where+ check = isInfixOf (toLower text) . toLower+ sts = filter check $ (^. ST.name) <$> (task ^. subtasks)++isBlank :: Task -> Bool+isBlank task =+ null (task ^. name) &&+ isNothing (task ^. description) && null (task ^. subtasks) && isNothing (task ^. due)++duplicate :: Task -> Task+duplicate (Task n d st du) = Task n d (ST.duplicate <$> st) du
+ src/Taskell/Events/Actions.hs view
@@ -0,0 +1,67 @@+module Taskell.Events.Actions+ ( event+ , generateActions+ , ActionSets+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Graphics.Vty.Input.Events (Event (..))++import Taskell.Events.State.Types (State, Stateful, mode)+import Taskell.Events.State.Types.Mode (DetailMode (..), ModalType (..), Mode (..))++import Taskell.IO.Keyboard (generate)+import Taskell.IO.Keyboard.Types (Bindings, BoundActions)++import qualified Taskell.Events.Actions.Insert as Insert+import qualified Taskell.Events.Actions.Modal as Modal+import qualified Taskell.Events.Actions.Modal.Detail as Detail+import qualified Taskell.Events.Actions.Modal.Due as Due+import qualified Taskell.Events.Actions.Modal.Help as Help+import qualified Taskell.Events.Actions.Normal as Normal+import qualified Taskell.Events.Actions.Search as Search++-- takes an event and returns a Maybe State+event' :: Event -> Stateful+-- for other events pass through to relevant modules+event' e state =+ case state ^. mode of+ Normal -> Normal.event e state+ Search -> Search.event e state+ Insert {} -> Insert.event e state+ Modal {} -> Modal.event e state+ _ -> pure state++-- returns new state if successful+event :: ActionSets -> Event -> State -> State+event actions e state = do+ let mEv =+ 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 $+ case mEv of+ Nothing -> event' e state+ Just ev -> ev state++data ActionSets = ActionSets+ { normal :: BoundActions+ , detail :: BoundActions+ , help :: BoundActions+ , due :: BoundActions+ }++generateActions :: Bindings -> ActionSets+generateActions bindings =+ ActionSets+ { normal = generate bindings Normal.events+ , detail = generate bindings Detail.events+ , help = generate bindings Help.events+ , due = generate bindings Due.events+ }
+ src/Taskell/Events/Actions/Insert.hs view
@@ -0,0 +1,36 @@+module Taskell.Events.Actions.Insert+ ( event+ ) where++import ClassyPrelude++import Control.Lens ((&), (.~), (^.))++import Graphics.Vty.Input.Events (Event (EvKey), Key (KEnter, KEsc))+import Taskell.Events.State+import Taskell.Events.State.Types+import Taskell.Events.State.Types.Mode (InsertMode (..), InsertType (..), Mode (Insert))+import qualified Taskell.UI.Draw.Field as F (event)++event :: Event -> Stateful+event (EvKey KEnter _) state =+ case state ^. mode of+ Insert IList ICreate _ ->+ (write =<<) . (startCreate =<<) . (newItem =<<) . (store =<<) $ createList state+ Insert IList IEdit _ -> (write =<<) . (normalMode =<<) $ finishListTitle state+ Insert ITask ICreate _ ->+ (write =<<) . (below =<<) . (removeBlank =<<) . (store =<<) $ finishTask state+ Insert ITask IEdit _ ->+ (write =<<) . (removeBlank =<<) . (normalMode =<<) $ finishTask state+ _ -> pure state+event (EvKey KEsc _) state =+ case state ^. mode of+ Insert IList ICreate _ -> (normalMode =<<) . (write =<<) $ createList state+ Insert IList IEdit _ -> (write =<<) . (normalMode =<<) $ finishListTitle state+ Insert ITask _ _ -> (write =<<) . (removeBlank =<<) . (normalMode =<<) $ finishTask state+ _ -> pure state+event e state =+ pure $+ case state ^. mode of+ Insert iType iMode field -> state & mode .~ Insert iType iMode (F.event e field)+ _ -> state
+ src/Taskell/Events/Actions/Modal.hs view
@@ -0,0 +1,25 @@+module Taskell.Events.Actions.Modal+ ( event+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Graphics.Vty.Input.Events+import Taskell.Events.State.Types (Stateful, mode)+import Taskell.Events.State.Types.Mode (ModalType (..), Mode (Modal))++import qualified Taskell.Events.Actions.Modal.Detail as Detail+import qualified Taskell.Events.Actions.Modal.Due as Due+import qualified Taskell.Events.Actions.Modal.Help as Help+import qualified Taskell.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+ Modal Due {} -> Due.event e s+ _ -> pure s
+ src/Taskell/Events/Actions/Modal/Detail.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedLists #-}++module Taskell.Events.Actions.Modal.Detail+ ( event+ , events+ ) where++import ClassyPrelude++import Graphics.Vty.Input.Events+import Taskell.Events.Actions.Types as A (ActionType (..))+import Taskell.Events.State (clearDate, normalMode, quit, store, undo, write)+import Taskell.Events.State.Modal.Detail as Detail+import Taskell.Events.State.Types+import Taskell.Events.State.Types.Mode (DetailItem (..), DetailMode (..))+import Taskell.IO.Keyboard.Types (Actions)+import qualified Taskell.UI.Draw.Field as F (event)++events :: Actions+events+ -- general+ =+ [ (A.Quit, quit)+ , (A.Undo, (write =<<) . undo)+ , (A.Previous, previousSubtask)+ , (A.Next, nextSubtask)+ , (A.MoveUp, (write =<<) . (up =<<) . store)+ , (A.MoveDown, (write =<<) . (down =<<) . store)+ , (A.New, (Detail.insertMode =<<) . (Detail.lastSubtask =<<) . (Detail.newItem =<<) . store)+ , (A.Edit, (Detail.insertMode =<<) . 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)+ ]++normal :: Event -> Stateful+normal (EvKey KEsc _) = normalMode+normal _ = pure++insert :: Event -> Stateful+insert (EvKey KEsc _) s = do+ item <- getCurrentItem s+ case item of+ DetailDescription -> (write =<<) $ finishDescription s+ DetailDate -> showDetail s+ (DetailItem _) -> (write =<<) . (showDetail =<<) $ finishSubtask s+insert (EvKey KEnter _) s = do+ item <- getCurrentItem s+ case item of+ DetailDescription -> (write =<<) $ finishDescription s+ DetailDate -> (write =<<) $ finishDue s+ (DetailItem _) ->+ (Detail.lastSubtask =<<) . (Detail.newItem =<<) . (store =<<) . (write =<<) $+ finishSubtask s+insert e s = updateField (F.event e) s++event :: Event -> Stateful+event e s = do+ m <- getCurrentMode s+ case m of+ DetailNormal -> normal e s+ (DetailInsert _) -> insert e s
+ src/Taskell/Events/Actions/Modal/Due.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedLists #-}++module Taskell.Events.Actions.Modal.Due+ ( event+ , events+ ) where++import ClassyPrelude++import Graphics.Vty.Input.Events (Event (EvKey))+import Taskell.Events.Actions.Types as A (ActionType (..))+import Taskell.Events.State (normalMode, store, write)+import Taskell.Events.State.Modal.Detail (showDetail)+import Taskell.Events.State.Modal.Due (clearDate, goto, next, previous)+import Taskell.Events.State.Types (Stateful)+import Taskell.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/Taskell/Events/Actions/Modal/Help.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE OverloadedLists #-}++module Taskell.Events.Actions.Modal.Help+ ( event+ , events+ ) where++import ClassyPrelude+import Graphics.Vty.Input.Events+import Taskell.Events.Actions.Types as A (ActionType (..))+import Taskell.Events.State+import Taskell.Events.State.Types (Stateful)+import Taskell.IO.Keyboard.Types (Actions)++events :: Actions+events = [(A.Quit, quit)]++event :: Event -> Stateful+event (EvKey _ _) = normalMode+event _ = pure
+ src/Taskell/Events/Actions/Modal/MoveTo.hs view
@@ -0,0 +1,14 @@+module Taskell.Events.Actions.Modal.MoveTo+ ( event+ ) where++import ClassyPrelude+import Graphics.Vty.Input.Events+import Taskell.Events.State+import Taskell.Events.State.Types (Stateful)++event :: Event -> Stateful+event (EvKey KEsc _) = normalMode+event (EvKey KEnter _) = normalMode+event (EvKey (KChar c) _) = (normalMode =<<) . (write =<<) . (moveTo c =<<) . store+event _ = pure
+ src/Taskell/Events/Actions/Normal.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE OverloadedLists #-}++module Taskell.Events.Actions.Normal+ ( event+ , events+ ) where++import ClassyPrelude hiding (delete)++import Data.Char (isDigit)+import Graphics.Vty.Input.Events+import Taskell.Events.Actions.Types as A (ActionType (..))+import Taskell.Events.State+import Taskell.Events.State.Modal.Detail (editDue, showDetail)+import Taskell.Events.State.Modal.Due (showDue)+import Taskell.Events.State.Types (Stateful)+import Taskell.IO.Keyboard.Types (Actions)++events :: Actions+events+ -- general+ =+ [ (A.Quit, quit)+ , (A.Undo, (write =<<) . undo)+ , (A.Redo, (write =<<) . redo)+ , (A.Search, searchMode)+ , (A.Help, showHelp)+ , (A.Due, showDue)+ -- navigation+ , (A.Previous, previous)+ , (A.Next, next)+ , (A.Left, left)+ , (A.Right, right)+ , (A.Bottom, bottom)+ , (A.Top, top)+ -- new tasks+ , (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.MoveLeftTop, (write =<<) . (top =<<) . (left =<<) . (moveLeftTop =<<) . store)+ , (A.MoveRightTop, (write =<<) . (top =<<) . (right =<<) . (moveRightTop =<<) . store)+ , (A.MoveLeftBottom, (write =<<) . (bottom =<<) . (left =<<) . (moveLeftBottom =<<) . store)+ , (A.MoveRightBottom, (write =<<) . (bottom =<<) . (right =<<) . (moveRightBottom =<<) . store)+ , (A.Complete, (write =<<) . (moveToLast =<<) . (clearDate =<<) . store)+ , (A.MoveMenu, showMoveTo)+ -- lists+ , (A.ListNew, (createListStart =<<) . store)+ , (A.ListEdit, (editListStart =<<) . store)+ , (A.ListDelete, (write =<<) . (deleteCurrentList =<<) . store)+ , (A.ListRight, (write =<<) . (listRight =<<) . store)+ , (A.ListLeft, (write =<<) . (listLeft =<<) . store)+ ]++-- Normal+event :: Event -> Stateful+-- selecting lists+event (EvKey (KChar n) _)+ | isDigit n = selectList n+ | otherwise = pure+event (EvKey KEsc _) = clearSearch+-- fallback+event _ = pure
+ src/Taskell/Events/Actions/Search.hs view
@@ -0,0 +1,21 @@+module Taskell.Events.Actions.Search+ ( event+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Graphics.Vty.Input.Events+import Taskell.Events.State+import Taskell.Events.State.Types (Stateful, mode)+import Taskell.Events.State.Types.Mode (Mode (Search))+import qualified Taskell.UI.Draw.Field as F (event)++event :: Event -> Stateful+event (EvKey KEsc _) s = clearSearch =<< normalMode s+event (EvKey KEnter _) s = normalMode s+event e s =+ case s ^. mode of+ Search -> appendSearch (F.event e) s+ _ -> pure s
+ src/Taskell/Events/Actions/Types.hs view
@@ -0,0 +1,85 @@+module Taskell.Events.Actions.Types where++import ClassyPrelude hiding (Left, Nothing, Right)++data ActionType+ = Quit+ | Undo+ | Redo+ | Search+ | Due+ | Help+ | Previous+ | Next+ | Left+ | Right+ | Bottom+ | Top+ | New+ | NewAbove+ | NewBelow+ | Duplicate+ | Edit+ | Clear+ | Delete+ | Detail+ | DueDate+ | ClearDate+ | MoveUp+ | MoveDown+ | MoveLeftTop+ | MoveRightTop+ | MoveLeftBottom+ | MoveRightBottom+ | Complete+ | MoveMenu+ | ListNew+ | ListEdit+ | ListDelete+ | ListRight+ | ListLeft+ | Nothing+ deriving (Show, Eq, Ord, Enum)++allActions :: [ActionType]+allActions = [toEnum 0 ..]++read :: Text -> ActionType+read "quit" = Quit+read "undo" = Undo+read "redo" = Redo+read "search" = Search+read "due" = Due+read "help" = Help+read "previous" = Previous+read "next" = Next+read "left" = Left+read "right" = Right+read "bottom" = Bottom+read "top" = Top+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 "moveLeftTop" = MoveLeftTop+read "moveRightTop" = MoveRightTop+read "moveLeft" = MoveLeftBottom+read "moveRight" = MoveRightBottom+read "moveLeftBottom" = MoveLeftBottom+read "moveRightBottom" = MoveRightBottom+read "complete" = Complete+read "moveMenu" = MoveMenu+read "listNew" = ListNew+read "listEdit" = ListEdit+read "listDelete" = ListDelete+read "listRight" = ListRight+read "listLeft" = ListLeft+read _ = Nothing
+ src/Taskell/Events/State.hs view
@@ -0,0 +1,419 @@+module Taskell.Events.State+ -- App+ ( continue+ , write+ , setTime+ , countCurrent+ , setHeight+ -- Taskell.UI.Main+ , normalise+ -- Main+ , create+ -- Taskell.Events.Actions.Normal+ , quit+ , startEdit+ , startCreate+ , createListStart+ , editListStart+ , deleteCurrentList+ , clearItem+ , clearDate+ , above+ , below+ , bottom+ , top+ , previous+ , duplicate+ , next+ , left+ , right+ , up+ , down+ , moveLeftTop+ , moveRightTop+ , moveLeftBottom+ , moveRightBottom+ , moveToLast+ , delete+ , selectList+ , listLeft+ , listRight+ , undo+ , redo+ , store+ , searchMode+ , clearSearch+ , appendSearch+ -- Taskell.Events.Actions.Insert+ , createList+ , removeBlank+ , newItem+ , normalMode+ , finishTask+ , finishListTitle+ -- Taskell.Events.Actions.Modal+ , showHelp+ , showMoveTo+ , moveTo+ , getCurrentList+ , getCurrentTask+ , setCurrentTask+ ) where++import ClassyPrelude hiding (delete)++import Control.Lens ((%~), (&), (.~), (?~), (^.))++import Data.Char (digitToInt, ord)+import Data.Time.Zones (TZ)++import qualified Taskell.Data.List as L (List, deleteTask, duplicate, getTask, move, nearest, new,+ newAt, nextTask, prevTask, title, update)+import qualified Taskell.Data.Lists as Lists+import Taskell.Data.Task (Task, isBlank, name)+import Taskell.Types++import qualified Taskell.Events.State.History as History (redo, store, undo)+import Taskell.Events.State.Types+import Taskell.Events.State.Types.Mode (InsertMode (..), InsertType (..), ModalType (..),+ Mode (..))+import Taskell.UI.Draw.Field (Field, blankField, getText, textToField)++type InternalStateful = State -> State++create :: TZ -> UTCTime -> FilePath -> Lists.Lists -> State+create tz t p ls =+ State+ { _mode = Normal+ , _history = fresh ls+ , _path = p+ , _io = Nothing+ , _height = 0+ , _searchTerm = Nothing+ , _time = t+ , _timeZone = tz+ }++-- app state+quit :: Stateful+quit = pure . (mode .~ Shutdown)++continue :: State -> State+continue = io .~ Nothing++store :: Stateful+store state = pure $ state & history %~ History.store++undo :: Stateful+undo state = pure $ state & history %~ History.undo++redo :: Stateful+redo state = pure $ state & history %~ History.redo++setTime :: UTCTime -> State -> State+setTime t = time .~ t++write :: Stateful+write state = pure $ state & (io ?~ (state ^. lists))++-- createList+createList :: Stateful+createList state =+ pure $+ case state ^. mode of+ Insert IList ICreate f ->+ updateListToLast . setLists state $ Lists.newList (getText f) $ state ^. lists+ _ -> state++updateListToLast :: InternalStateful+updateListToLast state = setCurrentList state (length (state ^. lists) - 1)++createListStart :: Stateful+createListStart = pure . (mode .~ Insert IList ICreate blankField)++-- editList+editListStart :: Stateful+editListStart state = do+ f <- textToField . (^. L.title) <$> getList state+ pure $ state & mode .~ Insert IList IEdit f++deleteCurrentList :: Stateful+deleteCurrentList state =+ pure . fixIndex . setLists state $ Lists.delete (getCurrentList state) (state ^. lists)++-- insert+getCurrentTask :: State -> Maybe Task+getCurrentTask state = getList state >>= L.getTask (getIndex state)++setCurrentTask :: Task -> Stateful+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 = pure . (mode .~ Insert ITask ICreate blankField)++startEdit :: Stateful+startEdit state = do+ field <- textToField . (^. name) <$> getCurrentTask state+ pure $ state & mode .~ Insert ITask IEdit field++finishTask :: Stateful+finishTask state =+ case state ^. mode of+ Insert ITask iMode f ->+ setCurrentTaskText (getText f) $ state & (mode .~ Insert ITask iMode blankField)+ _ -> pure state++finishListTitle :: Stateful+finishListTitle state =+ case state ^. mode of+ Insert IList iMode f ->+ setCurrentListTitle (getText f) $ state & (mode .~ Insert IList iMode blankField)+ _ -> pure state++normalMode :: Stateful+normalMode = pure . (mode .~ Normal)++addToListAt :: Int -> Stateful+addToListAt offset state = do+ let idx = getIndex state + offset+ fixIndex . setList (setIndex state idx) . L.newAt idx <$> getList state++above :: Stateful+above = addToListAt 0++below :: Stateful+below = addToListAt 1++newItem :: Stateful+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++selectLast :: InternalStateful+selectLast state = setIndex state (countCurrent state - 1)++top :: Stateful+top = pure . selectFirst++selectFirst :: InternalStateful+selectFirst state = setIndex state (0)++removeBlank :: Stateful+removeBlank state = do+ currentTask <- getCurrentTask state+ (if isBlank currentTask+ then delete+ else pure)+ 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 = moveVertical (-1)++down :: Stateful+down = moveVertical 1++moveHorizontal :: Int -> Lists.ListPosition -> State -> Maybe State+moveHorizontal idx pos state =+ fixIndex . setLists state <$> Lists.changeList pos (state ^. current) (state ^. lists) idx++moveLeftBottom :: Stateful+moveLeftBottom = moveHorizontal (-1) Lists.Bottom++moveRightBottom :: Stateful+moveRightBottom = moveHorizontal 1 Lists.Bottom++moveLeftTop :: Stateful+moveLeftTop = moveHorizontal (-1) Lists.Top++moveRightTop :: Stateful+moveRightTop = moveHorizontal 1 Lists.Top++moveToLast :: Stateful+moveToLast state =+ if idx == cur+ then pure state+ else moveHorizontal (idx - cur) Lists.Bottom state+ where+ idx = length (state ^. lists) - 1+ cur = getCurrentList state++selectList :: Char -> Stateful+selectList idx state =+ pure $+ (if exists+ then current .~ (ListIndex list, TaskIndex 0)+ else id)+ state+ where+ list = digitToInt idx - 1+ exists = Lists.exists list (state ^. lists)++-- removing+delete :: Stateful+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 .~ (ListIndex (getCurrentList state), TaskIndex idx)++setCurrentList :: State -> Int -> State+setCurrentList state idx = state & current .~ (ListIndex idx, TaskIndex (getIndex state))++getIndex :: State -> Int+getIndex = showTaskIndex . snd . (^. current)++changeTask :: (Int -> Maybe Text -> L.List -> Int) -> Stateful+changeTask fn state = do+ list <- getList state+ let idx = getIndex state+ let term = getText <$> state ^. searchTerm+ pure $ setIndex state (fn idx term list)++next :: Stateful+next = changeTask L.nextTask++previous :: Stateful+previous = changeTask L.prevTask++left :: Stateful+left state =+ pure . fixIndex . setCurrentList state $+ if list > 0+ then pred list+ else 0+ where+ list = getCurrentList state++right :: Stateful+right state =+ pure . fixIndex . setCurrentList state $+ if list < (count - 1)+ then succ list+ else list+ where+ 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 (L.nearest idx trm list)+ Nothing -> fixListIndex state+ where+ trm = getText <$> state ^. searchTerm+ idx = getIndex state++-- tasks+getCurrentList :: State -> Int+getCurrentList = showListIndex . fst . (^. current)++getList :: State -> Maybe L.List+getList state = Lists.get (state ^. lists) (getCurrentList 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 . (L.title .~ text) <$> getList state++setLists :: State -> Lists.Lists -> State+setLists state lists' = state & lists .~ lists'++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 <- moveHorizontal (li - cur) Lists.Bottom 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+ let currentList = getCurrentList state+ let lists' = state ^. lists+ if currentList + offset < 0 || currentList + offset >= length lists'+ then Nothing+ else do+ let state' = fixIndex $ setCurrentList state (currentList + offset)+ setLists state' <$> Lists.shiftBy currentList offset lists'++listLeft :: Stateful+listLeft = listMove (-1)++listRight :: Stateful+listRight = listMove 1++-- search+searchMode :: Stateful+searchMode state = pure . fixIndex $ (state & mode .~ Search) & searchTerm .~ sTerm+ where+ sTerm = pure (fromMaybe blankField (state ^. searchTerm))++clearSearch :: Stateful+clearSearch state = pure $ state & searchTerm .~ Nothing++appendSearch :: (Field -> Field) -> Stateful+appendSearch genField state = do+ let field = fromMaybe blankField (state ^. searchTerm)+ pure . fixIndex $ state & searchTerm .~ pure (genField field)++-- help+showHelp :: Stateful+showHelp = pure . (mode .~ Modal Help)++showMoveTo :: Stateful+showMoveTo state = const (state & mode .~ Modal MoveTo) <$> getCurrentTask state++-- view+setHeight :: Int -> State -> State+setHeight = (.~) height++-- more view - maybe shouldn't be in here...+newList :: State -> State+newList state =+ case state ^. mode of+ Insert IList ICreate f ->+ let ls = state ^. lists+ in fixIndex $ setCurrentList (setLists state (Lists.newList (getText f) ls)) (length ls)+ _ -> state++normalise :: State -> State+normalise = newList
+ src/Taskell/Events/State/History.hs view
@@ -0,0 +1,29 @@+module Taskell.Events.State.History+ ( undo+ , redo+ , store+ ) where++import ClassyPrelude++import Control.Lens (Lens', (&), (.~), (^.))++import Taskell.Events.State.Types (History (History), future, past, present)++λstack :: Lens' (History a) [a] -> History a -> [a]+λstack fn history = history ^. present : (history ^. fn)++store :: History a -> History a+store history = history & past .~ λstack past history & future .~ empty++undo :: History a -> History a+undo history =+ case history ^. past of+ [] -> history+ (moment:xs) -> History xs moment (λstack future history)++redo :: History a -> History a+redo history =+ case history ^. future of+ [] -> history+ (moment:xs) -> History (λstack past history) moment xs
+ src/Taskell/Events/State/Modal/Detail.hs view
@@ -0,0 +1,184 @@+module Taskell.Events.State.Modal.Detail+ ( updateField+ , finishSubtask+ , finishDescription+ , finishDue+ , showDetail+ , getCurrentItem+ , getCurrentMode+ , getField+ , setComplete+ , remove+ , insertMode+ , editDescription+ , editDue+ , newItem+ , nextSubtask+ , previousSubtask+ , lastSubtask+ , up+ , down+ ) where++import ClassyPrelude++import Control.Lens ((&), (.~), (^.))++import Taskell.Data.Date (timeToDisplay)+import qualified Taskell.Data.Seq as S+import qualified Taskell.Data.Subtask as ST (blank, name, toggle)+import Taskell.Data.Task (Task, addSubtask, countSubtasks, description, due,+ getSubtask, removeSubtask, setDescription, setDue,+ subtasks, updateSubtask)+import Taskell.Events.State (getCurrentTask, setCurrentTask)+import Taskell.Events.State.Types (State, Stateful, mode, time, timeZone)+import Taskell.Events.State.Types.Mode (DetailItem (..), DetailMode (..),+ ModalType (Detail), Mode (Modal))+import Taskell.UI.Draw.Field (Field, blankField, getText, textToField)++updateField :: (Field -> Field) -> Stateful+updateField fieldEvent s =+ pure $+ case s ^. mode of+ Modal (Detail detailItem (DetailInsert field)) ->+ s & mode .~ Modal (Detail detailItem (DetailInsert (fieldEvent field)))+ _ -> s++finishSubtask :: Stateful+finishSubtask state = do+ text <- getText <$> getField state+ i <- getCurrentSubtask state+ task <- updateSubtask i (ST.name .~ text) <$> getCurrentTask state+ setCurrentTask task $ state & mode .~ Modal (Detail (DetailItem i) (DetailInsert blankField))++finish :: (Text -> Task -> Task) -> Stateful+finish fn state = do+ text <- getText <$> getField state+ task <- fn text <$> getCurrentTask state+ setCurrentTask task $ state & mode .~ Modal (Detail (DetailItem 0) DetailNormal)++finishDescription :: Stateful+finishDescription = finish setDescription++finishDue :: Stateful+finishDue state = do+ let tz = state ^. timeZone+ let now = state ^. time+ text <- getText <$> getField state+ task <- setDue tz now text =<< getCurrentTask state+ setCurrentTask task $ state & mode .~ Modal (Detail (DetailItem 0) DetailNormal)++showDetail :: Stateful+showDetail s = do+ _ <- getCurrentTask s+ let i = fromMaybe 0 $ getCurrentSubtask s+ pure $ s & mode .~ Modal (Detail (DetailItem i) DetailNormal)++getCurrentSubtask :: State -> Maybe Int+getCurrentSubtask state =+ case state ^. mode of+ Modal (Detail (DetailItem i) _) -> Just i+ _ -> Nothing++getCurrentItem :: State -> Maybe DetailItem+getCurrentItem state =+ case state ^. mode of+ Modal (Detail item _) -> Just item+ _ -> Nothing++getCurrentMode :: State -> Maybe DetailMode+getCurrentMode state =+ case state ^. mode of+ Modal (Detail _ m) -> Just m+ _ -> Nothing++getField :: State -> Maybe Field+getField state =+ case state ^. mode of+ Modal (Detail _ (DetailInsert f)) -> Just f+ _ -> Nothing++setComplete :: Stateful+setComplete state = do+ i <- getCurrentSubtask state+ task <- updateSubtask i ST.toggle <$> getCurrentTask state+ setCurrentTask task state++remove :: Stateful+remove state = do+ i <- getCurrentSubtask state+ task <- removeSubtask i <$> getCurrentTask state+ state' <- setCurrentTask task state+ setIndex state' i++insertMode :: Stateful+insertMode state = do+ i <- getCurrentSubtask state+ task <- getCurrentTask state+ n <- (^. ST.name) <$> getSubtask i task+ case state ^. mode of+ Modal (Detail (DetailItem i') _) ->+ Just (state & mode .~ Modal (Detail (DetailItem i') (DetailInsert (textToField n))))+ _ -> Nothing++editDescription :: Stateful+editDescription state = do+ summ <- (^. description) <$> getCurrentTask state+ let summ' = fromMaybe "" summ+ pure $ state & mode .~ Modal (Detail DetailDescription (DetailInsert (textToField summ')))++editDue :: Stateful+editDue state = do+ day <- (^. due) <$> getCurrentTask state+ let tz = state ^. timeZone+ let day' = maybe "" (timeToDisplay tz) day+ pure $ state & mode .~ Modal (Detail DetailDate (DetailInsert (textToField day')))++newItem :: Stateful+newItem state = do+ task <- addSubtask ST.blank <$> getCurrentTask state+ setCurrentTask task state++-- list navigation+changeSubtask :: Int -> Stateful+changeSubtask inc state = do+ i <- (+ inc) <$> getCurrentSubtask state+ setIndex state i++nextSubtask :: Stateful+nextSubtask = changeSubtask 1++previousSubtask :: Stateful+previousSubtask = changeSubtask (-1)++lastSubtask :: Stateful+lastSubtask state = lastIndex state >>= setIndex state++lastIndex :: State -> Maybe Int+lastIndex state = (+ (-1)) . countSubtasks <$> getCurrentTask state++setIndex :: State -> Int -> Maybe State+setIndex state i = do+ lst <- lastIndex state+ m <- getCurrentMode state+ let newIndex+ | i > lst = lst+ | i < 0 = 0+ | otherwise = i+ return $ state & mode .~ Modal (Detail (DetailItem newIndex) m)++-- moving tasks+moveVertical :: Int -> Stateful+moveVertical dir state = do+ task <- getCurrentTask state+ let tasks = task ^. subtasks+ i <- getCurrentSubtask state+ shifted <- S.shiftBy i dir tasks+ state' <- setCurrentTask (task & subtasks .~ shifted) state+ changeSubtask dir state'++up :: Stateful+up = moveVertical (-1)++down :: Stateful+down = moveVertical 1
+ src/Taskell/Events/State/Modal/Due.hs view
@@ -0,0 +1,66 @@+module Taskell.Events.State.Modal.Due+ ( showDue+ , clearDate+ , previous+ , next+ , goto+ ) where++import ClassyPrelude++import Control.Lens ((&), (.~), (^.))+import Data.Sequence ((!?))++import qualified Taskell.Data.Lists as L (clearDue, due)+import Taskell.Data.Seq (bound)+import Taskell.Data.Task (Task)+import Taskell.Events.State.Types (Stateful, current, lists, mode)+import Taskell.Events.State.Types.Mode (ModalType (Due), Mode (..))+import Taskell.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/Taskell/Events/State/Types.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.Events.State.Types where++import ClassyPrelude++import Control.Lens (Lens', makeLenses)+import Control.Lens.Combinators (_1, _2)++import Data.Time.Zones (TZ)++import Taskell.Data.Lists (Lists)+import Taskell.Types (Pointer, startPointer)+import Taskell.UI.Draw.Field (Field)++import qualified Taskell.Events.State.Types.Mode as M (Mode)++type Moment = (Pointer, Lists)++data History a = History+ { _past :: [a]+ , _present :: a+ , _future :: [a]+ } deriving (Eq, Show)++fresh :: Lists -> History Moment+fresh ls = History empty (startPointer, ls) empty++data State = State+ { _mode :: M.Mode+ , _history :: History Moment+ , _path :: FilePath+ , _io :: Maybe Lists+ , _height :: Int+ , _searchTerm :: Maybe Field+ , _time :: UTCTime+ , _timeZone :: TZ+ } deriving (Eq, Show)++-- create lenses+$(makeLenses ''State)++$(makeLenses ''History)++type Stateful = State -> Maybe State++current :: Lens' State Pointer+current = history . present . _1++lists :: Lens' State Lists+lists = history . present . _2
+ src/Taskell/Events/State/Types/Mode.hs view
@@ -0,0 +1,47 @@+module Taskell.Events.State.Types.Mode where++import ClassyPrelude++import Taskell.Data.Task (Task)+import Taskell.Types (Pointer)+import Taskell.UI.Draw.Field (Field)++data DetailMode+ = DetailNormal+ | DetailInsert Field+ deriving (Eq, Show)++data DetailItem+ = DetailItem Int+ | DetailDescription+ | DetailDate+ deriving (Eq, Show)++data ModalType+ = Help+ | MoveTo+ | Due (Seq (Pointer, Task))+ Int+ | Detail DetailItem+ DetailMode+ deriving (Eq, Show)++data InsertType+ = ITask+ | IList+ deriving (Eq, Show)++data InsertMode+ = IEdit+ | ICreate+ deriving (Eq, Show)++data Mode+ = Normal+ | Insert InsertType+ InsertMode+ Field+ | Modal ModalType+ | Search+ | Shutdown+ deriving (Eq, Show)
+ src/Taskell/IO.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO where++import ClassyPrelude++import Control.Monad.Reader (runReader)+import Data.FileEmbed (embedFile)+import Data.Text.Encoding (decodeUtf8With)+import System.Directory (doesFileExist, getCurrentDirectory)++import Data.Time.Zones (TZ)++import Taskell.Config (usage, version)+import Taskell.Data.Lists (Lists, analyse, initial)++import Taskell.IO.Config (Config, general, github, markdown, trello)+import Taskell.IO.Config.General (filename)+import qualified Taskell.IO.Config.GitHub as GitHub (token)+import qualified Taskell.IO.Config.Trello as Trello (token)++import Taskell.IO.Markdown (MarkdownInfo (MarkdownInfo), parse, serialize)++import qualified Taskell.IO.HTTP.GitHub as GitHub (GitHubIdentifier, getLists)+import qualified Taskell.IO.HTTP.Trello as Trello (TrelloBoardID, getLists)++import Taskell.UI.CLI (PromptYN (PromptYes), promptYN)++data IOInfo = IOInfo+ { ioTZ :: TZ+ , ioConfig :: Config+ }++type ReaderConfig a = ReaderT IOInfo IO a++data Next+ = Output Text+ | Error Text+ | Load FilePath+ Lists+ | Exit++parseArgs :: [Text] -> ReaderConfig Next+parseArgs ["-v"] = pure $ Output version+parseArgs ["-h"] = pure $ Output usage+parseArgs ["-t", boardID, file] = loadTrello boardID file+parseArgs ["-g", identifier, file] = loadGitHub identifier file+parseArgs ["-i", file] = fileInfo file+parseArgs [file] = loadFile file+parseArgs [] = (pack . filename . general <$> asks ioConfig) >>= loadFile+parseArgs _ = pure $ Error (unlines ["Invalid options", "", usage])++load :: ReaderConfig Next+load = getArgs >>= parseArgs++colonic :: FilePath -> Text -> Text+colonic path = ((pack path <> ": ") <>)++loadFile :: Text -> ReaderConfig Next+loadFile filepath = do+ mPath <- exists filepath+ case mPath of+ Nothing -> pure Exit+ Just path -> either (Error . colonic path) (Load path) <$> readData path++loadRemote :: (token -> FilePath -> ReaderConfig Next) -> token -> Text -> ReaderConfig Next+loadRemote createFn identifier filepath = do+ let path = unpack filepath+ exists' <- fileExists path+ if exists'+ then pure $ Error (filepath <> " already exists")+ else createFn identifier path++loadTrello :: Trello.TrelloBoardID -> Text -> ReaderConfig Next+loadTrello = loadRemote createTrello++loadGitHub :: GitHub.GitHubIdentifier -> Text -> ReaderConfig Next+loadGitHub = loadRemote createGitHub++fileInfo :: Text -> ReaderConfig Next+fileInfo filepath = do+ let path = unpack filepath+ exists' <- fileExists path+ if exists'+ then either (Error . colonic path) (Output . analyse filepath) <$> readData path+ else pure $ Error (filepath <> " does not exist")++createRemote ::+ (Config -> Maybe token)+ -> Text+ -> (token -> ReaderT token IO (Either Text Lists))+ -> token+ -> FilePath+ -> ReaderConfig Next+createRemote tokenFn missingToken getFn identifier path = do+ config <- asks ioConfig+ tz <- asks ioTZ+ case tokenFn config of+ Nothing -> pure $ Error missingToken+ Just token -> do+ lists <- lift $ runReaderT (getFn identifier) token+ case lists of+ Left txt -> pure $ Error txt+ Right ls ->+ promptCreate path >>=+ bool (pure Exit) (Load path ls <$ lift (writeData tz config ls path))++createTrello :: Trello.TrelloBoardID -> FilePath -> ReaderConfig Next+createTrello =+ createRemote+ (Trello.token . trello)+ (decodeUtf8 $(embedFile "templates/trello-token.txt"))+ Trello.getLists++createGitHub :: GitHub.GitHubIdentifier -> FilePath -> ReaderConfig Next+createGitHub =+ createRemote+ (GitHub.token . github)+ (decodeUtf8 $(embedFile "templates/github-token.txt"))+ GitHub.getLists++exists :: Text -> ReaderConfig (Maybe FilePath)+exists filepath = do+ let path = unpack filepath+ exists' <- fileExists path+ if exists'+ then pure $ Just path+ else promptCreate path >>= bool (pure Nothing) (Just path <$ createPath path)++fileExists :: FilePath -> ReaderConfig Bool+fileExists path = lift $ doesFileExist path++promptCreate :: FilePath -> ReaderConfig Bool+promptCreate path = do+ cwd <- lift $ pack <$> getCurrentDirectory+ lift $ promptYN PromptYes $ concat ["Create ", cwd, "/", pack path, "?"]++-- creates taskell file+createPath :: FilePath -> ReaderConfig ()+createPath path = do+ config <- asks ioConfig+ tz <- asks ioTZ+ lift (writeData tz config initial path)++-- writes Tasks to json file+writeData :: TZ -> Config -> Lists -> FilePath -> IO ()+writeData tz config tasks path = void (writeFile path output)+ where+ output = encodeUtf8 $ runReader (serialize tasks) (MarkdownInfo tz (markdown config))++-- reads json file+decodeError :: String -> Maybe Word8 -> Maybe Char+decodeError _ _ = Just '\65533'++readData :: FilePath -> ReaderConfig (Either Text Lists)+readData path =+ parse <$> (markdown <$> asks ioConfig) <*> (decodeUtf8With decodeError <$> readFile path)
+ src/Taskell/IO/Config.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedLists #-}++module Taskell.IO.Config where++import ClassyPrelude++import qualified Data.Text.IO as T (readFile)+import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist,+ getHomeDirectory)+import System.Environment (lookupEnv)++import Brick (AttrMap)+import Brick.Themes (loadCustomizations, themeToAttrMap)+import Data.FileEmbed (embedFile)+import Data.Ini.Config++import Taskell.IO.Keyboard (addMissing, badMapping, defaultBindings)+import Taskell.IO.Keyboard.Parser (bindings)+import Taskell.IO.Keyboard.Types (Bindings)+import Taskell.UI.Theme (defaultTheme)++import qualified Taskell.IO.Config.General as General+import qualified Taskell.IO.Config.GitHub as GitHub+import qualified Taskell.IO.Config.Layout as Layout+import qualified Taskell.IO.Config.Markdown as Markdown+import qualified Taskell.IO.Config.Trello as Trello++data Config = Config+ { general :: General.Config+ , layout :: Layout.Config+ , markdown :: Markdown.Config+ , trello :: Trello.Config+ , github :: GitHub.Config+ }++debugging :: Config -> Bool+debugging config = General.debug (general config)++defaultConfig :: Config+defaultConfig =+ Config+ General.defaultConfig+ Layout.defaultConfig+ Markdown.defaultConfig+ Trello.defaultConfig+ GitHub.defaultConfig++directoryName :: FilePath+directoryName = "taskell"++legacyConfigPath :: IO FilePath+legacyConfigPath = (</> "." <> directoryName) <$> getHomeDirectory++xdgDefaultConfig :: IO FilePath+xdgDefaultConfig = (</> ".config") <$> getHomeDirectory++xdgConfigPath :: IO FilePath+xdgConfigPath =+ (</> directoryName) <$> (fromMaybe <$> xdgDefaultConfig <*> lookupEnv "XDG_CONFIG_HOME")++getDir :: IO FilePath+getDir = legacyConfigPath >>= doesDirectoryExist >>= bool xdgConfigPath legacyConfigPath++themePath :: FilePath -> FilePath+themePath = (</> "theme.ini")++configPath :: FilePath -> FilePath+configPath = (</> "config.ini")++bindingsPath :: FilePath -> FilePath+bindingsPath = (</> "bindings.ini")++setup :: IO Config+setup+ -- create config dir+ = do+ dir <- getDir+ createDirectoryIfMissing True dir+ -- create config files+ create (configPath dir) $(embedFile "templates/config.ini")+ create (themePath dir) $(embedFile "templates/theme.ini")+ create (bindingsPath dir) $(embedFile "templates/bindings.ini")+ -- get config+ getConfig++create :: FilePath -> ByteString -> IO ()+create path contents = doesFileExist path >>= flip unless (writeFile path contents)++configParser :: IniParser Config+configParser =+ Config <$> General.parser <*> Layout.parser <*> Markdown.parser <*> Trello.parser <*>+ GitHub.parser++getConfig :: IO Config+getConfig = do+ content <- T.readFile =<< (configPath <$> getDir)+ case parseIniFile content configParser of+ Right config -> pure config+ Left s -> putStrLn (pack $ "config.ini: " <> s) $> defaultConfig++getBindings :: IO Bindings+getBindings = do+ bnds <- bindings <$> (T.readFile =<< (bindingsPath <$> getDir))+ case addMissing <$> (badMapping =<< bnds) of+ Right b -> pure b+ Left err ->+ putStrLn ("bindings.ini: " <> err <> " - using default bindings") $> defaultBindings++-- generate theme+generateAttrMap :: IO AttrMap+generateAttrMap = do+ dir <- getDir+ customizedTheme <- loadCustomizations (themePath dir) defaultTheme+ case customizedTheme of+ Right theme -> pure $ themeToAttrMap theme+ Left s -> putStrLn (pack $ "theme.ini: " <> s) $> themeToAttrMap defaultTheme
+ src/Taskell/IO/Config/General.hs view
@@ -0,0 +1,30 @@+module Taskell.IO.Config.General+ ( Config+ , defaultConfig+ , parser+ , filename+ , debug+ ) where++import ClassyPrelude++import Data.Ini.Config++import Taskell.IO.Config.Parser (noEmpty)++data Config = Config+ { filename :: FilePath+ , debug :: Bool+ }++defaultConfig :: Config+defaultConfig = Config {filename = "taskell.md", debug = False}++filenameP :: SectionParser String+filenameP = maybe (filename defaultConfig) unpack . (noEmpty =<<) <$> fieldMb "filename"++debugP :: SectionParser Bool+debugP = fieldFlagDef "debug" False++parser :: IniParser Config+parser = fromMaybe defaultConfig <$> sectionMb "general" (Config <$> filenameP <*> debugP)
+ src/Taskell/IO/Config/GitHub.hs view
@@ -0,0 +1,25 @@+module Taskell.IO.Config.GitHub+ ( Config+ , defaultConfig+ , parser+ , token+ ) where++import ClassyPrelude++import Data.Ini.Config++import Taskell.IO.HTTP.GitHub (GitHubToken)++data Config = Config+ { token :: Maybe GitHubToken+ }++defaultConfig :: Config+defaultConfig = Config {token = Nothing}++tokenP :: SectionParser (Maybe GitHubToken)+tokenP = fieldMb "token"++parser :: IniParser Config+parser = fromMaybe defaultConfig <$> sectionMb "github" (Config <$> tokenP)
+ src/Taskell/IO/Config/Layout.hs view
@@ -0,0 +1,54 @@+module Taskell.IO.Config.Layout+ ( Config+ , defaultConfig+ , parser+ , padding+ , columnWidth+ , columnPadding+ , descriptionIndicator+ , statusBar+ ) where++import ClassyPrelude++import Data.Ini.Config++import Taskell.IO.Config.Parser (noEmpty, parseText)++data Config = Config+ { padding :: Int+ , columnWidth :: Int+ , columnPadding :: Int+ , descriptionIndicator :: Text+ , statusBar :: Bool+ }++defaultConfig :: Config+defaultConfig =+ Config+ {padding = 1, columnWidth = 30, columnPadding = 3, descriptionIndicator = "≡", statusBar = True}++paddingP :: SectionParser Int+paddingP = fromMaybe (padding defaultConfig) <$> fieldMbOf "padding" number++columnWidthP :: SectionParser Int+columnWidthP = fromMaybe (columnWidth defaultConfig) <$> fieldMbOf "column_width" number++columnPaddingP :: SectionParser Int+columnPaddingP = fromMaybe (columnPadding defaultConfig) <$> fieldMbOf "column_padding" number++descriptionIndicatorP :: SectionParser Text+descriptionIndicatorP =+ fromMaybe (descriptionIndicator defaultConfig) . (noEmpty . parseText =<<) <$>+ fieldMb "description_indicator"++statusBarP :: SectionParser Bool+statusBarP = fieldFlagDef "statusbar" (statusBar defaultConfig)++parser :: IniParser Config+parser =+ fromMaybe defaultConfig <$>+ sectionMb+ "layout"+ (Config <$> paddingP <*> columnWidthP <*> columnPaddingP <*> descriptionIndicatorP <*>+ statusBarP)
+ src/Taskell/IO/Config/Markdown.hs view
@@ -0,0 +1,66 @@+module Taskell.IO.Config.Markdown+ ( Config(Config)+ , defaultConfig+ , parser+ , titleOutput+ , taskOutput+ , descriptionOutput+ , dueOutput+ , subtaskOutput+ , localTimes+ ) where++import ClassyPrelude++import Data.Ini.Config++import Taskell.IO.Config.Parser (noEmpty, parseText)++data Config = Config+ { titleOutput :: Text+ , taskOutput :: Text+ , descriptionOutput :: Text+ , dueOutput :: Text+ , subtaskOutput :: Text+ , localTimes :: Bool+ }++defaultConfig :: Config+defaultConfig =+ Config+ { titleOutput = "##"+ , taskOutput = "-"+ , descriptionOutput = " >"+ , dueOutput = " @"+ , subtaskOutput = " *"+ , localTimes = False+ }++titleOutputP :: SectionParser Text+titleOutputP = fromMaybe (titleOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "title"++taskOutputP :: SectionParser Text+taskOutputP = fromMaybe (taskOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "task"++descriptionOutputP :: SectionParser Text+descriptionOutputP =+ fromMaybe (descriptionOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "summary"++dueOutputP :: SectionParser Text+dueOutputP = fromMaybe (dueOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "due"++subtaskOutputP :: SectionParser Text+subtaskOutputP =+ fromMaybe (subtaskOutput defaultConfig) . (noEmpty . parseText =<<) <$> fieldMb "subtask"++localTimesP :: SectionParser Bool+localTimesP = fieldFlagDef "localTimes" (localTimes defaultConfig)++parser :: IniParser Config+parser =+ fromMaybe defaultConfig <$>+ sectionMb+ "markdown"+ (Config <$> titleOutputP <*> taskOutputP <*> descriptionOutputP <*> dueOutputP <*>+ subtaskOutputP <*>+ localTimesP)
+ src/Taskell/IO/Config/Parser.hs view
@@ -0,0 +1,12 @@+module Taskell.IO.Config.Parser where++import ClassyPrelude++import Data.Text as T (dropAround, strip)++noEmpty :: Text -> Maybe Text+noEmpty "" = Nothing+noEmpty txt = Just txt++parseText :: Text -> Text+parseText = dropAround (== '"') . strip
+ src/Taskell/IO/Config/Trello.hs view
@@ -0,0 +1,25 @@+module Taskell.IO.Config.Trello+ ( Config+ , defaultConfig+ , parser+ , token+ ) where++import ClassyPrelude++import Data.Ini.Config++import Taskell.IO.HTTP.Trello (TrelloToken)++data Config = Config+ { token :: Maybe TrelloToken+ }++defaultConfig :: Config+defaultConfig = Config {token = Nothing}++tokenP :: SectionParser (Maybe TrelloToken)+tokenP = fieldMb "token"++parser :: IniParser Config+parser = fromMaybe defaultConfig <$> sectionMb "trello" (Config <$> tokenP)
+ src/Taskell/IO/HTTP/Aeson.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.Aeson where++import ClassyPrelude++import Data.Aeson (defaultOptions, fieldLabelModifier)+import qualified Data.Aeson.TH as TH (deriveFromJSON)+import Language.Haskell.TH (Dec, Name, Q)++import Data.FileEmbed (embedFile)++deriveFromJSON :: Name -> Q [Dec]+deriveFromJSON = TH.deriveFromJSON defaultOptions {fieldLabelModifier = drop 1}++parseError :: String -> Text+parseError err = decodeUtf8 $(embedFile "templates/api-error.txt") <> "\n\n" <> pack err
+ src/Taskell/IO/HTTP/GitHub.hs view
@@ -0,0 +1,161 @@+module Taskell.IO.HTTP.GitHub+ ( GitHubToken+ , GitHubIdentifier+ , getNextLink+ , getLists+ ) where++import ClassyPrelude++import Control.Lens ((^.))+import Data.Sequence (mapWithIndex, (!?))+import Data.Text (splitOn, strip)++import Data.Aeson+import Taskell.UI.CLI (prompt)++import Network.HTTP.Client (requestHeaders)+import Network.HTTP.Simple (Response, getResponseBody, getResponseHeader,+ getResponseStatusCode, httpBS, parseRequest)+import Network.HTTP.Types.Header (HeaderName)++import Taskell.IO.HTTP.Aeson (parseError)+import Taskell.IO.HTTP.GitHub.AutomatedCard (automatedCardToTask)+import Taskell.IO.HTTP.GitHub.Card (MaybeCard, content_url, maybeCardToTask)+import Taskell.IO.HTTP.GitHub.Column (Column, cardsURL, columnToList)+import Taskell.IO.HTTP.GitHub.Project (Project, columnsURL, name)++import Taskell.Data.List (List)+import Taskell.Data.Lists (Lists)+import Taskell.Data.Task (Task)++type GitHubToken = Text++type GitHubIdentifier = Text++type ReaderGitHubToken a = ReaderT GitHubToken IO a++concatEithers :: [Either String [a]] -> Either String [a]+concatEithers vals = bool (Left errs) (Right as) (null errs)+ where+ (errs, as) = bimap unlines concat $ partitionEithers vals++root :: Text+root = "https://api.github.com/"++headers :: ReaderGitHubToken [(HeaderName, ByteString)]+headers = do+ token <- ask+ pure+ [ ("User-Agent", "smallhadroncollider/taskell")+ , ("Accept", "application/vnd.github.inertia-preview+json")+ , ("Authorization", encodeUtf8 ("token " <> token))+ ]++getNextLink :: [ByteString] -> Maybe Text+getNextLink bs = do+ lnks <- splitOn "," . decodeUtf8 <$> headMay bs+ let rel = "rel=\"next\""+ next <- find (isSuffixOf rel) lnks+ stripPrefix "<" =<< stripSuffix (">; " <> rel) (strip next)++fetchURL :: Text -> ReaderGitHubToken (Response ByteString)+fetchURL url = do+ initialRequest <- lift $ parseRequest (unpack url)+ rHeaders <- headers+ lift . httpBS $ initialRequest {requestHeaders = rHeaders}++fetch' :: [ByteString] -> Text -> ReaderGitHubToken (Int, [ByteString])+fetch' bs url = do+ response <- fetchURL url+ let responses = bs <> [getResponseBody response]+ case getNextLink (getResponseHeader "Link" response) of+ Nothing -> pure (getResponseStatusCode response, responses)+ Just lnk -> fetch' responses lnk++fetch :: Text -> ReaderGitHubToken (Int, [ByteString])+fetch = fetch' []++fetchContent :: MaybeCard -> ReaderGitHubToken (Either Text Task)+fetchContent card =+ case maybeCardToTask card of+ Just tsk -> pure $ Right tsk+ Nothing ->+ case card ^. content_url of+ Nothing -> pure $ Left "Could not parse card"+ Just url -> do+ body <- getResponseBody <$> fetchURL url+ pure . first parseError $ automatedCardToTask <$> eitherDecodeStrict body++getCards :: Text -> ReaderGitHubToken (Either Text [Task])+getCards url = do+ (status, body) <- fetch url+ case status of+ 200 ->+ case concatEithers (eitherDecodeStrict <$> body) of+ Right cards -> do+ cds <- sequence (fetchContent <$> cards)+ let (ls, rs) = first unlines $ partitionEithers cds+ pure $ bool (Left ls) (Right rs) (null ls)+ Left err -> pure $ Left (parseError err)+ 429 -> pure $ Left "Too many cards"+ _ -> pure . Left $ tshow status <> " error while fetching " <> url++addCard :: Column -> ReaderGitHubToken (Either Text List)+addCard column = do+ cards <- getCards $ column ^. cardsURL+ pure $ columnToList column <$> cards++addCards :: [Column] -> ReaderGitHubToken (Either Text Lists)+addCards columns = (fromList <$>) . sequence <$> traverse addCard columns++getColumns :: Text -> ReaderGitHubToken (Either Text Lists)+getColumns url = do+ putStrLn "Fetching project from GitHub..."+ (status, body) <- fetch url+ case status of+ 200 ->+ case concatEithers (eitherDecodeStrict <$> body) of+ Right columns -> addCards columns+ Left err -> pure $ Left (parseError err)+ 404 -> pure . Left $ "Could not find GitHub project ."+ 401 -> pure . Left $ "You do not have permission to view GitHub project " <> url+ _ -> pure . Left $ tshow status <> " error. Cannot fetch columns from GitHub."++printProjects :: Seq Project -> Text+printProjects projects = unlines $ toList display+ where+ names = (^. name) <$> projects+ line i nm = concat ["[", tshow (i + 1), "] ", nm]+ display = line `mapWithIndex` names++chooseProject :: [Project] -> ReaderGitHubToken (Either Text Lists)+chooseProject projects = do+ let projects' = fromList projects+ putStrLn $ printProjects projects'+ chosen <- lift $ prompt "Import project"+ let project = (projects' !?) =<< (-) 1 <$> readMay chosen+ case project of+ Nothing -> pure $ Left "Invalid project selected"+ Just proj -> getColumns (proj ^. columnsURL)++getLists :: GitHubIdentifier -> ReaderGitHubToken (Either Text Lists)+getLists identifier = do+ putStrLn "Fetching project list from GitHub...\n"+ let url = concat [root, identifier, "/projects"]+ (status, body) <- fetch url+ case status of+ 200 ->+ case concatEithers (eitherDecodeStrict <$> body) of+ Right projects ->+ if null projects+ then pure . Left $ concat ["\nNo projects found for ", identifier, "\n"]+ else chooseProject projects+ Left err -> pure $ Left (parseError err)+ 404 ->+ pure . Left $+ "Could not find GitHub org/repo. For organisation make sure you use 'orgs/<org-name>' and for repos use 'repos/<username>/<repo-name>'"+ 401 ->+ pure . Left $+ "You do not have permission to view the GitHub projects for " <> identifier+ _ -> pure . Left $ tshow status <> " error. Cannot fetch projects from GitHub."
+ src/Taskell/IO/HTTP/GitHub/AutomatedCard.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.GitHub.AutomatedCard+ ( AutomatedCard(AutomatedCard)+ , automatedCardToTask+ ) where++import ClassyPrelude++import Control.Lens (makeLenses, (^.))++import qualified Taskell.Data.Task as T (Task, new, setDescription)+import Taskell.IO.HTTP.Aeson (deriveFromJSON)+import Taskell.IO.HTTP.GitHub.Utility (cleanUp)++data AutomatedCard = AutomatedCard+ { _title :: Text+ , _body :: Text+ } deriving (Eq, Show)++-- strip underscores from field labels+$(deriveFromJSON ''AutomatedCard)++-- create lenses+$(makeLenses ''AutomatedCard)++-- operations+automatedCardToTask :: AutomatedCard -> T.Task+automatedCardToTask automatedCard = T.setDescription (cleanUp (automatedCard ^. body)) task+ where+ task = T.new $ cleanUp (automatedCard ^. title)
+ src/Taskell/IO/HTTP/GitHub/Card.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.GitHub.Card+ ( MaybeCard(MaybeCard)+ , maybeCardToTask+ , content_url+ ) where++import ClassyPrelude++import Control.Lens (makeLenses, (^.))++import qualified Taskell.Data.Task as T (Task, new)+import Taskell.IO.HTTP.Aeson (deriveFromJSON)+import Taskell.IO.HTTP.GitHub.Utility (cleanUp)++data MaybeCard = MaybeCard+ { _note :: Maybe Text+ , _content_url :: Maybe Text+ } deriving (Eq, Show)++-- strip underscores from field labels+$(deriveFromJSON ''MaybeCard)++-- create lenses+$(makeLenses ''MaybeCard)++-- operations+maybeCardToTask :: MaybeCard -> Maybe T.Task+maybeCardToTask card = T.new . cleanUp <$> card ^. note
+ src/Taskell/IO/HTTP/GitHub/Column.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.GitHub.Column+ ( Column+ , columnToList+ , cardsURL+ ) where++import ClassyPrelude++import Control.Lens (Lens', makeLenses, (^.))++import Taskell.IO.HTTP.Aeson (deriveFromJSON)++import qualified Taskell.Data.List as L (List, create)+import qualified Taskell.Data.Task as T (Task)++data Column = Column+ { _name :: Text+ , _cards_url :: Text+ } deriving (Eq, Show)++-- create Aeson code+$(deriveFromJSON ''Column)++-- create lenses+$(makeLenses ''Column)++-- operations+cardsURL :: Lens' Column Text+cardsURL = cards_url++columnToList :: Column -> [T.Task] -> L.List+columnToList ls tasks = L.create (ls ^. name) (fromList tasks)
+ src/Taskell/IO/HTTP/GitHub/Project.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.GitHub.Project+ ( Project+ , columnsURL+ , name+ ) where++import ClassyPrelude++import Control.Lens (Lens', makeLenses)++import Taskell.IO.HTTP.Aeson (deriveFromJSON)++data Project = Project+ { _name :: Text+ , _columns_url :: Text+ } deriving (Eq, Show)++-- create Aeson code+$(deriveFromJSON ''Project)++-- create lenses+$(makeLenses ''Project)++-- operations+columnsURL :: Lens' Project Text+columnsURL = columns_url
+ src/Taskell/IO/HTTP/GitHub/Utility.hs view
@@ -0,0 +1,10 @@+module Taskell.IO.HTTP.GitHub.Utility+ ( cleanUp+ ) where++import ClassyPrelude++import Data.Text (replace)++cleanUp :: Text -> Text+cleanUp txt = replace "\r" "" $ replace "\n" " " txt
+ src/Taskell/IO/HTTP/Trello.hs view
@@ -0,0 +1,102 @@+module Taskell.IO.HTTP.Trello+ ( TrelloToken+ , TrelloBoardID+ , getLists+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Data.Aeson+import Network.HTTP.Simple (getResponseBody, getResponseStatusCode, httpBS, parseRequest)++import Taskell.IO.HTTP.Aeson (parseError)+import Taskell.IO.HTTP.Trello.Card (Card, idChecklists, setChecklists)+import Taskell.IO.HTTP.Trello.ChecklistItem (ChecklistItem, checkItems)+import Taskell.IO.HTTP.Trello.List (List, cards, listToList, setCards)++import Taskell.Data.Lists (Lists)++type ReaderTrelloToken a = ReaderT TrelloToken IO a++type TrelloToken = Text++type TrelloBoardID = Text++type TrelloChecklistID = Text++key :: Text+key = "80dbcf6f88f62cc5639774e13342c20b"++root :: Text+root = "https://api.trello.com/1/"++fullURL :: Text -> ReaderTrelloToken String+fullURL uri = do+ token <- ask+ pure . unpack $ concat [root, uri, "&key=", key, "&token=", token]++boardURL :: TrelloBoardID -> ReaderTrelloToken String+boardURL board =+ fullURL $+ concat+ [ "boards/"+ , board+ , "/lists"+ , "?cards=open"+ , "&card_fields=name,due,desc,idChecklists"+ , "&fields=name,cards"+ ]++checklistURL :: TrelloChecklistID -> ReaderTrelloToken String+checklistURL checklist =+ fullURL $ concat ["checklists/", checklist, "?fields=id", "&checkItem_fields=name,state"]++trelloListsToLists :: [List] -> Lists+trelloListsToLists ls = fromList $ listToList <$> ls++fetch :: String -> IO (Int, ByteString)+fetch url = do+ request <- parseRequest url+ response <- httpBS request+ pure (getResponseStatusCode response, getResponseBody response)++getChecklist :: TrelloChecklistID -> ReaderTrelloToken (Either Text [ChecklistItem])+getChecklist checklist = do+ url <- checklistURL checklist+ (status, body) <- lift $ fetch url+ pure $+ case status of+ 200 ->+ case (^. checkItems) <$> eitherDecodeStrict body of+ Right ls -> Right ls+ Left err -> Left $ parseError err+ 429 -> Left "Too many checklists"+ _ -> Left $ tshow status <> " error while fetching checklist " <> checklist++updateCard :: Card -> ReaderTrelloToken (Either Text Card)+updateCard card = (setChecklists card . concat <$>) . sequence <$> checklists+ where+ checklists = traverse getChecklist (card ^. idChecklists)++updateList :: List -> ReaderTrelloToken (Either Text List)+updateList l = (setCards l <$>) . sequence <$> traverse updateCard (l ^. cards)++getChecklists :: [List] -> ReaderTrelloToken (Either Text [List])+getChecklists ls = sequence <$> traverse updateList ls++getLists :: TrelloBoardID -> ReaderTrelloToken (Either Text Lists)+getLists board = do+ putStrLn "Fetching from Trello..."+ url <- boardURL board+ (status, body) <- lift $ fetch url+ case status of+ 200 ->+ case eitherDecodeStrict body of+ Right raw -> fmap trelloListsToLists <$> getChecklists raw+ Left err -> pure . Left $ parseError err+ 404 ->+ pure . Left $ "Could not find Trello board " <> board <> ". Make sure the ID is correct"+ 401 -> pure . Left $ "You do not have permission to view Trello board " <> board+ _ -> pure . Left $ tshow status <> " error. Cannot fetch from Trello."
+ src/Taskell/IO/HTTP/Trello/Card.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.Trello.Card+ ( Card+ , idChecklists+ , cardToTask+ , setChecklists+ ) where++import ClassyPrelude++import Control.Lens (makeLenses, (&), (.~), (^.))++import Taskell.IO.HTTP.Aeson (deriveFromJSON)+import Taskell.IO.HTTP.Trello.ChecklistItem (ChecklistItem, checklistItemToSubTask)++import Taskell.Data.Date (isoToTime)+import qualified Taskell.Data.Task as T (Task, due, new, setDescription, subtasks)++data Card = Card+ { _name :: Text+ , _desc :: Text+ , _due :: Maybe Text+ , _idChecklists :: [Text]+ , _checklists :: Maybe [ChecklistItem]+ } deriving (Eq, Show)++-- strip underscores from field labels+$(deriveFromJSON ''Card)++-- create lenses+$(makeLenses ''Card)++-- operations+cardToTask :: Card -> T.Task+cardToTask card =+ task & T.due .~ isoToTime (fromMaybe "" (card ^. due)) & T.subtasks .~+ fromList (checklistItemToSubTask <$> fromMaybe [] (card ^. checklists))+ where+ task = T.setDescription (card ^. desc) $ T.new (card ^. name)++setChecklists :: Card -> [ChecklistItem] -> Card+setChecklists card cls = card & checklists .~ Just cls
+ src/Taskell/IO/HTTP/Trello/ChecklistItem.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.Trello.ChecklistItem+ ( ChecklistItem+ , checklistItemToSubTask+ , checkItems+ ) where++import ClassyPrelude++import Control.Lens (makeLenses, (^.))+import qualified Taskell.Data.Subtask as ST (Subtask, new)+import Taskell.IO.HTTP.Aeson (deriveFromJSON)++data ChecklistItem = ChecklistItem+ { _name :: Text+ , _state :: Text+ } deriving (Eq, Show)++-- create Aeson code+$(deriveFromJSON ''ChecklistItem)++-- create lenses+$(makeLenses ''ChecklistItem)++-- operations+checklistItemToSubTask :: ChecklistItem -> ST.Subtask+checklistItemToSubTask cl = ST.new (cl ^. name) ((cl ^. state) == "complete")++-- check list wrapper+newtype ChecklistWrapper = ChecklistWrapper+ { _checkItems :: [ChecklistItem]+ } deriving (Eq, Show)++-- create Aeson code+$(deriveFromJSON ''ChecklistWrapper)++-- create lenses+$(makeLenses ''ChecklistWrapper)
+ src/Taskell/IO/HTTP/Trello/List.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.HTTP.Trello.List+ ( List+ , cards+ , setCards+ , listToList+ ) where++import ClassyPrelude++import Control.Lens (makeLenses, (&), (.~), (^.))++import Taskell.IO.HTTP.Aeson (deriveFromJSON)+import Taskell.IO.HTTP.Trello.Card (Card, cardToTask)++import qualified Taskell.Data.List as L (List, create)++data List = List+ { _name :: Text+ , _cards :: [Card]+ } deriving (Eq, Show)++-- create Aeson code+$(deriveFromJSON ''List)++-- create lenses+$(makeLenses ''List)++-- operations+setCards :: List -> [Card] -> List+setCards list cs = list & cards .~ cs++listToList :: List -> L.List+listToList ls = L.create (ls ^. name) (fromList $ cardToTask <$> (ls ^. cards))
+ src/Taskell/IO/Keyboard.hs view
@@ -0,0 +1,77 @@+module Taskell.IO.Keyboard+ ( generate+ , defaultBindings+ , badMapping+ , addMissing+ ) where++import ClassyPrelude hiding ((\\))++import Data.Bitraversable (bitraverse)+import Data.List ((\\))++import qualified Taskell.Events.Actions.Types as A+import Taskell.IO.Keyboard.Types++generate :: Bindings -> Actions -> BoundActions+generate bindings actions =+ mapFromList . catMaybes $ bitraverse bindingToEvent (`lookup` actions) <$> bindings++badMapping :: Bindings -> Either Text Bindings+badMapping bindings =+ if null result+ then Right bindings+ else Left "invalid mapping"+ where+ result = filter ((== A.Nothing) . snd) bindings++addMissing :: Bindings -> Bindings+addMissing bindings = bindings <> replaced+ where+ bnd = A.Nothing : (snd <$> bindings)+ result = A.allActions \\ bnd+ replaced = concat $ replace <$> result++replace :: A.ActionType -> Bindings+replace action = filter ((==) action . snd) defaultBindings++defaultBindings :: Bindings+defaultBindings =+ [ (BChar 'q', A.Quit)+ , (BChar 'u', A.Undo)+ , (BChar 'r', A.Redo)+ , (BChar '/', A.Search)+ , (BChar '!', A.Due)+ , (BChar '?', A.Help)+ , (BChar 'k', A.Previous)+ , (BChar 'j', A.Next)+ , (BChar 'h', A.Left)+ , (BChar 'l', A.Right)+ , (BChar 'G', A.Bottom)+ , (BChar 'g', A.Top)+ , (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)+ , (BChar 'C', A.Clear)+ , (BChar 'D', A.Delete)+ , (BKey "Enter", A.Detail)+ , (BChar '@', A.DueDate)+ , (BKey "Backspace", A.ClearDate)+ , (BChar 'K', A.MoveUp)+ , (BChar 'J', A.MoveDown)+ , (BChar '˙', A.MoveLeftTop)+ , (BChar '¬', A.MoveRightTop)+ , (BChar 'H', A.MoveLeftBottom)+ , (BChar 'L', A.MoveRightBottom)+ , (BKey "Space", A.Complete)+ , (BChar 'm', A.MoveMenu)+ , (BChar 'N', A.ListNew)+ , (BChar 'E', A.ListEdit)+ , (BChar 'X', A.ListDelete)+ , (BChar '>', A.ListRight)+ , (BChar '<', A.ListLeft)+ ]
+ src/Taskell/IO/Keyboard/Parser.hs view
@@ -0,0 +1,42 @@+module Taskell.IO.Keyboard.Parser where++import ClassyPrelude++import Data.Attoparsec.Text++import Taskell.Utility.Parser++import Taskell.Events.Actions.Types (ActionType, read)+import Taskell.IO.Keyboard.Types++-- utility functions+commentP :: Parser ()+commentP = lexeme $ skipMany ((char '#' <|> char ';') *> manyTill anyChar endOfLine)++stripComments :: Parser a -> Parser a+stripComments p = lexeme $ commentP *> p <* commentP++-- ini parser+keyP :: Parser Binding+keyP = lexeme $ BKey <$> (char '<' *> word <* char '>')++charP :: Parser Binding+charP = lexeme $ BChar <$> anyChar++bindingP :: Parser [Binding]+bindingP = lexeme $ (keyP <|> charP) `sepBy` char ','++lineP :: Parser [(Binding, ActionType)]+lineP =+ stripComments $ do+ name <- read <$> word+ _ <- lexeme $ char '='+ binds <- bindingP+ pure $ (, name) <$> binds++bindingsP :: Parser Bindings+bindingsP = stripComments $ concat <$> many' lineP++-- run parser+bindings :: Text -> Either Text Bindings+bindings ini = first (const "Could not parse keyboard bindings.") (parseOnly bindingsP ini)
+ src/Taskell/IO/Keyboard/Types.hs view
@@ -0,0 +1,41 @@+module Taskell.IO.Keyboard.Types where++import ClassyPrelude++import Graphics.Vty.Input.Events (Event (..), Key (..))++import qualified Taskell.Events.Actions.Types as A (ActionType)+import Taskell.Events.State.Types (Stateful)++data Binding+ = BChar Char+ | BKey Text+ deriving (Eq, Ord)++type Bindings = [(Binding, A.ActionType)]++type Actions = Map A.ActionType Stateful++type BoundActions = Map Event Stateful++instance Show Binding where+ show (BChar c) = singleton c+ show (BKey "Up") = "↑"+ show (BKey "Down") = "↓"+ show (BKey "Left") = "←"+ show (BKey "Right") = "→"+ show (BKey name) = "<" <> unpack name <> ">"++bindingsToText :: Bindings -> A.ActionType -> [Text]+bindingsToText bindings key = tshow . fst <$> toList (filterMap (== key) bindings)++bindingToEvent :: Binding -> Maybe Event+bindingToEvent (BChar char) = pure $ EvKey (KChar char) []+bindingToEvent (BKey "Space") = pure $ EvKey (KChar ' ') []+bindingToEvent (BKey "Backspace") = pure $ EvKey KBS []+bindingToEvent (BKey "Enter") = pure $ EvKey KEnter []+bindingToEvent (BKey "Left") = pure $ EvKey KLeft []+bindingToEvent (BKey "Right") = pure $ EvKey KRight []+bindingToEvent (BKey "Up") = pure $ EvKey KUp []+bindingToEvent (BKey "Down") = pure $ EvKey KDown []+bindingToEvent _ = Nothing
+ src/Taskell/IO/Markdown.hs view
@@ -0,0 +1,8 @@+module Taskell.IO.Markdown+ ( parse+ , serialize+ , MarkdownInfo(MarkdownInfo)+ ) where++import Taskell.IO.Markdown.Parser (parse)+import Taskell.IO.Markdown.Serializer (MarkdownInfo (MarkdownInfo), serialize)
+ src/Taskell/IO/Markdown/Parser.hs view
@@ -0,0 +1,64 @@+module Taskell.IO.Markdown.Parser+ ( parse+ ) where++import ClassyPrelude++import Data.Attoparsec.Text hiding (parse)++import qualified Taskell.Data.Date as D (Due, textToTime)+import qualified Taskell.Data.List as L (List, create)+import qualified Taskell.Data.Lists as LS (Lists)+import qualified Taskell.Data.Subtask as ST (Subtask, new)+import qualified Taskell.Data.Task as T (Task, create)++import Taskell.IO.Config.Markdown (Config, descriptionOutput, dueOutput, subtaskOutput, taskOutput,+ titleOutput)+import Taskell.Utility.Parser (lexeme, line)++-- config symbol parsing+type Symbol = (Config -> Text) -> Parser ()++symP :: Config -> Symbol+symP config fn = string (fn config) *> char ' ' $> ()++-- utility functions+emptyMay :: (MonoFoldable a) => a -> Maybe a+emptyMay a =+ if null a+ then Nothing+ else Just a++-- parsers+subtaskCompleteP :: Parser Bool+subtaskCompleteP = (== 'x') <$> (char '[' *> (char 'x' <|> char ' ') <* char ']' <* char ' ')++subtaskP :: Symbol -> Parser ST.Subtask+subtaskP sym = flip ST.new <$> (sym subtaskOutput *> subtaskCompleteP) <*> line++taskDescriptionP :: Symbol -> Parser (Maybe Text)+taskDescriptionP sym = emptyMay <$> (intercalate "\n" <$> many' (sym descriptionOutput *> line))++dueP :: Symbol -> Parser (Maybe D.Due)+dueP sym = (D.textToTime =<<) <$> optional (sym dueOutput *> line)++taskNameP :: Symbol -> Parser Text+taskNameP sym = sym taskOutput *> line++taskP :: Symbol -> Parser T.Task+taskP sym =+ T.create <$> taskNameP sym <*> dueP sym <*> taskDescriptionP sym <*>+ (fromList <$> many' (subtaskP sym))++listTitleP :: Symbol -> Parser Text+listTitleP sym = lexeme $ sym titleOutput *> line++listP :: Symbol -> Parser L.List+listP sym = L.create <$> listTitleP sym <*> (fromList <$> many' (taskP sym))++markdownP :: Symbol -> Parser LS.Lists+markdownP sym = fromList <$> many1 (listP sym) <* endOfInput++-- parse+parse :: Config -> Text -> Either Text LS.Lists+parse config txt = first (const "Could not parse file.") (parseOnly (markdownP (symP config)) txt)
+ src/Taskell/IO/Markdown/Serializer.hs view
@@ -0,0 +1,89 @@+module Taskell.IO.Markdown.Serializer+ ( serialize+ , MarkdownInfo(MarkdownInfo)+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import qualified Data.Text as T (splitOn)++import Data.Time.Zones (TZ)++import Taskell.Data.Date (Due, timeToOutput, timeToOutputLocal)+import Taskell.Data.List (List, tasks, title)+import Taskell.Data.Lists (Lists)+import qualified Taskell.Data.Subtask as ST (Subtask, complete, name)+import qualified Taskell.Data.Task as T (Task, description, due, name, subtasks)++import Taskell.IO.Config.Markdown (Config, descriptionOutput, dueOutput, localTimes, subtaskOutput,+ taskOutput, titleOutput)++data MarkdownInfo = MarkdownInfo+ { mdTZ :: TZ+ , mdConfig :: Config+ }++type ReaderMarkdown = Reader MarkdownInfo++-- utility functions+askConf :: (Config -> a) -> ReaderMarkdown a+askConf fn = fn <$> asks mdConfig++strMay :: (Applicative m) => (a -> m Text) -> Maybe a -> m Text+strMay _ Nothing = pure ""+strMay fn (Just a) = fn a++space :: Text -> Text -> Text+space symbol txt = symbol <> " " <> txt++timeFn :: ReaderMarkdown (Due -> Text)+timeFn = bool timeToOutput <$> (timeToOutputLocal <$> asks mdTZ) <*> askConf localTimes++-- serializers+subtaskCompleteS :: Bool -> Text+subtaskCompleteS True = "[x]"+subtaskCompleteS False = "[ ]"++subtaskS :: ST.Subtask -> ReaderMarkdown Text+subtaskS st = do+ symbol <- askConf subtaskOutput+ pure $ unwords [symbol, subtaskCompleteS (st ^. ST.complete), st ^. ST.name]++subtasksS :: Seq ST.Subtask -> ReaderMarkdown Text+subtasksS sts = intercalate "\n" <$> sequence (subtaskS <$> sts)++descriptionS :: Text -> ReaderMarkdown Text+descriptionS desc = do+ symbol <- askConf descriptionOutput+ pure . intercalate "\n" $ space symbol <$> T.splitOn "\n" desc++dueS :: Due -> ReaderMarkdown Text+dueS due = do+ symbol <- askConf dueOutput+ fn <- timeFn+ pure $ space symbol (fn due)++nameS :: Text -> ReaderMarkdown Text+nameS desc = space <$> askConf taskOutput <*> pure desc++taskS :: T.Task -> ReaderMarkdown Text+taskS t =+ unlines . filter (/= "") <$>+ sequence+ [ nameS (t ^. T.name)+ , strMay dueS (t ^. T.due)+ , strMay descriptionS (t ^. T.description)+ , subtasksS (t ^. T.subtasks)+ ]++listS :: List -> ReaderMarkdown Text+listS list = do+ symbol <- askConf titleOutput+ taskString <- concat <$> sequence (taskS <$> list ^. tasks)+ pure . space symbol $ concat [list ^. title, "\n\n", taskString]++-- serialize+serialize :: Lists -> ReaderMarkdown Text+serialize ls = intercalate "\n" <$> sequence (listS <$> ls)
+ src/Taskell/Types.hs view
@@ -0,0 +1,16 @@+module Taskell.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)++startPointer :: Pointer+startPointer = (ListIndex 0, TaskIndex 0)
+ src/Taskell/UI/CLI.hs view
@@ -0,0 +1,21 @@+module Taskell.UI.CLI+ ( prompt+ , promptYN+ , PromptYN(PromptYes)+ ) where++import ClassyPrelude++prompt :: Text -> IO Text+prompt s = do+ putStr $ s <> ": "+ hFlush stdout -- prevents buffering+ getLine++data PromptYN+ = PromptYes+ | PromptNo++promptYN :: PromptYN -> Text -> IO Bool+promptYN PromptYes s = not . flip elem ["n", "no"] . toLower <$> prompt (s <> " (Y/n)")+promptYN PromptNo s = not . flip elem ["y", "yes"] . toLower <$> prompt (s <> " (y/N)")
+ src/Taskell/UI/Draw.hs view
@@ -0,0 +1,39 @@+module Taskell.UI.Draw+ ( draw+ , chooseCursor+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Control.Monad.Reader (runReader)++import Brick++import Taskell.Events.State (normalise)+import Taskell.Events.State.Types (State, mode)+import Taskell.Events.State.Types.Mode (DetailMode (..), ModalType (..), Mode (..))+import Taskell.IO.Config.Layout (Config)+import Taskell.IO.Keyboard.Types (Bindings)+import Taskell.UI.Draw.Main (renderMain)+import Taskell.UI.Draw.Modal (renderModal)+import Taskell.UI.Draw.Types (DrawState (DrawState), ReaderDrawState, TWidget)+import Taskell.UI.Types (ResourceName (..))++-- draw+renderApp :: ReaderDrawState [TWidget]+renderApp = sequence [renderModal, renderMain]++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)+chooseCursor state =+ case normalise state ^. mode of+ Insert {} -> showCursorNamed RNCursor+ Search -> showCursorNamed RNCursor+ Modal (Detail _ (DetailInsert _)) -> showCursorNamed RNCursor+ _ -> neverShowCursor state
+ src/Taskell/UI/Draw/Field.hs view
@@ -0,0 +1,143 @@+module Taskell.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 Taskell.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/Taskell/UI/Draw/Main.hs view
@@ -0,0 +1,26 @@+module Taskell.UI.Draw.Main+ ( renderMain+ ) where++import ClassyPrelude++import Brick+import Control.Lens ((^.))+import Data.Sequence (mapWithIndex)++import Taskell.Events.State.Types (lists)+import Taskell.IO.Config.Layout (padding, statusBar)+import Taskell.UI.Draw.Main.List (renderList)+import Taskell.UI.Draw.Main.Search (renderSearch)+import Taskell.UI.Draw.Main.StatusBar (renderStatusBar)+import Taskell.UI.Draw.Types (DSWidget, DrawState (..))+import Taskell.UI.Types (ResourceName (..))++renderMain :: DSWidget+renderMain = do+ ls <- (^. lists) <$> asks dsState+ listWidgets <- toList <$> sequence (renderList `mapWithIndex` ls)+ pad <- padding <$> asks dsLayout+ let mainWidget = viewport RNLists Horizontal . padTopBottom pad $ hBox listWidgets+ sb <- bool emptyWidget <$> renderStatusBar <*> (statusBar <$> asks dsLayout)+ renderSearch (mainWidget <=> sb)
+ src/Taskell/UI/Draw/Main/List.hs view
@@ -0,0 +1,89 @@+module Taskell.UI.Draw.Main.List+ ( renderList+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Data.Char (chr, ord)+import Data.Sequence (mapWithIndex)++import Brick++import Taskell.Data.List (List, tasks, title)+import Taskell.Events.State.Types (current, mode)+import Taskell.IO.Config.Layout (columnPadding, columnWidth)+import Taskell.Types (ListIndex (ListIndex), TaskIndex (TaskIndex))+import Taskell.UI.Draw.Field (textField, widgetFromMaybe)+import Taskell.UI.Draw.Mode+import Taskell.UI.Draw.Task (renderTask)+import Taskell.UI.Draw.Types (DSWidget, DrawState (..), ReaderDrawState)+import Taskell.UI.Theme+import Taskell.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/Taskell/UI/Draw/Main/Search.hs view
@@ -0,0 +1,31 @@+module Taskell.UI.Draw.Main.Search+ ( renderSearch+ ) where++import Brick+import ClassyPrelude+import Control.Lens ((^.))++import Taskell.Events.State.Types (mode, searchTerm)+import Taskell.Events.State.Types.Mode (Mode (..))+import Taskell.IO.Config.Layout (columnPadding)+import Taskell.UI.Draw.Field (field)+import Taskell.UI.Draw.Types (DSWidget, DrawState (..))+import Taskell.UI.Theme+import Taskell.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/Taskell/UI/Draw/Main/StatusBar.hs view
@@ -0,0 +1,65 @@+module Taskell.UI.Draw.Main.StatusBar+ ( renderStatusBar+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick++import Taskell.Data.Lists (count)+import Taskell.Events.State.Types (current, lists, mode, path, searchTerm)++import Taskell.Events.State.Types.Mode (ModalType (..), Mode (..))+import Taskell.IO.Config.Layout (columnPadding)+import Taskell.Types (ListIndex (ListIndex), TaskIndex (TaskIndex))+import Taskell.UI.Draw.Field (Field)+import Taskell.UI.Draw.Types (DSWidget, DrawState (..), ReaderDrawState)+import Taskell.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/Taskell/UI/Draw/Modal.hs view
@@ -0,0 +1,44 @@+module Taskell.UI.Draw.Modal+ ( renderModal+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick+import Brick.Widgets.Border+import Brick.Widgets.Center++import Taskell.Events.State.Types (height, mode)+import Taskell.Events.State.Types.Mode (ModalType (..), Mode (..))+import Taskell.UI.Draw.Field (textField)+import Taskell.UI.Draw.Modal.Detail (detail)+import Taskell.UI.Draw.Modal.Due (due)+import Taskell.UI.Draw.Modal.Help (help)+import Taskell.UI.Draw.Modal.MoveTo (moveTo)+import Taskell.UI.Draw.Types (DSWidget, DrawState (dsState), TWidget)+import Taskell.UI.Theme (titleAttr)+import Taskell.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/Taskell/UI/Draw/Modal/Detail.hs view
@@ -0,0 +1,88 @@+module Taskell.UI.Draw.Modal.Detail+ ( detail+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Data.Sequence (mapWithIndex)++import Brick++import Data.Time.Zones (TZ)++import Taskell.Data.Date (deadline, timeToDisplay)+import qualified Taskell.Data.Subtask as ST (Subtask, complete, name)+import Taskell.Data.Task (Task, description, due, name, subtasks)+import Taskell.Events.State (getCurrentTask)+import Taskell.Events.State.Modal.Detail (getCurrentItem, getField)+import Taskell.Events.State.Types (time, timeZone)+import Taskell.Events.State.Types.Mode (DetailItem (..))+import Taskell.UI.Draw.Field (Field, textField, widgetFromMaybe)+import Taskell.UI.Draw.Types (DrawState (..), ModalWidget, TWidget)+import Taskell.UI.Theme (disabledAttr, dlToAttr, subtaskCompleteAttr,+ subtaskCurrentAttr, subtaskIncompleteAttr)++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 subtaskCurrentAttr+ else (if done+ then subtaskCompleteAttr+ else subtaskIncompleteAttr))+ prefix =+ attr . txt $+ if done+ then "[x] "+ else "[ ] "+ widget = textField (subtask ^. ST.name)+ final+ | cur = visible . attr $ widgetFromMaybe widget f+ | otherwise = attr 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 :: TZ -> UTCTime -> Maybe Field -> DetailItem -> Task -> TWidget+renderDate tz now field item task =+ case item of+ DetailDate -> visible $ prefix <+> widgetFromMaybe widget field+ _ ->+ case day of+ Just d -> prefix <+> withAttr (dlToAttr (deadline now d)) widget+ Nothing -> emptyWidget+ where+ day = task ^. due+ prefix = txt "Due: "+ widget = textField $ maybe "" (timeToDisplay tz) day++detail :: ModalWidget+detail = do+ state <- asks dsState+ let now = state ^. time+ let tz = state ^. timeZone+ 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 tz now f i task <=> renderSummary f i task <=> w)
+ src/Taskell/UI/Draw/Modal/Due.hs view
@@ -0,0 +1,39 @@+module Taskell.UI.Draw.Modal.Due+ ( due+ ) where++import ClassyPrelude++import Brick+import Taskell.Data.Seq ((<#>))++import qualified Taskell.Data.Task as T (Task)+import Taskell.Types (Pointer)+import Taskell.UI.Draw.Task (TaskWidget (..), parts)+import Taskell.UI.Draw.Types (DSWidget, ModalWidget)+import Taskell.UI.Theme (taskAttr, taskCurrentAttr)+import Taskell.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/Taskell/UI/Draw/Modal/Help.hs view
@@ -0,0 +1,68 @@+module Taskell.UI.Draw.Modal.Help+ ( help+ ) where++import ClassyPrelude++import Brick+import Data.Text as T (justifyRight)++import Taskell.Events.Actions.Types as A (ActionType (..))+import Taskell.IO.Keyboard.Types (bindingsToText)++import Taskell.IO.Keyboard.Types (Bindings)+import Taskell.UI.Draw.Field (textField)+import Taskell.UI.Draw.Types (DrawState (dsBindings), ModalWidget, TWidget)+import Taskell.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.Top], "Go to top 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.MoveLeftBottom, A.MoveRightBottom], "Shift task left / right (to bottom of list)")+ , ([A.MoveLeftTop, A.MoveRightTop], "Shift task left / right (to top of list)")+ , ( [A.Complete]+ , "Move task to last list and remove any due dates / Mark subtask as (in)complete")+ , ([A.MoveMenu], "Move task to specific list")+ , ([A.Delete], "Delete task")+ , ([A.Undo], "Undo")+ , ([A.Redo], "Redo")+ , ([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/Taskell/UI/Draw/Modal/MoveTo.hs view
@@ -0,0 +1,30 @@+module Taskell.UI.Draw.Modal.MoveTo+ ( moveTo+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick++import Taskell.Data.List (title)+import Taskell.Events.State.Types (current, lists)+import Taskell.Types (showListIndex)+import Taskell.UI.Draw.Field (textField)+import Taskell.UI.Draw.Types (DrawState (dsState), ModalWidget)+import Taskell.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/Taskell/UI/Draw/Mode.hs view
@@ -0,0 +1,18 @@+module Taskell.UI.Draw.Mode where++import ClassyPrelude++import Taskell.Events.State.Types.Mode (InsertType (..), ModalType (..), Mode (..))+import Taskell.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/Taskell/UI/Draw/Task.hs view
@@ -0,0 +1,107 @@+module Taskell.UI.Draw.Task+ ( TaskWidget(..)+ , renderTask+ , parts+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Brick++import Taskell.Data.Date (deadline, timeToText)+import qualified Taskell.Data.Task as T (Task, contains, countCompleteSubtasks,+ countSubtasks, description, due, hasSubtasks,+ name)+import Taskell.Events.State.Types (current, mode, searchTerm, time, timeZone)+import Taskell.IO.Config.Layout (descriptionIndicator)+import Taskell.Types (ListIndex (..), TaskIndex (..))+import Taskell.UI.Draw.Field (getText, textField, widgetFromMaybe)+import Taskell.UI.Draw.Mode+import Taskell.UI.Draw.Types (DSWidget, DrawState (..), ReaderDrawState, TWidget)+import Taskell.UI.Theme+import Taskell.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+ now <- (^. time) <$> asks dsState+ tz <- (^. timeZone) <$> asks dsState+ pure . fromMaybe emptyWidget $+ (\date -> withAttr (dlToAttr $ deadline now date) (txt $ timeToText tz now date)) <$>+ 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/Taskell/UI/Draw/Types.hs view
@@ -0,0 +1,27 @@+module Taskell.UI.Draw.Types where++import ClassyPrelude++import Brick (Widget)++import Taskell.Events.State.Types (State)+import Taskell.IO.Config.Layout (Config)+import Taskell.IO.Keyboard.Types (Bindings)+import Taskell.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/Taskell/UI/Theme.hs view
@@ -0,0 +1,84 @@+module Taskell.UI.Theme+ ( titleAttr+ , statusBarAttr+ , titleCurrentAttr+ , taskCurrentAttr+ , subtaskCurrentAttr+ , subtaskCompleteAttr+ , subtaskIncompleteAttr+ , taskAttr+ , disabledAttr+ , dlToAttr+ , defaultTheme+ ) where++import Brick (AttrName, attrName)+import Brick.Themes (Theme, newTheme)+import Brick.Util (fg, on)+import Graphics.Vty++import Taskell.Data.Date (Deadline (..))++-- attrs+statusBarAttr :: AttrName+statusBarAttr = attrName "statusBar"++titleAttr :: AttrName+titleAttr = attrName "title"++titleCurrentAttr :: AttrName+titleCurrentAttr = attrName "titleCurrent"++taskCurrentAttr :: AttrName+taskCurrentAttr = attrName "taskCurrent"++taskAttr :: AttrName+taskAttr = attrName "task"++subtaskCurrentAttr :: AttrName+subtaskCurrentAttr = attrName "subtaskCurrent"++subtaskCompleteAttr :: AttrName+subtaskCompleteAttr = attrName "subtaskComplete"++subtaskIncompleteAttr :: AttrName+subtaskIncompleteAttr = attrName "subtaskIncomplete"++disabledAttr :: AttrName+disabledAttr = attrName "disabled"++dlDue, dlSoon, dlFar :: AttrName+dlDue = attrName "dlDue"++dlSoon = attrName "dlSoon"++dlFar = attrName "dlFar"++-- convert deadline into attribute+dlToAttr :: Deadline -> AttrName+dlToAttr dl =+ case dl of+ Plenty -> dlFar+ ThisWeek -> dlSoon+ Tomorrow -> dlSoon+ Today -> dlDue+ Passed -> dlDue++-- default theme+defaultTheme :: Theme+defaultTheme =+ newTheme+ defAttr+ [ (statusBarAttr, black `on` green)+ , (titleAttr, fg green)+ , (titleCurrentAttr, fg blue)+ , (taskCurrentAttr, fg magenta)+ , (subtaskCurrentAttr, fg magenta)+ , (subtaskIncompleteAttr, fg blue)+ , (subtaskCompleteAttr, fg yellow)+ , (taskCurrentAttr, fg magenta)+ , (disabledAttr, fg yellow)+ , (dlDue, fg red)+ , (dlSoon, fg yellow)+ , (dlFar, fg green)+ ]
+ src/Taskell/UI/Types.hs view
@@ -0,0 +1,14 @@+module Taskell.UI.Types where++import ClassyPrelude (Eq, Int, Ord, Show)++import Taskell.Types (ListIndex, TaskIndex)++data ResourceName+ = RNCursor+ | RNTask (ListIndex, TaskIndex)+ | RNList Int+ | RNLists+ | RNModal+ | RNDue Int+ deriving (Show, Eq, Ord)
+ src/Taskell/Utility/Parser.hs view
@@ -0,0 +1,17 @@+module Taskell.Utility.Parser where++import ClassyPrelude++import Data.Attoparsec.Text++only :: Parser a -> Parser a+only p = p <* endOfInput++lexeme :: Parser a -> Parser a+lexeme p = skipSpace *> p <* skipSpace++line :: Parser Text+line = (takeTill isEndOfLine <* endOfLine) <|> takeText++word :: Parser Text+word = lexeme $ pack <$> many1 letter
− src/Types.hs
@@ -1,18 +0,0 @@-{-# 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)--startPointer :: Pointer-startPointer = (ListIndex 0, TaskIndex 0)
− src/UI/CLI.hs
@@ -1,24 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.CLI- ( prompt- , promptYN- , PromptYN(PromptYes)- ) where--import ClassyPrelude--prompt :: Text -> IO Text-prompt s = do- putStr $ s <> ": "- hFlush stdout -- prevents buffering- getLine--data PromptYN- = PromptYes- | PromptNo--promptYN :: PromptYN -> Text -> IO Bool-promptYN PromptYes s = not . flip elem ["n", "no"] . toLower <$> prompt (s <> " (Y/n)")-promptYN PromptNo s = not . flip elem ["y", "yes"] . toLower <$> prompt (s <> " (y/N)")
− src/UI/Draw.hs
@@ -1,41 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module UI.Draw- ( draw- , chooseCursor- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Control.Monad.Reader (runReader)--import Brick--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-renderApp :: ReaderDrawState [TWidget]-renderApp = sequence [renderModal, renderMain]--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)-chooseCursor state =- case normalise state ^. mode of- Insert {} -> showCursorNamed RNCursor- Search -> showCursorNamed RNCursor- Modal (Detail _ (DetailInsert _)) -> showCursorNamed RNCursor- _ -> neverShowCursor state
− src/UI/Draw/Field.hs
@@ -1,146 +0,0 @@-{-# 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
@@ -1,28 +0,0 @@-{-# 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 IO.Config.Layout (padding, statusBar)-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)- pad <- padding <$> asks dsLayout- let mainWidget = viewport RNLists Horizontal . padTopBottom pad $ hBox listWidgets- sb <- bool emptyWidget <$> renderStatusBar <*> (statusBar <$> asks dsLayout)- renderSearch (mainWidget <=> sb)
− src/UI/Draw/Main/List.hs
@@ -1,93 +0,0 @@-{-# 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
@@ -1,34 +0,0 @@-{-# 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
@@ -1,69 +0,0 @@-{-# 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
@@ -1,46 +0,0 @@-{-# 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
@@ -1,90 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.Draw.Modal.Detail- ( detail- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Data.Sequence (mapWithIndex)--import Brick--import Data.Time.Zones (TZ)--import Data.Taskell.Date (deadline, timeToDisplay)-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, timeZone)-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 :: TZ -> UTCTime -> Maybe Field -> DetailItem -> Task -> TWidget-renderDate tz now field item task =- case item of- DetailDate -> visible $ prefix <+> widgetFromMaybe widget field- _ ->- case day of- Just d -> prefix <+> withAttr (dlToAttr (deadline now d)) widget- Nothing -> emptyWidget- where- day = task ^. due- prefix = txt "Due: "- widget = textField $ maybe "" (timeToDisplay tz) day--detail :: ModalWidget-detail = do- state <- asks dsState- let now = state ^. time- let tz = state ^. timeZone- 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 tz now f i task <=> renderSummary f i task <=> w)
− src/UI/Draw/Modal/Due.hs
@@ -1,42 +0,0 @@-{-# 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
@@ -1,70 +0,0 @@-{-# 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.Top], "Go to top 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 / Mark subtask as (in)complete")- , ([A.MoveMenu], "Move task to specific list")- , ([A.Delete], "Delete task")- , ([A.Undo], "Undo")- , ([A.Redo], "Redo")- , ([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
@@ -1,33 +0,0 @@-{-# 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
@@ -1,20 +0,0 @@-{-# 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
@@ -1,109 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.Draw.Task- ( TaskWidget(..)- , renderTask- , parts- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Brick--import Data.Taskell.Date (deadline, timeToText)-import qualified Data.Taskell.Task as T (Task, contains, countCompleteSubtasks, countSubtasks,- description, due, hasSubtasks, name)-import Events.State.Types (current, mode, searchTerm, time, timeZone)-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- now <- (^. time) <$> asks dsState- tz <- (^. timeZone) <$> asks dsState- pure . fromMaybe emptyWidget $- (\date -> withAttr (dlToAttr $ deadline now date) (txt $ timeToText tz now date)) <$>- 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
@@ -1,29 +0,0 @@-{-# 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/Theme.hs
@@ -1,70 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module UI.Theme- ( titleAttr- , statusBarAttr- , titleCurrentAttr- , taskCurrentAttr- , taskAttr- , disabledAttr- , dlToAttr- , defaultTheme- ) where--import Brick (AttrName, attrName)-import Brick.Themes (Theme, newTheme)-import Brick.Util (fg, on)-import Graphics.Vty--import Data.Taskell.Date (Deadline (..))---- attrs-statusBarAttr :: AttrName-statusBarAttr = attrName "statusBar"--titleAttr :: AttrName-titleAttr = attrName "title"--titleCurrentAttr :: AttrName-titleCurrentAttr = attrName "titleCurrent"--taskCurrentAttr :: AttrName-taskCurrentAttr = attrName "taskCurrent"--taskAttr :: AttrName-taskAttr = attrName "task"--disabledAttr :: AttrName-disabledAttr = attrName "disabled"--dlDue, dlSoon, dlFar :: AttrName-dlDue = attrName "dlDue"--dlSoon = attrName "dlSoon"--dlFar = attrName "dlFar"---- convert deadline into attribute-dlToAttr :: Deadline -> AttrName-dlToAttr dl =- case dl of- Plenty -> dlFar- ThisWeek -> dlSoon- Tomorrow -> dlSoon- Today -> dlDue- Passed -> dlDue---- default theme-defaultTheme :: Theme-defaultTheme =- newTheme- defAttr- [ (statusBarAttr, black `on` green)- , (titleAttr, fg green)- , (titleCurrentAttr, fg blue)- , (taskCurrentAttr, fg magenta)- , (disabledAttr, fg yellow)- , (dlDue, fg red)- , (dlSoon, fg yellow)- , (dlFar, fg green)- ]
− src/UI/Types.hs
@@ -1,16 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module UI.Types where--import ClassyPrelude (Eq, Int, Ord, Show)--import Types (ListIndex, TaskIndex)--data ResourceName- = RNCursor- | RNTask (ListIndex, TaskIndex)- | RNList Int- | RNLists- | RNModal- | RNDue Int- deriving (Show, Eq, Ord)
− src/Utility/Parser.hs
@@ -1,19 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}--module Utility.Parser where--import ClassyPrelude--import Data.Attoparsec.Text--only :: Parser a -> Parser a-only p = p <* endOfInput--lexeme :: Parser a -> Parser a-lexeme p = skipSpace *> p <* skipSpace--line :: Parser Text-line = (takeTill isEndOfLine <* endOfLine) <|> takeText--word :: Parser Text-word = lexeme $ pack <$> many1 letter
taskell.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.12 name: taskell-version: 1.9.2.0+version: 1.9.3.0 license: BSD3 license-file: LICENSE copyright: 2019 Mark Wales@@ -22,6 +22,9 @@ templates/theme.ini templates/trello-token.txt templates/usage.txt+ test/Taskell/IO/data/trello-checklists.json+ test/Taskell/IO/data/trello.json+ test/Taskell/IO/Keyboard/data/bindings.ini source-repository head type: git@@ -29,106 +32,107 @@ library exposed-modules:- App- Events.State- IO.Config- IO.Taskell- Data.Taskell.Date- Data.Taskell.Date.RelativeParser- Data.Taskell.List- Data.Taskell.List.Internal- Data.Taskell.Lists- Data.Taskell.Lists.Internal- Data.Taskell.Seq- Data.Taskell.Subtask- Data.Taskell.Subtask.Internal- Data.Taskell.Task- Data.Taskell.Task.Internal- Events.Actions.Types- Events.State.History- Events.State.Types- Events.State.Types.Mode- IO.Config.Markdown- IO.Markdown.Parser- IO.Markdown.Serializer- IO.HTTP.GitHub- IO.HTTP.GitHub.Card- IO.HTTP.Trello.List- IO.HTTP.Trello.ChecklistItem- IO.Keyboard- IO.Keyboard.Parser- IO.Keyboard.Types- UI.Draw.Field- Types+ Taskell+ Taskell.Events.State+ Taskell.IO.Config+ Taskell.IO+ Taskell.Data.Date+ Taskell.Data.Date.RelativeParser+ Taskell.Data.List+ Taskell.Data.List.Internal+ Taskell.Data.Lists+ Taskell.Data.Lists.Internal+ Taskell.Data.Seq+ Taskell.Data.Subtask+ Taskell.Data.Subtask.Internal+ Taskell.Data.Task+ Taskell.Data.Task.Internal+ Taskell.Events.Actions.Types+ Taskell.Events.State.History+ Taskell.Events.State.Types+ Taskell.Events.State.Types.Mode+ Taskell.IO.Config.Markdown+ Taskell.IO.Markdown.Parser+ Taskell.IO.Markdown.Serializer+ Taskell.IO.HTTP.GitHub+ Taskell.IO.HTTP.GitHub.Card+ Taskell.IO.HTTP.Trello.List+ Taskell.IO.HTTP.Trello.ChecklistItem+ Taskell.IO.Keyboard+ Taskell.IO.Keyboard.Parser+ Taskell.IO.Keyboard.Types+ Taskell.UI.Draw.Field+ Taskell.Types hs-source-dirs: src other-modules:- Config- Data.Taskell.Date.Types- Events.Actions- 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- IO.Config.Parser- IO.Config.Trello- IO.HTTP.Aeson- IO.HTTP.GitHub.AutomatedCard- IO.HTTP.GitHub.Column- IO.HTTP.GitHub.Project- IO.HTTP.GitHub.Utility- IO.HTTP.Trello- IO.HTTP.Trello.Card- IO.Markdown- UI.CLI- UI.Draw- 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- Utility.Parser+ Taskell.Config+ Taskell.Data.Date.Types+ Taskell.Events.Actions+ Taskell.Events.Actions.Insert+ Taskell.Events.Actions.Modal+ Taskell.Events.Actions.Modal.Detail+ Taskell.Events.Actions.Modal.Due+ Taskell.Events.Actions.Modal.Help+ Taskell.Events.Actions.Modal.MoveTo+ Taskell.Events.Actions.Normal+ Taskell.Events.Actions.Search+ Taskell.Events.State.Modal.Detail+ Taskell.Events.State.Modal.Due+ Taskell.IO.Config.General+ Taskell.IO.Config.GitHub+ Taskell.IO.Config.Layout+ Taskell.IO.Config.Parser+ Taskell.IO.Config.Trello+ Taskell.IO.HTTP.Aeson+ Taskell.IO.HTTP.GitHub.AutomatedCard+ Taskell.IO.HTTP.GitHub.Column+ Taskell.IO.HTTP.GitHub.Project+ Taskell.IO.HTTP.GitHub.Utility+ Taskell.IO.HTTP.Trello+ Taskell.IO.HTTP.Trello.Card+ Taskell.IO.Markdown+ Taskell.UI.CLI+ Taskell.UI.Draw+ Taskell.UI.Draw.Main+ Taskell.UI.Draw.Main.List+ Taskell.UI.Draw.Main.Search+ Taskell.UI.Draw.Main.StatusBar+ Taskell.UI.Draw.Modal+ Taskell.UI.Draw.Modal.Detail+ Taskell.UI.Draw.Modal.Due+ Taskell.UI.Draw.Modal.Help+ Taskell.UI.Draw.Modal.MoveTo+ Taskell.UI.Draw.Mode+ Taskell.UI.Draw.Task+ Taskell.UI.Draw.Types+ Taskell.UI.Theme+ Taskell.UI.Types+ Taskell.Utility.Parser Paths_taskell default-language: Haskell2010 default-extensions: OverloadedStrings NoImplicitPrelude+ TupleSections LambdaCase RankNTypes build-depends:- aeson >=1.4.5.0 && <1.5,+ aeson >=1.4.6.0 && <1.5, attoparsec >=0.13.2.3 && <0.14,- base >=4.12.0.0 && <=5,- brick ==0.50.*,- bytestring >=0.10.8.2 && <0.11,+ base >=4.13.0.0 && <=5,+ brick ==0.52.*,+ bytestring >=0.10.10.0 && <0.11, classy-prelude >=1.5.0 && <1.6, config-ini >=0.2.4.0 && <0.3,- containers >=0.6.0.1 && <0.7,- directory >=1.3.3.0 && <1.4,- file-embed >=0.0.11 && <0.1,+ containers >=0.6.2.1 && <0.7,+ directory >=1.3.4.0 && <1.4,+ file-embed >=0.0.11.1 && <0.1, 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,+ lens >=4.18.1 && <4.19, mtl >=2.2.2 && <2.3,- template-haskell >=2.14.0.0 && <2.15,- text >=1.2.3.1 && <1.3,- time >=1.8.0.2 && <1.9,- tz >=0.1.3.2 && <0.2,+ template-haskell >=2.15.0.0 && <2.16,+ text >=1.2.4.0 && <1.3,+ time >=1.9.3 && <1.10,+ tz >=0.1.3.3 && <0.2, vty ==5.26.* executable taskell@@ -138,48 +142,50 @@ Paths_taskell default-language: Haskell2010 default-extensions: OverloadedStrings NoImplicitPrelude+ TupleSections LambdaCase RankNTypes ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends:- base >=4.12.0.0 && <4.13,+ base >=4.13.0.0 && <4.14, classy-prelude >=1.5.0 && <1.6, taskell -any,- tz >=0.1.3.2 && <0.2+ tz >=0.1.3.3 && <0.2 test-suite taskell-test type: exitcode-stdio-1.0 main-is: Spec.hs hs-source-dirs: test other-modules:- Data.Taskell.Date.RelativeParserTest- Data.Taskell.DateTest- Data.Taskell.ListNavigationTest- Data.Taskell.ListsTest- Data.Taskell.ListTest- Data.Taskell.SeqTest- Data.Taskell.SubtaskTest- Data.Taskell.TaskTest- Events.State.HistoryTest- Events.StateTest- IO.GitHub.CardsTest- IO.GitHubTest- IO.Keyboard.ParserTest- IO.Keyboard.TypesTest- IO.KeyboardTest- IO.Markdown.ParserTest- IO.Markdown.SerializerTest- IO.TrelloTest- UI.FieldTest+ Taskell.Data.Date.RelativeParserTest+ Taskell.Data.DateTest+ Taskell.Data.ListNavigationTest+ Taskell.Data.ListsTest+ Taskell.Data.ListTest+ Taskell.Data.SeqTest+ Taskell.Data.SubtaskTest+ Taskell.Data.TaskTest+ Taskell.Events.State.HistoryTest+ Taskell.Events.StateTest+ Taskell.IO.GitHub.CardsTest+ Taskell.IO.GitHubTest+ Taskell.IO.Keyboard.ParserTest+ Taskell.IO.Keyboard.TypesTest+ Taskell.IO.KeyboardTest+ Taskell.IO.Markdown.ParserTest+ Taskell.IO.Markdown.SerializerTest+ Taskell.IO.TrelloTest+ Taskell.UI.FieldTest Paths_taskell default-language: Haskell2010 default-extensions: OverloadedStrings NoImplicitPrelude+ TupleSections LambdaCase RankNTypes ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends:- aeson >=1.4.5.0 && <1.5,- base >=4.12.0.0 && <4.13,+ aeson >=1.4.6.0 && <1.5,+ base >=4.13.0.0 && <4.14, classy-prelude >=1.5.0 && <1.6,- containers >=0.6.0.1 && <0.7,- file-embed >=0.0.11 && <0.1,- lens >=4.17.1 && <4.18,+ containers >=0.6.2.1 && <0.7,+ file-embed >=0.0.11.1 && <0.1,+ lens >=4.18.1 && <4.19, mtl >=2.2.2 && <2.3, raw-strings-qq ==1.1.*, taskell -any,@@ -187,7 +193,7 @@ tasty-discover >=4.2.1 && <4.3, 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,- tz >=0.1.3.2 && <0.2,+ text >=1.2.4.0 && <1.3,+ time >=1.9.3 && <1.10,+ tz >=0.1.3.3 && <0.2, vty ==5.26.*
templates/bindings.ini view
@@ -31,9 +31,17 @@ # moving tasks moveUp = K moveDown = J-moveLeft = H-moveRight = L+# move to top of previous list+moveLeftTop = ˙+# move to top of next list+moveRightTop = ¬+# move to bottom of previous list+moveLeftBottom = H+# move to bottom of next list+moveRightBottom = L+# move to bottom of last list complete = <Space>+# select a list to move to moveMenu = m # lists
templates/theme.ini view
@@ -13,5 +13,13 @@ ; current task taskCurrent.fg = magenta +; subtasks+; selected+subtaskCurrent.fg = magenta+; incomplete+subtaskIncomplete.fg = blue+; complete+subtaskComplete.fg = yellow+ ; disabled disabled.fg = yellow
− test/Data/Taskell/Date/RelativeParserTest.hs
@@ -1,79 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.Date.RelativeParserTest- ( test_relative_parser- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Data.Time.Calendar (fromGregorian)-import Data.Time.Clock (secondsToDiffTime)--import Data.Taskell.Date (Due (DueDate, DueTime))-import Data.Taskell.Date.RelativeParser (parseRelative)--toTime :: (Integer, Int, Int) -> Integer -> Due-toTime (y, m, d) seconds = DueTime $ UTCTime (fromGregorian y m d) (secondsToDiffTime seconds)--toDay :: (Integer, Int, Int) -> Due-toDay (y, m, d) = DueDate $ fromGregorian y m d---- 08:53:03 18th December 2019-time :: UTCTime-time = UTCTime (fromGregorian 2019 12 18) 31983---- tests-test_relative_parser :: TestTree-test_relative_parser =- testGroup- "Data.Taskell.Date.RelativeParser"- [ testCase- "Second"- (assertEqual- "Adds a second"- (Right (toTime (2019, 12, 18) 31984))- (parseRelative time "1s"))- , testCase- "Minute"- (assertEqual- "Adds a minute"- (Right (toTime (2019, 12, 18) 32043))- (parseRelative time "1m"))- , testCase- "Hour"- (assertEqual- "Adds an hour"- (Right (toTime (2019, 12, 18) 35583))- (parseRelative time "1h"))- , testCase- "Day"- (assertEqual "Adds a day" (Right (toDay (2019, 12, 19))) (parseRelative time "1d"))- , testCase- "Days"- (assertEqual "Adds 29 days" (Right (toDay (2020, 1, 16))) (parseRelative time "29 d"))- , testCase- "Week"- (assertEqual "Adds a week" (Right (toDay (2019, 12, 25))) (parseRelative time "1w "))- , testCase- "Mix"- (assertEqual- "Adds 1 week 2 days and 29 seconds"- (Right (toTime (2019, 12, 27) 32012))- (parseRelative time " 1 w 2d 29 s "))- , testCase- "Mix out of order"- (assertEqual- "Adds 1 week 2 days and 29 seconds"- (Right (toTime (2019, 12, 27) 32012))- (parseRelative time " 2d 1 w 29 s"))- , testCase- "invalid format"- (assertEqual "Error" (Left "Could not parse date.") (parseRelative time "18/12/2019"))- , testCase- "invalid numbers"- (assertEqual "Error" (Left "Could not parse date.") (parseRelative time "2019-39-59"))- ]
− test/Data/Taskell/DateTest.hs
@@ -1,246 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.DateTest- ( test_date- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Data.Taskell.Date-import Data.Time-import Data.Time.Zones (utcTZ)-import Data.Time.Zones.All (TZLabel (America__New_York), tzByLabel)--testDate :: UTCTime-testDate = UTCTime (fromGregorian 2018 5 18) (secondsToDiffTime 0)---- sorting test data-sort1 :: Due-sort1 = DueTime (UTCTime (fromGregorian 2017 2 4) 0)--sort2 :: Due-sort2 = DueDate (fromGregorian 2016 12 15)--sort3 :: Due-sort3 = DueTime (UTCTime (fromGregorian 2020 8 9) 44000)--sort4 :: Due-sort4 = DueDate (fromGregorian 2019 8 30)---- tests-test_date :: TestTree-test_date =- testGroup- "Data.Taskell.Date"- [ testCase- "Sorting"- (assertEqual- "Sorted in date order"- [sort2, sort1, sort4, sort3]- (sort [sort1, sort2, sort3, sort4]))- , testGroup- "Date Output"- [ testGroup- "timeToDisplay"- [ testCase- "timeToDisplay time"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-18 00:00"- (timeToDisplay utcTZ (DueTime testDate)))- , testCase- "timeToDisplay time - non-utc"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-17 20:00"- (timeToDisplay (tzByLabel America__New_York) (DueTime testDate)))- , testCase- "timeToDisplay date"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-18"- (timeToDisplay utcTZ (DueDate (fromGregorian 2018 05 18))))- ]- , testGroup- "timeToOutput"- [ testCase- "timeToOutput time"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-18 00:00 UTC"- (timeToOutput (DueTime testDate)))- , testCase- "timeToOutput date"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-18"- (timeToOutput (DueDate (fromGregorian 2018 05 18))))- ]- , testGroup- "timeToOutputLocal"- [ testCase- "timeToOutputLocal time"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-18 00:00 UTC"- (timeToOutputLocal utcTZ (DueTime testDate)))- , testCase- "timeToOutputLocal time - non-utc"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-17 20:00 EDT"- (timeToOutputLocal (tzByLabel America__New_York) (DueTime testDate)))- , testCase- "timeToOutputLocal date"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-18"- (timeToOutputLocal utcTZ (DueDate (fromGregorian 2018 05 18))))- , testCase- "timeToOutputLocal date - non-utc"- (assertEqual- "Date in yyyy-mm-dd format"- "2018-05-18"- (timeToOutputLocal- (tzByLabel America__New_York)- (DueDate (fromGregorian 2018 05 18))))- ]- , testGroup- "timeToText"- [ testCase- "time"- (assertEqual- "Date in 18-May format"- "00:00 18-May"- (timeToText utcTZ testDate (DueTime testDate)))- , testCase- "time - non-utc"- (assertEqual- "Date in 17-May format"- "20:00 17-May"- (timeToText (tzByLabel America__New_York) testDate (DueTime testDate)))- , testCase- "same year"- (assertEqual- "Date in 18-May format"- "18-May"- (timeToText utcTZ testDate (DueDate (fromGregorian 2018 5 18))))- , testCase- "different year"- (assertEqual- "Date in 18-May 2019 format"- "18-May 2019"- (timeToText utcTZ testDate (DueDate (fromGregorian 2019 5 18))))- , testCase- "different year"- (assertEqual- "Date in 18-May 2017 format"- "18-May 2017"- (timeToText utcTZ testDate (DueDate (fromGregorian 2017 5 18))))- ]- ]- , testGroup- "Input"- [ testGroup- "textToTime"- [ testCase- "simple date"- (assertEqual- "A valid Day"- (DueDate <$> fromGregorianValid 2018 05 18)- (textToTime "2018-05-18"))- , testCase- "simple time"- (assertEqual- "A valid time"- (DueTime . flip UTCTime 64800 <$> fromGregorianValid 2018 05 18)- (textToTime "2018-05-18 18:00 UTC"))- , testCase- "time with timezone"- (assertEqual- "A valid time"- (DueTime . flip UTCTime 0 <$> fromGregorianValid 2018 05 18)- (textToTime "2018-05-17 20:00 EDT"))- ]- , testGroup- "inputToTime"- [ testCase- "simple date"- (assertEqual- "A valid Day"- (DueDate <$> fromGregorianValid 2018 05 18)- (inputToTime utcTZ testDate "2018-05-18"))- , testCase- "simple time"- (assertEqual- "A valid time"- (DueTime . flip UTCTime 64800 <$> fromGregorianValid 2018 05 18)- (inputToTime utcTZ testDate "2018-05-18 18:00"))- , testCase- "time with timezone"- (assertEqual- "A valid time"- (DueTime . flip UTCTime 79200 <$> fromGregorianValid 2018 05 18)- (inputToTime- (tzByLabel America__New_York)- testDate- "2018-05-18 18:00"))- , testCase- "relative time - seconds"- (assertEqual- "Adds 7 seconds"- (DueTime . flip UTCTime 7 <$> fromGregorianValid 2018 05 18)- (inputToTime utcTZ testDate "7s"))- , testCase- "relative time - days"- (assertEqual- "Adds 29 days, 12 hours"- (DueTime . flip UTCTime 43200 <$> fromGregorianValid 2018 06 16)- (inputToTime utcTZ testDate "29 d 12h"))- , testCase- "relative time - weeks"- (assertEqual- "Adds four weeks, two days, 12 hours"- (DueTime . flip UTCTime 43200 <$> fromGregorianValid 2018 06 17)- (inputToTime utcTZ testDate "4w 2d 12h"))- ]- , testCase- "isoToTime"- (assertEqual- "Gives back time"- (Just (DueTime (UTCTime (fromGregorian 2020 8 11) 82800)))- (isoToTime "2020-08-11T23:00:00.000Z"))- ]- , testGroup- "deadline"- [ testCase- "Plenty"- (assertEqual- "Plenty of time"- Plenty- (deadline testDate (DueDate (fromGregorian 2018 05 28))))- , testCase- "ThisWeek"- (assertEqual- "This week"- ThisWeek- (deadline testDate (DueDate (fromGregorian 2018 05 24))))- , testCase- "Tomorrow"- (assertEqual- "Tomorrow"- Tomorrow- (deadline testDate (DueDate (fromGregorian 2018 05 19))))- , testCase "Today" (assertEqual "Today" Today (deadline testDate (DueTime testDate)))- , testCase- "Passed"- (assertEqual- "Passed"- Passed- (deadline testDate (DueDate (fromGregorian 2018 05 17))))- ]- ]
@@ -1,81 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.ListNavigationTest- ( test_listNav- ) where--import ClassyPrelude as CP--import Test.Tasty-import Test.Tasty.HUnit--import Data.Taskell.List.Internal as L-import qualified Data.Taskell.Task as T (Task, new)--taskSeq :: Seq T.Task-taskSeq =- fromList- [ T.new "Hello"- , T.new "Blah"- , T.new "Fish"- , T.new "Spoons"- , T.new "Hello Again!"- , T.new "Computers"- ]--list :: List-list = List "Populated" taskSeq---- tests-test_listNav :: TestTree-test_listNav =- testGroup- "Data.Taskell.List Navigation"- [ testGroup- "next"- [ testCase "no term" (assertEqual "1" 1 (L.nextTask 0 Nothing list))- , testCase "with term" (assertEqual "4" 4 (L.nextTask 0 (Just "Hello") list))- , testCase- "with term - no match"- (assertEqual "0" 0 (L.nextTask 0 (Just "Wombat") list))- ]- , testGroup- "prev"- [ testCase "no term" (assertEqual "0" 0 (L.prevTask 1 Nothing list))- , testCase "with term" (assertEqual "0" 0 (L.prevTask 4 (Just "Hello") list))- , testCase- "with term - no match"- (assertEqual "4" 4 (L.prevTask 4 (Just "Wombat") list))- ]- , testGroup- "nearest"- [ testCase "no term" (assertEqual "same task" 2 (L.nearest 2 Nothing list))- , testCase "term matches" (assertEqual "same task" 2 (L.nearest 2 (Just "ish") list))- , testCase "term after" (assertEqual "next task" 3 (L.nearest 2 (Just "oons") list))- , testCase- "term before"- (assertEqual "previous task" 3 (L.nearest 5 (Just "oons") list))- , testCase- "term both - before closer"- (assertEqual "previous task" 0 (L.nearest 2 (Just "Hello") list))- , testCase- "term both - after closer"- (assertEqual "next task" 4 (L.nearest 3 (Just "Hello") list))- , testCase- "no matches"- (assertEqual "nothing" (-1) (L.nearest 3 (Just "Penguins") list))- , testCase- "nothing to match"- (assertEqual "nothing" 3 (L.nearest (-1) (Just "Spoon") list))- , testCase- "nothing with no term"- (assertEqual "nothing" 0 (L.nearest (-1) Nothing list))- , 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
@@ -1,191 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.ListTest- ( test_list- ) where--import ClassyPrelude as CP--import Control.Lens ((.~))--import Test.Tasty-import Test.Tasty.HUnit--import Data.Taskell.List.Internal as L-import qualified Data.Taskell.Task as T (Task, blank, name, new)--emptyList :: List-emptyList = L.empty "Test"--taskSeq :: Seq T.Task-taskSeq = fromList [T.new "Hello", T.new "Blah", T.new "Fish"]--populatedList :: List-populatedList = List "Populated" taskSeq---- tests-test_list :: TestTree-test_list =- testGroup- "Data.Taskell.List"- [ testCase- "create"- (assertEqual "Empty list with title" (List "Test" CP.empty) (create "Test" CP.empty))- , testCase "empty" (assertEqual "Empty list with title" (List "Test" CP.empty) emptyList)- , testCase- "new"- (assertEqual- "List with title and blank Task"- (List "Test" (fromList [T.blank]))- (new emptyList))- , testGroup- "count"- [ testCase "empty" (assertEqual "List length" 0 (count emptyList))- , testCase "one item" (assertEqual "List length" 1 (count $ new emptyList))- ]- , testCase- "newAt"- (assertEqual- "List with new item second position"- (List "Populated" (fromList [T.new "Hello", T.blank, T.new "Blah", T.new "Fish"]))- (newAt 1 populatedList))- , testGroup- "append"- [ testCase- "populated"- (assertEqual- "List with new item"- (List- "Populated"- (fromList [T.new "Hello", T.new "Blah", T.new "Fish", T.new "Spoon"]))- (append (T.new "Spoon") populatedList))- , testCase- "empty"- (assertEqual- "List with new item"- (List "Test" (fromList [T.new "Spoon"]))- (append (T.new "Spoon") emptyList))- ]- , testCase- "extract"- (assertEqual- "List and extracted item"- (Just (List "Populated" (fromList [T.new "Hello", T.new "Fish"]), T.new "Blah"))- (extract 1 populatedList))- , testCase- "updateFn"- (assertEqual- "List with updated item"- (List "Populated" (fromList [T.new "Hello", T.new "Monkey", T.new "Fish"]))- (updateFn 1 (T.name .~ "Monkey") populatedList))- , testCase- "update"- (assertEqual- "List with updated item"- (List "Populated" (fromList [T.new "Hello", T.new "Monkey", T.new "Fish"]))- (update 1 (T.new "Monkey") populatedList))- , testGroup- "move"- [ testCase- "up"- (assertEqual- "List with moved item"- (Just- ( 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"])- , 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"])- , 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"])- , 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"- (assertEqual- "List with removed item"- (List "Populated" (fromList [T.new "Hello", T.new "Fish"]))- (deleteTask 1 populatedList))- , testGroup- "getTask"- [ testCase- "exists"- (assertEqual "Middle item" (Just (T.new "Blah")) (getTask 1 populatedList))- , testCase "doesn't exist" (assertEqual "Nothing" Nothing (getTask 5 populatedList))- ]- , testGroup- "searchFor"- [ testCase- "exists"- (assertEqual- "Blah item"- (List "Populated" (fromList [T.new "Blah"]))- (searchFor "Bl" populatedList))- , testCase- "multiple"- (assertEqual- "Hello and Blah"- (List "Populated" (fromList [T.new "Hello", T.new "Blah"]))- (searchFor "l" populatedList))- , testCase- "case-insensitive"- (assertEqual- "Hello"- (List "Populated" (fromList [T.new "Hello"]))- (searchFor "hello" populatedList))- , testCase- "doesn't exist"- (assertEqual- "Empty list"- (List "Populated" CP.empty)- (searchFor "FFF" populatedList))- ]- ]
− test/Data/Taskell/ListsTest.hs
@@ -1,236 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.ListsTest- ( test_lists- ) where--import ClassyPrelude hiding (delete)--import Test.Tasty-import Test.Tasty.HUnit--import Control.Lens ((.~))--import Data.Taskell.Date (textToTime)-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.due .~ textToTime "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.due .~ textToTime "2018-12-03") (T.new "3")]--list3 =- foldl'- (flip L.append)- (L.empty "List 3")- [(T.due .~ textToTime "2019-04-05") (T.new "01"), T.new "10", T.new "11"]--testLists :: Lists-testLists = fromList [list1, list2, list3]---- tests-test_lists :: TestTree-test_lists =- testGroup- "Data.Taskell.Lists"- [ testCase- "initial"- (assertEqual- "Returns To Do and Done lists"- (fromList [L.empty "To Do", L.empty "Done"])- initial)- , testCase- "updateLists"- (assertEqual- "Replaces the middle list"- (fromList [list1, list1, list3])- (updateLists 1 list1 testLists))- , testGroup- "count"- [ testCase- "list exists"- (assertEqual "Returns length of middle list" 3 (count 1 testLists))- , testCase "list does not exist" (assertEqual "Returns 0" 0 (count 10 testLists))- ]- , testGroup- "get"- [ testCase- "list exists"- (assertEqual "Returns the list" (Just list2) (get testLists 1))- , testCase "list does not exist" (assertEqual "Nothing" Nothing (get testLists 10))- ]- , testGroup- "changeList"- [ testCase- "right"- (assertEqual- "Returns updated lists"- (Just- (fromList- [ list1- , foldl'- (flip L.append)- (L.empty "List 2")- [T.new "1", (T.due .~ textToTime "2018-12-03") (T.new "3")]- , foldl'- (flip L.append)- (L.empty "List 3")- [ (T.due .~ textToTime "2019-04-05") (T.new "01")- , T.new "10"- , T.new "11"- , T.new "2"- ]- ]))- (changeList (ListIndex 1, TaskIndex 1) testLists 1))- , testCase- "left"- (assertEqual- "Returns updated lists"- (Just- (fromList- [ foldl'- (flip L.append)- (L.empty "List 1")- [ T.new "One"- , (T.due .~ textToTime "2019-08-14") (T.new "Two")- , T.new "Three"- , T.new "2"- ]- , foldl'- (flip L.append)- (L.empty "List 2")- [T.new "1", (T.due .~ textToTime "2018-12-03") (T.new "3")]- , list3- ]))- (changeList (ListIndex 1, TaskIndex 1) testLists (-1)))- , testCase- "out of bounds list"- (assertEqual- "Nothing"- Nothing- (changeList (ListIndex 5, TaskIndex 1) testLists 1))- , testCase- "out of bounds task"- (assertEqual- "Nothing"- Nothing- (changeList (ListIndex 1, TaskIndex 10) testLists 1))- ]- , testCase- "newList"- (assertEqual- "Returns lists with new list"- (fromList [list1, list2, list3, L.empty "Hello"])- (newList "Hello" testLists))- , testCase- "delete"- (assertEqual- "Returns lists with middle list removed"- (fromList [list1, list3])- (delete 1 testLists))- , testGroup- "exists"- [ testCase "list exists" (assertEqual "Returns True" True (exists 1 testLists))- , testCase- "list does not exist"- (assertEqual "Returns False" False (exists 10 testLists))- ]- , testGroup- "shiftBy"- [ testCase- "right"- (assertEqual- "Returns updated lists"- (Just (fromList [list1, list3, list2]))- (shiftBy 1 1 testLists))- , testCase- "left"- (assertEqual- "Returns updated lists"- (Just (fromList [list2, list1, list3]))- (shiftBy 1 (-1) testLists))- , testCase- "out of bounds list"- (assertEqual "Nothing" Nothing (shiftBy 5 1 testLists))- , testCase- "out of bounds shift"- (assertEqual- "Returns updated lists"- (Just (fromList [list2, list1, list3]))- (shiftBy 1 (-10) testLists))- ]- , testGroup- "search"- [ testCase- "term exists"- (assertEqual- "Returns filtered lists"- (fromList- [ foldl' (flip L.append) (L.empty "List 1") [T.new "One"]- , L.empty "List 2"- , L.empty "List 3"- ])- (search "One" testLists))- , testCase- "term doesn't exist"- (assertEqual- "Returns updated lists"- (fromList [L.empty "List 1", L.empty "List 2", L.empty "List 3"])- (search "Fish" testLists))- ]- , testGroup- "appendToLast"- [ testCase- "previous list exists"- (assertEqual- "Returns updated lists"- (fromList- [ list1- , list2- , foldl'- (flip L.append)- (L.empty "List 3")- [ (T.due .~ textToTime "2019-04-05") (T.new "01")- , T.new "10"- , T.new "11"- , T.new "Blah"- ]- ])- (appendToLast (T.new "Blah") testLists))- , testCase- "previous list doesn't exist"- (assertEqual "Returns original list" empty (appendToLast (T.new "Blah") empty))- ]- , testCase- "analyse"- (assertEqual- "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.due .~ textToTime "2018-12-03") (T.new "3"))- , ( (ListIndex 2, TaskIndex 0)- , (T.due .~ textToTime "2019-04-05") (T.new "01"))- , ( (ListIndex 0, TaskIndex 1)- , (T.due .~ textToTime "2019-08-14") (T.new "Two"))- ])- (due testLists))- ]
− test/Data/Taskell/SeqTest.hs
@@ -1,95 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.SeqTest- ( test_seq- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Data.Taskell.Seq--testSeq :: Seq Int-testSeq = fromList [1, 2, 3, 4, 5]---- tests-test_seq :: TestTree-test_seq =- testGroup- "Data.Taskell.Seq"- [ testGroup- "extract"- [ testCase- "Standard"- (assertEqual- "Extracts third item"- (Just (fromList [1, 2, 4, 5], 3))- (extract 2 testSeq))- , testCase- "First"- (assertEqual- "Extracts first item"- (Just (fromList [2, 3, 4, 5], 1))- (extract 0 testSeq))- , testCase- "Last"- (assertEqual- "Extracts last item"- (Just (fromList [1, 2, 3, 4], 5))- (extract 4 testSeq))- , testCase- "Out of range - positive"- (assertEqual "Nothing" Nothing (extract 5 testSeq))- , testCase- "Out of range - negative"- (assertEqual "Nothing" Nothing (extract (-1) testSeq))- ]- , testGroup- "shiftBy"- [ testCase- "To right"- (assertEqual- "Moves third item right"- (Just (fromList [1, 2, 4, 3, 5]))- (shiftBy 2 1 testSeq))- , testCase- "To left"- (assertEqual- "Moves third item left"- (Just (fromList [1, 3, 2, 4, 5]))- (shiftBy 2 (-1) testSeq))- , testCase- "First to right"- (assertEqual- "Moves first item right"- (Just (fromList [2, 1, 3, 4, 5]))- (shiftBy 0 1 testSeq))- , testCase- "First to left"- (assertEqual "The original sequence" (Just testSeq) (shiftBy 0 (-1) testSeq))- , testCase- "Last to right"- (assertEqual "The original sequence" (Just testSeq) (shiftBy 4 1 testSeq))- , testCase- "Last to left"- (assertEqual- "Moves last item left"- (Just (fromList [1, 2, 3, 5, 4]))- (shiftBy 4 (-1) testSeq))- , testCase- "Ridiculous right shift"- (assertEqual- "Moves to right-most"- (Just (fromList [1, 2, 4, 5, 3]))- (shiftBy 2 20 testSeq))- , testCase- "Ridiculous left shift"- (assertEqual- "Moves to left-most"- (Just (fromList [3, 1, 2, 4, 5]))- (shiftBy 2 (-20) testSeq))- ]- ]
− test/Data/Taskell/SubtaskTest.hs
@@ -1,45 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.SubtaskTest- ( test_subtask- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Data.Taskell.Subtask.Internal---- tests-test_subtask :: TestTree-test_subtask =- testGroup- "Data.Taskell.Subtask"- [ testCase "blank" (assertEqual "creates an empty sub-task" (Subtask "" False) blank)- , testGroup- "new"- [ testCase- "completed"- (assertEqual "creates a new sub-task" (Subtask "Blah" True) (new "Blah" True))- , testCase- "not completed"- (assertEqual "creates a new sub-task" (Subtask "Blah" False) (new "Blah" False))- ]- , testGroup- "toggle"- [ testCase- "false to true"- (assertEqual- "Sets completed to True"- (Subtask "Blah" True)- (toggle $ new "Blah" False))- , testCase- "true to false"- (assertEqual- "Sets completed to False"- (Subtask "Blah" False)- (toggle $ new "Blah" True))- ]- ]
− test/Data/Taskell/TaskTest.hs
@@ -1,203 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Taskell.TaskTest- ( test_task- ) where--import ClassyPrelude--import Control.Lens ((.~))--import Test.Tasty-import Test.Tasty.HUnit--import Data.Time.Calendar (fromGregorianValid)--import Data.Taskell.Date (Due (DueDate))-import qualified Data.Taskell.Subtask as ST (name, new)-import Data.Taskell.Task.Internal--desc :: Maybe Text-desc = Just "A very boring description"--testTask :: Task-testTask =- Task- { _name = "Test"- , _description = desc- , _subtasks = fromList [ST.new "One" True, ST.new "Two" False, ST.new "Three" False]- , _due = Nothing- }---- tests-test_task :: TestTree-test_task =- testGroup- "Data.Taskell.Task"- [ testCase "blank" (assertEqual "Returns empty task" (Task "" Nothing empty Nothing) blank)- , testCase- "new"- (assertEqual "Returns a new task" (Task "Hello" Nothing empty Nothing) (new "Hello"))- , testGroup- "getSubtask"- [ testCase- "exists"- (assertEqual- "Returns a the sub-task"- (Just (ST.new "Two" False))- (getSubtask 1 testTask))- , testCase- "doesn't exist"- (assertEqual "Returns Nothing" Nothing (getSubtask 10 testTask))- ]- , testGroup- "setDescription"- [ testCase- "empty sub-tasks"- (assertEqual- "Updates description"- (Task "Test" (Just "Description") empty Nothing)- (setDescription "Description" (new "Test")))- , testCase- "empty"- (assertEqual- "Keeps description as Nothing"- (new "Test")- (setDescription "" (new "Test")))- , testCase- "blank"- (assertEqual- "Keeps description as Nothing"- (new "Test")- (setDescription " " (new "Test")))- ]- , testGroup- "addSubtask"- [ testCase- "existing"- (assertEqual- "Returns the task with added subtask"- (Task- "Test"- desc- (fromList- [ ST.new "One" True- , ST.new "Two" False- , ST.new "Three" False- , ST.new "Four" True- ])- Nothing)- (addSubtask (ST.new "Four" True) testTask))- , testCase- "empty sub-tasks"- (assertEqual- "Returns the task with added subtask"- (Task "Test" Nothing (fromList [ST.new "One" False]) Nothing)- (addSubtask (ST.new "One" False) (new "Test")))- ]- , testGroup- "hasSubtasks"- [ testCase "existing" (assertEqual "Returns True" True (hasSubtasks testTask))- , testCase- "empty sub-tasks"- (assertEqual "Returns False" False (hasSubtasks (new "Test")))- ]- , testGroup- "updateSubtask"- [ testCase- "exists"- (assertEqual- "Returns updated task"- (Task- "Test"- desc- (fromList- [ST.new "One" True, ST.new "Cow" False, ST.new "Three" False])- Nothing)- (updateSubtask 1 (ST.name .~ "Cow") testTask))- , testCase- "doesn't exist"- (assertEqual- "Returns task"- (Task- "Test"- desc- (fromList- [ST.new "One" True, ST.new "Two" False, ST.new "Three" False])- Nothing)- (updateSubtask 10 (ST.name .~ "Cow") testTask))- ]- , testGroup- "removeSubtask"- [ testCase- "exists"- (assertEqual- "Returns updated task"- (Task- "Test"- desc- (fromList [ST.new "One" True, ST.new "Three" False])- Nothing)- (removeSubtask 1 testTask))- , testCase- "doesn't exist"- (assertEqual- "Returns task"- (Task- "Test"- desc- (fromList- [ST.new "One" True, ST.new "Two" False, ST.new "Three" False])- Nothing)- (removeSubtask 10 testTask))- ]- , testCase "countSubtasks" (assertEqual "Returns 3" 3 (countSubtasks testTask))- , testCase- "countCompleteSubtasks"- (assertEqual "Returns 1" 1 (countCompleteSubtasks testTask))- , testGroup- "contains"- [ testCase "in task" (assertEqual "Finds in task" True (contains "Test" testTask))- , testCase "in sub-task" (assertEqual "Find sub-task" True (contains "One" testTask))- , testCase- "case-insensitive"- (assertEqual "Find sub-task" True (contains "ONE" testTask))- , testCase- "case-insensitive"- (assertEqual "Find sub-task" True (contains "two" testTask))- , testCase "missing" (assertEqual "Find sub-task" False (contains "Fish" testTask))- , testCase- "description"- (assertEqual "Finds in description" True (contains "boring" testTask))- , testCase- "not in description"- (assertEqual "Doesn't find" False (contains "escapades" testTask))- ]- , testGroup- "isBlank"- [ testCase- "blank"- (assertEqual "Return True" True (isBlank (Task "" Nothing empty Nothing)))- , testCase "name not blank" (assertEqual "Returns False" False (isBlank testTask))- , testCase- "description not blank"- (assertEqual- "Returns False"- False- (isBlank (Task "" (Just "Blah") empty Nothing)))- , testCase- "subtasks not blank"- (assertEqual- "Returns False"- False- (isBlank (Task "" Nothing (fromList [ST.new "One" True]) Nothing)))- , testCase- "due date not blank"- (assertEqual- "Returns False"- False- (isBlank- (Task "" Nothing empty (DueDate <$> (fromGregorianValid 2018 05 18)))))- ]- ]
− test/Events/State/HistoryTest.hs
@@ -1,98 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module Events.State.HistoryTest- ( test_history- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Events.State.History-import Events.State.Types--make :: [Integer] -> Integer -> [Integer] -> History Integer-make = History--testHistory :: History Integer-testHistory = make empty 0 empty---- tests-test_history :: TestTree-test_history =- testGroup- "Events.State.History"- [ testGroup- "undo"- [ testCase "empty" (assertEqual "Nothing changes" testHistory (undo testHistory))- , testCase- "one undo"- (assertEqual "Goes back" (make empty 0 [1]) (undo $ make [0] 1 empty))- , testCase- "two undo"- (assertEqual- "Goes back"- (make empty 0 [1, 2])- (undo . undo $ make [1, 0] 2 empty))- , testCase- "three undo"- (assertEqual- "Goes back"- (make empty 0 [1, 2, 3])- (undo . undo . undo $ make [2, 1, 0] 3 empty))- ]- , testGroup- "redo"- [ testCase "empty" (assertEqual "Nothing changes" testHistory (redo testHistory))- , testCase- "one redo"- (assertEqual "Goes forward" (make [0] 1 empty) (redo $ make empty 0 [1]))- , testCase- "two redo"- (assertEqual- "Goes forward"- (make [1, 0] 2 empty)- (redo . redo $ make empty 0 [1, 2]))- , testCase- "three redo"- (assertEqual- "Goes forward"- (make [2, 1, 0] 3 empty)- (redo . redo . redo $ make empty 0 [1, 2, 3]))- ]- , testGroup- "mix"- [ testCase- "empty"- (assertEqual- "Nothing changes"- testHistory- (undo . redo . undo . redo . undo $ testHistory))- , testCase- "redo undo"- (assertEqual- "Nothing changes"- (make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10])- (undo . redo $ make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10]))- , testCase- "undo redo"- (assertEqual- "Nothing changes"- (make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10])- (redo . undo $ make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10]))- ]- , testGroup- "store"- [ testCase- "empty"- (assertEqual "Stores current value" (make [0] 0 empty) (store testHistory))- , testCase- "undo store redo"- (assertEqual- "Clears redo"- (make [4, 3, 2, 1, 0] 4 empty)- (redo . store . undo $ make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10]))- ]- ]
− test/Events/StateTest.hs
@@ -1,132 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}--module Events.StateTest- ( test_state- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Control.Lens ((&), (.~), (^.))--import Data.Time.Clock (secondsToDiffTime)-import Data.Time.Zones (utcTZ)--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 (ListIndex), TaskIndex (TaskIndex))--mockTime :: UTCTime-mockTime = UTCTime (ModifiedJulianDay 20) (secondsToDiffTime 0)--testState :: State-testState =- State- { _mode = Normal- , _history = fresh empty- , _path = "test.md"- , _io = Nothing- , _height = 0- , _searchTerm = Nothing- , _time = mockTime- , _timeZone = utcTZ- }--moveToState :: State-moveToState =- State- { _mode = Modal MoveTo- , _history =- History- { _past = empty- , _present =- ( (ListIndex 4, TaskIndex 0)- , [ L.empty "List 1"- , L.empty "List 2"- , L.empty "List 3"- , L.empty "List 4"- , L.append (T.new "Test Item") (L.empty "List 5")- , L.empty "List 6"- , L.empty "List 7"- , L.empty "List 8"- , L.empty "List 9"- ])- , _future = empty- }- , _path = "test.md"- , _io = Nothing- , _height = 0- , _searchTerm = Nothing- , _time = mockTime- , _timeZone = utcTZ- }---- tests-test_state :: TestTree-test_state =- testGroup- "Events.State"- [ testCase- "quit"- (assertEqual- "Retuns Just with mode set to Shutdown"- (Just (testState & mode .~ Shutdown))- (quit testState))- , testCase- "continue"- (assertEqual "Retuns with io set to " (testState & io .~ Nothing) (continue testState))- -- 1 - a, 2 - b, 3 - c, 4 - d, 5 - e, 6 - f, 7 - g, 8 - h, 9 - i- , testGroup- "moveTo"- [ testCase- "first"- (assertEqual- "Moves to first list"- (Just (L.append (T.new "Test Item") (L.empty "List 1")))- (S.lookup 0 . (^. lists) =<< moveTo 'a' moveToState))- , testCase- "second"- (assertEqual- "Moves to second list"- (Just (L.append (T.new "Test Item") (L.empty "List 2")))- (S.lookup 1 . (^. lists) =<< moveTo 'b' moveToState))- , testCase- "fourth"- (assertEqual- "Moves to fourth list"- (Just (L.append (T.new "Test Item") (L.empty "List 4")))- (S.lookup 3 . (^. lists) =<< moveTo 'd' moveToState))- , testCase- "sixth"- (assertEqual- "Moves to sixth list"- (Just (L.append (T.new "Test Item") (L.empty "List 6")))- (S.lookup 5 . (^. lists) =<< moveTo 'f' moveToState))- , testCase- "penultimate"- (assertEqual- "Moves to penultime list"- (Just (L.append (T.new "Test Item") (L.empty "List 8")))- (S.lookup 7 . (^. lists) =<< moveTo 'h' moveToState))- , testCase- "last"- (assertEqual- "Moves to last list"- (Just (L.append (T.new "Test Item") (L.empty "List 9")))- (S.lookup 8 . (^. lists) =<< moveTo 'i' moveToState))- , testCase- "current list"- (assertEqual "Doesn't do anything" Nothing (moveTo 'e' moveToState))- , testCase- "invalid option"- (assertEqual "Doesn't do anything" Nothing (moveTo 'q' moveToState))- ]- ]
− test/IO/GitHub/CardsTest.hs
@@ -1,39 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.GitHub.CardsTest- ( test_cards- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Data.Aeson--import IO.HTTP.GitHub.Card (MaybeCard (MaybeCard))--decodeCards :: Text -> Either String [MaybeCard]-decodeCards txt = eitherDecodeStrict $ encodeUtf8 txt---- tests-test_cards :: TestTree-test_cards =- testGroup- "IO.HTTP.GitHub.Card"- [ testCase- "parses basic card"- (assertEqual- "Gives back card"- (Right [MaybeCard (Just "blah") Nothing])- (decodeCards "[{\"note\": \"blah\"}]"))- , testCase- "parses basic card"- (assertEqual- "Gives back card"- (Right- [MaybeCard Nothing (Just "https://api.github.com/projects/columns/7850783")])- (decodeCards- "[{\"note\": null, \"content_url\": \"https://api.github.com/projects/columns/7850783\"}]"))- ]
− test/IO/GitHubTest.hs
@@ -1,48 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.GitHubTest- ( test_github- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import IO.HTTP.GitHub (getNextLink)---- tests-test_github :: TestTree-test_github =- testGroup- "IO.HTTP.GitHub"- [ testGroup- "getNextLink"- [ testCase- "next exists"- (assertEqual- "Parses next link"- (Just "https://api.github.com/projects/columns/3152155/cards?page=2")- (getNextLink- [ "<https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"next\", <https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"last\""- ]))- , testCase "empty" (assertEqual "Returns Nothing" Nothing (getNextLink []))- , testCase- "no next"- (assertEqual- "Returns Nothing"- Nothing- (getNextLink- [ "<https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"prev\", <https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"last\""- ]))- , testCase- "next last"- (assertEqual- "Parses next link"- (Just "https://api.github.com/projects/columns/3152155/cards?page=2")- (getNextLink- [ "<https://api.github.com/projects/columns/3152155/cards?page=1>; rel=\"prev\", <https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"next\", <https://api.github.com/projects/columns/3152155/cards?page=3>; rel=\"last\""- ]))- ]- ]
− test/IO/Keyboard/ParserTest.hs
@@ -1,95 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE QuasiQuotes #-}--module IO.Keyboard.ParserTest- ( test_parser- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Data.FileEmbed (embedFile)-import Text.RawString.QQ (r)--import qualified Events.Actions.Types as A-import IO.Keyboard.Parser (bindings)-import IO.Keyboard.Types--basic :: Text-basic = "quit = q"--basicResult :: Bindings-basicResult = [(BChar 'q', A.Quit)]--basicMulti :: Text-basicMulti =- [r|- quit = q- detail = <Enter>- |]--basicMultiResult :: Bindings-basicMultiResult = [(BChar 'q', A.Quit), (BKey "Enter", A.Detail)]--ini :: Text-ini = decodeUtf8 $(embedFile "test/IO/Keyboard/data/bindings.ini")--iniResult :: Bindings-iniResult =- [ (BChar 'q', A.Quit)- , (BChar 'u', A.Undo)- , (BChar 'r', A.Redo)- , (BChar '/', A.Search)- , (BChar '?', A.Help)- , (BChar '!', A.Due)- , (BChar 'k', A.Previous)- , (BChar 'j', A.Next)- , (BChar 'h', A.Left)- , (BChar 'l', A.Right)- , (BChar 'G', A.Bottom)- , (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)- , (BChar 'C', A.Clear)- , (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.Complete)- , (BChar 'm', A.MoveMenu)- , (BChar 'N', A.ListNew)- , (BChar 'E', A.ListEdit)- , (BChar 'X', A.ListDelete)- , (BChar '>', A.ListRight)- , (BChar '<', A.ListLeft)- ]--comma :: Text-comma = "quit = ,"--commaResult :: Bindings-commaResult = [(BChar ',', A.Quit)]--test_parser :: TestTree-test_parser =- testGroup- "IO.Keyboard.Parser"- [ testCase "basic" (assertEqual "Parses quit" (Right basicResult) (bindings basic))- , testCase- "basic multiline"- (assertEqual "Parses both" (Right basicMultiResult) (bindings basicMulti))- , testCase "full ini file" (assertEqual "Parses all" (Right iniResult) (bindings ini))- , testCase "comma" (assertEqual "Parses comma" (Right commaResult) (bindings comma))- ]
− test/IO/Keyboard/TypesTest.hs
@@ -1,76 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.Keyboard.TypesTest- ( test_types- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import qualified Events.Actions.Types as A (ActionType (..))-import IO.Keyboard (addMissing, badMapping, defaultBindings)-import IO.Keyboard.Types (Binding (..), Bindings)--notFull :: Bindings-notFull = [(BChar 'œ', A.Quit), (BChar 'U', A.Undo)]--notFullResult :: Bindings-notFullResult =- [ (BChar 'œ', A.Quit)- , (BChar 'U', A.Undo)- , (BChar 'r', A.Redo)- , (BChar '/', A.Search)- , (BChar '!', A.Due)- , (BChar '?', A.Help)- , (BChar 'k', A.Previous)- , (BChar 'j', A.Next)- , (BChar 'h', A.Left)- , (BChar 'l', A.Right)- , (BChar 'G', A.Bottom)- , (BChar 'g', A.Top)- , (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)- , (BChar 'C', A.Clear)- , (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.Complete)- , (BChar 'm', A.MoveMenu)- , (BChar 'N', A.ListNew)- , (BChar 'E', A.ListEdit)- , (BChar 'X', A.ListDelete)- , (BChar '>', A.ListRight)- , (BChar '<', A.ListLeft)- ]--bad :: Bindings-bad = [(BChar 'q', A.Quit), (BChar 'u', A.Nothing), (BChar '>', A.Nothing), (BChar '<', A.ListLeft)]---- tests-test_types :: TestTree-test_types =- testGroup- "Events.Actions.Types"- [ testCase- "not missing"- (assertEqual "Finds no missing items" defaultBindings (addMissing defaultBindings))- , testCase- "not missing"- (assertEqual "Finds missing items" notFullResult (addMissing notFull))- , testCase- "bad mapping"- (assertEqual "Finds bad mapping" (Left "invalid mapping") (badMapping bad))- ]
− test/IO/KeyboardTest.hs
@@ -1,56 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}--module IO.KeyboardTest- ( test_keyboard- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Control.Lens ((.~))--import Data.Time.Clock (secondsToDiffTime)-import Data.Time.Zones (utcTZ)--import Data.Taskell.Lists.Internal (initial)-import Events.Actions.Types as A-import Events.State (create, quit)-import Events.State.Types (State, Stateful, mode)-import Events.State.Types.Mode (Mode (Shutdown))-import Graphics.Vty.Input.Events (Event (..), Key (..))-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 utcTZ mockTime "taskell.md" initial--basicBindings :: Bindings-basicBindings = [(BChar 'q', A.Quit)]--basicActions :: Actions-basicActions = [(A.Quit, quit)]--basicResult :: Maybe State-basicResult = Just $ (mode .~ Shutdown) cleanState--test_keyboard :: TestTree-test_keyboard =- testGroup- "IO.Keyboard"- [ testCase- "generate"- (assertEqual- "Parses basic"- basicResult- (tester (generate basicBindings basicActions) (EvKey (KChar 'q') []) cleanState))- ]
− test/IO/Markdown/ParserTest.hs
@@ -1,214 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.Markdown.ParserTest- ( test_parser- ) where--import ClassyPrelude--import Test.Tasty--import Test.Tasty.HUnit--import Control.Lens ((&), (.~))-import Data.FileEmbed (embedFile)--import Data.Taskell.Date (textToTime)-import Data.Taskell.List (create)-import Data.Taskell.Lists (Lists, analyse, appendToLast, newList)--import qualified Data.Taskell.Subtask as ST (new)-import Data.Taskell.Task (Task, addSubtask, due, new, setDescription)-import IO.Config.Markdown (Config (Config), defaultConfig, descriptionOutput, dueOutput,- localTimes, subtaskOutput, taskOutput, titleOutput)-import IO.Markdown.Parser (parse)---- error message-err :: Either Text Lists-err = Left "Could not parse file."---- complete taskell file-file :: Text-file = decodeUtf8 $(embedFile "test/IO/data/roadmap.md")---- alternative markdown configs-alternativeConfig :: Config-alternativeConfig =- Config- { titleOutput = "##"- , taskOutput = "###"- , descriptionOutput = ">"- , dueOutput = "@"- , subtaskOutput = "-"- , localTimes = True- }---- useful records-task :: Task-task = new "Test Item"--list :: Lists-list = newList "Test" empty--listWithItem :: Lists-listWithItem = appendToLast task list--multiList :: Lists-multiList =- fromList- [create "Test" (fromList [new "Test Item"]), create "Test 2" (fromList [new "Test Item 2"])]--makeSubTask :: Text -> Bool -> Lists-makeSubTask t b = appendToLast (addSubtask (ST.new t b) task) list--taskWithSummary :: Task-taskWithSummary = setDescription "Summary" task--taskWithMultiLineSummary :: Task-taskWithMultiLineSummary = setDescription "Summary Line 1\nSummary Line 2" task--listWithSummaryItem :: Lists-listWithSummaryItem = appendToLast taskWithSummary list--listWithMultiLineSummaryItem :: Lists-listWithMultiLineSummaryItem = appendToLast taskWithMultiLineSummary list--taskWithDueDate :: Task-taskWithDueDate = task & due .~ textToTime "2018-04-12"--listWithDueDateItem :: Lists-listWithDueDateItem = appendToLast taskWithDueDate list---- tests-test_parser :: TestTree-test_parser =- testGroup- "IO.Markdown"- [ testGroup- "Default Format"- [ testCase- "List Title"- (assertEqual "One list" (Right list) (parse defaultConfig "## Test"))- , testCase- "List item"- (assertEqual- "List item"- (Right listWithItem)- (parse defaultConfig "## Test\n\n- Test Item"))- , testCase- "Multiple Lists"- (assertEqual- "List item"- (Right multiList)- (parse defaultConfig "## Test\n\n- Test Item\n\n## Test 2\n\n- Test Item 2"))- , testCase- "Summary"- (assertEqual- "Summary"- (Right listWithSummaryItem)- (parse defaultConfig "## Test\n\n- Test Item\n > Summary"))- , testCase- "List Item with multi-line Summary"- (assertEqual- "List item with a summary"- (Right listWithMultiLineSummaryItem)- (parse- defaultConfig- "## Test\n\n- Test Item\n > Summary Line 1\n > Summary Line 2"))- , testCase- "Due Date"- (assertEqual- "Due Date"- (Right listWithDueDateItem)- (parse defaultConfig "## Test\n\n- Test Item\n @ 2018-04-12"))- , testCase- "Sub-Task"- (assertEqual- "List item with Sub-Task"- (Right (makeSubTask "Blah" False))- (parse defaultConfig "## Test\n\n- Test Item\n * [ ] Blah"))- , testCase- "Complete Sub-Task"- (assertEqual- "List item with Sub-Task"- (Right (makeSubTask "Blah" True))- (parse defaultConfig "## Test\n\n- Test Item\n * [x] Blah"))- ]- , testGroup- "Alternative Format"- [ testCase- "List Title"- (assertEqual "One list" (Right list) (parse alternativeConfig "## Test"))- , testCase- "List item"- (assertEqual- "List item"- (Right listWithItem)- (parse alternativeConfig "## Test\n\n### Test Item"))- , testCase- "Multiple Lists"- (assertEqual- "List item"- (Right multiList)- (parse- alternativeConfig- "## Test\n\n### Test Item\n\n## Test 2\n\n### Test Item 2"))- , testCase- "Summary"- (assertEqual- "Summary"- (Right listWithSummaryItem)- (parse alternativeConfig "## Test\n\n### Test Item\n> Summary"))- , testCase- "List Item with multi-line Summary"- (assertEqual- "List item with a summary"- (Right listWithMultiLineSummaryItem)- (parse- alternativeConfig- "## Test\n\n### Test Item\n> Summary Line 1\n> Summary Line 2"))- , testCase- "Due Date"- (assertEqual- "Due Date"- (Right listWithDueDateItem)- (parse alternativeConfig "## Test\n\n### Test Item\n@ 2018-04-12"))- , testCase- "Sub-Task"- (assertEqual- "List item with Sub-Task"- (Right (makeSubTask "Blah" False))- (parse alternativeConfig "## Test\n\n### Test Item\n- [ ] Blah"))- , testCase- "Complete Sub-Task"- (assertEqual- "List item with Sub-Task"- (Right (makeSubTask "Blah" True))- (parse alternativeConfig "## Test\n\n### Test Item\n- [x] Blah"))- ]- , testGroup- "Errors"- [ testCase "Blank line" (assertEqual "Parse Error" err (parse defaultConfig ""))- , testCase- "Just spaces"- (assertEqual "Parse Error" err (parse defaultConfig " "))- , testCase- "Just whitespace"- (assertEqual "Parse Error" err (parse defaultConfig " \n \n \t "))- , testCase "Error" (assertEqual "Parse Error" err (parse defaultConfig "Test"))- , testCase- "List item without list"- (assertEqual "Parse Error" err (parse defaultConfig "- Test Item"))- , testCase- "Sub task without list item"- (assertEqual "Parse Error" err (parse defaultConfig "## Test\n * Blah"))- ]- , testCase- "File"- (assertEqual- "Parses whole file"- (Right "test\nLists: 6\nTasks: 202")- (analyse "test" <$> parse defaultConfig file))- ]
− test/IO/Markdown/SerializerTest.hs
@@ -1,194 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module IO.Markdown.SerializerTest- ( test_serializer- ) where--import ClassyPrelude--import Test.Tasty-import Test.Tasty.HUnit--import Control.Lens ((&), (.~))-import Control.Monad.Reader (runReader)-import Data.FileEmbed (embedFile)--import Data.Time.Zones (utcTZ)-import Data.Time.Zones.All (TZLabel (America__New_York), tzByLabel)--import Data.Taskell.Date (textToTime)-import qualified Data.Taskell.List as L (append, empty)-import Data.Taskell.Lists (Lists, appendToLast, newList)-import qualified Data.Taskell.Subtask as ST (new)-import Data.Taskell.Task (Task, addSubtask, due, new, setDescription)-import IO.Config.Markdown (Config (..), defaultConfig)-import IO.Markdown.Parser (parse)-import IO.Markdown.Serializer (MarkdownInfo (MarkdownInfo), serialize)---- complete taskell file-file :: Text-file = decodeUtf8 $(embedFile "test/IO/data/roadmap.md")---- alternative markdown configs-alternativeConfig :: Config-alternativeConfig =- Config- { titleOutput = "##"- , taskOutput = "###"- , descriptionOutput = ">"- , dueOutput = "@"- , subtaskOutput = "-"- , localTimes = True- }---- useful records-task :: Task-task = new "Test Item"--list :: Lists-list = newList "Test" empty--lists :: Lists-lists = fromList [L.append task (L.empty "To Do"), L.append (new "Fish") (L.empty "Done")]--listWithItem :: Lists-listWithItem = appendToLast task list--makeSubTask :: Text -> Bool -> Lists-makeSubTask t b = appendToLast (addSubtask (ST.new t b) task) list--makeSubTasks :: [Text] -> Bool -> Lists-makeSubTasks ts b = appendToLast (foldr addSubtask task subtasks) list- where- subtasks = flip ST.new b <$> ts--taskWithSummary :: Task-taskWithSummary = setDescription "Summary" task--taskWithMultiLineSummary :: Task-taskWithMultiLineSummary = setDescription "Summary Line 1\nSummary Line 2" task--listWithSummaryItem :: Lists-listWithSummaryItem = appendToLast taskWithSummary list--listWithMultiLineSummaryItem :: Lists-listWithMultiLineSummaryItem = appendToLast taskWithMultiLineSummary list--taskWithDueDate :: Task-taskWithDueDate = task & due .~ textToTime "2018-04-12"--listWithDueDateItem :: Lists-listWithDueDateItem = appendToLast taskWithDueDate list--taskWithDueTime :: Task-taskWithDueTime = task & due .~ textToTime "2018-04-12 12:00 UTC"--listWithDueTimeItem :: Lists-listWithDueTimeItem = appendToLast taskWithDueTime list---- reader-defaultReader :: MarkdownInfo-defaultReader = MarkdownInfo utcTZ defaultConfig--alternativeReader :: MarkdownInfo-alternativeReader = MarkdownInfo utcTZ alternativeConfig---- simplify tests-serialize' :: MarkdownInfo -> Lists -> Text-serialize' md ls = runReader (serialize ls) md---- tests-test_serializer :: TestTree-test_serializer =- testGroup- "IO.Markdown"- [ testGroup- "Serialisation"- [ testGroup- "Default Format"- [ testCase- "Standard lists"- (assertEqual- "Markdown formatted output"- "## To Do\n\n- Test Item\n\n## Done\n\n- Fish\n"- (serialize' defaultReader lists))- , testCase- "Standard list"- (assertEqual- "Markdown formatted output"- "## Test\n\n- Test Item\n"- (serialize' defaultReader listWithItem))- , testCase- "Standard list with summary"- (assertEqual- "Markdown formatted output"- "## Test\n\n- Test Item\n > Summary\n"- (serialize' defaultReader listWithSummaryItem))- , testCase- "Standard list with date"- (assertEqual- "Markdown formatted output"- "## Test\n\n- Test Item\n @ 2018-04-12\n"- (serialize' defaultReader listWithDueDateItem))- , testCase- "Standard list with date - timezone"- (assertEqual- "Use UTC timezone"- "## Test\n\n- Test Item\n @ 2018-04-12 12:00 UTC\n"- (serialize'- (MarkdownInfo (tzByLabel America__New_York) defaultConfig)- listWithDueTimeItem))- , testCase- "Standard list with sub-task"- (assertEqual- "Markdown formatted output"- "## Test\n\n- Test Item\n * [x] Blah\n"- (serialize' defaultReader (makeSubTask "Blah" True)))- , testCase- "Standard list with sub-tasks"- (assertEqual- "Markdown formatted output"- "## Test\n\n- Test Item\n * [x] Blah\n * [x] Cow\n * [x] Spoon\n"- (serialize'- defaultReader- (makeSubTasks ["Spoon", "Cow", "Blah"] True)))- , testCase- "Standard list with multi-line summary"- (assertEqual- "Markdown formatted output"- "## Test\n\n- Test Item\n > Summary Line 1\n > Summary Line 2\n"- (serialize' defaultReader listWithMultiLineSummaryItem))- ]- , testGroup- "Alternative Format"- [ testCase- "Standard list"- (assertEqual- "Markdown formatted output"- "## Test\n\n### Test Item\n"- (serialize' alternativeReader listWithItem))- , testCase- "Standard list with sub-task"- (assertEqual- "Markdown formatted output"- "## Test\n\n### Test Item\n- [ ] Blah\n"- (serialize' alternativeReader (makeSubTask "Blah" False)))- , testCase- "Standard list with date - timezone"- (assertEqual- "Uses local timezone"- "## Test\n\n### Test Item\n@ 2018-04-12 08:00 EDT\n"- (serialize'- (MarkdownInfo (tzByLabel America__New_York) alternativeConfig)- listWithDueTimeItem))- ]- ]- , testCase- "Parse then serialize"- (assertEqual- "Returns same text"- (Right file)- (serialize' defaultReader <$> parse defaultConfig file))- ]
− test/IO/TrelloTest.hs
@@ -1,42 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}--module IO.TrelloTest- ( test_trello- ) where--import ClassyPrelude--import Control.Lens ((^.))--import Test.Tasty-import Test.Tasty.HUnit--import Data.Aeson (decodeStrict)-import Data.FileEmbed (embedFile)--import IO.HTTP.Trello.ChecklistItem (ChecklistItem, checkItems, checklistItemToSubTask)-import IO.HTTP.Trello.List (List, listToList)--json :: Maybe [List]-json = decodeStrict $(embedFile "test/IO/data/trello.json")--checklistJson :: Maybe [ChecklistItem]-checklistJson = (^. checkItems) <$> decodeStrict $(embedFile "test/IO/data/trello-checklists.json")---- tests-test_trello :: TestTree-test_trello =- testGroup- "IO.Trello"- [ testCase- "Lists"- (assertEqual "Parses list JSON" (Just 5) (length . (listToList <$>) <$> json))- , testCase- "Checklists"- (assertEqual- "Parses checklist JSON"- (Just 5)- (length . (checklistItemToSubTask <$>) <$> checklistJson))- ]
+ test/Taskell/Data/Date/RelativeParserTest.hs view
@@ -0,0 +1,75 @@+module Taskell.Data.Date.RelativeParserTest+ ( test_relative_parser+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Data.Time.Clock (secondsToDiffTime)++import Taskell.Data.Date (Due (DueDate, DueTime))+import Taskell.Data.Date.RelativeParser (parseRelative)++toTime :: (Integer, Int, Int) -> Integer -> Due+toTime (y, m, d) seconds = DueTime $ UTCTime (fromGregorian y m d) (secondsToDiffTime seconds)++toDay :: (Integer, Int, Int) -> Due+toDay (y, m, d) = DueDate $ fromGregorian y m d++-- 08:53:03 18th December 2019+time :: UTCTime+time = UTCTime (fromGregorian 2019 12 18) 31983++-- tests+test_relative_parser :: TestTree+test_relative_parser =+ testGroup+ "Data.Taskell.Date.RelativeParser"+ [ testCase+ "Second"+ (assertEqual+ "Adds a second"+ (Right (toTime (2019, 12, 18) 31984))+ (parseRelative time "1s"))+ , testCase+ "Minute"+ (assertEqual+ "Adds a minute"+ (Right (toTime (2019, 12, 18) 32043))+ (parseRelative time "1m"))+ , testCase+ "Hour"+ (assertEqual+ "Adds an hour"+ (Right (toTime (2019, 12, 18) 35583))+ (parseRelative time "1h"))+ , testCase+ "Day"+ (assertEqual "Adds a day" (Right (toDay (2019, 12, 19))) (parseRelative time "1d"))+ , testCase+ "Days"+ (assertEqual "Adds 29 days" (Right (toDay (2020, 1, 16))) (parseRelative time "29 d"))+ , testCase+ "Week"+ (assertEqual "Adds a week" (Right (toDay (2019, 12, 25))) (parseRelative time "1w "))+ , testCase+ "Mix"+ (assertEqual+ "Adds 1 week 2 days and 29 seconds"+ (Right (toTime (2019, 12, 27) 32012))+ (parseRelative time " 1 w 2d 29 s "))+ , testCase+ "Mix out of order"+ (assertEqual+ "Adds 1 week 2 days and 29 seconds"+ (Right (toTime (2019, 12, 27) 32012))+ (parseRelative time " 2d 1 w 29 s"))+ , testCase+ "invalid format"+ (assertEqual "Error" (Left "Could not parse date.") (parseRelative time "18/12/2019"))+ , testCase+ "invalid numbers"+ (assertEqual "Error" (Left "Could not parse date.") (parseRelative time "2019-39-59"))+ ]
+ test/Taskell/Data/DateTest.hs view
@@ -0,0 +1,243 @@+module Taskell.Data.DateTest+ ( test_date+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Data.Time+import Data.Time.Zones (utcTZ)+import Data.Time.Zones.All (TZLabel (America__New_York), tzByLabel)+import Taskell.Data.Date++testDate :: UTCTime+testDate = UTCTime (fromGregorian 2018 5 18) (secondsToDiffTime 0)++-- sorting test data+sort1 :: Due+sort1 = DueTime (UTCTime (fromGregorian 2017 2 4) 0)++sort2 :: Due+sort2 = DueDate (fromGregorian 2016 12 15)++sort3 :: Due+sort3 = DueTime (UTCTime (fromGregorian 2020 8 9) 44000)++sort4 :: Due+sort4 = DueDate (fromGregorian 2019 8 30)++-- tests+test_date :: TestTree+test_date =+ testGroup+ "Data.Taskell.Date"+ [ testCase+ "Sorting"+ (assertEqual+ "Sorted in date order"+ [sort2, sort1, sort4, sort3]+ (sort [sort1, sort2, sort3, sort4]))+ , testGroup+ "Date Output"+ [ testGroup+ "timeToDisplay"+ [ testCase+ "timeToDisplay time"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-18 00:00"+ (timeToDisplay utcTZ (DueTime testDate)))+ , testCase+ "timeToDisplay time - non-utc"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-17 20:00"+ (timeToDisplay (tzByLabel America__New_York) (DueTime testDate)))+ , testCase+ "timeToDisplay date"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-18"+ (timeToDisplay utcTZ (DueDate (fromGregorian 2018 05 18))))+ ]+ , testGroup+ "timeToOutput"+ [ testCase+ "timeToOutput time"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-18 00:00 UTC"+ (timeToOutput (DueTime testDate)))+ , testCase+ "timeToOutput date"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-18"+ (timeToOutput (DueDate (fromGregorian 2018 05 18))))+ ]+ , testGroup+ "timeToOutputLocal"+ [ testCase+ "timeToOutputLocal time"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-18 00:00 UTC"+ (timeToOutputLocal utcTZ (DueTime testDate)))+ , testCase+ "timeToOutputLocal time - non-utc"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-17 20:00 EDT"+ (timeToOutputLocal (tzByLabel America__New_York) (DueTime testDate)))+ , testCase+ "timeToOutputLocal date"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-18"+ (timeToOutputLocal utcTZ (DueDate (fromGregorian 2018 05 18))))+ , testCase+ "timeToOutputLocal date - non-utc"+ (assertEqual+ "Date in yyyy-mm-dd format"+ "2018-05-18"+ (timeToOutputLocal+ (tzByLabel America__New_York)+ (DueDate (fromGregorian 2018 05 18))))+ ]+ , testGroup+ "timeToText"+ [ testCase+ "time"+ (assertEqual+ "Date in 18-May format"+ "00:00 18-May"+ (timeToText utcTZ testDate (DueTime testDate)))+ , testCase+ "time - non-utc"+ (assertEqual+ "Date in 17-May format"+ "20:00 17-May"+ (timeToText (tzByLabel America__New_York) testDate (DueTime testDate)))+ , testCase+ "same year"+ (assertEqual+ "Date in 18-May format"+ "18-May"+ (timeToText utcTZ testDate (DueDate (fromGregorian 2018 5 18))))+ , testCase+ "different year"+ (assertEqual+ "Date in 18-May 2019 format"+ "18-May 2019"+ (timeToText utcTZ testDate (DueDate (fromGregorian 2019 5 18))))+ , testCase+ "different year"+ (assertEqual+ "Date in 18-May 2017 format"+ "18-May 2017"+ (timeToText utcTZ testDate (DueDate (fromGregorian 2017 5 18))))+ ]+ ]+ , testGroup+ "Input"+ [ testGroup+ "textToTime"+ [ testCase+ "simple date"+ (assertEqual+ "A valid Day"+ (DueDate <$> fromGregorianValid 2018 05 18)+ (textToTime "2018-05-18"))+ , testCase+ "simple time"+ (assertEqual+ "A valid time"+ (DueTime . flip UTCTime 64800 <$> fromGregorianValid 2018 05 18)+ (textToTime "2018-05-18 18:00 UTC"))+ , testCase+ "time with timezone"+ (assertEqual+ "A valid time"+ (DueTime . flip UTCTime 0 <$> fromGregorianValid 2018 05 18)+ (textToTime "2018-05-17 20:00 EDT"))+ ]+ , testGroup+ "inputToTime"+ [ testCase+ "simple date"+ (assertEqual+ "A valid Day"+ (DueDate <$> fromGregorianValid 2018 05 18)+ (inputToTime utcTZ testDate "2018-05-18"))+ , testCase+ "simple time"+ (assertEqual+ "A valid time"+ (DueTime . flip UTCTime 64800 <$> fromGregorianValid 2018 05 18)+ (inputToTime utcTZ testDate "2018-05-18 18:00"))+ , testCase+ "time with timezone"+ (assertEqual+ "A valid time"+ (DueTime . flip UTCTime 79200 <$> fromGregorianValid 2018 05 18)+ (inputToTime+ (tzByLabel America__New_York)+ testDate+ "2018-05-18 18:00"))+ , testCase+ "relative time - seconds"+ (assertEqual+ "Adds 7 seconds"+ (DueTime . flip UTCTime 7 <$> fromGregorianValid 2018 05 18)+ (inputToTime utcTZ testDate "7s"))+ , testCase+ "relative time - days"+ (assertEqual+ "Adds 29 days, 12 hours"+ (DueTime . flip UTCTime 43200 <$> fromGregorianValid 2018 06 16)+ (inputToTime utcTZ testDate "29 d 12h"))+ , testCase+ "relative time - weeks"+ (assertEqual+ "Adds four weeks, two days, 12 hours"+ (DueTime . flip UTCTime 43200 <$> fromGregorianValid 2018 06 17)+ (inputToTime utcTZ testDate "4w 2d 12h"))+ ]+ , testCase+ "isoToTime"+ (assertEqual+ "Gives back time"+ (Just (DueTime (UTCTime (fromGregorian 2020 8 11) 82800)))+ (isoToTime "2020-08-11T23:00:00.000Z"))+ ]+ , testGroup+ "deadline"+ [ testCase+ "Plenty"+ (assertEqual+ "Plenty of time"+ Plenty+ (deadline testDate (DueDate (fromGregorian 2018 05 28))))+ , testCase+ "ThisWeek"+ (assertEqual+ "This week"+ ThisWeek+ (deadline testDate (DueDate (fromGregorian 2018 05 24))))+ , testCase+ "Tomorrow"+ (assertEqual+ "Tomorrow"+ Tomorrow+ (deadline testDate (DueDate (fromGregorian 2018 05 19))))+ , testCase "Today" (assertEqual "Today" Today (deadline testDate (DueTime testDate)))+ , testCase+ "Passed"+ (assertEqual+ "Passed"+ Passed+ (deadline testDate (DueDate (fromGregorian 2018 05 17))))+ ]+ ]
@@ -0,0 +1,78 @@+module Taskell.Data.ListNavigationTest+ ( test_listNav+ ) where++import ClassyPrelude as CP++import Test.Tasty+import Test.Tasty.HUnit++import Taskell.Data.List.Internal as L+import qualified Taskell.Data.Task as T (Task, new)++taskSeq :: Seq T.Task+taskSeq =+ fromList+ [ T.new "Hello"+ , T.new "Blah"+ , T.new "Fish"+ , T.new "Spoons"+ , T.new "Hello Again!"+ , T.new "Computers"+ ]++list :: List+list = List "Populated" taskSeq++-- tests+test_listNav :: TestTree+test_listNav =+ testGroup+ "Data.Taskell.List Navigation"+ [ testGroup+ "next"+ [ testCase "no term" (assertEqual "1" 1 (L.nextTask 0 Nothing list))+ , testCase "with term" (assertEqual "4" 4 (L.nextTask 0 (Just "Hello") list))+ , testCase+ "with term - no match"+ (assertEqual "0" 0 (L.nextTask 0 (Just "Wombat") list))+ ]+ , testGroup+ "prev"+ [ testCase "no term" (assertEqual "0" 0 (L.prevTask 1 Nothing list))+ , testCase "with term" (assertEqual "0" 0 (L.prevTask 4 (Just "Hello") list))+ , testCase+ "with term - no match"+ (assertEqual "4" 4 (L.prevTask 4 (Just "Wombat") list))+ ]+ , testGroup+ "nearest"+ [ testCase "no term" (assertEqual "same task" 2 (L.nearest 2 Nothing list))+ , testCase "term matches" (assertEqual "same task" 2 (L.nearest 2 (Just "ish") list))+ , testCase "term after" (assertEqual "next task" 3 (L.nearest 2 (Just "oons") list))+ , testCase+ "term before"+ (assertEqual "previous task" 3 (L.nearest 5 (Just "oons") list))+ , testCase+ "term both - before closer"+ (assertEqual "previous task" 0 (L.nearest 2 (Just "Hello") list))+ , testCase+ "term both - after closer"+ (assertEqual "next task" 4 (L.nearest 3 (Just "Hello") list))+ , testCase+ "no matches"+ (assertEqual "nothing" (-1) (L.nearest 3 (Just "Penguins") list))+ , testCase+ "nothing to match"+ (assertEqual "nothing" 3 (L.nearest (-1) (Just "Spoon") list))+ , testCase+ "nothing with no term"+ (assertEqual "nothing" 0 (L.nearest (-1) Nothing list))+ , 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/Taskell/Data/ListTest.hs view
@@ -0,0 +1,188 @@+module Taskell.Data.ListTest+ ( test_list+ ) where++import ClassyPrelude as CP++import Control.Lens ((.~))++import Test.Tasty+import Test.Tasty.HUnit++import Taskell.Data.List.Internal as L+import qualified Taskell.Data.Task as T (Task, blank, name, new)++emptyList :: List+emptyList = L.empty "Test"++taskSeq :: Seq T.Task+taskSeq = fromList [T.new "Hello", T.new "Blah", T.new "Fish"]++populatedList :: List+populatedList = List "Populated" taskSeq++-- tests+test_list :: TestTree+test_list =+ testGroup+ "Data.Taskell.List"+ [ testCase+ "create"+ (assertEqual "Empty list with title" (List "Test" CP.empty) (create "Test" CP.empty))+ , testCase "empty" (assertEqual "Empty list with title" (List "Test" CP.empty) emptyList)+ , testCase+ "new"+ (assertEqual+ "List with title and blank Task"+ (List "Test" (fromList [T.blank]))+ (new emptyList))+ , testGroup+ "count"+ [ testCase "empty" (assertEqual "List length" 0 (count emptyList))+ , testCase "one item" (assertEqual "List length" 1 (count $ new emptyList))+ ]+ , testCase+ "newAt"+ (assertEqual+ "List with new item second position"+ (List "Populated" (fromList [T.new "Hello", T.blank, T.new "Blah", T.new "Fish"]))+ (newAt 1 populatedList))+ , testGroup+ "append"+ [ testCase+ "populated"+ (assertEqual+ "List with new item"+ (List+ "Populated"+ (fromList [T.new "Hello", T.new "Blah", T.new "Fish", T.new "Spoon"]))+ (append (T.new "Spoon") populatedList))+ , testCase+ "empty"+ (assertEqual+ "List with new item"+ (List "Test" (fromList [T.new "Spoon"]))+ (append (T.new "Spoon") emptyList))+ ]+ , testCase+ "extract"+ (assertEqual+ "List and extracted item"+ (Just (List "Populated" (fromList [T.new "Hello", T.new "Fish"]), T.new "Blah"))+ (extract 1 populatedList))+ , testCase+ "updateFn"+ (assertEqual+ "List with updated item"+ (List "Populated" (fromList [T.new "Hello", T.new "Monkey", T.new "Fish"]))+ (updateFn 1 (T.name .~ "Monkey") populatedList))+ , testCase+ "update"+ (assertEqual+ "List with updated item"+ (List "Populated" (fromList [T.new "Hello", T.new "Monkey", T.new "Fish"]))+ (update 1 (T.new "Monkey") populatedList))+ , testGroup+ "move"+ [ testCase+ "up"+ (assertEqual+ "List with moved item"+ (Just+ ( 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"])+ , 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"])+ , 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"])+ , 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"+ (assertEqual+ "List with removed item"+ (List "Populated" (fromList [T.new "Hello", T.new "Fish"]))+ (deleteTask 1 populatedList))+ , testGroup+ "getTask"+ [ testCase+ "exists"+ (assertEqual "Middle item" (Just (T.new "Blah")) (getTask 1 populatedList))+ , testCase "doesn't exist" (assertEqual "Nothing" Nothing (getTask 5 populatedList))+ ]+ , testGroup+ "searchFor"+ [ testCase+ "exists"+ (assertEqual+ "Blah item"+ (List "Populated" (fromList [T.new "Blah"]))+ (searchFor "Bl" populatedList))+ , testCase+ "multiple"+ (assertEqual+ "Hello and Blah"+ (List "Populated" (fromList [T.new "Hello", T.new "Blah"]))+ (searchFor "l" populatedList))+ , testCase+ "case-insensitive"+ (assertEqual+ "Hello"+ (List "Populated" (fromList [T.new "Hello"]))+ (searchFor "hello" populatedList))+ , testCase+ "doesn't exist"+ (assertEqual+ "Empty list"+ (List "Populated" CP.empty)+ (searchFor "FFF" populatedList))+ ]+ ]
+ test/Taskell/Data/ListsTest.hs view
@@ -0,0 +1,233 @@+module Taskell.Data.ListsTest+ ( test_lists+ ) where++import ClassyPrelude hiding (delete)++import Test.Tasty+import Test.Tasty.HUnit++import Control.Lens ((.~))++import Taskell.Data.Date (textToTime)+import qualified Taskell.Data.List as L+import Taskell.Data.Lists.Internal+import qualified Taskell.Data.Task as T+import Taskell.Types (ListIndex (ListIndex), TaskIndex (TaskIndex))++-- test data+list1, list2, list3 :: L.List+list1 =+ foldl'+ (flip L.append)+ (L.empty "List 1")+ [T.new "One", (T.due .~ textToTime "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.due .~ textToTime "2018-12-03") (T.new "3")]++list3 =+ foldl'+ (flip L.append)+ (L.empty "List 3")+ [(T.due .~ textToTime "2019-04-05") (T.new "01"), T.new "10", T.new "11"]++testLists :: Lists+testLists = fromList [list1, list2, list3]++-- tests+test_lists :: TestTree+test_lists =+ testGroup+ "Data.Taskell.Lists"+ [ testCase+ "initial"+ (assertEqual+ "Returns To Do and Done lists"+ (fromList [L.empty "To Do", L.empty "Done"])+ initial)+ , testCase+ "updateLists"+ (assertEqual+ "Replaces the middle list"+ (fromList [list1, list1, list3])+ (updateLists 1 list1 testLists))+ , testGroup+ "count"+ [ testCase+ "list exists"+ (assertEqual "Returns length of middle list" 3 (count 1 testLists))+ , testCase "list does not exist" (assertEqual "Returns 0" 0 (count 10 testLists))+ ]+ , testGroup+ "get"+ [ testCase+ "list exists"+ (assertEqual "Returns the list" (Just list2) (get testLists 1))+ , testCase "list does not exist" (assertEqual "Nothing" Nothing (get testLists 10))+ ]+ , testGroup+ "changeList"+ [ testCase+ "right"+ (assertEqual+ "Returns updated lists"+ (Just+ (fromList+ [ list1+ , foldl'+ (flip L.append)+ (L.empty "List 2")+ [T.new "1", (T.due .~ textToTime "2018-12-03") (T.new "3")]+ , foldl'+ (flip L.append)+ (L.empty "List 3")+ [ (T.due .~ textToTime "2019-04-05") (T.new "01")+ , T.new "10"+ , T.new "11"+ , T.new "2"+ ]+ ]))+ (changeList Bottom (ListIndex 1, TaskIndex 1) testLists 1))+ , testCase+ "left"+ (assertEqual+ "Returns updated lists"+ (Just+ (fromList+ [ foldl'+ (flip L.append)+ (L.empty "List 1")+ [ T.new "One"+ , (T.due .~ textToTime "2019-08-14") (T.new "Two")+ , T.new "Three"+ , T.new "2"+ ]+ , foldl'+ (flip L.append)+ (L.empty "List 2")+ [T.new "1", (T.due .~ textToTime "2018-12-03") (T.new "3")]+ , list3+ ]))+ (changeList Bottom (ListIndex 1, TaskIndex 1) testLists (-1)))+ , testCase+ "out of bounds list"+ (assertEqual+ "Nothing"+ Nothing+ (changeList Bottom (ListIndex 5, TaskIndex 1) testLists 1))+ , testCase+ "out of bounds task"+ (assertEqual+ "Nothing"+ Nothing+ (changeList Bottom (ListIndex 1, TaskIndex 10) testLists 1))+ ]+ , testCase+ "newList"+ (assertEqual+ "Returns lists with new list"+ (fromList [list1, list2, list3, L.empty "Hello"])+ (newList "Hello" testLists))+ , testCase+ "delete"+ (assertEqual+ "Returns lists with middle list removed"+ (fromList [list1, list3])+ (delete 1 testLists))+ , testGroup+ "exists"+ [ testCase "list exists" (assertEqual "Returns True" True (exists 1 testLists))+ , testCase+ "list does not exist"+ (assertEqual "Returns False" False (exists 10 testLists))+ ]+ , testGroup+ "shiftBy"+ [ testCase+ "right"+ (assertEqual+ "Returns updated lists"+ (Just (fromList [list1, list3, list2]))+ (shiftBy 1 1 testLists))+ , testCase+ "left"+ (assertEqual+ "Returns updated lists"+ (Just (fromList [list2, list1, list3]))+ (shiftBy 1 (-1) testLists))+ , testCase+ "out of bounds list"+ (assertEqual "Nothing" Nothing (shiftBy 5 1 testLists))+ , testCase+ "out of bounds shift"+ (assertEqual+ "Returns updated lists"+ (Just (fromList [list2, list1, list3]))+ (shiftBy 1 (-10) testLists))+ ]+ , testGroup+ "search"+ [ testCase+ "term exists"+ (assertEqual+ "Returns filtered lists"+ (fromList+ [ foldl' (flip L.append) (L.empty "List 1") [T.new "One"]+ , L.empty "List 2"+ , L.empty "List 3"+ ])+ (search "One" testLists))+ , testCase+ "term doesn't exist"+ (assertEqual+ "Returns updated lists"+ (fromList [L.empty "List 1", L.empty "List 2", L.empty "List 3"])+ (search "Fish" testLists))+ ]+ , testGroup+ "appendToLast"+ [ testCase+ "previous list exists"+ (assertEqual+ "Returns updated lists"+ (fromList+ [ list1+ , list2+ , foldl'+ (flip L.append)+ (L.empty "List 3")+ [ (T.due .~ textToTime "2019-04-05") (T.new "01")+ , T.new "10"+ , T.new "11"+ , T.new "Blah"+ ]+ ])+ (appendToLast (T.new "Blah") testLists))+ , testCase+ "previous list doesn't exist"+ (assertEqual "Returns original list" empty (appendToLast (T.new "Blah") empty))+ ]+ , testCase+ "analyse"+ (assertEqual+ "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.due .~ textToTime "2018-12-03") (T.new "3"))+ , ( (ListIndex 2, TaskIndex 0)+ , (T.due .~ textToTime "2019-04-05") (T.new "01"))+ , ( (ListIndex 0, TaskIndex 1)+ , (T.due .~ textToTime "2019-08-14") (T.new "Two"))+ ])+ (due testLists))+ ]
+ test/Taskell/Data/SeqTest.hs view
@@ -0,0 +1,92 @@+module Taskell.Data.SeqTest+ ( test_seq+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Taskell.Data.Seq++testSeq :: Seq Int+testSeq = fromList [1, 2, 3, 4, 5]++-- tests+test_seq :: TestTree+test_seq =+ testGroup+ "Data.Taskell.Seq"+ [ testGroup+ "extract"+ [ testCase+ "Standard"+ (assertEqual+ "Extracts third item"+ (Just (fromList [1, 2, 4, 5], 3))+ (extract 2 testSeq))+ , testCase+ "First"+ (assertEqual+ "Extracts first item"+ (Just (fromList [2, 3, 4, 5], 1))+ (extract 0 testSeq))+ , testCase+ "Last"+ (assertEqual+ "Extracts last item"+ (Just (fromList [1, 2, 3, 4], 5))+ (extract 4 testSeq))+ , testCase+ "Out of range - positive"+ (assertEqual "Nothing" Nothing (extract 5 testSeq))+ , testCase+ "Out of range - negative"+ (assertEqual "Nothing" Nothing (extract (-1) testSeq))+ ]+ , testGroup+ "shiftBy"+ [ testCase+ "To right"+ (assertEqual+ "Moves third item right"+ (Just (fromList [1, 2, 4, 3, 5]))+ (shiftBy 2 1 testSeq))+ , testCase+ "To left"+ (assertEqual+ "Moves third item left"+ (Just (fromList [1, 3, 2, 4, 5]))+ (shiftBy 2 (-1) testSeq))+ , testCase+ "First to right"+ (assertEqual+ "Moves first item right"+ (Just (fromList [2, 1, 3, 4, 5]))+ (shiftBy 0 1 testSeq))+ , testCase+ "First to left"+ (assertEqual "The original sequence" (Just testSeq) (shiftBy 0 (-1) testSeq))+ , testCase+ "Last to right"+ (assertEqual "The original sequence" (Just testSeq) (shiftBy 4 1 testSeq))+ , testCase+ "Last to left"+ (assertEqual+ "Moves last item left"+ (Just (fromList [1, 2, 3, 5, 4]))+ (shiftBy 4 (-1) testSeq))+ , testCase+ "Ridiculous right shift"+ (assertEqual+ "Moves to right-most"+ (Just (fromList [1, 2, 4, 5, 3]))+ (shiftBy 2 20 testSeq))+ , testCase+ "Ridiculous left shift"+ (assertEqual+ "Moves to left-most"+ (Just (fromList [3, 1, 2, 4, 5]))+ (shiftBy 2 (-20) testSeq))+ ]+ ]
+ test/Taskell/Data/SubtaskTest.hs view
@@ -0,0 +1,42 @@+module Taskell.Data.SubtaskTest+ ( test_subtask+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Taskell.Data.Subtask.Internal++-- tests+test_subtask :: TestTree+test_subtask =+ testGroup+ "Data.Taskell.Subtask"+ [ testCase "blank" (assertEqual "creates an empty sub-task" (Subtask "" False) blank)+ , testGroup+ "new"+ [ testCase+ "completed"+ (assertEqual "creates a new sub-task" (Subtask "Blah" True) (new "Blah" True))+ , testCase+ "not completed"+ (assertEqual "creates a new sub-task" (Subtask "Blah" False) (new "Blah" False))+ ]+ , testGroup+ "toggle"+ [ testCase+ "false to true"+ (assertEqual+ "Sets completed to True"+ (Subtask "Blah" True)+ (toggle $ new "Blah" False))+ , testCase+ "true to false"+ (assertEqual+ "Sets completed to False"+ (Subtask "Blah" False)+ (toggle $ new "Blah" True))+ ]+ ]
+ test/Taskell/Data/TaskTest.hs view
@@ -0,0 +1,200 @@+module Taskell.Data.TaskTest+ ( test_task+ ) where++import ClassyPrelude++import Control.Lens ((.~))++import Test.Tasty+import Test.Tasty.HUnit++import Data.Time.Calendar (fromGregorianValid)++import Taskell.Data.Date (Due (DueDate))+import qualified Taskell.Data.Subtask as ST (name, new)+import Taskell.Data.Task.Internal++desc :: Maybe Text+desc = Just "A very boring description"++testTask :: Task+testTask =+ Task+ { _name = "Test"+ , _description = desc+ , _subtasks = fromList [ST.new "One" True, ST.new "Two" False, ST.new "Three" False]+ , _due = Nothing+ }++-- tests+test_task :: TestTree+test_task =+ testGroup+ "Data.Taskell.Task"+ [ testCase "blank" (assertEqual "Returns empty task" (Task "" Nothing empty Nothing) blank)+ , testCase+ "new"+ (assertEqual "Returns a new task" (Task "Hello" Nothing empty Nothing) (new "Hello"))+ , testGroup+ "getSubtask"+ [ testCase+ "exists"+ (assertEqual+ "Returns a the sub-task"+ (Just (ST.new "Two" False))+ (getSubtask 1 testTask))+ , testCase+ "doesn't exist"+ (assertEqual "Returns Nothing" Nothing (getSubtask 10 testTask))+ ]+ , testGroup+ "setDescription"+ [ testCase+ "empty sub-tasks"+ (assertEqual+ "Updates description"+ (Task "Test" (Just "Description") empty Nothing)+ (setDescription "Description" (new "Test")))+ , testCase+ "empty"+ (assertEqual+ "Keeps description as Nothing"+ (new "Test")+ (setDescription "" (new "Test")))+ , testCase+ "blank"+ (assertEqual+ "Keeps description as Nothing"+ (new "Test")+ (setDescription " " (new "Test")))+ ]+ , testGroup+ "addSubtask"+ [ testCase+ "existing"+ (assertEqual+ "Returns the task with added subtask"+ (Task+ "Test"+ desc+ (fromList+ [ ST.new "One" True+ , ST.new "Two" False+ , ST.new "Three" False+ , ST.new "Four" True+ ])+ Nothing)+ (addSubtask (ST.new "Four" True) testTask))+ , testCase+ "empty sub-tasks"+ (assertEqual+ "Returns the task with added subtask"+ (Task "Test" Nothing (fromList [ST.new "One" False]) Nothing)+ (addSubtask (ST.new "One" False) (new "Test")))+ ]+ , testGroup+ "hasSubtasks"+ [ testCase "existing" (assertEqual "Returns True" True (hasSubtasks testTask))+ , testCase+ "empty sub-tasks"+ (assertEqual "Returns False" False (hasSubtasks (new "Test")))+ ]+ , testGroup+ "updateSubtask"+ [ testCase+ "exists"+ (assertEqual+ "Returns updated task"+ (Task+ "Test"+ desc+ (fromList+ [ST.new "One" True, ST.new "Cow" False, ST.new "Three" False])+ Nothing)+ (updateSubtask 1 (ST.name .~ "Cow") testTask))+ , testCase+ "doesn't exist"+ (assertEqual+ "Returns task"+ (Task+ "Test"+ desc+ (fromList+ [ST.new "One" True, ST.new "Two" False, ST.new "Three" False])+ Nothing)+ (updateSubtask 10 (ST.name .~ "Cow") testTask))+ ]+ , testGroup+ "removeSubtask"+ [ testCase+ "exists"+ (assertEqual+ "Returns updated task"+ (Task+ "Test"+ desc+ (fromList [ST.new "One" True, ST.new "Three" False])+ Nothing)+ (removeSubtask 1 testTask))+ , testCase+ "doesn't exist"+ (assertEqual+ "Returns task"+ (Task+ "Test"+ desc+ (fromList+ [ST.new "One" True, ST.new "Two" False, ST.new "Three" False])+ Nothing)+ (removeSubtask 10 testTask))+ ]+ , testCase "countSubtasks" (assertEqual "Returns 3" 3 (countSubtasks testTask))+ , testCase+ "countCompleteSubtasks"+ (assertEqual "Returns 1" 1 (countCompleteSubtasks testTask))+ , testGroup+ "contains"+ [ testCase "in task" (assertEqual "Finds in task" True (contains "Test" testTask))+ , testCase "in sub-task" (assertEqual "Find sub-task" True (contains "One" testTask))+ , testCase+ "case-insensitive"+ (assertEqual "Find sub-task" True (contains "ONE" testTask))+ , testCase+ "case-insensitive"+ (assertEqual "Find sub-task" True (contains "two" testTask))+ , testCase "missing" (assertEqual "Find sub-task" False (contains "Fish" testTask))+ , testCase+ "description"+ (assertEqual "Finds in description" True (contains "boring" testTask))+ , testCase+ "not in description"+ (assertEqual "Doesn't find" False (contains "escapades" testTask))+ ]+ , testGroup+ "isBlank"+ [ testCase+ "blank"+ (assertEqual "Return True" True (isBlank (Task "" Nothing empty Nothing)))+ , testCase "name not blank" (assertEqual "Returns False" False (isBlank testTask))+ , testCase+ "description not blank"+ (assertEqual+ "Returns False"+ False+ (isBlank (Task "" (Just "Blah") empty Nothing)))+ , testCase+ "subtasks not blank"+ (assertEqual+ "Returns False"+ False+ (isBlank (Task "" Nothing (fromList [ST.new "One" True]) Nothing)))+ , testCase+ "due date not blank"+ (assertEqual+ "Returns False"+ False+ (isBlank+ (Task "" Nothing empty (DueDate <$> (fromGregorianValid 2018 05 18)))))+ ]+ ]
+ test/Taskell/Events/State/HistoryTest.hs view
@@ -0,0 +1,95 @@+module Taskell.Events.State.HistoryTest+ ( test_history+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Taskell.Events.State.History+import Taskell.Events.State.Types++make :: [Integer] -> Integer -> [Integer] -> History Integer+make = History++testHistory :: History Integer+testHistory = make empty 0 empty++-- tests+test_history :: TestTree+test_history =+ testGroup+ "Events.State.History"+ [ testGroup+ "undo"+ [ testCase "empty" (assertEqual "Nothing changes" testHistory (undo testHistory))+ , testCase+ "one undo"+ (assertEqual "Goes back" (make empty 0 [1]) (undo $ make [0] 1 empty))+ , testCase+ "two undo"+ (assertEqual+ "Goes back"+ (make empty 0 [1, 2])+ (undo . undo $ make [1, 0] 2 empty))+ , testCase+ "three undo"+ (assertEqual+ "Goes back"+ (make empty 0 [1, 2, 3])+ (undo . undo . undo $ make [2, 1, 0] 3 empty))+ ]+ , testGroup+ "redo"+ [ testCase "empty" (assertEqual "Nothing changes" testHistory (redo testHistory))+ , testCase+ "one redo"+ (assertEqual "Goes forward" (make [0] 1 empty) (redo $ make empty 0 [1]))+ , testCase+ "two redo"+ (assertEqual+ "Goes forward"+ (make [1, 0] 2 empty)+ (redo . redo $ make empty 0 [1, 2]))+ , testCase+ "three redo"+ (assertEqual+ "Goes forward"+ (make [2, 1, 0] 3 empty)+ (redo . redo . redo $ make empty 0 [1, 2, 3]))+ ]+ , testGroup+ "mix"+ [ testCase+ "empty"+ (assertEqual+ "Nothing changes"+ testHistory+ (undo . redo . undo . redo . undo $ testHistory))+ , testCase+ "redo undo"+ (assertEqual+ "Nothing changes"+ (make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10])+ (undo . redo $ make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10]))+ , testCase+ "undo redo"+ (assertEqual+ "Nothing changes"+ (make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10])+ (redo . undo $ make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10]))+ ]+ , testGroup+ "store"+ [ testCase+ "empty"+ (assertEqual "Stores current value" (make [0] 0 empty) (store testHistory))+ , testCase+ "undo store redo"+ (assertEqual+ "Clears redo"+ (make [4, 3, 2, 1, 0] 4 empty)+ (redo . store . undo $ make [4, 3, 2, 1, 0] 5 [6, 7, 8, 9, 10]))+ ]+ ]
+ test/Taskell/Events/StateTest.hs view
@@ -0,0 +1,130 @@+{-# LANGUAGE OverloadedLists #-}++module Taskell.Events.StateTest+ ( test_state+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Control.Lens ((&), (.~), (^.))++import Data.Time.Clock (secondsToDiffTime)+import Data.Time.Zones (utcTZ)++import qualified Data.Sequence as S (lookup)+import qualified Taskell.Data.List as L (append, empty)+import qualified Taskell.Data.Task as T (new)+import Taskell.Events.State+import Taskell.Events.State.Types+import Taskell.Events.State.Types.Mode+import Taskell.Types (ListIndex (ListIndex), TaskIndex (TaskIndex))++mockTime :: UTCTime+mockTime = UTCTime (ModifiedJulianDay 20) (secondsToDiffTime 0)++testState :: State+testState =+ State+ { _mode = Normal+ , _history = fresh empty+ , _path = "test.md"+ , _io = Nothing+ , _height = 0+ , _searchTerm = Nothing+ , _time = mockTime+ , _timeZone = utcTZ+ }++moveToState :: State+moveToState =+ State+ { _mode = Modal MoveTo+ , _history =+ History+ { _past = empty+ , _present =+ ( (ListIndex 4, TaskIndex 0)+ , [ L.empty "List 1"+ , L.empty "List 2"+ , L.empty "List 3"+ , L.empty "List 4"+ , L.append (T.new "Test Item") (L.empty "List 5")+ , L.empty "List 6"+ , L.empty "List 7"+ , L.empty "List 8"+ , L.empty "List 9"+ ])+ , _future = empty+ }+ , _path = "test.md"+ , _io = Nothing+ , _height = 0+ , _searchTerm = Nothing+ , _time = mockTime+ , _timeZone = utcTZ+ }++-- tests+test_state :: TestTree+test_state =+ testGroup+ "Events.State"+ [ testCase+ "quit"+ (assertEqual+ "Retuns Just with mode set to Shutdown"+ (Just (testState & mode .~ Shutdown))+ (quit testState))+ , testCase+ "continue"+ (assertEqual "Retuns with io set to " (testState & io .~ Nothing) (continue testState))+ -- 1 - a, 2 - b, 3 - c, 4 - d, 5 - e, 6 - f, 7 - g, 8 - h, 9 - i+ , testGroup+ "moveTo"+ [ testCase+ "first"+ (assertEqual+ "Moves to first list"+ (Just (L.append (T.new "Test Item") (L.empty "List 1")))+ (S.lookup 0 . (^. lists) =<< moveTo 'a' moveToState))+ , testCase+ "second"+ (assertEqual+ "Moves to second list"+ (Just (L.append (T.new "Test Item") (L.empty "List 2")))+ (S.lookup 1 . (^. lists) =<< moveTo 'b' moveToState))+ , testCase+ "fourth"+ (assertEqual+ "Moves to fourth list"+ (Just (L.append (T.new "Test Item") (L.empty "List 4")))+ (S.lookup 3 . (^. lists) =<< moveTo 'd' moveToState))+ , testCase+ "sixth"+ (assertEqual+ "Moves to sixth list"+ (Just (L.append (T.new "Test Item") (L.empty "List 6")))+ (S.lookup 5 . (^. lists) =<< moveTo 'f' moveToState))+ , testCase+ "penultimate"+ (assertEqual+ "Moves to penultime list"+ (Just (L.append (T.new "Test Item") (L.empty "List 8")))+ (S.lookup 7 . (^. lists) =<< moveTo 'h' moveToState))+ , testCase+ "last"+ (assertEqual+ "Moves to last list"+ (Just (L.append (T.new "Test Item") (L.empty "List 9")))+ (S.lookup 8 . (^. lists) =<< moveTo 'i' moveToState))+ , testCase+ "current list"+ (assertEqual "Doesn't do anything" Nothing (moveTo 'e' moveToState))+ , testCase+ "invalid option"+ (assertEqual "Doesn't do anything" Nothing (moveTo 'q' moveToState))+ ]+ ]
+ test/Taskell/IO/GitHub/CardsTest.hs view
@@ -0,0 +1,36 @@+module Taskell.IO.GitHub.CardsTest+ ( test_cards+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Data.Aeson++import Taskell.IO.HTTP.GitHub.Card (MaybeCard (MaybeCard))++decodeCards :: Text -> Either String [MaybeCard]+decodeCards txt = eitherDecodeStrict $ encodeUtf8 txt++-- tests+test_cards :: TestTree+test_cards =+ testGroup+ "IO.HTTP.GitHub.Card"+ [ testCase+ "parses basic card"+ (assertEqual+ "Gives back card"+ (Right [MaybeCard (Just "blah") Nothing])+ (decodeCards "[{\"note\": \"blah\"}]"))+ , testCase+ "parses basic card"+ (assertEqual+ "Gives back card"+ (Right+ [MaybeCard Nothing (Just "https://api.github.com/projects/columns/7850783")])+ (decodeCards+ "[{\"note\": null, \"content_url\": \"https://api.github.com/projects/columns/7850783\"}]"))+ ]
+ test/Taskell/IO/GitHubTest.hs view
@@ -0,0 +1,45 @@+module Taskell.IO.GitHubTest+ ( test_github+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Taskell.IO.HTTP.GitHub (getNextLink)++-- tests+test_github :: TestTree+test_github =+ testGroup+ "IO.HTTP.GitHub"+ [ testGroup+ "getNextLink"+ [ testCase+ "next exists"+ (assertEqual+ "Parses next link"+ (Just "https://api.github.com/projects/columns/3152155/cards?page=2")+ (getNextLink+ [ "<https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"next\", <https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"last\""+ ]))+ , testCase "empty" (assertEqual "Returns Nothing" Nothing (getNextLink []))+ , testCase+ "no next"+ (assertEqual+ "Returns Nothing"+ Nothing+ (getNextLink+ [ "<https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"prev\", <https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"last\""+ ]))+ , testCase+ "next last"+ (assertEqual+ "Parses next link"+ (Just "https://api.github.com/projects/columns/3152155/cards?page=2")+ (getNextLink+ [ "<https://api.github.com/projects/columns/3152155/cards?page=1>; rel=\"prev\", <https://api.github.com/projects/columns/3152155/cards?page=2>; rel=\"next\", <https://api.github.com/projects/columns/3152155/cards?page=3>; rel=\"last\""+ ]))+ ]+ ]
+ test/Taskell/IO/Keyboard/ParserTest.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE QuasiQuotes #-}++module Taskell.IO.Keyboard.ParserTest+ ( test_parser+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Data.FileEmbed (embedFile)+import Text.RawString.QQ (r)++import qualified Taskell.Events.Actions.Types as A+import Taskell.IO.Keyboard.Parser (bindings)+import Taskell.IO.Keyboard.Types++basic :: Text+basic = "quit = q"++basicResult :: Bindings+basicResult = [(BChar 'q', A.Quit)]++basicMulti :: Text+basicMulti =+ [r|+ quit = q+ detail = <Enter>+ |]++basicMultiResult :: Bindings+basicMultiResult = [(BChar 'q', A.Quit), (BKey "Enter", A.Detail)]++ini :: Text+ini = decodeUtf8 $(embedFile "test/Taskell/IO/Keyboard/data/bindings.ini")++iniResult :: Bindings+iniResult =+ [ (BChar 'q', A.Quit)+ , (BChar 'u', A.Undo)+ , (BChar 'r', A.Redo)+ , (BChar '/', A.Search)+ , (BChar '?', A.Help)+ , (BChar '!', A.Due)+ , (BChar 'k', A.Previous)+ , (BChar 'j', A.Next)+ , (BChar 'h', A.Left)+ , (BChar 'l', A.Right)+ , (BChar 'G', A.Bottom)+ , (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)+ , (BChar 'C', A.Clear)+ , (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.MoveLeftBottom)+ , (BChar 'L', A.MoveRightBottom)+ , (BKey "Space", A.Complete)+ , (BChar 'm', A.MoveMenu)+ , (BChar 'N', A.ListNew)+ , (BChar 'E', A.ListEdit)+ , (BChar 'X', A.ListDelete)+ , (BChar '>', A.ListRight)+ , (BChar '<', A.ListLeft)+ ]++comma :: Text+comma = "quit = ,"++commaResult :: Bindings+commaResult = [(BChar ',', A.Quit)]++test_parser :: TestTree+test_parser =+ testGroup+ "IO.Keyboard.Parser"+ [ testCase "basic" (assertEqual "Parses quit" (Right basicResult) (bindings basic))+ , testCase+ "basic multiline"+ (assertEqual "Parses both" (Right basicMultiResult) (bindings basicMulti))+ , testCase "full ini file" (assertEqual "Parses all" (Right iniResult) (bindings ini))+ , testCase "comma" (assertEqual "Parses comma" (Right commaResult) (bindings comma))+ ]
+ test/Taskell/IO/Keyboard/TypesTest.hs view
@@ -0,0 +1,37 @@+module Taskell.IO.Keyboard.TypesTest+ ( test_types+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import qualified Taskell.Events.Actions.Types as A (ActionType (..))+import Taskell.IO.Keyboard (addMissing, badMapping, defaultBindings)+import Taskell.IO.Keyboard.Types (Binding (..), Bindings)++notFull :: Bindings+notFull = [(BChar 'œ', A.Quit), (BChar 'U', A.Undo)]++notFullResult :: Bindings+notFullResult = notFull <> (drop 2 defaultBindings)++bad :: Bindings+bad = [(BChar 'q', A.Quit), (BChar 'u', A.Nothing), (BChar '>', A.Nothing), (BChar '<', A.ListLeft)]++-- tests+test_types :: TestTree+test_types =+ testGroup+ "Events.Actions.Types"+ [ testCase+ "not missing"+ (assertEqual "Finds no missing items" defaultBindings (addMissing defaultBindings))+ , testCase+ "not missing"+ (assertEqual "Finds missing items" notFullResult (addMissing notFull))+ , testCase+ "bad mapping"+ (assertEqual "Finds bad mapping" (Left "invalid mapping") (badMapping bad))+ ]
+ test/Taskell/IO/Keyboard/data/bindings.ini view
@@ -0,0 +1,43 @@+# general+quit = q+undo = u+redo = r+search = /+help = ?+due = !++# navigation+previous = k+next = j+left = h+right = l+bottom = G++# new tasks+new = a+newAbove = O+newBelow = o+duplicate = +++# editing tasks+edit = e, A, i+clear = C+delete = D+detail = <Enter>+dueDate = @+clearDate = <Backspace>++# moving tasks+moveUp = K+moveDown = J+moveLeft = H+moveRight = L+complete = <Space>+moveMenu = m++# lists+listNew = N+listEdit = E+listDelete = X+listRight = >+listLeft = <
+ test/Taskell/IO/KeyboardTest.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedLists #-}++module Taskell.IO.KeyboardTest+ ( test_keyboard+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Control.Lens ((.~))++import Data.Time.Clock (secondsToDiffTime)+import Data.Time.Zones (utcTZ)++import Graphics.Vty.Input.Events (Event (..), Key (..))+import Taskell.Data.Lists.Internal (initial)+import Taskell.Events.Actions.Types as A+import Taskell.Events.State (create, quit)+import Taskell.Events.State.Types (State, Stateful, mode)+import Taskell.Events.State.Types.Mode (Mode (Shutdown))+import Taskell.IO.Keyboard (generate)+import Taskell.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 utcTZ mockTime "taskell.md" initial++basicBindings :: Bindings+basicBindings = [(BChar 'q', A.Quit)]++basicActions :: Actions+basicActions = [(A.Quit, quit)]++basicResult :: Maybe State+basicResult = Just $ (mode .~ Shutdown) cleanState++test_keyboard :: TestTree+test_keyboard =+ testGroup+ "IO.Keyboard"+ [ testCase+ "generate"+ (assertEqual+ "Parses basic"+ basicResult+ (tester (generate basicBindings basicActions) (EvKey (KChar 'q') []) cleanState))+ ]
+ test/Taskell/IO/Markdown/ParserTest.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.Markdown.ParserTest+ ( test_parser+ ) where++import ClassyPrelude++import Test.Tasty++import Test.Tasty.HUnit++import Control.Lens ((&), (.~))+import Data.FileEmbed (embedFile)++import Taskell.Data.Date (textToTime)+import Taskell.Data.List (create)+import Taskell.Data.Lists (Lists, analyse, appendToLast, newList)++import qualified Taskell.Data.Subtask as ST (new)+import Taskell.Data.Task (Task, addSubtask, due, new, setDescription)+import Taskell.IO.Config.Markdown (Config (Config), defaultConfig, descriptionOutput,+ dueOutput, localTimes, subtaskOutput, taskOutput,+ titleOutput)+import Taskell.IO.Markdown.Parser (parse)++-- error message+err :: Either Text Lists+err = Left "Could not parse file."++-- complete taskell file+file :: Text+file = decodeUtf8 $(embedFile "test/Taskell/IO/data/roadmap.md")++-- alternative markdown configs+alternativeConfig :: Config+alternativeConfig =+ Config+ { titleOutput = "##"+ , taskOutput = "###"+ , descriptionOutput = ">"+ , dueOutput = "@"+ , subtaskOutput = "-"+ , localTimes = True+ }++-- useful records+task :: Task+task = new "Test Item"++list :: Lists+list = newList "Test" empty++listWithItem :: Lists+listWithItem = appendToLast task list++multiList :: Lists+multiList =+ fromList+ [create "Test" (fromList [new "Test Item"]), create "Test 2" (fromList [new "Test Item 2"])]++makeSubTask :: Text -> Bool -> Lists+makeSubTask t b = appendToLast (addSubtask (ST.new t b) task) list++taskWithSummary :: Task+taskWithSummary = setDescription "Summary" task++taskWithMultiLineSummary :: Task+taskWithMultiLineSummary = setDescription "Summary Line 1\nSummary Line 2" task++listWithSummaryItem :: Lists+listWithSummaryItem = appendToLast taskWithSummary list++listWithMultiLineSummaryItem :: Lists+listWithMultiLineSummaryItem = appendToLast taskWithMultiLineSummary list++taskWithDueDate :: Task+taskWithDueDate = task & due .~ textToTime "2018-04-12"++listWithDueDateItem :: Lists+listWithDueDateItem = appendToLast taskWithDueDate list++-- tests+test_parser :: TestTree+test_parser =+ testGroup+ "IO.Markdown"+ [ testGroup+ "Default Format"+ [ testCase+ "List Title"+ (assertEqual "One list" (Right list) (parse defaultConfig "## Test"))+ , testCase+ "List item"+ (assertEqual+ "List item"+ (Right listWithItem)+ (parse defaultConfig "## Test\n\n- Test Item"))+ , testCase+ "Multiple Lists"+ (assertEqual+ "List item"+ (Right multiList)+ (parse defaultConfig "## Test\n\n- Test Item\n\n## Test 2\n\n- Test Item 2"))+ , testCase+ "Summary"+ (assertEqual+ "Summary"+ (Right listWithSummaryItem)+ (parse defaultConfig "## Test\n\n- Test Item\n > Summary"))+ , testCase+ "List Item with multi-line Summary"+ (assertEqual+ "List item with a summary"+ (Right listWithMultiLineSummaryItem)+ (parse+ defaultConfig+ "## Test\n\n- Test Item\n > Summary Line 1\n > Summary Line 2"))+ , testCase+ "Due Date"+ (assertEqual+ "Due Date"+ (Right listWithDueDateItem)+ (parse defaultConfig "## Test\n\n- Test Item\n @ 2018-04-12"))+ , testCase+ "Sub-Task"+ (assertEqual+ "List item with Sub-Task"+ (Right (makeSubTask "Blah" False))+ (parse defaultConfig "## Test\n\n- Test Item\n * [ ] Blah"))+ , testCase+ "Complete Sub-Task"+ (assertEqual+ "List item with Sub-Task"+ (Right (makeSubTask "Blah" True))+ (parse defaultConfig "## Test\n\n- Test Item\n * [x] Blah"))+ ]+ , testGroup+ "Alternative Format"+ [ testCase+ "List Title"+ (assertEqual "One list" (Right list) (parse alternativeConfig "## Test"))+ , testCase+ "List item"+ (assertEqual+ "List item"+ (Right listWithItem)+ (parse alternativeConfig "## Test\n\n### Test Item"))+ , testCase+ "Multiple Lists"+ (assertEqual+ "List item"+ (Right multiList)+ (parse+ alternativeConfig+ "## Test\n\n### Test Item\n\n## Test 2\n\n### Test Item 2"))+ , testCase+ "Summary"+ (assertEqual+ "Summary"+ (Right listWithSummaryItem)+ (parse alternativeConfig "## Test\n\n### Test Item\n> Summary"))+ , testCase+ "List Item with multi-line Summary"+ (assertEqual+ "List item with a summary"+ (Right listWithMultiLineSummaryItem)+ (parse+ alternativeConfig+ "## Test\n\n### Test Item\n> Summary Line 1\n> Summary Line 2"))+ , testCase+ "Due Date"+ (assertEqual+ "Due Date"+ (Right listWithDueDateItem)+ (parse alternativeConfig "## Test\n\n### Test Item\n@ 2018-04-12"))+ , testCase+ "Sub-Task"+ (assertEqual+ "List item with Sub-Task"+ (Right (makeSubTask "Blah" False))+ (parse alternativeConfig "## Test\n\n### Test Item\n- [ ] Blah"))+ , testCase+ "Complete Sub-Task"+ (assertEqual+ "List item with Sub-Task"+ (Right (makeSubTask "Blah" True))+ (parse alternativeConfig "## Test\n\n### Test Item\n- [x] Blah"))+ ]+ , testGroup+ "Errors"+ [ testCase "Blank line" (assertEqual "Parse Error" err (parse defaultConfig ""))+ , testCase+ "Just spaces"+ (assertEqual "Parse Error" err (parse defaultConfig " "))+ , testCase+ "Just whitespace"+ (assertEqual "Parse Error" err (parse defaultConfig " \n \n \t "))+ , testCase "Error" (assertEqual "Parse Error" err (parse defaultConfig "Test"))+ , testCase+ "List item without list"+ (assertEqual "Parse Error" err (parse defaultConfig "- Test Item"))+ , testCase+ "Sub task without list item"+ (assertEqual "Parse Error" err (parse defaultConfig "## Test\n * Blah"))+ ]+ , testCase+ "File"+ (assertEqual+ "Parses whole file"+ (Right "test\nLists: 6\nTasks: 202")+ (analyse "test" <$> parse defaultConfig file))+ ]
+ test/Taskell/IO/Markdown/SerializerTest.hs view
@@ -0,0 +1,192 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.Markdown.SerializerTest+ ( test_serializer+ ) where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Control.Lens ((&), (.~))+import Control.Monad.Reader (runReader)+import Data.FileEmbed (embedFile)++import Data.Time.Zones (utcTZ)+import Data.Time.Zones.All (TZLabel (America__New_York), tzByLabel)++import Taskell.Data.Date (textToTime)+import qualified Taskell.Data.List as L (append, empty)+import Taskell.Data.Lists (Lists, appendToLast, newList)+import qualified Taskell.Data.Subtask as ST (new)+import Taskell.Data.Task (Task, addSubtask, due, new, setDescription)+import Taskell.IO.Config.Markdown (Config (..), defaultConfig)+import Taskell.IO.Markdown.Parser (parse)+import Taskell.IO.Markdown.Serializer (MarkdownInfo (MarkdownInfo), serialize)++-- complete taskell file+file :: Text+file = decodeUtf8 $(embedFile "test/Taskell/IO/data/roadmap.md")++-- alternative markdown configs+alternativeConfig :: Config+alternativeConfig =+ Config+ { titleOutput = "##"+ , taskOutput = "###"+ , descriptionOutput = ">"+ , dueOutput = "@"+ , subtaskOutput = "-"+ , localTimes = True+ }++-- useful records+task :: Task+task = new "Test Item"++list :: Lists+list = newList "Test" empty++lists :: Lists+lists = fromList [L.append task (L.empty "To Do"), L.append (new "Fish") (L.empty "Done")]++listWithItem :: Lists+listWithItem = appendToLast task list++makeSubTask :: Text -> Bool -> Lists+makeSubTask t b = appendToLast (addSubtask (ST.new t b) task) list++makeSubTasks :: [Text] -> Bool -> Lists+makeSubTasks ts b = appendToLast (foldr addSubtask task subtasks) list+ where+ subtasks = flip ST.new b <$> ts++taskWithSummary :: Task+taskWithSummary = setDescription "Summary" task++taskWithMultiLineSummary :: Task+taskWithMultiLineSummary = setDescription "Summary Line 1\nSummary Line 2" task++listWithSummaryItem :: Lists+listWithSummaryItem = appendToLast taskWithSummary list++listWithMultiLineSummaryItem :: Lists+listWithMultiLineSummaryItem = appendToLast taskWithMultiLineSummary list++taskWithDueDate :: Task+taskWithDueDate = task & due .~ textToTime "2018-04-12"++listWithDueDateItem :: Lists+listWithDueDateItem = appendToLast taskWithDueDate list++taskWithDueTime :: Task+taskWithDueTime = task & due .~ textToTime "2018-04-12 12:00 UTC"++listWithDueTimeItem :: Lists+listWithDueTimeItem = appendToLast taskWithDueTime list++-- reader+defaultReader :: MarkdownInfo+defaultReader = MarkdownInfo utcTZ defaultConfig++alternativeReader :: MarkdownInfo+alternativeReader = MarkdownInfo utcTZ alternativeConfig++-- simplify tests+serialize' :: MarkdownInfo -> Lists -> Text+serialize' md ls = runReader (serialize ls) md++-- tests+test_serializer :: TestTree+test_serializer =+ testGroup+ "IO.Markdown"+ [ testGroup+ "Serialisation"+ [ testGroup+ "Default Format"+ [ testCase+ "Standard lists"+ (assertEqual+ "Markdown formatted output"+ "## To Do\n\n- Test Item\n\n## Done\n\n- Fish\n"+ (serialize' defaultReader lists))+ , testCase+ "Standard list"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n- Test Item\n"+ (serialize' defaultReader listWithItem))+ , testCase+ "Standard list with summary"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n- Test Item\n > Summary\n"+ (serialize' defaultReader listWithSummaryItem))+ , testCase+ "Standard list with date"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n- Test Item\n @ 2018-04-12\n"+ (serialize' defaultReader listWithDueDateItem))+ , testCase+ "Standard list with date - timezone"+ (assertEqual+ "Use UTC timezone"+ "## Test\n\n- Test Item\n @ 2018-04-12 12:00 UTC\n"+ (serialize'+ (MarkdownInfo (tzByLabel America__New_York) defaultConfig)+ listWithDueTimeItem))+ , testCase+ "Standard list with sub-task"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n- Test Item\n * [x] Blah\n"+ (serialize' defaultReader (makeSubTask "Blah" True)))+ , testCase+ "Standard list with sub-tasks"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n- Test Item\n * [x] Blah\n * [x] Cow\n * [x] Spoon\n"+ (serialize'+ defaultReader+ (makeSubTasks ["Spoon", "Cow", "Blah"] True)))+ , testCase+ "Standard list with multi-line summary"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n- Test Item\n > Summary Line 1\n > Summary Line 2\n"+ (serialize' defaultReader listWithMultiLineSummaryItem))+ ]+ , testGroup+ "Alternative Format"+ [ testCase+ "Standard list"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n### Test Item\n"+ (serialize' alternativeReader listWithItem))+ , testCase+ "Standard list with sub-task"+ (assertEqual+ "Markdown formatted output"+ "## Test\n\n### Test Item\n- [ ] Blah\n"+ (serialize' alternativeReader (makeSubTask "Blah" False)))+ , testCase+ "Standard list with date - timezone"+ (assertEqual+ "Uses local timezone"+ "## Test\n\n### Test Item\n@ 2018-04-12 08:00 EDT\n"+ (serialize'+ (MarkdownInfo (tzByLabel America__New_York) alternativeConfig)+ listWithDueTimeItem))+ ]+ ]+ , testCase+ "Parse then serialize"+ (assertEqual+ "Returns same text"+ (Right file)+ (serialize' defaultReader <$> parse defaultConfig file))+ ]
+ test/Taskell/IO/TrelloTest.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE TemplateHaskell #-}++module Taskell.IO.TrelloTest+ ( test_trello+ ) where++import ClassyPrelude++import Control.Lens ((^.))++import Test.Tasty+import Test.Tasty.HUnit++import Data.Aeson (decodeStrict)+import Data.FileEmbed (embedFile)++import Taskell.IO.HTTP.Trello.ChecklistItem (ChecklistItem, checkItems, checklistItemToSubTask)+import Taskell.IO.HTTP.Trello.List (List, listToList)++json :: Maybe [List]+json = decodeStrict $(embedFile "test/Taskell/IO/data/trello.json")++checklistJson :: Maybe [ChecklistItem]+checklistJson =+ (^. checkItems) <$> decodeStrict $(embedFile "test/Taskell/IO/data/trello-checklists.json")++-- tests+test_trello :: TestTree+test_trello =+ testGroup+ "IO.Trello"+ [ testCase+ "Lists"+ (assertEqual "Parses list JSON" (Just 5) (length . (listToList <$>) <$> json))+ , testCase+ "Checklists"+ (assertEqual+ "Parses checklist JSON"+ (Just 5)+ (length . (checklistItemToSubTask <$>) <$> checklistJson))+ ]
+ test/Taskell/IO/data/trello-checklists.json view
@@ -0,0 +1,35 @@+{+ "id": "5b04450ff88a64205bf04939",+ "checkItems": [+ {+ "state": "incomplete",+ "idChecklist": "5b04450ff88a64205bf04939",+ "id": "5b044511abc222721a03a478",+ "name": "Sub-item 1"+ },+ {+ "state": "incomplete",+ "idChecklist": "5b04450ff88a64205bf04939",+ "id": "5b0445123561ce689e3827b4",+ "name": "Sub-item 2"+ },+ {+ "state": "incomplete",+ "idChecklist": "5b04450ff88a64205bf04939",+ "id": "5b044513fe0a46480d6bcc87",+ "name": "Sub-item 3"+ },+ {+ "state": "incomplete",+ "idChecklist": "5b04450ff88a64205bf04939",+ "id": "5b04451489b89cde3b7a3af1",+ "name": "Sub-item 4"+ },+ {+ "state": "incomplete",+ "idChecklist": "5b04450ff88a64205bf04939",+ "id": "5b044516152060b914e3fa2f",+ "name": "Sub-item 5"+ }+ ]+}
+ test/Taskell/IO/data/trello.json view
@@ -0,0 +1,146 @@+[+ {+ "id": "5b04449b124fd7d02a6700ac",+ "name": "List 1",+ "cards": [+ {+ "id": "5b0444a892cac8a91ce4ac63",+ "name": "Item 1",+ "desc": "",+ "due": null,+ "idChecklists": []+ },+ {+ "id": "5b0444aa28c4fade8972564a",+ "name": "Item 2",+ "desc": "",+ "due": null,+ "idChecklists": [+ "5b04450ff88a64205bf04939",+ "5b0445196d92125a52c5af19",+ "5b044524cd09c4cfcf17a7e1"+ ]+ },+ {+ "id": "5b0444acb5d32e68affe0fbc",+ "name": "Item 3",+ "desc": "",+ "due": null,+ "idChecklists": []+ },+ {+ "id": "5b0444bba2b49880d3cf5ef4",+ "name": "Item 4",+ "desc": "",+ "due": null,+ "idChecklists": []+ }+ ]+ },+ {+ "id": "5b04449c4a149b7886795834",+ "name": "List 2",+ "cards": [+ {+ "id": "5b0444be54ac75e4315f8c75",+ "name": "Item 1",+ "desc": "",+ "due": null,+ "idChecklists": [+ "5b0444def123ada312ce7b61"+ ]+ },+ {+ "id": "5b0444bfcf2ae87de87efff6",+ "name": "Item 2",+ "desc": "",+ "due": null,+ "idChecklists": []+ },+ {+ "id": "5b0444c05b48f5803fb6fbe6",+ "name": "Item 3",+ "desc": "",+ "due": null,+ "idChecklists": []+ }+ ]+ },+ {+ "id": "5b04449e55eeab1a57d78284",+ "name": "List 3",+ "cards": [+ {+ "id": "5b0444c56eb336a91504b5e4",+ "name": "Item 1",+ "desc": "",+ "due": null,+ "idChecklists": []+ },+ {+ "id": "5b0444c60af8bc226f9efcd9",+ "name": "Item 2",+ "desc": "",+ "due": null,+ "idChecklists": []+ },+ {+ "id": "5b0444c875057fa8ab6e516c",+ "name": "Item 3",+ "desc": "",+ "due": null,+ "idChecklists": [+ "5b0444f2ac1dcba5f3803f70",+ "5b0444fcaafc8fd6f5a49071"+ ]+ },+ {+ "id": "5b0444ca8650ff18224ea71d",+ "name": "Item 4",+ "desc": "",+ "due": null,+ "idChecklists": []+ },+ {+ "id": "5b0444cc368a78a8bded6c90",+ "name": "Item 5",+ "desc": "",+ "due": null,+ "idChecklists": []+ }+ ]+ },+ {+ "id": "5b0444a16c17cd686e07b76c",+ "name": "List 4",+ "cards": [+ {+ "id": "5b0444d02e30881629179287",+ "name": "Item 1",+ "desc": "",+ "due": null,+ "idChecklists": []+ }+ ]+ },+ {+ "id": "5b0444a4a3565ea909f506ee",+ "name": "List 5",+ "cards": [+ {+ "id": "5b0444d34f5a66ec48672bdb",+ "name": "Item 1",+ "desc": "",+ "due": null,+ "idChecklists": []+ },+ {+ "id": "5b0444d522f50607e73595b1",+ "name": "Item 2",+ "desc": "",+ "due": null,+ "idChecklists": []+ }+ ]+ }+]
+ test/Taskell/UI/FieldTest.hs view
@@ -0,0 +1,171 @@+module Taskell.UI.FieldTest+ ( test_field+ ) where++import ClassyPrelude++import Taskell.UI.Draw.Field (Field (Field), backspace, cursorPosition, insertCharacter, insertText,+ updateCursor, wrap)++import Test.Tasty+import Test.Tasty.HUnit++width :: Int+width = 30++cursorPosition' :: Text -> Int -> (Int, Int)+cursorPosition' text cursor = cursorPosition wrapped width (cursor - offset)+ where+ (wrapped, offset) = wrap width text++test_field :: TestTree+test_field =+ testGroup+ "UI.Draw.Field"+ [ testGroup+ "Field cursor left"+ [ testCase+ "Standard"+ (assertEqual+ "Should move left"+ (Field "Blah" 1)+ (updateCursor (-1) (Field "Blah" 2)))+ , testCase+ "Out of bounds"+ (assertEqual+ "Should stay 0"+ (Field "Blah" 0)+ (updateCursor (-1) (Field "Blah" 0)))+ ]+ , testGroup+ "Field cursor right"+ [ testCase+ "Standard"+ (assertEqual+ "Should move right"+ (Field "Blah" 3)+ (updateCursor 1 (Field "Blah" 2)))+ , testCase+ "At end"+ (assertEqual+ "Should move right"+ (Field "Blah" 4)+ (updateCursor 1 (Field "Blah" 3)))+ , testCase+ "Out of bounds"+ (assertEqual "Should stay" (Field "Blah" 4) (updateCursor 1 (Field "Blah" 4)))+ ]+ , testGroup+ "Field insert character"+ [ testCase+ "At end"+ (assertEqual+ "Should insert character at end"+ (Field "Blahs" 5)+ (insertCharacter 's' (Field "Blah" 4)))+ , testCase+ "At beginning"+ (assertEqual+ "Should insert character at beginning"+ (Field "sBlah" 1)+ (insertCharacter 's' (Field "Blah" 0)))+ ]+ , testGroup+ "Field backspace"+ [ testCase+ "At end"+ (assertEqual+ "Should remove last character"+ (Field "Bla" 3)+ (backspace (Field "Blah" 4)))+ , testCase+ "At beginning"+ (assertEqual+ "Should stay the same"+ (Field "Blah" 0)+ (backspace (Field "Blah" 0)))+ , testCase+ "In middle"+ (assertEqual "Should remove middle" (Field "Bah" 1) (backspace (Field "Blah" 2)))+ ]+ , testGroup+ "Field insert text"+ [ testCase+ "At end"+ (assertEqual+ "Should insert text at end"+ (Field "Blah hello" 10)+ (insertText " hello" (Field "Blah" 4)))+ , testCase+ "At beginning"+ (assertEqual+ "Should insert text at beginning"+ (Field "Hello Blah" 6)+ (insertText "Hello " (Field "Blah" 0)))+ ]+ , testGroup+ "Cursor position"+ [ testCase+ "Empty"+ (assertEqual "Should be at beginning" (0, 0) (cursorPosition' "" 0))+ , testCase+ "First line"+ (assertEqual+ "Should be on first line"+ (14, 0)+ (cursorPosition' "Blah blah blah" 14))+ , testCase+ "Half way along"+ (assertEqual+ "Should be on first line"+ (7, 0)+ (cursorPosition' "Blah blah blah" 7))+ , testCase+ "End of line"+ (assertEqual+ "Should be on first line"+ (29, 0)+ (cursorPosition' "Blah blah blah blah blah blah" 29))+ , testCase+ "End of line wrap"+ (assertEqual+ "Should wrap"+ (0, 1)+ (cursorPosition' "Blah blah blah blah blah blahs" 30))+ , testCase+ "End of line with space wrap"+ (assertEqual+ "Should wrap"+ (0, 1)+ (cursorPosition' "Blah blah blah blah blah blah " 30))+ , testCase+ "Long words"+ (assertEqual+ "Should wrap to correct position"+ (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))+ ]+ ]
− test/UI/FieldTest.hs
@@ -1,174 +0,0 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}--module UI.FieldTest- ( test_field- ) where--import ClassyPrelude--import UI.Draw.Field (Field (Field), backspace, cursorPosition, insertCharacter, insertText,- updateCursor, wrap)--import Test.Tasty-import Test.Tasty.HUnit--width :: Int-width = 30--cursorPosition' :: Text -> Int -> (Int, Int)-cursorPosition' text cursor = cursorPosition wrapped width (cursor - offset)- where- (wrapped, offset) = wrap width text--test_field :: TestTree-test_field =- testGroup- "UI.Draw.Field"- [ testGroup- "Field cursor left"- [ testCase- "Standard"- (assertEqual- "Should move left"- (Field "Blah" 1)- (updateCursor (-1) (Field "Blah" 2)))- , testCase- "Out of bounds"- (assertEqual- "Should stay 0"- (Field "Blah" 0)- (updateCursor (-1) (Field "Blah" 0)))- ]- , testGroup- "Field cursor right"- [ testCase- "Standard"- (assertEqual- "Should move right"- (Field "Blah" 3)- (updateCursor 1 (Field "Blah" 2)))- , testCase- "At end"- (assertEqual- "Should move right"- (Field "Blah" 4)- (updateCursor 1 (Field "Blah" 3)))- , testCase- "Out of bounds"- (assertEqual "Should stay" (Field "Blah" 4) (updateCursor 1 (Field "Blah" 4)))- ]- , testGroup- "Field insert character"- [ testCase- "At end"- (assertEqual- "Should insert character at end"- (Field "Blahs" 5)- (insertCharacter 's' (Field "Blah" 4)))- , testCase- "At beginning"- (assertEqual- "Should insert character at beginning"- (Field "sBlah" 1)- (insertCharacter 's' (Field "Blah" 0)))- ]- , testGroup- "Field backspace"- [ testCase- "At end"- (assertEqual- "Should remove last character"- (Field "Bla" 3)- (backspace (Field "Blah" 4)))- , testCase- "At beginning"- (assertEqual- "Should stay the same"- (Field "Blah" 0)- (backspace (Field "Blah" 0)))- , testCase- "In middle"- (assertEqual "Should remove middle" (Field "Bah" 1) (backspace (Field "Blah" 2)))- ]- , testGroup- "Field insert text"- [ testCase- "At end"- (assertEqual- "Should insert text at end"- (Field "Blah hello" 10)- (insertText " hello" (Field "Blah" 4)))- , testCase- "At beginning"- (assertEqual- "Should insert text at beginning"- (Field "Hello Blah" 6)- (insertText "Hello " (Field "Blah" 0)))- ]- , testGroup- "Cursor position"- [ testCase- "Empty"- (assertEqual "Should be at beginning" (0, 0) (cursorPosition' "" 0))- , testCase- "First line"- (assertEqual- "Should be on first line"- (14, 0)- (cursorPosition' "Blah blah blah" 14))- , testCase- "Half way along"- (assertEqual- "Should be on first line"- (7, 0)- (cursorPosition' "Blah blah blah" 7))- , testCase- "End of line"- (assertEqual- "Should be on first line"- (29, 0)- (cursorPosition' "Blah blah blah blah blah blah" 29))- , testCase- "End of line wrap"- (assertEqual- "Should wrap"- (0, 1)- (cursorPosition' "Blah blah blah blah blah blahs" 30))- , testCase- "End of line with space wrap"- (assertEqual- "Should wrap"- (0, 1)- (cursorPosition' "Blah blah blah blah blah blah " 30))- , testCase- "Long words"- (assertEqual- "Should wrap to correct position"- (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))- ]- ]