todos 0.2 → 0.3
raw patch · 42 files changed
+2440/−1851 lines, 42 filesdep +dyre
Dependencies added: dyre
Files
- CmdLine.hs +0/−260
- Color.hs +0/−102
- CommandParser.hs +0/−26
- Config.hs +0/−88
- ConstrSet.hs +0/−40
- Dates.hs +0/−301
- Dot.hs +0/−57
- IO.hs +0/−20
- Makefile +3/−1
- README +79/−24
- README.ru +54/−3
- Shapes.hs +0/−42
- TodoLoader.hs +0/−104
- TodoParser.hs +0/−150
- TodoTree.hs +0/−153
- Todos.hs +25/−0
- Todos/CmdLine.hs +297/−0
- Todos/Color.hs +145/−0
- Todos/CommandParser.hs +26/−0
- Todos/Config.hs +79/−0
- Todos/ConfigInstances.hs +16/−0
- Todos/ConfigUtils.hs +92/−0
- Todos/Dates.hs +303/−0
- Todos/Dot.hs +95/−0
- Todos/Formatters.hs +122/−0
- Todos/IO.hs +19/−0
- Todos/Loader.hs +227/−0
- Todos/Main.hs +61/−0
- Todos/Parser.hs +146/−0
- Todos/ParserTypes.hs +10/−0
- Todos/Print.hs +79/−0
- Todos/ReadConfig.hs +85/−0
- Todos/Shapes.hs +44/−0
- Todos/Tree.hs +103/−0
- Todos/Types.hs +247/−0
- Todos/Unicode.hs +27/−0
- Types.hs +0/−373
- Unicode.hs +0/−13
- test.hs +0/−23
- test.txt +0/−11
- todos.cabal +53/−9
- todos.hs +3/−51
− CmdLine.hs
@@ -1,260 +0,0 @@-{-# LANGUAGE UnicodeSyntax, PatternGuards #-}---- | Module for parsing command line options and build queries-module CmdLine- (parseCmdLine,- glob,- buildQuery,- composeAll,- usage)- where--import Prelude hiding (putStrLn,readFile,getContents,print)-import IO-import Codec.Binary.UTF8.String-import System (getArgs)-import System.Console.GetOpt-import System.FilePath.Glob-import Data.Maybe-import Data.List (sort)-import Control.Monad.Reader--import Unicode-import Types-import TodoTree-import Dates (parseDate)---- | Default limit for tree height-pruneByDefault ∷ Limit-pruneByDefault = Limit 20---- | Compose predicate from Composed-compose ∷ DateTime -- ^ Current date/time- → Composed -- ^ Composed query- → (TodoItem → 𝔹)-compose _ Empty = const True-compose _ (Pred NoFilter) = const True-compose _ (Pred (Tag s)) = tagPred s-compose _ (Pred (Name s)) = grepPred s-compose _ (Pred (Description s)) = descPred s-compose _ (Pred (Status s)) = statusPred s-compose _ (Pred (IdIs s)) = idPred s-compose dt (Pred (StartDateIs d)) = datePred startDate dt d-compose dt (Pred (EndDateIs d)) = datePred endDate dt d-compose dt (Pred (DeadlineIs d)) = datePred deadline dt d-compose dt (Not p) = not ∘ (compose dt p)-compose dt (And (Pred NoFilter) p) = compose dt p-compose dt (And p (Pred NoFilter)) = compose dt p-compose dt (And p1 p2) = \item → (compose dt p1 item) ∧ (compose dt p2 item)-compose dt (Or (Pred NoFilter) p) = compose dt p-compose dt (Or p (Pred NoFilter)) = compose dt p-compose dt (Or p1 p2) = \item → (compose dt p1 item) ∨ (compose dt p2 item)-compose _ x = error $ show x--concatMapM ∷ (Monad m) ⇒ m (t → [t]) → m ([t] → [t])-concatMapM = liftM concatMap---- | Make a list transformer-composeAll ∷ DateTime → ListTransformer-composeAll date = do- pred ← asks ((compose date) ∘ query)- concatMapM (pruneSelector pred)--appendC ∷ Composed → QueryFlag → Composed-appendC (Not (Pred NoFilter)) f = Not (Pred f)-appendC Empty OrCons = (Pred NoFilter) `Or` (Pred NoFilter)-appendC Empty AndCons = (Pred NoFilter) `And` (Pred NoFilter)-appendC Empty NotCons = Not (Pred NoFilter)-appendC Empty f = Pred f-appendC c NoFilter = c-appendC c AndCons = c `And` (Pred NoFilter)-appendC c OrCons = c `Or` (Pred NoFilter)-appendC c NotCons = c `And` (Pred NoFilter)-appendC (And c (Pred NoFilter)) f = c `And` (Pred f) -appendC (And (Pred NoFilter) c) f = c `And` (Pred f) -appendC c@(And _ _) f = c `And` (Pred f)-appendC (Or c (Pred NoFilter)) f = c `Or` (Pred f)-appendC (Or (Pred NoFilter) c) f = c `Or` (Pred f)-appendC c@(Or _ _) f = c `Or` (Pred f)-appendC c@(Pred _) f = c `And` (Pred f)-appendC c f = c `And` (Pred f)--appendF (O q m o l) (QF f) = O (f:q) m o l-appendF (O q m o l) (MF f) = O q (f:m) o l-appendF (O q m o l) (OF f) = O q m (f:o) l-appendF (O q m o l) (LF f) = O q m o (f:l)-appendF _ HelpF = Help--parseFlags ∷ [CmdLineFlag] → Options-parseFlags lst | HelpF ∈ lst = Help-parseFlags [] = O [] [] [] []-parseFlags (f:fs) = (parseFlags fs) `appendF` f---- | Build Config (with query etc) from Options-buildQuery ∷ Options → Config-buildQuery (O qflags mflags oflags lflags) = Config onlyFirst colors showIds srt limitP limitM command aprefix dformat noStatus setStatus composedFlags - where- composedFlags = parseQuery qflags- (limitP,limitM) = parseLimits lflags-- onlyFirst = OnlyFirst ∈ oflags- colors = Colors ∈ oflags- showIds = Ids ∈ oflags- srtFlags = filter isSort oflags- srt | null srtFlags = DoNotSort - | otherwise = getSorting (last srtFlags)-- cmdFlags = filter isCommand mflags- command | DotExport ∈ oflags = ShowAsDot- | null cmdFlags = JustShow- | otherwise = SystemCommand $ unExecute (last cmdFlags)-- prefixFlags = filter isPrefix mflags- aprefix | null prefixFlags = Nothing- | otherwise = Just $ unPrefix (last prefixFlags)-- dflags = filter isDescribe mflags- dformat | null dflags = "%d"- | otherwise = unDescribe $ last dflags-- noStatus = not $ null $ filter isNoStatus mflags- newStatusFlags = filter isSetStatus mflags- setStatus | null newStatusFlags = Nothing- | otherwise = Just $ newStatus $ last newStatusFlags-- isSort (Sort _) = True- isSort _ = False- isDescribe (Describe _) = True- isDescribe _ = False- isCommand (Execute _) = True- isCommand _ = False- isPrefix (Prefix _) = True- isPrefix _ = False- isNoStatus DoNotReadStatus = True- isNoStatus _ = False- isSetStatus (SetStatus _) = True- isSetStatus _ = False--parseLimits ∷ [LimitFlag] → (Limit,Limit)-parseLimits flags = (limitP,limitM)- where- pruneFlags = filter isPrune flags- minFlags = filter isMin flags-- limitP' = foldl min Unlimited $ map (Limit ∘ unPrune) pruneFlags- limitP | Unlimited ← limitP' = pruneByDefault- | otherwise = limitP'-- limitM' = foldl max (Limit 0) $ map (Limit ∘ unMin) minFlags- limitM | Unlimited ← limitM' = pruneByDefault- | otherwise = limitM'-- isPrune (Prune _) = True- isPrune _ = False-- isMin (Start x) = True- isMin _ = False--parseQuery ∷ [QueryFlag] → Composed-parseQuery flags = foldl appendC Empty flags---- | Parse command line-parseCmdLine ∷ DateTime -- ^ Current date/time- → [String] -- ^ Command line args- → (Options, [FilePath]) -- ^ (Options, list of files)-parseCmdLine currDate args = - case getOpt Permute (options currDate) (map decodeString args) of- (flags, [], []) → (parseFlags flags, ["TODO"])- (flags, nonOpts, []) → (parseFlags flags, nonOpts)- (_, _, msgs) → error $ concat msgs ⧺ usage--isPattern s = ('*' ∈ s) || ('?' ∈ s)--glob ∷ [FilePath] → IO [FilePath]-glob list = do- let patterns = filter isPattern list- files = filter (not ∘ isPattern) list- (matches, _) ← globDir (map compile patterns) "." - return $ sort $ files ⧺ concat matches--usage ∷ String-usage = usageInfo header (options undefined)- where - header = "Usage: todos [OPTION...] [INPUT FILES]"--options ∷ DateTime → [OptDescr CmdLineFlag]-options currDate = [- Option "1" ["only-first"] (NoArg (OF OnlyFirst)) "show only first matching entry",- Option "c" ["color"] (NoArg (OF Colors)) "show colored output",- Option "I" ["show-ids"] (NoArg (OF Ids)) "show IDs of todos",- Option "A" ["prefix"] (OptArg mkPrefix "PREFIX") "use alternate parser: read only lines starting with PREFIX",- Option "" ["dot"] (NoArg (OF DotExport)) "output entries in DOT (graphviz) format",- Option "D" ["describe"] (OptArg mkDescribe "FORMAT") "use FORMAT for descriptions",- Option "w" ["no-status"] (NoArg (MF DoNotReadStatus)) "do not read status field from TODOs",- Option "" ["set-status"] (ReqArg mkSetStatus "STRING") "force all TODOs status to be equal to STRING",- Option "p" ["prune"] (ReqArg mkPrune "N") "limit tree height to N",- Option "m" ["min-depth"] (ReqArg mkMin "N") "show first N levels of tree unconditionally",- Option "t" ["tag"] (ReqArg mkTag "TAG") "find items marked with TAG",- Option "g" ["grep"] (ReqArg mkName "PATTERN") "find items with PATTERN in name",- Option "G" ["description"] (ReqArg mkDescr "PATTERN") "find items with PATTERN in description",- Option "s" ["status"] (ReqArg mkStatus "STRING") "find items with status equal to STRING",- Option "i" ["id"] (ReqArg mkIdQ "STRING") "find items with ID equal to STRING",- Option "a" ["and"] (NoArg (QF AndCons)) "logical AND",- Option "o" ["or"] (NoArg (QF OrCons)) "logical OR",- Option "n" ["not"] (NoArg (QF NotCons)) "logical NOT",- Option "" ["sort"] (ReqArg mkSort "FIELD") "specify sorting",- Option "e" ["exec"] (OptArg mkExecute "COMMAND") "run COMMAND on each matching entry",- Option "S" ["start-date"] (ReqArg (mkStartDate currDate) "DATE") "find items with start date bounded with DATE",- Option "E" ["end-date"] (ReqArg (mkEndDate currDate) "DATE") "find items with end date bounded with DATE",- Option "d" ["deadline"] (ReqArg (mkDeadline currDate) "DATE") "find items with deadline bounded with DATE",- Option "h" ["help"] (NoArg HelpF) "display this help"- ]--mkSort s = OF $ Sort $ readSort s--mkTag ∷ String → CmdLineFlag-mkTag t = QF $ Tag t--mkName ∷ String → CmdLineFlag-mkName n = QF $ Name n--mkStatus ∷ String → CmdLineFlag-mkStatus s = QF $ Status s--mkIdQ ∷ String → CmdLineFlag-mkIdQ s = QF $ IdIs s--mkDescr ∷ String → CmdLineFlag-mkDescr s = QF $ Description s--forceEither ∷ (Show t) ⇒ Either t b → b-forceEither (Right x) = x-forceEither (Left x) = error $ show x--mkStartDate ∷ DateTime → String → CmdLineFlag-mkStartDate dt s = QF $ StartDateIs $ forceEither $ parseDate emptyConfig dt s--mkEndDate ∷ DateTime → String → CmdLineFlag-mkEndDate dt s = QF $ EndDateIs $ forceEither $ parseDate emptyConfig dt s--mkDeadline ∷ DateTime → String → CmdLineFlag-mkDeadline dt s = QF $ DeadlineIs $ forceEither $ parseDate emptyConfig dt s--mkDescribe ∷ Maybe String → CmdLineFlag-mkDescribe Nothing = MF $ Describe "%d"-mkDescribe (Just f) = MF $ Describe f--mkSetStatus ∷ String → CmdLineFlag-mkSetStatus st = MF $ SetStatus st--mkPrune ∷ String → CmdLineFlag-mkPrune s = LF $ Prune (read s)--mkMin ∷ String → CmdLineFlag-mkMin s = LF $ Start (read s)--mkPrefix ∷ Maybe [Char] → CmdLineFlag-mkPrefix = MF ∘ Prefix ∘ fromMaybe "TODO:"--mkExecute ∷ Maybe [Char] → CmdLineFlag-mkExecute = MF ∘ Execute ∘ fromMaybe "echo %n %d"
− Color.hs
@@ -1,102 +0,0 @@-{-# LANGUAGE UnicodeSyntax #-}-module Color where--import qualified Data.Map as M-import Text.Printf-import Data.Word-import Data.Hash--import Unicode-import Types--data HSV = HSV {- colorHue ∷ Double,- colorSaturation ∷ Double,- colorValue ∷ Double }- deriving (Eq)--instance Show HSV where- show (HSV h s v) = printf "\"%.3f %.3f %.3f\"" h s v--(<#>) ∷ HSV → HSV → HSV-(HSV h1 s1 v1) <#> (HSV h2 s2 v2) = HSV ((h1+h2)/2) ((s1+s2)/2) ((v1+v2)/2)--tagHues ∷ M.Map String Double-tagHues = M.fromList $ [- ("BUG", 0.07),- ("NOTE", 0.25),- ("ERROR", 0.0),- ("TODO", 0.55)]--hashAsDouble ∷ Hashable a ⇒ a → Double-hashAsDouble x = (fromIntegral $ asWord64 $ hash x) / (fromIntegral (maxBound :: Word64))--tagHue ∷ String → Double-tagHue tag =- case M.lookup tag tagHues of- Nothing → hashAsDouble tag- Just h → h--statusSats ∷ M.Map String Double-statusSats = M.fromList $ [- ("*", 1.0),- ("URGENT", 1.0),- ("+", 0.9),- ("W", 0.9),- ("?", 0.8),- ("Q", 0.8),- ("M", 0.7),- ("L", 0.6),- ("DONE", 0.5),- ("FIXED", 0.5),- ("WORKSFORME", 0.45),- (":", 0.4),- ("-", 0.3),- ("INVALID", 0.3),- ("o", 0.3),- ("x", 0.2)]--statusSat ∷ String → Double-statusSat st = - case M.lookup st statusSats of- Nothing → 0.4* hashAsDouble st- Just s → s--statusHues ∷ M.Map String Double-statusHues = M.fromList $ [- ("*", 0.0),- ("URGENT", 0.0),- ("+", 0.16),- ("W", 0.08),- ("?", 0.7),- ("Q", 0.8),- ("M", 0.2),- ("L", 0.6),- ("DONE", 0.25),- ("FIXED", 0.25),- ("NOTE", 0.3),- ("WORKSFORME", 0.1),- ("INVALID", 0.45),- (":", 0.4),- ("-", 0.09),- ("o", 0.8),- ("x", 0.2)]--statusHue ∷ String → Double-statusHue st =- case M.lookup st statusHues of- Nothing → hashAsDouble st- Just h → h--getColor ∷ TodoItem → HSV-getColor item = HSV h s v - where- tags = itemTags item- n = length tags- st = itemStatus item- h = case n of- 0 → statusHue st- _ → (statusHue st + sum (map tagHue tags))/fromIntegral (n+1)- s = statusSat st- v = 0.8-
− CommandParser.hs
@@ -1,26 +0,0 @@-{-# LANGUAGE UnicodeSyntax #-}--module CommandParser where--import Unicode-import Types---- | Format item info-printfItem ∷ String -- ^ Format string- → TodoItem - → String-printfItem pattern item = printf pattern- where- printf "" = ""- printf [x] = [x]- printf ('%':c:xs) = (itemPart c) ⧺ printf xs- printf (x:xs) = x:printf xs-- itemPart 'L' = show $ itemLevel item- itemPart 'n' = itemName item- itemPart 't' = unwords $ itemTags item- itemPart 's' = itemStatus item- itemPart 'd' = itemDescr item- itemPart 'f' = fileName item- itemPart 'l' = show $ lineNr item- itemPart x = [x]
− Config.hs
@@ -1,88 +0,0 @@-{-# LANGUAGE UnicodeSyntax #-}---- | Module for parsing config files-module Config- (readConfig)- where--import Prelude hiding (putStrLn,readFile,getContents,print)-import IO-import System.Environment-import System.FilePath -import System.Directory (doesFileExist)-import Data.Maybe-import Data.Either-import Text.ParserCombinators.Parsec--import Unicode-import Types--word ∷ Parser String-word = choice $ map try [quotedOption, simpleOption, quoted, simpleWord]--simpleWord = many1 $ noneOf " \t\r\n=\"'"--quotedOption = (try quotedLongOption) <|> quotedShortOption--quotedLongOption ∷ Parser String-quotedLongOption = do- string "--"- o ← simpleWord- char '='- v ← quoted- return ("--" ⧺ o ⧺ "=" ⧺ v)--quotedShortOption ∷ Parser String-quotedShortOption = do- string "-"- o ← simpleWord- v ← quoted- return ("-" ⧺ o ⧺ v)--simpleOption = do- o ← simpleWord- optional $ char '='- v ← simpleWord- return (o ⧺ "=" ⧺ v)--quoted = quoted1 <|> quoted2--quoted1 = do- char '\''- s ← many1 $ noneOf "'"- char '\''- return s--quoted2 = do- char '"'- s ← many1 $ noneOf "\""- char '"'- return s--pConfig ∷ Parser [String]-pConfig = word `sepBy` space--parseConfig ∷ String → [String]-parseConfig str = - case parse pConfig "config file" str of- Right lst → lst- Left err → error $ show err--readFile' ∷ FilePath → IO [String]-readFile' path = - do b ← doesFileExist path- if not b- then return []- else do- str ← readFile path- return $ parseConfig (unwords $ lines str)---- | Read list of options from config files-readConfig ∷ IO [String]-readConfig = do- home ← getEnv "HOME"- let homepath = home </> ".config" </> "todos"- homecfg ← readFile' homepath- localcfg ← readFile' ".todos.conf"- return $ homecfg ⧺ localcfg-
− ConstrSet.hs
@@ -1,40 +0,0 @@-{-# LANGUAGE UnicodeSyntax, DeriveDataTypeable, NoMonomorphismRestriction #-}--module ConstrSet - (CSet,- empty,- fromList, toList,- insert, append,- elemC,- selectByConstrOf,- selectByConstrOf'- ) where--import Data.Generics-import Data.Function (on)-import Data.List (nubBy)-import Data.Maybe (fromMaybe)--teq = (==) `on` toConstr--data (Data a) ⇒ CSet a = CSet [a]--empty = CSet []--fromList lst = CSet $ nubBy teq lst-toList (CSet lst) = lst--insert item (CSet []) = CSet [item]-insert item (CSet lst) = CSet $ nubBy teq (item:lst)--selectByConstrOf (CSet []) _ = Nothing-selectByConstrOf (CSet (x:xs)) y | x `teq` y = Just x- | otherwise = selectByConstrOf (CSet xs) y--selectByConstrOf' def cset x = fromMaybe def $ selectByConstrOf cset x--elemC _ (CSet []) = False-elemC x (CSet (y:ys)) | x `teq` y = True- | otherwise = elemC x (CSet ys)--append (CSet xs) (CSet ys) = CSet $ nubBy teq (xs++ys)
− Dates.hs
@@ -1,301 +0,0 @@-{-# LANGUAGE UnicodeSyntax #-}--- | Operations with dates-module Dates- (parseDate, getCurrentDateTime,- pSpecDates)- where--import Data.Char (toUpper)-import Data.Function (on)-import Data.List-import Data.Time.Calendar-import Data.Time.Clock-import Data.Time.LocalTime-import Text.ParserCombinators.Parsec--import Types-import Unicode--getCurrentDateTime ∷ IO DateTime-getCurrentDateTime = do- zt ← getZonedTime- let lt = zonedTimeToLocalTime zt- ld = localDay lt- ltod = localTimeOfDay lt- (y,m,d) = toGregorian ld- h = todHour ltod- min = todMin ltod- s = round $ todSec ltod- return $ DateTime (fromIntegral y) m d h min s--uppercase ∷ String → String-uppercase = map toUpper--isPrefixOfI ∷ String → String → Bool-p `isPrefixOfI` s = (uppercase p) `isPrefixOf` (uppercase s)--lookupS ∷ String → [(String,a)] → Maybe a-lookupS _ [] = Nothing-lookupS k ((k',v):other) | k `isPrefixOfI` k' = Just v- | otherwise = lookupS k other--monthsN ∷ [(String,Int)]-monthsN = zip months [1..]--lookupMonth ∷ String → Maybe Int-lookupMonth n = lookupS n monthsN--date ∷ Int → Int → Int → DateTime-date y m d = DateTime y m d 0 0 0--addTime ∷ DateTime → Time → DateTime-addTime dt t = dt {- hour = tHour t + hour dt,- minute = tMinute t + minute dt,- second = tSecond t + second dt }--times ∷ Int → TParser t → TParser [t]-times 0 _ = return []-times n p = do- ts ← times (n-1) p- t ← optionMaybe p- case t of- Just t' → return (ts ++ [t'])- Nothing → return ts- -number ∷ Int → Int → TParser Int-number n m = do- t ← read `fmap` (n `times` digit)- if t > m- then fail "number too large"- else return t--pYear ∷ TParser Int-pYear = do- y ← number 4 10000- if y < 2000- then return (y+2000)- else return y--pMonth ∷ TParser Int-pMonth = number 2 12--pDay ∷ TParser Int-pDay = number 2 31--euroNumDate ∷ TParser DateTime-euroNumDate = do- d ← pDay- char '.'- m ← pMonth- char '.'- y ← pYear- return $ date y m d--americanDate ∷ TParser DateTime-americanDate = do- y ← pYear- char '/'- m ← pMonth- char '/'- d ← pDay- return $ date y m d--euroNumDate' ∷ Int → TParser DateTime-euroNumDate' year = do- d ← pDay- char '.'- m ← pMonth- return $ date year m d--americanDate' ∷ Int → TParser DateTime-americanDate' year = do- m ← pMonth- char '/'- d ← pDay- return $ date year m d--strDate ∷ TParser DateTime-strDate = do- d ← pDay- space- ms ← many1 letter- case lookupMonth ms of- Nothing → fail $ "unknown month: "++ms- Just m → do- space- y ← pYear- notFollowedBy $ char ':'- return $ date y m d--strDate' ∷ Int → TParser DateTime-strDate' year = do- d ← pDay- space- ms ← many1 letter- case lookupMonth ms of- Nothing → fail $ "unknown month: "++ms- Just m → return $ date year m d--time24 ∷ TParser Time-time24 = do- h ← number 2 23- char ':'- m ← number 2 59- x ← optionMaybe $ char ':'- case x of- Nothing → return $ Time h m 0- Just _ → do- s ← number 2 59- notFollowedBy letter- return $ Time h m s--ampm ∷ TParser Int-ampm = do- s ← many1 letter- case map toUpper s of- "AM" → return 0- "PM" → return 12- _ → fail "AM/PM expected"--time12 ∷ TParser Time-time12 = do- h ← number 2 12- char ':'- m ← number 2 59- x ← optionMaybe $ char ':'- s ← case x of- Nothing → return 0- Just s' → number 2 59- optional space- hd ← ampm- return $ Time (h+hd) m s--pAbsDate ∷ Int → TParser DateTime-pAbsDate year = do- date ← choice $ map try $ map ($ year) $ [- const euroNumDate,- const americanDate,- const strDate,- strDate',- euroNumDate',- americanDate']- optional $ char ','- s ← optionMaybe space- case s of- Nothing → return date- Just _ → do- t ← choice $ map try [time12,time24]- return $ date `addTime` t--data DateIntervalType = Day | Week | Month | Year- deriving (Eq,Show,Read)--data DateInterval = Days ℤ- | Weeks ℤ- | Months ℤ- | Years ℤ- deriving (Eq,Show)--convertTo ∷ DateTime → Day-convertTo dt = fromGregorian (fromIntegral $ year dt) (month dt) (day dt)--convertFrom ∷ Day → DateTime-convertFrom dt = - let (y,m,d) = toGregorian dt- in date (fromIntegral y) m d--modifyDate ∷ (t → Day → Day) → t → DateTime → DateTime-modifyDate fn x dt = convertFrom $ fn x $ convertTo dt--addInterval ∷ DateTime → DateInterval → DateTime-addInterval dt (Days ds) = modifyDate addDays ds dt-addInterval dt (Weeks ws) = modifyDate addDays (ws*7) dt-addInterval dt (Months ms) = modifyDate addGregorianMonthsClip ms dt-addInterval dt (Years ys) = modifyDate addGregorianYearsClip ys dt--maybePlural ∷ String → TParser String-maybePlural str = do- r ← string str- optional $ char 's'- return (capitalize r)--pDateInterval ∷ TParser DateIntervalType-pDateInterval = do- s ← choice $ map maybePlural ["day", "week", "month", "year"]- return $ read s--pRelDate ∷ DateTime → TParser DateTime-pRelDate date = do- offs ← (try futureDate) <|> (try passDate) <|> (try today) <|> (try tomorrow) <|> yesterday- return $ date `addInterval` offs--futureDate ∷ TParser DateInterval-futureDate = do- string "in "- n ← many1 digit- char ' '- tp ← pDateInterval- case tp of- Day → return $ Days (read n)- Week → return $ Weeks (read n)- Month → return $ Months (read n)- Year → return $ Years (read n)--passDate ∷ TParser DateInterval-passDate = do- n ← many1 digit- char ' '- tp ← pDateInterval- string " ago"- case tp of- Day → return $ Days $ - (read n)- Week → return $ Weeks $ - (read n)- Month → return $ Months $ - (read n)- Year → return $ Years $ - (read n)--today ∷ TParser DateInterval-today = do- string "today"- return $ Days 0--tomorrow ∷ TParser DateInterval-tomorrow = do- string "tomorrow"- return $ Days 1--yesterday ∷ TParser DateInterval-yesterday = do- string "yesterday"- return $ Days (-1)--pDate ∷ DateTime → TParser DateTime-pDate date = (try $ pRelDate date) <|> (try $ pAbsDate $ year date)--dateType ∷ String → DateType-dateType "start" = StartDate-dateType "end" = EndDate-dateType "deadline" = Deadline-dateType _ = error "unknown date type"--pSpecDate ∷ DateTime → TParser (DateType, DateTime)-pSpecDate date = do- tp ← choice $ map string ["start","end","deadline"]- string ": "- dt ← pDate date- return (dateType tp, dt)--pSpecDates ∷ DateTime → TParser [(DateType, DateTime)]-pSpecDates date = do- char '('- pairs ← (pSpecDate date) `sepBy1` (string "; ")- string ") "- return pairs---- | Parse date/time-parseDate ∷ Config- → DateTime -- ^ Current date/time- → String -- ^ String to parse- → Either ParseError DateTime-parseDate conf date s = runParser (pDate date) conf "" s-
− Dot.hs
@@ -1,57 +0,0 @@-{-# LANGUAGE UnicodeSyntax, TypeSynonymInstances, FlexibleInstances #-}-module Dot- (showAsDot)- where--import Data.List-import Data.Tree-import Text.Printf--import Unicode-import Types-import Color-import Shapes--data Dot = Dot {- dotVertices ∷ [TodoItem],- dotEdges ∷ [(TodoItem, TodoItem)]- }--toDot ∷ Todo → Dot-toDot todo = Dot (getVertices todo) (getEdges todo)--getVertices ∷ Todo → [TodoItem]-getVertices (Node item forest) =- [item] ⧺ concatMap getVertices forest--getEdges ∷ Todo → [(TodoItem, TodoItem)]-getEdges (Node item forest) =- [(item, rootLabel child) | child ← forest] ⧺ concatMap getEdges forest--instance Show Dot where- show (Dot vs es) = "digraph Todo {\n"- ⧺ unlines (map showDotNode vs)- ⧺ unlines (map showDotEdge es)- ⧺ "}\n"--showD ∷ [Dot] → String-showD dots = "digraph Todo {\n"- ⧺ " rankdir = \"RL\";\n"- ⧺ " node [shape=\"box\", style=\"filled\"];\n"- ⧺ unlines (map showDotNode $ nub $ sort $ concatMap dotVertices dots)- ⧺ unlines (map showDotEdge $ nub $ sort $ concatMap dotEdges dots)- ⧺ "}\n"--makeName ∷ TodoItem → String-makeName item = "\"" ⧺ makeId item ⧺ "\""--showDotNode ∷ TodoItem → String-showDotNode item =- printf " %s [label=\"%s\\n%s\\n%s\", fillcolor=%s, shape=\"%s\"];" (makeName item) (itemStatus item) (unwords $ itemTags item) (itemName item) (show $ getColor item) (show $ getShape item)--showDotEdge ∷ (TodoItem, TodoItem) → String-showDotEdge (x,y) = printf " %s -> %s;" (makeName y) (makeName x)--showAsDot ∷ [Todo] → String-showAsDot todos = showD (map toDot todos)-
− IO.hs
@@ -1,20 +0,0 @@-{-# LANGUAGE CPP #-}--- | Wrapper to support unicode IO with GHC 6.10 and 6.12-module IO -#if __GLASGOW_HASKELL__ < 612- (module System.IO.UTF8)-#else- (module Prelude)-#endif- where--#if __GLASGOW_HASKELL__ < 612-- import Prelude hiding (putStr, putStrLn,readFile,getContents,print)- import System.IO.UTF8--#else-- import Prelude--#endif
Makefile view
@@ -5,4 +5,6 @@ $(GHC) $(GHCFLAGS) --make todos.hs clean:- rm -f *.hi *.o *~+ find . -name \*.hi -delete+ find . -name \*.o -delete+ find . -name \*~ -delete
README view
@@ -28,8 +28,9 @@ (see below), «TAGS» is a comma-separated list of tags in brackets, «title» is title of record, «depends» — comma-separated list of depends (titles of other records) in parenthesis, «description» is description of the record. All fields-except status and title are optional. Description is separated from title with-not less than 2 spaces.+except status and title are optional. If «-w» option is specified, status field+is not read (it's optional; when it's present, it will be part of title).+Description is separated from title with not less than 2 spaces. Using different indents one can create sub-records (and, so, trees of records). Moreover, depends are interpreted in same way as sub-records. So, one can@@ -40,37 +41,57 @@ Dates information is described in parenthesis, as not more than 3 records, separated with semicolon. Each records has format «date type: date». Date type can be one of: start, end, deadline. For dates, many formats are supported; for-example, «18 Feb», или «02/18/2010», «2010/02/18» are all valid dates. One can+example, «18 Feb», or «02/18/2010», «2010/02/18» are all valid dates. One can even use relative dates specification here, such as «in 2 weeks» or «2 days ago», but there is no point to this: all dates are counted from program start date. Next commandline options are supported: - -1 --only-first show only first matching entry- -c --color show colored output- -A[PREFIX] --prefix[=PREFIX] use alternate parser: read only lines starting with PREFIX- -D[FORMAT] --describe[=FORMAT] use FORMAT for descriptions- -p N --prune=N limit tree height to N- -m N --min-depth=N show first N levels of tree unconditionally- -t TAG --tag=TAG find items marked with TAG- -g PATTERN --grep=PATTERN find items with PATTERN in name- -s STRING --status=STRING find items with status equal to STRING- -a --and logical AND- -o --or logical OR- -n --not logical NOT- --sort=FIELD specify sorting- -e[COMMAND] --exec[=COMMAND] run COMMAND on each matching entry- -S DATE --start-date=DATE find items with start date bounded with DATE- -E DATE --end-date=DATE find items with end date bounded with DATE- -d DATE --deadline=DATE find items with deadline bounded with DATE- -h --help display this help+ -1 --only-first show only first matching entry+ -c --color show colored output+ -H --highlight instead of filtering TODOs, just highlight matching the query+ -I --show-ids show IDs of todos+ -A[PREFIX] --prefix[=PREFIX] use alternate parser: read only lines starting with PREFIX+ --dot output entries in DOT (graphviz) format+ -D[FORMAT] --describe[=FORMAT] use FORMAT for descriptions+ -w --no-status do not read status field from TODOs+ --set-status=STRING force all TODOs status to be equal to STRING+ --set-root-status=STRING force statuses of root TODOs to be equal to STRING+ -F --by-file group TODOs by source file+ -T --by-tag group TODOs by tag+ -Z --by-status group TODOs by status+ -p N --prune=N limit tree height to N+ -m N --min-depth=N show first N levels of tree unconditionally+ -t TAG --tag=TAG find items marked with TAG+ -g PATTERN --grep=PATTERN find items with PATTERN in name+ -G PATTERN --description=PATTERN find items with PATTERN in description+ -s STRING --status=STRING find items with status equal to STRING+ -i STRING --id=STRING find items with ID equal to STRING+ -a --and logical AND+ -o --or logical OR+ -n --not logical NOT+ --sort=FIELD specify sorting+ -e[COMMAND] --exec[=COMMAND] run COMMAND on each matching entry+ -S DATE --start-date=DATE find items with start date bounded with DATE+ -E DATE --end-date=DATE find items with end date bounded with DATE+ -d DATE --deadline=DATE find items with deadline bounded with DATE+ -h --help display this help Dates in the commandline are specified in any of supported formats. There relative dates specifications are useful: how about something like «todos --deadline=yesterday»? ;) -For --describe and --exec options, printf-style format strings should be+When «-H» option is specified, items are not filtered at all (all items will be+printed). Instead, items matching to your query will be highlighted. This works+only with «-c» option.++When «-I» option is specified, for each TODO it's unique identifier is shown.+That ID is simply hash from item's title, status, tags and description, so+only identical items will have identical ID. One can ask todos to show item+with specified ID using «-i» option.++For «--describe» and «--exec» options, printf-style format strings should be specified. Following substitution sequences are supported: %n :: title of the record@@ -83,9 +104,43 @@ For example, «todos -tBUG -e"vi %f +%l"» command for each record with «BUG» tag will open vi with corresponding file on corresponding line. -todos can read configuration files: ~/.config/todos («global») and-./.todos.conf («local»). Config files contain command line options, in the same+When «--dot» option is specified, todos will output a graph definition for DOT+(graphviz), instead of normal output. For example, you can try++ todos --dot | dot -Tpng -o todos.png++and then see the file todos.png. Items with status «GROUP» will become DOT's+clusters. More precisely, if one item have «GROUP» status, it will become a+cluster, and all it's children will be inside that cluster. There is a+limitation in DOT: it can not draw graphs where one item is in several clusters+properly, it will draw such item only in one cluster.++todos can read configuration files: `~/.config/todos/todos.conf` («global») and+`./.todos.conf` («local»). Config files contain command line options, in the same format as in the commandline. Resulting commandline is composed as: (options in global config) + (options in local config) + (options in the actual command line). If there is no one of configs, todos will ignore it.++Moreover, todos could be customized in «xmonad way». You write file+`~/.config/todos/todos.hs` and run todos. todos will compile and run that file.+Simplest example of `todos.hs` possible is `todos.hs` in this directory. A bit+more complex example:++.................................................................+ import System.Console.ANSI++ import Todos++ main :: IO ()+ main = todos $ defaultConfig {+ itemConsoleColor = myItemColor+ }++ myItemColor item =+ if itemStatus item == ":"+ then Just (Dull, Blue)+ else Nothing+.................................................................++So, items with ":" status will be printed in blue color.
README.ru view
@@ -27,7 +27,8 @@ информация о датах (см. ниже), TAGS — список тегов через пробел в квадратных скобках, title — заголовок записи, depends — список зависимостей (заголовков других записей) в скобках через запятую, description — описание записи. Все-поля кроме статуса и заголовка необязательны. Описание отделяется от заголовка+поля кроме статуса и заголовка необязательны. В случае, если указана опция+«-w», поле статуса не читается. Описание отделяется от заголовка не менее чем двумя пробелами. С помощью разных отступов можно создавать вложенные записи. Кроме того,@@ -51,13 +52,24 @@ -1, --only-first :: показывать только первую запись из всех, что должны быть показаны -c, --color :: раскрашивать вывод todos (заголовки записей — полужирным, некоторые статусы выделяются цветом)+-H, --highlight :: подсвечивать подходящие записи, вместо того чтобы фильтровать их+-I, --show-ids :: показывать идентификаторы записей -A, --prefix= :: использовать альтернативный формат файлов, тут же указывается префикс+--dot :: вывести записи в виде графа для DOT (graphviz) -D, --describe :: можно указать формат для выводимого описания (см. ниже)+-w, --no-status :: не читать поле статуса из файлов+--set-status=STRING :: установить статус всех TODO равным STRING+--set-root-status=STRING :: установить статусы корневых TODO равным STRING+-F, --by-file :: группировать записи по исходному файлу+-T, --by-tag :: группировать записи по тегам+-Z, --by-status :: группировать записи по статусам -p, --prune :: ограничить максимальную высоту дерева (для каждой из отобранных записей будет показано не более чем N уровней вложенности) -m, --min-depth :: ограничить минимальную высоту дерева -t, --tag :: вывести только записи с заданным тегом и их вложенные записи -g, --grep :: искать по заголовкам записей+-G, --description :: искать по описаниям записей -s, --status :: искать по статусам записей+-i, --id :: искать по идентификаторам записей -a, --and :: логическое И; например, todos -s! -a -tBUG — искать записи со статусом «!» и тегом «BUG» -o, --or :: логическое ИЛИ; -n, --not :: логическое НЕ;@@ -75,6 +87,10 @@ «меньше или равно»; например, todos --start-date="in 2 weeks" будет искать записи с датой начала через 2 недели или раньше (в т.ч. и в прошлом). +С указанной опцией «-H» записи не фильтруются совсем. Вместо этого те записи,+которые подходят под ваш запрос, будут подсвечены цветом (только с включённой+опцией «-c»).+ Для опций --describe и --exec указываются строки формата с символами подстановки в стиле printf. Поддерживаются следующие символы подстановки: @@ -88,9 +104,44 @@ Например, todos -tBUG -e"vi %f +%l" для каждой записи с тегом BUG откроет vi на строчке с этой записью. -todos может читать конфигурационные файлы: ~/.config/todos (глобальный) и-.todos.conf в текущей директории (локальный). Конфигурационные файлы содержат+С опцией «--dot» todos выведет на стандартный выход описание графа для DOT+(graphviz) вместо обычного вывода. Можно попробовать, например, так:++ todos --dot | dot -Tpng -o todos.png++и см. получившийся файл todos.png. Записи со статусом «GROUP» становятся+кластерами. Точнее говоря, если какая-то запись имеет статус «GROUP», она будет+отображаться как кластер, а все её дочерние записи будут находиться внутри+этого кластера. При этом есть одно ограничение, накладываемое DOT: DOT не умеет+правильно рисовать графы, в которых один узел находится сразу в нескольких+кластерах; в таких случаях узел будет нарисован только в одном кластере.++todos может читать конфигурационные файлы: `~/.config/todos/todos.conf` (глобальный) и+`.todos.conf` в текущей директории (локальный). Конфигурационные файлы содержат ключи командной строки. «Эффективная командная строка» составляется как (ключи в глобальном конфиге) + (ключи в локальном конфиге) + ключи в командной строке. Если какого-то из конфигов нет, todos это проигнорирует.++Кроме того, todos можно настраивать под себя «в стиле xmonad». Вы пишете файл+`~/.config/todos/todos.hs` и запускаете todos. todos компилирует и запускает+тот файл. Простейший возможный пример этого `todos.hs` — это файл `todos.hs` в+этой директории. Вот чуть более сложный пример:++.................................................................+ import System.Console.ANSI++ import Todos++ main :: IO ()+ main = todos $ defaultConfig {+ itemConsoleColor = myItemColor+ }++ myItemColor item =+ if itemStatus item == ":"+ then Just (Dull, Blue)+ else Nothing+.................................................................++Таким образом, записи со статусом «:» будут напечатаны синим цветом.
− Shapes.hs
@@ -1,42 +0,0 @@-{-# LANGUAGE UnicodeSyntax #-}-module Shapes where--import qualified Data.Map as M--import Unicode-import Types--data Shape = - Box - | Ellipse- | Diamond- | DCircle- | Note- | Parallelogram- | Folder- deriving (Eq)--instance Show Shape where- show Box = "box"- show Ellipse = "ellipse"- show Diamond = "diamond"- show DCircle = "doublecircle"- show Note = "note"- show Parallelogram = "parallelogram"- show Folder = "folder"--shapes ∷ M.Map String Shape-shapes = M.fromList $ [- ("o", Ellipse),- ("O", DCircle),- (":", Folder),- ("*", Diamond),- ("/", Parallelogram),- ("NOTE", Note) ]--getShape ∷ TodoItem → Shape-getShape item = - case M.lookup (itemStatus item) shapes of- Nothing → Box- Just s → s-
− TodoLoader.hs
@@ -1,104 +0,0 @@-{-# LANGUAGE UnicodeSyntax, PatternGuards #-}--- | Read TODOs from files and construct corresponding ADTs.-module TodoLoader- (loadTodo)- where--import Prelude hiding (putStrLn,readFile,getContents,print)-import IO-import Control.Monad (forM)-import qualified Data.Map as M-import Text.ParserCombinators.Parsec-import Data.Maybe-import Data.Tree--import Unicode-import Types-import TodoParser--todoName ∷ Todo → String-todoName todo = itemName ⋄ rootLabel todo--getDepends ∷ TodoMap → TodoItem → [Todo]-getDepends m item = catMaybes [M.lookup name m | name ← depends item] --normalizeItem ∷ TodoMap → TodoItem → Todo-normalizeItem m item = Node item (map (normalize m) ⋄ getDepends m item)--normalize ∷ TodoMap → Todo → Todo -normalize m todo = Node item' ((map (normalize m) subTodos) ⧺ (map (normalize m) deps))- where- item ∷ TodoItem- item = rootLabel todo-- item' ∷ TodoItem- item' = item {depends=[]}-- subTodos ∷ [Todo]- subTodos = subForest todo-- deps ∷ [Todo]- deps = getDepends m item--normalizeList ∷ TodoMap → [Todo] → [Todo]-normalizeList m todos = map (normalize m) todos--readFile' ∷ FilePath → IO String-readFile' "-" = getContents-readFile' file = readFile file--loadFile ∷ Config- → DateTime- → FilePath- → IO [TodoItem]-loadFile conf year path =- case prefix conf of- Nothing → do- text ← readFile' path- return $ parsePlain conf year path text- Just p → do- text ← readFile' path- return $ parseAlternate conf 2 p year path text--(~-) ∷ TodoItem → ℤ → TodoItem-i@(Item {itemLevel=n}) ~- k = i {itemLevel=n-k}--iszero ∷ TodoItem → 𝔹-iszero item = (itemLevel item)==0--group' ∷ [TodoItem] → [[TodoItem]]-group' [] = []-group' (x:xs) = let (one,other) = break iszero xs- in (x:one):group' other--mkTodo ∷ [TodoItem] → [Todo]-mkTodo = (map mkTodo') ∘ group'--mkTodo' ∷ [TodoItem] → Todo-mkTodo' (x:xs) = Node x other- where other = mkTodo ⋄ map (~-lvl) xs- lvl = itemLevel (head xs)- -consTodoMap ∷ [Todo] → TodoMap-consTodoMap todos = M.fromList (cons1 100 todos)- where- cons1 ∷ Int → [Todo] → [(String,Todo)]- cons1 0 _ = []- cons1 max trees = [(todoName todo, todo) | todo ← trees] ⧺ cons1 (max-1) (children trees)- children ∷ [Todo] → [Todo]- children trees = concatMap subForest trees--stitchTodos ∷ [TodoItem] → [Todo]-stitchTodos items = - let m = consTodoMap t- t = mkTodo items- in normalizeList m t---- | Load list of TODO trees from files-loadTodo ∷ Config- → DateTime -- ^ Current date/time- → [FilePath] -- ^ List of files- → IO [Todo]-loadTodo conf date paths = do- tss ← forM paths (loadFile conf date)- return $ stitchTodos (concat tss)
− TodoParser.hs
@@ -1,150 +0,0 @@-{-# LANGUAGE UnicodeSyntax, NoMonomorphismRestriction, TypeSynonymInstances, DeriveDataTypeable #-}-module TodoParser- (parsePlain, parseAlternate)- where--import Prelude hiding (putStrLn,readFile,getContents,print)-import IO-import Control.Monad-import Data.List-import Text.ParserCombinators.Parsec-import qualified Data.Map as M-import Data.Tree-import Data.Maybe-import Data.Char-import Data.Function--import Unicode-import Types-import Dates--strip ∷ String → String-strip = reverse ∘ p ∘ reverse ∘ p- where- p = dropWhile isSpace--pSpace ∷ TParser Char-pSpace = oneOf " \t"--pSpace' ∷ TParser String-pSpace' = do- pSpace- return " "--pSpaces ∷ TParser String-pSpaces = many pSpace--pDeps ∷ TParser [String]-pDeps = do- string "("- ws ← (many1 ⋄ noneOf ",)\n\r") `sepBy` (char ',')- string ")"- return $ map strip ws--pTags ∷ TParser [String]-pTags = do- ts ← between (char '[') (char ']') $ word `sepBy1` pSpace- pSpaces- return ts- where- word = many1 (noneOf " \t\n\r]")--pItem ∷ DateTime → TParser TodoItem-pItem date = do- pos ← getPosition- s ← pSpaces- conf ← getState- stat ← if skipStatus conf- then case forcedStatus conf of- Just fs → return fs- Nothing → return "*"- else do - rs ← pWord- case forcedStatus conf of- Just fs → return fs- Nothing → return rs- dates ← (try (pSpecDates date) <|> return [])- tags ← (try pTags <|> return [])- namew ← many1 pWord- pSpaces- deps ← (try pDeps <|> return [])- pSpaces- descr ← many (noneOf "\n\r")- pSpaces- many ⋄ oneOf "\n\r"- return ⋄ Item {- itemLevel = fromIntegral $ length s,- itemName = unwords namew,- itemTags = tags,- depends = deps,- itemStatus = stat,- itemDescr = descr,- startDate = lookup StartDate dates,- endDate = lookup EndDate dates,- deadline = lookup Deadline dates,- fileName = sourceName pos,- lineNr = sourceLine pos }--pWord ∷ TParser String-pWord = do- w ← many1 (noneOf " \t\n\r")- (try pSpace') <|> (return w)- return w--pItems ∷ DateTime → TParser [TodoItem]-pItems date = do- its ← many (pItem date)- eof- return its--unwords' ∷ String → [String] → String-unwords' prefix lst =- let (hd:tl) = map (filter (/='\r')) lst- addLines = filter (not ∘ (prefix `isPrefixOf`)) tl- in case addLines of- [] → hd- _ → hd ⧺ " {" ++ (unwords addLines) ++ "}"--filterN ∷ (Num a, Enum a) ⇒ Int → String → [String] → ([a], [String])-filterN n prefix lst = - let zipped = zip [0..] lst- good = filter (isGood ∘ snd) zipped- lns = map fst good- sub k l = (take l) ∘ (drop k)- ans = map (unwords' prefix) [sub j n lst | j ← lns]- isGood x = prefix `isPrefixOf` x- cut = drop (1+length prefix) - in (map (+1) lns, map cut ans)--filterJoin ∷ Int → String → String → ([Int], String)-filterJoin n prefix str = - let (ns, lns) = filterN n prefix (lines str)- in (ns, unlines lns)---- | Read list of TODO items from plain format -parsePlain ∷ Config- → DateTime -- ^ Current date/time- → SourceName -- ^ Source file name- → String -- ^ String to parse- → [TodoItem]-parsePlain conf date path text = - case runParser (pItems date) conf path text of- Right items → items- Left e → error $ show e---- | Read list of TODO items from alternate format-parseAlternate ∷ Config - → Int -- ^ Number of lones after matching to include to item's description- → String -- ^ Prefix to match- → DateTime -- ^ Current date/time- → SourceName -- ^ Source file name- → String -- ^ String to parse- → [TodoItem]-parseAlternate conf next prefix date path text = - let (ns, filtered) = filterJoin next prefix text- renumber lst = zipWith renumber1 ns lst- renumber1 n item = item {lineNr=n}- in case runParser (pItems date) conf path filtered of- Right items → renumber items- Left e → error $ show e-
− TodoTree.hs
@@ -1,153 +0,0 @@-{-# LANGUAGE UnicodeSyntax, NoMonomorphismRestriction, FlexibleInstances, TypeSynonymInstances #-}-module TodoTree - (delTag,- pruneSelector,- tagPred, statusPred, grepPred, descPred, datePred, idPred,- forT, mapT,- printTodos)- where--import Prelude hiding (putStrLn,readFile,getContents,print)-import IO-import System.Console.ANSI-import Control.Monad-import Control.Monad.Reader-import qualified Data.Map as M-import Data.Generics-import Data.List-import Data.Function (on)-import Data.Tree-import Data.Maybe-import Text.Regex.PCRE-import Data.Hash-import Numeric--import Types-import TodoLoader-import Unicode--sortBy' s | s == DoNotSort = id- | otherwise = sortBy sorter- where- sorter = compare `on` (f ∘ rootLabel)- f = case s of- ByTitle → itemName- ByStatus → itemStatus- ByTags → unwords ∘ itemTags- ByStartDate → show ∘ startDate- ByEndDate → show ∘ endDate- ByDeadline → show ∘ deadline --showT ∷ SortingType → Int → Todo → [Formatter]-showT s n (Node item todos) = - (startFormat <++> showId item <++> replicate n ' ' <++> configShow item) :- (concatMap (showT s (n+2)) $ sortBy' s todos)- where- showId :: TodoItem → Formatter- showId item = do- s ← asks outIds- c ← asks outColors- if s- then if c - then return [OutSetColor Yellow, OutString $ makeId item ++ " ", ResetAll]- else return [OutString $ makeId item ++ " "]- else return [OutString ""]--unlines'' ∷ [Formatter] → Formatter-unlines'' lst = concat `fmap` (sequence $ intersperse newLine lst)--showTodo ∷ Todo → Formatter-showTodo t = do- conf ← ask- let f = case outOnlyFirst conf of- False → unlines''- True → head- f $ showT (sorting conf) 0 t--showTodos ∷ [Todo] → Formatter-showTodos lst = do- conf ← ask- let f = case outOnlyFirst conf of- False → unlines''- True → head- f $ map showTodo $ sortBy' (sorting conf) $ nub lst--printTodos ∷ Config → [Todo] → IO ()-printTodos conf lst = - let lst' = runReader (showTodos lst) conf- in mapM_ outItem lst'--mapTags ∷ (Data a) ⇒ ([String] → [String]) → [a] → [a]-mapTags f = map ⋄ everywhere ⋄ mkT changeTags- where- changeTags item@(Item {itemTags=ts}) = item {itemTags = f ts}- -addTag ∷ (Data a) ⇒ String → [a] → [a]-addTag t = mapTags (t:)--delTag ∷ (Data a) ⇒ String → [a] → [a]-delTag t = mapTags (delete t)--pruneSelector ∷ (TodoItem → 𝔹) → Transformer-pruneSelector pred = do- (Limit n) ← asks pruneL- (Limit m) ← asks minL- return $ pruneSelector' n m pred- -pruneSelector' ∷ ℤ → ℤ → (TodoItem → 𝔹) → (Todo → [Todo])-pruneSelector' n m pred = select n 0 False- where- select k t b (Node item trees) | t < m = [Node item ⋄ concatMap (select (n-1) (t+1) True) trees]- | pred item = [Node item ⋄ concatMap (select (n-1) (t+1) True) trees]- | (k > 0) ∧ b = [Node item ⋄ concatMap (select (k-1) (t+1) True) trees]- | k > 0 = concatMap (select (k-1) (t+1) False) trees- | otherwise = [] --addS ∷ (Show a) ⇒ a → TodoItem → TodoItem-addS s item@(Item {itemName=name}) = item {itemName = name ⧺ " — " ⧺ show s}--tagPred ∷ String → TodoItem → 𝔹-tagPred tag = \item → tag ∈ itemTags item--statusPred ∷ String → TodoItem → 𝔹-statusPred st = \item → st == itemStatus item- -grepPred ∷ String → TodoItem → 𝔹-grepPred pattern = \item → itemName item =~ pattern--descPred ∷ String → TodoItem → 𝔹-descPred pattern = \item → itemDescr item =~ pattern--idPred :: String → TodoItem → 𝔹-idPred hash = \item → makeId item == hash--isLT ∷ (Ord t) ⇒ Maybe t → t → 𝔹-isLT Nothing _ = False-isLT (Just x) y = x <= y--isGT ∷ (Ord t) ⇒ Maybe t → t → 𝔹-isGT Nothing _ = False-isGT (Just x) y = x >= y--datePred ∷ (Ord a) ⇒ (t → Maybe a) → a → a → t → 𝔹-datePred selector curr dt | dt >= curr = \item → selector item `isLT` dt- | otherwise = \item → selector item `isGT` dt--flattern ∷ [Todo] → [Todo]-flattern = concatMap flat- where- flat ∷ Todo → [Todo]- flat (Node item trees) = (Node item []):(concatMap flat trees)--forT ∷ (Monad m, Eq t) ⇒ [Tree t] → (t → m a) → m [b]-forT todos f = forM (nub todos) forT'- where- forT' (Node item trees) =- do f item- res ← forM trees forT'- return $ last res--mapT ∷ (t → t) → [Tree t] → [Tree t]-mapT f todos = map mapT' todos- where- mapT' (Node item trees) = Node (f item) (mapT f trees)
+ Todos.hs view
@@ -0,0 +1,25 @@+module Todos + (module Todos.Unicode,+ module Todos.Types,+ module Todos.Loader,+ module Todos.Tree,+ module Todos.CommandParser,+ module Todos.Config,+ module Todos.ConfigUtils,+ module Todos.CmdLine,+ module Todos.Dot,+ module Todos.Main,+ getCurrentDateTime) where++import Todos.Unicode+import Todos.Types+import Todos.Loader+import Todos.Tree+import Todos.CommandParser+import Todos.Config+import Todos.ConfigUtils+import Todos.CmdLine+import Todos.Dates (getCurrentDateTime)+import Todos.Dot+import Todos.Main+
+ Todos/CmdLine.hs view
@@ -0,0 +1,297 @@+{-# LANGUAGE UnicodeSyntax, PatternGuards #-}++-- | Module for parsing command line options and build queries+module Todos.CmdLine+ (parseCmdLine',+ glob,+ buildQuery,+ compose,+ usage)+ where++import Prelude hiding (putStrLn,readFile,getContents,print)+import Todos.IO+import System.Console.GetOpt+import System.FilePath.Glob+import Data.Maybe+import Data.List (sort)++import Todos.Unicode+import Todos.Types+import Todos.Tree+import Todos.Config+import Todos.Dates (parseDate)++-- | Compose predicate from Composed+compose ∷ DateTime -- ^ Current date/time+ → Composed -- ^ Composed query+ → (TodoItem → 𝔹)+compose _ Empty = const True+compose _ (Pred NoFilter) = const True+compose _ (Pred (Tag s)) = tagPred s+compose _ (Pred (Name s)) = grepPred s+compose _ (Pred (Description s)) = descPred s+compose _ (Pred (Status s)) = statusPred s+compose _ (Pred (IdIs s)) = idPred s+compose dt (Pred (StartDateIs d)) = datePred startDate dt d+compose dt (Pred (EndDateIs d)) = datePred endDate dt d+compose dt (Pred (DeadlineIs d)) = datePred deadline dt d+compose dt (Not p) = not ∘ (compose dt p)+compose dt (And (Pred NoFilter) p) = compose dt p+compose dt (And p (Pred NoFilter)) = compose dt p+compose dt (And p1 p2) = \item → (compose dt p1 item) ∧ (compose dt p2 item)+compose dt (Or (Pred NoFilter) p) = compose dt p+compose dt (Or p (Pred NoFilter)) = compose dt p+compose dt (Or p1 p2) = \item → (compose dt p1 item) ∨ (compose dt p2 item)+compose _ x = error $ show x++appendC ∷ Composed → QueryFlag → Composed+appendC (Not (Pred NoFilter)) f = Not (Pred f)+appendC Empty OrCons = (Pred NoFilter) `Or` (Pred NoFilter)+appendC Empty AndCons = (Pred NoFilter) `And` (Pred NoFilter)+appendC Empty NotCons = Not (Pred NoFilter)+appendC Empty f = Pred f+appendC c NoFilter = c+appendC c AndCons = c `And` (Pred NoFilter)+appendC c OrCons = c `Or` (Pred NoFilter)+appendC c NotCons = c `And` (Pred NoFilter)+appendC (And c (Pred NoFilter)) f = c `And` (Pred f) +appendC (And (Pred NoFilter) c) f = c `And` (Pred f) +appendC c@(And _ _) f = c `And` (Pred f)+appendC (Or c (Pred NoFilter)) f = c `Or` (Pred f)+appendC (Or (Pred NoFilter) c) f = c `Or` (Pred f)+appendC c@(Or _ _) f = c `Or` (Pred f)+appendC c@(Pred _) f = c `And` (Pred f)+appendC c f = c `And` (Pred f)++appendF ∷ Options → CmdLineFlag → Options+appendF (O q m o l) (QF f) = O (f:q) m o l+appendF (O q m o l) (MF f) = O q (f:m) o l+appendF (O q m o l) (OF f) = O q m (f:o) l+appendF (O q m o l) (LF f) = O q m o (f:l)+appendF _ HelpF = Help+appendF Help _ = Help++parseFlags ∷ [CmdLineFlag] → Options+parseFlags lst | HelpF ∈ lst = Help+parseFlags [] = O [] [] [] []+parseFlags (f:fs) = (parseFlags fs) `appendF` f++-- | Build DefaultConfig (with query etc) from Options+buildQuery ∷ BaseConfig -- ^ Default config+ → Options -- ^ Cmdline options+ → DefaultConfig+buildQuery _ Help = error "Internal error: buildQuery does no sense for Help!"+buildQuery dc (O qflags mflags oflags lflags) =+ DConfig {+ baseConfig = BConfig {+ outOnlyFirst = update outOnlyFirst onlyFirst,+ outColors = update outColors colors,+ outIds = update outIds showIds,+ outHighlight = update outHighlight highlight,+ sorting = update sorting srt,+ pruneL = update pruneL limitP,+ minL = update minL limitM,+ commandToRun = update commandToRun command,+ prefix = update prefix aprefix,+ descrFormat = update descrFormat dformat,+ skipStatus = update skipStatus noStatus,+ groupByFile = update groupByFile doGroupByFile,+ groupByTag = update groupByTag doGroupByTag,+ groupByStatus = update groupByStatus doGroupByStatus,+ forcedStatus = update forcedStatus setStatus,+ topStatus = update topStatus setTopStatus },+ query = fromMaybe Empty composedFlags }+ where+ update fn Nothing = fn dc+ update _ (Just x) = x++ x ? lst | x ∈ lst = Just True+ | otherwise = Nothing++ composedFlags | null qflags = Nothing+ | otherwise = Just $ parseQuery qflags+ (limitP,limitM) | null lflags = (Nothing, Nothing)+ | otherwise = parseLimits (unLimit $ pruneL dc) (unLimit $ minL dc) lflags++ onlyFirst = OnlyFirst ? oflags+ colors = Colors ? oflags+ highlight = Highlight ? oflags+ showIds = Ids ? oflags++ srtFlags = filter isSort oflags+ srt | null srtFlags = Nothing+ | otherwise = Just $ getSorting (last srtFlags)++ doGroupByFile = GroupByFile ? mflags+ doGroupByTag = GroupByTag ? mflags+ doGroupByStatus = GroupByStatus ? mflags++ cmdFlags = filter isCommand mflags+ command | DotExport ∈ oflags = Just $ ShowAsDot+ | null cmdFlags = Nothing+ | otherwise = Just $ SystemCommand $ unExecute (last cmdFlags)++ prefixFlags = filter isPrefix mflags+ aprefix | null prefixFlags = Nothing+ | otherwise = Just $ Just $ unPrefix (last prefixFlags)++ dflags = filter isDescribe mflags+ dformat | null dflags = Nothing+ | otherwise = Just $unDescribe $ last dflags++ noStatus = DoNotReadStatus ? mflags+ newStatusFlags = filter isSetStatus mflags+ setStatus | null newStatusFlags = Nothing+ | otherwise = Just $ Just $ newStatus $ last newStatusFlags++ topStatusFlags = filter isTopStatus mflags+ setTopStatus | null topStatusFlags = Nothing+ | otherwise = Just $ Just $ newTopStatus $ last topStatusFlags++ isSort (Sort _) = True+ isSort _ = False+ isDescribe (Describe _) = True+ isDescribe _ = False+ isCommand (Execute _) = True+ isCommand _ = False+ isPrefix (Prefix _) = True+ isPrefix _ = False+ isSetStatus (SetStatus _) = True+ isSetStatus _ = False+ isTopStatus (SetTopStatus _) = True+ isTopStatus _ = False++parseLimits ∷ ℤ → ℤ → [LimitFlag] → (Maybe Limit,Maybe Limit)+parseLimits dlp dlm flags = (Just limitP, Just limitM)+ where+ pruneFlags = filter isPrune flags+ minFlags = filter isMin flags++ limitP' = foldl min Unlimited $ map (Limit ∘ unPrune) pruneFlags+ limitP | Unlimited ← limitP' = Limit dlp+ | otherwise = limitP'++ limitM' = foldl max (Limit 0) $ map (Limit ∘ unMin) minFlags+ limitM | Unlimited ← limitM' = Limit dlm+ | otherwise = limitM'++ isPrune (Prune _) = True+ isPrune _ = False++ isMin (Start _) = True+ isMin _ = False++parseQuery ∷ [QueryFlag] → Composed+parseQuery flags = foldl appendC Empty flags++-- | Parse command line+parseCmdLine' ∷ DateTime -- ^ Current date/time+ → [String] -- ^ Command line args+ → Either String (Options, [FilePath]) -- ^ Error message or (Options, list of files)+parseCmdLine' currDate args = + case getOpt Permute (options currDate) (map decodeString args) of+ (flags, [], []) → Right (parseFlags flags, ["TODO"])+ (flags, nonOpts, []) → Right (parseFlags flags, nonOpts)+ (_, _, msgs) → Left $ concat msgs ⧺ usage++isPattern s = ('*' ∈ s) || ('?' ∈ s)++-- | For given list of glob masks, return list of matching files+glob ∷ [FilePath] → IO [FilePath]+glob list = do+ let patterns = filter isPattern list+ files = filter (not ∘ isPattern) list+ (matches, _) ← globDir (map compile patterns) "." + return $ sort $ files ⧺ concat matches++-- | Usage help for default command line options+usage ∷ String+usage = usageInfo header (options undefined)+ where + header = "Usage: todos [OPTION...] [INPUT FILES]"++options ∷ DateTime → [OptDescr CmdLineFlag]+options currDate = [+ Option "1" ["only-first"] (NoArg (OF OnlyFirst)) "show only first matching entry",+ Option "c" ["color"] (NoArg (OF Colors)) "show colored output",+ Option "H" ["highlight"] (NoArg (OF Highlight)) "instead of filtering TODOs, just highlight matching the query",+ Option "I" ["show-ids"] (NoArg (OF Ids)) "show IDs of todos",+ Option "A" ["prefix"] (OptArg mkPrefix "PREFIX") "use alternate parser: read only lines starting with PREFIX",+ Option "" ["dot"] (NoArg (OF DotExport)) "output entries in DOT (graphviz) format",+ Option "D" ["describe"] (OptArg mkDescribe "FORMAT") "use FORMAT for descriptions",+ Option "w" ["no-status"] (NoArg (MF DoNotReadStatus)) "do not read status field from TODOs",+ Option "" ["set-status"] (ReqArg mkSetStatus "STRING") "force all TODOs status to be equal to STRING",+ Option "" ["set-root-status"] (ReqArg mkTopStatus "STRING") "force statuses of root TODOs to be equal to STRING",+ Option "F" ["by-file"] (NoArg (MF GroupByFile)) "group TODOs by source file",+ Option "T" ["by-tag"] (NoArg (MF GroupByTag)) "group TODOs by tag",+ Option "Z" ["by-status"] (NoArg (MF GroupByStatus)) "group TODOs by status",+ Option "p" ["prune"] (ReqArg mkPrune "N") "limit tree height to N",+ Option "m" ["min-depth"] (ReqArg mkMin "N") "show first N levels of tree unconditionally",+ Option "t" ["tag"] (ReqArg mkTag "TAG") "find items marked with TAG",+ Option "g" ["grep"] (ReqArg mkName "PATTERN") "find items with PATTERN in name",+ Option "G" ["description"] (ReqArg mkDescr "PATTERN") "find items with PATTERN in description",+ Option "s" ["status"] (ReqArg mkStatus "STRING") "find items with status equal to STRING",+ Option "i" ["id"] (ReqArg mkIdQ "STRING") "find items with ID equal to STRING",+ Option "a" ["and"] (NoArg (QF AndCons)) "logical AND",+ Option "o" ["or"] (NoArg (QF OrCons)) "logical OR",+ Option "n" ["not"] (NoArg (QF NotCons)) "logical NOT",+ Option "" ["sort"] (ReqArg mkSort "FIELD") "specify sorting",+ Option "e" ["exec"] (OptArg mkExecute "COMMAND") "run COMMAND on each matching entry",+ Option "S" ["start-date"] (ReqArg (mkStartDate currDate) "DATE") "find items with start date bounded with DATE",+ Option "E" ["end-date"] (ReqArg (mkEndDate currDate) "DATE") "find items with end date bounded with DATE",+ Option "d" ["deadline"] (ReqArg (mkDeadline currDate) "DATE") "find items with deadline bounded with DATE",+ Option "h" ["help"] (NoArg HelpF) "display this help"+ ]++mkSort s = OF $ Sort $ readSort s++mkTag ∷ String → CmdLineFlag+mkTag t = QF $ Tag t++mkName ∷ String → CmdLineFlag+mkName n = QF $ Name n++mkStatus ∷ String → CmdLineFlag+mkStatus s = QF $ Status s++mkIdQ ∷ String → CmdLineFlag+mkIdQ s = QF $ IdIs s++mkDescr ∷ String → CmdLineFlag+mkDescr s = QF $ Description s++forceEither ∷ (Show t) ⇒ Either t b → b+forceEither (Right x) = x+forceEither (Left x) = error $ show x++mkStartDate ∷ DateTime → String → CmdLineFlag+mkStartDate dt s = QF $ StartDateIs $ forceEither $ parseDate undefined dt s++mkEndDate ∷ DateTime → String → CmdLineFlag+mkEndDate dt s = QF $ EndDateIs $ forceEither $ parseDate undefined dt s++mkDeadline ∷ DateTime → String → CmdLineFlag+mkDeadline dt s = QF $ DeadlineIs $ forceEither $ parseDate undefined dt s++mkDescribe ∷ Maybe String → CmdLineFlag+mkDescribe Nothing = MF $ Describe "%d"+mkDescribe (Just f) = MF $ Describe f++mkSetStatus ∷ String → CmdLineFlag+mkSetStatus st = MF $ SetStatus st++mkTopStatus ∷ String → CmdLineFlag+mkTopStatus st = MF $ SetTopStatus st++mkPrune ∷ String → CmdLineFlag+mkPrune s = LF $ Prune (read s)++mkMin ∷ String → CmdLineFlag+mkMin s = LF $ Start (read s)++mkPrefix ∷ Maybe [Char] → CmdLineFlag+mkPrefix = MF ∘ Prefix ∘ fromMaybe "TODO:"++mkExecute ∷ Maybe [Char] → CmdLineFlag+mkExecute = MF ∘ Execute ∘ fromMaybe "echo %n %d"
+ Todos/Color.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE UnicodeSyntax #-}+module Todos.Color where++import qualified Data.Map as M+import Text.Printf+import Data.Word+import Data.Hash+import Data.List (minimumBy)+import Data.Function (on)+import qualified System.Console.ANSI as ANSI++import Todos.Types++-- | Hue, saturation and value; all are in [0; 1] range.+data HSV = HSV {+ colorHue ∷ Double,+ colorSaturation ∷ Double,+ colorValue ∷ Double }+ deriving (Eq)++instance Show HSV where+ show (HSV h s v) = printf "\"%.3f %.3f %.3f\"" h s v++-- | Correspondence between console colors and HSV colors+consoleColors ∷ [((ANSI.ColorIntensity, ANSI.Color), HSV)]+consoleColors = + [((ANSI.Dull, ANSI.Black), HSV 0 0 0),+ ((ANSI.Dull, ANSI.Red), HSV 0 1 0.67),+ ((ANSI.Dull, ANSI.Green), HSV 0.333 1 0.67),+ ((ANSI.Dull, ANSI.Yellow), HSV (1.0/12.0) 1 0.67),+ ((ANSI.Dull, ANSI.Blue), HSV 0.667 1 0.67),+ ((ANSI.Dull, ANSI.Magenta), HSV (5.0/6.0) 1 0.67),+ ((ANSI.Dull, ANSI.Cyan), HSV 0.5 1 0.67),+ ((ANSI.Dull, ANSI.White), HSV 0 0 0.67),+ ((ANSI.Vivid, ANSI.Black), HSV 0 0 0.33),+ ((ANSI.Vivid, ANSI.Red), HSV 0 0.67 1),+ ((ANSI.Vivid, ANSI.Green), HSV 0.333 0.67 1),+ ((ANSI.Vivid, ANSI.Yellow), HSV (1.0/12.0) 0.67 1),+ ((ANSI.Vivid, ANSI.Blue), HSV 0.667 0.67 1),+ ((ANSI.Vivid, ANSI.Magenta), HSV (5.0/6.0) 0.67 1),+ ((ANSI.Vivid, ANSI.Cyan), HSV 0.5 0.67 1),+ ((ANSI.Vivid, ANSI.White), HSV 0 0 1) ]++-- | Get console color which is nearest to given HSV color+consoleColor ∷ HSV → (ANSI.ColorIntensity, ANSI.Color)+consoleColor hsv = fst $ minimumBy (compare `on` ρ) consoleColors+ where ρ (_, clr) = sum $ map (^2) [colorHue hsv - colorHue clr,+ colorSaturation hsv - colorSaturation clr,+ colorValue hsv - colorValue clr ]++-- | Hue values for some common tags+tagHues ∷ M.Map String Double+tagHues = M.fromList $ [+ ("BUG", 0.07),+ ("NOTE", 0.25),+ ("ERROR", 0.0),+ ("TAG", 0.16),+ ("TODO", 0.55)]++hashAsDouble ∷ Hashable a ⇒ a → Double+hashAsDouble x = (fromIntegral $ asWord64 $ hash x) / (fromIntegral (maxBound :: Word64))++-- | Get color hue from tag name+tagHue ∷ String → Double+tagHue tag =+ case M.lookup tag tagHues of+ Nothing → hashAsDouble tag+ Just h → h++-- | Color saturation values for some common statuses+statusSats ∷ M.Map String Double+statusSats = M.fromList $ [+ ("*", 1.0),+ ("URGENT", 1.0),+ ("+", 0.9),+ ("W", 0.9),+ ("?", 0.8),+ ("Q", 0.8),+ ("M", 0.7),+ ("L", 0.6),+ ("DONE", 0.5),+ ("FIXED", 0.5),+ ("WORKSFORME", 0.45),+ (":", 0.4),+ ("-", 0.3),+ ("INVALID", 0.3),+ ("o", 0.3),+ ("x", 0.2)]++-- | Get color saturation from item status+statusSat ∷ String → Double+statusSat st = + case M.lookup st statusSats of+ Nothing → 0.4* hashAsDouble st+ Just s → s++-- | Color hue values for some common item statuses+statusHues ∷ M.Map String Double+statusHues = M.fromList $ [+ ("*", 0.0),+ ("URGENT", 0.0),+ ("+", 0.16),+ ("W", 0.08),+ ("?", 0.7),+ ("Q", 0.8),+ ("M", 0.2),+ ("L", 0.6),+ ("DONE", 0.25),+ ("FIXED", 0.25),+ ("NOTE", 0.3),+ ("WORKSFORME", 0.1),+ ("INVALID", 0.45),+ (":", 0.4),+ ("-", 0.09),+ ("o", 0.8),+ ("x", 0.2)]++-- | Get color hue from item status+statusHue ∷ String → Double+statusHue st =+ case M.lookup st statusHues of+ Nothing → hashAsDouble st+ Just h → h++-- | Get console color for item status+statusColor ∷ String → (ANSI.ColorIntensity, ANSI.Color)+statusColor st = consoleColor $ HSV (statusHue st) (statusSat st) 0.2++-- | Get console color for item name (this is const Nothing)+defItemConsoleColor ∷ TodoItem → Maybe (ANSI.ColorIntensity, ANSI.Color)+defItemConsoleColor _ = Nothing++-- | Get color for item (this is used in DOT output)+getColor ∷ TodoItem → HSV+getColor item = HSV h s v + where+ tags = itemTags item+ n = length tags+ st = itemStatus item+ h = case n of+ 0 → statusHue st+ _ → (statusHue st + sum (map tagHue tags))/fromIntegral (n+1)+ s = statusSat st+ v = 0.8+
+ Todos/CommandParser.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE UnicodeSyntax #-}++module Todos.CommandParser where++import Todos.Unicode+import Todos.Types++-- | Format item info+printfItem ∷ String -- ^ Format string+ → TodoItem + → String+printfItem pattern item = printf pattern+ where+ printf "" = ""+ printf [x] = [x]+ printf ('%':c:xs) = (itemPart c) ⧺ printf xs+ printf (x:xs) = x:printf xs++ itemPart 'L' = show $ itemLevel item+ itemPart 'n' = itemName item+ itemPart 't' = unwords $ itemTags item+ itemPart 's' = itemStatus item+ itemPart 'd' = itemDescr item+ itemPart 'f' = fileName item+ itemPart 'l' = show $ lineNr item+ itemPart x = [x]
+ Todos/Config.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE UnicodeSyntax, MultiParamTypeClasses #-}++module Todos.Config where++import Control.Monad.Reader++import Todos.Unicode+import Todos.Types+import Todos.Color+import Todos.Shapes+import qualified System.Console.ANSI as ANSI++-- | Any user-specified runtime config type should belong to this class+class RuntimeConfig c where+ -- | Does given TODO item match query?+ getPredicate ∷ DateTime → c → (TodoItem → 𝔹) + -- | Get basic configuration+ toBaseConfig ∷ c → BaseConfig++-- | Any user-specified runtime config type should include at least this properties+data BaseConfig = BConfig {+ outOnlyFirst ∷ 𝔹, -- ^ Output only first matching entry+ outColors ∷ 𝔹, -- ^ Show colored output+ outIds :: 𝔹, -- ^ Show IDs+ outHighlight ∷ 𝔹, -- ^ Highlight matching items+ sorting ∷ SortingType, -- ^ How to sort items+ pruneL ∷ Limit, + minL ∷ Limit,+ commandToRun ∷ TodoCommand,+ prefix ∷ Maybe String, -- ^ Nothing — use default parser, Just p — use alternate parser with prefix «p»+ descrFormat ∷ String,+ skipStatus ∷ 𝔹, -- ^ Skip status field in input+ groupByFile ∷ 𝔹,+ groupByTag ∷ 𝔹,+ groupByStatus ∷ 𝔹,+ forcedStatus ∷ Maybe String,+ topStatus ∷ Maybe String+ }+ deriving (Eq, Show)++-- | Default runtime configuration type. Is read from command line and configs.+data DefaultConfig = DConfig {+ baseConfig ∷ BaseConfig,+ query ∷ Composed }+ deriving (Eq,Show)++-- | Configuration for console output. Is generated in runtime from TodosConfig and Config.+data PrintConfig c = PConfig {+ printConfig ∷ c,+ printStatusColor ∷ String → (ANSI.ColorIntensity, ANSI.Color), -- ^ Color of status field from status+ printItemColor ∷ TodoItem → Maybe (ANSI.ColorIntensity, ANSI.Color), -- ^ Color of item name+ printHighlightColor ∷ (ANSI.ColorIntensity, ANSI.Color), -- ^ Color to use for highlighting+ doHighlight ∷ TodoItem → 𝔹 -- ^ Whether to highlight given item+ }++-- | User Todos config. User can specify it in @~/.config/todos/todos.hs@.+data TodosConfig c = Todos {+ parseCommandLine ∷ DateTime → c → [String] → CmdLineParseResult c, -- ^ Function to parse command line+ filterTodos ∷ DateTime → c → [Todo] → [Todo], -- ^ Any function to be run to transform read TODOs tree+ statusConsoleColor ∷ String → (ANSI.ColorIntensity, ANSI.Color), -- ^ Function to select a color of item's status field in console output+ itemConsoleColor ∷ TodoItem → Maybe (ANSI.ColorIntensity, ANSI.Color), -- ^ Function to select a color of item's name in console output+ highlightColor ∷ (ANSI.ColorIntensity, ANSI.Color), -- ^ Color to use for highlighting+ itemColor ∷ TodoItem → HSV, -- ^ Function to select color for item's node in DOT output+ itemShape ∷ TodoItem → Shape, -- ^ Function to select shape for item's node in DOT output+ printTodos ∷ PrintConfig c → [Todo] → IO (), -- ^ Any function to output TODOs list+ nullConfig ∷ c -- ^ Default Config (to be used without any options in command line and configs)+}++-- | Result of parsing command line+data CmdLineParseResult c = + Parsed c [FilePath] -- ^ Parsed successfully, got Config and list of source files+ | ParseError String -- ^ Some error occured+ | CmdLineHelp -- ^ User asked for help+ deriving (Eq,Show)++-- | ask field from BaseConfig+askBase ∷ (RuntimeConfig c) ⇒ (BaseConfig → a) → Reader c a+askBase field = asks (field ∘ toBaseConfig)+
+ Todos/ConfigInstances.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE UnicodeSyntax #-}+-- | This module contains instances of RuntimeConfig class for DefaultConfig and PrintConfig+module Todos.ConfigInstances where++import Todos.Unicode+import Todos.Config+import Todos.CmdLine++instance RuntimeConfig DefaultConfig where+ getPredicate dt conf = compose dt $ query conf+ toBaseConfig = baseConfig++instance (RuntimeConfig c) ⇒ RuntimeConfig (PrintConfig c) where+ getPredicate = const doHighlight+ toBaseConfig = toBaseConfig ∘ printConfig+
+ Todos/ConfigUtils.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE UnicodeSyntax, MultiParamTypeClasses #-}+-- | This module contains some empty configs definitions and some function fields of defaultConfig+module Todos.ConfigUtils where++import System.Console.ANSI++import Todos.Types+import Todos.Config+import Todos.Color+import Todos.Shapes+import Todos.CmdLine+import Todos.Print+import Todos.Tree++-- | Empty BaseConfig+emptyBaseConfig ∷ BaseConfig+emptyBaseConfig = BConfig {+ outOnlyFirst = False,+ outColors = False,+ outIds = False,+ outHighlight = False,+ sorting = DoNotSort,+ pruneL = Limit 20,+ minL = Limit 0,+ commandToRun = JustShow,+ prefix = Nothing,+ descrFormat = "%d",+ skipStatus = False,+ groupByFile = False,+ groupByTag = False,+ groupByStatus = False,+ forcedStatus = Nothing,+ topStatus = Nothing+}++-- | Default empty DefaultConfig (nullConfig field of defaultConfig)+emptyConfig ∷ DefaultConfig+emptyConfig = DConfig {+ baseConfig = emptyBaseConfig,+ query = Empty }++-- | Default Todos config+defaultConfig ∷ TodosConfig DefaultConfig+defaultConfig = Todos {+ parseCommandLine = parseCmdLine,+ filterTodos = defaultTodosFilter,+ statusConsoleColor = statusColor,+ itemConsoleColor = defItemConsoleColor,+ highlightColor = (Vivid, Magenta),+ itemColor = getColor,+ itemShape = getShape,+ printTodos = defaultPrintTodos,+ nullConfig = emptyConfig+}++-- | Make a list transformer+composeAll ∷ DateTime → DefaultConfig → (Todo → [Todo])+composeAll date conf =+ let pred = compose date $ query conf+ bc = baseConfig conf+ in pruneSelector bc pred++-- | Default filter for TODOs (filterTodos field of defaultConfig)+defaultTodosFilter ∷ DateTime → DefaultConfig → [Todo] → [Todo]+defaultTodosFilter dt conf todos =+ let t = delTag "-" todos+ bc = baseConfig conf+ in if outHighlight bc+ then t+ else concatMap (composeAll dt conf) t++-- | Parse command line (default function)+parseCmdLine ∷ DateTime -- ^ Current date/time+ → DefaultConfig -- ^ Default config+ → [String] -- ^ Command line args+ → CmdLineParseResult DefaultConfig+parseCmdLine currDate dc args = + case parseCmdLine' currDate args of+ Right (opts, files) → case opts of+ Help → CmdLineHelp+ _ → Parsed (buildQuery (baseConfig dc) opts) files+ Left str → ParseError str++-- | Prepare PrintConfig for console output functions. Is called from realTodos.+mkPrintConfig ∷ (RuntimeConfig c) ⇒ DateTime → c → TodosConfig c → PrintConfig c+mkPrintConfig dt conf tcfg = PConfig {+ printConfig = conf,+ printStatusColor = statusConsoleColor tcfg,+ printItemColor = itemConsoleColor tcfg,+ printHighlightColor = highlightColor tcfg,+ doHighlight = getPredicate dt conf }+
+ Todos/Dates.hs view
@@ -0,0 +1,303 @@+{-# LANGUAGE UnicodeSyntax #-}+-- | Operations with dates+module Todos.Dates+ (parseDate, getCurrentDateTime,+ pSpecDates)+ where++import Data.Char (toUpper)+import Data.List+import Data.Time.Calendar+import Data.Time.LocalTime+import Text.ParserCombinators.Parsec++import Todos.Types+import Todos.Unicode+import Todos.Config+import Todos.ParserTypes++getCurrentDateTime ∷ IO DateTime+getCurrentDateTime = do+ zt ← getZonedTime+ let lt = zonedTimeToLocalTime zt+ ld = localDay lt+ ltod = localTimeOfDay lt+ (y,m,d) = toGregorian ld+ h = todHour ltod+ min = todMin ltod+ s = round $ todSec ltod+ return $ DateTime (fromIntegral y) m d h min s++uppercase ∷ String → String+uppercase = map toUpper++isPrefixOfI ∷ String → String → Bool+p `isPrefixOfI` s = (uppercase p) `isPrefixOf` (uppercase s)++lookupS ∷ String → [(String,a)] → Maybe a+lookupS _ [] = Nothing+lookupS k ((k',v):other) | k `isPrefixOfI` k' = Just v+ | otherwise = lookupS k other++monthsN ∷ [(String,Int)]+monthsN = zip months [1..]++lookupMonth ∷ String → Maybe Int+lookupMonth n = lookupS n monthsN++date ∷ Int → Int → Int → DateTime+date y m d = DateTime y m d 0 0 0++addTime ∷ DateTime → Time → DateTime+addTime dt t = dt {+ hour = tHour t + hour dt,+ minute = tMinute t + minute dt,+ second = tSecond t + second dt }++times ∷ Int → TParser t → TParser [t]+times 0 _ = return []+times n p = do+ ts ← times (n-1) p+ t ← optionMaybe p+ case t of+ Just t' → return (ts ++ [t'])+ Nothing → return ts+ +number ∷ Int → Int → TParser Int+number n m = do+ t ← read `fmap` (n `times` digit)+ if t > m+ then fail "number too large"+ else return t++pYear ∷ TParser Int+pYear = do+ y ← number 4 10000+ if y < 2000+ then return (y+2000)+ else return y++pMonth ∷ TParser Int+pMonth = number 2 12++pDay ∷ TParser Int+pDay = number 2 31++euroNumDate ∷ TParser DateTime+euroNumDate = do+ d ← pDay+ char '.'+ m ← pMonth+ char '.'+ y ← pYear+ return $ date y m d++americanDate ∷ TParser DateTime+americanDate = do+ y ← pYear+ char '/'+ m ← pMonth+ char '/'+ d ← pDay+ return $ date y m d++euroNumDate' ∷ Int → TParser DateTime+euroNumDate' year = do+ d ← pDay+ char '.'+ m ← pMonth+ return $ date year m d++americanDate' ∷ Int → TParser DateTime+americanDate' year = do+ m ← pMonth+ char '/'+ d ← pDay+ return $ date year m d++strDate ∷ TParser DateTime+strDate = do+ d ← pDay+ space+ ms ← many1 letter+ case lookupMonth ms of+ Nothing → fail $ "unknown month: "++ms+ Just m → do+ space+ y ← pYear+ notFollowedBy $ char ':'+ return $ date y m d++strDate' ∷ Int → TParser DateTime+strDate' year = do+ d ← pDay+ space+ ms ← many1 letter+ case lookupMonth ms of+ Nothing → fail $ "unknown month: "++ms+ Just m → return $ date year m d++time24 ∷ TParser Time+time24 = do+ h ← number 2 23+ char ':'+ m ← number 2 59+ x ← optionMaybe $ char ':'+ case x of+ Nothing → return $ Time h m 0+ Just _ → do+ s ← number 2 59+ notFollowedBy letter+ return $ Time h m s++ampm ∷ TParser Int+ampm = do+ s ← many1 letter+ case map toUpper s of+ "AM" → return 0+ "PM" → return 12+ _ → fail "AM/PM expected"++time12 ∷ TParser Time+time12 = do+ h ← number 2 12+ char ':'+ m ← number 2 59+ x ← optionMaybe $ char ':'+ s ← case x of+ Nothing → return 0+ Just s' → number 2 59+ optional space+ hd ← ampm+ return $ Time (h+hd) m s++pAbsDate ∷ Int → TParser DateTime+pAbsDate year = do+ date ← choice $ map try $ map ($ year) $ [+ const euroNumDate,+ const americanDate,+ const strDate,+ strDate',+ euroNumDate',+ americanDate']+ optional $ char ','+ s ← optionMaybe space+ case s of+ Nothing → return date+ Just _ → do+ t ← choice $ map try [time12,time24]+ return $ date `addTime` t++data DateIntervalType = Day | Week | Month | Year+ deriving (Eq,Show,Read)++data DateInterval = Days ℤ+ | Weeks ℤ+ | Months ℤ+ | Years ℤ+ deriving (Eq,Show)++convertTo ∷ DateTime → Day+convertTo dt = fromGregorian (fromIntegral $ year dt) (month dt) (day dt)++convertFrom ∷ Day → DateTime+convertFrom dt = + let (y,m,d) = toGregorian dt+ in date (fromIntegral y) m d++modifyDate ∷ (t → Day → Day) → t → DateTime → DateTime+modifyDate fn x dt = convertFrom $ fn x $ convertTo dt++addInterval ∷ DateTime → DateInterval → DateTime+addInterval dt (Days ds) = modifyDate addDays ds dt+addInterval dt (Weeks ws) = modifyDate addDays (ws*7) dt+addInterval dt (Months ms) = modifyDate addGregorianMonthsClip ms dt+addInterval dt (Years ys) = modifyDate addGregorianYearsClip ys dt++maybePlural ∷ String → TParser String+maybePlural str = do+ r ← string str+ optional $ char 's'+ return (capitalize r)++pDateInterval ∷ TParser DateIntervalType+pDateInterval = do+ s ← choice $ map maybePlural ["day", "week", "month", "year"]+ return $ read s++pRelDate ∷ DateTime → TParser DateTime+pRelDate date = do+ offs ← (try futureDate) <|> (try passDate) <|> (try today) <|> (try tomorrow) <|> yesterday+ return $ date `addInterval` offs++futureDate ∷ TParser DateInterval+futureDate = do+ string "in "+ n ← many1 digit+ char ' '+ tp ← pDateInterval+ case tp of+ Day → return $ Days (read n)+ Week → return $ Weeks (read n)+ Month → return $ Months (read n)+ Year → return $ Years (read n)++passDate ∷ TParser DateInterval+passDate = do+ n ← many1 digit+ char ' '+ tp ← pDateInterval+ string " ago"+ case tp of+ Day → return $ Days $ - (read n)+ Week → return $ Weeks $ - (read n)+ Month → return $ Months $ - (read n)+ Year → return $ Years $ - (read n)++today ∷ TParser DateInterval+today = do+ string "today"+ return $ Days 0++tomorrow ∷ TParser DateInterval+tomorrow = do+ string "tomorrow"+ return $ Days 1++yesterday ∷ TParser DateInterval+yesterday = do+ string "yesterday"+ return $ Days (-1)++pDate ∷ DateTime → TParser DateTime+pDate date = (try $ pRelDate date) <|> (try $ pAbsDate $ year date)++dateType ∷ String → DateType+dateType "start" = StartDate+dateType "end" = EndDate+dateType "deadline" = Deadline+dateType _ = error "unknown date type"++-- | Parse date/time with date type+pSpecDate ∷ DateTime → TParser (DateType, DateTime)+pSpecDate date = do+ tp ← choice $ map string ["start","end","deadline"]+ string ": "+ dt ← pDate date+ return (dateType tp, dt)++-- | Parse set of dates with types (in parenthesis)+pSpecDates ∷ DateTime → TParser [(DateType, DateTime)]+pSpecDates date = do+ char '('+ pairs ← (pSpecDate date) `sepBy1` (string "; ")+ string ") "+ return pairs++-- | Parse date/time+parseDate ∷ BaseConfig+ → DateTime -- ^ Current date/time+ → String -- ^ String to parse+ → Either ParseError DateTime+parseDate conf date s = runParser (pDate date) conf "" s+
+ Todos/Dot.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE UnicodeSyntax, TypeSynonymInstances, FlexibleInstances #-}+module Todos.Dot+ (showAsDot)+ where++import Data.List+import Data.Tree+import Text.Printf++import Todos.Unicode+import Todos.Types+import Todos.Color+import Todos.Shapes++data Dot = Dot {+ dotVertices ∷ [TodoItem],+ dotEdges ∷ [(TodoItem, TodoItem)],+ dotSubgraphs ∷ [Subgraph]+ }++data Subgraph = Subgraph {+ subLabel ∷ String,+ subItems ∷ [TodoItem] }+ deriving (Eq)++toDot ∷ Todo → Dot+toDot todo = Dot (getVertices todo) (getEdges todo) (getSubgraphs todo)++getVertices ∷ Todo → [TodoItem]+getVertices (Node item forest) + | itemStatus item == "GROUP" = concatMap getVertices forest+ | otherwise = [item] ⧺ concatMap getVertices forest++getEdges ∷ Todo → [(TodoItem, TodoItem)]+getEdges (Node item forest) + | itemStatus item == "GROUP" = concatMap getEdges forest+ | otherwise = [(item, rootLabel child) | child ← forest] ⧺ concatMap getEdges forest++getSubgraphs ∷ Todo → [Subgraph]+getSubgraphs (Node item forest)+ | itemStatus item == "GROUP" = Subgraph (showItem item) (nub $ sort $ concatMap flattern forest) :+ concatMap getSubgraphs forest+ | otherwise = []+ where + showItem item = showTags item ⧺ itemName item ⧺ "\\n" ⧺ itemDescr item+ showTags item | null (itemTags item) = ""+ | otherwise = "[" ⧺ unwords (itemTags item) ⧺ "] "+ flattern (Node item children) = item: concatMap flattern children++instance Show Dot where+ show (Dot vs es subs) = "digraph Todo {\n"+ ⧺ unlines (map (showDotNode getColor getShape) vs)+ ⧺ unlines (map showDotEdge es)+ ⧺ unlines (map showSubgraph subs)+ ⧺ "}\n"++showD ∷ (TodoItem → HSV) → (TodoItem → Shape) → [Dot] → String+showD colorFn shapeFn dots+ = "digraph Todo {\n"+ ⧺ " rankdir = \"RL\";\n"+ ⧺ " node [shape=\"box\", style=\"filled\"];\n"+ ⧺ unlines (map (showDotNode colorFn shapeFn) $ nub $ sort $ concatMap dotVertices dots)+ ⧺ unlines (map showDotEdge $ nub $ sort $ concatMap dotEdges dots)+ ⧺ unlines (map showSubgraph $ nub $ concatMap dotSubgraphs dots)+ ⧺ "}\n"++makeName ∷ TodoItem → String+makeName item = "\"" ⧺ makeId item ⧺ "\""++showDotNode ∷ (TodoItem → HSV) → (TodoItem → Shape) → TodoItem → String+showDotNode colorFn shapeFn item =+ printf " %s [label=\"%s\\n%s\\n%s\", fillcolor=%s, shape=\"%s\"];" (makeName item) (itemStatus item) (unwords $ itemTags item) (itemName item) (show $ colorFn item) (show $ shapeFn item)++showDotEdge ∷ (TodoItem, TodoItem) → String+showDotEdge (x,y) + | itemStatus x == "GROUP" = ""+ | otherwise = printf " %s -> %s;" (makeName y) (makeName x)++showSubgraph ∷ Subgraph → String+showSubgraph (Subgraph label items) + | null items = ""+ | otherwise = " subgraph \"cluster_" ⧺ makeId label ⧺ "\" {\n" + ⧺ " label=\"" ⧺ label ⧺ "\";\n"+ ⧺ (unlines $ map (" " ⧺) $ map showItem items)+ ⧺ "\n }"+ where+ showItem item = makeName item ⧺ ";"++-- | Return DOT output for Todos+showAsDot ∷ (TodoItem → HSV) -- ^ Function to determine node color+ → (TodoItem → Shape) -- ^ Function to determine node shape+ → [Todo] -- ^ Todo list+ → String+showAsDot colorFn shapeFn todos = (showD colorFn shapeFn) (map toDot todos)+
+ Todos/Formatters.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE UnicodeSyntax, TypeSynonymInstances, MultiParamTypeClasses, FlexibleInstances, ScopedTypeVariables #-}+module Todos.Formatters + (OutItem (..),+ Formatter,+ outItem,+ startFormat, newLine,+ ConfigShow (..),+ ConfigAdd (..)+ ) where++import Control.Monad+import Control.Monad.Reader+import System.Console.ANSI++import Todos.Unicode+import Todos.Types+import Todos.Config+import Todos.ConfigInstances ()++-- | Item which could be printed to the console+data OutItem = OutString String+ | OutSetColor ColorIntensity Color+ | SetBold+ | ResetAll+ deriving (Show)++-- | Produce a list of OutItem's depending on PrintConfig+type Formatter c = Reader (PrintConfig c) [OutItem]++-- | Empty Formatter+startFormat ∷ Formatter c+startFormat = return []++-- | Output given string+outString ∷ String → Formatter c+outString s = return [OutString s]++-- | Output new line+newLine ∷ Formatter c+newLine = outString "\n"++class ConfigAdd c a where+ -- | Execute Formatter and a consequently+ (<++>) ∷ Formatter c → a → Formatter c++instance ConfigAdd c (Formatter c) where+ (<++>) = liftM2 (⧺)++instance ConfigAdd c String where+ cm <++> s = cm <++> ((return [OutString s]) ∷ Formatter c)++setBold ∷ IO ()+setBold = setSGR [SetConsoleIntensity BoldIntensity]++setColor ∷ ColorIntensity → Color → IO ()+setColor int clr = setSGR [SetColor Foreground int clr]++-- | Reset all (color, bold, ...) attributes+reset ∷ IO ()+reset = setSGR []++-- | Print OutItem to console+outItem ∷ OutItem → IO ()+outItem (OutString s) = putStr s+outItem (OutSetColor i c) = setColor i c+outItem SetBold = setBold+outItem ResetAll = reset++-- | Similar to Show, but output can depend on PrintConfig+class ConfigShow c s where+ configShow ∷ s → Formatter c++instance ConfigShow c String where+ configShow s = return [OutString s]++instance ConfigShow c (Formatter c) where+ configShow = id+ +-- | Output bold (and maybe colored) item name+bold ∷ (RuntimeConfig c) ⇒ TodoItem → Formatter c+bold item = do+ let s = itemName item+ showColors ← askBase outColors+ hlOn ← askBase outHighlight+ getclr ← asks printItemColor+ hlPred ← asks doHighlight+ (hlInt, hlClr) ← asks printHighlightColor+ return $ if showColors+ then if hlOn && hlPred item+ then [SetBold, OutSetColor hlInt hlClr, OutString s, ResetAll]+ else case getclr item of+ Nothing → [SetBold, OutString s, ResetAll]+ Just (int,clr) → [SetBold, OutSetColor int clr, OutString s, ResetAll]+ else [OutString s]++-- | Output colored item status+colorStatus ∷ (RuntimeConfig c) ⇒ String → Formatter c+colorStatus st = do+ getclr ← asks printStatusColor+ let (int, clr) = getclr st+ col ← askBase outColors+ if col+ then return [OutSetColor int clr, OutString st, ResetAll]+ else return [OutString st]++instance (RuntimeConfig c) ⇒ ConfigShow c TodoItem where+ configShow item = sf <++> (colorStatus s ∷ Formatter c) <++> " " <++> dates <++> tags <++> title <++> (if null descr then "" else " "⧺descr)+ where+ sf ∷ Formatter c+ sf = startFormat+ ts = filter (not ∘ null) $ itemTags item+ s = itemStatus item+ descr = itemDescr item+ dates | null dates' = ""+ | otherwise = "(" ⧺ dates' ⧺ ") "+ dates' = showDates [StartDate `is` startDate item, EndDate `is` endDate item, Deadline `is` deadline item]+ tags = if null ts+ then ""+ else "[" ⧺ (unwords ts) ⧺ "] "+ title ∷ Formatter c+ title = bold item+
+ Todos/IO.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE CPP #-}+-- | Wrapper to support unicode IO with GHC 6.10 and 6.12+module Todos.IO + (putStr, putStrLn,readFile,getContents,print,+ encodeString, decodeString)+ where++import Codec.Binary.UTF8.String (encodeString, decodeString)++#if __GLASGOW_HASKELL__ < 612++import Prelude hiding (putStr, putStrLn,readFile,getContents,print)+import System.IO.UTF8++#else++import Prelude++#endif
+ Todos/Loader.hs view
@@ -0,0 +1,227 @@+{-# LANGUAGE UnicodeSyntax, PatternGuards #-}+-- | Read TODOs from files and construct corresponding ADTs.+module Todos.Loader+ (loadTodo)+ where++import Prelude hiding (putStrLn,readFile,getContents,print)+import Todos.IO+import Control.Monad (forM)+import qualified Data.Map as M+import System.FilePath+import Data.Maybe+import Data.Tree+import Data.List (nub, sort)++import Todos.Unicode+import Todos.Types+import Todos.Config+import Todos.Parser++todoName ∷ Todo → String+todoName todo = itemName ⋄ rootLabel todo++getDepends ∷ TodoMap → TodoItem → [Todo]+getDepends m item = catMaybes [M.lookup name m | name ← depends item] ++normalizeItem ∷ TodoMap → TodoItem → Todo+normalizeItem m item = Node item (map (normalize m) ⋄ getDepends m item)++normalize ∷ TodoMap → Todo → Todo +normalize m todo = Node item' ((map (normalize m) subTodos) ⧺ (map (normalize m) deps))+ where+ item ∷ TodoItem+ item = rootLabel todo++ item' ∷ TodoItem+ item' = item {depends=[]}++ subTodos ∷ [Todo]+ subTodos = subForest todo++ deps ∷ [Todo]+ deps = getDepends m item++normalizeList ∷ TodoMap → [Todo] → [Todo]+normalizeList m todos = map (normalize m) todos++-- | Almost same that readFile, but also works for special "-" file (stdin)+readFile' ∷ FilePath → IO String+readFile' "-" = getContents+readFile' file = readFile file++-- | Load items from given file+loadFile ∷ BaseConfig+ → DateTime -- ^ Current date/time+ → FilePath -- ^ Path to file+ → IO [TodoItem]+loadFile conf year path =+ case prefix conf of+ Nothing → do+ text ← readFile' path+ return $ parsePlain conf year path text+ Just p → do+ text ← readFile' path+ return $ parseAlternate conf 2 p year path text++-- | Decrease item level+(~-) ∷ TodoItem → ℤ → TodoItem+i@(Item {itemLevel=n}) ~- k = i {itemLevel=n-k}++-- | Increase item level+(~+) ∷ TodoItem → ℤ → TodoItem+i@(Item {itemLevel=n}) ~+ k = i {itemLevel=n+k}++-- | Check if item level is 0+iszero ∷ TodoItem → 𝔹+iszero item = (itemLevel item)==0++group' ∷ [TodoItem] → [[TodoItem]]+group' [] = []+group' (x:xs) = let (one,other) = break iszero xs+ in (x:one):group' other++mkTodo ∷ [TodoItem] → [Todo]+mkTodo = (map mkTodo') ∘ group'++mkTodo' ∷ [TodoItem] → Todo+mkTodo' [] = error "Internal error: mkTodo' does not sense for empty list!"+mkTodo' (x:xs) = Node x other+ where other = mkTodo ⋄ map (~-lvl) xs+ lvl = itemLevel (head xs)+ +consTodoMap ∷ [Todo] → TodoMap+consTodoMap todos = M.fromList (cons1 100 todos)+ where+ cons1 ∷ Int → [Todo] → [(String,Todo)]+ cons1 0 _ = []+ cons1 max trees = [(todoName todo, todo) | todo ← trees] ⧺ cons1 (max-1) (children trees)+ children ∷ [Todo] → [Todo]+ children trees = concatMap subForest trees++stitchTodos ∷ [TodoItem] → [Todo]+stitchTodos items = + let m = consTodoMap t+ t = mkTodo items+ in normalizeList m t++-- | Get all (different) tags from Todo list+allTags ∷ [Todo] → [String]+allTags todos = nub $ sort $ concatMap getTags todos+ where+ getTags (Node item children) = itemTags item ⧺ concatMap getTags children++-- | Get all (different) statuses from Todo list+allStatuses ∷ [Todo] → [String]+allStatuses todos = nub $ sort $ concatMap getStatus todos+ where+ getStatus (Node item children) = itemStatus item: concatMap getStatus children++grepBy ∷ (TodoItem → 𝔹) → [Todo] → [Todo]+grepBy cond todos = concatMap grep todos+ where+ grep ∷ Todo → [Todo]+ grep n@(Node _ children) = test n ⧺ concatMap grep children++ test ∷ Todo → [Todo]+ test n@(Node item _)+ | cond item = [n]+ | otherwise = []++grepByTag ∷ String → [Todo] → [Todo]+grepByTag tag todos = grepBy (\item → tag ∈ itemTags item) todos++grepByStatus ∷ String → [Todo] → [Todo]+grepByStatus st todos = grepBy (\item → st == itemStatus item) todos++tagTodo ∷ String → [Todo] → Todo+tagTodo tag todos = Node item $ grepByTag tag todos+ where+ item = Item {+ itemLevel = 0,+ itemName = tag,+ itemTags = ["TAG"],+ depends = [],+ itemStatus = ":",+ itemDescr = "",+ startDate = Nothing,+ endDate = Nothing,+ deadline = Nothing,+ fileName = "(no file)",+ lineNr = 0 }++groupByTag' ∷ [Todo] → [Todo]+groupByTag' todos = + map (\t → tagTodo t todos) (allTags todos) ⧺ todos++statusTodo ∷ String → [Todo] → Todo+statusTodo st todos = Node item $ grepByStatus st todos+ where+ item = Item {+ itemLevel = 0,+ itemName = st,+ itemTags = ["STATUS"],+ depends = [],+ itemStatus = ":",+ itemDescr = "",+ startDate = Nothing,+ endDate = Nothing,+ deadline = Nothing,+ fileName = "(no file)",+ lineNr = 0 }++groupByStatus' ∷ [Todo] → [Todo]+groupByStatus' todos =+ map (\s → statusTodo s todos) (allStatuses todos) ⧺ todos++dirname ∷ FilePath → FilePath+dirname path =+ case dropFileName path of+ [] → []+ dir → takeFileName (init dir)++fileTodo ∷ FilePath → TodoItem+fileTodo path = Item {+ itemLevel = 0,+ itemName = takeFileName path,+ itemTags = [dirname path],+ depends = [],+ itemStatus = ":",+ itemDescr = path,+ startDate = Nothing,+ endDate = Nothing,+ deadline = Nothing,+ fileName = path,+ lineNr = 0 }++todosGroup ∷ FilePath → [TodoItem] -> [TodoItem]+todosGroup path items =+ if null items+ then []+ else fileTodo path: map (~+ 1) items++changeTopStatus ∷ Maybe String → [Todo] → [Todo]+changeTopStatus Nothing todos = todos+changeTopStatus (Just st) todos = map setStatus todos+ where+ setStatus (Node item children) = Node (item {itemStatus = st}) children++-- | Load list of TODO trees from files+loadTodo ∷ BaseConfig+ → DateTime -- ^ Current date/time+ → [FilePath] -- ^ List of files+ → IO [Todo]+loadTodo conf date paths = do+ let grp = if groupByFile conf+ then todosGroup+ else const id+ tss ← forM paths $ \path → grp path `fmap` loadFile conf date path+ let todos = stitchTodos (concat tss)+ byTag = if groupByTag conf+ then groupByTag' todos+ else todos+ byStatus = if groupByStatus conf+ then groupByStatus' byTag+ else byTag+ return $ changeTopStatus (topStatus conf) byStatus+
+ Todos/Main.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE UnicodeSyntax #-}++module Todos.Main (todos) where++import Prelude hiding (putStrLn,readFile,getContents,print)+import IO+import Data.Tree+import System.Exit+import System.Cmd (system)++import System.Environment+import Config.Dyre++import Todos.Types+import Todos.Unicode+import Todos.Dates+import Todos.Dot+import Todos.CmdLine+import Todos.Tree+import Todos.ReadConfig+import Todos.Loader+import Todos.CommandParser+import Todos.Config+import Todos.ConfigUtils++realTodos ∷ (RuntimeConfig c) ⇒ TodosConfig c → IO ()+realTodos tcfg = do+ currDate ← getCurrentDateTime + config ← readConfig+ args ← getArgs+ let pres = (parseCommandLine tcfg) currDate (nullConfig tcfg) (config ⧺ args)+ case pres of+ Parsed q files' → do+ let bc = toBaseConfig q+ files ← glob files'+ todos ← loadTodo bc currDate files+ let queried = (filterTodos tcfg) currDate q todos+ format item = item {itemDescr = printfItem (descrFormat bc) item}+ case commandToRun bc of+ JustShow → printTodos tcfg (mkPrintConfig currDate q tcfg) (mapT format queried)+ ShowAsDot → + putStrLn $ showAsDot (itemColor tcfg) (itemShape tcfg) (mapT format queried)+ SystemCommand cmd → do+ forT selected (\item → system $ printfItem cmd (format item))+ return ()+ where selected | outOnlyFirst bc = [Node (rootLabel $ head queried) []]+ | otherwise = queried++ ParseError str → error str+ CmdLineHelp → do putStrLn usage+ exitWith ExitSuccess++-- | Main function to run. User can specify TodosConfig with any runtime config+-- type. By default (in todos.hs) defaultConfig is used, which uses DefaultConfig type.+todos ∷ (RuntimeConfig c) ⇒ TodosConfig c → IO ()+todos = wrapMain $ defaultParams {+ projectName = "todos",+ realMain = realTodos,+ statusOut = const (return ())+ }+
+ Todos/Parser.hs view
@@ -0,0 +1,146 @@+{-# LANGUAGE UnicodeSyntax, NoMonomorphismRestriction, TypeSynonymInstances, DeriveDataTypeable #-}+module Todos.Parser+ (parsePlain, parseAlternate)+ where++import Prelude hiding (putStrLn,readFile,getContents,print)+import Data.List+import Text.ParserCombinators.Parsec+import Data.Char++import Todos.Unicode+import Todos.Types+import Todos.Dates+import Todos.ParserTypes+import Todos.Config++strip ∷ String → String+strip = reverse ∘ p ∘ reverse ∘ p+ where+ p = dropWhile isSpace++pSpace ∷ TParser Char+pSpace = oneOf " \t"++pSpace' ∷ TParser String+pSpace' = do+ pSpace+ return " "++pSpaces ∷ TParser String+pSpaces = many pSpace++pDeps ∷ TParser [String]+pDeps = do+ string "("+ ws ← (many1 ⋄ noneOf ",)\n\r") `sepBy` (char ',')+ string ")"+ return $ map strip ws++pTags ∷ TParser [String]+pTags = do+ ts ← between (char '[') (char ']') $ word `sepBy1` pSpace+ pSpaces+ return ts+ where+ word = many1 (noneOf " \t\n\r]")++pItem ∷ DateTime → TParser TodoItem+pItem date = do+ pos ← getPosition+ s ← pSpaces+ conf ← getState+ stat ← if skipStatus conf+ then case forcedStatus conf of+ Just fs → return fs+ Nothing → return "*"+ else do + rs ← pWord+ case forcedStatus conf of+ Just fs → return fs+ Nothing → return rs+ dates ← (try (pSpecDates date) <|> return [])+ tags ← (try pTags <|> return [])+ namew ← many1 pWord+ pSpaces+ deps ← (try pDeps <|> return [])+ pSpaces+ descr ← many (noneOf "\n\r")+ pSpaces+ many ⋄ oneOf "\n\r"+ return ⋄ Item {+ itemLevel = fromIntegral $ length s,+ itemName = unwords namew,+ itemTags = tags,+ depends = deps,+ itemStatus = stat,+ itemDescr = descr,+ startDate = lookup StartDate dates,+ endDate = lookup EndDate dates,+ deadline = lookup Deadline dates,+ fileName = sourceName pos,+ lineNr = sourceLine pos }++pWord ∷ TParser String+pWord = do+ w ← many1 (noneOf " \t\n\r")+ (try pSpace') <|> (return w)+ return w++pItems ∷ DateTime → TParser [TodoItem]+pItems date = do+ its ← many (pItem date)+ eof+ return its++unwords' ∷ String → [String] → String+unwords' prefix lst =+ let (hd:tl) = map (filter (/='\r')) lst+ addLines = filter (not ∘ (prefix `isPrefixOf`)) tl+ in case addLines of+ [] → hd+ _ → hd ⧺ " {" ++ (unwords addLines) ++ "}"++filterN ∷ (Num a, Enum a) ⇒ Int → String → [String] → ([a], [String])+filterN n prefix lst = + let zipped = zip [0..] lst+ good = filter (isGood ∘ snd) zipped+ lns = map fst good+ sub k l = (take l) ∘ (drop k)+ ans = map (unwords' prefix) [sub j n lst | j ← lns]+ isGood x = prefix `isPrefixOf` x+ cut = drop (1+length prefix) + in (map (+1) lns, map cut ans)++filterJoin ∷ Int → String → String → ([Int], String)+filterJoin n prefix str = + let (ns, lns) = filterN n prefix (lines str)+ in (ns, unlines lns)++-- | Read list of TODO items from plain format +parsePlain ∷ BaseConfig+ → DateTime -- ^ Current date/time+ → SourceName -- ^ Source file name+ → String -- ^ String to parse+ → [TodoItem]+parsePlain conf date path text = + case runParser (pItems date) conf path text of+ Right items → items+ Left e → error $ show e++-- | Read list of TODO items from alternate format+parseAlternate ∷ BaseConfig + → Int -- ^ Number of lines after matching to include to item's description+ → String -- ^ Prefix to match+ → DateTime -- ^ Current date/time+ → SourceName -- ^ Source file name+ → String -- ^ String to parse+ → [TodoItem]+parseAlternate conf next prefix date path text = + let (ns, filtered) = filterJoin next prefix text+ renumber lst = zipWith renumber1 ns lst+ renumber1 n item = item {lineNr=n}+ in case runParser (pItems date) conf path filtered of+ Right items → renumber items+ Left e → error $ show e+
+ Todos/ParserTypes.hs view
@@ -0,0 +1,10 @@+{-# LANGUAGE UnicodeSyntax #-}+module Todos.ParserTypes where++import Text.ParserCombinators.Parsec++import Todos.Config+++type TParser a = GenParser Char BaseConfig a+
+ Todos/Print.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE UnicodeSyntax #-}+module Todos.Print+ (defaultPrintTodos, showTodos)+ where++import Control.Monad+import Control.Monad.Reader+import Data.List+import Data.Tree+import Data.Function (on)+import System.Console.ANSI++import Todos.Unicode+import Todos.Types+import Todos.Config+import Todos.ConfigInstances ()+import Todos.Formatters++sortBy' ∷ SortingType → [Todo] → [Todo]+sortBy' s | s == DoNotSort = id+ | otherwise = sortBy sorter+ where+ sorter = compare `on` (f ∘ rootLabel)+ f = case s of+ DoNotSort → error "Internal error: sortBy' should not be called when DoNotSort is specified!"+ ByTitle → itemName+ ByStatus → itemStatus+ ByTags → unwords ∘ itemTags+ ByStartDate → show ∘ startDate+ ByEndDate → show ∘ endDate+ ByDeadline → show ∘ deadline ++showT ∷ SortingType → Int → Todo → [Formatter DefaultConfig]+showT s n (Node item todos) = + (sf <++> showId item <++> replicate n ' ' <++> item') :+ (concatMap (showT s (n+2)) $ sortBy' s todos)+ where+ sf ∷ Formatter DefaultConfig+ sf = startFormat++ item' ∷ Formatter DefaultConfig+ item' = configShow item++ showId :: TodoItem → Formatter DefaultConfig+ showId item = do+ s ← askBase outIds+ c ← askBase outColors+ if s+ then if c + then return [OutSetColor Dull Yellow, OutString $ makeId item ++ " ", ResetAll]+ else return [OutString $ makeId item ++ " "]+ else return [OutString ""]++unlines'' ∷ [Formatter c] → Formatter c+unlines'' lst = concat `fmap` (sequence $ intersperse newLine lst)++showTodo ∷ Todo → Formatter DefaultConfig+showTodo t = do+ conf ← asks toBaseConfig+ let f = case outOnlyFirst conf of+ False → unlines''+ True → head+ f $ showT (sorting conf) 0 t++-- | Prepare TODOs for console output+showTodos ∷ [Todo] → Formatter DefaultConfig+showTodos lst = do+ conf ← asks toBaseConfig+ let f = case outOnlyFirst conf of+ False → unlines''+ True → head+ f $ map showTodo $ sortBy' (sorting conf) $ nub lst++-- | Default function to output TODOs to console+defaultPrintTodos ∷ PrintConfig DefaultConfig → [Todo] → IO ()+defaultPrintTodos cfg lst = + let lst' = runReader (showTodos lst) cfg+ in forM lst' outItem >> putStrLn ""+
+ Todos/ReadConfig.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE UnicodeSyntax #-}++-- | Module for parsing config files+module Todos.ReadConfig+ (readConfig)+ where++import Prelude hiding (putStrLn,readFile,getContents,print)+import Todos.IO+import System.Environment+import System.FilePath +import System.Directory (doesFileExist)+import Text.ParserCombinators.Parsec++import Todos.Unicode++word ∷ Parser String+word = choice $ map try [quotedOption, simpleOption, quoted, simpleWord]++simpleWord = many1 $ noneOf " \t\r\n=\"'"++quotedOption = (try quotedLongOption) <|> quotedShortOption++quotedLongOption ∷ Parser String+quotedLongOption = do+ string "--"+ o ← simpleWord+ char '='+ v ← quoted+ return ("--" ⧺ o ⧺ "=" ⧺ v)++quotedShortOption ∷ Parser String+quotedShortOption = do+ string "-"+ o ← simpleWord+ v ← quoted+ return ("-" ⧺ o ⧺ v)++simpleOption = do+ o ← simpleWord+ optional $ char '='+ v ← simpleWord+ return (o ⧺ "=" ⧺ v)++quoted = quoted1 <|> quoted2++quoted1 = do+ char '\''+ s ← many1 $ noneOf "'"+ char '\''+ return s++quoted2 = do+ char '"'+ s ← many1 $ noneOf "\""+ char '"'+ return s++pConfig ∷ Parser [String]+pConfig = word `sepBy` space++parseConfig ∷ String → [String]+parseConfig str = + case parse pConfig "config file" str of+ Right lst → lst+ Left err → error $ show err++readFile' ∷ FilePath → IO [String]+readFile' path = + do b ← doesFileExist path+ if not b+ then return []+ else do+ str ← readFile path+ return $ parseConfig (unwords $ lines str)++-- | Read list of options from config files+readConfig ∷ IO [String]+readConfig = do+ home ← getEnv "HOME"+ let homepath = home </> ".config" </> "todos" </> "todos.conf"+ homecfg ← readFile' homepath+ localcfg ← readFile' ".todos.conf"+ return $ homecfg ⧺ localcfg+
+ Todos/Shapes.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE UnicodeSyntax #-}+module Todos.Shapes where++import qualified Data.Map as M++import Todos.Types++-- | Supported node shapes for DOT output+data Shape = + Box + | Ellipse+ | Diamond+ | DCircle+ | Note+ | Parallelogram+ | Folder+ deriving (Eq)++instance Show Shape where+ show Box = "box"+ show Ellipse = "ellipse"+ show Diamond = "diamond"+ show DCircle = "doublecircle"+ show Note = "note"+ show Parallelogram = "parallelogram"+ show Folder = "folder"++-- | Node shapes for some common item statuses+shapes ∷ M.Map String Shape+shapes = M.fromList $ [+ ("o", Ellipse),+ ("O", DCircle),+ (":", Folder),+ ("*", Diamond),+ ("/", Parallelogram),+ ("NOTE", Note) ]++-- | Get item shape for this item (default funciton)+getShape ∷ TodoItem → Shape+getShape item = + case M.lookup (itemStatus item) shapes of+ Nothing → Box+ Just s → s+
+ Todos/Tree.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE UnicodeSyntax, NoMonomorphismRestriction, FlexibleInstances, TypeSynonymInstances #-}+module Todos.Tree + (delTag,+ pruneSelector,+ tagPred, statusPred, grepPred, descPred, datePred, idPred,+ forT, mapT)+ where++import Prelude hiding (putStrLn,readFile,getContents,print)+import Control.Monad+import Data.Generics+import Data.List+import Data.Tree+import Text.Regex.PCRE++import Todos.Types+import Todos.Unicode+import Todos.Config++mapTags ∷ (Data a) ⇒ ([String] → [String]) → [a] → [a]+mapTags f = map ⋄ everywhere ⋄ mkT changeTags+ where+ changeTags item@(Item {itemTags=ts}) = item {itemTags = f ts}+ +addTag ∷ (Data a) ⇒ String → [a] → [a]+addTag t = mapTags (t:)++delTag ∷ (Data a) ⇒ String → [a] → [a]+delTag t = mapTags (delete t)++pruneSelector ∷ BaseConfig → (TodoItem → 𝔹) → (Todo → [Todo])+pruneSelector bc pred =+ let Limit n = pruneL bc+ Limit m = minL bc+ in pruneSelector' n m pred+ +pruneSelector' ∷ ℤ → ℤ → (TodoItem → 𝔹) → (Todo → [Todo])+pruneSelector' n m pred = select n 0 False+ where+ select k t b (Node item trees) | t < m = [Node item ⋄ concatMap (select (n-1) (t+1) True) trees]+ | pred item = [Node item ⋄ concatMap (select (n-1) (t+1) True) trees]+ | (k > 0) ∧ b = [Node item ⋄ concatMap (select (k-1) (t+1) True) trees]+ | k > 0 = concatMap (select (k-1) (t+1) False) trees+ | otherwise = [] ++addS ∷ (Show a) ⇒ a → TodoItem → TodoItem+addS s item@(Item {itemName=name}) = item {itemName = name ⧺ " — " ⧺ show s}++-- | Check if item has given tag+tagPred ∷ String → TodoItem → 𝔹+tagPred tag = \item → tag ∈ itemTags item++-- | Check if item has given status+statusPred ∷ String → TodoItem → 𝔹+statusPred st = \item → st == itemStatus item+ +-- | Check if item's title matches to given regexp+grepPred ∷ String → TodoItem → 𝔹+grepPred pattern = \item → itemName item =~ pattern++-- | Check if item's description matches to given regexp+descPred ∷ String → TodoItem → 𝔹+descPred pattern = \item → itemDescr item =~ pattern++-- | Check if item has given ID+idPred :: String → TodoItem → 𝔹+idPred hash = \item → makeId item == hash++isLT ∷ (Ord t) ⇒ Maybe t → t → 𝔹+isLT Nothing _ = False+isLT (Just x) y = x <= y++isGT ∷ (Ord t) ⇒ Maybe t → t → 𝔹+isGT Nothing _ = False+isGT (Just x) y = x >= y++datePred ∷ (Ord a) ⇒ (t → Maybe a) → a → a → t → 𝔹+datePred selector curr dt | dt >= curr = \item → selector item `isLT` dt+ | otherwise = \item → selector item `isGT` dt++-- | Flattern the tree+flattern ∷ [Todo] → [Todo]+flattern = concatMap flat+ where+ flat ∷ Todo → [Todo]+ flat (Node item trees) = (Node item []):(concatMap flat trees)++-- | For each item in the tree, execute given monadic action (this is similar+-- to forM, but for trees instead of lists).+forT ∷ (Monad m, Eq t) ⇒ [Tree t] → (t → m a) → m [b]+forT todos f = forM (nub todos) forT'+ where+ forT' (Node item trees) =+ do f item+ res ← forM trees forT'+ return $ last res++-- | Similar to map, but for trees instead of lists.+mapT ∷ (t → t) → [Tree t] → [Tree t]+mapT f todos = map mapT' todos+ where+ mapT' (Node item trees) = Node (f item) (mapT f trees)+
+ Todos/Types.hs view
@@ -0,0 +1,247 @@+{-# LANGUAGE UnicodeSyntax, DeriveDataTypeable, TypeSynonymInstances, FlexibleInstances, NoMonomorphismRestriction #-}++module Todos.Types where++import Prelude hiding (putStr, putStrLn,readFile,getContents,print)+import Data.Hash++import Data.Function +import Data.Generics hiding (GT)+import Data.Char (toUpper)+import Data.Maybe+import Data.Tree+import Data.List+import qualified Data.Map as M+import Text.ParserCombinators.Parsec+import Numeric++import Todos.Unicode++-- | Kind of date+data DateType = StartDate+ | EndDate+ | Deadline+ deriving (Eq)++instance Show DateType where+ show StartDate = "start"+ show EndDate = "end"+ show Deadline = "deadline"++data DateTime =+ DateTime {+ year ∷ Int,+ month ∷ Int,+ day ∷ Int,+ hour ∷ Int,+ minute ∷ Int,+ second ∷ Int }+ deriving (Eq,Ord,Data,Typeable)++-- | 12 months names.+months ∷ [String]+months = ["january",+ "february",+ "march",+ "april",+ "may",+ "june",+ "july",+ "august",+ "september",+ "october",+ "november",+ "december"]++-- | capitalize first letter of the string+capitalize ∷ String → String+capitalize [] = []+capitalize (x:xs) = (toUpper x):xs++-- | Show name of given month+showMonth ∷ Int → String+showMonth i = capitalize $ months !! (i-1)++instance Show DateTime where+ show (DateTime y m d h min s) = + show d ⧺ " " ⧺ showMonth m ⧺ " " ⧺ show y ⧺ ", " ⧺+ show h ⧺ ":" ⧺ show min ⧺ ":" ⧺ show s++-- | Only time, without date+data Time = + Time {+ tHour ∷ Int,+ tMinute ∷ Int,+ tSecond ∷ Int }+ deriving (Eq,Ord,Show,Data,Typeable)++-- | TODO item itself.+data TodoItem = Item {+ itemLevel ∷ ℤ, -- ^ Indentation level (from source file)+ itemName ∷ String, -- ^ Name (title) of the item+ itemTags ∷ [String], -- ^ Tags of the item+ depends ∷ [String], -- ^ Names (titles) of item's depends+ itemStatus ∷ String, -- ^ Status of the item+ itemDescr ∷ String, -- ^ Description of the item+ startDate ∷ Maybe DateTime, -- ^ Date when TODO is planned to start+ endDate ∷ Maybe DateTime, -- ^ Date when TODO is planned to end+ deadline ∷ Maybe DateTime, -- ^ Deadline for this TODO+ fileName ∷ FilePath, -- ^ Path to the source file+ lineNr ∷ Line -- ^ Line in the source file, where this item was defined+ }+ deriving (Eq,Data,Typeable)++instance Hashable TodoItem where+ hash item = foldl1 combine $ map ($ item) [hash ∘ itemName, hash ∘ itemDescr,+ hash ∘ itemTags, hash ∘ itemStatus]++-- | Make an ID for any hashable item. 16 hexadecimal digits.+makeId :: (Hashable a) ⇒ a → String+makeId item =+ let s = showHex (asWord64 $ hash item) ""+ l = length s + in if l < 16 + then replicate (16-l) '0' ++ s+ else s++-- | Tree of TODO items.+type Todo = Tree TodoItem++type TodoMap = M.Map String Todo++data Limit = Unlimited+ | Limit {unLimit ∷ ℤ}+ deriving (Eq,Show)++instance Ord Limit where+ compare Unlimited Unlimited = EQ+ compare Unlimited _ = GT+ compare _ Unlimited = LT+ compare (Limit x) (Limit y) = compare x y++-- | Command line flag+data CmdLineFlag = QF {queryFlag ∷ QueryFlag}+ | MF {modeFlag ∷ ModeFlag}+ | OF {outFlag ∷ OutFlag}+ | LF {limFlag ∷ LimitFlag}+ | HelpF+ deriving (Eq,Show)++-- | Flags to specify query+data QueryFlag = Tag String+ | Name {unName ∷ String}+ | IdIs String+ | Status String+ | Description String+ | StartDateIs DateTime+ | EndDateIs DateTime+ | DeadlineIs DateTime+ | AndCons+ | OrCons+ | NotCons+ | NoFilter+ deriving (Eq,Ord,Show) ++data LimitFlag = Prune {unPrune ∷ ℤ}+ | Start {unMin ∷ ℤ}+ deriving (Eq,Show)++-- | Flags to specify parsing mode+data ModeFlag = Execute {unExecute ∷ String}+ | Prefix {unPrefix ∷ String}+ | Describe {unDescribe ∷ String}+ | DoNotReadStatus+ | SetStatus {newStatus ∷ String}+ | SetTopStatus {newTopStatus ∷ String}+ | GroupByFile+ | GroupByTag+ | GroupByStatus+ deriving (Eq,Ord,Show)++-- | Flags to control output+data OutFlag = OnlyFirst + | Colors+ | Highlight+ | Ids+ | DotExport+ | Sort {getSorting ∷ SortingType}+ deriving (Eq,Ord,Show)++-- | Type of sorting+data SortingType = DoNotSort+ | ByTitle+ | ByStatus+ | ByTags + | ByStartDate+ | ByEndDate+ | ByDeadline+ deriving (Eq,Ord,Show)++readSort ∷ String → SortingType+readSort "no" = DoNotSort+readSort "title" = ByTitle+readSort "status" = ByStatus+readSort "tags" = ByTags+readSort "start-date" = ByStartDate+readSort "end-date" = ByEndDate+readSort "deadline" = ByDeadline+readSort s = error $ "Unknown sorting type: "++s++instance (Ord a) ⇒ Ord (Tree a) where+ compare = compare `on` rootLabel++-- TODO: - rename Options → QueryOptions or similar+-- | Result of parsing command line+data Options = O [QueryFlag] [ModeFlag] [OutFlag] [LimitFlag]+ | Help++-- | What to do with selected items+data TodoCommand =+ JustShow -- ^ Just output items to console+ | ShowAsDot -- ^ Output graph in DOT format+ | SystemCommand String -- ^ Execute this system command for each item+ deriving (Eq, Show)++-- | Data type to store complex queries+data Composed = Pred QueryFlag -- ^ Simple query+ | And Composed Composed -- ^ Logical AND+ | Or Composed Composed -- ^ Logical OR+ | Not Composed -- ^ Logical NOT+ | Empty -- ^ Empty query+ | HelpC -- ^ User requests help+ deriving (Eq,Show)++is ∷ (Functor f) ⇒ t → f a → f (t, a)+t `is` x = (\a → (t,a)) `fmap` x++showDate ∷ (DateType, DateTime) → String+showDate (t,d) = show t ⧺ ": " ⧺ show d++showDates ∷ [Maybe (DateType, DateTime)] → String+showDates = intercalate "; " ∘ map showDate ∘ catMaybes++instance Show TodoItem where+ show item = s ⧺ " " ⧺ dates ⧺ tags ⧺ name ⧺ (if null descr then "" else " "⧺descr)+ where+ name = itemName item+ ts = itemTags item+ s = itemStatus item+ descr = itemDescr item+ dates | null dates' = ""+ | otherwise = "(" ⧺ dates' ⧺ ") "+ dates' = showDates [StartDate `is` startDate item, EndDate `is` endDate item, Deadline `is` deadline item]+ tags = if null ts+ then ""+ else "[" ⧺ (unwords ts) ⧺ "] "++instance Ord TodoItem where+ compare item1 item2 = + let c1 = (compare `on` itemLevel) item1 item2+ c2 = (compare `on` itemStatus) item1 item2+ c3 = (compare `on` itemName) item1 item2+ in if c1 == EQ+ then if c2 == EQ + then c3+ else c2+ else c1+
+ Todos/Unicode.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE UnicodeSyntax #-}+module Todos.Unicode where++type ℝ = Float+type ℤ = Integer+type 𝔹 = Bool++-- | Concatenate lists +(⧺) ∷ [a] → [a] → [a]+(⧺) = (++)++(⋄) ∷ (a -> b) -> a -> b+(⋄) = ($)+infixr 0 ⋄++(∘) :: (b -> c) -> (a -> b) -> a -> c+(∘) = (.)++(∨) :: Bool -> Bool -> Bool+(∨) = (||)++(∧) :: Bool -> Bool -> Bool+(∧) = (&&)++(∈) :: (Eq a) => a -> [a] -> Bool+x ∈ lst = elem x lst+
− Types.hs
@@ -1,373 +0,0 @@-{-# LANGUAGE UnicodeSyntax, DeriveDataTypeable, TypeSynonymInstances, FlexibleInstances, NoMonomorphismRestriction #-}--module Types where--import Prelude hiding (putStr, putStrLn,readFile,getContents,print)-import IO-import System.Console.ANSI-import Data.Hash--import Control.Monad.Reader-import Data.Function -import Data.Generics hiding (GT)-import Data.Char (toUpper)-import Data.Maybe-import Data.Tree-import Data.List-import qualified Data.Map as M-import Text.ParserCombinators.Parsec-import Numeric--import Unicode--data DateType = StartDate- | EndDate- | Deadline- deriving (Eq)--instance Show DateType where- show StartDate = "start"- show EndDate = "end"- show Deadline = "deadline"--data DateTime =- DateTime {- year ∷ Int,- month ∷ Int,- day ∷ Int,- hour ∷ Int,- minute ∷ Int,- second ∷ Int }- deriving (Eq,Ord,Data,Typeable)--months ∷ [String]-months = ["january",- "february",- "march",- "april",- "may",- "june",- "july",- "august",- "september",- "october",- "november",- "december"]--capitalize ∷ String → String-capitalize [] = []-capitalize (x:xs) = (toUpper x):xs--showMonth ∷ Int → String-showMonth i = capitalize $ months !! (i-1)--instance Show DateTime where- show (DateTime y m d h min s) = - show d ⧺ " " ⧺ showMonth m ⧺ " " ⧺ show y ⧺ ", " ⧺- show h ⧺ ":" ⧺ show min ⧺ ":" ⧺ show s--data Time = - Time {- tHour ∷ Int,- tMinute ∷ Int,- tSecond ∷ Int }- deriving (Eq,Ord,Show,Data,Typeable)--data TodoItem = Item {- itemLevel ∷ ℤ,- itemName ∷ String,- itemTags ∷ [String],- depends ∷ [String],- itemStatus ∷ String,- itemDescr ∷ String,- startDate ∷ Maybe DateTime,- endDate ∷ Maybe DateTime,- deadline ∷ Maybe DateTime,- fileName ∷ FilePath,- lineNr ∷ Line}- deriving (Eq,Data,Typeable)--instance Hashable TodoItem where- hash item = foldl1 combine $ map ($ item) [hash ∘ itemName, hash ∘ itemDescr,- hash ∘ itemTags, hash ∘ itemStatus]--makeId :: TodoItem → String-makeId item =- let s = showHex (asWord64 $ hash item) ""- l = length s - in if l < 16 - then replicate (16-l) '0' ++ s- else s--type Todo = Tree TodoItem--type TodoMap = M.Map String Todo--data Limit = Unlimited- | Limit ℤ- deriving (Eq,Show)--instance Ord Limit where- compare Unlimited Unlimited = EQ- compare Unlimited _ = GT- compare _ Unlimited = LT- compare (Limit x) (Limit y) = compare x y--data CmdLineFlag = QF {queryFlag ∷ QueryFlag}- | MF {modeFlag ∷ ModeFlag}- | OF {outFlag ∷ OutFlag}- | LF {limFlag ∷ LimitFlag}- | HelpF- deriving (Eq,Show)--data QueryFlag = Tag String- | Name {unName ∷ String}- | IdIs String- | Status String- | Description String- | StartDateIs DateTime- | EndDateIs DateTime- | DeadlineIs DateTime- | AndCons- | OrCons- | NotCons- | NoFilter- deriving (Eq,Ord,Show) --data LimitFlag = Prune {unPrune ∷ ℤ}- | Start {unMin ∷ ℤ}- deriving (Eq,Show)--data ModeFlag = Execute {unExecute ∷ String}- | Prefix {unPrefix ∷ String}- | Describe {unDescribe ∷ String}- | DoNotReadStatus- | SetStatus {newStatus ∷ String}- deriving (Eq,Ord,Show)--data OutFlag = OnlyFirst - | Colors- | Ids- | DotExport- | Sort {getSorting ∷ SortingType}- deriving (Eq,Ord,Show)--data SortingType = DoNotSort- | ByTitle- | ByStatus- | ByTags - | ByStartDate- | ByEndDate- | ByDeadline- deriving (Eq,Ord,Show)--readSort ∷ String → SortingType-readSort "no" = DoNotSort-readSort "title" = ByTitle-readSort "status" = ByStatus-readSort "tags" = ByTags-readSort "start-date" = ByStartDate-readSort "end-date" = ByEndDate-readSort "deadline" = ByDeadline-readSort s = error $ "Unknown sorting type: "++s--type Transformer = Reader Config (Todo → [Todo])-type ListTransformer = Reader Config ([Todo] → [Todo])--transformList ∷ r → Reader r (t → a) → t → a-transformList conf tr list = do- f ← tr- return (f list)- `runReader` conf--data OutItem = OutString String- | OutSetColor Color- | SetBold- | ResetAll- deriving (Show)--type Formatter = Reader Config [OutItem]--newtype IOList = IOL [Formatter]--startFormat ∷ Formatter-startFormat = return []--outString ∷ String → Formatter-outString s = return [OutString s]--newLine ∷ Formatter-newLine = outString "\n"--class ConfigAdd a where- (<++>) ∷ Formatter → a → Formatter--instance ConfigAdd Formatter where- (<++>) = liftM2 (⧺)--instance ConfigAdd String where- cm <++> s = cm <++> ((return [OutString s]) ∷ Formatter)--setBold ∷ IO ()-setBold = setSGR [SetConsoleIntensity BoldIntensity]--setColor ∷ Color → IO ()-setColor clr = setSGR [SetColor Foreground Dull clr]--reset ∷ IO ()-reset = setSGR []--outItem ∷ OutItem → IO ()-outItem (OutString s) = putStr s-outItem (OutSetColor c) = setColor c-outItem SetBold = setBold-outItem ResetAll = reset--runFormatter ∷ Config → Formatter → IO ()-runFormatter conf cm = - let lst = runReader cm conf- in mapM_ outItem lst--class ConfigShow s where- configShow ∷ s → Formatter--instance ConfigShow String where- configShow s = return [OutString s]--instance ConfigShow Formatter where- configShow = id- -showIO ∷ (ConfigShow a) ⇒ Config → a → IO ()-showIO conf = (runFormatter conf) ∘ configShow--instance (Ord a) ⇒ Ord (Tree a) where- compare = compare `on` rootLabel---- TODO: - rename Options → QueryOptions or similar-data Options = O [QueryFlag] [ModeFlag] [OutFlag] [LimitFlag]- | Help--data TodoCommand =- JustShow- | ShowAsDot- | SystemCommand String- deriving (Eq, Show)--data Config = Config {- outOnlyFirst ∷ 𝔹,- outColors ∷ 𝔹,- outIds :: 𝔹,- sorting ∷ SortingType,- pruneL ∷ Limit,- minL ∷ Limit,- commandToRun ∷ TodoCommand,- prefix ∷ Maybe String,- descrFormat ∷ String,- skipStatus ∷ 𝔹,- forcedStatus ∷ Maybe String,- query ∷ Composed }- deriving (Eq,Show)--emptyConfig = Config {- outOnlyFirst = False,- outColors = False,- outIds = False,- sorting = DoNotSort,- pruneL = Unlimited,- minL = Unlimited,- commandToRun = JustShow,- prefix = Nothing,- descrFormat = "%d",- skipStatus = False,- forcedStatus = Nothing,- query = Empty }--data Composed = Pred QueryFlag- | And Composed Composed- | Or Composed Composed- | Not Composed- | Empty- | HelpC- deriving (Eq,Show)--is ∷ (Functor f) ⇒ t → f a → f (t, a)-t `is` x = (\a → (t,a)) `fmap` x--showDate ∷ (Show t, Show t1) ⇒ (t, t1) → [Char]-showDate (t,d) = show t ⧺ ": " ⧺ show d--showDates ∷ (Show t, Show t1) ⇒ [Maybe (t, t1)] → [Char]-showDates = intercalate "; " ∘ map showDate ∘ catMaybes--instance Show TodoItem where- show item = s ⧺ " " ⧺ dates ⧺ tags ⧺ name ⧺ (if null descr then "" else " "⧺descr)- where- n = itemLevel item- name = itemName item- ts = itemTags item- s = itemStatus item- descr = itemDescr item- dates | null dates' = ""- | otherwise = "(" ⧺ dates' ⧺ ") "- dates' = showDates [StartDate `is` startDate item, EndDate `is` endDate item, Deadline `is` deadline item]- tags = if null ts- then ""- else "[" ⧺ (unwords ts) ⧺ "] "--bold ∷ String → Formatter-bold s = do- col ← asks outColors - if col- then return [SetBold, OutString s, ResetAll]- else return [OutString s]--lookupC ∷ (Eq a1) ⇒ a1 → [([a1], a)] → Maybe a-lookupC k [] = Nothing-lookupC k ((lst,c):other) | k ∈ lst = Just c- | otherwise = lookupC k other--statusColors ∷ [([String], Color)]-statusColors = - [(["FIXED", "DONE"], Green),- (["INVALID"], Magenta),- (["*"], Red),- (["?"], Blue)]--colorStatus ∷ String → Formatter-colorStatus st =- case lookupC st statusColors of- Nothing → return [OutString st]- Just clr → do- col ← asks outColors - if col- then return [OutSetColor clr, OutString st, ResetAll]- else return [OutString st]--instance ConfigShow TodoItem where- configShow item = startFormat <++> colorStatus s <++> " " <++> dates <++> tags <++> bold name <++> (if null descr then "" else " "⧺descr)- where- n = itemLevel item- name = itemName item- ts = itemTags item- s = itemStatus item- descr = itemDescr item- dates | null dates' = ""- | otherwise = "(" ⧺ dates' ⧺ ") "- dates' = showDates [StartDate `is` startDate item, EndDate `is` endDate item, Deadline `is` deadline item]- tags = if null ts- then ""- else "[" ⧺ (unwords ts) ⧺ "] "--instance Ord TodoItem where- compare item1 item2 = - let c1 = (compare `on` itemLevel) item1 item2- c2 = (compare `on` itemStatus) item1 item2- c3 = (compare `on` itemName) item1 item2- in if c1 == EQ- then if c2 == EQ - then c3- else c2- else c1--type TParser a = GenParser Char Config a-
− Unicode.hs
@@ -1,13 +0,0 @@-module Unicode where--type ℝ = Float-type ℤ = Integer-type 𝔹 = Bool--(⧺) = (++)-(⋄) = ($)-infixr 0 ⋄-(∘) = (.)-(∨) = (||)-(∧) = (&&)-x ∈ lst = elem x lst
− test.hs
@@ -1,23 +0,0 @@-{-# LANGUAGE UnicodeSyntax, PatternGuards #-}-module Test where--import Control.Monad.Reader-import Data.Maybe-import Data.Tree-import Data.List--import Unicode-import Types-import TodoLoader-import TodoTree-import CommandParser-import Config-import CmdLine--cfg = Config False False (Limit 20) (Limit 20) Nothing Nothing "%d" Empty--test file = do- todos ← loadTodo Nothing [file]- printTodos cfg todos- putStrLn ""-
− test.txt
@@ -1,11 +0,0 @@-Line of text-Text-TODO: * one todo-description-text-text-TODO: * another todo-descr-text-text-TODO: + last todo
todos.cabal view
@@ -1,5 +1,5 @@ Name: todos-Version: 0.2+Version: 0.3 Cabal-Version: >= 1.6 License: BSD3 License-File: LICENSE@@ -13,16 +13,60 @@ plain-text file, and todos allows you to show only needed of them. So, todos works as specialized `grep' utility. -Extra-source-files: CmdLine.hs CommandParser.hs Config.hs ConstrSet.hs Dates.hs IO.hs- Makefile README README.ru Setup.hs test.hs test.txt TODO- TodoLoader.hs TodoParser.hs todos.hs TodoTree.hs Types.hs- Unicode.hs Dot.hs Color.hs Shapes.hs+Extra-source-files: Makefile README README.ru TODO -Executable todos- Build-Depends: base >= 3 && <= 5, haskell98, utf8-string, containers, parsec >= 2 && < 3,+library+ Exposed-Modules: Todos+ Todos.CmdLine+ Todos.Color+ Todos.ParserTypes+ Todos.Dates+ Todos.CommandParser+ Todos.Parser+ Todos.Config+ Todos.ConfigInstances+ Todos.ConfigUtils+ Todos.ReadConfig+ Todos.Dot+ Todos.IO+ Todos.Main+ Todos.Shapes+ Todos.Loader+ Todos.Tree+ Todos.Types+ Todos.Unicode+ Todos.Formatters+ Todos.Print++ Build-Depends: base >= 3 && <= 5, haskell98, containers, parsec >= 2 && < 3, syb, mtl, ansi-terminal, Glob, time, regex-pcre, directory, filepath,- process, data-hash- Main-Is: todos.hs+ process, data-hash, dyre, utf8-string++ ghc-options: -fwarn-unused-imports++Executable todos+ Main-Is: todos.hs+ Other-Modules: Todos+ Todos.CmdLine+ Todos.Color+ Todos.ParserTypes+ Todos.Dates+ Todos.CommandParser+ Todos.Parser+ Todos.Config+ Todos.ConfigUtils+ Todos.ConfigInstances+ Todos.ReadConfig+ Todos.Dot+ Todos.IO+ Todos.Main+ Todos.Shapes+ Todos.Loader+ Todos.Tree+ Todos.Types+ Todos.Unicode+ Todos.Formatters+ Todos.Print Source-repository head type: git
todos.hs view
@@ -1,55 +1,7 @@-{-# LANGUAGE UnicodeSyntax, PatternGuards #-}--import Prelude hiding (putStrLn,readFile,getContents,print)-import IO-import Codec.Binary.UTF8.String-import System (getArgs)-import System.Exit-import System.Cmd (system)--import Control.Monad.Reader-import Data.Maybe-import Data.Tree-import Data.List+{-# LANGUAGE UnicodeSyntax #-} -import Unicode-import Types-import TodoLoader-import TodoTree-import CommandParser-import Config-import CmdLine-import Dates (getCurrentDateTime)-import Dot+import Todos main ∷ IO ()-main = do- currDate ← getCurrentDateTime - config ← readConfig- args ← getArgs- let (loptions, files') = parseCmdLine currDate (config ⧺ args)- files ← glob files'- case loptions of- O qflags mflags oflags lflags → - do- let options = O qflags mflags oflags lflags- q = buildQuery options- todos ← loadTodo q currDate files- let todos' = delTag "-" todos- queried = transformList q (composeAll currDate) todos'- format item = item {itemDescr = printfItem (descrFormat q) item}- case commandToRun q of- JustShow → do- printTodos q (mapT format queried)- putStrLn ""- ShowAsDot → - putStrLn $ showAsDot (mapT format queried)- SystemCommand cmd → do- forT selected (\item → system $ printfItem cmd (format item))- return ()- where selected | outOnlyFirst q = [Node (rootLabel $ head queried) []]- | otherwise = queried-- Help → do putStrLn usage- exitWith ExitSuccess+main = todos defaultConfig