1.0 - first working version!
This commit is contained in:
parent
5843a137f8
commit
8b6443164a
6
TODO
6
TODO
|
@ -1,5 +1,11 @@
|
||||||
|
handle clipboard errors
|
||||||
|
|
||||||
make it look nicer :)
|
make it look nicer :)
|
||||||
|
|
||||||
fix import/exports to be conservative
|
fix import/exports to be conservative
|
||||||
|
|
||||||
write tests
|
write tests
|
||||||
|
|
||||||
|
theme selection config
|
||||||
|
|
||||||
|
refactor to use more Text?
|
||||||
|
|
|
@ -9,6 +9,8 @@ module AppTypes where
|
||||||
|
|
||||||
import JSONTypes
|
import JSONTypes
|
||||||
import Lens.Micro.TH
|
import Lens.Micro.TH
|
||||||
|
import Brick.Widgets.Edit (Editor(..))
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
-- | Contains the different elements which we
|
-- | Contains the different elements which we
|
||||||
-- might want brick to be able to identify
|
-- might want brick to be able to identify
|
||||||
|
@ -26,14 +28,15 @@ data ScrollDirection = Up | Down
|
||||||
|
|
||||||
-- | The state of our app
|
-- | The state of our app
|
||||||
data AppS = AppS
|
data AppS = AppS
|
||||||
{ _appMode :: Mode -- ^ The current mode of the app
|
{ _appMode :: Mode -- ^ The current mode of the app
|
||||||
, _appCursor :: Int -- ^ The selected into the listing
|
, _appCursor :: Int -- ^ The selected into the listing
|
||||||
, _appExpanded :: Bool -- ^ If the currently selected listing is expanded
|
, _appExpanded :: Bool -- ^ If the currently selected listing is expanded
|
||||||
, _appPage :: Int -- ^ The page currently being viewed
|
, _appPage :: Int -- ^ The page currently being viewed
|
||||||
, _appListing :: Maybe JSONListMovies -- ^ The movies being browsed
|
, _appListing :: JSONListMovies -- ^ The movies being browsed
|
||||||
, _appDetails :: Maybe JSONMovie -- ^ The movie being focused
|
, _appDetails :: Maybe JSONMovie -- ^ The movie being focused
|
||||||
, _appMessage :: Maybe String -- ^ The message to be shown in message mode
|
, _appMessage :: Maybe String -- ^ The message to be shown in message mode
|
||||||
, _appContinue :: Bool -- ^ If to continue after showing message
|
, _appContinue :: Bool -- ^ If to continue after showing message
|
||||||
|
, _appEditor :: Editor T.Text Ident -- ^ The state for the editor widget
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
makeLenses ''AppS
|
makeLenses ''AppS
|
||||||
|
|
|
@ -11,6 +11,7 @@ module JSONTypes where
|
||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Aeson.Types
|
import Data.Aeson.Types
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
|
||||||
data JSONResponse d = JSONResponse
|
data JSONResponse d = JSONResponse
|
||||||
|
@ -23,7 +24,7 @@ instance (FromJSON d) => FromJSON (JSONResponse d) where
|
||||||
parseJSON (Object v) = JSONResponse
|
parseJSON (Object v) = JSONResponse
|
||||||
<$> v .: "status"
|
<$> v .: "status"
|
||||||
<*> v .: "status_message"
|
<*> v .: "status_message"
|
||||||
<*> v .: "data"
|
<*> v .:? "data"
|
||||||
parseJSON invalid =
|
parseJSON invalid =
|
||||||
prependFailure "parsing JSONResponse failed, "
|
prependFailure "parsing JSONResponse failed, "
|
||||||
(typeMismatch "Object" invalid)
|
(typeMismatch "Object" invalid)
|
||||||
|
@ -31,8 +32,8 @@ instance (FromJSON d) => FromJSON (JSONResponse d) where
|
||||||
data JSONListMovies = JSONListMovies
|
data JSONListMovies = JSONListMovies
|
||||||
{ moviesCount :: Int
|
{ moviesCount :: Int
|
||||||
, moviesLimit :: Int
|
, moviesLimit :: Int
|
||||||
, pagenumber :: Int
|
, moviesPage :: Int
|
||||||
, movies :: [JSONMovie]
|
, moviesMovies :: [JSONMovie]
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
instance FromJSON JSONListMovies where
|
instance FromJSON JSONListMovies where
|
||||||
|
@ -40,7 +41,7 @@ instance FromJSON JSONListMovies where
|
||||||
<$> v .: "movie_count"
|
<$> v .: "movie_count"
|
||||||
<*> v .: "limit"
|
<*> v .: "limit"
|
||||||
<*> v .: "page_number"
|
<*> v .: "page_number"
|
||||||
<*> v .: "movies"
|
<*> v .:? "movies" .!= []
|
||||||
parseJSON invalid =
|
parseJSON invalid =
|
||||||
prependFailure "parsing JSONListMovies failed, "
|
prependFailure "parsing JSONListMovies failed, "
|
||||||
(typeMismatch "Object" invalid)
|
(typeMismatch "Object" invalid)
|
||||||
|
|
|
@ -11,3 +11,8 @@ infixl 3 !?
|
||||||
[] !? i = Nothing
|
[] !? i = Nothing
|
||||||
(x:xs) !? 0 = Just x
|
(x:xs) !? 0 = Just x
|
||||||
(x:xs) !? n = xs !? (n-1)
|
(x:xs) !? n = xs !? (n-1)
|
||||||
|
|
||||||
|
infixl 0 $>
|
||||||
|
-- | Backwards function application
|
||||||
|
($>) :: a -> (a -> b) -> b
|
||||||
|
x $> f = f x
|
||||||
|
|
55
src/UI.hs
55
src/UI.hs
|
@ -1,3 +1,5 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Module : UI
|
Module : UI
|
||||||
Description : This is the code which interacts with Brick
|
Description : This is the code which interacts with Brick
|
||||||
|
@ -8,9 +10,13 @@ module UI where
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
|
|
||||||
import Brick hiding (Direction(..))
|
import Brick hiding (Direction(..))
|
||||||
|
import Brick.Types (handleEventLensed)
|
||||||
|
import Brick.Util (fg)
|
||||||
|
import Brick.Widgets.Edit (Editor(..), handleEditorEvent, editorText, editAttr, getEditContents)
|
||||||
import Graphics.Vty (defAttr)
|
import Graphics.Vty (defAttr)
|
||||||
import Graphics.Vty.Input.Events
|
import Graphics.Vty.Input.Events
|
||||||
import Graphics.Vty.Attributes (withStyle, reverseVideo)
|
import Graphics.Vty.Attributes (withStyle, reverseVideo)
|
||||||
|
import Graphics.Vty.Attributes.Color (brightBlack)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Lens.Micro
|
import Lens.Micro
|
||||||
|
@ -30,10 +36,11 @@ initialState = AppS
|
||||||
, _appCursor = 0
|
, _appCursor = 0
|
||||||
, _appExpanded = False
|
, _appExpanded = False
|
||||||
, _appPage = 1
|
, _appPage = 1
|
||||||
, _appListing = Nothing
|
, _appListing = JSONListMovies 0 0 0 []
|
||||||
, _appDetails = Nothing
|
, _appDetails = Nothing
|
||||||
, _appMessage = Nothing
|
, _appMessage = Nothing
|
||||||
, _appContinue = True
|
, _appContinue = True
|
||||||
|
, _appEditor = editorText Input (Just 1) mempty
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Our brick app specification
|
-- | Our brick app specification
|
||||||
|
@ -50,16 +57,20 @@ app = App
|
||||||
runApp :: IO AppS
|
runApp :: IO AppS
|
||||||
runApp = defaultMain app initialState
|
runApp = defaultMain app initialState
|
||||||
|
|
||||||
|
setMovies :: AppS -> IO (Either T.Text JSONListMovies) -> IO AppS
|
||||||
|
setMovies s mb = do
|
||||||
|
m <- mb
|
||||||
|
case m of
|
||||||
|
(Left t) -> pure (displayMessage s True (T.unpack t))
|
||||||
|
(Right m) -> pure (s & appListing .~ m
|
||||||
|
& appCursor .~ 0
|
||||||
|
& appDetails .~ (moviesMovies m !? 0))
|
||||||
|
|
||||||
-- | The starting event which grabs the inital listing
|
-- | The starting event which grabs the inital listing
|
||||||
startEvent :: AppS -> EventM Ident AppS
|
startEvent :: AppS -> EventM Ident AppS
|
||||||
startEvent s = do
|
startEvent s = do
|
||||||
-- todo move unwrapping our response structure into a function
|
-- todo move unwrapping our response structure into a function
|
||||||
response <- liftIO getMovies
|
pure =<< liftIO (setMovies s getMovies)
|
||||||
pure $ case response of
|
|
||||||
(Left msg) -> s & appMessage .~ Just (T.unpack msg)
|
|
||||||
& appContinue .~ False
|
|
||||||
(Right listing) -> s & appListing .~ Just listing
|
|
||||||
& appDetails .~ (movies listing !? s ^. appCursor)
|
|
||||||
|
|
||||||
-- | The drawing function which defers to the proper function
|
-- | The drawing function which defers to the proper function
|
||||||
-- from UI.Widgets
|
-- from UI.Widgets
|
||||||
|
@ -78,18 +89,20 @@ chooseCursor = showFirstCursor
|
||||||
scroll :: ScrollDirection -> AppS -> AppS
|
scroll :: ScrollDirection -> AppS -> AppS
|
||||||
scroll d s = s & appCursor .~ newCursor
|
scroll d s = s & appCursor .~ newCursor
|
||||||
& appExpanded .~ False
|
& appExpanded .~ False
|
||||||
& appDetails .~ (s ^. appListing >>= ((!? newCursor) . movies))
|
& appDetails .~ (moviesMovies (s ^. appListing) !? newCursor)
|
||||||
where
|
where
|
||||||
upperLimit = fromMaybe 0 (subtract 1 . length . movies <$> s ^. appListing)
|
upperLimit = length (moviesMovies (s ^. appListing)) - 1
|
||||||
new = case d of { Up -> (subtract 1); Down -> (+1) }
|
new = case d of { Up -> (subtract 1); Down -> (+1) }
|
||||||
newCursor = max 0 (min upperLimit (new (s ^. appCursor)))
|
newCursor = max 0 (min upperLimit (new (s ^. appCursor)))
|
||||||
|
|
||||||
|
|
||||||
-- | Given a String, modify our state so that string
|
-- | Given a String, modify our state so that string
|
||||||
-- is displayed as a message
|
-- is displayed as a message, alternatively forcing
|
||||||
displayMessage :: AppS -> String -> AppS
|
-- an exit after the message is aknowledged.
|
||||||
displayMessage s str = s & appMode .~ Message
|
displayMessage :: AppS -> Bool -> String -> AppS
|
||||||
& appMessage .~ Just str
|
displayMessage s fatal str = s & appMode .~ Message
|
||||||
|
& appMessage .~ Just str
|
||||||
|
& appContinue .~ not fatal
|
||||||
|
|
||||||
-- | Copy the magnet link of the focused movie at
|
-- | Copy the magnet link of the focused movie at
|
||||||
-- the given index
|
-- the given index
|
||||||
|
@ -101,17 +114,22 @@ copyMagnet s i = case (do
|
||||||
Nothing -> continue s
|
Nothing -> continue s
|
||||||
(Just magnet) -> do
|
(Just magnet) -> do
|
||||||
liftIO (setClipboardString magnet)
|
liftIO (setClipboardString magnet)
|
||||||
continue (displayMessage s "Copied magnet link to clipboard!")
|
continue (displayMessage s False "Copied magnet link to clipboard!")
|
||||||
|
|
||||||
-- The event handler, takes care of keyboard events.
|
-- The event handler, takes care of keyboard events.
|
||||||
eventHandler :: AppS -> BrickEvent Ident () -> EventM Ident (Next AppS)
|
eventHandler :: AppS -> BrickEvent Ident () -> EventM Ident (Next AppS)
|
||||||
eventHandler s (VtyEvent (EvKey k _)) =
|
eventHandler s (VtyEvent e@(EvKey k _)) =
|
||||||
case s ^. appMode of
|
case s ^. appMode of
|
||||||
Message -> if s ^. appContinue
|
Message -> if s ^. appContinue
|
||||||
then continue (s & appMode .~ Browse
|
then continue (s & appMode .~ Browse
|
||||||
& appMessage .~ Nothing)
|
& appMessage .~ Nothing)
|
||||||
else halt s
|
else halt s
|
||||||
Search -> undefined
|
Search -> case k of
|
||||||
|
(KEnter) -> do
|
||||||
|
let queryText = T.unlines (getEditContents (s ^. appEditor))
|
||||||
|
newS <- liftIO (setMovies s (queryMovies queryText))
|
||||||
|
continue (newS & appMode .~ Browse)
|
||||||
|
_ -> continue =<< handleEventLensed s appEditor handleEditorEvent e
|
||||||
Browse -> case k of
|
Browse -> case k of
|
||||||
(KChar 'q') -> halt s
|
(KChar 'q') -> halt s
|
||||||
(KEsc) -> halt s
|
(KEsc) -> halt s
|
||||||
|
@ -137,10 +155,11 @@ eventHandler s (VtyEvent (EvKey k _)) =
|
||||||
(KChar '/') -> continue (s & appMode .~ Search)
|
(KChar '/') -> continue (s & appMode .~ Search)
|
||||||
(KChar 's') -> continue (s & appMode .~ Search)
|
(KChar 's') -> continue (s & appMode .~ Search)
|
||||||
|
|
||||||
_ -> continue s
|
_ -> continueWithoutRedraw s
|
||||||
|
|
||||||
eventHandler s _ = continue s
|
eventHandler s _ = continue s
|
||||||
|
|
||||||
-- | The attribute map, currently not dependant on state
|
-- | The attribute map, currently not dependant on state
|
||||||
attributeMap :: AppS -> AttrMap
|
attributeMap :: AppS -> AttrMap
|
||||||
attributeMap = const $ attrMap defAttr [(attrName "selected", withStyle defAttr reverseVideo)]
|
attributeMap = const $ attrMap defAttr [ (attrName "selected", withStyle defAttr reverseVideo)
|
||||||
|
, (editAttr, fg brightBlack) ]
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Brick
|
||||||
import Brick.Main
|
import Brick.Main
|
||||||
import Brick.Widgets.Center
|
import Brick.Widgets.Center
|
||||||
import Brick.Widgets.Border
|
import Brick.Widgets.Border
|
||||||
|
import Brick.Widgets.Edit
|
||||||
import Brick.AttrMap (attrMap)
|
import Brick.AttrMap (attrMap)
|
||||||
import Graphics.Vty (defAttr)
|
import Graphics.Vty (defAttr)
|
||||||
import Graphics.Vty.Input.Events
|
import Graphics.Vty.Input.Events
|
||||||
|
@ -44,9 +45,9 @@ widgetCons m (w, s) =
|
||||||
embed x = (x <=> w, s)
|
embed x = (x <=> w, s)
|
||||||
|
|
||||||
-- | Returns a big list of all movies
|
-- | Returns a big list of all movies
|
||||||
movieWidgets :: AppS -> JSONListMovies -> Widget Ident
|
movieWidgets :: AppS -> Widget Ident
|
||||||
movieWidgets s m = let (items, _) = foldr widgetCons (emptyWidget, s) (movies m)
|
movieWidgets s = let (items, _) = foldr widgetCons (emptyWidget, s) (moviesMovies (s ^. appListing))
|
||||||
in items
|
in items
|
||||||
|
|
||||||
-- | Returns a single movie listing
|
-- | Returns a single movie listing
|
||||||
movieWidget :: AppS -> JSONMovie -> Widget Ident
|
movieWidget :: AppS -> JSONMovie -> Widget Ident
|
||||||
|
@ -61,23 +62,34 @@ expandedWidget s m = movieWidget s m
|
||||||
<=> (padRight (Pad 2) (str "Magnets") <+> str (listTorrents m))
|
<=> (padRight (Pad 2) (str "Magnets") <+> str (listTorrents m))
|
||||||
<=> (padRight (Pad 2) (str "Summary") <+> txtWrap (movieSummary m))
|
<=> (padRight (Pad 2) (str "Summary") <+> txtWrap (movieSummary m))
|
||||||
|
|
||||||
-- | The search mode widget (unimplemented)
|
-- | The search mode widget. Uses the brick built in Editor widget
|
||||||
searchWidget :: AppS -> Widget Ident
|
searchWidget :: AppS -> Widget Ident
|
||||||
searchWidget = undefined
|
searchWidget s = center $ border $
|
||||||
|
padAll 1 (editorRenderer (s ^. appEditor))
|
||||||
|
<=> padAll 1 (hCenter (str "[Press enter to search]"))
|
||||||
|
|
||||||
|
editorRenderer :: Editor T.Text Ident -> Widget Ident
|
||||||
|
editorRenderer e = renderEditor render True e
|
||||||
|
where
|
||||||
|
render :: [T.Text] -> Widget Ident
|
||||||
|
render [] = txt "Enter query term" $> withAttr editAttr
|
||||||
|
render [""] = txt "Enter query term" $> withAttr editAttr
|
||||||
|
render (t:_) = txt t
|
||||||
|
|
||||||
-- | The browse mode widget which returns a full listing of movies
|
-- | The browse mode widget which returns a full listing of movies
|
||||||
-- or reports that there are no movies to list
|
-- or reports that there are no movies to list
|
||||||
browseWidget :: AppS -> Widget Ident
|
browseWidget :: AppS -> Widget Ident
|
||||||
browseWidget s =
|
browseWidget s =
|
||||||
headings <=> case (s ^. appListing) of
|
str "Title" <+> padLeft Max (str "Year")
|
||||||
Nothing -> center $ str "No movies found matching query."
|
<=> viewport Listing Vertical (movieWidgets s)
|
||||||
(Just m) -> (viewport Listing Vertical (movieWidgets s m))
|
|
||||||
where
|
|
||||||
headings = str "Title" <+> padLeft Max (str "Year")
|
|
||||||
|
|
||||||
-- | The message widget which simply shows a message and informs
|
-- | The message widget which simply shows a message and informs
|
||||||
-- the user of how to escape
|
-- the user of how to escape
|
||||||
messageWidget :: AppS -> Widget Ident
|
messageWidget :: AppS -> Widget Ident
|
||||||
messageWidget s = center $ border $
|
messageWidget s = center $ border $
|
||||||
padAll 1 (hCenter (str (fromMaybe "Unkown Error!" (s ^. appMessage))))
|
padAll 1 (hCenter (str (fromMaybe "Unkown Error!" (s ^. appMessage))))
|
||||||
<=> padAll 1 (hCenter (str "(Press any key to continue)"))
|
<=> padAll 1 (hCenter (str ("[Press any key to " <> action <> "]")))
|
||||||
|
where
|
||||||
|
action = if s ^. appContinue
|
||||||
|
then "continue"
|
||||||
|
else "exit"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user