module Daffm.View where import Brick.Types (Context (availWidth), Size (Fixed), Widget (Widget, render), getContext) import Brick.Widgets.Core (Padding (Max, Pad), TextWidth (textWidth), emptyWidget, hBox, hLimit, padLeft, padRight, str, txt, vBox, vLimit, withAttr, (<+>)) import Brick.Widgets.Edit (renderEditor) import qualified Brick.Widgets.List as L import Daffm.Attrs import Daffm.Keymap (showKeySequence) import Daffm.Types (AppState (..), FileInfo (..), FileType (..), FocusTarget (..)) import Data.Int (Int64) import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.Vector as Vec import System.Posix.Types (FileMode) import qualified System.PosixCompat as Posix import Text.Printf (printf) appView :: AppState -> [Widget FocusTarget] appView appState = [ vBox [ headerView appState, fileListView appState, messageView appState, vLimit 1 $ cmdlineView appState ] ] hFixed :: Int -> Widget n -> Widget n hFixed w = hLimit w . padRight Max headerView :: AppState -> Widget n headerView (AppState {stateCwd}) = vLimit 1 $ Widget Fixed Fixed $ do c <- getContext let width = availWidth c - 2 -- with padding render . toWidget . trunc width $ stateCwd where toWidget = padLeft (Pad 1) . padRight (Pad 1) . txt trunc width text | width < textWidth text = "…" <> Text.takeEnd (width - 1) text | otherwise = Text.takeEnd width text fileListView :: AppState -> Widget FocusTarget fileListView appState@(AppState {stateFiles}) = L.renderListWithIndex (fileItemView appState) True stateFiles fileItemView :: AppState -> Int -> Bool -> FileInfo -> Widget FocusTarget fileItemView appState index sel fileInfo@(FileInfo {filePath, fileSize, fileType, fileMode, fileUser, fileGroup}) = hBox [ hFixed 2 fileSelectionView, hFixed 1 $ fileTypeView fileType, hFixed 10 $ fileModeView fileMode, hFixed 16 $ fileOwnerView fileUser fileGroup, hFixed 7 $ fileSizeView fileSize, fileNameView sel fileInfo, searchMatchIndicatorView ] where fileSizeView = txt . prettyFileSize . fromIntegral fileTypeView = withAttr fileTypeAttr . txt . showFileType fileModeView = withAttr fileModeAttr . txt . showFileMode fileOwnerView user group = withAttr fileOwnerAttr . txt $ user <> ":" <> group fileSelectionView = txt $ if Set.member filePath $ stateFileSelections appState then ">" else " " searchMatchIndicatorView | index `Vec.elem` stateSearchMatches appState = padLeft (Pad 1) $ withAttr searchMarchAttr $ txt "*" | otherwise = emptyWidget showFileType :: FileType -> Text.Text showFileType Directory = "d" showFileType SymbolicLink = "l" showFileType UnixSocket = "s" showFileType NamedPipe = "p" showFileType CharacterDevice = "c" showFileType BlockDevice = "b" showFileType RegularFile = "-" showFileType UnknownFileType = "?" showFileMode :: FileMode -> Text.Text showFileMode mode = permchars where perm m c = if Posix.intersectFileModes mode m == m then c else '-' permchars = Text.pack [ perm Posix.ownerReadMode 'r', perm Posix.ownerWriteMode 'w', perm Posix.ownerExecuteMode 'x', perm Posix.groupReadMode 'r', perm Posix.groupWriteMode 'w', perm Posix.groupExecuteMode 'x', perm Posix.otherReadMode 'r', perm Posix.otherWriteMode 'w', perm Posix.otherExecuteMode 'x' ] fileNameView :: Bool -> FileInfo -> Widget FocusTarget fileNameView True (FileInfo {fileName, fileType = Directory}) = withAttr directorySelectedAttr $ txt $ fileName <> "/" fileNameView False (FileInfo {fileName, fileType = Directory}) = withAttr directoryAttr $ txt $ fileName <> "/" fileNameView _ file@(FileInfo {fileType = SymbolicLink}) = symbolicLinkNameView file fileNameView True (FileInfo {fileName}) = withAttr fileSelectedAttr $ txt fileName fileNameView False (FileInfo {fileName}) = withAttr fileAttr $ txt fileName symbolicLinkNameView :: FileInfo -> Widget n symbolicLinkNameView (FileInfo {fileName, fileLinkTarget, fileLinkType = Just Directory}) = withAttr directoryLinkAttr (txt $ fileName <> "/") <+> txt " -> " <+> symTargetView (Just Directory) fileLinkTarget symbolicLinkNameView (FileInfo {fileName, fileLinkType, fileLinkTarget}) = withAttr linkAttr (txt fileName) <+> txt " -> " <+> symTargetView fileLinkType fileLinkTarget symTargetView :: Maybe FileType -> Maybe Text.Text -> Widget n symTargetView _ Nothing = withAttr invalidLinkAttr $ txt "" symTargetView Nothing (Just target) = withAttr invalidLinkAttr $ txt target symTargetView _ (Just target) = txt target messageView :: AppState -> Widget FocusTarget messageView (AppState {stateMessage = Just message}) = txt message messageView _ = emptyWidget cmdlineView :: AppState -> Widget FocusTarget cmdlineView (AppState {stateFocusTarget = FocusCmdline, stateCmdlineEditor}) = txt ":" <+> editor where editor = renderEditor (txt . Text.unlines) True stateCmdlineEditor cmdlineView (AppState {stateFocusTarget = FocusMain, stateFiles, stateFileSelections, stateKeySequence}) = hBox [ txt ":", rightAligned [keysView, padLeft (Pad 2) selectionsCountView, padLeft (Pad 2) posIndicatorView] ] where keysView = txt $ showKeySequence stateKeySequence rightAligned = padLeft Max . padRight (Pad 1) . hBox posIndicatorView = str $ cur <> "/" <> total selectionsCountView = if Set.null stateFileSelections then emptyWidget else str $ show $ Set.size stateFileSelections cur = case L.listSelected stateFiles of Nothing -> "-" Just n -> show (n + 1) total = show $ Vec.length $ L.listElements stateFiles prettyFileSize :: Int64 -> Text.Text prettyFileSize i | i >= 2 ^ (40 :: Int64) = format (i `divBy` (2 ** 40)) <> "T" | i >= 2 ^ (30 :: Int64) = format (i `divBy` (2 ** 30)) <> "G" | i >= 2 ^ (20 :: Int64) = format (i `divBy` (2 ** 20)) <> "M" | i >= 2 ^ (10 :: Int64) = format (i `divBy` (2 ** 10)) <> "K" | otherwise = Text.pack $ show i where format = Text.pack . printf "%0.1f" divBy :: Int64 -> Double -> Double divBy a b = (fromIntegral a :: Double) / b