packages feed

mushu (empty) → 0.1.0

raw patch · 21 files changed

+1419/−0 lines, 21 filesdep +QuickCheckdep +asyncdep +attoparsecsetup-changed

Dependencies added: QuickCheck, async, attoparsec, base, brick, bytestring, classy-prelude, connection, containers, data-default, directory, filepath, fuzzy, hscurses, htoml, libmpd, list-t, microlens, microlens-platform, microlens-th, mtl, network, pretty, process, quickcheck-text, safe-exceptions, tasty, tasty-hunit, tasty-quickcheck, template-haskell, text, text-zipper, time, transformers, unordered-containers, vector, vty

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Elie Genard (c) 2016++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 Elie Genard 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.
+ README.md view
@@ -0,0 +1,56 @@+Mushu [![Build Status](https://travis-ci.org/elaye/mushu.svg?branch=master)](https://travis-ci.org/elaye/mushu)+=====++Mushu is a minimalist MPD client with a Text User Interface (TUI) focused on ease of use.+Its main feature is an incremental fuzzy finder for the music library.+The interface is largely inspired by [ncmpcpp](https://github.com/arybczak/ncmpcpp).++![Screenshot](screenshot.png?raw=true "Mushu screenshot")++This client was made to fit my needs so it may not fit yours.+A lot of MPD features are not implemented.++Dependencies+--------------++`libncurses5-dev`++Install+-------++Mushu is available on stack.++  `stack install mushu`++If you don't have `stack` please install it following instructions [here](https://docs.haskellstack.org/en/stable/README/).++Why another MPD client?+-----------------------++I used [ncmpcpp](https://github.com/arybczak/ncmpcpp) for quite some time and realised that I was always doing the same thing with it: search for music -> add to playlist and play.+I was not entirely satisfied with the search in ncmpcpp so I decided to make my own.+Also, ncmpcpp as way too many features that I don't use.++The aim is to keep the number of features to a minimum without being unusable.++I also wanted to develop a real world haskell project and this seemed like a good opportunity.++TODO+----++ - [ ] Add config file (mainly to be able to choose a different server than `localhost:6600` for MPD)+ - [ ] Add MPD error handling and show error in a notification widget+ - [ ] Add MPD playback modes handling+ - UI+   - [ ] Improve help screen+   - [ ] Add top / bottom list keybindings (`G` and `g`)+   - [ ] Add delete from playlist keybinding (`d` or `x`?)+ - [ ] Fix all the bugs and refactor!++Contributing+------------++Contributions are very welcome. However please open an issue before developing anything too big so we can discuss it, I'd hate to have wasted your time if the Pull Request you made can't be merged.+Please read [CONTRIBUTING.md](CONTRIBUTING.md) before contributing.+If you want an overview of the app, check out [DEVELOPER.md](DEVELOPER.md).+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Library.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+module Library+( fetchLibrary+, Library(..)+, ArtistName+, AlbumName+, artistsL+, albumsL+) where++import ClassyPrelude++import TH (makeSuffixLenses)+import MPD+  ( fetchAllArtists+  , fetchAllSongs+  , fetchArtistAlbums+  , fetchArtistAlbumSongs+  )++-- import Network.MPD (Song(..), Artist(..), Album(..), toString)+import Network.MPD (Song(..), Metadata(..), toString)++import Data.Map.Strict (Map(..))+import qualified Data.Map.Strict as Map+import Data.Set (Set(..))+import qualified Data.Set as Set+import Data.Vector (Vector(..))+import qualified Data.Vector as V++type ArtistName = Text+type AlbumName = Text++data Library = Library+  { _artists :: Map ArtistName (Set AlbumName)+  , _albums :: Map AlbumName (Vector Song)+  }++makeSuffixLenses ''Library++fetchLibrary :: IO Library+fetchLibrary = fetchAllSongs >>= (return . mkLibrary)++mkLibrary :: [Song] -> Library+mkLibrary songs = Library { _artists = artists, _albums = albums }+  where+    (artists, albums) = foldl' songsToLibraryFolder (emptyArtists, emptyAlbums) songs+    emptyArtists = Map.empty :: Map ArtistName (Set AlbumName)+    emptyAlbums = Map.empty :: Map AlbumName (Vector Song)++songsToLibraryFolder ::+  (Map ArtistName (Set AlbumName), Map AlbumName (Vector Song))+  -> Song+  -> (Map ArtistName (Set AlbumName), Map AlbumName (Vector Song))+songsToLibraryFolder (artists, albums) song = (newArtists, newAlbums)+  where+    tag key def song = concat (pack <$> toString <$> findWithDefault [fromString def] key (sgTags song))+    newArtists = insertWith union (tag Artist "<unknown>" song) (Set.singleton (tag Album "<untitled>" song)) artists+    newAlbums = insertWith (++) (tag Album "<untitled>" song) (V.singleton song) albums
+ app/MPD.hs view
@@ -0,0 +1,145 @@+module MPD+( currentSong+, togglePlayPause+, fetchPlaylist+, fetchAllSongs+, fetchAllArtists+, fetchArtistAlbums+, fetchArtistAlbumSongs+, fetchStatus+, clearPlaylist+, addArtistToPlaylist+, addAlbumToPlaylist+, addSongToPlaylist+, setVolume+, tag+, mpdReq+) where++import ClassyPrelude++import Control.Exception.Safe (throw)++import Network.MPD+  ( MPD+  , State(..)+  , Status(..)+  , Song(..)+  , Metadata(..)+  , Query+  , Artist+  , Album+  , def+  , withMPD+  , status+  , play+  , pause+  , playlistInfo+  , list+  , search+  , find+  , anything+  , clear+  , findAdd+  , toString+  , (=?), (<&>)+  )+import qualified Network.MPD as M++import Types+import Brick.BChan (BChan(..), writeBChan)++currentSong :: IO (Maybe Song)+currentSong = do+  res <- withMPD M.currentSong+  case res of+    Left err -> print err >> return Nothing+    Right song -> return song++togglePlayPause :: IO ()+togglePlayPause = do+  resStatus <- withMPD status+  case resStatus of+    Left _ -> return ()+    Right st -> do+      case (stState st) of+        Playing -> void $ withMPD $ pause True+        Stopped -> void $ withMPD $ play Nothing+        Paused -> void $ withMPD $ pause False++mpdReq :: MPD [a] -> IO [a]+mpdReq req = do+  res <- withMPD req+  case res of+    Left err -> print ("MPD error: " ++ (show err)) >> return []+    Right r -> return r++fetchAllArtists :: IO [M.Value]+fetchAllArtists = mpdReq $ list Artist Nothing++fetchArtistAlbums :: Artist -> IO (Artist, [Album])+fetchArtistAlbums artist = do+  albums <- mpdReq $ list Album (Just artist)+  return (artist, albums)++fetchArtistAlbumSongs :: Artist -> Album -> IO [Song]+fetchArtistAlbumSongs artist album = mpdReq req+  where req = search (Artist =? artist <&> Album =? album)++fetchAllSongs :: IO [Song]+fetchAllSongs = mpdReq $ search (Artist =? fromString "")+-- fetchAllSongs = mpdReq $ M.find anything++fetchPlaylist :: IO [Song]+fetchPlaylist = mpdReq $ playlistInfo Nothing++fetchStatus :: IO Status+fetchStatus = do+  st <- withMPD status+  case st of+    Left err -> return def+    Right st -> return st++clearPlaylist :: IO ()+clearPlaylist = void $ withMPD clear++toValue = fromString . unpack++addArtistToPlaylist :: Text -> Bool -> IO ()+addArtistToPlaylist artist play = do+  let query = Artist =? toValue artist+  if play then addToPlaylistAndPlay query else addToPlaylist query++addAlbumToPlaylist :: Maybe Text -> Text -> Bool -> IO ()+addAlbumToPlaylist maybeArtist album play = do+  let+    albumQuery = Album =? toValue album+    query = case maybeArtist of+        Just artist -> Artist =? toValue artist <&> albumQuery+        Nothing -> albumQuery+  if play then addToPlaylistAndPlay query else addToPlaylist query++addSongToPlaylist :: Text -> Text -> Bool -> IO ()+addSongToPlaylist album song play = do+  let query = Album =? toValue album <&> Title =? toValue song+  if play then addToPlaylist query else addToPlaylist query++addToPlaylist :: Query -> IO ()+addToPlaylist query = do+  res <- withMPD $ findAdd query+  case res of+    Left err -> throw MPDException+    Right _ -> return ()++addToPlaylistAndPlay :: Query -> IO ()+addToPlaylistAndPlay query = do+  n <- length <$> fetchPlaylist+  void $ addToPlaylist query+  r <- withMPD $ play (Just n)+  return ()++setVolume :: Int -> IO ()+setVolume volume = void $ withMPD $ M.setVolume volume++tag :: Metadata -> Text -> Song -> Text+tag key def song = concat (pack <$> toString <$> findWithDefault [fromString (unpack def)] key (sgTags song))
+ app/Main.hs view
@@ -0,0 +1,8 @@+module Main where++import ClassyPrelude++import UI (start)++main :: IO ()+main = start
+ app/TH.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE TemplateHaskell #-}++module TH+( makeSuffixLenses+) where++import ClassyPrelude++import qualified Language.Haskell.TH.Syntax as TH+import qualified Language.Haskell.TH.Lib as TH++import Lens.Micro ((&), (.~))+import Lens.Micro.TH (DefName(..), makeLensesWith, lensRules, lensField)++-- | A 'FieldNamer' that strips the _ off of the field name,+-- lowercases the name, add 'L' as a suffix and skips the field if it doesn't start with+-- an '_'.+-- underscoreNoPrefixSuffixNamer :: TH.FieldNamer+underscoreNoPrefixSuffixNamer _ _ n =+  case TH.nameBase n of+    '_':x:xs -> [TopName (TH.mkName ((x:xs) ++ "L"))]+    _        -> []++-- | A template haskell function to build lenses for a record type.+makeSuffixLenses :: TH.Name -> TH.DecsQ+makeSuffixLenses = makeLensesWith $+  lensRules & lensField .~ underscoreNoPrefixSuffixNamer+
+ app/UI.hs view
@@ -0,0 +1,143 @@+module UI+( start+) where++import ClassyPrelude hiding (catch)+import Library+import UI.Types++import Control.Concurrent.Async (async)++import Lens.Micro.Platform ((^.), (&), (.~), (%~))++import qualified UI.Utils as Utils+import qualified UI.Views.Playlist as PlaylistView+import qualified UI.Views.Library as LibraryView+import qualified UI.Views.Help as HelpView+import qualified UI.Widgets.Status as Status+import qualified UI.Widgets.Playlist as Playlist+import qualified UI.Widgets.Filter as Filter+import qualified UI.Widgets.Notification as Notification+import qualified UI.Widgets.Library as LibraryWidget+import UI.Widgets.Filter (isFocusedL)++import Brick.Types (Widget, EventM, Next, BrickEvent(..))+import Brick.AttrMap (AttrMap, attrMap)+import Brick.BChan (BChan, newBChan, writeBChan)+import Brick.Util (clamp)+import qualified Brick.Main as M++import qualified Graphics.Vty as Vty+import Graphics.Vty.Config (defaultConfig)++import Network.MPD (Song, idle, Subsystem(..), Status(..))+import MPD (togglePlayPause, fetchPlaylist, fetchStatus, clearPlaylist, mpdReq, setVolume, currentSong)++type NextState n = EventM n (Next (AppState n))++drawUI :: (Show n, Ord n) => AppState n -> [Widget n]+drawUI state = if state^.helpActive+  then HelpView.draw+  else case state^.activeView of+    PlaylistView -> PlaylistView.draw state+    LibraryView -> LibraryView.draw state++appEvent :: AppState UIName -> BrickEvent UIName MPDEvent -> NextState UIName+appEvent state event = case event of+  AppEvent MPDPlaylistEvent -> updatePlaylist state+  AppEvent MPDStatusEvent -> updateStatus state+  vtyEvent -> case state^.filterStateL.isFocusedL of+    True -> handleViewEvent state vtyEvent+    False -> case vtyEvent of+      VtyEvent (Vty.EvKey (Vty.KChar '?') []) -> M.continue $ state & helpActive %~ not+      VtyEvent (Vty.EvKey (Vty.KChar 'p') []) -> void (liftIO togglePlayPause) >> M.continue state+      VtyEvent (Vty.EvKey (Vty.KChar 'c') []) -> void (liftIO clearPlaylist) >> M.continue state+      VtyEvent (Vty.EvKey (Vty.KChar '1') []) -> M.continue $ state & activeView .~ PlaylistView+      VtyEvent (Vty.EvKey (Vty.KChar '2') []) -> M.continue $ state & activeView .~ LibraryView+      VtyEvent (Vty.EvKey (Vty.KChar '-') []) -> void (liftIO (decreaseVolume state)) >> M.continue state+      VtyEvent (Vty.EvKey (Vty.KChar '+') []) -> void (liftIO (increaseVolume state)) >> M.continue state+      ev -> handleViewEvent state ev++handleViewEvent :: AppState UIName -> BrickEvent UIName e -> NextState UIName+handleViewEvent state event = case state^.activeView of+  PlaylistView -> PlaylistView.event state event+  LibraryView -> LibraryView.event state event++updatePlaylist :: AppState UIName -> NextState UIName+updatePlaylist state = do+  songs <- liftIO fetchPlaylist+  M.continue $ state & playlistStateL %~ Playlist.update songs++updateStatus :: AppState n -> NextState n+updateStatus state = do+  st <- liftIO fetchStatus+  song <- liftIO currentSong+  M.continue $ state & status .~ st+                    & playlistStateL . Playlist.playingSongL .~ song++increaseVolume :: AppState n -> IO ()+increaseVolume = changeVolume (+3)++decreaseVolume :: AppState n -> IO ()+decreaseVolume = changeVolume (\i -> i-3) ++changeVolume :: (Int -> Int) -> AppState n -> IO ()+changeVolume f state = case volume of+  Nothing -> return ()+  Just v -> setVolume $ clamp (f v) 0 100+  where volume = stVolume (state^.status)++initialState :: [Song] -> Library -> Status -> AppState UIName+initialState playlist library st = AppState+  { _playlistStateL = Playlist.mkState (UIName "playlist") playlist+  , _filterStateL = Filter.mkState (UIName "filter")+  , _activeView = LibraryView+  , _libraryStateL = LibraryWidget.mkState (UIName "artists") (UIName "albums") (UIName "songs") library+  , _status = st+  , _helpActive = False+  , _notificationState = Notification.mkState+  }++attributesMap :: AttrMap+attributesMap = attrMap Vty.defAttr $ concat+  [ Utils.attrs+  , Playlist.attrs+  , Status.attrs+  , LibraryWidget.attrs+  , Filter.attrs+  ]++app :: M.App (AppState UIName) MPDEvent UIName+app = M.App+  { M.appDraw = drawUI+  , M.appChooseCursor = M.showFirstCursor+  , M.appHandleEvent = appEvent+  , M.appStartEvent = return+  , M.appAttrMap = const attributesMap+  }++toEvent :: Subsystem -> MPDEvent+toEvent UpdateS = MPDDatabaseEvent+toEvent PlaylistS = MPDPlaylistEvent+toEvent PlayerS = MPDStatusEvent+toEvent OptionsS = MPDStatusEvent+toEvent MixerS = MPDStatusEvent+toEvent _ = MPDUnknownEvent++mpdLoop :: BChan MPDEvent -> IO ()+mpdLoop chan = forever $ do+  subsChanges <- mpdReq $ idle [UpdateS, PlaylistS, OptionsS, PlayerS, MixerS]+  mapM_ (writeBChan chan.toEvent) subsChanges++start :: IO ()+start = do+  playlist <- fetchPlaylist+  library <- fetchLibrary+  st <- fetchStatus+  mpdEventChan <- newBChan 10+  let initState = initialState playlist library st+  song <- currentSong+  let appState = initState & playlistStateL . Playlist.playingSongL .~ song+  _ <- async $ mpdLoop mpdEventChan+  void $ M.customMain (Vty.mkVty defaultConfig)+                    (Just mpdEventChan) app appState
+ app/UI/Types.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE TemplateHaskell #-}++module UI.Types+( AppState(..)+, ViewType(..)+-- , LibraryColumn(..)+, MPDEvent(..)+, UIName(..)+-- , LibraryMode(..)+, activeView+, playlistStateL+, helpActive+, filterStateL+, libraryStateL+-- , library+-- , libraryArtists+-- , libraryAlbums+-- , librarySongs+-- , libraryActiveColumn+-- , libraryMode+-- , filteredLibrary+, notificationState+, status+) where++import ClassyPrelude++import qualified Brick.Widgets.List as L+-- import Library++import UI.Widgets.Filter+import UI.Widgets.Notification+import UI.Widgets.Library+import UI.Widgets.Playlist++-- import Data.Map.Strict (Map(..))++import Lens.Micro.Platform (makeLenses)+-- import qualified Graphics.Vty as V++import qualified Network.MPD as M++data ViewType = PlaylistView | LibraryView++instance Show ViewType where+  show PlaylistView = "Playlist"+  show LibraryView = "Library"++-- data LibraryColumn = ArtistsColumn | AlbumsColumn | SongsColumn deriving (Show, Eq)+-- data LibraryMode = ArtistsAlbumsSongsMode | AlbumsSongsMode | SongsMode deriving (Show, Eq)++data AppState n = AppState+  { _playlistStateL :: PlaylistState n+  , _filterStateL :: FilterState n+  , _activeView :: ViewType+  , _libraryStateL :: LibraryState n+  -- , _library :: Library+  -- , _filteredLibrary :: Library+  -- , _libraryArtists :: L.List n Text+  -- , _libraryAlbums :: L.List n Text+  -- , _librarySongs :: L.List n Text+  -- , _libraryActiveColumn :: LibraryColumn+  -- , _libraryMode :: LibraryMode+  , _status :: M.Status+  , _helpActive :: Bool+  , _notificationState :: NotificationState+  }++data UIName = UIName Text deriving (Show, Eq, Ord)++data MPDEvent = MPDPlaylistEvent+  | MPDDatabaseEvent+  | MPDStatusEvent+  | MPDUnknownEvent deriving (Show)++instance IsString UIName where+  fromString s = UIName (pack s)++makeLenses ''AppState+
+ app/UI/Utils.hs view
@@ -0,0 +1,46 @@+module UI.Utils+( attrs+, secondsToTime+, listGetSelected+) where++import ClassyPrelude hiding (on)++import Lens.Micro.Platform ((^.))+import Data.Vector ((!?))++import Brick.Widgets.List (List, listAttr, listSelectedAttr, listSelectedL, listElementsL)+import Brick.Util (fg, on)+import Brick.AttrMap (AttrName)++import qualified Graphics.Vty as V++attrList :: (AttrName, V.Attr)+attrList = (listAttr, fg V.white)++attrSelectedList :: (AttrName, V.Attr)+attrSelectedList = (listSelectedAttr, V.black `on` V.white)++attrs :: [(AttrName, V.Attr)]+attrs = [ attrList+        , attrSelectedList+        ]++secondsToTime :: Integer -> Text+secondsToTime secs = txt+  where+    txt = case (h == 0) of+      True -> (tshow m) ++ ":" ++ sTxt+      False -> (tshow h) ++ (pad (tshow m) "0") ++ ":" ++ sTxt+    sTxt = pad (tshow s) "0"+    s = secs `mod` 60+    m = ((secs - s) `div` 60) `mod` 60+    h = ((secs - s) `div` (60 * 60)) `mod` 60++pad :: Text -> Text -> Text+pad txt p = case (length txt == 1) of+  True -> p ++ txt+  False -> txt++listGetSelected :: List n e -> Maybe e+listGetSelected list = (list^.listSelectedL) >>= (\i -> (list^.listElementsL) !? i)
+ app/UI/Views/Help.hs view
@@ -0,0 +1,37 @@+module UI.Views.Help+( draw+) where++import ClassyPrelude+import Brick.Types (Widget)+import Brick.Widgets.Core (str, vBox)++draw :: [Widget n]+draw = [vBox $ str <$> [ " Help"+       , " "+       , " General"+       , " "+       , "   j      move down"+       , "   k      move up"+       , "   h      move left"+       , "   l      move right"+       , " "+       , "   c      clear playlist"+       , "   p      play/pause"+       , "   -      decrease volume"+       , "   +      increase volume"+       , " "+       , "   1      show playlist"+       , "   2      show library"+       , " "+       , "   q      quit app or close current view"+       , "   ?      toggle help"+       , " "+       , " Library"+       , " "+       , "   a        add to playlist"+       , "   <Enter>  add to playlist and play"+       , "   t        toggle between artist/albums and albums view"+       , "   /        filter"+       , "   <Esc>    disable filter if active"+       ]]
+ app/UI/Views/Library.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE GADTs #-}+module UI.Views.Library+( draw+, event+) where++import ClassyPrelude hiding ((<>), on)++import qualified UI.Views.Main as Main++import Brick.Types (Widget(..), BrickEvent(..), EventM, Next)+import Brick.Widgets.Core ((<=>))+import Brick.Widgets.Border (hBorder)+import Brick.Main (continue, halt)+import Lens.Micro.Platform ((^.), (%~), (&), (.~))++import qualified Graphics.Vty as Vty++import qualified UI.Widgets.Library as Library+import qualified UI.Widgets.Filter as Filter+import UI.Widgets.Filter (isActiveL, isFocusedL)++import UI.Types++import Text.Fuzzy (simpleFilter)++draw :: (Show n, Ord n) => AppState n -> [Widget n]+draw state = Main.draw state widget+  where+    fzf = Filter.mkWidget $ state^.filterStateL+    library = Library.mkWidget $ state^.libraryStateL+    widget = if state^.filterStateL.isActiveL then+      fzf <=> hBorder <=> library else library++event :: AppState UIName -> BrickEvent UIName e -> EventM UIName (Next (AppState UIName))+event state (VtyEvent e) = if state^.filterStateL.isFocusedL then+  filterFocusedEvent state e else filterBlurredEvent state e+event state _ = continue state++filterFocusedEvent :: AppState UIName -> Vty.Event -> EventM UIName (Next (AppState UIName))+filterFocusedEvent state e = case e of+    (Vty.EvKey Vty.KEsc []) -> continue $ resetFilter state+    (Vty.EvKey Vty.KEnter []) -> continue (state & filterStateL %~ Filter.blur)+    ev -> do+      newFilterState <- Filter.handleEvent ev $ state^.filterStateL+      let newLibraryState = filterLibrary (Filter.getValue newFilterState) (state^.libraryStateL)+      continue $ state & filterStateL .~ newFilterState+                           & libraryStateL .~ newLibraryState++filterBlurredEvent :: AppState UIName -> Vty.Event -> EventM UIName (Next (AppState UIName))+filterBlurredEvent state e = case e of+    (Vty.EvKey (Vty.KChar '/') []) -> continue $ state & filterStateL %~ Filter.focus+    (Vty.EvKey (Vty.KChar 'q') []) -> halt state+    (Vty.EvKey Vty.KEsc []) -> continue $ if state^.filterStateL.isActiveL then+      resetFilter state else state+    ev -> do+      newLibraryState <- Library.handleEvent ev $ state^.libraryStateL+      continue $ state & libraryStateL .~ newLibraryState++resetFilter :: AppState n -> AppState n+resetFilter state = state & filterStateL %~ Filter.reset+                    & libraryStateL %~ Library.resetFilter++filterLibrary :: Text -> Library.LibraryState n -> Library.LibraryState n+filterLibrary token = Library.applyFilter ft+  where ft xs = pack <$> simpleFilter (unpack token) (unpack <$> xs)
+ app/UI/Views/Main.hs view
@@ -0,0 +1,39 @@+module UI.Views.Main+( draw+) where++import ClassyPrelude++import Brick.Widgets.Center (vCenter, hCenter)+import Brick.Widgets.Border (hBorder)++import Brick.Types (Widget, Padding(..))+import Brick.Widgets.Core (vBox, hLimit, str, padLeft, padRight, (<=>), (<+>))++import Lens.Micro.Platform ((^.))++import qualified UI.Widgets.Status as Status+import qualified UI.Widgets.Help as Help+import qualified UI.Widgets.Command as Command+import qualified UI.Widgets.Notification as Notification+import UI.Widgets.Playlist (playingSongL)++import UI.Types++draw :: AppState n -> Widget n -> [Widget n]+draw state widget = [ui]+    where+        {-total = str $ show $ Vec.length $ mails^.(L.listElementsL)-}+        ui = vCenter $ vBox widgets+        -- Cheap centering+        view = hBorder <=> (str "       " <+> hCenter activeViewWidget <+> str "?: Help")+        activeViewWidget = str $ show (state^.activeView)+        widgets = [--Help.mkWidget+                   Status.mkWidget state (state^.playlistStateL.playingSongL)+                  , hBorder+                  , hCenter widget+                  , view+                  , Notification.mkWidget (state^.notificationState)+                  -- , Command.mkWidget+                  ]+
+ app/UI/Views/Playlist.hs view
@@ -0,0 +1,29 @@+module UI.Views.Playlist+( draw+, event+) where++import ClassyPrelude hiding ((<>))++import Brick.Types (BrickEvent(..), Widget(..), EventM, Next)+import qualified Graphics.Vty as V+import Brick.Main (continue, halt)+import Lens.Micro.Platform ((^.), (&), (.~))++import UI.Types (AppState, playlistStateL)+import qualified UI.Views.Main as Main+import qualified UI.Widgets.Playlist as Playlist++type NextState n = EventM n (Next (AppState n))++draw :: (Show n, Ord n) => AppState n -> [Widget n]+draw state = Main.draw state playlst+  where playlst = Playlist.mkWidget (state^.playlistStateL)++event :: (Ord n) => AppState n -> BrickEvent n e -> NextState n+event state (VtyEvent e) = case e of+    (V.EvKey (V.KChar 'q') []) -> halt state+    ev -> do+      newPlaylistState <- Playlist.handleEvent ev (state^.playlistStateL)+      continue $ state & playlistStateL .~ newPlaylistState+event state _ = continue state
+ app/UI/Widgets/Command.hs view
@@ -0,0 +1,14 @@+module UI.Widgets.Command+( mkWidget+) where++import ClassyPrelude++import Brick.Types (Widget)+import Brick.Widgets.Core (str)+import Brick.Widgets.Center (hCenter)++import UI.Types (UIName)++mkWidget :: Widget UIName+mkWidget = hCenter $ str "Command"
+ app/UI/Widgets/Help.hs view
@@ -0,0 +1,13 @@+module UI.Widgets.Help+( mkWidget+) where++import ClassyPrelude++import Brick.Types (Widget, Padding(..))+import Brick.Widgets.Core (str, withAttr, padLeft)++import qualified UI.Widgets.Status as Status++mkWidget :: Widget n+mkWidget = withAttr Status.attrName $ padLeft Max $ str "?: help"
+ app/UI/Widgets/Library.hs view
@@ -0,0 +1,304 @@+{-# LANGUAGE TemplateHaskell #-}+module UI.Widgets.Library+( mkState+, mkWidget+, LibraryState+, handleEvent+, applyFilter+, resetFilter+, attrs+) where++import ClassyPrelude hiding ((<>), on)+import Data.Monoid ((<>))++import TH (makeSuffixLenses)+import Lens.Micro.Platform ((^.), (&), (.~), (%~))++import Brick.Widgets.Core ((<+>), (<=>), str, padLeft, padRight, withAttr)+import Brick.Widgets.Center (hCenter)+import Brick.Widgets.Border (vBorder, hBorder)+import Brick.Types (EventM, Widget, Padding(..))+import Brick.AttrMap (AttrName)+import Brick.Widgets.List (List(..), list, listMoveDown, listMoveUp, listReplace, listSelectedElement, renderList, listAttr, listSelectedAttr, listSelectedFocusedAttr)+import Brick.Util (fg, on)+import qualified Graphics.Vty as Vty++import Data.Map.Strict (elemAt)+import qualified Data.Vector as V+import qualified Data.Set as Set++import Network.MPD (Metadata(..))+import MPD (tag, addArtistToPlaylist, addAlbumToPlaylist, addSongToPlaylist)+import qualified MPD+import UI.Utils (listGetSelected)++import Library++data LibraryColumn = ArtistsColumn | AlbumsColumn | SongsColumn deriving (Show, Eq)+data LibraryMode = ArtistsAlbumsSongsMode | AlbumsSongsMode deriving (Show, Eq)++data LibraryState n = LibraryState+  { _library :: Library+  , _filteredLibrary :: Library+  , _libraryArtists :: List n Text+  , _libraryAlbums :: List n Text+  , _librarySongs :: List n Text+  , _libraryActiveColumn :: LibraryColumn+  , _libraryMode :: LibraryMode+  , _libraryFilter :: Maybe ([Text] -> [Text])+  }++makeSuffixLenses ''LibraryState++mkWidget :: (Show n, Ord n) => LibraryState n -> Widget n+mkWidget state = columns+  where+    columns = case state^.libraryModeL of+      ArtistsAlbumsSongsMode -> column "Artists" True artistsWidget <+> column "Albums" True albumsWidget <+> column "Songs" False songsWidget+      AlbumsSongsMode -> column "Albums" True albumsWidget <+> column "Songs" False songsWidget+      -- SongsMode -> column "Songs" False songsWidget++    column name bBorder widget = title name <=> if bBorder then widget <+> vBorder else widget+    title t = padRight Max (str t) <=> hBorder++    artistsWidget = columnWidget ArtistsColumn (state^.libraryArtistsL)+    albumsWidget = columnWidget AlbumsColumn (state^.libraryAlbumsL)+    songsWidget = columnWidget SongsColumn (state^.librarySongsL)++    columnWidget col = renderList (listDrawElement col activeColumn) False+    activeColumn = state^.libraryActiveColumnL++listDrawElement :: LibraryColumn -> LibraryColumn -> Bool -> Text -> Widget n+listDrawElement column activeColumn sel el = hCenter $ formatListElement column activeColumn sel $ pad $ str (unpack el)+  where+    pad w = padLeft Max $ padRight Max w++formatListElement :: LibraryColumn -> LibraryColumn -> Bool -> Widget n -> Widget n+formatListElement column activeColumn sel = withAttr attr+  where+    attr+     | column == activeColumn =+       if sel then selActiveColAttrName else activeColAttrName+     | columnIsBefore column activeColumn =+       if sel then listSelAttrName else listAttrName+     | otherwise = listAttrName++-- TODO: find a better way to do this+-- column != activeColumn must be true before calling this function+columnIsBefore :: LibraryColumn -> LibraryColumn -> Bool+columnIsBefore column activeColumn = case activeColumn of+  ArtistsColumn -> False+  SongsColumn -> True+  AlbumsColumn -> case column of+    ArtistsColumn -> True+    AlbumsColumn -> False+    SongsColumn -> False++listAttrName :: AttrName+listAttrName = listAttr <> "custom"++listSelAttrName :: AttrName+listSelAttrName = listSelectedAttr <> "custom-selected"++activeColAttrName :: AttrName+activeColAttrName = listAttrName <> "active-column"++selActiveColAttrName :: AttrName+selActiveColAttrName = listSelAttrName <> "selected-active-column"++attrs :: [(AttrName, Vty.Attr)]+attrs = [ (listAttrName, Vty.white `on` Vty.black)+        , (listSelAttrName, Vty.black `on` Vty.yellow)+        , (activeColAttrName, fg Vty.white)+        , (selActiveColAttrName, Vty.black `on` Vty.green)+        , (listSelectedFocusedAttr, fg Vty.red)+        ]++mkState :: n -> n -> n -> Library -> LibraryState n+mkState artistsName albumsName songsName library = LibraryState+  { _library = library+  , _filteredLibrary = library+  , _libraryArtists = list artistsName (fromList (keys artists)) 1+  , _libraryAlbums = list albumsName firstArtistAlbums 1+  , _librarySongs = list songsName V.empty 1+  , _libraryActiveColumn = ArtistsColumn+  , _libraryMode = ArtistsAlbumsSongsMode+  , _libraryFilter = Nothing+  }+  where+    artists = library^.artistsL+    firstArtistAlbums = V.fromList . Set.toAscList $ snd $ elemAt 0 artists++handleEvent :: Vty.Event -> LibraryState n -> EventM n (LibraryState n)+handleEvent event state = case event of+  (Vty.EvKey (Vty.KChar 'j') []) -> case activeColumn of+    ArtistsColumn -> return $ nextArtist state+    AlbumsColumn -> return $ nextAlbum state+    SongsColumn -> return $ nextSong state+  (Vty.EvKey (Vty.KChar 'k') []) -> case activeColumn of+    ArtistsColumn -> return $ previousArtist state+    AlbumsColumn -> return $ previousAlbum state+    SongsColumn -> return $ previousSong state+  (Vty.EvKey (Vty.KChar 'l') []) -> return $ nextColumn state+  (Vty.EvKey (Vty.KChar 'h') []) -> return $ previousColumn state+  (Vty.EvKey (Vty.KChar 'a') []) -> void (liftIO (addToPlaylist state False)) >> return (next state)+  (Vty.EvKey Vty.KEnter []) -> void (liftIO (addToPlaylist state True)) >> return (next state)+  (Vty.EvKey (Vty.KChar 't') []) -> return $ toggleMode state+  _ -> return state+  where activeColumn = state^.libraryActiveColumnL++next :: LibraryState n -> LibraryState n+next state = case state^.libraryActiveColumnL of+  ArtistsColumn -> nextArtist state+  AlbumsColumn -> nextAlbum state+  SongsColumn -> nextSong state++nextArtist :: LibraryState n -> LibraryState n+nextArtist state = updateArtistAlbumSongs $ updateArtistAlbums (state & libraryArtistsL %~ listMoveDown)++previousArtist :: LibraryState n -> LibraryState n+previousArtist state = updateArtistAlbumSongs $ updateArtistAlbums (state & libraryArtistsL %~ listMoveUp)++nextAlbum :: LibraryState n  -> LibraryState n+nextAlbum state = updateFn $ state & libraryAlbumsL %~ listMoveDown+  where updateFn = case state^.libraryModeL of+          ArtistsAlbumsSongsMode -> updateArtistAlbumSongs+          AlbumsSongsMode -> updateAlbumSongs++previousAlbum :: LibraryState n -> LibraryState n+previousAlbum state = updateFn $ state & libraryAlbumsL %~ listMoveUp+  where updateFn = case state^.libraryModeL of+          ArtistsAlbumsSongsMode -> updateArtistAlbumSongs+          AlbumsSongsMode -> updateAlbumSongs++nextSong :: LibraryState n -> LibraryState n+nextSong state = state & librarySongsL %~ listMoveDown++previousSong :: LibraryState n -> LibraryState n+previousSong state = state & librarySongsL %~ listMoveUp++update :: LibraryState n -> LibraryState n+update state = updateFn state+  where updateFn = case state^.libraryModeL of+          ArtistsAlbumsSongsMode -> updateArtistAlbums . updateArtistAlbumSongs+          AlbumsSongsMode -> updateAlbumSongs++updateArtistAlbums :: LibraryState n -> LibraryState n+updateArtistAlbums state = state & libraryAlbumsL %~ listReplace newAlbums (Just 0)+  where+    selArtist = snd <$> listSelectedElement (state^.libraryArtistsL)+    newAlbums = case selArtist of+      Just a -> fromMaybe V.empty ((V.fromList . Set.toAscList) <$> lookup a (state^.filteredLibraryL.artistsL))+      Nothing -> V.empty++updateArtistAlbumSongs :: LibraryState n -> LibraryState n+updateArtistAlbumSongs state = state & librarySongsL %~ listReplace (toTxt newSongsFiltered) (Just 0)+  where+    selAlbum = snd <$> listSelectedElement (state^.libraryAlbumsL)+    selArtist = snd <$> listSelectedElement (state^.libraryArtistsL)+    toTxt = map (tag Title "<no title>")+    newSongsFiltered = case selArtist of+      Just a -> filter (\s -> tag Artist "" s == a) newSongs+      Nothing -> newSongs+    newSongs = case selAlbum of+      Just a -> fromMaybe V.empty (lookup a (state^.filteredLibraryL.albumsL))+      Nothing -> V.empty++updateAlbumSongs :: LibraryState n -> LibraryState n+updateAlbumSongs state = state & librarySongsL %~ listReplace (toTxt newSongs) (Just 0)+  where+    selAlbum = snd <$> listSelectedElement (state^.libraryAlbumsL)+    toTxt = map (tag Title "<no title>")+    newSongs = case selAlbum of+      Just a -> fromMaybe V.empty (lookup a (state^.filteredLibraryL.albumsL))+      Nothing -> V.empty++nextColumn :: LibraryState n -> LibraryState n+nextColumn state = state & libraryActiveColumnL .~ nextCol+  where+    activeColumn = state^.libraryActiveColumnL+    nextCol = case state^.libraryModeL of+      ArtistsAlbumsSongsMode -> case activeColumn of+                ArtistsColumn -> AlbumsColumn+                AlbumsColumn -> SongsColumn+                SongsColumn -> SongsColumn+      AlbumsSongsMode -> SongsColumn++previousColumn :: LibraryState n  -> LibraryState n+previousColumn state = state & libraryActiveColumnL .~ prevCol+  where+    activeColumn = state^.libraryActiveColumnL+    prevCol = case state^.libraryModeL of+      ArtistsAlbumsSongsMode -> case activeColumn of+                ArtistsColumn -> ArtistsColumn+                AlbumsColumn -> ArtistsColumn+                SongsColumn -> AlbumsColumn+      AlbumsSongsMode -> AlbumsColumn++getSelected :: LibraryState n -> (Maybe Text, Maybe Text, Maybe Text)+getSelected state = (artist, album, title)+  where+    activeColumn = state^.libraryActiveColumnL+    artist = case state^.libraryModeL of+      ArtistsAlbumsSongsMode -> listGetSelected $ state^.libraryArtistsL+      AlbumsSongsMode -> Nothing+    album = if activeColumn == AlbumsColumn then+      listGetSelected $ state^.libraryAlbumsL else Nothing+    title = if activeColumn == SongsColumn then+      listGetSelected $ state^.librarySongsL else Nothing++addToPlaylist :: LibraryState n -> Bool -> IO ()+addToPlaylist state = addSelectionToPlaylist (getSelected state)++addSelectionToPlaylist :: (Maybe Text, Maybe Text, Maybe Text) -> Bool -> IO ()+addSelectionToPlaylist (Just artist, Nothing, Nothing) play = addArtistToPlaylist artist play+addSelectionToPlaylist (maybeArtist, Just album, Nothing) play = addAlbumToPlaylist maybeArtist album play+addSelectionToPlaylist (_, Just album, Just song) play = addSongToPlaylist album song play+addSelectionToPlaylist _ _ = putStrLn "This shouldn't happen"++toggleMode :: LibraryState n -> LibraryState n+toggleMode state = update $ case state^.libraryFilterL of+  Nothing -> newModeState+  Just f -> applyFilter f newModeState+  where+    newModeState = case state^.libraryModeL of+      ArtistsAlbumsSongsMode -> setMode AlbumsSongsMode state+      AlbumsSongsMode -> setMode ArtistsAlbumsSongsMode state++setMode :: LibraryMode -> LibraryState n -> LibraryState n+setMode mode state = case mode of+  ArtistsAlbumsSongsMode -> updateArtistAlbumSongs $ updateArtistAlbums $ state & libraryModeL .~ ArtistsAlbumsSongsMode+                                  & libraryActiveColumnL .~ ArtistsColumn+  AlbumsSongsMode -> updateAlbumSongs $ state & libraryModeL .~ AlbumsSongsMode+                                  & libraryActiveColumnL .~ AlbumsColumn+                                  & libraryAlbumsL %~ listReplace (fromList (keys (state^.libraryL.albumsL))) (Just 0)++-- TODO: modify this to filter depending on the mode (artists/albums or albums)+-- Only filter on artists atm+-- filter :: (Text -> Text) -> LibraryState -> LibraryState+applyFilter :: ([Text] -> [Text]) -> LibraryState n -> LibraryState n+applyFilter f state = newState & libraryFilterL .~ Just f+  where+    newState = case state^.libraryModeL of+      ArtistsAlbumsSongsMode -> filterArtists f state+      AlbumsSongsMode -> filterAlbums f state++filterArtists :: ([Text] -> [Text]) -> LibraryState n -> LibraryState n+filterArtists f state = state & libraryArtistsL %~ listReplace (V.fromList filteredArtists) (Just 0)+  where+    filteredArtists = f (keys (state^.libraryL.artistsL))++filterAlbums :: ([Text] -> [Text]) -> LibraryState n -> LibraryState n+filterAlbums f state = state & libraryAlbumsL %~ listReplace (V.fromList filteredAlbums) (Just 0)+  where+    filteredAlbums = f (keys (state^.libraryL.albumsL))++resetFilter :: LibraryState n -> LibraryState n+resetFilter state = newState & libraryFilterL .~ Nothing+  where+    newState = case state^.libraryModeL of+      ArtistsAlbumsSongsMode -> updateArtistAlbumSongs $ updateArtistAlbums $ state & libraryArtistsL %~ listReplace artists (Just 0)+        where artists = V.fromList . keys $ state^.libraryL.artistsL+      AlbumsSongsMode -> updateAlbumSongs $ state & libraryAlbumsL %~ listReplace albums (Just 0)+        where albums = V.fromList . keys $ state^.libraryL.albumsL
+ app/UI/Widgets/Playlist.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE TemplateHaskell #-}+module UI.Widgets.Playlist+( PlaylistState+, mkState+, mkWidget+, handleEvent+, update+, playingSongL+, attrs+) where++import ClassyPrelude hiding ((<>), on)+import Data.Monoid ((<>))+import TH (makeSuffixLenses)+import Lens.Micro.Platform ((^.), (%~), (&))++import Brick.Types (Widget(..), Padding(..), EventM)+import Brick.Widgets.List (List, list, renderList, listSelectedAttr, listAttr, listMoveDown, listMoveUp, listSelectedL, listElementsL, listReplace)+import Brick.AttrMap (AttrName)+import Brick.Widgets.Center (hCenter)+import Brick.Widgets.Core ((<=>), (<+>), str, withAttr, padLeft, padRight, hLimit)+import Brick.Widgets.Border (hBorder)+import Brick.Util (fg, on)++import Data.Vector ((!))++import qualified Graphics.Vty as Vty++import UI.Utils (secondsToTime)++import Network.MPD (Song(..), Metadata(..), withMPD, playId)+-- import Network.MPD (withMPD, Song(..), Id(..), playId)+import MPD (tag)++data PlaylistState n = PlaylistState+  { _songs :: List n Song+  , _playingSong :: Maybe Song+  }++mkState :: n -> [Song] -> PlaylistState n+mkState name songs = PlaylistState+  { _songs = list name (fromList songs) 1+  , _playingSong = Nothing+  }++makeSuffixLenses ''PlaylistState++-- mkWidget :: (Show n, Ord n) => List n Song -> Widget n+mkWidget :: (Show n, Ord n) => PlaylistState n -> Widget n+mkWidget state = header <=> hBorder <=> renderList listDrawElement True (state^.songsL)+  where+    header = artist <+> track <+> title <+> album <+> time+    artist = column (Just 25) (Pad 0) Max $ str "Artist"+    track = column (Just 5) Max (Pad 0) $ str "Track"+    title = column Nothing (Pad 2) Max $ str "Title"+    album = column (Just 35) (Pad 2) Max $ str "Album"+    time = column (Just 8) Max (Pad 1) $ str "Time"++update :: [Song] -> PlaylistState n -> PlaylistState n+update songs state = state & songsL %~ (listReplace (fromList songs) (Just 0))++handleEvent :: Vty.Event -> PlaylistState n -> EventM n (PlaylistState n)+handleEvent event state = case event of+  (Vty.EvKey (Vty.KChar 'j') []) -> return $ nextSong state+  (Vty.EvKey (Vty.KChar 'k') []) -> return $ previousSong state+  (Vty.EvKey Vty.KEnter []) -> play state+  _ -> return state++nextSong :: PlaylistState n -> PlaylistState n+nextSong state = state & songsL %~ listMoveDown++previousSong :: PlaylistState n -> PlaylistState n+previousSong state = state & songsL %~ listMoveUp++play :: PlaylistState n -> EventM n (PlaylistState n)+play state = case (state^.songsL.listSelectedL) of+  Nothing -> return state+  Just i -> do+    let selectedSong = (state^.songsL.listElementsL) ! i+    case (sgId selectedSong) of+      Nothing -> return state+      Just id -> do+        _ <- liftIO $ withMPD $ playId id+        return state++listDrawElement ::  Bool -> Song -> Widget n+listDrawElement sel song = hCenter $ formatListElement False sel $ artist <+> track <+> title <+> album <+> time+  where+    artist = column (Just 25) (Pad 0) Max $ str. unpack $ tag Artist "<unknown>" song+    track = column (Just 5) Max (Pad 0) $ str. unpack $ tag Track "?" song+    title = column Nothing (Pad 2) Max $ str . unpack $ tag Title "<no title>" song+    album = column (Just 35) (Pad 2) Max $ str . unpack $ tag Album "<no album>" song+    time = column (Just 8) Max (Pad 1) $ str . unpack $ secondsToTime $ sgLength song++-- Make a column considering a left padding, a right padding and an optional width+column :: Maybe Int -> Padding -> Padding -> Widget n -> Widget n+column maybeWidth left right widget = case maybeWidth of+  Nothing -> w+  Just wth -> hLimit wth w+  where w = padLeft left $ padRight right widget++formatListElement :: Bool -> Bool -> Widget n -> Widget n+formatListElement playing sel = withAttr attr+  where attr = case playing of+                True -> case sel of+                  True -> playlistSelPlayingAttrName+                  False -> playlistPlayingAttrName+                False -> case sel of+                  True -> playlistSelAttrName+                  False -> playlistListAttrName++playlistListAttrName :: AttrName+playlistListAttrName = listAttr <> "playlist"++playlistSelAttrName :: AttrName+playlistSelAttrName = listSelectedAttr <> "playlist-selected"++playlistPlayingAttrName :: AttrName+playlistPlayingAttrName = listAttr <> "playlist-playing"++playlistSelPlayingAttrName :: AttrName+playlistSelPlayingAttrName = listSelectedAttr <> "playlist-selected-playing"++attrs :: [(AttrName, Vty.Attr)]+attrs = [ (playlistListAttrName, fg Vty.white)+        , (playlistPlayingAttrName, Vty.withStyle (fg Vty.white) Vty.bold)+        , (playlistSelAttrName, Vty.withStyle (Vty.green `on` Vty.black) Vty.standout)+        , (playlistSelPlayingAttrName, Vty.withStyle (Vty.withStyle (Vty.green `on` Vty.black) Vty.standout) Vty.bold)+        ]
+ app/UI/Widgets/Status.hs view
@@ -0,0 +1,91 @@+module UI.Widgets.Status+( attrs+, attrName+, mkWidget+) where++import ClassyPrelude hiding (on, (<>))+import Data.Monoid ((<>))++import UI.Types++import Lens.Micro.Platform ((^.))+import Brick.Types (Widget, Padding(..))+import Brick.Widgets.Core (str, withAttr, padRight, padLeft, hBox, hLimit, (<=>), (<+>))+import Brick.Widgets.Center (hCenter)+import Brick.Widgets.List (listElementsL)+import Brick.AttrMap (AttrName)+import Brick.Util (on, fg)++import qualified Graphics.Vty as V+import Data.Vector ((!?))+++import Network.MPD (Metadata(..), Status(..), Song, State(..))+import MPD (tag)++attrName :: AttrName+attrName = "status"++artistAttrName :: AttrName+artistAttrName = attrName <> "artist"++albumAttrName :: AttrName+albumAttrName = attrName <> "album"++titleAttrName :: AttrName+titleAttrName = attrName <> "title"++playingAttrName :: AttrName+playingAttrName = attrName <> "playing"++pausedAttrName :: AttrName+pausedAttrName = attrName <> "paused"++stoppedAttrName :: AttrName+stoppedAttrName = attrName <> "stopped"++attrs :: [(AttrName, V.Attr)]+attrs = [ (attrName, fg V.white)+        , (artistAttrName, fg V.white)+        , (titleAttrName, fg V.yellow)+        , (albumAttrName, fg V.blue)+        , (playingAttrName, fg V.green)+        , (pausedAttrName, fg V.yellow)+        , (stoppedAttrName, fg V.red)+        ]++-- TODO: make a StatusState data type+mkWidget :: AppState n -> Maybe Song -> Widget n+mkWidget state playingSong = hCenter $ withAttr attrName $ widgets+  where+    widgets = hBox $+      [ str " " <=> (hLimit 15 (padRight Max playbackState))+      , title <=> artistAlbum+      , str " " <=> (hLimit 15 (padLeft Max volume))+      ]+    st = state^.status+    title = hCenter $ withAttr titleAttrName $ mkTagWidget Title "<untitled>" playingSong+    artist = withAttr artistAttrName $ mkTagWidget Artist "<unknown artist>" playingSong+    album = withAttr albumAttrName $ mkTagWidget Album "<unknown album>" playingSong+    artistAlbum = hCenter $ artist <+> (str " - ") <+> album+    playbackState = mkStateWidget st+    volume = mkVolumeWidget st++mkStateWidget :: Status -> Widget n+mkStateWidget status = withAttr attr $ str $ "[" ++ state ++ "]"+  where+    st = stState status+    -- state = toLower <$> (show st)+    state = (show st)+    attr = case st of+      Playing -> playingAttrName+      Paused -> pausedAttrName+      Stopped -> stoppedAttrName++mkVolumeWidget :: Status -> Widget n+mkVolumeWidget status = str $ "Vol: " ++ (fromMaybe "-" $ maybeVolume)+  where maybeVolume = (\v -> show v ++ "%") <$> (stVolume status)++mkTagWidget :: Metadata -> Text -> Maybe Song -> Widget n+mkTagWidget metaTag def song = str . unpack $ fromMaybe " " $ (tag metaTag def) <$> song
+ mushu.cabal view
@@ -0,0 +1,83 @@+name:                mushu+version:             0.1.0+synopsis:            Mushu is a minimalist MPD client with a TUI and an incremental fuzzy finder for your music library+description:         Please see README.md+homepage:            https://github.com/elaye/mushu#README.md+license:             BSD3+license-file:        LICENSE+author:              Elie Genard+maintainer:          elaye.github.io@gmail.com+copyright:           2016 Elie Genard+category:            Sound+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++executable mushu+  hs-source-dirs:      app+  main-is:             Main.hs+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  other-modules:       Library+                      , MPD+                      , TH+                      , UI, UI.Types, UI.Utils+                      , UI.Views.Help, UI.Views.Library, UI.Views.Main, UI.Views.Playlist+                      , UI.Widgets.Command, UI.Widgets.Help, UI.Widgets.Library, UI.Widgets.Playlist, UI.Widgets.Status+  build-depends:       base >= 4 && < 5+                      ,classy-prelude+                      ,safe-exceptions+                      ,containers+                      ,network+                      ,async+                      ,libmpd+                      ,text+                      ,fuzzy+                      ,time+                      ,bytestring+                      ,list-t+                      ,connection+                      ,mtl+                      ,hscurses+                      ,pretty+                      ,htoml+                      ,unordered-containers+                      ,brick+                      ,text-zipper+                      ,microlens+                      ,microlens-th+                      ,microlens-platform >= 0.3.1.0+                      ,vty >= 5.5.0+                      ,process+                      ,vector+                      ,transformers+                      ,data-default+                      ,filepath+                      ,directory >= 1.2.5.0+                      ,template-haskell >= 2.10+  default-language:    Haskell2010+  default-extensions:         NoImplicitPrelude, OverloadedStrings++test-suite mushu-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test, app+  main-is:             Spec.hs+  build-depends:       base+                      ,attoparsec+                      ,bytestring+                      ,classy-prelude+                      ,microlens-platform >= 0.3.1.0+                      ,unordered-containers+                      ,text+                      ,time+                      ,QuickCheck+                      ,quickcheck-text+                      ,tasty+                      ,tasty-quickcheck+                      ,tasty-hunit+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010+  default-extensions:         NoImplicitPrelude++source-repository head+  type:     git+  location: https://github.com/elaye/mushu
+ test/Spec.hs view
@@ -0,0 +1,16 @@+import ClassyPrelude++import Test.Tasty+import qualified MIME.Parser.Test as MP+import Test.Tasty.QuickCheck+import Mail (Target)++tests :: TestTree+tests = testGroup "Tests"+  [ MP.tests+  ]++main :: IO ()+main = do+  {-sample $ (MP.arbitrary :: Gen Target)-}+  defaultMain tests