{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Brick.Widgets.FileBrowser
  ( 
    FileBrowser
  , FileInfo(..)
  , FileStatus(..)
  , FileType(..)
  
  , newFileBrowser
  , selectNonDirectories
  , selectDirectories
  
  , setWorkingDirectory
  , getWorkingDirectory
  , updateFileBrowserSearch
  , setFileBrowserEntryFilter
  
  , handleFileBrowserEvent
  
  , renderFileBrowser
  
  , fileBrowserCursor
  , fileBrowserIsSearching
  , fileBrowserSelection
  , fileBrowserException
  , fileBrowserSelectable
  , fileInfoFileType
  
  , fileBrowserAttr
  , fileBrowserCurrentDirectoryAttr
  , fileBrowserSelectionInfoAttr
  , fileBrowserSelectedAttr
  , fileBrowserDirectoryAttr
  , fileBrowserBlockDeviceAttr
  , fileBrowserRegularFileAttr
  , fileBrowserCharacterDeviceAttr
  , fileBrowserNamedPipeAttr
  , fileBrowserSymbolicLinkAttr
  , fileBrowserUnixSocketAttr
  
  , fileTypeMatch
  , fileExtensionMatch
  
  , fileBrowserEntryFilterL
  , fileBrowserSelectableL
  , fileInfoFilenameL
  , fileInfoSanitizedFilenameL
  , fileInfoFilePathL
  , fileInfoFileStatusL
  , fileStatusSizeL
  , fileStatusFileTypeL
  
  , prettyFileSize
  
  , entriesForDirectory
  , getFileInfo
  )
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, 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
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
data FileBrowser n =
    FileBrowser { fileBrowserWorkingDirectory :: FilePath
                , fileBrowserEntries :: List n FileInfo
                , fileBrowserLatestResults :: [FileInfo]
                , fileBrowserSelectedFiles :: Set.Set String
                , fileBrowserName :: n
                , fileBrowserEntryFilter :: Maybe (FileInfo -> Bool)
                , fileBrowserSearchString :: Maybe T.Text
                , fileBrowserException :: Maybe E.IOException
                
                
                
                
                
                
                , fileBrowserSelectable :: FileInfo -> Bool
                
                
                
                
                
                
                
                
                
                }
data FileStatus =
    FileStatus { fileStatusSize :: Int64
               
               , fileStatusFileType :: Maybe FileType
               
               
               }
               deriving (Show, Eq)
data FileInfo =
    FileInfo { fileInfoFilename :: String
             
             
             
             , fileInfoSanitizedFilename :: String
             
             
             
             , fileInfoFilePath :: FilePath
             
             , fileInfoFileStatus :: Either E.IOException FileStatus
             
             
             
             }
             deriving (Show, Eq)
data FileType =
    RegularFile
    
    | BlockDevice
    
    | CharacterDevice
    
    | NamedPipe
    
    | Directory
    
    | SymbolicLink
    
    | UnixSocket
    
    deriving (Read, Show, Eq)
suffixLenses ''FileBrowser
suffixLenses ''FileInfo
suffixLenses ''FileStatus
newFileBrowser :: (FileInfo -> Bool)
               
               
               
               
               
               -> n
               
               
               -> Maybe FilePath
               
               
               
               -> 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
                        , fileBrowserSelectedFiles = mempty
                        , fileBrowserName = name
                        , fileBrowserEntryFilter = Nothing
                        , fileBrowserSearchString = Nothing
                        , fileBrowserException = Nothing
                        , fileBrowserSelectable = selPredicate
                        }
    setWorkingDirectory initialCwd b
selectNonDirectories :: FileInfo -> Bool
selectNonDirectories i =
    case fileInfoFileType i of
        Just Directory -> False
        _ -> True
selectDirectories :: FileInfo -> Bool
selectDirectories i =
    case fileInfoFileType i of
        Just Directory -> True
        _ -> False
setFileBrowserEntryFilter :: Maybe (FileInfo -> Bool) -> FileBrowser n -> FileBrowser n
setFileBrowserEntryFilter f b =
    applyFilterAndSearch $ b & fileBrowserEntryFilterL .~ f
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
                & fileBrowserSelectedFilesL .~ mempty
parentOf :: FilePath -> IO FileInfo
parentOf path = getFileInfo ".." $ FP.takeDirectory path
getFileInfo :: String
            
            
            
            
            
            
            
            -> FilePath
            
            
            -> 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 = sanitizeFilename name
                    , fileInfoFileStatus = stat
                    }
fileInfoFileType :: FileInfo -> Maybe FileType
fileInfoFileType i =
    case fileInfoFileStatus i of
        Left _ -> Nothing
        Right stat -> fileStatusFileType stat
