aboutsummaryrefslogtreecommitdiff
path: root/lib/Daffm/View.hs
blob: fb72ea6d1c42284fe9820fba7510a10310eb4857 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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}) =
  hBox
    [ hFixed 2 fileSelectionView,
      hFixed 1 $ fileTypeView fileType,
      hFixed 10 $ fileModeView fileMode,
      hFixed (maxOwnerSize + 1) $ fileOwnerView fileInfo,
      hFixed 7 $ fileSizeView fileSize,
      fileNameView sel fileInfo,
      searchMatchIndicatorView
    ]
  where
    maxOwnerSize = Vec.maximum . Vec.map (Text.length . ownerInfo) . L.listElements $ stateFiles appState
    ownerInfo (FileInfo {fileUser = user, fileGroup = group}) = user <> ":" <> group
    fileSizeView = txt . prettyFileSize . fromIntegral
    fileTypeView = withAttr fileTypeAttr . txt . showFileType
    fileModeView = withAttr fileModeAttr . txt . showFileMode
    fileOwnerView = withAttr fileOwnerAttr . txt . ownerInfo
    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 "<none>"
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