packages feed

brick 0.41.5 → 0.42

raw patch · 5 files changed

+797/−4 lines, 5 filesdep +directorydep +filepathdep +unixdep ~basedep ~vtynew-component:exe:brick-file-browser-demoPVP ok

version bump matches the API change (PVP)

Dependencies added: directory, filepath, unix

Dependency ranges changed: base, vty

API changes (from Hackage documentation)

+ Brick.Widgets.FileBrowser: BlockDevice :: FileType
+ Brick.Widgets.FileBrowser: CharacterDevice :: FileType
+ Brick.Widgets.FileBrowser: Directory :: FileType
+ Brick.Widgets.FileBrowser: FileInfo :: String -> String -> FilePath -> Maybe FileType -> Int64 -> FileInfo
+ Brick.Widgets.FileBrowser: NamedPipe :: FileType
+ Brick.Widgets.FileBrowser: RegularFile :: FileType
+ Brick.Widgets.FileBrowser: SymbolicLink :: FileType
+ Brick.Widgets.FileBrowser: UnixSocket :: FileType
+ Brick.Widgets.FileBrowser: [fileInfoFilePath] :: FileInfo -> FilePath
+ Brick.Widgets.FileBrowser: [fileInfoFileSize] :: FileInfo -> Int64
+ Brick.Widgets.FileBrowser: [fileInfoFileType] :: FileInfo -> Maybe FileType
+ Brick.Widgets.FileBrowser: [fileInfoFilename] :: FileInfo -> String
+ Brick.Widgets.FileBrowser: [fileInfoSanitizedFilename] :: FileInfo -> String
+ Brick.Widgets.FileBrowser: data FileBrowser n
+ Brick.Widgets.FileBrowser: data FileInfo
+ Brick.Widgets.FileBrowser: data FileType
+ Brick.Widgets.FileBrowser: entriesForDirectory :: FilePath -> IO [FileInfo]
+ Brick.Widgets.FileBrowser: fileBrowserAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserBlockDeviceAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserCharacterDeviceAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserCurrentDirectoryAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserCursor :: FileBrowser n -> Maybe FileInfo
+ Brick.Widgets.FileBrowser: fileBrowserDirectoryAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserEntryFilterL :: forall n_a1pzw. Lens' (FileBrowser n_a1pzw) (Maybe (FileInfo -> Bool))
+ Brick.Widgets.FileBrowser: fileBrowserException :: FileBrowser n -> Maybe IOException
+ Brick.Widgets.FileBrowser: fileBrowserIsSearching :: FileBrowser n -> Bool
+ Brick.Widgets.FileBrowser: fileBrowserNamedPipeAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserRegularFileAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserSelectable :: FileBrowser n -> FileInfo -> Bool
+ Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1pzw. Lens' (FileBrowser n_a1pzw) (FileInfo -> Bool)
+ Brick.Widgets.FileBrowser: fileBrowserSelection :: FileBrowser n -> Maybe FileInfo
+ Brick.Widgets.FileBrowser: fileBrowserSelectionInfoAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserSymbolicLinkAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileBrowserUnixSocketAttr :: AttrName
+ Brick.Widgets.FileBrowser: fileExtensionMatch :: String -> FileInfo -> Bool
+ Brick.Widgets.FileBrowser: fileInfoFilePathL :: Lens' FileInfo FilePath
+ Brick.Widgets.FileBrowser: fileInfoFileSizeL :: Lens' FileInfo Int64
+ Brick.Widgets.FileBrowser: fileInfoFileTypeL :: Lens' FileInfo (Maybe FileType)
+ Brick.Widgets.FileBrowser: fileInfoFilenameL :: Lens' FileInfo String
+ Brick.Widgets.FileBrowser: fileInfoSanitizedFilenameL :: Lens' FileInfo String
+ Brick.Widgets.FileBrowser: fileTypeMatch :: [FileType] -> FileInfo -> Bool
+ Brick.Widgets.FileBrowser: getWorkingDirectory :: FileBrowser n -> FilePath
+ Brick.Widgets.FileBrowser: handleFileBrowserEvent :: Ord n => Event -> FileBrowser n -> EventM n (FileBrowser n)
+ Brick.Widgets.FileBrowser: instance GHC.Classes.Eq Brick.Widgets.FileBrowser.FileInfo
+ Brick.Widgets.FileBrowser: instance GHC.Classes.Eq Brick.Widgets.FileBrowser.FileType
+ Brick.Widgets.FileBrowser: instance GHC.Read.Read Brick.Widgets.FileBrowser.FileInfo
+ Brick.Widgets.FileBrowser: instance GHC.Read.Read Brick.Widgets.FileBrowser.FileType
+ Brick.Widgets.FileBrowser: instance GHC.Show.Show Brick.Widgets.FileBrowser.FileInfo
+ Brick.Widgets.FileBrowser: instance GHC.Show.Show Brick.Widgets.FileBrowser.FileType
+ Brick.Widgets.FileBrowser: newFileBrowser :: (FileInfo -> Bool) -> n -> Maybe FilePath -> IO (FileBrowser n)
+ Brick.Widgets.FileBrowser: prettyFileSize :: Int64 -> Text
+ Brick.Widgets.FileBrowser: renderFileBrowser :: (Show n, Ord n) => Bool -> FileBrowser n -> Widget n
+ Brick.Widgets.FileBrowser: selectDirectories :: FileInfo -> Bool
+ Brick.Widgets.FileBrowser: selectNonDirectories :: FileInfo -> Bool
+ Brick.Widgets.FileBrowser: setFileBrowserEntryFilter :: Maybe (FileInfo -> Bool) -> FileBrowser n -> FileBrowser n
+ Brick.Widgets.FileBrowser: setWorkingDirectory :: FilePath -> FileBrowser n -> IO (FileBrowser n)
+ Brick.Widgets.FileBrowser: updateFileBrowserSearch :: (Maybe Text -> Maybe Text) -> FileBrowser n -> FileBrowser n

