hum 0.1.0.0 → 0.2.0.0
raw patch · 22 files changed
+991/−1216 lines, 22 filesdep +arraydep +regex-tdfadep +witherabledep −witherable-classdep ~brickdep ~bytestringdep ~libmpdPVP ok
version bump matches the API change (PVP)
Dependencies added: array, regex-tdfa, witherable
Dependencies removed: witherable-class
Dependency ranges changed: brick, bytestring, libmpd, time, vty
API changes (from Hackage documentation)
- Hum.Attributes: highlightOverQueueAttrs :: Widget n -> Widget n
- Hum.Attributes: queueAlbumAttr :: AttrName
- Hum.Attributes: queueArtistAttr :: AttrName
- Hum.Attributes: queueDateAttr :: AttrName
- Hum.Attributes: queueTimeAttr :: AttrName
- Hum.Attributes: queueTitleAttr :: AttrName
- Hum.Attributes: queueTitleBoldAttr :: AttrName
- Hum.Attributes: queueTrackAttr :: AttrName
- Hum.Types: HState :: !BChan HumEvent -> !View -> !Maybe Status -> !Mode -> !ExState -> !Maybe Song -> !SongList -> !LibraryState -> !PlaylistsState -> !Map Name (Maybe (Extent Name)) -> !Clipboard -> !Focus -> !Bool -> !Prompts -> !Int -> HState
- Hum.Types: [albums] :: LibraryState -> !List Name Value
- Hum.Types: [exitPrompt] :: Prompts -> HState -> EventM Name HState
- Hum.Types: [extentMap] :: HState -> !Map Name (Maybe (Extent Name))
- Hum.Types: [helpScreen] :: HState -> !Int
- Hum.Types: albumsL :: Lens' LibraryState (List Name Value)
- Hum.Types: data HState
- Hum.Types: exitPromptL :: Lens' Prompts (HState -> EventM Name HState)
- Hum.Types: extentMapL :: Lens' HState (Map Name (Maybe (Extent Name)))
- Hum.Types: helpScreenL :: Lens' HState Int
- Hum.UI: hBoxPad :: Padding -> [Widget n] -> Widget n
- Hum.UI: seekCurEventM :: FractionalSeconds -> HState -> EventM Name HState
- Hum.Utils: deleteAll :: MonadMPD m => SongList -> m ()
- Hum.Utils: listHighlight :: Traversable t => GenericList n t (e, Highlight) -> GenericList n t (e, Highlight)
- Hum.Utils: updateExtentMap :: EventM Name (Map Name (Maybe (Extent Name)))
- Hum.Views.Common: choosePlRow :: Int -> Bool -> Maybe PlaylistName -> Widget n
- Hum.Views.Help: helpText :: HState -> Text
- Hum.Views.Library: addToPl :: EventM Name HState
- Hum.Views.Queue: pasteDeleteCleanup :: HState -> SongList -> EventM Name HState
+ Brick.Widgets.Search: regexW :: Int -> Regex -> Text -> Widget n
+ Brick.Widgets.Search: searchW :: Int -> Text -> Text -> Widget n
+ Brick.Widgets.Search: splitRegex :: Regex -> Text -> [[(Text, Text)]]
+ Hum.Attributes: albumAttr :: AttrName
+ Hum.Attributes: artistAttr :: AttrName
+ Hum.Attributes: dateAttr :: AttrName
+ Hum.Attributes: highlightOverAttrs :: Widget n -> Widget n
+ Hum.Attributes: searchAttr :: AttrName
+ Hum.Attributes: searchFocusedAttr :: AttrName
+ Hum.Attributes: timeAttr :: AttrName
+ Hum.Attributes: titleAttr :: AttrName
+ Hum.Attributes: titleBoldAttr :: AttrName
+ Hum.Attributes: trackAttr :: AttrName
+ Hum.Orphans: instance (Data.Traversable.Traversable t, Witherable.Filterable t) => Witherable.Witherable (Brick.Widgets.List.GenericList n t)
+ Hum.Orphans: instance Witherable.Filterable t => Witherable.Filterable (Brick.Widgets.List.GenericList n t)
+ Hum.Rebuild: rebuildStatus :: MonadIO m => HumState -> m HumState
+ Hum.Rebuild: reloadLibAlbums :: MonadIO m => HumState -> m HumState
+ Hum.Rebuild: reloadLibArtists :: MonadIO m => HumState -> m HumState
+ Hum.Rebuild: reloadPlList :: MonadIO m => HumState -> m HumState
+ Hum.Rebuild: yalbumsOfArtist :: Bool -> Value -> IO (Vector (Value, Value))
+ Hum.Rebuild: yearOfAlbum :: Value -> IO Value
+ Hum.Types: Help :: Name
+ Hum.Types: HelpState :: !Text -> !Int -> HelpState
+ Hum.Types: HumState :: !BChan HumEvent -> !View -> !Maybe Status -> !Mode -> !ExState -> !Maybe Song -> !SongList -> !LibraryState -> !PlaylistsState -> !Clipboard -> !Focus -> !Bool -> !Prompts -> !HelpState -> HumState
+ Hum.Types: YalbumsList :: Name
+ Hum.Types: [exitPromptFunc] :: Prompts -> Bool -> HumState -> EventM Name HumState
+ Hum.Types: [helpSearchInt] :: HelpState -> !Int
+ Hum.Types: [helpText] :: HelpState -> !Text
+ Hum.Types: [help] :: HumState -> !HelpState
+ Hum.Types: [yalbumSort] :: LibraryState -> !Bool
+ Hum.Types: [yalbums] :: LibraryState -> !List Name (Value, Value)
+ Hum.Types: data HelpState
+ Hum.Types: data HumState
+ Hum.Types: exitPromptFuncL :: Lens' Prompts (Bool -> HumState -> EventM Name HumState)
+ Hum.Types: helpL :: Lens' HumState HelpState
+ Hum.Types: helpSearchIntL :: Lens' HelpState Int
+ Hum.Types: helpTextL :: Lens' HelpState Text
+ Hum.Types: yalbumSortL :: Lens' LibraryState Bool
+ Hum.Types: yalbumsL :: Lens' LibraryState (List Name (Value, Value))
+ Hum.Utils: deleteBulkfromQ :: MonadMPD m => SongList -> m ()
+ Hum.Utils: listHighlightSelected :: Traversable t => GenericList n t (e, Highlight) -> GenericList n t (e, Highlight)
+ Hum.Utils: unusedPlName :: PlaylistName -> IO PlaylistName
+ Hum.Utils: yankHighlighted :: HumState -> Lens' HumState SongList -> HumState
+ Hum.Views.Common: drawPlSelectRow :: Int -> Bool -> Maybe PlaylistName -> Widget n
+ Hum.Views.Help: helpSearch :: Bool -> HumState -> EventM Name HumState
+ Hum.Views.Help: helpText' :: Text
+ Hum.Views.Help: helpW :: HumState -> Widget Name
+ Hum.Views.Help: helpW' :: HumState -> Widget Name
+ Hum.Views.Library: libraryAlbumRow :: (Value, Value) -> Widget n
+ Hum.Views.Playlists: renamePl :: Text -> Bool -> HumState -> EventM n HumState
- Hum.Modes.ExMode: exCmdExecute :: [Text] -> HState -> EventM Name (Next HState)
+ Hum.Modes.ExMode: exCmdExecute :: [Text] -> HumState -> EventM Name (Next HumState)
- Hum.Modes.ExMode: exEnd :: HState -> EventM Name (Next HState)
+ Hum.Modes.ExMode: exEnd :: HumState -> EventM Name (Next HumState)
- Hum.Modes.ExMode: handleExEvent :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Modes.ExMode: handleExEvent :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Modes.PromptMode: handlePlSelectPromptEvent :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Modes.PromptMode: handlePlSelectPromptEvent :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Modes.PromptMode: handlePromptEvent :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Modes.PromptMode: handlePromptEvent :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Modes.PromptMode: handleTextPromptEvent :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Modes.PromptMode: handleTextPromptEvent :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Modes.PromptMode: handleYNPromptEvent :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Modes.PromptMode: handleYNPromptEvent :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Modes.PromptMode: songBulkAddtoNewPl :: Vector Song -> HState -> EventM n HState
+ Hum.Modes.PromptMode: songBulkAddtoNewPl :: Vector Song -> Bool -> HumState -> EventM n HumState
- Hum.Rebuild: albumsOfArtist :: Maybe Value -> IO (Vector Value)
+ Hum.Rebuild: albumsOfArtist :: Value -> IO (Vector Value)
- Hum.Rebuild: rebuildLib :: MonadIO m => HState -> m HState
+ Hum.Rebuild: rebuildLib :: MonadIO m => HumState -> m HumState
- Hum.Rebuild: rebuildLibAlbums :: MonadIO m => HState -> m HState
+ Hum.Rebuild: rebuildLibAlbums :: MonadIO m => HumState -> m HumState
- Hum.Rebuild: rebuildLibArtists :: MonadIO m => HState -> m HState
+ Hum.Rebuild: rebuildLibArtists :: MonadIO m => HumState -> m HumState
- Hum.Rebuild: rebuildPl :: MonadIO m => HState -> m HState
+ Hum.Rebuild: rebuildPl :: MonadIO m => HumState -> m HumState
- Hum.Rebuild: rebuildPlList :: MonadIO m => HState -> m HState
+ Hum.Rebuild: rebuildPlList :: MonadIO m => HumState -> m HumState
- Hum.Rebuild: rebuildQueue :: MonadIO m => HState -> m HState
+ Hum.Rebuild: rebuildQueue :: MonadIO m => HumState -> m HumState
- Hum.Rebuild: songsOfAlbum :: Maybe Value -> IO (Vector Song)
+ Hum.Rebuild: songsOfAlbum :: Value -> IO (Vector Song)
- Hum.Rebuild: songsOfArtist :: Maybe Value -> IO (Vector Song)
+ Hum.Rebuild: songsOfArtist :: Value -> IO (Vector Song)
- Hum.Types: LibraryState :: !List Name Value -> !List Name Value -> !List Name Song -> LibraryState
+ Hum.Types: LibraryState :: !List Name Value -> !List Name (Value, Value) -> !Bool -> !List Name Song -> LibraryState
- Hum.Types: Prompts :: !PromptType -> Text -> !List Name (Maybe PlaylistName) -> !Editor Text Name -> (HState -> EventM Name HState) -> Prompts
+ Hum.Types: Prompts :: !PromptType -> Text -> !List Name (Maybe PlaylistName) -> !Editor Text Name -> (Bool -> HumState -> EventM Name HumState) -> Prompts
- Hum.Types: [chan] :: HState -> !BChan HumEvent
+ Hum.Types: [chan] :: HumState -> !BChan HumEvent
- Hum.Types: [clipboard] :: HState -> !Clipboard
+ Hum.Types: [clipboard] :: HumState -> !Clipboard
- Hum.Types: [currentSong] :: HState -> !Maybe Song
+ Hum.Types: [currentSong] :: HumState -> !Maybe Song
- Hum.Types: [editable] :: HState -> !Bool
+ Hum.Types: [editable] :: HumState -> !Bool
- Hum.Types: [ex] :: HState -> !ExState
+ Hum.Types: [ex] :: HumState -> !ExState
- Hum.Types: [focus] :: HState -> !Focus
+ Hum.Types: [focus] :: HumState -> !Focus
- Hum.Types: [hview] :: HState -> !View
+ Hum.Types: [hview] :: HumState -> !View
- Hum.Types: [library] :: HState -> !LibraryState
+ Hum.Types: [library] :: HumState -> !LibraryState
- Hum.Types: [mode] :: HState -> !Mode
+ Hum.Types: [mode] :: HumState -> !Mode
- Hum.Types: [playlists] :: HState -> !PlaylistsState
+ Hum.Types: [playlists] :: HumState -> !PlaylistsState
- Hum.Types: [prompts] :: HState -> !Prompts
+ Hum.Types: [prompts] :: HumState -> !Prompts
- Hum.Types: [queue] :: HState -> !SongList
+ Hum.Types: [queue] :: HumState -> !SongList
- Hum.Types: [status] :: HState -> !Maybe Status
+ Hum.Types: [status] :: HumState -> !Maybe Status
- Hum.Types: chanL :: Lens' HState (BChan HumEvent)
+ Hum.Types: chanL :: Lens' HumState (BChan HumEvent)
- Hum.Types: clipboardL :: Lens' HState Clipboard
+ Hum.Types: clipboardL :: Lens' HumState Clipboard
- Hum.Types: currentSongL :: Lens' HState (Maybe Song)
+ Hum.Types: currentSongL :: Lens' HumState (Maybe Song)
- Hum.Types: editableL :: Lens' HState Bool
+ Hum.Types: editableL :: Lens' HumState Bool
- Hum.Types: exL :: Lens' HState ExState
+ Hum.Types: exL :: Lens' HumState ExState
- Hum.Types: focusL :: Lens' HState Focus
+ Hum.Types: focusL :: Lens' HumState Focus
- Hum.Types: hviewL :: Lens' HState View
+ Hum.Types: hviewL :: Lens' HumState View
- Hum.Types: libraryL :: Lens' HState LibraryState
+ Hum.Types: libraryL :: Lens' HumState LibraryState
- Hum.Types: modeL :: Lens' HState Mode
+ Hum.Types: modeL :: Lens' HumState Mode
- Hum.Types: playlistsL :: Lens' HState PlaylistsState
+ Hum.Types: playlistsL :: Lens' HumState PlaylistsState
- Hum.Types: promptsL :: Lens' HState Prompts
+ Hum.Types: promptsL :: Lens' HumState Prompts
- Hum.Types: queueL :: Lens' HState SongList
+ Hum.Types: queueL :: Lens' HumState SongList
- Hum.Types: statusL :: Lens' HState (Maybe Status)
+ Hum.Types: statusL :: Lens' HumState (Maybe Status)
- Hum.UI: app :: App HState HumEvent Name
+ Hum.UI: app :: App HumState HumEvent Name
- Hum.UI: buildInitialState :: BChan HumEvent -> IO HState
+ Hum.UI: buildInitialState :: BChan HumEvent -> IO HumState
- Hum.UI: chooseCursor :: HState -> [CursorLocation Name] -> Maybe (CursorLocation Name)
+ Hum.UI: chooseCursor :: HumState -> [CursorLocation Name] -> Maybe (CursorLocation Name)
- Hum.UI: drawUI :: HState -> [Widget Name]
+ Hum.UI: drawUI :: HumState -> [Widget Name]
- Hum.UI: handleEvent :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.UI: handleEvent :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.UI: humStartEvent :: HState -> EventM Name HState
+ Hum.UI: humStartEvent :: HumState -> EventM Name HumState
- Hum.Utils: deleteHighlighted :: HState -> Lens' HState SongList -> HState
+ Hum.Utils: deleteHighlighted :: HumState -> Lens' HumState SongList -> HumState
- Hum.Utils: deleteSelectedPl :: HState -> EventM n HState
+ Hum.Utils: deleteSelectedPl :: Bool -> HumState -> EventM n HumState
- Hum.Utils: duplicatePlaylist :: PlaylistName -> HState -> EventM n HState
+ Hum.Utils: duplicatePlaylist :: PlaylistName -> HumState -> EventM n HumState
- Hum.Utils: getHighlighted :: (Eq e, Filterable t, Foldable t, Splittable t) => GenericList n t (e, Highlight) -> GenericList n t (e, Highlight)
+ Hum.Utils: getHighlighted :: (Filterable t, Traversable t) => GenericList n t (e, Highlight) -> GenericList n t (e, Highlight)
- Hum.Utils: pastePlaylist :: HState -> EventM n HState
+ Hum.Utils: pastePlaylist :: HumState -> EventM n HumState
- Hum.Utils: saveEditedPl :: HState -> EventM n HState
+ Hum.Utils: saveEditedPl :: Bool -> HumState -> EventM n HumState
- Hum.Utils: songBulkAddtoPl :: String -> Vector Song -> HState -> EventM n HState
+ Hum.Utils: songBulkAddtoPl :: String -> Vector Song -> HumState -> EventM n HumState
- Hum.Utils: songBulkAddtoQ :: Bool -> Vector Song -> HState -> EventM n HState
+ Hum.Utils: songBulkAddtoQ :: Bool -> Vector Song -> HumState -> EventM n HumState
- Hum.Views.Common: drawNowPlaying :: HState -> Widget Name
+ Hum.Views.Common: drawNowPlaying :: HumState -> Widget Name
- Hum.Views.Common: drawProgressBar :: HState -> Widget Name
+ Hum.Views.Common: drawProgressBar :: HumState -> Widget Name
- Hum.Views.Common: drawPrompt :: HState -> Widget Name
+ Hum.Views.Common: drawPrompt :: HumState -> Widget Name
- Hum.Views.Help: drawViewHelp :: HState -> Widget Name
+ Hum.Views.Help: drawViewHelp :: HumState -> Widget Name
- Hum.Views.Help: handleEventHelp :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Views.Help: handleEventHelp :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Views.Library: drawLibraryLeft :: HState -> Widget Name
+ Hum.Views.Library: drawLibraryLeft :: HumState -> Widget Name
- Hum.Views.Library: drawLibraryMid :: HState -> Widget Name
+ Hum.Views.Library: drawLibraryMid :: HumState -> Widget Name
- Hum.Views.Library: drawLibraryRight :: HState -> Widget Name
+ Hum.Views.Library: drawLibraryRight :: HumState -> Widget Name
- Hum.Views.Library: drawViewLibrary :: HState -> Widget Name
+ Hum.Views.Library: drawViewLibrary :: HumState -> Widget Name
- Hum.Views.Library: handleEventLibrary :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Views.Library: handleEventLibrary :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Views.Library: libraryAddtoQ :: Bool -> HState -> EventM Name HState
+ Hum.Views.Library: libraryAddtoQ :: Bool -> HumState -> EventM Name HumState
- Hum.Views.Library: libraryMove :: (forall e. List Name e -> List Name e) -> HState -> EventM Name HState
+ Hum.Views.Library: libraryMove :: (forall e. List Name e -> List Name e) -> HumState -> EventM Name HumState
- Hum.Views.Library: libraryRow :: HState -> Name -> Text -> Widget n
+ Hum.Views.Library: libraryRow :: HumState -> Name -> Text -> Widget n
- Hum.Views.Library: librarySearch :: Bool -> HState -> EventM Name HState
+ Hum.Views.Library: librarySearch :: Bool -> HumState -> EventM Name HumState
- Hum.Views.Library: librarySongRow :: HState -> Song -> Widget n
+ Hum.Views.Library: librarySongRow :: HumState -> Song -> Widget n
- Hum.Views.Playlists: drawPlaylistLeft :: HState -> Widget Name
+ Hum.Views.Playlists: drawPlaylistLeft :: HumState -> Widget Name
- Hum.Views.Playlists: drawPlaylistRight :: HState -> Widget Name
+ Hum.Views.Playlists: drawPlaylistRight :: HumState -> Widget Name
- Hum.Views.Playlists: drawViewPlaylists :: HState -> Widget Name
+ Hum.Views.Playlists: drawViewPlaylists :: HumState -> Widget Name
- Hum.Views.Playlists: handleEventPlaylists :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Views.Playlists: handleEventPlaylists :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Views.Playlists: playlistRow :: HState -> Text -> Widget n
+ Hum.Views.Playlists: playlistRow :: HumState -> Text -> Widget n
- Hum.Views.Playlists: playlistSongRow :: HState -> (Song, Highlight) -> Widget n
+ Hum.Views.Playlists: playlistSongRow :: HumState -> (Song, Highlight) -> Widget n
- Hum.Views.Playlists: playlistsAddtoQ :: Bool -> HState -> EventM Name HState
+ Hum.Views.Playlists: playlistsAddtoQ :: Bool -> HumState -> EventM Name HumState
- Hum.Views.Playlists: playlistsMove :: (forall e. List Name e -> List Name e) -> HState -> EventM Name HState
+ Hum.Views.Playlists: playlistsMove :: (forall e. List Name e -> List Name e) -> HumState -> EventM Name HumState
- Hum.Views.Playlists: playlistsSearch :: Bool -> HState -> EventM Name HState
+ Hum.Views.Playlists: playlistsSearch :: Bool -> HumState -> EventM Name HumState
- Hum.Views.Queue: drawViewQueue :: HState -> Widget Name
+ Hum.Views.Queue: drawViewQueue :: HumState -> Widget Name
- Hum.Views.Queue: handleEventQueue :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)
+ Hum.Views.Queue: handleEventQueue :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState)
- Hum.Views.Queue: queueAddToPl :: HState -> String -> EventM Name HState
+ Hum.Views.Queue: queueAddToPl :: HumState -> String -> EventM Name HumState
- Hum.Views.Queue: queueRow :: HState -> (Song, Highlight) -> Widget n
+ Hum.Views.Queue: queueRow :: HumState -> (Song, Highlight) -> Widget n
- Hum.Views.Queue: queueSearch :: Bool -> HState -> EventM Name HState
+ Hum.Views.Queue: queueSearch :: Bool -> HumState -> EventM Name HumState
Files
- CHANGELOG.md +14/−0
- README.md +1/−1
- app/Main.hs +4/−1
- default.nix +29/−0
- hum.cabal +15/−10
- release.nix +4/−0
- src/Brick/Widgets/List.hs +0/−648
- src/Brick/Widgets/Search.hs +83/−0
- src/Hum/Attributes.hs +40/−30
- src/Hum/BChan.hs +0/−6
- src/Hum/Modes/ExMode.hs +29/−12
- src/Hum/Modes/PromptMode.hs +51/−25
- src/Hum/Orphans.hs +19/−0
- src/Hum/Rebuild.hs +119/−35
- src/Hum/Types.hs +74/−46
- src/Hum/UI.hs +65/−75
- src/Hum/Utils.hs +99/−68
- src/Hum/Views/Common.hs +43/−28
- src/Hum/Views/Help.hs +61/−21
- src/Hum/Views/Library.hs +96/−87
- src/Hum/Views/Playlists.hs +81/−51
- src/Hum/Views/Queue.hs +64/−72
CHANGELOG.md view
@@ -1,6 +1,20 @@ # Revision history for hum +## 0.2.0.0 - 2020-01-25++- Add to playlist prompt now updates for new playlists+- Playlists now sorted alphabetically+- List of playlists keeps cursor position after edit+- Library now shows track number on songs+- Library now uses album artist on left column+- Library now displays and sorts by album year by default+- `\`` toggles album sort order in library (between year and alphabetical)+- Help can now be scrolled and searched with case insensitive POSIX regex+- Fix add-to-playlist prompt not quitting when adding to old playlist+- Playlists can now be renamed++ ## 0.1.0.0 - 2020-12-12 - First version. Released on an unsuspecting world.
README.md view
@@ -35,7 +35,7 @@ - better documentation - tag info and editing-- mpdish file browser browser and more advanced search+- mpdish file browser and more advanced search - visualizer because why not - having a random mode when you can see what the next song is - making the UI nicer
app/Main.hs view
@@ -1,3 +1,4 @@+ module Main where import Brick.Main@@ -13,7 +14,7 @@ import Network.MPD ( withMPD , idle )-+-- | Calls functions from "Hum.UI", go there next. main :: IO () main = do chan <- BC.newBChan 10@@ -25,11 +26,13 @@ _ <- customMain initialVty buildVty (Just chan) app initialState pass +-- | Channel that waits for mpd events. mpdListenForever :: BC.BChan HumEvent -> IO () mpdListenForever chan = forever $ do response <- withMPD $ idle [] BC.writeBChan chan (Right response) +-- | Channel that sends ticks every 0.1 seconds. tickTock :: BC.BChan HumEvent -> IO () tickTock chan = forever $ do BC.writeBChan chan (Left Tick)
+ default.nix view
@@ -0,0 +1,29 @@+{ mkDerivation, array, base, brick, bytestring, containers+, directory, filepath, lens, libmpd, mtl, regex-tdfa, relude+, stdenv, template-haskell, text, text-zipper, time, transformers+, vector, vty, witherable-class+}:+mkDerivation {+ pname = "hum";+ version = "0.2.0.0";+ src = ./.;+ isLibrary = true;+ isExecutable = true;+ libraryHaskellDepends = [+ array base brick bytestring containers directory filepath lens+ libmpd mtl regex-tdfa relude template-haskell text text-zipper time+ transformers vector vty witherable-class+ ];+ executableHaskellDepends = [+ array base brick bytestring containers directory filepath lens+ libmpd mtl regex-tdfa relude template-haskell text text-zipper time+ transformers vector vty witherable-class+ ];+ testHaskellDepends = [+ array base brick bytestring containers directory filepath lens+ libmpd mtl regex-tdfa relude template-haskell text text-zipper time+ transformers vector vty witherable-class+ ];+ description = "A TUI MPD client, inspired by ncmpcpp";+ license = stdenv.lib.licenses.gpl2Plus;+}
hum.cabal view
@@ -12,7 +12,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.1.0.0+version: 0.2.0.0 -- A short (one-line) description of the package. synopsis: A TUI MPD client, inspired by ncmpcpp@@ -42,12 +42,14 @@ category: Sound -tested-with: GHC == 8.10.2, GHC == 8.8.4, GHC == 8.6.5, GHC == 8.4.4+tested-with: GHC == 8.10.3, GHC == 8.8.4, GHC == 8.6.5, GHC == 8.4.4 -- Extra files to be distributed with the package, such as examples or a -- README. extra-source-files: CHANGELOG.md README.md+ default.nix+ release.nix source-repository head type: git@@ -56,7 +58,7 @@ common deps build-depends: base >= 4.11 && < 4.15- ,brick >= 0.57.1 && < 0.58+ ,brick >= 0.58 && < 0.59 ,bytestring >= 0.10.10 && < 0.11 ,containers >= 0.6.2 && < 0.7 ,text >= 1.2.3 && < 1.3@@ -64,15 +66,17 @@ ,transformers >= 0.5.6.2 && < 0.6 ,template-haskell >= 2.13.0 && < 2.17 ,filepath >= 1.4.2 && < 1.5- ,directory >= 1.3.6.1 && < 1.4- ,time >= 1.9.3 && < 1.10+ ,directory >= 1.3.6.0 && < 1.3.6.1+ ,time >= 1.9.3 && < 1.11 ,lens >= 4.19.2 && < 4.20- ,text-zipper >= 0.10.1 && < 0.11+ ,text-zipper >= 0.10.1 && < 0.12 ,vector >= 0.12.1 && < 0.13 ,vty >= 5.32 && < 5.33- ,libmpd >= 0.9.2 && < 0.10+ ,libmpd >= 0.10 && < 0.11 ,relude >= 0.7.0 && < 0.8- ,witherable-class+ ,regex-tdfa >= 1.3.1.0 && < 1.4+ ,array >= 0.5.4.0 && < 0.6+ ,witherable >= 0.4 && < 0.5 mixins: base hiding (Prelude)@@ -116,7 +120,6 @@ exposed-modules: Hum.UI , Hum.Types- , Hum.BChan , Hum.Attributes , Hum.Views , Hum.Views.Common@@ -129,8 +132,10 @@ , Hum.Modes.ExMode , Hum.Modes.PromptMode , Hum.Rebuild+ , Hum.Orphans+ , Brick.Widgets.Search -- Modules included in this library but not exported.- other-modules: Brick.Widgets.List+ -- other-modules: -- Other library packages from which modules are imported. -- Directories containing source files.
+ release.nix view
@@ -0,0 +1,4 @@+ let+ pkgs = import <nixpkgs> { };+ in+ pkgs.haskellPackages.callPackage ./default.nix { }
− src/Brick/Widgets/List.hs
@@ -1,648 +0,0 @@-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE DeriveFoldable#-}-{-# LANGUAGE DeriveTraversable #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE DeriveGeneric #-}--- | This module provides a scrollable list type and functions for--- manipulating and rendering it.------ Note that lenses are provided for direct manipulation purposes, but--- lenses are *not* safe and should be used with care. (For example,--- 'listElementsL' permits direct manipulation of the list container--- without performing bounds checking on the selected index.) If you--- need a safe API, consider one of the various functions for list--- manipulation. For example, instead of 'listElementsL', consider--- 'listReplace'.-module Brick.Widgets.List- ( GenericList- , List-- -- * Constructing a list- , list-- -- * Rendering a list- , renderList- , renderListWithIndex-- -- * Handling events- , handleListEvent- , handleListEventVi-- -- * Lenses- , listElementsL- , listSelectedL- , listNameL- , listItemHeightL-- -- * Accessors- , listElements- , listName- , listSelectedElement- , listSelected- , listItemHeight-- -- * Manipulating a list- , listMoveBy- , listMoveTo- , listMoveToElement- , listFindBy- , listFindByOld- , listMoveUp- , listMoveDown- , listMoveByPages- , listMovePageUp- , listMovePageDown- , listInsert- , listRemove- , listReplace- , listClear- , listReverse- , listModify-- -- * Attributes- , listAttr- , listSelectedAttr- , listSelectedFocusedAttr-- -- * Classes- , Splittable(..)- , Reversible(..)- )-where--import Prelude hiding (reverse, splitAt, filter)---import Control.Lens ((^.), (^?), (.~), (%~), _2, _head, set)-import qualified Data.Sequence as Seq-import Graphics.Vty (Event(..), Key(..), Modifier(..))-import qualified Data.Vector as V-import qualified Data.Witherable.Class as W--import Brick.Types-import Brick.Main (lookupViewport)-import Brick.Widgets.Core-import Brick.Util (clamp)-import Brick.AttrMap---- | List state. Lists have a container @t@ of element type @e@ that is--- the data stored by the list. Internally, Lists handle the following--- events by default:------ * Up/down arrow keys: move cursor of selected item--- * Page up / page down keys: move cursor of selected item by one page--- at a time (based on the number of items shown)--- * Home/end keys: move cursor of selected item to beginning or end of--- list------ The 'List' type synonym fixes @t@ to 'V.Vector' for compatibility--- with previous versions of this library.------ For a container type to be usable with 'GenericList', it must have--- instances of 'Traversable' and 'Splittable'. The following functions--- impose further constraints:------ * 'listInsert': 'Applicative' and 'Semigroup'--- * 'listRemove': 'Semigroup'--- * 'listClear': 'Monoid'--- * 'listReverse': 'Reversible'----data GenericList n t e =- List { listElements :: !(t e)- -- ^ The list's sequence of elements.- , listSelected :: !(Maybe Int)- -- ^ The list's selected element index, if any.- , listName :: n- -- ^ The list's name.- , listItemHeight :: Int- -- ^ The height of an individual item in the list.- } deriving (Functor, Foldable, Traversable, Show, Generic)--suffixLenses ''GenericList---- | An alias for 'GenericList' specialized to use a 'Vector' as its--- container type.-type List n e = GenericList n V.Vector e--instance Named (GenericList n t e) n where- getName = listName---- | Ordered container types that can be split at a given index. An--- instance of this class is required for a container type to be usable--- with 'GenericList'.-class Splittable t where- {-# MINIMAL splitAt #-}-- -- | Split at the given index. Equivalent to @(take n xs, drop n xs)@- -- and therefore total.- splitAt :: Int -> t a -> (t a, t a)-- -- | Slice the structure. Equivalent to @(take n . drop i) xs@ and- -- therefore total.- --- -- The default implementation applies 'splitAt' two times: first to- -- drop elements leading up to the slice, and again to drop elements- -- after the slice.- slice :: Int {- ^ start index -} -> Int {- ^ length -} -> t a -> t a- slice i n = fst . splitAt n . snd . splitAt i---- | /O(1)/ 'splitAt'.-instance Splittable V.Vector where- splitAt = V.splitAt---- | /O(log(min(i,n-i)))/ 'splitAt'.-instance Splittable Seq.Seq where- splitAt = Seq.splitAt---- | Ordered container types where the order of elements can be--- reversed. Only required if you want to use 'listReverse'.-class Reversible t where- {-# MINIMAL reverse #-}- reverse :: t a -> t a---- | /O(n)/ 'reverse'-instance Reversible V.Vector where- reverse = V.reverse---- | /O(n)/ 'reverse'-instance Reversible Seq.Seq where- reverse = Seq.reverse---- | Handle events for list cursor movement. Events handled are:------ * Up (up arrow key)--- * Down (down arrow key)--- * Page Up (PgUp)--- * Page Down (PgDown)--- * Go to first element (Home)--- * Go to last element (End)-handleListEvent :: (Foldable t, Splittable t, Ord n)- => Event- -> GenericList n t e- -> EventM n (GenericList n t e)-handleListEvent e theList =- case e of- EvKey KUp [] -> return $ listMoveUp theList- EvKey KDown [] -> return $ listMoveDown theList- EvKey KHome [] -> return $ listMoveTo 0 theList- EvKey KEnd [] -> return $ listMoveTo (length $ listElements theList) theList- EvKey KPageDown [] -> listMovePageDown theList- EvKey KPageUp [] -> listMovePageUp theList- _ -> return theList---- | Enable list movement with the vi keys with a fallback handler if--- none match. Use 'handleListEventVi' 'handleListEvent' in place of--- 'handleListEvent' to add the vi keys bindings to the standard ones.--- Movements handled include:------ * Up (k)--- * Down (j)--- * Page Up (Ctrl-b)--- * Page Down (Ctrl-f)--- * Half Page Up (Ctrl-u)--- * Half Page Down (Ctrl-d)--- * Go to first element (g)--- * Go to last element (G)-handleListEventVi :: (Foldable t, Splittable t, Ord n)- => (Event -> GenericList n t e -> EventM n (GenericList n t e))- -- ^ Fallback event handler to use if none of the vi keys- -- match.- -> Event- -> GenericList n t e- -> EventM n (GenericList n t e)-handleListEventVi fallback e theList =- case e of- EvKey (KChar 'k') [] -> return $ listMoveUp theList- EvKey (KChar 'j') [] -> return $ listMoveDown theList- EvKey (KChar 'g') [] -> return $ listMoveTo 0 theList- EvKey (KChar 'G') [] -> return $ listMoveTo (length $ listElements theList) theList- EvKey (KChar 'f') [MCtrl] -> listMovePageDown theList- EvKey (KChar 'b') [MCtrl] -> listMovePageUp theList- EvKey (KChar 'd') [MCtrl] -> listMoveByPages (0.5::Double) theList- EvKey (KChar 'u') [MCtrl] -> listMoveByPages (-0.5::Double) theList- _ -> fallback e theList---- | The top-level attribute used for the entire list.-listAttr :: AttrName-listAttr = "list"---- | The attribute used only for the currently-selected list item when--- the list does not have focus. Extends 'listAttr'.-listSelectedAttr :: AttrName-listSelectedAttr = listAttr <> "selected"---- | The attribute used only for the currently-selected list item when--- the list has focus. Extends 'listSelectedAttr'.-listSelectedFocusedAttr :: AttrName-listSelectedFocusedAttr = listSelectedAttr <> "focused"---- | Construct a list in terms of container 't' with element type 'e'.-list :: (Foldable t)- => n- -- ^ The list name (must be unique)- -> t e- -- ^ The initial list contents- -> Int- -- ^ The list item height in rows (all list item widgets must be- -- this high).- -> GenericList n t e-list name es h =- let selIndex = if null es then Nothing else Just 0- safeHeight = max 1 h- in List es selIndex name safeHeight---- | Render a list using the specified item drawing function.------ Evaluates the underlying container up to, and a bit beyond, the--- selected element. The exact amount depends on available height--- for drawing and 'listItemHeight'. At most, it will evaluate up to--- element @(i + h + 1)@ where @i@ is the selected index and @h@ is the--- available height.------ Note that this function renders the list with the 'listAttr' as--- the default attribute and then uses 'listSelectedAttr' as the--- default attribute for the selected item if the list is not focused--- or 'listSelectedFocusedAttr' otherwise. This is provided as a--- convenience so that the item rendering function doesn't have to be--- concerned with attributes, but if those attributes are undesirable--- for your purposes, 'forceAttr' can always be used by the item--- rendering function to ensure that another attribute is used instead.-renderList :: (Traversable t, Splittable t, Ord n, Show n)- => (Bool -> e -> Widget n)- -- ^ Rendering function, True for the selected element- -> Bool- -- ^ Whether the list has focus- -> GenericList n t e- -- ^ The List to be rendered- -> Widget n- -- ^ rendered widget-renderList drawElem = renderListWithIndex $ const drawElem---- | Like 'renderList', except the render function is also provided with--- the index of each element.------ Has the same evaluation characteristics as 'renderList'.-renderListWithIndex :: (Traversable t, Splittable t, Ord n, Show n)- => (Int -> Bool -> e -> Widget n)- -- ^ Rendering function, taking index, and True for- -- the selected element- -> Bool- -- ^ Whether the list has focus- -> GenericList n t e- -- ^ The List to be rendered- -> Widget n- -- ^ rendered widget-renderListWithIndex drawElem foc l =- withDefAttr listAttr $- drawListElements foc l drawElem--imap :: (Traversable t) => (Int -> a -> b) -> t a -> t b-imap f xs =- let act = traverse (\a -> get >>= \i -> put (i + 1) $> f i a) xs- in evalState act 0---- | Draws the list elements.------ Evaluates the underlying container up to, and a bit beyond, the--- selected element. The exact amount depends on available height--- for drawing and 'listItemHeight'. At most, it will evaluate up to--- element @(i + h + 1)@ where @i@ is the selected index and @h@ is the--- available height.-drawListElements :: (Traversable t, Splittable t, Ord n, Show n)- => Bool- -> GenericList n t e- -> (Int -> Bool -> e -> Widget n)- -> Widget n-drawListElements foc l drawElem =- Widget Greedy Greedy $ do- c <- getContext-- -- Take (numPerHeight * 2) elements, or whatever is left- let es = slice start (numPerHeight * 2) (l^.listElementsL)-- idx = fromMaybe 0 (l^.listSelectedL)-- start = max 0 $ idx - numPerHeight + 1-- -- The number of items to show is the available height- -- divided by the item height...- initialNumPerHeight = (c^.availHeightL) `div` (l^.listItemHeightL)- -- ... but if the available height leaves a remainder of- -- an item height then we need to ensure that we render an- -- extra item to show a partial item at the top or bottom to- -- give the expected result when an item is more than one- -- row high. (Example: 5 rows available with item height- -- of 3 yields two items: one fully rendered, the other- -- rendered with only its top 2 or bottom 2 rows visible,- -- depending on how the viewport state changes.)- numPerHeight = initialNumPerHeight +- if initialNumPerHeight * (l^.listItemHeightL) == c^.availHeightL- then 0- else 1-- off = start * (l^.listItemHeightL)-- drawnElements = flip imap es $ \i e ->- let j = i + start- isSelected = Just j == l^.listSelectedL- elemWidget = drawElem j isSelected e- selItemAttr = if foc- then withDefAttr listSelectedFocusedAttr- else withDefAttr listSelectedAttr- makeVisible = if isSelected- then visible . selItemAttr- else id- in makeVisible elemWidget-- render $ viewport (l^.listNameL) Vertical $- translateBy (Location (0, off)) $- vBox $ toList drawnElements---- | Insert an item into a list at the specified position.------ Complexity: the worse of 'splitAt' and `<>` for the container type.------ @--- listInsert for 'List': O(n)--- listInsert for 'Seq.Seq': O(log(min(i, length n - i)))--- @-listInsert :: (Splittable t, Applicative t, Semigroup (t e))- => Int- -- ^ The position at which to insert (0 <= i <= size)- -> e- -- ^ The element to insert- -> GenericList n t e- -> GenericList n t e-listInsert pos e l =- let es = l^.listElementsL- newSel = case l^.listSelectedL of- Nothing -> 0- Just s -> if pos <= s- then s + 1- else s- (front, back) = splitAt pos es- in l & listSelectedL .~ Just newSel- & listElementsL .~ sconcat (front :| [pure e, back])---- | Remove an element from a list at the specified position.------ Applies 'splitAt' two times: first to split the structure at the--- given position, and again to remove the first element from the tail.--- Consider the asymptotics of `splitAt` for the container type when--- using this function.------ Complexity: the worse of 'splitAt' and `<>` for the container type.------ @--- listRemove for 'List': O(n)--- listRemove for 'Seq.Seq': O(log(min(i, n - i)))--- @-listRemove :: (Splittable t, Foldable t, Semigroup (t e))- => Int- -- ^ The position at which to remove an element (0 <= i <- -- size)- -> GenericList n t e- -> GenericList n t e-listRemove pos l | null l = l- | pos /= splitClamp l pos = l- | otherwise =- let newSel = case l^.listSelectedL of- Nothing -> 0- Just s | pos == 0 -> 0- | pos == s -> pos - 1- | pos < s -> s - 1- | otherwise -> s- (front, rest) = splitAt pos es- (_, back) = splitAt 1 rest- es' = front <> back- es = l^.listElementsL- in l & listSelectedL .~ (if null es' then Nothing else Just newSel)- & listElementsL .~ es'---- | Replace the contents of a list with a new set of elements and--- update the new selected index. If the list is empty, empty selection--- is used instead. Otherwise, if the specified selected index (via--- 'Just') is not in the list bounds, zero is used instead.------ Complexity: same as 'splitAt' for the container type.-listReplace :: (Foldable t, Splittable t)- => t e- -> Maybe Int- -> GenericList n t e- -> GenericList n t e-listReplace es idx l =- let l' = l & listElementsL .~ es- newSel = if null es then Nothing else inBoundsOrZero <$> idx- inBoundsOrZero i- | i == splitClamp l' i = i- | otherwise = 0- in l' & listSelectedL .~ newSel---- | Move the list selected index up by one. (Moves the cursor up,--- subtracts one from the index.)-listMoveUp :: (Foldable t, Splittable t)- => GenericList n t e- -> GenericList n t e-listMoveUp = listMoveBy (-1)---- | Move the list selected index up by one page.-listMovePageUp :: (Foldable t, Splittable t, Ord n)- => GenericList n t e- -> EventM n (GenericList n t e)-listMovePageUp = listMoveByPages (-1::Double)---- | Move the list selected index down by one. (Moves the cursor down,--- adds one to the index.)-listMoveDown :: (Foldable t, Splittable t)- => GenericList n t e- -> GenericList n t e-listMoveDown = listMoveBy 1---- | Move the list selected index down by one page.-listMovePageDown :: (Foldable t, Splittable t, Ord n)- => GenericList n t e- -> EventM n (GenericList n t e)-listMovePageDown = listMoveByPages (1::Double)---- | Move the list selected index by some (fractional) number of pages.-listMoveByPages :: (Foldable t, Splittable t, Ord n, RealFrac m)- => m- -> GenericList n t e- -> EventM n (GenericList n t e)-listMoveByPages pages theList = do- v <- lookupViewport (theList^.listNameL)- case v of- Nothing -> return theList- Just vp -> do- let nElems = round $ pages * fromIntegral (vp^.vpSize._2) /- fromIntegral (theList^.listItemHeightL)- return $ listMoveBy nElems theList---- | Move the list selected index.------ If the current selection is @Just x@, the selection is adjusted by--- the specified amount. The value is clamped to the extents of the list--- (i.e. the selection does not "wrap").------ If the current selection is @Nothing@ (i.e. there is no selection)--- and the direction is positive, set to @Just 0@ (first element),--- otherwise set to @Just (length - 1)@ (last element).------ Complexity: same as 'splitAt' for the container type.------ @--- listMoveBy for 'List': O(1)--- listMoveBy for 'Seq.Seq': O(log(min(i,n-i)))--- @-listMoveBy :: (Foldable t, Splittable t)- => Int- -> GenericList n t e- -> GenericList n t e-listMoveBy amt l =- let target = case l ^. listSelectedL of- Nothing- | amt > 0 -> 0- | otherwise -> length l - 1- Just i -> max 0 (amt + i) -- don't be negative- in listMoveTo target l---- | Set the selected index for a list to the specified index, subject--- to validation.------ If @pos >= 0@, indexes from the start of the list (which gets--- evaluated up to the target index)------ If @pos < 0@, indexes from the end of the list (which evalutes--- 'length' of the list).------ Complexity: same as 'splitAt' for the container type.------ @--- listMoveTo for 'List': O(1)--- listMoveTo for 'Seq.Seq': O(log(min(i,n-i)))--- @-listMoveTo :: (Foldable t, Splittable t)- => Int- -> GenericList n t e- -> GenericList n t e-listMoveTo pos l =- let len = length l- i = if pos < 0 then len - pos else pos- newSel = splitClamp l i- in l & listSelectedL .~ if null l then Nothing else Just newSel---- | Split-based clamp that avoids evaluating 'length' of the structure--- (unless the structure is already fully evaluated).-splitClamp :: (Foldable t, Splittable t) => GenericList n t e -> Int -> Int-splitClamp l i =- let (_, t) = splitAt i (l ^. listElementsL) -- split at i- in- -- If the tail is empty, then the requested index is not in the- -- list. And because we have already seen the end of the list,- -- using 'length' will not force unwanted computation.- --- -- Otherwise if tail is not empty, then we already know that i- -- is in the list, so we don't need to know the length- clamp 0 (if null t then length l - 1 else i) i---- | Set the selected index for a list to the index of the first--- occurrence of the specified element if it is in the list, or leave--- the list unmodified otherwise.------ /O(n)/. Only evaluates as much of the container as needed.-listMoveToElement :: (Eq e, Foldable t, Splittable t)- => e- -> GenericList n t e- -> GenericList n t e-listMoveToElement e = listFindBy (== e) . set listSelectedL Nothing---- | Set the selected index to the next element matching the--- predicate. If there is no selected element, the search starts at--- the beginning. If no matching element is found, leave the list--- unmodified.------ /O(n)/. Only evaluates as much of the container as needed.-listFindBy :: (Foldable t, Splittable t)- => (e -> Bool)- -> GenericList n t e- -> GenericList n t e-listFindBy test l =- let start = maybe 0 (+1) (l ^. listSelectedL)- (right,left) = splitAt start (l ^. listElementsL)- resultLeft = find (test . snd) . zip [0..] . toList $ left- resultRight = find (test . snd) . zip [0..] . toList $ right- in maybe (maybe id (set listSelectedL . Just . fst) resultRight)- (set listSelectedL . Just . (start +) . fst) resultLeft l--listFindByOld :: (Foldable t, Splittable t)- => (e -> Bool)- -> GenericList n t e- -> GenericList n t e-listFindByOld test l =- let start = maybe 0 (+1) (l ^. listSelectedL)- (_, t) = splitAt start (l ^. listElementsL)- result = find (test . snd) . zip [0..] . toList $ t- in maybe id (set listSelectedL . Just . (start +) . fst) result l---- | Return a list's selected element, if any.------ Only evaluates as much of the container as needed.------ Complexity: same as 'splitAt' for the container type.------ @--- listSelectedElement for 'List': O(1)--- listSelectedElement for 'Seq.Seq': O(log(min(i, n - i)))--- @-listSelectedElement :: (Splittable t, Foldable t)- => GenericList n t e- -> Maybe (Int, e)-listSelectedElement l = do- sel <- l^.listSelectedL- let (_, xs) = splitAt sel (l ^. listElementsL)- (sel,) <$> toList xs ^? _head---- | Remove all elements from the list and clear the selection.------ /O(1)/-listClear :: (Monoid (t e)) => GenericList n t e -> GenericList n t e-listClear l = l & listElementsL .~ mempty & listSelectedL .~ Nothing---- | Reverse the list. The element selected before the reversal will--- again be the selected one.------ Complexity: same as 'reverse' for the container type.------ @--- listReverse for 'List': O(n)--- listReverse for 'Seq.Seq': O(n)--- @-listReverse :: (Reversible t, Foldable t)- => GenericList n t e- -> GenericList n t e-listReverse l =- l & listElementsL %~ reverse- & listSelectedL %~ fmap (length l - 1 -)---- | Apply a function to the selected element. If no element is selected--- the list is not modified.------ Complexity: same as 'traverse' for the container type (typically--- /O(n)/).-listModify :: (Traversable t)- => (e -> e)- -> GenericList n t e- -> GenericList n t e-listModify f l =- case l ^. listSelectedL of- Nothing -> l- Just j -> l & listElementsL %~ imap (\i e -> if i == j then f e else e)--instance W.Filterable t => W.Filterable (GenericList n t) where- catMaybes l = l & listElementsL %~ W.catMaybes--instance (Traversable t, W.Filterable t) => W.Witherable (GenericList n t) where
+ src/Brick/Widgets/Search.hs view
@@ -0,0 +1,83 @@+-- |++module Brick.Widgets.Search where++import Text.Regex.TDFA+import qualified Data.Text as T+import Hum.Attributes+import Brick.Types+import Brick.Widgets.Core+import qualified Data.Array as A++searchW :: Int -> Text -> Text -> Widget n+searchW highlight term contents =+ let splits = T.splitOn term <$> lines contents+ numMatches = sum $ (\ls -> max 0 (length ls - 1)) <$> splits+ mkLine :: [Text] -> State Int (Widget n)+ mkLine [] = pure emptyWidget+ mkLine [""] = pure $ txt " "+ mkLine [t] = pure $ txt t+ mkLine (t:ts) = do+ num <- get+ let hl = (num - highlight) `mod` numMatches == 0+ modify (+1)+ let (rest,numFinal) = runState (mkLine ts) (num+1)+ put numFinal+ pure $ txt t+ <+> ((if hl then+ visible . withAttr searchFocusedAttr+ else withAttr searchAttr) $ txt term)+ <+> rest+ mkWidget :: [[Text]] -> State Int (Widget n)+ mkWidget [] = pure emptyWidget+ mkWidget [t] = join . pure $ mkLine t+ mkWidget (t:ts) = do+ num <- get+ let (line,numLine) = runState (mkLine t) num+ let (rest,numFinal) = runState (mkWidget ts) numLine+ put numFinal+ pure $ line <=> rest+ in evalState (mkWidget splits) 0++splitRegex :: Regex -> Text -> [[(Text, Text)]]+splitRegex rg source = go [] rg <$> lines source+ where+ go :: [(Text, Text)] -> Regex -> Text -> [(Text, Text)]+ go prev rg' rest =+ let matches = matchOnceText rg' rest+ in case matches of+ Nothing -> prev ++ [(rest, "")]+ Just (pre, mt, post) -> go+ (prev ++ [(pre, maybe "" fst $ viaNonEmpty head (A.elems mt))])+ rg+ post++regexW :: Int -> Regex -> Text -> Widget n+regexW highlight term contents =+ let splits = splitRegex term contents+ numMatches = sum $ (\ls -> max 0 (length ls - 1)) <$> splits+ mkLine :: [(Text,Text)] -> State Int (Widget n)+ mkLine [] = pure emptyWidget+ mkLine [("",_)] = pure $ txt " "+ mkLine [(tx,_)] = pure $ txt tx+ mkLine ((tx,mtch):ts) = do+ num <- get+ let hl = (num - highlight) `mod` numMatches == 0+ modify (+1)+ let (rest,numFinal) = runState (mkLine ts) (num+1)+ put numFinal+ pure $ txt tx+ <+> ((if hl then+ visible . withAttr searchFocusedAttr+ else withAttr searchAttr) $ txt mtch)+ <+> rest+ mkWidget :: [[(Text,Text)]] -> State Int (Widget n)+ mkWidget [] = pure emptyWidget+ mkWidget [t] = join . pure $ mkLine t+ mkWidget (t:ts) = do+ num <- get+ let (line,numLine) = runState (mkLine t) num+ let (rest,numFinal) = runState (mkWidget ts) numLine+ put numFinal+ pure $ line <=> rest+ in evalState (mkWidget splits) 0
src/Hum/Attributes.hs view
@@ -1,5 +1,11 @@--- | +-- | Module : Hum.Attributes+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Brick/Vty color attributes.+ module Hum.Attributes where import Brick.AttrMap@@ -13,7 +19,6 @@ import Graphics.Vty ( defAttr ) import qualified Graphics.Vty as Vty - humAttrMap :: AttrMap humAttrMap = attrMap defAttr@@ -22,31 +27,32 @@ , (listSelectedFocusedAttr, Vty.withStyle defAttr Vty.reverseVideo) , (listHighlightedAttr , BU.fg Vty.yellow) , (headerAttr , Vty.withStyle defAttr Vty.underline)- , (queueAlbumAttr , BU.fg Vty.red)- , (queueTrackAttr , BU.fg Vty.magenta)- , (queueTitleAttr , BU.fg Vty.cyan)- , (queueArtistAttr , BU.fg Vty.green)- , (queueTimeAttr , BU.fg Vty.blue)- , (queueDateAttr , BU.fg Vty.yellow)- , ( queueNowPlayingAttr- , Vty.withStyle (Vty.withStyle defAttr Vty.bold) Vty.underline- )- , (queueTitleBoldAttr, Vty.withStyle defAttr Vty.bold)- , (editorAttr , BU.bg Vty.black)+ , (albumAttr , BU.fg Vty.red)+ , (trackAttr , BU.fg Vty.magenta)+ , (titleAttr , BU.fg Vty.cyan)+ , (artistAttr , BU.fg Vty.green)+ , (timeAttr , BU.fg Vty.blue)+ , (dateAttr , BU.fg Vty.yellow)+ , ( queueNowPlayingAttr , Vty.withStyle+ (Vty.withStyle defAttr Vty.bold) Vty.underline)+ , (titleBoldAttr , Vty.withStyle defAttr Vty.bold)+ , (editorAttr , BU.bg Vty.black)+ , (searchAttr , Vty.withStyle defAttr Vty.reverseVideo)+ , (searchFocusedAttr , BU.fg Vty.red) ] wobAttr :: Vty.Attr wobAttr = BU.fg Vty.white -queueAttr, queueAlbumAttr, queueTitleAttr, queueTrackAttr, queueArtistAttr, queueTimeAttr, queueDateAttr+queueAttr, albumAttr, titleAttr, trackAttr, artistAttr, timeAttr, dateAttr :: AttrName queueAttr = "queue"-queueAlbumAttr = queueAttr <> "album"-queueTitleAttr = queueAttr <> "title"-queueTrackAttr = queueAttr <> "track"-queueArtistAttr = queueAttr <> "artist"-queueTimeAttr = queueAttr <> "time"-queueDateAttr = queueAttr <> "date"+albumAttr = queueAttr <> "album"+titleAttr = queueAttr <> "title"+trackAttr = queueAttr <> "track"+artistAttr = queueAttr <> "artist"+timeAttr = queueAttr <> "time"+dateAttr = queueAttr <> "date" headerAttr :: AttrName headerAttr = "header"@@ -56,22 +62,26 @@ queueNowPlayingAttr :: AttrName queueNowPlayingAttr = queueAttr <> "now playing" -queueTitleBoldAttr :: AttrName-queueTitleBoldAttr = queueTitleAttr <> "bold"-+titleBoldAttr :: AttrName+titleBoldAttr = titleAttr <> "bold" editorAttr :: AttrName editorAttr = "editor" -highlightOverQueueAttrs :: Widget n -> Widget n-highlightOverQueueAttrs = updateAttrMap+searchAttr, searchFocusedAttr :: AttrName+searchAttr = "search"+searchFocusedAttr = searchAttr <> "focus"++-- | Overwrites attributes for item to be highlighted.+highlightOverAttrs :: Widget n -> Widget n -- HACK?+highlightOverAttrs = updateAttrMap (mapAttrNames ( (listHighlightedAttr, )- <$> [ queueAlbumAttr- , queueTrackAttr- , queueTitleAttr- , queueArtistAttr- , queueTimeAttr+ <$> [ albumAttr+ , trackAttr+ , titleAttr+ , artistAttr+ , timeAttr ] ) )
− src/Hum/BChan.hs
@@ -1,6 +0,0 @@--- |-module Hum.BChan where---import Network.MPD---import Hum.Types---- put stuff dealing with Types here later
src/Hum/Modes/ExMode.hs view
@@ -1,5 +1,11 @@--- | +-- | Module : Hum.Modes.ExMode+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Functions for the ex mode style prompt.+ module Hum.Modes.ExMode where import Brick.Widgets.Edit hiding ( decodeUtf8 )@@ -7,15 +13,17 @@ import Brick.Main import Hum.Types import Hum.Views-import Hum.Utils import Hum.Rebuild import Graphics.Vty.Input.Events import qualified Data.Text.Zipper as Z- hiding ( textZipper )+import qualified Data.Text as T import Control.Lens hiding (uncons) import qualified Network.MPD as MPD+import Hum.Utils -exEnd :: HState -> EventM Name (Next HState)+-- | Executed after pressing enter in the ex mode prompt.+-- Either executes the command, or updates the search state for n/N to work.+exEnd :: HumState -> EventM Name (Next HumState) exEnd s = let searched = (s ^. exL . exEditorL . editContentsL & Z.currentLine) s'= s & exL . exEditorL .~ editorText ExEditor (Just 1) ""@@ -28,27 +36,36 @@ QueueView -> continue =<< queueSearch (srch == FSearch) s'' LibraryView -> continue =<< librarySearch (srch == FSearch) s'' PlaylistsView -> continue =<< playlistsSearch (srch == FSearch) s''- HelpView -> continue s'+ HelpView -> continue =<< helpSearch (srch == FSearch) s'' where s''= s' & exL . searchHistoryL %~ (searched :)++-- | Handle key inputs for ex mode. handleExEvent- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleExEvent s e = case e of+ VtyEvent (EvKey KEsc []) ->+ continue $ s & exL . exEditorL .~ editorText ExEditor (Just 1) ""+ & modeL .~ NormalMode+ & focusL . focExL .~ False VtyEvent (EvKey KEnter []) -> exEnd s VtyEvent vtye -> continue =<< handleEventLensed s (exL . exEditorL) handleEditorEvent vtye _ -> continue s +-- | Prefix of ex mode prompt. exPrefixTxt :: ExSubMode -> Text exPrefixTxt Cmd = ":" exPrefixTxt FSearch= "/" exPrefixTxt BSearch = "?" -exCmdExecute :: [Text] -> HState -> EventM Name (Next HState)+-- | Executes ex mode command.+exCmdExecute :: [Text] -> HumState -> EventM Name (Next HumState) exCmdExecute ("help":_) s = continue s { hview = HelpView } exCmdExecute ("q":_) s = halt s-exCmdExecute ("save":name) s = do- let songs = queue s- let name' = maybe "unnamed" fst (uncons name)- _ <- liftIO $ MPD.withMPD $ saveListToPl songs name'- continue =<< rebuildPl s+exCmdExecute ("save":name) s =+ let name' = if null name then "unnamed" else unwords name in+ do+ name'' <- liftIO $ unusedPlName (fromString . T.unpack $ name')+ _ <- liftIO $ MPD.withMPD $ MPD.save name''+ continue =<< rebuildPl s exCmdExecute _ s = continue s
src/Hum/Modes/PromptMode.hs view
@@ -1,8 +1,16 @@--- | +-- | Module : Hum.Modes.PromptMode+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Functions for prompts.++ module Hum.Modes.PromptMode where import Brick.Widgets.Edit hiding ( decodeUtf8 )+import Brick.Widgets.List import Brick.Types import Brick.Main import Hum.Types@@ -13,22 +21,24 @@ import Control.Lens import qualified Network.MPD as MPD import qualified Data.Vector as V-import Brick.Widgets.List -+-- | Prompt key event dispatch. handlePromptEvent- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handlePromptEvent s e = case s ^. promptsL . currentPromptL of PlSelectPrompt -> handlePlSelectPromptEvent s e TextPrompt -> handleTextPromptEvent s e YNPrompt -> handleYNPromptEvent s e +-- | Handles key events for playlist select prompt. handlePlSelectPromptEvent- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handlePlSelectPromptEvent s e = case e of VtyEvent vtye -> case vtye of- EvKey KEsc [] -> continue $ s & modeL .~ NormalMode- EvKey (KChar 'q') [] -> continue $ s & modeL .~ NormalMode+ EvKey KEsc [] -> continue+ =<< (s ^. promptsL . exitPromptFuncL) False (s & modeL .~ NormalMode)+ EvKey (KChar 'q') [] -> continue+ =<< (s ^. promptsL . exitPromptFuncL) False (s & modeL .~ NormalMode) EvKey (KChar 'j') [] -> do continue $ s & promptsL . plSelectPromptL %~ listMoveDown EvKey (KChar 'k') [] -> do@@ -37,40 +47,56 @@ let songs = s ^. queueL & getHighlighted <&> fst & listElements case s ^. promptsL . plSelectPromptL & listSelectedElement <&> snd & join of Nothing -> continue $ s & promptsL . currentPromptL .~ TextPrompt- & promptsL . exitPromptL .~ songBulkAddtoNewPl songs- Just plname -> continue =<< songBulkAddtoPl (MPD.toString plname) songs s+ & promptsL . exitPromptFuncL .~ songBulkAddtoNewPl songs+ Just plname -> continue+ =<< songBulkAddtoPl (MPD.toString plname) songs (s & modeL .~ NormalMode) EvKey KEnter [] -> do -- HACK find a way to unduplucate this let songs = s ^. queueL & getHighlighted <&> fst & listElements case s ^. promptsL . plSelectPromptL & listSelectedElement <&> snd & join of Nothing -> continue $ s & promptsL . currentPromptL .~ TextPrompt- & promptsL . exitPromptL .~ songBulkAddtoNewPl songs- Just plname -> continue =<< songBulkAddtoPl (MPD.toString plname) songs s+ & promptsL . exitPromptFuncL .~ songBulkAddtoNewPl songs+ Just plname -> continue+ =<< songBulkAddtoPl (MPD.toString plname) songs (s & modeL .~ NormalMode) _ -> continue s _ -> continue s -songBulkAddtoNewPl :: V.Vector MPD.Song -> HState -> EventM n HState-songBulkAddtoNewPl songs st = songBulkAddtoPl- (toString $ st ^. promptsL . textPromptL . editContentsL & Z.currentLine)- songs- st+-- | Add given songs to new playlist entered in prompt.+songBulkAddtoNewPl+ :: V.Vector MPD.Song -> Bool -> HumState -> EventM n HumState+songBulkAddtoNewPl songs bl st = if bl+ then songBulkAddtoPl+ (toString $ st ^. promptsL . textPromptL . editContentsL & Z.currentLine)+ songs+ st+ else pure st +-- | Handles key events for generic text prompt. handleTextPromptEvent- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleTextPromptEvent s e = case e of- VtyEvent (EvKey KEsc []) -> continue (s & modeL .~ NormalMode)- VtyEvent (EvKey KEnter []) ->- continue =<< (s ^. promptsL . exitPromptL) (s & modeL .~ NormalMode)+ VtyEvent (EvKey KEsc []) -> continue+ =<< (s ^. promptsL . exitPromptFuncL) False (s & modeL .~ NormalMode)+ VtyEvent (EvKey KEnter []) -> continue+ =<< (s ^. promptsL . exitPromptFuncL) True (s & modeL .~ NormalMode) VtyEvent vtye -> continue =<< handleEventLensed s (promptsL . textPromptL) handleEditorEvent vtye _ -> continue s +-- | Handles key events for generic yes/no prompt. handleYNPromptEvent- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleYNPromptEvent s e = case e of- VtyEvent (EvKey KEsc []) -> continue (s & modeL .~ NormalMode)+ VtyEvent (EvKey KEsc []) ->+ continue+ =<< (s ^. promptsL . exitPromptFuncL) False (s & modeL .~ NormalMode) VtyEvent (EvKey (KChar 'y') []) ->- continue =<< (s ^. promptsL . exitPromptL) (s & modeL .~ NormalMode)- VtyEvent (EvKey (KChar 'n') []) -> continue (s & modeL .~ NormalMode)- VtyEvent (EvKey (KChar 'q') []) -> continue (s & modeL .~ NormalMode)+ continue+ =<< (s ^. promptsL . exitPromptFuncL) True (s & modeL .~ NormalMode)+ VtyEvent (EvKey (KChar 'n') []) ->+ continue+ =<< (s ^. promptsL . exitPromptFuncL) False (s & modeL .~ NormalMode)+ VtyEvent (EvKey (KChar 'q') []) ->+ continue+ =<< (s ^. promptsL . exitPromptFuncL) False (s & modeL .~ NormalMode) _ -> continue s
+ src/Hum/Orphans.hs view
@@ -0,0 +1,19 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Module : Hum.Orphans+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--++module Hum.Orphans where++import qualified Witherable as W+import Brick.Widgets.List+import Control.Lens++-- | There isn't one obvious implementation fo this, so it can't be upstreamed.+instance W.Filterable t => W.Filterable (GenericList n t) where+ catMaybes l = l & listElementsL %~ W.catMaybes++instance (Traversable t, W.Filterable t) => W.Witherable (GenericList n t) where
src/Hum/Rebuild.hs view
@@ -1,62 +1,121 @@--- |+-- | Module : Hum.Rebuild+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Functions that rebuild the state after changes that require it,+-- Or that have to be here to avoid dependency loops. module Hum.Rebuild where import Hum.Types import Control.Lens+import Data.Foldable import Brick.Widgets.List import Network.MPD ( withMPD ) import qualified Network.MPD as MPD import qualified Data.Vector as V--- in which we have funcitons to rebuild the state when it changes. -songsOfArtist :: Maybe MPD.Value -> IO (V.Vector MPD.Song)-songsOfArtist martist = V.fromList . fromRight [] <$> withMPD- (MPD.find (MPD.AlbumArtist MPD.=? fromMaybe "" martist))+-- | All songs of a given artist+songsOfArtist :: MPD.Value -> IO (V.Vector MPD.Song)+songsOfArtist artist' =+ (V.fromList . fromRight [] <$>)+ . withMPD+ . MPD.find $ (MPD.AlbumArtist MPD.=? artist') -songsOfAlbum :: Maybe MPD.Value -> IO (V.Vector MPD.Song)-songsOfAlbum malbum = V.fromList . fromRight [] <$> withMPD- (MPD.find (MPD.Album MPD.=? fromMaybe "" malbum))-albumsOfArtist :: Maybe MPD.Value -> IO (V.Vector MPD.Value)-albumsOfArtist martist =- V.fromList . fromRight [] <$> withMPD (MPD.list MPD.Album martist)+-- | All songs in a given album+songsOfAlbum ::MPD.Value -> IO (V.Vector MPD.Song)+songsOfAlbum album' =+ (V.fromList . fromRight [] <$>)+ . withMPD+ . MPD.find $ (MPD.Album MPD.=? album') +-- | All albums of a given artist+albumsOfArtist :: MPD.Value -> IO (V.Vector MPD.Value)+albumsOfArtist artist' =+ V.fromList . fromRight [] <$> withMPD (MPD.list MPD.Album (MPD.AlbumArtist MPD.=? artist')) -rebuildLib :: MonadIO m => HState -> m HState+-- | All year-album pairs of a given artist+yalbumsOfArtist :: Bool -> MPD.Value -> IO (V.Vector (MPD.Value,MPD.Value))+yalbumsOfArtist bl artist' = let srt = (if bl then fst else snd)+ in do+ albums' <- fromRight [] <$> withMPD (MPD.list MPD.Album (MPD.AlbumArtist MPD.=? artist'))+ yalbums' <- liftIO . sequence $ (\x -> (,x) <$> yearOfAlbum x) <$> albums'+ pure $ V.fromList (sortBy (\x y -> compare (srt x) (srt y)) yalbums')++-- | Earliest year of any song in the given album+yearOfAlbum :: MPD.Value -> IO MPD.Value+yearOfAlbum album' = fromRight "????" <$> (minYear <<$>> withMPD (MPD.list MPD.Date (MPD.Album MPD.=? album')))+ where minYear :: [MPD.Value] -> MPD.Value+ minYear [] = "????"+ minYear vals = minimum vals++-- | Rebuild entire library state, keeping the index of the left column if possible.+rebuildLib :: MonadIO m => HumState -> m HumState rebuildLib s = do+ let mi = s ^. libraryL . artistsL & listSelected artistsVec <- liftIO (V.fromList . fromRight [] <$> withMPD- (MPD.list MPD.AlbumArtist Nothing))- let artists' = list ArtistsList artistsVec 1- albumsVec <- liftIO $ albumsOfArtist (snd <$> listSelectedElement artists')+ (MPD.list MPD.AlbumArtist mempty))+ let artists' = maybe id listMoveTo mi $ list ArtistsList artistsVec 1+ albumsVec <- liftIO $ maybe (pure empty) albumsOfArtist (snd <$> listSelectedElement artists') let albums' = list AlbumsList albumsVec 1- songsVec <- liftIO $ songsOfAlbum (snd <$> listSelectedElement albums')+ yalbumsVec <- liftIO $ maybe (pure empty) (yalbumsOfArtist (s ^. libraryL . yalbumSortL)) (snd <$> listSelectedElement artists')+ let yalbums' = list YalbumsList yalbumsVec 1+ songsVec <- liftIO $ maybe (pure empty) songsOfAlbum (snd <$> listSelectedElement albums') let songs' = list SongsList songsVec 1 pure $ s & libraryL . artistsL .~ artists'- & libraryL . albumsL .~ albums'+ & libraryL . yalbumsL .~ yalbums' & libraryL . songsL .~ songs' -rebuildLibArtists :: MonadIO m => HState -> m HState+-- | Rebuild library state from selected artist.+rebuildLibArtists :: MonadIO m => HumState -> m HumState rebuildLibArtists s = do let artists' = s ^. libraryL . artistsL- albumsVec <- liftIO $ albumsOfArtist (snd <$> listSelectedElement artists')- let albums' = list AlbumsList albumsVec 1- songsVec <- liftIO $ songsOfAlbum (snd <$> listSelectedElement albums')+ yalbumsVec <- liftIO $ maybe (pure empty) (yalbumsOfArtist (s ^. libraryL . yalbumSortL)) (snd <$> listSelectedElement artists')+ let yalbums' = list YalbumsList yalbumsVec 1+ songsVec <- liftIO $ maybe (pure empty) songsOfAlbum (snd . snd <$> listSelectedElement yalbums') let songs' = list SongsList songsVec 1- pure $ s & libraryL . artistsL .~ artists'- & libraryL . albumsL .~ albums'+ pure $ s & libraryL . yalbumsL .~ yalbums' & libraryL . songsL .~ songs' -rebuildLibAlbums :: MonadIO m => HState -> m HState+-- | Rebuild library state from selected artist.+reloadLibArtists :: MonadIO m => HumState -> m HumState+reloadLibArtists s = do+ let mi = s ^. libraryL . yalbumsL & listSelected+ let mj = s ^. libraryL . songsL & listSelected+ let artists' = s ^. libraryL . artistsL+ yalbumsVec <- liftIO $ maybe (pure empty) (yalbumsOfArtist (s ^. libraryL . yalbumSortL)) (snd <$> listSelectedElement artists')+ let yalbums' = maybe id listMoveTo mi $ list YalbumsList yalbumsVec 1+ songsVec <- liftIO $ maybe (pure empty) songsOfAlbum (snd . snd <$> listSelectedElement yalbums')+ let songs' = maybe id listMoveTo mj $ list SongsList songsVec 1+ pure $ s & libraryL . yalbumsL .~ yalbums'+ & libraryL . songsL .~ songs'+++-- | Rebuild library state from selected album.+rebuildLibAlbums :: MonadIO m => HumState -> m HumState rebuildLibAlbums s = do- let albums' = s ^. libraryL . albumsL- songsVec <- liftIO $ songsOfAlbum (snd <$> listSelectedElement albums')+ let yalbums' = s ^. libraryL . yalbumsL+ songsVec <- liftIO $ maybe (pure empty) songsOfAlbum (snd . snd <$> listSelectedElement yalbums') let songs' = list SongsList songsVec 1- pure $ s & libraryL . albumsL .~ albums' & libraryL . songsL .~ songs'+ pure $ s & libraryL . songsL .~ songs' -rebuildPl :: MonadIO m => HState -> m HState+-- | Rebuild library state from selected album, keeping the selected song index if possible.+reloadLibAlbums :: MonadIO m => HumState -> m HumState+reloadLibAlbums s = do+ let mi = s ^. libraryL . songsL & listSelected+ let yalbums' = s ^. libraryL . yalbumsL+ songsVec <- liftIO $ maybe (pure empty) songsOfAlbum (snd . snd <$> listSelectedElement yalbums')+ let songs' = maybe id listMoveTo mi $ list SongsList songsVec 1+ pure $ s & libraryL . songsL .~ songs'+++-- | Rebuild entire stored playlists state, keeping the index of the left column if possible.+rebuildPl :: MonadIO m => HumState -> m HumState rebuildPl s = do- plListVec <- liftIO $ V.fromList . fromRight [] <$> withMPD MPD.listPlaylists- let plList' = list PlaylistList plListVec 1+ let mi = s ^. playlistsL . plListL & listSelected+ plListVec <- liftIO $ V.fromList . sort . fromRight [] <$> withMPD MPD.listPlaylists+ let plList' = maybe id listMoveTo mi $ list PlaylistList plListVec 1 plSongsVec <- liftIO $ V.fromList . fromRight [] <$> withMPD (MPD.listPlaylistInfo (maybe "<no playlists>" snd (listSelectedElement plList'))@@ -65,7 +124,8 @@ pure $ s & playlistsL . plListL .~ plList' & playlistsL . plSongsL .~ plSongs' -rebuildPlList :: MonadIO m => HState -> m HState+-- | Rebuild stored playlists state from selected playlist.+rebuildPlList :: MonadIO m => HumState -> m HumState rebuildPlList s = do let plList' = s ^. playlistsL . plListL plSongsVec <- liftIO@@ -75,11 +135,35 @@ ) ) let plSongs' = (, False) <$> list PlaylistSongs plSongsVec 1- pure $ s & playlistsL . plListL .~ plList'- & playlistsL . plSongsL .~ plSongs'+ pure $ s & playlistsL . plSongsL .~ plSongs' -rebuildQueue :: MonadIO m => HState -> m HState+-- | Rebuild stored playlists state from selected playlist, keeping the selected song index if possible.+reloadPlList :: MonadIO m => HumState -> m HumState+reloadPlList s = do+ let mi = s ^. playlistsL . plSongsL & listSelected+ let plList' = s ^. playlistsL . plListL+ plSongsVec <- liftIO+ (V.fromList . fromRight [] <$> withMPD+ (MPD.listPlaylistInfo+ (maybe "<no playlists>" snd (listSelectedElement plList'))+ )+ )+ let plSongs' = maybe id listMoveTo mi $ (, False) <$> list PlaylistSongs plSongsVec 1+ pure $ s & playlistsL . plSongsL .~ plSongs'+++-- | Rebuild queue state, keeping the index of the left column if possible.+rebuildQueue :: MonadIO m => HumState -> m HumState rebuildQueue s = do+ let mi = s ^. queueL & listSelected queueVec <- liftIO $ V.fromList . fromRight [] <$> withMPD (MPD.playlistInfo Nothing)- let queue' = (, False) <$> list QueueList queueVec 1+ let queue' = maybe id listMoveTo mi $ (, False) <$> list QueueList queueVec 1 pure $ s & queueL .~ queue'++-- | Rebuild status and current song state.+rebuildStatus :: MonadIO m => HumState -> m HumState+rebuildStatus s = do+ currentSong' <- liftIO (fromRight Nothing <$> withMPD MPD.currentSong)+ status' <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))+ pure $ s & currentSongL .~ currentSong'+ & statusL .~ status'
src/Hum/Types.hs view
@@ -1,89 +1,109 @@--- |+-- | Module : Hum.Types+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Types! module Hum.Types where-import Network.MPD as MPD-import qualified Brick.BChan as BC+import Network.MPD+import Brick.BChan import Brick.Types import Brick.Widgets.Edit import Brick.Widgets.List-+import Hum.Orphans ( ) -data HState = HState- { chan :: !(BC.BChan HumEvent)- , hview :: !View- , status :: !(Maybe MPD.Status)- , mode :: !Mode- , ex :: !ExState- , currentSong :: !(Maybe MPD.Song)- , queue :: !SongList+-- | Describes the state of the app.+data HumState = HumState+ { chan :: !(BChan HumEvent) -- ^ The channel for MPD and time events+ , hview :: !View -- ^ The current view: Queue, Library, etc.+ , status :: !(Maybe Status) -- ^ MPD's status+ , mode :: !Mode -- ^ Input mode+ , ex :: !ExState -- ^ The state of the ex mode style prompt at the bottom+ , currentSong :: !(Maybe Song)+ , queue :: !SongList -- ^ Also called the playlist in MPD , library :: !LibraryState , playlists :: !PlaylistsState- , extentMap :: !(Map Name (Maybe (Extent Name))) , clipboard :: !Clipboard- , focus :: !Focus- , editable :: !Bool+ , focus :: !Focus -- ^ The current focus in each view+ , editable :: !Bool -- ^ Whether the selected stored playlist is editable , prompts :: !Prompts- , helpScreen :: !Int --HACK+ , help :: !HelpState -- ^ Help View }--- deriving (Show) --, Eq) + data LibraryState = LibraryState- { artists :: !(List Name MPD.Value)- , albums :: !(List Name MPD.Value)- , songs :: !(List Name Song)+ { artists :: !(List Name Value) -- ^ All album artists+ , yalbums :: !(List Name (Value,Value)) -- ^ Year-Album pairs of the selected artist+ , yalbumSort :: !Bool -- ^ Toggle sort of yalbums between years and alphabeitcal order+ , songs :: !(List Name Song) -- ^ Songs in selected album } +-- | Stored playlists data PlaylistsState = PlaylistsState- { plList :: !(List Name PlaylistName)- , plSongs :: !SongList+ { plList :: !(List Name PlaylistName) -- ^ List of stored playlists+ , plSongs :: !SongList -- ^ Songs in selected playlist }--data ExSubMode = Cmd | FSearch | BSearch+data HelpState = HelpState+ { helpText :: !Text -- ^ Contents of help screen+ , helpSearchInt :: !Int -- ^ number of focused search match+ }+-- | Specific mode in the bottom prompt+data ExSubMode =+ Cmd -- ^ Function commands+ | FSearch -- ^ Forward search+ | BSearch -- ^ Backwards search deriving (Show, Eq, Ord) data ExState = ExState { exPrefix :: !ExSubMode , exEditor :: !(Editor Text Name)- , searchDirection :: !Bool+ , searchDirection :: !Bool -- ^ Search direction of last search , searchHistory :: ![Text] , cmdHistory :: ![Text] } data Prompts = Prompts- { currentPrompt :: !PromptType- , promptTitle :: Text- , plSelectPrompt :: !(List Name (Maybe PlaylistName))- , textPrompt :: !(Editor Text Name)- , exitPrompt :: HState -> EventM Name HState+ { currentPrompt :: !PromptType+ , promptTitle :: Text+ , plSelectPrompt :: !(List Name (Maybe PlaylistName)) -- ^ List to select playlist from+ , textPrompt :: !(Editor Text Name) -- ^ Editor if needed+ , exitPromptFunc :: Bool -> HumState -> EventM Name HumState -- ^ Executes on exit from prompt, True for execute and False for quit. } -data PromptType = PlSelectPrompt | YNPrompt | TextPrompt+data PromptType =+ PlSelectPrompt -- ^ Select playlist to add songs to+ | YNPrompt -- ^ General yes/no prompt+ | TextPrompt -- ^ General enter text and do stuff prompt deriving (Show,Eq) -data Clipboard = Clipboard { clSongs :: !SongList- , clPlName :: !(Maybe PlaylistName)}--data Mode = NormalMode | ExMode | PromptMode+data Clipboard = Clipboard { clSongs :: !SongList -- ^ Last list of songs copied+ , clPlName :: !(Maybe PlaylistName) -- ^ Last playlist name copied+ }+-- | General input mode+data Mode =+ NormalMode -- ^ Vim normal mode style movement+ | ExMode -- ^ Type ex style commands or search+ | PromptMode -- ^ Interact with a prompt deriving (Show,Eq) +type Highlight = Bool+ type SongList = List Name (Song, Highlight) type HumEvent = Either Tick (Response [Subsystem]) -data Name = NowPlaying | ClSongs+-- | Brick widget names+data Name =+ NowPlaying | ClSongs | Queue | QueueList- | Library | ArtistsList | LibraryLeft | AlbumsList | LibraryMid | SongsList | LibraryRight+ | Library | ArtistsList | LibraryLeft | AlbumsList | YalbumsList | LibraryMid | SongsList | LibraryRight | PlaylistList | PlaylistLeft | PlaylistSongs | PlaylistRight+ | Help | ExEditor | TextPromptEditor deriving (Show, Eq, Ord) -data FocQueue = FocQueue- deriving (Show, Eq, Ord)-data FocLib = FocArtists | FocAlbums | FocSongs- deriving(Show,Eq,Ord,Enum)-data FocPlay = FocPlaylists | FocPSongs- deriving(Show,Eq,Ord,Enum) data Focus = Focus { focQueue :: FocQueue , focLib :: FocLib@@ -92,17 +112,25 @@ } deriving (Show, Eq, Ord) +data FocQueue = FocQueue+ deriving (Show, Eq, Ord)++data FocLib = FocArtists | FocAlbums | FocSongs+ deriving(Show,Eq,Ord,Enum)++data FocPlay = FocPlaylists | FocPSongs+ deriving(Show,Eq,Ord,Enum)+ data View = QueueView | LibraryView | PlaylistsView | HelpView deriving (Show,Eq,Ord) -type Highlight = Bool- data Tick = Tick -suffixLenses ''HState+suffixLenses ''HumState suffixLenses ''Focus suffixLenses ''LibraryState suffixLenses ''PlaylistsState+suffixLenses ''HelpState suffixLenses ''ExState suffixLenses ''Prompts suffixLenses ''Clipboard
src/Hum/UI.hs view
@@ -1,4 +1,12 @@-{-# LANGUAGE LambdaCase #-}++-- | Module : Hum.UI+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- The starting point of Hum's logic. Contains things like building the initial+-- state, shared keypresses, and the UI drawing function.+ module Hum.UI where @@ -17,14 +25,12 @@ import Hum.Attributes import Hum.Views import Hum.Modes-import Hum.Utils import Hum.Rebuild-import qualified Data.Map.Strict as Map import Control.Lens import System.Directory -app :: App HState HumEvent Name-+-- | The brick app record for Hum.+app :: App HumState HumEvent Name app = App { appDraw = drawUI , appChooseCursor = chooseCursor , appHandleEvent = handleEvent@@ -32,7 +38,8 @@ , appAttrMap = const humAttrMap } -drawUI :: HState -> [Widget Name]+-- | Draws shared UI elements and dispatches for view specific ones.+drawUI :: HumState -> [Widget Name] drawUI st = [if st ^. modeL == PromptMode then drawPrompt st else emptyWidget, drawNowPlaying st@@ -50,7 +57,8 @@ else txt " " ] -chooseCursor :: HState -> [CursorLocation Name] -> Maybe (CursorLocation Name)+-- | Make sure cursor is displayed when editing text.+chooseCursor :: HumState -> [CursorLocation Name] -> Maybe (CursorLocation Name) chooseCursor st ls | st ^. focusL . focExL = find (isCurrent ExEditor) ls | st ^. promptsL . currentPromptL == TextPrompt = find (isCurrent TextPromptEditor) ls@@ -58,7 +66,9 @@ where isCurrent n cl = cl ^. cursorLocationNameL == Just n -buildInitialState :: BC.BChan HumEvent -> IO HState+-- | builds ''HumState' for the first time. A lot of overlap with functions from+-- | "Hum.Rebuild", but they are hard to reuse because 'HumState' is strict.+buildInitialState :: BC.BChan HumEvent -> IO HumState buildInitialState chan = do configDir <- getXdgDirectory XdgConfig "hum" _ <- createDirectoryIfMissing True configDir@@ -72,7 +82,6 @@ currentSong <- fromRight Nothing <$> withMPD MPD.currentSong status <- fromRight Nothing <$> (Just <<$>> withMPD MPD.status) let hview = QueueView- let extentMap = Map.empty let focus = Focus { focQueue = FocQueue , focLib = FocArtists , focPlay = FocPlaylists@@ -82,21 +91,21 @@ , clPlName = Nothing} queueVec <- V.fromList . fromRight [] <$> withMPD (MPD.playlistInfo Nothing) let queue = (, False) <$> list QueueList queueVec 1- artistsVec <- V.fromList . fromRight [] <$> withMPD- (MPD.list MPD.AlbumArtist Nothing)+ artistsVec <- V.fromList . fromRight [] <$> withMPD (MPD.list MPD.AlbumArtist mempty) let artists = list ArtistsList artistsVec 1- albumsVec <- albumsOfArtist (snd <$> listSelectedElement artists)- let albums = list AlbumsList albumsVec 1- songsVec <- songsOfAlbum (snd <$> listSelectedElement albums)+ let yalbumSort = True+ yalbumsVec <- maybe (pure empty) (yalbumsOfArtist yalbumSort) (snd <$> listSelectedElement artists)+ let yalbums = list YalbumsList yalbumsVec 1+ songsVec <- maybe (pure empty) songsOfAlbum (snd . snd <$> listSelectedElement yalbums) let songs = list SongsList songsVec 1- plListVec <- V.fromList . fromRight [] <$> withMPD MPD.listPlaylists+ let library = LibraryState { artists, yalbums, yalbumSort, songs }+ plListVec <- V.fromList . sort . fromRight [] <$> withMPD MPD.listPlaylists let plList = list PlaylistList plListVec 1 plSongsVec <- V.fromList . fromRight [] <$> withMPD ( MPD.listPlaylistInfo $ maybe "<no playlists>" snd (listSelectedElement plList) ) let plSongs = (, False) <$> list PlaylistSongs plSongsVec 1- let library = LibraryState { artists, albums, songs } let playlists = PlaylistsState { plList, plSongs } let editable = False let prompts = Prompts@@ -104,42 +113,36 @@ , promptTitle = "" , textPrompt = editorText TextPromptEditor (Just 1) "" , plSelectPrompt = listInsert 0 Nothing (Just <$> plList)- , exitPrompt = \s -> pure $ s & modeL .~ NormalMode+ , exitPromptFunc = \_ s -> pure $ s & modeL .~ NormalMode }- let helpScreen = 0- pure HState { chan+ let help = HelpState+ { helpText = helpText'+ , helpSearchInt = 0+ }+ pure HumState { chan , hview , mode , ex , status , currentSong , queue- , extentMap , clipboard , library , playlists+ , help , focus , editable , prompts- , helpScreen } -humStartEvent :: HState -> EventM Name HState+-- | Initial event.+humStartEvent :: HumState -> EventM Name HumState humStartEvent = pure -hBoxPad :: Padding -> [Widget n] -> Widget n-hBoxPad _ [] = emptyWidget-hBoxPad _ [w ] = w-hBoxPad p (w : ws) = padRight p w <+> hBoxPad p ws--seekCurEventM :: MPD.FractionalSeconds -> HState -> EventM Name HState-seekCurEventM i s = do- _ <- liftIO (withMPD $ MPD.seekCur False i)- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- song <- liftIO (withMPD MPD.currentSong)- pure s { currentSong = fromRight Nothing song, status }--handleEvent :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+-- | handles keypresses shared between views, and nonkeypress events from the+-- channels (ticks every 0.1 seconds and mpd events). Dispatches for view+-- specific keypresses.+handleEvent :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleEvent s e = case e of VtyEvent vtye -> case s ^. modeL of ExMode -> handleExEvent s e@@ -149,24 +152,20 @@ EvKey (KChar 't') [] -> do st <- liftIO ((MPD.stState <$>) <$> withMPD MPD.status) _ <- case st of- Left _ -> liftIO (withMPD $ MPD.pause True)- Right MPD.Paused -> liftIO (withMPD $ MPD.play Nothing)- Right MPD.Stopped -> liftIO (withMPD $ MPD.play Nothing)- Right MPD.Playing -> liftIO (withMPD $ MPD.pause True)+ Left _ -> liftIO (withMPD $ MPD.pause True)+ Right _ -> liftIO (withMPD $ MPD.toggle) continue s EvKey (KChar 's') [] -> do _ <- liftIO (withMPD $ MPD.single (maybe False (not . MPD.stSingle) (status s)))- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- continue s { status }+ continue =<< rebuildStatus s EvKey (KChar 'c') [] -> do _ <- liftIO ( withMPD $ MPD.consume (maybe False (not . MPD.stConsume) (status s)) )- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- continue s { status }+ continue =<< rebuildStatus s EvKey (KChar 'x') [] -> do _ <- liftIO (withMPD $ MPD.crossfade@@ -176,19 +175,16 @@ ) $ maybe 0 MPD.stXFadeWidth (status s) )- ) -- TODO- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- continue s { status }+ )+ continue =<< rebuildStatus s EvKey (KChar 'r') [] -> do _ <- liftIO (withMPD $ MPD.repeat (maybe False (not . MPD.stRepeat) (status s)))- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- continue s { status }+ continue =<< rebuildStatus s EvKey (KChar 'z') [] -> do _ <- liftIO (withMPD $ MPD.random (maybe False (not . MPD.stRandom) (status s)))- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- continue s { status }+ continue =<< rebuildStatus s EvKey (KChar '/') [] -> continue $ s & modeL .~ ExMode & exL . exPrefixL .~ FSearch@@ -205,16 +201,22 @@ & focusL . focExL .~ True EvKey (KChar '.') [] -> do _ <- liftIO (withMPD MPD.next)- song <- liftIO (withMPD MPD.currentSong)- continue s { currentSong = fromRight Nothing song }+ continue =<< rebuildStatus s EvKey (KChar ',') [] -> do _ <- liftIO (withMPD MPD.previous)- song <- liftIO (withMPD MPD.currentSong)- continue s { currentSong = fromRight Nothing song }- EvKey (KChar ']') [] -> continue =<< seekCurEventM 5 s- EvKey (KChar '[') [] -> continue =<< seekCurEventM (-5) s- EvKey (KChar '}') [] -> continue =<< seekCurEventM 30 s- EvKey (KChar '{') [] -> continue =<< seekCurEventM (-30) s+ continue =<< rebuildStatus s+ EvKey (KChar ']') [] -> do+ _ <- liftIO (withMPD $ MPD.seekCur False 5)+ continue =<< rebuildStatus s+ EvKey (KChar '[') [] -> do+ _ <- liftIO (withMPD $ MPD.seekCur False (-5))+ continue =<< rebuildStatus s+ EvKey (KChar '}') [] -> do+ _ <- liftIO (withMPD $ MPD.seekCur False 30)+ continue =<< rebuildStatus s+ EvKey (KChar '{') [] -> do+ _ <- liftIO (withMPD $ MPD.seekCur False (-30))+ continue =<< rebuildStatus s EvKey (KChar '1') [] -> do _ <- liftIO (BC.writeBChan (chan s) (Left Tick)) continue $ s & editableL .~ False@@ -227,26 +229,14 @@ _ <- liftIO (BC.writeBChan (chan s) (Left Tick)) continue $ s & editableL .~ False & hviewL .~ PlaylistsView- EvResize _ _ -> do- extentMap <- updateExtentMap- continue s { extentMap }+ EvResize _ _ -> continue s _ -> case hview s of QueueView -> handleEventQueue s e LibraryView -> handleEventLibrary s e PlaylistsView -> handleEventPlaylists s e HelpView -> handleEventHelp s e- (AppEvent (Left Tick)) -> do- extentMap <- updateExtentMap- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- continue s { status, extentMap }- (AppEvent (Right (Right _))) -> do- currentSong <- liftIO (fromRight Nothing <$> withMPD MPD.currentSong)- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- queueVec <- liftIO- (V.fromList . fromRight [] <$> withMPD (MPD.playlistInfo Nothing))- let queueUnmoved = (, False) <$> list QueueList queueVec 1- let queueNew = case listSelected (queue s) of- Nothing -> queueUnmoved- Just i -> listMoveTo i queueUnmoved- continue s { currentSong, status, queue = queueNew }+ (AppEvent (Left Tick)) -> continue =<< rebuildStatus s+ (AppEvent (Right (Right subs)))+ | MPD.PlaylistS `elem` subs -> continue =<< rebuildStatus =<< rebuildQueue s+ | otherwise -> continue =<< rebuildStatus s _ -> continue s
src/Hum/Utils.hs view
@@ -1,21 +1,27 @@--- | +-- | Module : Hum.Utils+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--++ module Hum.Utils where import Hum.Types import Hum.Rebuild import Brick.Types-import Brick.Main import Brick.Widgets.List import qualified Data.Vector as V import qualified Data.Text as T+import qualified Data.ByteString as BS import Network.MPD ( withMPD ) import qualified Network.MPD as MPD import qualified Data.Map.Strict as Map import Text.Printf ( printf ) import Control.Lens-import qualified Data.Witherable.Class as W+import qualified Witherable as W --- | A backwards function composition operator+-- | A backwards function composition operator that I love with my whole heart. infixl 8 ? {-# INLINE (?) #-} -- Make sure it has TWO args only on the left, so that it inlines@@ -23,44 +29,25 @@ (?) :: (a -> b) -> (b -> c) -> a -> c (?) f g = \x -> g (f x) --- | Get comma seperated metedata from tag+-- | Get comma seperated metedata from tag. meta :: Text -> MPD.Metadata -> MPD.Song -> Text meta notFound tag song = maybe notFound (T.intercalate ",") (MPD.toText <<$>> Map.lookup tag (MPD.sgTags song)) --- | like meta, but returns a Maybe for future use+-- | Like 'meta', but returns a Maybe for future use. mmeta :: MPD.Metadata -> MPD.Song -> Maybe Text mmeta tag song = T.intercalate "," <$> (MPD.toText <<$>> Map.lookup tag (MPD.sgTags song)) -+-- | Formats seconds to %M:%S. secondsToTime :: Integer -> Text secondsToTime sec = let (minutes, seconds) = divMod sec 60 in toText (printf "%d:%02d" minutes seconds :: String) -updateExtentMap :: EventM Name (Map Name (Maybe (Extent Name)))-updateExtentMap = do- queueE <- lookupExtent Queue- nowPlayingE <- lookupExtent NowPlaying- libLeftE <- lookupExtent LibraryLeft- libMidE <- lookupExtent LibraryMid- libRightE <- lookupExtent LibraryRight- playLeftE <- lookupExtent PlaylistLeft- playRightE <- lookupExtent PlaylistRight- let extentMap = Map.fromList- [ (Queue , queueE)- , (NowPlaying , nowPlayingE)- , (LibraryLeft , libLeftE)- , (LibraryMid , libMidE)- , (LibraryRight , libRightE)- , (PlaylistLeft , playLeftE)- , (PlaylistRight, playRightE)- ]- pure extentMap-+-- | Deletes highlighted songs in list from queue, does not rebuild queue. deleteHighlightedfromQ :: MPD.MonadMPD m => SongList -> m () deleteHighlightedfromQ ls = let (hls :: SongList) = W.filter snd ls@@ -69,28 +56,29 @@ ((MPD.sgId . fst . snd) =<< listSelectedElement ls) MPD.deleteId -deleteAll :: MPD.MonadMPD m => SongList -> m ()-deleteAll ls = for_ ls (\s -> whenJust (MPD.sgId . fst $ s) MPD.deleteId)+-- | Deletes list of songs from queue in MPD, does not rebuild queue.+deleteBulkfromQ :: MPD.MonadMPD m => SongList -> m ()+deleteBulkfromQ ls = for_ ls (\s -> whenJust (MPD.sgId . fst $ s) MPD.deleteId) -pasteSongstoQ :: MPD.MonadMPD m => SongList -> SongList -> m ()+-- | Adds songs to queue under the selected item in it in MPD, does not rebuild queue.+pasteSongstoQ :: MPD.MonadMPD m => SongList -> SongList -> m () -- TODO refactor to act on HumState pasteSongstoQ clip ls = let pos = listSelected ls indexedClip = V.indexed $ MPD.sgFilePath . fst <$> listElements clip in for_ indexedClip (\(n, song) -> MPD.addId song $ (+ (n + 1)) <$> pos) +-- | Produce list of highligted elements (and selected element) in input list. getHighlighted- :: (Eq e, W.Filterable t, Foldable t, Splittable t)+ :: (W.Filterable t, Traversable t) => GenericList n t (e, Highlight) -> GenericList n t (e, Highlight)-getHighlighted ls = hls where- hls = W.filter- (\(el, hl) -> hl || Just (el, hl) == (snd <$> listSelectedElement ls))- ls+getHighlighted ls = ls & listHighlightSelected ? W.filter snd ? listUnhighlightAll +-- | Paste one list into another under the selected item. listPaste :: (Splittable t, Semigroup (t e))- => GenericList n t e- -> GenericList n t e+ => GenericList n t e -- ^ List pasted into+ -> GenericList n t e -- ^ Pasted list -> GenericList n t e listPaste paste ls = let es = listElements ls@@ -98,30 +86,47 @@ (es1, es2) = Brick.Widgets.List.splitAt (pos + 1) es in ls { listElements = es1 <> listElements paste <> es2 } -deleteHighlighted :: HState- -> Lens' HState SongList- -> HState-deleteHighlighted st lns = st & clipboardL . clSongsL .~ (st ^. lns & listHighlight ? W.filter snd ? listUnhighlightAll)- & lns %~ listHighlight ? W.filter (not . snd)+-- | Delete highlighted element (and selected element) from list.+deleteHighlighted+ :: HumState+ -> Lens' HumState SongList -- ^ Lens that leads to list+ -> HumState+deleteHighlighted st lns =+ st & clipboardL . clSongsL .~ (st ^. lns & getHighlighted)+ & lns %~ listHighlightSelected ? W.filter (not . snd) --- | toggle selected items highlight status+-- | Copy highlighted element (and selected element) from list to 'Clipboard'.+yankHighlighted+ :: HumState+ -> Lens' HumState SongList -- ^ Lens that leads to list+ -> HumState+yankHighlighted st lns =+ st & clipboardL . clSongsL .~ (st ^. lns & getHighlighted)++-- | Toggle selected items highlight status. listToggleHighlight :: Traversable t => GenericList n t (e,Highlight) -> GenericList n t (e,Highlight) listToggleHighlight = listModify (second not) --- | Highlight selcted item status-listHighlight :: Traversable t => GenericList n t (e,Highlight) -> GenericList n t (e,Highlight)-listHighlight = listModify (second (const True))+-- | Highlight selcted item.+listHighlightSelected :: Traversable t => GenericList n t (e,Highlight) -> GenericList n t (e,Highlight)+listHighlightSelected = listModify (second (const True)) +-- | Unhighlight selcted item. listUnhighlightAll :: Traversable t => GenericList n t (e,Highlight) -> GenericList n t (e,Highlight) listUnhighlightAll = fmap (second $ const False) -saveListToPl :: MPD.MonadMPD m => SongList -> Text -> m ()+-- | Save list of songs to a stored playlist. If exists does nothing.+saveListToPl :: MPD.MonadMPD m =>+ SongList+ -> Text -- ^ Name of playlist to save to+ -> m () -- TODO use unusedPlName saveListToPl ls name = let songpaths = MPD.sgFilePath . fst <$> listElements ls name' = fromString . T.unpack $ name in for_ songpaths (MPD.playlistAdd name') +-- | Overwrite stored playlist with new song list. overwriteListToPl :: MPD.MonadMPD m => SongList -> Text -> m () overwriteListToPl ls name = let songpaths = MPD.sgFilePath . fst <$> listElements ls@@ -129,34 +134,56 @@ in MPD.playlistClear name' >> for_ songpaths (MPD.playlistAdd name') -saveEditedPl :: HState -> EventM n HState-saveEditedPl st = do- let plSongs = st ^. playlistsL . plSongsL- let plName = st ^. playlistsL . plListL & listSelectedElement ? maybe "unnamed" snd ? MPD.toText- _ <- liftIO . withMPD $ overwriteListToPl plSongs plName- pure st+-- | Save edited playlist in Playlist view to disk.+saveEditedPl :: Bool -> HumState -> EventM n HumState+saveEditedPl bl st = if bl+ then do+ let plSongs = st ^. playlistsL . plSongsL+ let plName = st ^. playlistsL . plListL & listSelectedElement ? maybe "unnamed" snd ? MPD.toText+ _ <- liftIO . withMPD $ overwriteListToPl plSongs plName+ reloadPlList st+ else reloadPlList st -deleteSelectedPl :: HState -> EventM n HState-deleteSelectedPl st = do- let plName = st ^. playlistsL . plListL & listSelectedElement <&> snd- _ <- liftIO . withMPD $ traverse MPD.rm plName- rebuildPl st+-- | Deletes selected playlist in Playlist view from disk.+deleteSelectedPl :: Bool -> HumState -> EventM n HumState+deleteSelectedPl bl st = if bl+ then do+ let plName = st ^. playlistsL . plListL & listSelectedElement <&> snd+ _ <- liftIO . withMPD $ traverse MPD.rm plName+ rebuildPl st+ else pure st -duplicatePlaylist :: MPD.PlaylistName -> HState -> EventM n HState+-- | Appends smallest number possible to playlist name for it to not be taken.+-- Does nothing if name is untaken.+unusedPlName :: MPD.PlaylistName -> IO MPD.PlaylistName+unusedPlName prefix = do+ plNames <- fromRight [] <$> (liftIO . withMPD $ MPD.listPlaylists)+ let newPlName = viaNonEmpty head $ filter (`notElem` plNames) (prefix:(append' prefix . show <$> [2::Int ..]))+ pure (fromMaybe "unnamed" newPlName) -- HACK+ where+ append' (MPD.PlaylistName x) (MPD.PlaylistName y) = MPD.PlaylistName (BS.append x y)++-- | Duplicates stored playlist on disk (with nonconflicting name).+duplicatePlaylist :: MPD.PlaylistName -> HumState -> EventM n HumState -- HACK duplicatePlaylist pl st = do songs <- V.fromList . fromRight [] <$> (liftIO . withMPD $ MPD.listPlaylistInfo pl)- plNames <- (MPD.toText <$>) . fromRight [] <$> (liftIO . withMPD $ MPD.listPlaylists)- let newPlName = viaNonEmpty head $ filter (`notElem` plNames) (map ((\tx num -> tx <> "-copy" <> show num) (MPD.toText pl)) [1::Int ..])- traverse_ (\pln -> songBulkAddtoPl pln songs st) (T.unpack <$> newPlName)+ newPlName <- liftIO $ unusedPlName pl+ _ <- songBulkAddtoPl (MPD.toString newPlName) songs st rebuildPl st -pastePlaylist :: HState -> EventM n HState+-- | Pastes playlist in clipboard to disk (with nonconflicting name).+pastePlaylist :: HumState -> EventM n HumState pastePlaylist st = do let plName = fromMaybe "<error>" (st ^. clipboardL . clPlNameL) duplicatePlaylist plName st -songBulkAddtoQ :: Bool -> V.Vector MPD.Song -> HState -> EventM n HState-songBulkAddtoQ play songs s = do+-- | Adds list of songs to queue in MPD. Does not rebuild state.+songBulkAddtoQ+ :: Bool -- ^ If true plays first song added+ -> V.Vector MPD.Song+ -> HumState+ -> EventM n HumState+songBulkAddtoQ play songs s = do -- TODO don't need s? maybe should rebuild? overlap with system in Hum.UI let songPaths = MPD.sgFilePath <$> songs traverse_ (\sel -> liftIO@@ -168,10 +195,14 @@ (V.take 1 songPaths) traverse_ (\sel -> liftIO (withMPD $ MPD.addId sel Nothing)) (V.drop 1 songPaths)- song <- liftIO (withMPD MPD.currentSong)- pure s { currentSong = fromRight Nothing song, queue = queue s }+ pure s -songBulkAddtoPl :: String -> V.Vector MPD.Song -> HState -> EventM n HState+-- | Adds list of songs to stored playlist in MPD. Does not rebuild state.+songBulkAddtoPl+ :: String -- ^ Playlist Name+ -> V.Vector MPD.Song -- ^ Songs to add+ -> HumState+ -> EventM n HumState songBulkAddtoPl pl songs s = do let songPaths = MPD.sgFilePath <$> songs traverse_
src/Hum/Views/Common.hs view
@@ -1,6 +1,11 @@-{-# LANGUAGE LambdaCase #-}--- | +-- | Module : Hum.Views.Library+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Functions for the Help view.+ module Hum.Views.Common where import Hum.Types import Brick.Types@@ -11,12 +16,12 @@ import Hum.Attributes import Hum.Utils import qualified Network.MPD as MPD-import qualified Data.Map.Strict as Map import qualified Data.Text as T import Brick.Widgets.List import Control.Lens -drawNowPlaying :: HState -> Widget Name+-- | Draw Now Playing box.+drawNowPlaying :: HumState -> Widget Name drawNowPlaying st = reportExtent NowPlaying $ vLimit 5 . center $ maybe (txt "nothing.") nowPlaying@@ -29,14 +34,14 @@ <=> progbar <=> (padRight Max playing <+> padLeft Max mode) where- title = withAttr queueTitleAttr $ txt $ meta "<no title>" MPD.Title song+ title = withAttr titleAttr $ txt $ meta "<no title>" MPD.Title song album =- withAttr queueAlbumAttr (txt $ meta "<no album>" MPD.Album song)+ withAttr albumAttr (txt $ meta "<no album>" MPD.Album song) <+> txt " ("- <+> withAttr queueDateAttr (txt $ meta "????" MPD.Date song)+ <+> withAttr dateAttr (txt $ meta "????" MPD.Date song) <+> txt ")"- artist = withAttr queueArtistAttr $ txt $ meta "<no one>" MPD.Artist song- progbar = withAttr queueTimeAttr $ drawProgressBar st+ artist = withAttr artistAttr $ txt $ meta "<no one>" MPD.Artist song+ progbar = withAttr timeAttr $ drawProgressBar st playing = txt $ maybe "[ ]" ((\t -> "[" <> t <> "]") . T.toLower . show . MPD.stState)@@ -58,51 +63,61 @@ mode = txt $ "[" <> repeatmpd <> random <> single <> consume <> crossfade <> "]" -drawProgressBar :: HState -> Widget Name-drawProgressBar st = case width of- 0 -> txt $ Prelude.toText timeText- _ -> bar+-- | Draw progress bar for song time.+drawProgressBar :: HumState -> Widget Name+drawProgressBar st =+ Widget Fixed Fixed $ do+ ctx <- getContext+ let width = ctx ^. windowWidthL+ render $ bar width where- width =- maybe 0 (fst . extentSize) (join (Map.lookup NowPlaying $ extentMap st)) songTime = fromMaybe (0, 1) (MPD.stTime =<< status st) timeText = toString . (\(i, j) -> secondsToTime (round i) <> "/" <> secondsToTime (round j)) $ songTime- completed = (\w (i, j) -> round ((i / j) * fromIntegral w)) width songTime- bar = str+ completed width = (\w (i, j) -> round ((i / j) * fromIntegral w)) width songTime+ bar width = str (zipWith (\a b -> if a `elem` ("1234567890/:" :: String) then a else b) (replicate (-5 + div width 2) ' ' ++ timeText ++ replicate (-3 + div width 2) ' ' )- (replicate completed '=' ++ replicate (width - completed) ' ')+ (replicate (completed width) '=' ++ replicate (width - (completed width)) ' ') ) ---drawEx :: HState -> Widget Name---drawEx st =+-- | Either a number or a percent. To be used for widget horizontal size.+data PerCol =+ Per Int -- ^ percent size+ | Col Int -- ^ column number size -data PerCol = Per Int | Col Int-column :: Maybe PerCol -> Padding -> Padding -> Widget n -> Widget n+-- | Helper function for drawing column rows.+column+ :: Maybe PerCol -- ^ Maximum width, greedy if Nothing.+ -> Padding -- ^ Left padding+ -> Padding -- ^ Right padding+ -> Widget n+ -> Widget n column maxWidth left right w = case maxWidth of Nothing -> wpad Just (Per m) -> hLimitPercent m wpad Just (Col m) -> hLimit m wpad where wpad = padLeft left . padRight right $ w +-- | Returns True if text is substring of one of the given tags of the given song. songSearch :: Text -> [MPD.Metadata] -> MPD.Song -> Bool songSearch text metadata song = let mtags = (T.toLower <$>) . (`mmeta` song) <$> metadata in or $ fromMaybe False <$> (T.isInfixOf (T.toLower text) <<$>> mtags) -+-- | Returns True if text is substring of the given strings. stringySearch :: MPD.ToString a => Text -> a -> Bool stringySearch text value = T.isInfixOf (T.toLower text) (T.toLower . MPD.toText $ value) -drawPrompt :: HState -> Widget Name+-- | Draws a prompt.+drawPrompt :: HumState -> Widget Name drawPrompt st = case st ^. promptsL . currentPromptL of PlSelectPrompt -> centerLayer@@ -111,7 +126,7 @@ . center $ (hCenter . txt $ st ^. promptsL . promptTitleL) <=> hBorder- <=> renderListWithIndex choosePlRow+ <=> renderListWithIndex drawPlSelectRow True (st ^. promptsL . plSelectPromptL) TextPrompt ->@@ -130,8 +145,8 @@ $ (hCenter . txt $ st ^. promptsL . promptTitleL) <=> (hCenter . txt $ "[y/n]") --choosePlRow :: Int -> Bool -> Maybe MPD.PlaylistName -> Widget n-choosePlRow i _ pl = if i==0 then+-- | Draw row in playlist select prompt.+drawPlSelectRow :: Int -> Bool -> Maybe MPD.PlaylistName -> Widget n+drawPlSelectRow i _ pl = if i==0 then str "New Playlist" <=> modifyDefAttr (const wobAttr) hBorder else str (MPD.toString $ fromMaybe "<error getting playlist name>" pl)
src/Hum/Views/Help.hs view
@@ -1,6 +1,12 @@-{-#LANGUAGE RankNTypes#-}--- | +-- | Module : Hum.Views.Help+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Shared functions for views.++ module Hum.Views.Help where import Prelude hiding ( Down ) import Hum.Types@@ -8,22 +14,44 @@ import Graphics.Vty.Input.Events import Brick.Main import Brick.Widgets.Core-import Brick.Widgets.Center+import Brick.Widgets.Search import Control.Lens+import Text.Regex.TDFA.Text+import Text.Regex.TDFA +-- | Draws help.+drawViewHelp :: HumState -> Widget Name+drawViewHelp st = viewport Help Vertical (helpW st) -drawViewHelp :: HState -> Widget Name-drawViewHelp st = center ((hCenter . txt $ "j/k cycle between help screens.") <=> txt (helpText st))+-- | Help widget. Parses the last search as a case insensitive POSIX regex.+helpW :: HumState -> Widget Name+helpW st =+ let htx = st ^. helpL . helpTextL+ hi = st ^. helpL . helpSearchIntL+ mterm = viaNonEmpty head (st ^. exL . searchHistoryL)+ mterm' = if mterm == Just "" then Nothing else mterm+ eterm = maybe (Left "empty") (compile defaultCompOpt{caseSensitive = False} defaultExecOpt) mterm'+ in either (\_ -> txt htx) (\term -> regexW hi term htx) eterm -helpText :: HState -> Text-helpText st = unlines $ case st^.helpScreenL of- 0 -> [+-- | Help widget. Parses the last search as an exact match.+helpW' :: HumState -> Widget Name+helpW' st =+ let htx = st ^. helpL . helpTextL+ hi = st ^. helpL . helpSearchIntL+ mterm = viaNonEmpty head (st ^. exL . searchHistoryL)+ mterm' = if mterm == Just "" then Nothing else mterm+ in maybe (txt htx) (\term -> searchW hi term htx) mterm'++-- | Helper function that keeps "Hum.UI" tidy.+helpText' :: Text+helpText' = unlines+ [ "Change views:" , " 1 - queue" , " 2 - library" , " 3 - playlists" , ""- , "General Bindings:"+ , "General bindings:" , " t - play/pause toggle" , " , - previous song" , " . - next song"@@ -38,9 +66,9 @@ , " c - toggle consume mode in mpd" , " x - toggle crossfade mode in mpd" , " r - toggle repeat mode in mpd"- , " z - toggle random mode in mpd"]- 1 -> [- "Queue keybindings:"+ , " z - toggle random mode in mpd"+ , ""+ , "Queue keybindings:" , " SPC - select song" , " y and d - yank and delete the selected songs" , " p - paste selected song"@@ -49,31 +77,43 @@ , "Library and Playlists keybindigns:" , " SPC - add song/song collection to queue" , " RET - add song/song collection to queue, and start playing the first one"+ , " ` - toggle sort of the album column between release order and alphabetical order" , "" , "Playlists keybindigns:"- , " on playlist conents:"+ , " On playlist contents:" , " e - make playlist editable, press again to get save prompt." , " editing a playlist is the same as editing the queue"- , " on list of playlists:"+ , ""+ , " On list of playlists:"+ , " e - rename playlist" , " y and p - copy and paste playlists (with -copy added to the name)" , " d - delete playlist (with prompt)" , ""- , "Commands:"+ , "commands:" , ":help - gets you this" , ":q - quits" , ":save $name - saves the queue to a playlist called $name" ]- _ -> ["something went wrong."] +-- | Updates which mathc is focused in the help buffer.+helpSearch+ :: Bool -- ^ Search direction, True for forward.+ -> HumState+ -> EventM Name HumState+helpSearch dir st =+ pure $ if dir+ then st & helpL . helpSearchIntL %~ (\x->x+1)+ else st & helpL . helpSearchIntL %~ (\x->x-1) +-- | handle key events in help view. handleEventHelp- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleEventHelp s e = case e of VtyEvent vtye -> case vtye of- EvKey (KChar 'j') [] -> continue $ s & helpScreenL %~ (\x -> if x==1 then 0 else x+1)- EvKey (KChar 'k') [] -> continue $ s & helpScreenL %~ (\x -> if x==0 then 1 else x-1)- EvKey (KChar 'n') [] -> continue s- EvKey (KChar 'N') [] -> continue s+ EvKey (KChar 'j') [] -> vScrollBy (viewportScroll Help) 1 >> continue s+ EvKey (KChar 'k') [] -> vScrollBy (viewportScroll Help) (-1) >> continue s+ EvKey (KChar 'n') [] -> continue =<< helpSearch (s ^. exL . searchDirectionL) s+ EvKey (KChar 'N') [] -> continue =<< helpSearch (s ^. exL . searchDirectionL & not) s EvKey (KChar 'G') [] -> continue s EvKey (KChar 'g') [] -> continue s _ -> continue s
src/Hum/Views/Library.hs view
@@ -1,8 +1,12 @@-{-#LANGUAGE RankNTypes#-}--- | +-- | Module : Hum.Views.Library+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Functions for the Library view.+ module Hum.Views.Library where-import Prelude hiding ( Down ) import Hum.Types import Brick.Types import Graphics.Vty.Input.Events@@ -16,21 +20,18 @@ import Hum.Views.Common import Hum.Rebuild import qualified Data.Text as T-import Network.MPD ( withMPD )+import qualified Data.Vector as V import qualified Network.MPD as MPD-import qualified Data.Map.Strict as Map import Hum.Utils ---drawLibraryLeft :: HState -> Widget Name-drawLibraryLeft st =- let vsize = case join $ Map.lookup LibraryLeft $ extentMap st of- Just e -> snd . extentSize $ e- Nothing -> 20- in reportExtent LibraryLeft $ hCenter- ( viewport LibraryLeft Vertical- . visible+-- | Draw left column in Library view.+drawLibraryLeft :: HumState -> Widget Name+drawLibraryLeft st = reportExtent LibraryLeft $+ Widget Greedy Greedy $ do+ ctx <- getContext+ let vsize = ctx ^. windowHeightL - 6 -- HACK Don't hardcode nowplaying size?+ render $ hCenter+ ( visible . vLimit vsize . center $ hBorder@@ -40,33 +41,33 @@ (MPD.toText <$> st ^. libraryL . artistsL) ) )-drawLibraryMid :: HState -> Widget Name-drawLibraryMid st =- let vsize = case join $ Map.lookup LibraryMid $ extentMap st of- Just e -> snd . extentSize $ e- Nothing -> 20- in reportExtent LibraryMid $ hCenter- ( viewport LibraryMid Vertical- . visible++-- | Draw middle column in Library view.+drawLibraryMid :: HumState -> Widget Name+drawLibraryMid st = reportExtent LibraryMid $+ Widget Greedy Greedy $ do+ ctx <- getContext+ let vsize = ctx ^. windowHeightL - 6 -- HACK Don't hardcode nowplaying size?+ render $ hCenter+ ( visible . vLimit vsize . center $ hBorder <=> hCenter- (renderList (const $ libraryRow st LibraryMid)+ (renderList (const $ libraryAlbumRow) ((focLib . focus $ st) == FocAlbums)- (MPD.toText <$> st ^. libraryL . albumsL)+ (st ^. libraryL . yalbumsL) ) ) --drawLibraryRight :: HState -> Widget Name-drawLibraryRight st =- let vsize = case join $ Map.lookup LibraryRight $ extentMap st of- Just e -> snd . extentSize $ e- Nothing -> 20- in reportExtent LibraryRight $ hCenter- ( viewport LibraryRight Vertical- . visible+-- | Draw right column in Library view.+drawLibraryRight :: HumState -> Widget Name+drawLibraryRight st = reportExtent LibraryRight $+ Widget Greedy Greedy $ do+ ctx <- getContext+ let vsize = ctx ^. windowHeightL - 6 -- HACK Don't hardcode nowplaying size?+ render $ hCenter+ ( visible . vLimit vsize . center $ hBorder@@ -77,87 +78,101 @@ ) ) --libraryRow :: HState -> Name -> T.Text -> Widget n+-- | Draw generic column row in Library view.+libraryRow :: HumState -> Name -> T.Text -> Widget n -- TODO refactor? libraryRow _ name val = withAttr (case name of- LibraryLeft -> queueArtistAttr- LibraryMid -> queueAlbumAttr+ LibraryLeft -> artistAttr+ LibraryMid -> albumAttr _ -> listAttr ) $ column Nothing (Pad 1) Max $ txt val -librarySongRow :: HState -> MPD.Song -> Widget n+-- | Draw row in album column in Library view.+libraryAlbumRow :: (MPD.Value,MPD.Value) -> Widget n+libraryAlbumRow (yr,al) =+ let year = MPD.toText yr+ album = MPD.toText al+ yearW = withAttr dateAttr $ column (Just (Col 7)) Max Max $+ if T.null year+ then txt " "+ else txt "(" <+> txt (T.take 4 year) <+> txt ")"+ albumW = withAttr albumAttr $ column Nothing Max Max $ txt album+ in yearW <+> albumW++-- | Draw row in song column in Library view.+librarySongRow :: HumState -> MPD.Song -> Widget n librarySongRow st song = let pathsInQueue = (MPD.sgFilePath <$>) . (fst <$>) . listElements . queue $ st- in withAttr- (if MPD.sgFilePath song `elem` pathsInQueue- then queueTitleBoldAttr- else queueTitleAttr+ title = meta (MPD.toText . MPD.sgFilePath $ song) MPD.Title song+ titleW = withAttr titleAttr $ column Nothing Max Max $ txt title+ track = meta "-" MPD.Track song+ trackW = withAttr trackAttr $ column (Just (Col 3)) Max (Pad 1) $ txt track+ in (if MPD.sgFilePath song `elem` pathsInQueue+ then withAttr titleBoldAttr+ else id )- $ column Nothing (Pad 1) Max- $ txt (meta (MPD.toText . MPD.sgFilePath $ song) MPD.Title song)+ $ trackW <+> titleW +-- | Move focus right in Library view. libraryMoveRight :: FocLib -> FocLib libraryMoveRight FocArtists = FocAlbums libraryMoveRight _ = FocSongs +-- | Move focus left in Library view. libraryMoveLeft :: FocLib -> FocLib libraryMoveLeft FocSongs = FocAlbums libraryMoveLeft _ = FocArtists -drawViewLibrary :: HState -> Widget Name+-- | Draw Library view.+drawViewLibrary :: HumState -> Widget Name drawViewLibrary st = drawLibraryLeft st <+> drawLibraryMid st <+> drawLibraryRight st +-- | Move focused library column by given function libraryMove- :: (forall e . List Name e -> List Name e) -> HState -> EventM Name HState+ :: (forall e . List Name e -> List Name e) -- ^ Function to move the focused column with+ -> HumState+ -> EventM Name HumState libraryMove moveFunc s = let libfoc = s ^. focusL . focLibL in case libfoc of FocArtists -> rebuildLibArtists $ s & libraryL . artistsL %~ moveFunc- FocAlbums -> rebuildLibAlbums $ s & libraryL . albumsL %~ moveFunc+ FocAlbums -> rebuildLibAlbums $ s & libraryL . yalbumsL %~ moveFunc FocSongs -> do pure $ s & libraryL . songsL %~ moveFunc --libraryAddtoQ :: Bool -> HState -> EventM Name HState+-- | Add selected element in Library view to queue.+-- If the element is an album or artist add all songs under it.+libraryAddtoQ+ :: Bool -- ^ Play first item added to queue+ -> HumState+ -> EventM Name HumState libraryAddtoQ play s = let libfoc = s ^. focusL . focLibL in case libfoc of FocArtists -> do- songs <-- liftIO- (songsOfArtist- (snd <$> listSelectedElement (s ^. libraryL . artistsL))- )+ let martist = snd <$> listSelectedElement (s ^. libraryL . artistsL)+ songs <-liftIO $ maybe (pure empty) songsOfArtist martist songBulkAddtoQ play songs s FocAlbums -> do- songs <-- liftIO- (songsOfAlbum- (snd <$> listSelectedElement (s ^. libraryL . albumsL))- )+ let malbum = snd . snd <$> listSelectedElement (s ^. libraryL . yalbumsL)+ songs <- liftIO $ maybe (pure empty) songsOfAlbum malbum songBulkAddtoQ play songs s FocSongs -> do- let maybeFilePath = MPD.sgFilePath . snd <$> listSelectedElement+ let songs = maybe V.empty V.singleton $ snd <$> listSelectedElement (s ^. libraryL . songsL)- traverse_- (\sel -> liftIO- (withMPD $ MPD.addId sel Nothing >>= if play- then MPD.playId- else const pass- )- )- maybeFilePath- song <- liftIO (withMPD MPD.currentSong)- pure s { currentSong = fromRight Nothing song, queue = queue s }+ songBulkAddtoQ play songs s -librarySearch :: Bool -> HState -> EventM Name HState+-- | Search focused library column for next instance of last search.+librarySearch+ :: Bool -- ^ Search direction, True for forward.+ -> HumState+ -> EventM Name HumState librarySearch direction s = let libfoc = s ^. focusL . focLibL dir = if direction then id else listReverse@@ -166,30 +181,27 @@ then pure s else case libfoc of FocArtists -> do- extentMap <- updateExtentMap rebuildLibArtists- $ (s { extentMap })+ $ s & libraryL . artistsL %~ (dir . listFindBy (stringySearch searchkey) . dir) FocAlbums -> do- extentMap <- updateExtentMap rebuildLibAlbums- $ (s { extentMap })+ $ s & libraryL- . albumsL- %~ (dir . listFindBy (stringySearch searchkey) . dir)+ . yalbumsL+ %~ (dir . listFindBy (stringySearch searchkey . snd) . dir) FocSongs -> do- extentMap <- updateExtentMap pure- $ s { extentMap }+ $ s & libraryL . songsL %~ (dir . listFindBy (songSearch searchkey [MPD.Title]) . dir) -+-- | handle key inputs for Library view. handleEventLibrary- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleEventLibrary s e = case e of VtyEvent vtye -> case vtye of EvKey (KChar 'j') [] -> continue =<< libraryMove listMoveDown s@@ -204,11 +216,8 @@ continue =<< libraryMove listMoveDown =<< libraryAddtoQ True s EvKey (KChar ' ') [] -> continue =<< libraryMove listMoveDown =<< libraryAddtoQ False s- EvKey (KChar 'G') [] ->- continue =<< libraryMove (\ls -> listMoveBy (length ls) ls) s+ EvKey (KChar 'G') [] -> continue =<< libraryMove (listMoveTo (-1)) s EvKey (KChar 'g') [] -> continue =<< libraryMove (listMoveTo 0) s -- TODO change this to 'gg', somehow+ EvKey (KChar '`') [] -> continue =<< rebuildLibArtists (s & libraryL . yalbumSortL %~ not) _ -> continue s _ -> continue s--addToPl :: EventM Name HState-addToPl = error "not implemented"
src/Hum/Views/Playlists.hs view
@@ -1,10 +1,16 @@-{-#LANGUAGE RankNTypes#-}--- | +-- | Module : Hum.Views.Library+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Functions for the Playlist view.+ module Hum.Views.Playlists where import Hum.Types import Brick.Types+import Brick.Widgets.Edit hiding ( decodeUtf8 ) import Graphics.Vty.Input.Events import Brick.Main import Brick.Widgets.Core@@ -17,21 +23,19 @@ import Hum.Views.Common import Hum.Rebuild import qualified Data.Text as T+import qualified Data.Text.Zipper as Z import Network.MPD ( withMPD ) import qualified Network.MPD as MPD-import qualified Data.Map.Strict as Map import Hum.Utils ---drawPlaylistLeft :: HState -> Widget Name-drawPlaylistLeft st =- let vsize = case join $ Map.lookup PlaylistLeft $ extentMap st of- Just e -> snd . extentSize $ e- Nothing -> 20- in reportExtent PlaylistLeft $ hCenter- ( viewport PlaylistLeft Vertical- . visible+-- | Draw left column in Playlist view.+drawPlaylistLeft :: HumState -> Widget Name+drawPlaylistLeft st = reportExtent PlaylistLeft $+ Widget Greedy Greedy $ do+ ctx <- getContext+ let vsize = ctx ^. windowHeightL - 6 -- HACK Don't hardcode nowplaying size?+ render $ hCenter+ ( visible . vLimit vsize . center $ hBorder@@ -41,14 +45,15 @@ (MPD.toText <$> st ^. playlistsL . plListL) ) )-drawPlaylistRight :: HState -> Widget Name-drawPlaylistRight st =- let vsize = case join $ Map.lookup PlaylistRight $ extentMap st of- Just e -> snd . extentSize $ e- Nothing -> 20- in reportExtent PlaylistRight $ hCenter- ( viewport PlaylistRight Vertical- . visible++-- | Draw right column in Playlist view.+drawPlaylistRight :: HumState -> Widget Name+drawPlaylistRight st = reportExtent PlaylistRight $+ Widget Greedy Greedy $ do+ ctx <- getContext+ let vsize = ctx ^. windowHeightL - 6 -- HACK Don't hardcode nowplaying size?+ render $ hCenter+ ( visible . vLimit vsize . center $ (if st ^. editableL@@ -66,33 +71,34 @@ ) ) -playlistRow :: HState -> T.Text -> Widget n+-- | Draw row in playlist column in Playlist view.+playlistRow :: HumState -> T.Text -> Widget n -- TODO rename? playlistRow _ val =- withAttr queueAlbumAttr $ column Nothing (Pad 1) Max $ txt val+ withAttr albumAttr $ column Nothing (Pad 1) Max $ txt val -playlistSongRow :: HState -> (MPD.Song,Highlight) -> Widget n+-- | Draw row in song column in Playlist view.+playlistSongRow :: HumState -> (MPD.Song,Highlight) -> Widget n playlistSongRow st (song,hl) = let pathsInQueue = (MPD.sgFilePath <$>) . (fst <$>) . listElements . queue $ st- in (if hl then highlightOverQueueAttrs else id) . withAttr+ in (if hl then highlightOverAttrs else id) . withAttr (if MPD.sgFilePath song `elem` pathsInQueue- then queueTitleBoldAttr- else queueTitleAttr+ then titleBoldAttr+ else titleAttr ) $ column Nothing (Pad 1) Max $ txt (meta (MPD.toText . MPD.sgFilePath $ song) MPD.Title song) ------drawViewPlaylists :: HState -> Widget Name+-- | Draw Playlist view.+drawViewPlaylists :: HumState -> Widget Name drawViewPlaylists st = hLimitPercent 25 (drawPlaylistLeft st) <+> drawPlaylistRight st +-- | Move focused playlist column by given function playlistsMove- :: (forall e . List Name e -> List Name e) -> HState -> EventM Name HState+ :: (forall e . List Name e -> List Name e) -- ^ Function to move the focused column with+ -> HumState+ -> EventM Name HumState playlistsMove moveFunc s = let playfoc = s ^. focusL . focPlayL in case playfoc of@@ -100,7 +106,12 @@ FocPSongs -> do pure $ s & playlistsL . plSongsL %~ moveFunc -playlistsAddtoQ :: Bool -> HState -> EventM Name HState+-- | Add selected element in Playlist view to queue.+-- If the element is a playlist adds entire playlist.+playlistsAddtoQ+ :: Bool -- ^ Play first item added to queue+ -> HumState+ -> EventM Name HumState playlistsAddtoQ play s = let playfoc = s ^. focusL . focPlayL in case playfoc of@@ -120,7 +131,11 @@ song <- liftIO (withMPD MPD.currentSong) pure s { currentSong = fromRight Nothing song, queue = queue s } -playlistsSearch :: Bool -> HState -> EventM Name HState+-- | Search focused playlist column for next instance of last search.+playlistsSearch+ :: Bool -- ^ Search direction, True for forward.+ -> HumState+ -> EventM Name HumState playlistsSearch direction s = let playfoc = s ^. focusL . focPlayL dir = if direction then id else listReverse@@ -129,23 +144,33 @@ then pure s else case playfoc of FocPlaylists -> do- extentMap <- updateExtentMap rebuildPlList- $ (s { extentMap })+ $ s & playlistsL . plListL %~ (dir . listFindBy (stringySearch searchkey) . dir) FocPSongs -> do- extentMap <- updateExtentMap pure- $ s { extentMap }+ $ s & playlistsL . plSongsL %~ (dir . listFindBy (songSearch searchkey [MPD.Title] . fst) . dir) +-- | Rename the given playlist with input from the text prompt.+renamePl+ :: T.Text -> Bool -> HumState -> EventM n HumState+renamePl oldname bl st = if bl+ then+ let newname = fromString (toString $ st ^. promptsL . textPromptL . editContentsL & Z.currentLine)+ oldname' = fromString . T.unpack $ oldname+ in do+ _ <- liftIO . withMPD $MPD.rename oldname' newname+ rebuildPl st+ else pure st +-- | handle key inputs for Playlist view. handleEventPlaylists- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleEventPlaylists s e = case e of VtyEvent vtye -> case vtye of EvKey (KChar 'j') [] -> continue =<< playlistsMove listMoveDown s@@ -169,7 +194,7 @@ -> continue $ s & modeL .~ PromptMode & promptsL . currentPromptL .~ YNPrompt & promptsL . promptTitleL .~ ("DELETE " <> selectedPl <> "?\nYou can't paste it back yet")- & promptsL . exitPromptL .~ deleteSelectedPl+ & promptsL . exitPromptFuncL .~ deleteSelectedPl | otherwise -> continue s EvKey (KChar 'y') [] | s ^. editableL@@ -183,16 +208,21 @@ | s ^. focusL . focPlayL == FocPlaylists -> continue =<< pastePlaylist s | otherwise -> continue s- EvKey (KChar 'G') [] ->- continue =<< playlistsMove (\ls -> listMoveBy (length ls) ls) s+ EvKey (KChar 'G') [] -> continue =<< playlistsMove (listMoveTo (-1)) s EvKey (KChar 'g') [] -> continue =<< playlistsMove (listMoveTo 0) s -- TODO change this to 'gg', somehow- EvKey (KChar 'e') [] -> if s ^. editableL then- continue $ s & editableL %~ not- & modeL .~ PromptMode- & promptsL . currentPromptL .~ YNPrompt- & promptsL . promptTitleL .~ ("Save changes to " <> selectedPl <> "?")- & promptsL . exitPromptL .~ saveEditedPl- else continue =<< rebuildPlList (s & editableL %~ not)+ EvKey (KChar 'e') [] -> case s ^. focusL . focPlayL of+ FocPlaylists+ -> continue $ s & modeL .~ PromptMode+ & promptsL . currentPromptL .~ TextPrompt+ & promptsL . promptTitleL .~ ("Rename " <> selectedPl <> " to:")+ & promptsL . exitPromptFuncL .~ renamePl selectedPl+ FocPSongs -> if s ^. editableL then+ continue $ s & editableL %~ not+ & modeL .~ PromptMode+ & promptsL . currentPromptL .~ YNPrompt+ & promptsL . promptTitleL .~ ("Save changes to " <> selectedPl <> "?")+ & promptsL . exitPromptFuncL .~ saveEditedPl+ else continue =<< reloadPlList (s & editableL %~ not) _ -> continue s _ -> continue s where selectedPl = s ^. playlistsL . plListL & listSelectedElement ? maybe "<error>" snd ? MPD.toText
src/Hum/Views/Queue.hs view
@@ -1,5 +1,12 @@--- | +-- | Module : Hum.Views.Queue+-- Copyright : (c) Itai Y. Efrat 2020-2021+-- License : GPLv2-or-later (see LICENSE)+-- Maintainer : Itai Y. Efrat <itai3397@gmail.com>+--+-- Functions for the Queue view.++ module Hum.Views.Queue where import Hum.Types import Brick.Main@@ -14,18 +21,16 @@ import Hum.Views.Common import Network.MPD ( withMPD ) import qualified Network.MPD as MPD-import qualified Data.Map.Strict as Map import Control.Lens -drawViewQueue :: HState -> Widget Name-drawViewQueue st =- let vsize = case join $ Map.lookup Queue $ extentMap st of- Just e -> snd . extentSize $ e- Nothing -> 60- in reportExtent Queue $ hCenter- ( viewport Queue Vertical--- . visible- . vLimit vsize+-- | Draw the queue.+drawViewQueue :: HumState -> Widget Name+drawViewQueue st = reportExtent Queue $+ Widget Greedy Greedy $ do+ ctx <- getContext+ let vsize = ctx ^. windowHeightL - 6 -- HACK Don't hardcode nowplaying size?+ render $ hCenter+ ( vLimit vsize . center $ hCenter header <=> hCenter@@ -39,20 +44,21 @@ {-songIdx = column (Just (Col 4)) Max (Pad 1) $ txt "Inx" songId = column (Just (Col 3)) Max (Pad 1) $ txt "ID"-} album =- withAttr queueAlbumAttr $ column (Just (Per 25)) (Pad 1) Max $ txt "Album"- track = withAttr queueTrackAttr $ column (Just (Col 3)) Max (Pad 1) $ txt "#"- title = withAttr queueTitleAttr $ column Nothing Max Max $ txt "Title"+ withAttr albumAttr $ column (Just (Per 25)) (Pad 1) Max $ txt "Album"+ track = withAttr trackAttr $ column (Just (Col 3)) Max (Pad 1) $ txt "#"+ title = withAttr titleAttr $ column Nothing Max Max $ txt "Title" artist =- withAttr queueArtistAttr $ column (Just (Per 25)) Max (Pad 1) $ txt "Artist"+ withAttr artistAttr $ column (Just (Per 25)) Max (Pad 1) $ txt "Artist" time =- withAttr queueTimeAttr $ column (Just (Col 5)) Max (Pad 1) $ txt "Time"+ withAttr timeAttr $ column (Just (Col 5)) Max (Pad 1) $ txt "Time" header = withDefAttr headerAttr ({-songIdx <+> songId <+>-} album <+> track <+> title <+> artist <+> time) -queueRow :: HState -> (MPD.Song, Highlight) -> Widget n+-- | Draw individual row in queue.+queueRow :: HumState -> (MPD.Song, Highlight) -> Widget n queueRow st (song, hl) =- (if hl then highlightOverQueueAttrs else id)+ (if hl then highlightOverAttrs else id) . (if Just (MPD.sgIndex song) == (MPD.sgIndex <$> nowPlaying) then withDefAttr queueNowPlayingAttr else id@@ -71,45 +77,36 @@ $ maybe "?" (\(MPD.Id x) -> show x) $ MPD.sgId song-} album =- withAttr queueAlbumAttr $ column (Just (Per 25)) (Pad 1) Max $ txt $ meta+ withAttr albumAttr $ column (Just (Per 25)) (Pad 1) Max $ txt $ meta "<no album>" MPD.Album song track =- withAttr queueTrackAttr $ column (Just (Col 3)) Max (Pad 1) $ txt $ meta+ withAttr trackAttr $ column (Just (Col 3)) Max (Pad 1) $ txt $ meta "?" MPD.Track song- title = withAttr queueTitleAttr $ column Nothing Max Max $ txt $ meta+ title = withAttr titleAttr $ column Nothing Max Max $ txt $ meta "<no title>" MPD.Title song artist =- withAttr queueArtistAttr $ column (Just (Per 25)) Max (Pad 1) $ txt $ meta+ withAttr artistAttr $ column (Just (Per 25)) Max (Pad 1) $ txt $ meta "<no artist>" MPD.Artist song time =- withAttr queueTimeAttr+ withAttr timeAttr $ column (Just (Col 5)) Max (Pad 1) $ txt $ secondsToTime $ MPD.sgLength song -pasteDeleteCleanup :: HState -> SongList -> EventM Name HState-pasteDeleteCleanup s clSongs' = do- let mi = listSelected (queue s)- extentMap <- updateExtentMap- currentSong <- liftIO (fromRight Nothing <$> withMPD MPD.currentSong)- status <- liftIO (fromRight Nothing <$> (Just <<$>> withMPD MPD.status))- s' <- rebuildQueue s- pure (s' & queueL %~ maybe id listMoveTo mi- & clipboardL . clSongsL .~ clSongs') { currentSong- , status- , extentMap- }--queueSearch :: Bool -> HState -> EventM Name HState+-- | Search queue for next instance of last search.+queueSearch+ :: Bool -- ^ Search direction, True for forward.+ -> HumState+ -> EventM Name HumState queueSearch direction s = let dir = if direction then id else listReverse@@ -118,9 +115,8 @@ if searchkey == "" then pure s else do- extentMap <- updateExtentMap pure- $ s { extentMap }+ $ s & queueL %~ ( dir . listFindBy@@ -130,7 +126,11 @@ . dir ) -queueAddToPl :: HState -> String -> EventM Name HState+-- | Add highlighted songs to given playlist.+queueAddToPl+ :: HumState+ -> String -- ^ Playlist name+ -> EventM Name HumState queueAddToPl s plName = let songs = (s ^. queueL)@@ -139,50 +139,42 @@ <&> fst in songBulkAddtoPl plName songs s --+-- | handle key inputs for Queue view. handleEventQueue- :: HState -> BrickEvent Name HumEvent -> EventM Name (Next HState)+ :: HumState -> BrickEvent Name HumEvent -> EventM Name (Next HumState) handleEventQueue s e = case e of VtyEvent vtye -> case vtye of- EvKey (KChar 'j') [] -> do- extentMap <- updateExtentMap- continue s { queue = listMoveDown $ queue s, extentMap }- EvKey (KChar 'k') [] -> do- extentMap <- updateExtentMap- continue s { queue = listMoveUp $ queue s, extentMap }+ EvKey (KChar 'j') [] -> continue $ s & queueL %~ listMoveDown+ EvKey (KChar 'k') [] -> continue $ s & queueL %~ listMoveUp EvKey (KChar 'n') [] -> continue =<< queueSearch (s ^. exL . searchDirectionL) s EvKey (KChar 'N') [] -> continue =<< queueSearch (s ^. exL . searchDirectionL & not) s- EvKey (KChar 'a') [] -> continue $ s & modeL .~ PromptMode- & promptsL . currentPromptL .~ PlSelectPrompt- & promptsL . promptTitleL .~ "Add selected Item(s) to:"+ EvKey (KChar 'a') [] ->+ continue $ s & modeL .~ PromptMode+ & promptsL . plSelectPromptL .~ listInsert 0 Nothing (Just <$> (s ^. playlistsL . plListL))+ & promptsL . currentPromptL .~ PlSelectPrompt+ & promptsL . promptTitleL .~ "Add selected Item(s) to:" EvKey KEnter [] -> do let maybeSelectedId = MPD.sgId . fst . snd =<< listSelectedElement (queue s) traverse_ (\sel -> liftIO (withMPD $ MPD.playId sel)) maybeSelectedId- song <- liftIO (withMPD MPD.currentSong)- continue s { currentSong = fromRight Nothing song, queue = queue s }+ rebuildStatus s >>= continue EvKey (KChar ' ') [] -> continue $ s & queueL %~ (listMoveDown . listToggleHighlight) EvKey (KChar 'd') [] -> do- let clSongs' = getHighlighted (queue s)- _ <- liftIO (withMPD $ deleteHighlightedfromQ (queue s))- continue =<< pasteDeleteCleanup s clSongs'+ let clSongs' = s ^. queueL & getHighlighted+ _ <- liftIO (withMPD $ deleteHighlightedfromQ (s ^. queueL))+ let s' = s & clipboardL . clSongsL .~ clSongs'+ rebuildQueue s' >>= rebuildStatus >>= continue EvKey (KChar 'D') [] -> do- let clip = queue s- _ <- liftIO (withMPD $ deleteAll (queue s))- continue =<< pasteDeleteCleanup s clip- EvKey (KChar 'y') [] -> continue $ s & clipboardL . clSongsL .~ (s ^. queueL & getHighlighted)+ let clSongs' = s^. queueL+ let s' = s & clipboardL . clSongsL .~ clSongs'+ _ <- liftIO (withMPD MPD.clear)+ rebuildQueue s' >>= rebuildStatus >>= continue+ EvKey (KChar 'y') [] -> continue $ yankHighlighted s queueL EvKey (KChar 'p') [] -> do- let clip = s ^. clipboardL . clSongsL- _ <- liftIO (withMPD $ pasteSongstoQ clip (queue s))- continue =<< pasteDeleteCleanup s clip- EvKey (KChar 'G') [] -> do- extentMap <- updateExtentMap- continue s { queue = listMoveTo (length . queue $ s) $ queue s- , extentMap- }- EvKey (KChar 'g') [] -> do -- TODO change this to 'gg', somehow- extentMap <- updateExtentMap- continue s { queue = listMoveTo 0 $ queue s, extentMap }+ let clSongs' = s ^. clipboardL . clSongsL+ _ <- liftIO (withMPD $ pasteSongstoQ clSongs' (s ^. queueL))+ rebuildQueue s >>= rebuildStatus >>= continue+ EvKey (KChar 'G') [] -> continue $ s & queueL %~ listMoveTo (-1)+ EvKey (KChar 'g') [] -> continue $ s & queueL %~ listMoveTo 0 -- TODO change this to 'gg', somehow _ -> continue s _ -> continue s