getWorkingDirectory :: FileBrowser n -> FilePath
getWorkingDirectory = fileBrowserWorkingDirectory
setEntries :: [FileInfo] -> FileBrowser n -> FileBrowser n
setEntries es b =
    applyFilterAndSearch $ b & fileBrowserLatestResultsL .~ es
fileBrowserIsSearching :: FileBrowser n -> Bool
fileBrowserIsSearching b = isJust $ b^.fileBrowserSearchStringL
fileBrowserSelection :: FileBrowser n -> [FileInfo]
fileBrowserSelection b =
    let getEntry filename = fromJust $ F.find ((== filename) . fileInfoFilename) $ b^.fileBrowserLatestResultsL
    in fmap getEntry $ F.toList $ b^.fileBrowserSelectedFilesL
updateFileBrowserSearch :: (Maybe T.Text -> Maybe T.Text)
                        
                        
                        
                        
                        
                        -> FileBrowser n
                        
                        -> 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
            
            
            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 }
prettyFileSize :: Int64
               
               -> 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
entriesForDirectory :: FilePath -> IO [FileInfo]
entriesForDirectory rawPath = do
    path <- D.makeAbsolute rawPath
    
    dirContents <- D.listDirectory path
    infos <- forM dirContents $ \f -> do
        getFileInfo f (path FP.</> f)
    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
fileBrowserCursor :: FileBrowser n -> Maybe FileInfo
fileBrowserCursor b = snd <$> listSelectedElement (b^.fileBrowserEntriesL)
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 [] ->
            updateFileBrowserSearch (const Nothing) <$>
                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 '/') [] ->
            
            return $ updateFileBrowserSearch (const $ Just "") b
        Vty.EvKey Vty.KEnter [] ->
            
            maybeSelectCurrentEntry b
        Vty.EvKey (Vty.KChar ' ') [] ->
            
            selectCurrentEntry 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 & 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)
renderFileBrowser :: (Show n, Ord n)
                  => Bool
                  
                  -> FileBrowser n
                  
                  -> 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 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 $
                      txt "Search: " <+>
                      showCursor (b^.fileBrowserNameL) (Location (T.length s, 0)) (txt s)
    in withDefAttr fileBrowserAttr $
       vBox [ withDefAttr fileBrowserCurrentDirectoryAttr cwdHeader
            , renderList (renderFileInfo foc maxFilenameLength (b^.fileBrowserSelectedFilesL))
                         foc (b^.fileBrowserEntriesL)
            , maybeSearchInfo
            , withDefAttr fileBrowserSelectionInfoAttr selInfo
            ]
renderFileInfo :: Bool -> Int -> Set.Set String -> Bool -> FileInfo -> Widget n
renderFileInfo foc maxLen selFiles listSel info =
    (if foc
     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 "") <>
                 (if sel then "*" else "")
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
fileBrowserAttr :: AttrName
fileBrowserAttr = "fileBrowser"
fileBrowserCurrentDirectoryAttr :: AttrName
fileBrowserCurrentDirectoryAttr = fileBrowserAttr <> "currentDirectory"
fileBrowserSelectionInfoAttr :: AttrName
fileBrowserSelectionInfoAttr = fileBrowserAttr <> "selectionInfo"
fileBrowserDirectoryAttr :: AttrName
fileBrowserDirectoryAttr = fileBrowserAttr <> "directory"
fileBrowserBlockDeviceAttr :: AttrName
fileBrowserBlockDeviceAttr = fileBrowserAttr <> "block"
fileBrowserRegularFileAttr :: AttrName
fileBrowserRegularFileAttr = fileBrowserAttr <> "regular"
fileBrowserCharacterDeviceAttr :: AttrName
fileBrowserCharacterDeviceAttr = fileBrowserAttr <> "char"
fileBrowserNamedPipeAttr :: AttrName
fileBrowserNamedPipeAttr = fileBrowserAttr <> "pipe"
fileBrowserSymbolicLinkAttr :: AttrName
fileBrowserSymbolicLinkAttr = fileBrowserAttr <> "symlink"
fileBrowserUnixSocketAttr :: AttrName
fileBrowserUnixSocketAttr = fileBrowserAttr <> "unixSocket"
fileBrowserSelectedAttr :: AttrName
fileBrowserSelectedAttr = fileBrowserAttr <> "selected"
fileTypeMatch :: [FileType] -> FileInfo -> Bool
fileTypeMatch tys i = maybe False (`elem` tys) $ fileInfoFileType i
fileExtensionMatch :: String -> FileInfo -> Bool
fileExtensionMatch ext i =
    ('.' : (toLower <$> ext)) `isSuffixOf` (toLower <$> fileInfoFilename i)