Files

CHANGELOG.md view
@@ -2,6 +2,16 @@ Brick changelog --------------- +0.42+----++New features:+ * Added `Brick.Widgets.FileBrowser`, which provides a filesystem+   browser for selecting files and directories. Read the Haddock+   module documentation and see the included demo program,+   `programs/FileBrowserDemo.hs`, for information on using the new+   functionality.+ 0.41.5 ------ 
README.md view
@@ -105,12 +105,11 @@  * Simple dialog box widget  * Border-drawing widgets (put borders around or in between things)  * Generic scrollable viewports- * (And many more general-purpose layout control combinators)--Other killer features:+ * General-purpose layout control combinators  * Extensible widget-building API  * User-customizable attribute themes  * Type-safe, validated input form API (see the `Brick.Forms` module)+ * A filesystem browser for file and directory selection  * Borders can be configured to automatically connect!  Brick-Users Discussion
brick.cabal view
@@ -1,5 +1,5 @@ name:                brick-version:             0.41.5+version:             0.42 synopsis:            A declarative terminal user interface library description:   Write terminal applications painlessly with 'brick'! You write an@@ -76,6 +76,7 @@     Brick.Widgets.Core     Brick.Widgets.Dialog     Brick.Widgets.Edit+    Brick.Widgets.FileBrowser     Brick.Widgets.List     Brick.Widgets.ProgressBar     Data.IMap@@ -90,7 +91,9 @@                        vty >= 5.24,                        transformers,                        data-clist >= 0.1,+                       directory,                        dlist,+                       filepath,                        containers,                        microlens >= 0.3.0.0,                        microlens-th,@@ -103,6 +106,7 @@                        text-zipper >= 0.7.1,                        template-haskell,                        deepseq >= 1.3 && < 1.5,+                       unix,                        word-wrap >= 0.2   if impl(ghc < 8.0)     build-depends:     semigroups@@ -116,6 +120,19 @@   default-extensions:  CPP   main-is:             ReadmeDemo.hs   build-depends:       base,+                       brick,+                       text++executable brick-file-browser-demo+  if !flag(demos)+    Buildable: False+  hs-source-dirs:      programs+  ghc-options:         -threaded -Wall -fno-warn-unused-do-bind -O3+  default-language:    Haskell2010+  default-extensions:  CPP+  main-is:             FileBrowserDemo.hs+  build-depends:       base,+                       vty,                        brick,                        text 
+ programs/FileBrowserDemo.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+module Main where++import qualified Control.Exception as E+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Graphics.Vty as V++import qualified Data.Text as Text+import qualified Brick.Main as M+import qualified Brick.Widgets.List as L+import Brick.AttrMap (AttrName)+import Brick.Types+  ( Widget+  , BrickEvent(..)+  )+import Brick.Widgets.Center+  ( center+  , hCenter+  )+import Brick.Widgets.Border+  ( borderWithLabel+  )+import Brick.Widgets.Core+  ( vBox, (<=>), padTop+  , hLimit, vLimit, txt+  , withDefAttr, emptyWidget+  )+import Brick.Widgets.FileBrowser as FB+import qualified Brick.AttrMap as A+import Brick.Util (on, fg)+import qualified Brick.Types as T++data Name = FileBrowser1+          deriving (Eq, Show, Ord)++drawUI :: FileBrowser Name -> [Widget Name]+drawUI b = [center $ ui <=> help]+    where+        ui = hCenter $+             vLimit 15 $+             hLimit 50 $+             borderWithLabel (txt "Choose a file") $+             FB.renderFileBrowser True b+        help = padTop (T.Pad 1) $+               vBox [ case fileBrowserException b of+                          Nothing -> emptyWidget+                          Just e -> hCenter $ withDefAttr errorAttr $+                                    txt $ Text.pack $ E.displayException e+                    , hCenter $ txt "Up/Down: select"+                    , hCenter $ txt "/: search, Ctrl-C or Esc: cancel search"+                    , hCenter $ txt "Enter: change directory or select file"+                    , hCenter $ txt "Esc: quit"+                    ]++appEvent :: FB.FileBrowser Name -> BrickEvent Name e -> T.EventM Name (T.Next (FB.FileBrowser Name))+appEvent b (VtyEvent ev) =+    case ev of+        V.EvKey V.KEsc [] | not (fileBrowserIsSearching b) ->+            M.halt b+        _ -> do+            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'+appEvent b _ = M.continue b++errorAttr :: AttrName+errorAttr = "error"++theMap :: A.AttrMap+theMap = A.attrMap V.defAttr+    [ (L.listSelectedFocusedAttr, V.black `on` V.yellow)+    , (FB.fileBrowserCurrentDirectoryAttr, V.white `on` V.blue)+    , (FB.fileBrowserSelectionInfoAttr, V.white `on` V.blue)+    , (FB.fileBrowserDirectoryAttr, fg V.blue)+    , (FB.fileBrowserBlockDeviceAttr, fg V.magenta)+    , (FB.fileBrowserCharacterDeviceAttr, fg V.green)+    , (FB.fileBrowserNamedPipeAttr, fg V.yellow)+    , (FB.fileBrowserSymbolicLinkAttr, fg V.cyan)+    , (FB.fileBrowserUnixSocketAttr, fg V.red)+    , (errorAttr, fg V.red)+    ]++theApp :: M.App (FileBrowser Name) e Name+theApp =+    M.App { M.appDraw = drawUI+          , M.appChooseCursor = M.showFirstCursor+          , M.appHandleEvent = appEvent+          , M.appStartEvent = return+          , M.appAttrMap = const theMap+          }++main :: IO ()+main = do+    b <- M.defaultMain theApp =<< FB.newFileBrowser FB.selectNonDirectories FileBrowser1 Nothing+    putStrLn $ "Selected entry: " <> show (FB.fileBrowserSelection b)
+ src/Brick/Widgets/FileBrowser.hs view
@@ -0,0 +1,666 @@+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | 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+-- this module, see @programs/FileBrowserDemo.hs@.+--+-- To use this module:+--+-- * Embed a 'FileBrowser' in your application state.+-- * 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'.+-- * Inspect 'fileBrowserException' to determine whether the+--   file browser encountered an error when reading a directory in+--   'setWorkingDirectory' or when changing directories in the event+--   handler.+--+-- File browsers have a built-in user-configurable function to limit the+-- entries displayed that defaults to showing all files. For example,+-- an application might want to limit the browser to just directories+-- and XML files. That is accomplished by setting the filter with+-- 'setFileBrowserEntryFilter' and some examples are provided in this+-- module: 'fileTypeMatch' and 'fileExtensionMatch'.+--+-- File browsers are styled using the provided collection of attribute+-- names, so add those to your attribute map to get the appearance you+-- want. File browsers also make use of a 'List' internally, so the+-- 'List' attributes will affect how the list appears.+--+-- File browsers catch 'IOException's when changing directories. If a+-- call to 'setWorkingDirectory' triggers an 'IOException' while reading+-- the working directory, the resulting 'IOException' is stored in the+-- file browser and is accessible with 'fileBrowserException'. The+-- 'setWorkingDirectory' function clears the exception field if the+-- working directory is read successfully. The caller is responsible for+-- deciding when and whether to display the exception to the user. In+-- the event that an 'IOException' is raised as described here, the file+-- browser will always present @..@ as a navigation option to allow the+-- user to continue navigating up the directory tree. It does this even+-- if the current or parent directory does not exist or cannot be read,+-- so it is always safe to present a file browser for any working+-- directory. Bear in mind that the @..@ entry is always subjected to+-- filtering and searching.+module Brick.Widgets.FileBrowser+  ( -- * Types+    FileBrowser+  , FileInfo(..)+  , FileType(..)+  -- * Making a new file browser+  , newFileBrowser+  , selectNonDirectories+  , selectDirectories++  -- * Manipulating a file browser's state+  , setWorkingDirectory+  , getWorkingDirectory+  , updateFileBrowserSearch+  , setFileBrowserEntryFilter++  -- * Handling events+  , handleFileBrowserEvent++  -- * Rendering+  , renderFileBrowser++  -- * Getting information+  , fileBrowserCursor+  , fileBrowserIsSearching+  , fileBrowserSelection+  , fileBrowserException+  , fileBrowserSelectable++  -- * Attributes+  , fileBrowserAttr+  , fileBrowserCurrentDirectoryAttr+  , fileBrowserSelectionInfoAttr+  , fileBrowserDirectoryAttr+  , fileBrowserBlockDeviceAttr+  , fileBrowserRegularFileAttr+  , fileBrowserCharacterDeviceAttr+  , fileBrowserNamedPipeAttr+  , fileBrowserSymbolicLinkAttr+  , fileBrowserUnixSocketAttr++  -- * Example browser entry filters+  , fileTypeMatch+  , fileExtensionMatch++  -- * Lenses+  , fileBrowserEntryFilterL+  , fileBrowserSelectableL+  , fileInfoFilenameL+  , fileInfoFileSizeL+  , fileInfoSanitizedFilenameL+  , fileInfoFilePathL+  , fileInfoFileTypeL++  -- * Miscellaneous+  , prettyFileSize++  -- * Utilities+  , entriesForDirectory+  )+where++import qualified Control.Exception as E+import Control.Monad (forM)+import Control.Monad.IO.Class (liftIO)+import Data.Char (toLower, isPrint)+import Data.Maybe (fromMaybe, isJust)+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.Vector as V+import Lens.Micro+import qualified Graphics.Vty as Vty+import qualified System.Directory as D+import qualified System.Posix.Files as U+import qualified System.Posix.Types as U+import qualified System.FilePath as FP+import Text.Printf (printf)++import Brick.Types+import Brick.AttrMap (AttrName)+import Brick.Widgets.Core+import Brick.Widgets.List++-- | A file browser's state. Embed this in your application state and+-- transform it with 'handleFileBrowserEvent' and the functions included+-- in this module.+data FileBrowser n =+    FileBrowser { fileBrowserWorkingDirectory :: FilePath+                , fileBrowserEntries :: List n FileInfo+                , fileBrowserLatestResults :: [FileInfo]+                , fileBrowserName :: n+                , fileBrowserSelectionState :: Maybe FileInfo+                , fileBrowserEntryFilter :: Maybe (FileInfo -> Bool)+                , fileBrowserSearchString :: Maybe T.Text+                , fileBrowserException :: Maybe E.IOException+                -- ^ The exception status of the latest directory+                -- change. If 'Nothing', the latest directory change+                -- was successful and all entries were read. Otherwise,+                -- this contains the exception raised by the latest+                -- directory change in case the calling application+                -- 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+                -- anything else; directory changes can only occur if+                -- this returns 'False' for directories.+                --+                -- Note that this is a record field so it can be used to+                -- change the selection function.+                }++-- | Information about a file entry in the browser.+data FileInfo =+    FileInfo { fileInfoFilename :: String+             -- ^ The filename of this entry, without its path.+             -- This is not for display purposes; for that, use+             -- 'fileInfoSanitizedFilename'.+             , fileInfoSanitizedFilename :: String+             -- ^ The filename of this entry with out its path,+             -- sanitized of non-printable characters (replaced with+             -- '?'). 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.+             }+             deriving (Show, Eq, Read)++-- | The type of file entries in the browser.+data FileType =+    RegularFile+    -- ^ A regular disk file.+    | BlockDevice+    -- ^ A block device.+    | CharacterDevice+    -- ^ A character device.+    | NamedPipe+    -- ^ A named pipe.+    | Directory+    -- ^ A directory.+    | SymbolicLink+    -- ^ A symbolic link.+    | UnixSocket+    -- ^ A Unix socket.+    deriving (Read, Show, Eq)++suffixLenses ''FileBrowser+suffixLenses ''FileInfo++-- | Make a new file browser state. The provided resource name will be+-- used to render the 'List' viewport of the browser.+--+-- By default, the browser will show all files and directories+-- in its working directory. To change that behavior, see+-- '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.+               -> n+               -- ^ The resource name associated with the browser's+               -- entry listing.+               -> Maybe FilePath+               -- ^ The initial working directory that the browser+               -- displays. If not provided, this defaults to the+               -- executable's current working directory.+               -> IO (FileBrowser n)+newFileBrowser selPredicate name mCwd = do+    initialCwd <- case mCwd of+        Just path -> return path+        Nothing -> D.getCurrentDirectory++    let b = FileBrowser { fileBrowserWorkingDirectory = initialCwd+                        , fileBrowserEntries = list name mempty 1+                        , fileBrowserLatestResults = mempty+                        , fileBrowserName = name+                        , fileBrowserSelectionState = Nothing+                        , fileBrowserEntryFilter = Nothing+                        , fileBrowserSearchString = Nothing+                        , fileBrowserException = Nothing+                        , fileBrowserSelectable = selPredicate+                        }++    setWorkingDirectory initialCwd b++-- | A file entry selector that permits selection of all file entries+-- except directories. Use this if you want users to be able to navigate+-- directories in the browser. If you want users to be able to select+-- only directories, use 'selectDirectories'.+selectNonDirectories :: FileInfo -> Bool+selectNonDirectories i =+    case fileInfoFileType i of+        Just Directory -> False+        _ -> True+++-- | A file entry selector that permits selection of directories+-- only. This prevents directory navigation and only supports directory+-- selection.+selectDirectories :: FileInfo -> Bool+selectDirectories i =+    case fileInfoFileType i of+        Just Directory -> True+        _ -> False++-- | Set the filtering function used to determine which entries in+-- the browser's current directory appear in the browser. 'Nothing'+-- indicates no filtering, meaning all entries will be shown. 'Just'+-- indicates a function that should return 'True' for entries that+-- should be permitted to appear.+setFileBrowserEntryFilter :: Maybe (FileInfo -> Bool) -> FileBrowser n -> FileBrowser n+setFileBrowserEntryFilter f b =+    applyFilterAndSearch $ b & fileBrowserEntryFilterL .~ f++-- | Set the working directory of the file browser. This scans the new+-- directory and repopulates the browser while maintaining any active+-- search string and/or entry filtering.+--+-- If the directory scan raises an 'IOException', the exception is+-- stored the browser and is accessible with 'fileBrowserException'. If+-- no exception is raised, the exception field is cleared. Regardless of+-- whether an exception is raised, @..@ is always presented as a valid+-- option in the browser.+setWorkingDirectory :: FilePath -> FileBrowser n -> IO (FileBrowser n)+setWorkingDirectory path b = do+    entriesResult <- E.try $ entriesForDirectory path++    let (entries, exc) = case entriesResult of+            Left (e::E.IOException) -> ([], Just e)+            Right es -> (es, Nothing)++    allEntries <- if path == "/" then return entries else do+        parentResult <- E.try $ parentOf path+        return $ case parentResult of+            Left (_::E.IOException) -> entries+            Right parent -> parent : entries++    let b' = setEntries allEntries b+    return $ b' & fileBrowserWorkingDirectoryL .~ path+                & fileBrowserExceptionL .~ exc++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 = ".."+                    , fileInfoFilePath = filePath+                    , fileInfoSanitizedFilename = ".."+                    , fileInfoFileType = fileTypeFromStatus status+                    , fileInfoFileSize = sz+                    }++-- | Get the working directory of the file browser.+getWorkingDirectory :: FileBrowser n -> FilePath+getWorkingDirectory = fileBrowserWorkingDirectory++setEntries :: [FileInfo] -> FileBrowser n -> FileBrowser n+setEntries es b =+    applyFilterAndSearch $ b & fileBrowserLatestResultsL .~ es++-- | Returns whether the file browser is in search mode, i.e., the mode+-- in which user input affects the browser's active search string and+-- displayed entries. This is used to aid in event dispatching in the+-- calling program.+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++-- | Modify the file browser's active search string. This causes the+-- browser's displayed entries to change to those in its current+-- directory that match the search string, if any. If a search string+-- is provided, it is matched case-insensitively anywhere in file or+-- directory names.+updateFileBrowserSearch :: (Maybe T.Text -> Maybe T.Text)+                        -- ^ The search transformation. 'Nothing'+                        -- indicates that search mode should be off;+                        -- 'Just' indicates that it should be on and+                        -- that the provided search string should be+                        -- used.+                        -> FileBrowser n+                        -- ^ The browser to modify.+                        -> FileBrowser n+updateFileBrowserSearch f b =+    let old = b^.fileBrowserSearchStringL+        new = f $ b^.fileBrowserSearchStringL+        oldLen = maybe 0 T.length old+        newLen = maybe 0 T.length new+    in if old == new+       then b+       else if oldLen == newLen+            -- This case avoids a list rebuild and cursor position reset+            -- when the search state isn't *really* changing.+            then b & fileBrowserSearchStringL .~ new+            else applyFilterAndSearch $ b & fileBrowserSearchStringL .~ new++applyFilterAndSearch :: FileBrowser n -> FileBrowser n+applyFilterAndSearch b =+    let filterMatch = fromMaybe (const True) (b^.fileBrowserEntryFilterL)+        searchMatch = maybe (const True)+                            (\search i -> (T.toLower search `T.isInfixOf` (T.pack $ toLower <$> fileInfoSanitizedFilename i)))+                            (b^.fileBrowserSearchStringL)+        match i = filterMatch i && searchMatch i+        matching = filter match $ b^.fileBrowserLatestResultsL+    in b { fileBrowserEntries = list (b^.fileBrowserNameL) (V.fromList matching) 1 }++-- | Generate a textual abbreviation of a file size, e.g. "10.2M" or "12+-- bytes".+prettyFileSize :: Int64+               -- ^ A file size in bytes.+               -> T.Text+prettyFileSize i+    | i >= 2 ^ (40::Int64) = T.pack $ format (i `divBy` (2 ** 40)) <> "T"+    | i >= 2 ^ (30::Int64) = T.pack $ format (i `divBy` (2 ** 30)) <> "G"+    | i >= 2 ^ (20::Int64) = T.pack $ format (i `divBy` (2 ** 20)) <> "M"+    | i >= 2 ^ (10::Int64) = T.pack $ format (i `divBy` (2 ** 10)) <> "K"+    | otherwise    = T.pack $ show i <> " bytes"+    where+        format = printf "%0.1f"+        divBy :: Int64 -> Double -> Double+        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.+--+-- The entries returned are all entries in the specified directory+-- except for @.@ and @..@. Directories are always given first. Entries+-- are sorted in case-insensitive lexicographic order.+--+-- This function is exported for those who want to implement their own+-- file browser using the types in this module.+entriesForDirectory :: FilePath -> IO [FileInfo]+entriesForDirectory rawPath = do+    path <- D.makeAbsolute rawPath++    -- Get all entries except "." and "..", then sort them+    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+                        }++    let dirsFirst a b = if fileInfoFileType a == Just Directory &&+                           fileInfoFileType b == Just Directory+                        then compare (toLower <$> fileInfoFilename a)+                                     (toLower <$> fileInfoFilename b)+                        else if fileInfoFileType a == Just Directory &&+                                fileInfoFileType b /= Just Directory+                             then LT+                             else if fileInfoFileType b == Just Directory &&+                                     fileInfoFileType a /= Just Directory+                                  then GT+                                  else compare (toLower <$> fileInfoFilename a)+                                               (toLower <$> fileInfoFilename b)++        allEntries = sortBy dirsFirst infos++    return allEntries++fileTypeFromStatus :: U.FileStatus -> Maybe FileType+fileTypeFromStatus s =+    if | U.isBlockDevice s     -> Just BlockDevice+       | U.isCharacterDevice s -> Just CharacterDevice+       | U.isNamedPipe s       -> Just NamedPipe+       | U.isRegularFile s     -> Just RegularFile+       | U.isDirectory s       -> Just Directory+       | U.isSocket s          -> Just UnixSocket+       | U.isSymbolicLink s    -> Just SymbolicLink+       | otherwise             -> Nothing++-- | Get the file information for the file under the cursor, if any.+fileBrowserCursor :: FileBrowser n -> Maybe FileInfo+fileBrowserCursor b = snd <$> listSelectedElement (b^.fileBrowserEntriesL)++-- | Handle a Vty input event. Note that event handling can+-- cause a directory change so the caller should be aware that+-- 'fileBrowserException' may need to be checked after handling an+-- event in case an exception was triggered while scanning the working+-- directory.+--+-- Events handled regardless of mode:+--+-- * @Enter@: set the file browser's selected entry+--   ('fileBrowserSelection') for use by the calling application,+--   subject to 'fileBrowserSelectable'.+-- * @Ctrl-n@: select the next entry+-- * @Ctrl-p@: select the previous entry+-- * 'List' navigation keys+--+-- Events handled only in normal mode:+--+-- * @/@: enter search mode+--+-- Events handled only in search mode:+--+-- * @Esc@, @Ctrl-C@: cancel search mode+-- * Text input: update search string+handleFileBrowserEvent :: (Ord n) => Vty.Event -> FileBrowser n -> EventM n (FileBrowser n)+handleFileBrowserEvent e b =+    if fileBrowserIsSearching b+    then handleFileBrowserEventSearching e b+    else handleFileBrowserEventNormal e b++safeInit :: T.Text -> T.Text+safeInit t | T.length t == 0 = t+           | otherwise = T.init t++handleFileBrowserEventSearching :: (Ord n) => Vty.Event -> FileBrowser n -> EventM n (FileBrowser n)+handleFileBrowserEventSearching e b =+    case e of+        Vty.EvKey (Vty.KChar 'c') [Vty.MCtrl] ->+            return $ updateFileBrowserSearch (const Nothing) b+        Vty.EvKey Vty.KEsc [] ->+            return $ updateFileBrowserSearch (const Nothing) b+        Vty.EvKey Vty.KBS [] ->+            return $ updateFileBrowserSearch (fmap safeInit) b+        Vty.EvKey Vty.KEnter [] ->+            maybeSelectCurrentEntry b+        Vty.EvKey (Vty.KChar c) [] ->+            return $ updateFileBrowserSearch (fmap (flip T.snoc c)) b+        _ ->+            handleFileBrowserEventCommon e b++handleFileBrowserEventNormal :: (Ord n) => Vty.Event -> FileBrowser n -> EventM n (FileBrowser n)+handleFileBrowserEventNormal e b =+    case e of+        Vty.EvKey (Vty.KChar '/') [] ->+            -- Begin file search+            return $ updateFileBrowserSearch (const $ Just "") b+        Vty.EvKey Vty.KEnter [] ->+            -- Select file or enter directory+            maybeSelectCurrentEntry b+        _ ->+            handleFileBrowserEventCommon e b++handleFileBrowserEventCommon :: (Ord n) => Vty.Event -> FileBrowser n -> EventM n (FileBrowser n)+handleFileBrowserEventCommon e b =+    case e of+        Vty.EvKey (Vty.KChar 'n') [Vty.MCtrl] ->+            return $ b & fileBrowserEntriesL %~ listMoveBy 1+        Vty.EvKey (Vty.KChar 'p') [Vty.MCtrl] ->+            return $ b & fileBrowserEntriesL %~ listMoveBy (-1)+        _ ->+            handleEventLensed b fileBrowserEntriesL handleListEvent e++maybeSelectCurrentEntry :: FileBrowser n -> EventM n (FileBrowser n)+maybeSelectCurrentEntry b =+    case fileBrowserCursor b of+        Nothing -> return b+        Just entry ->+            if fileBrowserSelectable b entry+            then return $ b & fileBrowserSelectionStateL .~ Just entry+            else case fileInfoFileType entry of+                Just Directory -> liftIO $ setWorkingDirectory (fileInfoFilePath entry) b+                _ -> return b++-- | 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+-- information about the selected entry.+--+-- Note that if the most recent file browser operation produced an+-- exception in 'fileBrowserException', that exception is not rendered+-- by this function. That exception needs to be rendered (if at all) by+-- the calling application.+--+-- The file browser is greedy in both dimensions.+renderFileBrowser :: (Show n, Ord n)+                  => Bool+                  -- ^ Whether the file browser has input focus.+                  -> FileBrowser n+                  -- ^ The browser to render.+                  -> Widget n+renderFileBrowser foc b =+    let maxFilenameLength = maximum $ (length . fileInfoFilename) <$> (b^.fileBrowserEntriesL)+        cwdHeader = padRight Max $+                    str $ sanitizeFilename $ fileBrowserWorkingDirectory b+        selInfo = case listSelectedElement (b^.fileBrowserEntriesL) of+            Nothing -> vLimit 1 $ fill ' '+            Just (_, i) -> padRight Max $ selInfoFor i+        fileTypeLabel Nothing = "unknown"+        fileTypeLabel (Just t) =+            case t of+                RegularFile -> "file"+                BlockDevice -> "block device"+                CharacterDevice -> "character device"+                NamedPipe -> "pipe"+                Directory -> "directory"+                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+        maybeSearchInfo = case b^.fileBrowserSearchStringL of+            Nothing -> emptyWidget+            Just s -> padRight Max $+                      txt "Search: " <+>+                      showCursor (b^.fileBrowserNameL) (Location (T.length s, 0)) (txt s)++    in withDefAttr fileBrowserAttr $+       vBox [ withDefAttr fileBrowserCurrentDirectoryAttr cwdHeader+            , renderList (renderFileInfo foc maxFilenameLength) foc (b^.fileBrowserEntriesL)+            , maybeSearchInfo+            , withDefAttr fileBrowserSelectionInfoAttr selInfo+            ]++renderFileInfo :: Bool -> Int -> Bool -> FileInfo -> Widget n+renderFileInfo foc maxLen sel info =+    (if foc+     then (if sel then forceAttr listSelectedFocusedAttr else id)+     else (if sel then forceAttr listSelectedAttr else id)) $+    padRight Max body+    where+        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 ""++-- | Sanitize a filename for terminal display, replacing non-printable+-- characters with '?'.+sanitizeFilename :: String -> String+sanitizeFilename = fmap toPrint+    where+        toPrint c | isPrint c = c+                  | otherwise = '?'++attrForFileType :: FileType -> AttrName+attrForFileType RegularFile = fileBrowserRegularFileAttr+attrForFileType BlockDevice = fileBrowserBlockDeviceAttr+attrForFileType CharacterDevice = fileBrowserCharacterDeviceAttr+attrForFileType NamedPipe = fileBrowserNamedPipeAttr+attrForFileType Directory = fileBrowserDirectoryAttr+attrForFileType SymbolicLink = fileBrowserSymbolicLinkAttr+attrForFileType UnixSocket = fileBrowserUnixSocketAttr++-- | The base attribute for all file browser attributes.+fileBrowserAttr :: AttrName+fileBrowserAttr = "fileBrowser"++-- | The attribute used for the current directory displayed at the top+-- of the browser.+fileBrowserCurrentDirectoryAttr :: AttrName+fileBrowserCurrentDirectoryAttr = fileBrowserAttr <> "currentDirectory"++-- | The attribute used for the entry information displayed at the+-- bottom of the browser.+fileBrowserSelectionInfoAttr :: AttrName+fileBrowserSelectionInfoAttr = fileBrowserAttr <> "selectionInfo"++-- | The attribute used to render directory entries.+fileBrowserDirectoryAttr :: AttrName+fileBrowserDirectoryAttr = fileBrowserAttr <> "directory"++-- | The attribute used to render block device entries.+fileBrowserBlockDeviceAttr :: AttrName+fileBrowserBlockDeviceAttr = fileBrowserAttr <> "block"++-- | The attribute used to render regular file entries.+fileBrowserRegularFileAttr :: AttrName+fileBrowserRegularFileAttr = fileBrowserAttr <> "regular"++-- | The attribute used to render character device entries.+fileBrowserCharacterDeviceAttr :: AttrName+fileBrowserCharacterDeviceAttr = fileBrowserAttr <> "char"++-- | The attribute used to render named pipe entries.+fileBrowserNamedPipeAttr :: AttrName+fileBrowserNamedPipeAttr = fileBrowserAttr <> "pipe"++-- | The attribute used to render symbolic link entries.+fileBrowserSymbolicLinkAttr :: AttrName+fileBrowserSymbolicLinkAttr = fileBrowserAttr <> "symlink"++-- | The attribute used to render Unix socket entries.+fileBrowserUnixSocketAttr :: AttrName+fileBrowserUnixSocketAttr = fileBrowserAttr <> "unixSocket"++-- | A file type filter for use with 'setFileBrowserEntryFilter'. This+-- filter permits entries whose file types are in the specified list.+fileTypeMatch :: [FileType] -> FileInfo -> Bool+fileTypeMatch tys i = maybe False (`elem` tys) $ fileInfoFileType i++-- | A filter that matches any directory regardless of name, or any+-- regular file with the specified extension. For example, an extension+-- argument of @"xml"@ would match regular files @test.xml@ and+-- @TEST.XML@ and it will match directories regardless of name.+fileExtensionMatch :: String -> FileInfo -> Bool+fileExtensionMatch ext i =+    ('.' : (toLower <$> ext)) `isSuffixOf` (toLower <$> fileInfoFilename i)