packages feed

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 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