ureader (empty) → 0.1.0.0
raw patch · 8 files changed
+762/−0 lines, 8 filesdep +ansi-wl-pprintdep +asyncdep +basesetup-changed
Dependencies added: ansi-wl-pprint, async, base, bytestring, containers, curl, data-default, directory, download-curl, feed, filepath, implicit-params, network, old-locale, optparse-applicative, parallel-io, split, tagsoup, terminal-size, text, time, xml
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- src/Main.hs +116/−0
- src/UReader/Localization.hs +57/−0
- src/UReader/Options.hs +149/−0
- src/UReader/RSS.hs +95/−0
- src/UReader/Rendering.hs +235/−0
- ureader.cabal +78/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Sam T.++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Sam T. nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Main.hs view
@@ -0,0 +1,116 @@+module Main (main) where++import Control.Applicative as A+import Control.Concurrent+import Control.Concurrent.Async+import Control.Exception+import Control.Monad+import Data.Default+import Data.List as L+import Data.Maybe+import Data.Time+import Data.Time.Clock.POSIX+import Data.Version (showVersion)+import Network.URI+import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>), (</>), width)+import Text.RSS.Syntax+import System.Directory+import System.FilePath ((<.>))+import System.IO++import Paths_ureader++import UReader.Localization+import UReader.Options+import UReader.Rendering+import UReader.RSS+++parseFeedList :: String -> [URI]+parseFeedList = mapMaybe parseURI . L.lines++getFeedList :: FilePath -> IO [URI]+getFeedList feedList = parseFeedList <$> Prelude.readFile feedList+++getLastSeen :: FilePath -> IO UTCTime+getLastSeen lastPath = do+ exist <- doesFileExist lastPath+ if exist+ then do+ !mLastSeen <- parsePubDate <$> Prelude.readFile lastPath+ Prelude.writeFile lastPath . formatPubDate =<< getCurrentTime+ return $ fromMaybe epochStart mLastSeen+ else do+ Prelude.writeFile lastPath . formatPubDate =<< getCurrentTime+ return epochStart+ where+ epochStart = posixSecondsToUTCTime 0++putBroken :: [(URI, SomeException)] -> IO ()+putBroken broken = do+ forM_ broken $ \(url, e) ->+ hPrint stderr $ red $ text $ show url ++ " - " ++ show e++timestampExt :: FilePath+timestampExt = "lastseen"++fetch :: Maybe UTCTime -> [URI] -> IO [RSS]+fetch t uris = do+ (broken, feeds) <- fetchFeeds t uris+ putBroken broken+ return feeds++filterNew :: FilePath -> [URI] -> IO [RSS]+filterNew feedList uris = do+ lastSeen <- getLastSeen (feedList <.> timestampExt)+ feeds <- fetch (Just lastSeen) uris+ let isNew item = pubDate item > Just lastSeen+ let userFeeds = L.map (filterItems isNew) feeds+ unless (L.all emptyFeed userFeeds) $ do+ localTime <- utcToLocalTime <$> getCurrentTimeZone <*> pure lastSeen+ print $ green $ "Showed from:" <+> text (formatPubDate localTime)+ return userFeeds++previewFeed :: URI -> IO ()+previewFeed = getRSS >=> setCurrentZone >=> renderRSS def . return++showBatch :: Style -> FilePath -> [URI] -> IO ()+showBatch style @ Style {..} feedList uris = do+ renderRSS style =<< setCurrentZone =<<+ (if newOnly then filterNew feedList else fetch Nothing) uris++streamStyle :: Style+streamStyle = Style+ { feedOrder = OldFirst+ , feedDesc = False+ , feedMerge = True+ , newOnly = True+ }++updateStream :: FilePath -> [URI] -> IO ()+updateStream feedList uris+ = filterNew feedList uris >>= setCurrentZone >>= renderRSS streamStyle++pollBy :: Int -> IO () -> IO ()+pollBy interval action = forever $ do+ end <- async $ handle handler action+ threadDelay $ interval * 1000000+ wait end+ where+ handler :: SomeException -> IO ()+ handler = print++streamFeeds :: FilePath -> Int -> [URI] -> IO ()+streamFeeds feedList interval uris =+ pollBy interval $ do updateStream feedList uris++run :: Options -> IO ()+run Add {..} = appendFile feedList $ show feedURI ++ "\n"+run Batch {..} = getFeedList feedList >>= showBatch feedStyle feedList+run Preview {..} = previewFeed feedURI+run Stream {..} = getFeedList feedList >>= streamFeeds feedList feedInterval+run Version = putStrLn $ "ureader version " ++ showVersion version++main :: IO ()+main = getOptions >>= run
+ src/UReader/Localization.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE OverlappingInstances #-}+module UReader.Localization+ ( pubDate+ , parsePubDate+ , formatPubDate++ , LocalZone (..)+ , setCurrentZone+ ) where++import Control.Applicative+import Control.Monad+import Data.Implicit+import Data.Time+import Text.RSS.Syntax+import System.Locale++pubDate :: RSSItem -> Maybe UTCTime+pubDate = parsePubDate <=< rssItemPubDate++pubDateFormat :: String+pubDateFormat = "%a, %e %b %Y %H:%M:%S %Z"++parsePubDate :: DateString -> Maybe UTCTime+parsePubDate = parseTime defaultTimeLocale pubDateFormat++formatPubDate :: FormatTime t => t -> DateString+formatPubDate = formatTime defaultTimeLocale pubDateFormat++localizeUTC :: Implicit_ TimeZone => UTCTime -> LocalTime+localizeUTC = utcToLocalTime param_++setCurrentZone :: LocalZone a => a -> IO a+setCurrentZone x = (localize x $~) <$> getCurrentTimeZone++class LocalZone a where+ localize :: Implicit_ TimeZone => a -> a++instance (Functor f, LocalZone a) => LocalZone (f a) where+ localize = fmap localize++instance LocalZone DateString where+ localize ds = maybe ds (formatPubDate . localizeUTC) $ parsePubDate ds++instance LocalZone RSSItem where+ localize item = item { rssItemPubDate = localize (rssItemPubDate item) }++instance LocalZone RSSChannel where+ localize chan @ RSSChannel {..} = chan+ { rssPubDate = localize rssPubDate+ , rssLastUpdate = localize rssLastUpdate+ , rssItems = fmap localize rssItems+ }++instance LocalZone RSS where+ localize rss @ RSS {..} = rss { rssChannel = localize rssChannel }
+ src/UReader/Options.hs view
@@ -0,0 +1,149 @@+module UReader.Options+ ( Order (..)+ , Style (..)++ , Options (..)+ , getOptions+ ) where++import Control.Monad+import Data.Implicit+import Data.Monoid+import Network.URI+import Options.Applicative+import System.Directory+import System.FilePath++import UReader.Rendering+++styleParser :: Parser Style+styleParser = Style+ <$> option+ ( long "order"+ <> short 'o'+ <> metavar "ORDER"+ <> value NewFirst <> showDefault+ <> help "Specifies entries order: either NewFirst or OldFirst"+ )+ <*> switch+ ( long "description"+ <> short 'd'+ <> help "Include auxilary info like rss version and channel title"+ )+ <*> switch+ ( long "merge"+ <> short 'm'+ <> help "Merge multiple feed info one by time"+ )+ <*> switch+ ( long "unread"+ <> short 'u'+ <> help "Show only unread feed and ignore feed will shown in future"+ )++feedListParser :: Implicit_ FilePath => Parser FilePath+feedListParser = option+ ( long "feeds"+ <> metavar "PATH"+ <> value param_ <> showDefault+ <> help "Path to file with list of feed urls"+ )++feedLinkParser :: Parser URI+feedLinkParser = argument parseURI+ ( metavar "URI"+ <> help "URI to feed"+ )++intervalTime :: Parser Int+intervalTime = option+ ( long "interval"+ <> short 'i'+ <> metavar "SECONDS"+ <> value 600 <> showDefault+ <> help "Minimal interval between feed updates"+ )++data Options+ = Add { feedList :: FilePath+ , feedURI :: URI+ }+ | Batch { feedList :: FilePath+ , feedStyle :: Style+ }+ | Preview { feedURI :: URI }+ | Stream { feedList :: FilePath+ , feedInterval :: Int+ }+ | Version+ deriving (Show, Eq)++addParser :: Implicit_ String => Parser Options+addParser = Add <$> feedListParser <*> feedLinkParser++addInfo :: Implicit_ String => ParserInfo Options+addInfo = info (helper <*> addParser) modifier+ where+ modifier = progDesc "Add a feed to the feed list"++streamParser :: Implicit_ String => Parser Options+streamParser = Stream <$> feedListParser <*> intervalTime++streamInfo :: Implicit_ String => ParserInfo Options+streamInfo = info (helper <*> streamParser) modifier+ where+ modifier = progDesc "Show feed as never ending stream"++feedParser :: Implicit_ String => Parser Options+feedParser = Batch <$> feedListParser <*> styleParser++feedInfo :: Implicit_ String => ParserInfo Options+feedInfo = info (helper <*> feedParser) modifier+ where+ modifier = progDesc "Show all feeds specified in feed list"++previewParser :: Parser Options+previewParser = Preview <$> feedLinkParser++previewInfo :: ParserInfo Options+previewInfo = info (helper <*> previewParser) modifier+ where+ modifier = progDesc "Show feed specified by URI or file path"++versionParser :: Parser Options+versionParser = flag' Version+ ( long "version"+ <> short 'V'+ <> hidden+ <> help "Show program version and exit"+ )++optionsParser :: Implicit_ String => Parser Options+optionsParser = subparser $ mconcat+ [ command "add" addInfo+ , command "feed" feedInfo+ , command "stream" streamInfo+ , command "view" previewInfo+ ]++optionsInfo :: Implicit_ String => ParserInfo Options+optionsInfo = info (helper <*> (versionParser <|> optionsParser)) modifier+ where+ modifier = fullDesc <> header hdr <> progDesc desc+ hdr = "ureader is minimalistic CLI RSS reader"+ desc =+ "$ ureader COMMAND --help # for info about particular command"++getDefaultFeeds :: IO FilePath+getDefaultFeeds = do+ udir <- getAppUserDataDirectory "ureader"+ createDirectoryIfMissing False udir+ let configPath = udir </> "feeds"+ exist <- doesFileExist configPath+ unless exist $ do+ writeFile configPath ""+ return configPath++getOptions :: IO Options+getOptions = getDefaultFeeds >>= (execParser optionsInfo $~)
+ src/UReader/RSS.hs view
@@ -0,0 +1,95 @@+module UReader.RSS+ ( filterItems+ , reverseItems+ , emptyFeed++ , getRSS+ , fetchFeeds++ , resolveComments+ ) where++import Control.Applicative+import Control.Arrow+import Control.Concurrent.ParallelIO+import Control.Exception+import Data.Char+import Data.Either+import Data.List as L+import Data.Maybe+import Data.Text.Encoding as T+import Data.Time+import Network.URI+import Network.Curl+import Network.Curl.Download+import Text.RSS.Syntax+import Text.RSS.Import+import Text.XML.Light.Input+import System.Locale+++-- TODO filter by time = drop . find+filterItems :: (RSSItem -> Bool) -> RSS -> RSS+filterItems p rss = rss+ { rssChannel = let ch = rssChannel rss+ in ch { rssItems = L.filter p (rssItems ch) }+ } -- TODO use lens++reverseItems :: RSS -> RSS+reverseItems rss = rss+ { rssChannel = let ch = rssChannel rss+ in ch { rssItems = L.reverse (rssItems ch) }+ }++emptyFeed :: RSS -> Bool+emptyFeed = L.null . rssItems . rssChannel++-- TODO openAsFeed+getRSS :: URI -> IO RSS+getRSS uri = do+ body <- either (throwIO . userError) return =<< openURI (show uri)+ xml <- maybe (throwIO $ userError "invalid XML") return $+ parseXMLDoc (T.decodeUtf8 body)+ rss <- maybe (throwIO $ userError "invalid RSS") return $+ elementToRSS xml+ return rss++hdrLastModified :: String+hdrLastModified = "last-modified"++lastModifiedFormat :: String+lastModifiedFormat = "%a, %e %b %Y %H:%M:%S %Z"++parseLastModified :: String -> Maybe UTCTime+parseLastModified = parseTime defaultTimeLocale lastModifiedFormat++resourceExpired :: UTCTime -> URI -> IO Bool+resourceExpired threshold uri = do+ (_, headers) <- curlHead (show uri) []+ let cheaders = L.map (first canonicalize) headers+ return $ case L.lookup hdrLastModified cheaders of+ Just (parseLastModified -> Just lm) -> threshold < lm+ _ -> True+ where+ canonicalize = L.map toLower+++getRSSFrom :: Maybe UTCTime -> URI -> IO (Maybe RSS)+getRSSFrom Nothing uri = Just <$> getRSS uri+getRSSFrom (Just t) uri = do+ updated <- resourceExpired t uri+ if updated+ then Just <$> getRSS uri+ else return Nothing++fetchFeeds :: Maybe UTCTime -> [URI]+ -> IO ([(URI, SomeException)], [RSS])+fetchFeeds mtime urls = do+ res <- parallelE (L.map (getRSSFrom mtime) urls)+ return $ second catMaybes $ partitionEithers $ urlfy res+ where+ urlfy = L.zipWith (\url -> either (Left . (,) url) Right) urls+++resolveComments :: RSS -> IO RSS+resolveComments = error "resolveComments"
+ src/UReader/Rendering.hs view
@@ -0,0 +1,235 @@+{-# OPTIONS -fno-warn-orphans #-}+module UReader.Rendering+ ( Order (..)+ , Style (..)+ , renderRSS+ ) where++import Control.Applicative+import Control.Monad+import Data.Char+import Data.Default+import Data.Function+import Data.Maybe+import Data.Monoid+import Data.List as L+import Data.List.Split as L+import Data.Set as S+import Text.HTML.TagSoup+import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>), width)+import Text.RSS.Syntax+import Text.XML.Light.Types+import System.IO+import System.Console.Terminal.Size as Terminal++import UReader.RSS+import UReader.Localization+++data Order = NewFirst+ | OldFirst+ deriving (Show, Read, Eq, Ord, Bounded, Enum)++data Style = Style+ { feedOrder :: !Order+ , feedDesc :: !Bool+ , feedMerge :: !Bool+ , newOnly :: !Bool+ } deriving (Show, Eq)++instance Default Style where+ def = Style+ { feedOrder = NewFirst+ , feedDesc = True+ , feedMerge = False+ , newOnly = False+ }++prettyDesc :: Bool -> RSS -> Doc+prettyDesc keepDesc+ | keepDesc = pretty+ | otherwise+ = vsep . punctuate linebreak . L.map pretty . rssItems . rssChannel++merge :: Bool -> [RSS] -> [RSS]+merge byTime+ | byTime = return . mconcat+ | otherwise = id++formatOrder :: Order -> RSS -> RSS+formatOrder NewFirst = id+formatOrder OldFirst = reverseItems++formatFeeds :: Style -> [RSS] -> Doc+formatFeeds Style {..}+ = vsep . punctuate linebreak+ . L.map (prettyDesc feedDesc . formatOrder feedOrder) . merge feedMerge++renderRSS :: Style -> [RSS] -> IO ()+renderRSS style feeds = do+ Window {..} <- fromMaybe (Window 80 60) <$> Terminal.size+ displayIO stdout $ renderPretty 0.8 width $ formatFeeds style feeds++instance Monoid RSS where+ mempty = nullRSS "" ""+ mappend a b = mempty+ { rssVersion = unwords $ S.toList $+ mergeVersions (rssVersion a) (rssVersion b)+ , rssAttrs = rssAttrs a <> rssAttrs b+ , rssChannel = rssChannel a <> rssChannel b+ , rssOther = rssOther a <> rssOther b+ }+ where+ mergeVersions = (<>) `on` S.fromList . words++instance Monoid RSSChannel where+ mempty = nullChannel "" ""+ mappend a b = mempty+ { rssTitle = rssTitle a <> "|" <> rssTitle b+ , rssLink = rssLink a <> " " <> rssLink b+ , rssDescription = rssDescription a+ , rssItems = mergeBy cmpPubDate (rssItems a) (rssItems b)+ }+ where+ cmpPubDate = (>) `on` (parsePubDate <=< rssItemPubDate)++ mergeBy :: (a -> a -> Bool) -> [a] -> [a] -> [a]+ mergeBy _ [] xs = xs+ mergeBy _ xs [] = xs+ mergeBy f (x : xs) (y : ys)+ | f x y = x : mergeBy f xs (y : ys)+ | otherwise = y : mergeBy f (x : xs) ys++instance Pretty RSS where+ pretty RSS {..} =+ pretty rssChannel </>+ dullblack ("rss version" <+> pretty rssVersion)++instance Pretty RSSChannel where+ pretty RSSChannel {..} =+ vcat (L.zipWith heading (splitOn "|" rssTitle) (words rssLink)) </>+ pretty rssDescription </>+ pretty rssPubDate <$$>+ vcat (punctuate linebreak $ L.map pretty rssItems)+ where+ heading title link = blue (fill 24 (text title)) </> text link++instance Pretty RSSItem where+ pretty RSSItem {..} =+ (bold (magenta (pretty rssItemTitle)) </> pretty rssItemLink) <$$>+ red (hsep $ L.map ppCategory rssItemCategories) <$$>+ indent 2 (maybe mempty ppItemDesc rssItemDescription) <$$>+ (maybe mempty ppComments rssItemComments) <$$>+ (green (pretty rssItemGuid)) <$$>+ (yellow (pretty rssItemPubDate) </>+ maybe mempty ppAuthor rssItemAuthor)++ where+ ppItemDesc = nest 2 . prettySoup False False . extDesc+ ppComments comments = "Comments: " <+> pretty comments+ ppAuthor author = "posted by" <+> red (pretty author)+ ppCategory category = dullblack "*" <> pretty category+++instance Pretty RSSGuid where+ pretty RSSGuid {..}+ | Just True <- rssGuidPermanentURL = "Permalink:" <+> pretty rssGuidValue+ | otherwise = "Link: " <+> pretty rssGuidValue++instance Pretty RSSCategory where+ pretty RSSCategory {..} =+ dullyellow (maybe mempty text rssCategoryDomain) <>+ dullred (hsep $ L.map pretty rssCategoryAttrs) <>+ dullblue (text rssCategoryValue)++instance Pretty Attr where+ pretty Attr {..} = text (show attrKey) <+> "=" <+> text attrVal++extDesc :: String -> [Tag String]+extDesc = canonicalizeTags . parseTags++{- NOTE: the findCloseTag could lead to serious performance+degradation, but this is very unlikely for HTML embedded in RSS. -}++findCloseTag :: Eq a => a -> [Tag a] -> ([Tag a], [Tag a])+findCloseTag t = go (0 :: Int) []+ where+ go _ acc [] = (reverse acc, [])+ go n acc (x : xs) =+ case x of+ TagOpen t' _+ | t == t' -> go (succ n) (x : acc) xs+ | otherwise -> go n (x : acc) xs+ TagClose t'+ | t == t' -> if n == 0+ then (reverse acc, xs)+ else go (pred n) (x : acc) xs+ | otherwise -> go n (x : acc) xs+ _ -> go n (x : acc) xs++prettySoup :: Bool -> Bool -> [Tag String] -> Doc+prettySoup _ _ [] = mempty+prettySoup upper raw (x : xs) = case x of+ TagText t -> text (upperize (canonicalize t))+ <> prettySoup upper raw xs+ where+ canonicalize | raw = id+ | otherwise = L.filter isPrint+ upperize | upper = L.map toUpper+ | otherwise = id++ TagOpen t attrs -> maybe err closeTag $ L.lookup t rules+ where+ rules =+ [ "p" --> \par -> linebreak <> par <> linebreak+ , "i" --> underline+ , "em" --> underline+ , "u" --> underline+ , "strong" --> bold+ , "b" --> bold+ , "tt" --> dullwhite+ , "hr" --> \body -> body <> linebreak <>+ underline (text (L.replicate 72 ' ')) <> linebreak+ , "a" --> \desc -> blue desc </> pretty (L.lookup "href" attrs)+ , "br" --> (linebreak <>)+ , "ul" --> id+ , "li" --> \li -> green "*" <+> li <> linebreak+ , "span" --> id+ , "code" ~-> (onwhite . black)+ , "img" --> \desc -> blue desc </> pretty (L.lookup "src" attrs)+ , "pre" ~-> \body -> linebreak <> align body <> linebreak++ , "h1" ==> heading+ , "h2" --> heading+ , "h3" --> heading+ , "h4" --> heading+ , "h5" --> heading+ , "h6" --> heading++ , "div" --> \body -> linebreak <> body <> linebreak+ , "blockquote" --> indent 4++ , "table" --> \body -> linebreak <> body <> linebreak+ , "tbody" --> id+ , "tr" --> \body -> linebreak <> body <> linebreak+ , "td" --> fill 40+ ]+ where+ a --> f = (a, f . prettySoup False False)+ a ~-> f = (a, f . prettySoup False True)+ a ==> f = (a, f . prettySoup True False)++ heading body = linebreak <> bold (underline body) <> linebreak++ err = red ("<" <> text t <+> def_attrs <> ">")+ <+> prettySoup upper raw xs+ where+ def_attrs = hcat $ punctuate space $ L.map pattr attrs+ where pattr (n, v) = text n <> "=" <> text v++ closeTag m = m a <> prettySoup upper raw b+ where+ (a, b) = findCloseTag t xs++ TagClose t -> red ("</" <> text t <> ">") <+> prettySoup upper raw xs+ t -> text (show t) <> prettySoup upper raw xs
+ ureader.cabal view
@@ -0,0 +1,78 @@+name: ureader+version: 0.1.0.0+license: BSD3+license-file: LICENSE+author: Sam T.+maintainer: Sam T. <pxqr.sta@gmail.com>+copyright: (c) 2013, Sam T.+category: RSS+build-type: Simple+cabal-version: >= 1.10+tested-with: GHC == 7.6.3+homepage: https://github.com/pxqr/ureader+bug-reports: https://github.com/pxqr/ureader/issues+synopsis: Minimalistic CLI RSS reader.+description:++ `ureader` is minimalistic command line RSS reader with unicode+ and color support. Everything it does is fetch RSS documents,+ merge them according to specified options, format and flush+ resulting feed to stdout. So `ureader` could be used with+ pagers like `more(1)` or in linux terminal.+ .+ [/Release Notes/]+ .+ * /0.1.0.0:/ Initial version.+++source-repository head+ type: git+ location: git://github.com/pxqr/ureader.git++executable ureader+ default-language: Haskell2010+ default-extensions: OverloadedStrings+ , PatternGuards+ , RecordWildCards+ , ViewPatterns+ , BangPatterns++ , FlexibleInstances+ , FlexibleContexts+ hs-source-dirs: src+ main-is: Main.hs+ other-modules: UReader.Localization+ UReader.Options+ UReader.Rendering+ UReader.RSS++ build-depends: base == 4.*+ , implicit-params == 0.2.*+ , data-default++ , async+ , parallel-io == 0.3.*++ , bytestring == 0.10.*+ , text == 0.11.*+ , containers == 0.5.*+ , split == 0.2.*++ , time == 1.4.*+ , old-locale == 1.0.*++ , network == 2.4.*+ , curl+ , download-curl++ , directory == 1.2.*+ , filepath == 1.3.*+ , ansi-wl-pprint == 0.6.*+ , optparse-applicative == 0.5.*+ , terminal-size == 0.2.*++ , xml == 1.3.*+ , feed == 0.3.*+ , tagsoup == 0.12.*++ ghc-options: -Wall -O2