rascal-1.1.4: src/Main.hs
--
-- RASCAL, a Haskell cli reddit client
-- Copyright (c) 2013 Sylvain Soliman <Sylvain.Soliman@gmail.com>
-- MIT License, see LICENSE
--
import Control.Monad (when, unless)
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Control.Monad.Trans (liftIO)
import Control.Applicative ((<$>))
import Text.Printf (printf)
import System.Environment (getArgs)
import Data.Char (ord)
import Data.List (intercalate, elemIndices)
import Data.Maybe (isJust)
import System.IO
import Control.Exception (handle)
import Data.Aeson (parseJSON, FromJSON)
import Network.Curl.Aeson (curlAeson, noData, CurlAesonException, errorMsg, curlCode)
import Network.Curl.Opts (CurlOption(CurlUserAgent))
import Network.Curl.Code (CurlCode(CurlOK))
import System.Process (readProcess)
import System.Console.ANSI (clearLine)
import Data.Map ((!))
import Rascal.Constants
import Rascal.Utils
import Rascal.Types
import Rascal.Conf
-- we do not use Show because we depend on (an IO generated) width
showLink :: Link -> Int -> String
showLink l width =
let titlewidth = width - 34
self | isSelf l = green ++ "♦"
| otherwise = " "
color | score l == 0 = blue
| otherwise = red
format = printf " %%s%%3d%%s%%s %%-%d.%ds %%20.20s %%s%%3d%%s "
titlewidth titlewidth
in printf format color (score l) self reset (title l) (author l)
magenta (numComments l) reset
showListing :: NamedListing -> Int -> String
showListing l width =
let lnks = links $ listing l
in bold ++ "\n--=| /r/" ++ name l ++ " |=--\n\n" ++ reset ++
-- the -4 comes from letterizeLines
letterizeLines (map (`showLink` (width - 4)) lnks)
showComment :: Int -> String -> String -> String -> String -> Comment -> [String]
showComment width prefix addedPrefix futurePrefix op
(Comment cauthor ups downs _ body children) =
let prefix' = prefix ++ futurePrefix
author' | cauthor == op = green ++ cauthor ++ reset
| otherwise = cauthor
header = printf "%s─ %.20s (%s%d%s|%s%d%s)" addedPrefix author' red
ups reset blue downs reset
headerBlock = indentString width prefix header
commentBlock = indentString width prefix' (unescape body)
in (prefix ++ "│ "):(headerBlock ++ init commentBlock):
showCommentListing width prefix' op children
showComment _ _ _ _ _ OriginalArticle =
[]
showCommentListing :: Int -> String -> String -> CommentListing -> [String]
showCommentListing width prefix op (CommentListing cl) =
case cl of
[] -> []
_ -> concatMap (showComment width prefix "├" "│ " op) (init cl) ++
showComment width prefix "└" " " op (last cl)
-- |print a listing on screen and ask for a command
displayListing :: NamedListing -> ReaderT RuntimeConf IO ()
displayListing l = do
conf <- ask
let w = textWidth conf
liftIO $ do
putStrLn $ showListing l w
displayCommands w
-- |get posts according to selection in argument's subreddit as a listing
getListing :: String -> String -> Int -> Maybe String -> IO NamedListing
getListing select subreddit cnt aftr =
let apiurl = "http://www.reddit.com/r/" ++ subreddit ++ "/%s.json?count="
++ show cnt ++ maybe "" ("&after=" ++) aftr
in NamedListing (subreddit ++ " -- " ++ select) cnt <$>
getThing apiurl select emptyListing
-- |get posts or comments from an apiurl, a sort order and a default in case
-- of error
getThing :: FromJSON a => String -> String -> a -> IO a
getThing apiurl sort emptyThing =
let sort' = if sort `notElem` map snd availableSorts
then snd . head $ availableSorts
else sort
apiurl' = printf apiurl sort'
in handle (handleCurlAesonException emptyThing) $ do
l <- curlAeson parseJSON "GET" apiurl' [CurlUserAgent userAgent] noData
return $! l
-- |print error message if there is a cURL exception
handleCurlAesonException :: a -> CurlAesonException -> IO a
handleCurlAesonException x e = do
putStrLn $ red ++ "Caught exception: " ++ reset ++ errorMsg e
putStrLn $ if curlCode e == CurlOK
then "(Might indicate a non-existing subreddit)"
else "cURL code: " ++ (drop 4 . show . curlCode) e
return x
-- |open nth link in a listing in given width
open :: NamedListing -> Int -> ReaderT RuntimeConf IO ()
open nl@(NamedListing _ _ (Listing l _)) n = do
conf <- ask
-- n >= 0 by construction on call of open, but...
when (0 <= n && n < length l) $
let ln = (l !! n)
w = textWidth conf
subreddit = takeWhile (/=' ') (name nl)
in do
if isSelf ln
then openSelf ln
else liftIO $ openUrl (link ln) w
openComments subreddit ln
displayListing nl
-- |display a self link, with its contained hrefs
openSelf :: Link -> ReaderT RuntimeConf IO ()
openSelf ln = do
conf <- ask
let refs = hrefs (selfHtml ln)
w = textWidth conf
liftIO $ do
message "" w
putStrLn $ cleanUp (selfText ln) w
if null refs
then waitKey w
else do
putStr "\n"
mapM_ (putStrLn . showRef) $ zip [1..] refs
message "press link number to open or a key to continue" w
openRefs refs w
-- |display all comments of an article in a subreddit
openComments :: String -> Link -> ReaderT RuntimeConf IO ()
openComments subreddit ln = do
conf <- ask
let w = textWidth conf
csort = commentSort conf
when (numComments ln > 0) $ liftIO $ do
(Comments cll) <- getComments subreddit (drop 3 (uid ln)) csort
putStrLn ""
-- the first is OriginalArticle, the length is always 2
unless (null cll) $ do
let allComments = showCommentListing w "" (author ln) (cll !! 1)
if pageComments conf
then doPageComments (textHeight conf - 2) 0 w allComments
else mapM_ putStrLn allComments
waitKey w
doPageComments :: Int -> Int -> Int -> [String] -> IO ()
doPageComments _ _ _ [] = return ()
doPageComments _ _ _ [_] = return () -- should never happen...
doPageComments height shown w allComments@(a:b:remaining)
| shown > height = waitKey w >> doPageComments height 0 w allComments
| otherwise = do
let nbLines = 2 + sum (map (length . elemIndices '\n') [a, b])
empty = height - shown
shown' <- if empty > nbLines
then return shown
else do
waitKey w
-- '\b' is backspace, since even clearLine does not enforce
-- clearing the key typed for input
putStr "\b"
return 0
putStrLn a
putStrLn b
doPageComments height (shown' + nbLines) w remaining
-- |request comments for a given article
getComments :: String -> String -> String -> IO Comments
getComments subreddit article csort =
let apiurl = "http://www.reddit.com/r/" ++ subreddit ++
"/comments/" ++ article ++ ".json?sort=%s"
in getThing apiurl csort emptyComments
-- |open requested hrefs as urls and stop if anything else
openRefs :: [String] -> Int -> IO ()
openRefs l w = do
c <- getChar
clearLine
let n = ord c - ord '1'
when (0 <= n && n < length l) $ do
openUrl (l !! n) w
openRefs l w
main :: IO ()
main = do
hSetBuffering stdin NoBuffering
args <- getArgs
columns <- readProcess "tput" ["cols"] []
nbLines <- readProcess "tput" ["lines"] []
conf <- getUserConfig ".rascalrc" defaultConf
let width = read columns
height = read nbLines
subreddit | null args = conf ! "subreddit"
| otherwise = head args
lSort = conf ! "linkSort"
cSort = conf ! "commentSort"
pComments = reads (conf ! "pageComments")
pComments' | null pComments = True -- TODO get from defaultConf
| otherwise = fst $ head pComments
conf' = RuntimeConf width height cSort lSort pComments'
list <- getListing lSort subreddit 0 Nothing
runReaderT (displayListing list >> loop list) conf'
-- |show possible commands
displayCommands :: Int -> IO ()
displayCommands =
message $ intercalate "/" (map makeCmd availableSorts) ++
"/⟨m⟩ore links/⟨s⟩witch subreddit/open ⟨A-Y⟩"
-- |main event loop
loop :: NamedListing -> ReaderT RuntimeConf IO ()
loop l = do
cmd <- liftIO getChar
liftIO clearLine
conf <- ask
case cmd of
's' -> do
subreddit <- liftIO $ do
putStr "\nsubreddit to switch to: "
hFlush stdout
getLine
list <- liftIO $ getListing (linkSort conf) subreddit 0 Nothing
displayListing list
loop list
'm' -> if isJust (after (listing l))
then do
let subreddit = takeWhile (/= ' ') (name l)
sort = drop 4 (dropWhile (/= ' ') (name l))
cnt = count l + length (links (listing l))
aftr = after $ listing l
list <- liftIO $ getListing sort subreddit cnt aftr
displayListing list
loop list
else do
liftIO $ message "no more posts…" (textWidth conf)
loop l
-- is this one of the sort options?
x | isJust (getFullSort x) -> do
list <- let (Just sort) = getFullSort x
in liftIO $ getListing sort (takeWhile (/=' ') (name l)) 0 Nothing
displayListing list
loop list
-- 25 elements displayed max
| x `elem` ['A'..'Y'] -> do
open l (ord x - ord 'A')
loop l
_ -> return ()