brick 0.42.1 → 0.43
raw patch · 6 files changed
+170/−71 lines, 6 filesdep ~directoryPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: directory
API changes (from Hackage documentation)
- Brick.Widgets.FileBrowser: [fileInfoFileSize] :: FileInfo -> Int64
- Brick.Widgets.FileBrowser: [fileInfoFileType] :: FileInfo -> Maybe FileType
- Brick.Widgets.FileBrowser: fileInfoFileSizeL :: Lens' FileInfo Int64
- Brick.Widgets.FileBrowser: fileInfoFileTypeL :: Lens' FileInfo (Maybe FileType)
- Brick.Widgets.FileBrowser: instance GHC.Read.Read Brick.Widgets.FileBrowser.FileInfo
+ Brick.Widgets.FileBrowser: FileStatus :: Int64 -> Maybe FileType -> FileStatus
+ Brick.Widgets.FileBrowser: [fileInfoFileStatus] :: FileInfo -> Either IOException FileStatus
+ Brick.Widgets.FileBrowser: [fileStatusFileType] :: FileStatus -> Maybe FileType
+ Brick.Widgets.FileBrowser: [fileStatusSize] :: FileStatus -> Int64
+ Brick.Widgets.FileBrowser: data FileStatus
+ Brick.Widgets.FileBrowser: fileBrowserSelectedAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileInfoFileStatusL :: Lens' FileInfo (Either IOException FileStatus)
+ Brick.Widgets.FileBrowser: fileInfoFileType :: FileInfo -> Maybe FileType
+ Brick.Widgets.FileBrowser: fileStatusFileTypeL :: Lens' FileStatus (Maybe FileType)
+ Brick.Widgets.FileBrowser: fileStatusSizeL :: Lens' FileStatus Int64
+ Brick.Widgets.FileBrowser: getFileInfo :: String -> FilePath -> IO FileInfo
+ Brick.Widgets.FileBrowser: instance GHC.Classes.Eq Brick.Widgets.FileBrowser.FileStatus
+ Brick.Widgets.FileBrowser: instance GHC.Show.Show Brick.Widgets.FileBrowser.FileStatus
- Brick.Widgets.FileBrowser: FileInfo :: String -> String -> FilePath -> Maybe FileType -> Int64 -> FileInfo
+ Brick.Widgets.FileBrowser: FileInfo :: String -> String -> FilePath -> Either IOException FileStatus -> FileInfo
- Brick.Widgets.FileBrowser: fileBrowserEntryFilterL :: forall n_a1pzw. Lens' (FileBrowser n_a1pzw) (Maybe (FileInfo -> Bool))
+ Brick.Widgets.FileBrowser: fileBrowserEntryFilterL :: forall n_a1pzz. Lens' (FileBrowser n_a1pzz) (Maybe (FileInfo -> Bool))
- Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1pzw. Lens' (FileBrowser n_a1pzw) (FileInfo -> Bool)
+ Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1pzz. Lens' (FileBrowser n_a1pzz) (FileInfo -> Bool)
- Brick.Widgets.FileBrowser: fileBrowserSelection :: FileBrowser n -> Maybe FileInfo
+ Brick.Widgets.FileBrowser: fileBrowserSelection :: FileBrowser n -> [FileInfo]
Files
- CHANGELOG.md +24/−0
- brick.cabal +2/−2
- docs/guide.rst +1/−1
- programs/FileBrowserDemo.hs +7/−3
- src/Brick/Widgets/FileBrowser.hs +131/−63
- tests/Main.hs +5/−2
CHANGELOG.md view
@@ -2,6 +2,30 @@ Brick changelog --------------- +0.43+----++API changes:+ * The FileBrowser module got the ability to select multiple files+ (#204). This means that the `fileBrowserSelection` function now+ returns a list of `FileInfo` rather than at most one via `Maybe`.+ The module also now uses a new attribute, `fileBrowserSelectedAttr`,+ to indicate entries that are currently selected (in addition to+ displaying an asterisk after their filenames). Lastly, the file+ size and type fields of `FileInfo` have been replaced with a+ new type, `FileStatus`, and `FileInfo` now carries an `Either+ IOException FileStatus`. As part of that safety improvement,+ `setWorkingDirectory` now no longer clobbers the entire entry listing+ if any of the listings fail to stat. In addition, the FileBrowser now+ uses the correct file stat routines to deal with symbolic links.++Package changes:+ * Added lower bound on `directory` (thanks Fraser Tweedale)++Test suite changes:+ * Test suite now propagates success/failure to exit status (thanks+ Fraser Tweedale)+ 0.42.1 ------
brick.cabal view
@@ -1,5 +1,5 @@ name: brick-version: 0.42.1+version: 0.43 synopsis: A declarative terminal user interface library description: Write terminal applications painlessly with 'brick'! You write an@@ -91,7 +91,7 @@ vty >= 5.24, transformers, data-clist >= 0.1,- directory,+ directory >= 1.2.5.0, dlist, filepath, containers,
docs/guide.rst view
@@ -333,7 +333,7 @@ .. code:: haskell myEvent :: s -> BrickEvent n CounterEvent -> EventM n (Next s)- myEvent s (AppEvent (CounterEvent i)) = ...+ myEvent s (AppEvent (Counter i)) = ... The next step is to actually *generate* our custom events and inject them into the ``brick`` event stream so they make it to the
programs/FileBrowserDemo.hs view
@@ -64,9 +64,12 @@ b' <- FB.handleFileBrowserEvent ev b -- If the browser has a selected file after handling the -- event (because the user pressed Enter), shut down.- case fileBrowserSelection b' of- Nothing -> M.continue b'- Just _ -> M.halt b'+ case ev of+ V.EvKey V.KEnter [] ->+ case fileBrowserSelection b' of+ [] -> M.continue b'+ _ -> M.halt b'+ _ -> M.continue b' appEvent b _ = M.continue b errorAttr :: AttrName@@ -83,6 +86,7 @@ , (FB.fileBrowserNamedPipeAttr, fg V.yellow) , (FB.fileBrowserSymbolicLinkAttr, fg V.cyan) , (FB.fileBrowserUnixSocketAttr, fg V.red)+ , (FB.fileBrowserSelectedAttr, V.white `on` V.magenta) , (errorAttr, fg V.red) ]
src/Brick/Widgets/FileBrowser.hs view
@@ -2,9 +2,10 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-} -- | This module provids a file browser widget that allows users to -- navigate directory trees, search for files and directories, and--- select a file of interest. For a complete working demonstration of+-- select entries of interest. For a complete working demonstration of -- this module, see @programs/FileBrowserDemo.hs@. -- -- To use this module:@@ -13,8 +14,8 @@ -- * Dispatch events to it in your event handler with -- 'handleFileBrowserEvent'. -- * Get the entry under the browser's cursor with 'fileBrowserCursor'--- and get the entry selected by the user with 'Enter' using--- 'fileBrowserSelection'.+-- and get the entries selected by the user with 'Enter' or 'Space'+-- using 'fileBrowserSelection'. -- * Inspect 'fileBrowserException' to determine whether the -- file browser encountered an error when reading a directory in -- 'setWorkingDirectory' or when changing directories in the event@@ -50,6 +51,7 @@ ( -- * Types FileBrowser , FileInfo(..)+ , FileStatus(..) , FileType(..) -- * Making a new file browser , newFileBrowser@@ -74,11 +76,13 @@ , fileBrowserSelection , fileBrowserException , fileBrowserSelectable+ , fileInfoFileType -- * Attributes , fileBrowserAttr , fileBrowserCurrentDirectoryAttr , fileBrowserSelectionInfoAttr+ , fileBrowserSelectedAttr , fileBrowserDirectoryAttr , fileBrowserBlockDeviceAttr , fileBrowserRegularFileAttr@@ -95,16 +99,18 @@ , fileBrowserEntryFilterL , fileBrowserSelectableL , fileInfoFilenameL- , fileInfoFileSizeL , fileInfoSanitizedFilenameL , fileInfoFilePathL- , fileInfoFileTypeL+ , fileInfoFileStatusL+ , fileStatusSizeL+ , fileStatusFileTypeL -- * Miscellaneous , prettyFileSize -- * Utilities , entriesForDirectory+ , getFileInfo ) where @@ -112,13 +118,15 @@ import Control.Monad (forM) import Control.Monad.IO.Class (liftIO) import Data.Char (toLower, isPrint)-import Data.Maybe (fromMaybe, isJust)+import Data.Maybe (fromMaybe, isJust, fromJust)+import qualified Data.Foldable as F import qualified Data.Text as T #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid #endif import Data.Int (Int64) import Data.List (sortBy, isSuffixOf)+import qualified Data.Set as Set import qualified Data.Vector as V import Lens.Micro import qualified Graphics.Vty as Vty@@ -140,8 +148,8 @@ FileBrowser { fileBrowserWorkingDirectory :: FilePath , fileBrowserEntries :: List n FileInfo , fileBrowserLatestResults :: [FileInfo]+ , fileBrowserSelectedFiles :: Set.Set String , fileBrowserName :: n- , fileBrowserSelectionState :: Maybe FileInfo , fileBrowserEntryFilter :: Maybe (FileInfo -> Bool) , fileBrowserSearchString :: Maybe T.Text , fileBrowserException :: Maybe E.IOException@@ -153,9 +161,9 @@ -- needs to inspect or present the error to the user. , fileBrowserSelectable :: FileInfo -> Bool -- ^ The function that determines what kinds of entries- -- are selectable with @Enter@ in the event handler.- -- Note that if this returns 'True' for an entry, an- -- @Enter@ keypress selects that entry rather than doing+ -- are selectable with in the event handler. Note that+ -- if this returns 'True' for an entry, an @Enter@ or+ -- @Space@ keypress selects that entry rather than doing -- anything else; directory changes can only occur if -- this returns 'False' for directories. --@@ -163,6 +171,16 @@ -- change the selection function. } +-- | File status information.+data FileStatus =+ FileStatus { fileStatusSize :: Int64+ -- ^ The size, in bytes, of this entry's file.+ , fileStatusFileType :: Maybe FileType+ -- ^ The type of this entry's file, if it could be+ -- determined.+ }+ deriving (Show, Eq)+ -- | Information about a file entry in the browser. data FileInfo = FileInfo { fileInfoFilename :: String@@ -175,13 +193,12 @@ -- '?'). This is for display purposes only. , fileInfoFilePath :: FilePath -- ^ The full path to this entry's file.- , fileInfoFileType :: Maybe FileType- -- ^ The type of this entry's file, if it could be- -- determined.- , fileInfoFileSize :: Int64- -- ^ The size, in bytes, of this entry's file.+ , fileInfoFileStatus :: Either E.IOException FileStatus+ -- ^ The file status if it could be obtained, or the+ -- exception that was caught when attempting to read the+ -- file's status. }- deriving (Show, Eq, Read)+ deriving (Show, Eq) -- | The type of file entries in the browser. data FileType =@@ -203,6 +220,7 @@ suffixLenses ''FileBrowser suffixLenses ''FileInfo+suffixLenses ''FileStatus -- | Make a new file browser state. The provided resource name will be -- used to render the 'List' viewport of the browser.@@ -212,11 +230,10 @@ -- 'setFileBrowserEntryFilter'. newFileBrowser :: (FileInfo -> Bool) -- ^ The function used to determine what kinds of entries- -- can be selected with @Enter@ (see- -- 'handleFileBrowserEvent'). A good default is- -- 'selectNonDirectories'. This can be changed at- -- 'any time with 'fileBrowserSelectable' or its- -- 'corresponding lens.+ -- can be selected (see 'handleFileBrowserEvent'). A+ -- good default is 'selectNonDirectories'. This can be+ -- changed at 'any time with 'fileBrowserSelectable' or+ -- its 'corresponding lens. -> n -- ^ The resource name associated with the browser's -- entry listing.@@ -233,8 +250,8 @@ let b = FileBrowser { fileBrowserWorkingDirectory = initialCwd , fileBrowserEntries = list name mempty 1 , fileBrowserLatestResults = mempty+ , fileBrowserSelectedFiles = mempty , fileBrowserName = name- , fileBrowserSelectionState = Nothing , fileBrowserEntryFilter = Nothing , fileBrowserSearchString = Nothing , fileBrowserException = Nothing@@ -253,7 +270,6 @@ Just Directory -> False _ -> True - -- | A file entry selector that permits selection of directories -- only. This prevents directory navigation and only supports directory -- selection.@@ -298,19 +314,52 @@ let b' = setEntries allEntries b return $ b' & fileBrowserWorkingDirectoryL .~ path & fileBrowserExceptionL .~ exc+ & fileBrowserSelectedFilesL .~ mempty parentOf :: FilePath -> IO FileInfo-parentOf path = do- filePath <- D.canonicalizePath $ FP.takeDirectory path- status <- U.getFileStatus filePath- let U.COff sz = U.fileSize status- return FileInfo { fileInfoFilename = ".."+parentOf path = getFileInfo ".." $ FP.takeDirectory path++-- | Build a 'FileInfo' for the specified file and path. If an+-- 'IOException' is raised while attempting to get the file information,+-- the 'fileInfoFileStatus' field is populated with the exception.+-- Otherwise it is populated with the 'FileStatus' for the file.+getFileInfo :: String+ -- ^ The name of the file to inspect. This filename is only+ -- used to set the 'fileInfoFilename' and sanitized filename+ -- fields; the actual file to be inspected is referred+ -- to by the second argument. This is decomposed so that+ -- 'FileInfo's can be used to represent information about+ -- entries like @..@, whose display names differ from their+ -- physical paths.+ -> FilePath+ -- ^ The actual full path to the file or directory to+ -- inspect.+ -> IO FileInfo+getFileInfo name fullPath = do+ filePath <- D.makeAbsolute fullPath+ statusResult <- E.try $ U.getSymbolicLinkStatus filePath++ let stat = do+ status <- statusResult+ let U.COff sz = U.fileSize status+ return FileStatus { fileStatusFileType = fileTypeFromStatus status+ , fileStatusSize = sz+ }++ return FileInfo { fileInfoFilename = name , fileInfoFilePath = filePath- , fileInfoSanitizedFilename = ".."- , fileInfoFileType = fileTypeFromStatus status- , fileInfoFileSize = sz+ , fileInfoSanitizedFilename = sanitizeFilename name+ , fileInfoFileStatus = stat } +-- | Get the file type for this file info entry. If the file type could+-- not be obtained due to an 'IOException', return 'Nothing'.+fileInfoFileType :: FileInfo -> Maybe FileType+fileInfoFileType i =+ case fileInfoFileStatus i of+ Left _ -> Nothing+ Right stat -> fileStatusFileType stat+ -- | Get the working directory of the file browser. getWorkingDirectory :: FileBrowser n -> FilePath getWorkingDirectory = fileBrowserWorkingDirectory@@ -326,11 +375,13 @@ fileBrowserIsSearching :: FileBrowser n -> Bool fileBrowserIsSearching b = isJust $ b^.fileBrowserSearchStringL --- | Get the entry chosen by the user, if any. The entry is chosen--- by an 'Enter' keypress; if you want the entry under the cursor, use--- 'fileBrowserCursor'.-fileBrowserSelection :: FileBrowser n -> Maybe FileInfo-fileBrowserSelection = fileBrowserSelectionState+-- | Get the entries chosen by the user, if any. Entries are chosen by+-- an 'Enter' or 'Space' keypress; if you want the entry under the+-- cursor, use 'fileBrowserCursor'.+fileBrowserSelection :: FileBrowser n -> [FileInfo]+fileBrowserSelection b =+ let getEntry filename = fromJust $ F.find ((== filename) . fileInfoFilename) $ b^.fileBrowserLatestResultsL+ in fmap getEntry $ F.toList $ b^.fileBrowserSelectedFilesL -- | Modify the file browser's active search string. This causes the -- browser's displayed entries to change to those in its current@@ -386,8 +437,11 @@ divBy a b = ((fromIntegral a) :: Double) / b -- | Build a list of file info entries for the specified directory. This--- does not catch any exceptions, so the caller is responsible for--- handling them.+-- function does not catch any exceptions raised by calling+-- 'makeAbsolute' or 'listDirectory', but it does catch exceptions on+-- a per-file basis. Any exceptions caught when inspecting individual+-- files are stored in the 'fileInfoFileStatus' field of each+-- 'FileInfo'. -- -- The entries returned are all entries in the specified directory -- except for @.@ and @..@. Directories are always given first. Entries@@ -403,15 +457,7 @@ dirContents <- D.listDirectory path infos <- forM dirContents $ \f -> do- filePath <- D.canonicalizePath $ path FP.</> f- status <- U.getFileStatus filePath- let U.COff sz = U.fileSize status- return FileInfo { fileInfoFilename = f- , fileInfoFilePath = filePath- , fileInfoSanitizedFilename = sanitizeFilename f- , fileInfoFileType = fileTypeFromStatus status- , fileInfoFileSize = sz- }+ getFileInfo f (path FP.</> f) let dirsFirst a b = if fileInfoFileType a == Just Directory && fileInfoFileType b == Just Directory@@ -453,7 +499,7 @@ -- -- Events handled regardless of mode: ----- * @Enter@: set the file browser's selected entry+-- * @Enter@, @Space@: set the file browser's selected entry -- ('fileBrowserSelection') for use by the calling application, -- subject to 'fileBrowserSelectable'. -- * @Ctrl-n@: select the next entry@@ -504,6 +550,9 @@ Vty.EvKey Vty.KEnter [] -> -- Select file or enter directory maybeSelectCurrentEntry b+ Vty.EvKey (Vty.KChar ' ') [] ->+ -- Select entry+ selectCurrentEntry b _ -> handleFileBrowserEventCommon e b @@ -523,11 +572,17 @@ Nothing -> return b Just entry -> if fileBrowserSelectable b entry- then return $ b & fileBrowserSelectionStateL .~ Just entry+ then return $ b & fileBrowserSelectedFilesL %~ Set.insert (fileInfoFilename entry) else case fileInfoFileType entry of Just Directory -> liftIO $ setWorkingDirectory (fileInfoFilePath entry) b _ -> return b +selectCurrentEntry :: FileBrowser n -> EventM n (FileBrowser n)+selectCurrentEntry b =+ case fileBrowserCursor b of+ Nothing -> return b+ Just e -> return $ b & fileBrowserSelectedFilesL %~ Set.insert (fileInfoFilename e)+ -- | Render a file browser. This renders a list of entries in the -- working directory, a cursor to select from among the entries, a -- header displaying the working directory, and a footer displaying@@ -563,11 +618,15 @@ SymbolicLink -> "symbolic link" UnixSocket -> "socket" selInfoFor i =- let maybeSize = if fileInfoFileType i == Just RegularFile- then ", " <> prettyFileSize (fileInfoFileSize i)- else ""- in txt $ (T.pack $ fileInfoSanitizedFilename i) <> ": " <>- fileTypeLabel (fileInfoFileType i) <> maybeSize+ let label = case fileInfoFileStatus i of+ Left _ -> "unknown"+ Right stat ->+ let maybeSize = if fileStatusFileType stat == Just RegularFile+ then ", " <> prettyFileSize (fileStatusSize stat)+ else ""+ in fileTypeLabel (fileStatusFileType stat) <> maybeSize+ in txt $ (T.pack $ fileInfoSanitizedFilename i) <> ": " <> label+ maybeSearchInfo = case b^.fileBrowserSearchStringL of Nothing -> emptyWidget Just s -> padRight Max $@@ -576,23 +635,28 @@ in withDefAttr fileBrowserAttr $ vBox [ withDefAttr fileBrowserCurrentDirectoryAttr cwdHeader- , renderList (renderFileInfo foc maxFilenameLength) foc (b^.fileBrowserEntriesL)+ , renderList (renderFileInfo foc maxFilenameLength (b^.fileBrowserSelectedFilesL))+ foc (b^.fileBrowserEntriesL) , maybeSearchInfo , withDefAttr fileBrowserSelectionInfoAttr selInfo ] -renderFileInfo :: Bool -> Int -> Bool -> FileInfo -> Widget n-renderFileInfo foc maxLen sel info =+renderFileInfo :: Bool -> Int -> Set.Set String -> Bool -> FileInfo -> Widget n+renderFileInfo foc maxLen selFiles listSel info = (if foc- then (if sel then forceAttr listSelectedFocusedAttr else id)- else (if sel then forceAttr listSelectedAttr else id)) $+ then (if listSel then forceAttr listSelectedFocusedAttr+ else if sel then forceAttr fileBrowserSelectedAttr else id)+ else (if listSel then forceAttr listSelectedAttr+ else if sel then forceAttr fileBrowserSelectedAttr else id)) $ padRight Max body where+ sel = fileInfoFilename info `Set.member` selFiles addAttr = maybe id (withDefAttr . attrForFileType) (fileInfoFileType info)- body = addAttr (hLimit (maxLen + 1) $ padRight Max $ str $ fileInfoSanitizedFilename info <> suffix)- suffix = if fileInfoFileType info == Just Directory- then "/"- else ""+ body = addAttr (hLimit (maxLen + 1) $+ padRight Max $+ str $ fileInfoSanitizedFilename info <> suffix)+ suffix = (if fileInfoFileType info == Just Directory then "/" else "") <>+ (if sel then "*" else "") -- | Sanitize a filename for terminal display, replacing non-printable -- characters with '?'.@@ -652,6 +716,10 @@ -- | The attribute used to render Unix socket entries. fileBrowserUnixSocketAttr :: AttrName fileBrowserUnixSocketAttr = fileBrowserAttr <> "unixSocket"++-- | The attribute used for selected entries in the file browser.+fileBrowserSelectedAttr :: AttrName+fileBrowserSelectedAttr = fileBrowserAttr <> "selected" -- | A file type filter for use with 'setFileBrowserEntryFilter'. This -- filter permits entries whose file types are in the specified list.
tests/Main.hs view
@@ -2,6 +2,9 @@ {-# LANGUAGE TypeFamilies #-} import Control.Applicative+import Data.Bool (bool)+import System.Exit (exitFailure, exitSuccess)+ import Data.IMap (IMap, Run(Run)) import Data.IntMap (IntMap) import Test.QuickCheck@@ -103,5 +106,5 @@ return [] -main :: IO Bool-main = $quickCheckAll+main :: IO ()+main = $quickCheckAll >>= bool exitFailure exitSuccess