packages feed

nix-tree-0.8.0: app/NixTree/BrickApp.hs

module NixTree.BrickApp (run, helpText) where

import qualified Brick as B
import qualified Brick.BChan as B
import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.Center as B
import qualified Brick.Widgets.List as B
import Control.Concurrent
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import qualified Data.Sequence as S
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Graphics.Vty as V
import Lens.Micro (Traversal', _1, _Just, (^.))
import qualified NixTree.Clipboard as Clipboard
import NixTree.Data.InvertedIndex
import NixTree.PathStats
import qualified System.Clock as Clock
import qualified System.HrfSize as HRF

sortOrderChangeHighlightPeriod :: Clock.TimeSpec
sortOrderChangeHighlightPeriod = Clock.TimeSpec 0 (500 * 1_000_000)

data Event
  = EventTick Clock.TimeSpec

data Widgets
  = WidgetPrevPane
  | WidgetCurrPane
  | WidgetNextPane
  | WidgetWhyDepends
  | WidgetSearch
  | WidgetWhyDependsViewport
  deriving (Show, Eq, Ord)

data Notice = Notice Text Text

data Modal s
  = ModalNotice Notice
  | ModalWhyDepends (B.GenericList Widgets Seq (NonEmpty Path))
  | ModalSearch Text Text (B.GenericList Widgets Seq Path)

succCycle :: forall a. (Bounded a, Enum a) => a -> a
succCycle a
  | fromEnum a == fromEnum (maxBound @a) = minBound
  | otherwise = succ a

data AppEnv s = AppEnv
  { aeActualStoreEnv :: StoreEnv PathStats,
    aeInvertedIndex :: InvertedIndex Path,
    aePrevPane :: List,
    aeCurrPane :: List,
    aeNextPane :: List,
    aeParents :: [List],
    aeOpenModal :: Maybe (Modal s),
    aeSortOrder :: SortOrder,
    aeSortOrderLastChanged :: Clock.TimeSpec,
    aeCurrTime :: Clock.TimeSpec
  }

type Path = StorePath StoreName PathStats

type List = B.GenericList Widgets Seq Path

data SortOrder
  = SortOrderAlphabetical
  | SortOrderClosureSize
  | SortOrderAddedSize
  deriving (Show, Eq, Enum, Bounded)

B.suffixLenses ''AppEnv

_ModalWhyDepends :: Traversal' (Modal s) (B.GenericList Widgets Seq (NonEmpty Path))
_ModalWhyDepends f m = case m of
  ModalWhyDepends l -> ModalWhyDepends <$> f l
  _ -> pure m

compareBySortOrder :: SortOrder -> Path -> Path -> Ordering
compareBySortOrder SortOrderAlphabetical = compare `on` T.toLower . storeNameToShortText . spName
compareBySortOrder SortOrderClosureSize = compare `on` Down . psTotalSize . spPayload
compareBySortOrder SortOrderAddedSize = compare `on` Down . psAddedSize . spPayload

attrTerminal, attrUnderlined :: B.AttrName
attrTerminal = B.attrName "terminal"
attrUnderlined = B.attrName "underlined"

run :: StoreEnv PathStats -> IO ()
run env = do
  -- Create the inverted index, and start evaluating it in the background
  let ii = iiFromList . toList . fmap (\sp -> (storeNameToText (spName sp), sp)) $ seAll env
  _ <- forkIO $ evaluateNF_ ii

  -- Initial state
  let getTime = Clock.getTime Clock.Monotonic
  currTime <- getTime
  let defaultSortOrder = SortOrderClosureSize

  let appEnv =
        AppEnv
          { aeActualStoreEnv =
              env,
            aeInvertedIndex =
              ii,
            aePrevPane =
              B.list WidgetPrevPane S.empty 0,
            aeCurrPane =
              B.list
                WidgetCurrPane
                (S.fromList . sortBy (compareBySortOrder defaultSortOrder) . NE.toList $ seGetRoots env)
                0,
            aeNextPane =
              B.list WidgetNextPane S.empty 0,
            aeParents =
              [],
            aeOpenModal =
              Nothing,
            aeSortOrder =
              defaultSortOrder,
            aeSortOrderLastChanged =
              Clock.TimeSpec 0 0,
            aeCurrTime =
              currTime
          }
          & repopulateNextPane

  -- Create a channel that's fed by current time
  chan <- B.newBChan 10
  void . forkIO $
    forever $ do
      threadDelay (100 * 1000)
      t <- getTime
      _ <- B.writeBChanNonBlocking chan (EventTick t)
      return ()

  -- And run the application
  (_, vty) <- B.customMainWithDefaultVty (Just chan) app appEnv
  V.shutdown vty

  return ()

renderList ::
  Maybe SortOrder ->
  Bool ->
  List ->
  B.Widget Widgets
renderList highlightSort =
  B.renderList
    ( \_
       StorePath
         { spName,
           spPayload = PathStats {psTotalSize, psAddedSize, psDisambiguationChars},
           spRefs,
           spSignatures
         } ->
          let color =
                if null spRefs
                  then B.withAttr attrTerminal
                  else identity
           in color $
                B.hBox
                  [ if null spSignatures
                      then B.txt "  "
                      else B.txt "✓ ",
                    B.txt (storeNameToShortTextWithDisambiguation psDisambiguationChars spName)
                      & underlineWhen SortOrderAlphabetical
                      & B.padRight (B.Pad 1)
                      & B.padRight B.Max,
                    if null spRefs
                      then
                        B.txt (prettySize psTotalSize)
                          & underlineWhen SortOrderClosureSize
                          & underlineWhen SortOrderAddedSize
                      else
                        B.hBox
                          [ B.txt (prettySize psTotalSize)
                              & underlineWhen SortOrderClosureSize,
                            B.txt " (",
                            B.txt (prettySize psAddedSize)
                              & underlineWhen SortOrderAddedSize,
                            B.txt ")"
                          ]
                  ]
    )
  where
    underlineWhen so =
      if Just so == highlightSort
        then B.withDefAttr attrUnderlined
        else identity

app :: B.App (AppEnv s) Event Widgets
app =
  B.App
    { B.appDraw = \env@AppEnv {aeOpenModal} ->
        [ case aeOpenModal of
            Nothing -> B.emptyWidget
            Just (ModalWhyDepends l) -> renderWhyDependsModal l
            Just (ModalSearch l r xs) -> renderSearchModal l r xs
            Just (ModalNotice notice) -> renderNotice notice,
          renderMainScreen env
        ],
      B.appChooseCursor = \_ _ -> Nothing,
      B.appHandleEvent = \e -> do
        s <- get
        case (e, aeOpenModal s) of
          -- main screen
          (B.VtyEvent (V.EvKey k []), Nothing)
            | k `elem` [V.KChar 'q', V.KEsc] ->
                B.halt
          (B.VtyEvent (V.EvKey (V.KChar '?') []), Nothing) ->
            put s {aeOpenModal = Just (ModalNotice helpNotice)}
          (B.VtyEvent (V.EvKey (V.KChar 'w') []), Nothing) -> do
            B.hScrollToBeginning (B.viewportScroll WidgetWhyDependsViewport)
            modify showWhyDepends
          (B.VtyEvent (V.EvKey (V.KChar '/') []), Nothing) ->
            modify $ showAndUpdateSearch "" ""
          (B.VtyEvent (V.EvKey (V.KChar 'y') []), Nothing) -> do
            liftIO (yankToClipboard $ spName (selectedPath s))
              >>= \case
                Right () -> return ()
                Left n -> put s {aeOpenModal = Just (ModalNotice n)}
          (B.VtyEvent (V.EvKey (V.KChar 's') []), Nothing) ->
            put $
              s
                { aeSortOrder = succCycle (aeSortOrder s),
                  aeSortOrderLastChanged = aeCurrTime s
                }
                & sortPanes
          (B.VtyEvent (V.EvKey k []), Nothing)
            | k `elem` [V.KChar 'h', V.KLeft] ->
                modify moveLeft
          (B.VtyEvent (V.EvKey k []), Nothing)
            | k `elem` [V.KChar 'j', V.KDown, V.KChar '\t'] ->
                move B.listMoveDown
          (B.VtyEvent (V.EvKey k []), Nothing)
            | k `elem` [V.KChar 'k', V.KUp, V.KBackTab] ->
                move B.listMoveUp
          (B.VtyEvent (V.EvKey k []), Nothing)
            | k `elem` [V.KChar 'l', V.KRight] ->
                modify moveRight
          (B.VtyEvent (V.EvKey V.KPageUp []), Nothing) ->
            moveF B.listMovePageUp
          (B.VtyEvent (V.EvKey V.KPageDown []), Nothing) ->
            moveF B.listMovePageDown
          -- why-depends modal
          (B.VtyEvent (V.EvKey k []), Just (ModalWhyDepends _))
            | k `elem` [V.KChar 'q', V.KEsc] ->
                put s {aeOpenModal = Nothing}
          (B.VtyEvent (V.EvKey k []), Just (ModalWhyDepends _))
            | k `elem` [V.KChar 'h', V.KLeft] ->
                B.hScrollBy (B.viewportScroll WidgetWhyDependsViewport) (-1)
          (B.VtyEvent (V.EvKey k []), Just (ModalWhyDepends l))
            | k `elem` [V.KChar 'j', V.KDown, V.KChar '\t'] ->
                put s {aeOpenModal = Just $ ModalWhyDepends (B.listMoveDown l)}
          (B.VtyEvent (V.EvKey k []), Just (ModalWhyDepends l))
            | k `elem` [V.KChar 'k', V.KUp, V.KBackTab] ->
                put s {aeOpenModal = Just $ ModalWhyDepends (B.listMoveUp l)}
          (B.VtyEvent (V.EvKey k []), Just (ModalWhyDepends _))
            | k `elem` [V.KChar 'l', V.KRight] ->
                B.hScrollBy (B.viewportScroll WidgetWhyDependsViewport) 1
          (B.VtyEvent (V.EvKey k []), Just (ModalWhyDepends _))
            | k == V.KHome ->
                B.hScrollToBeginning (B.viewportScroll WidgetWhyDependsViewport)
          (B.VtyEvent (V.EvKey k []), Just (ModalWhyDepends l))
            | k == V.KEnd ->
                scrollToSelectedLineEnd l
          (B.VtyEvent (V.EvKey V.KPageUp []), Just (ModalWhyDepends _)) ->
            B.zoom (aeOpenModalL . _Just . _ModalWhyDepends) B.listMovePageUp
          (B.VtyEvent (V.EvKey V.KPageDown []), Just (ModalWhyDepends _)) ->
            B.zoom (aeOpenModalL . _Just . _ModalWhyDepends) B.listMovePageDown
          (B.VtyEvent (V.EvKey V.KEnter []), Just (ModalWhyDepends l)) ->
            let closed = s {aeOpenModal = Nothing}
             in case B.listSelectedElement l of
                  Nothing -> put closed
                  Just (_, path) -> put $ selectPath path closed
          -- search modal
          (B.VtyEvent (V.EvKey V.KEsc []), Just (ModalSearch {})) ->
            put s {aeOpenModal = Nothing}
          (B.VtyEvent (V.EvKey k []), Just (ModalSearch l r xs))
            | k `elem` [V.KDown, V.KChar '\t'] ->
                put s {aeOpenModal = Just $ ModalSearch l r (B.listMoveDown xs)}
          (B.VtyEvent (V.EvKey k []), Just (ModalSearch l r xs))
            | k `elem` [V.KUp, V.KBackTab] ->
                put s {aeOpenModal = Just $ ModalSearch l r (B.listMoveUp xs)}
          (B.VtyEvent (V.EvKey V.KLeft []), Just (ModalSearch l r xs)) ->
            put
              s
                { aeOpenModal =
                    Just $ ModalSearch (T.dropEnd 1 l) (T.takeEnd 1 l <> r) (B.listMoveUp xs)
                }
          (B.VtyEvent (V.EvKey V.KRight []), Just (ModalSearch l r xs)) ->
            put
              s
                { aeOpenModal =
                    Just $ ModalSearch (l <> T.take 1 r) (T.drop 1 r) (B.listMoveUp xs)
                }
          (B.VtyEvent (V.EvKey (V.KChar c) []), Just (ModalSearch l r _))
            | c `Set.member` allowedSearchChars ->
                modify (showAndUpdateSearch (l <> T.singleton c) r)
          (B.VtyEvent (V.EvKey V.KBS []), Just (ModalSearch l r _)) ->
            modify (showAndUpdateSearch (T.dropEnd 1 l) r)
          (B.VtyEvent (V.EvKey V.KEnter []), Just (ModalSearch _ _ xs)) ->
            let closed = s {aeOpenModal = Nothing}
             in case B.listSelectedElement xs of
                  Nothing -> put closed
                  Just (_, path) ->
                    put $
                      selectPath
                        (shortestPathTo (aeActualStoreEnv s) (spName path))
                        closed
          -- notices
          (B.VtyEvent (V.EvKey k []), Just (ModalNotice _))
            | k `elem` [V.KChar 'q', V.KEsc] ->
                put s {aeOpenModal = Nothing}
          -- handle our events
          (B.AppEvent (EventTick t), _) ->
            let new = s {aeCurrTime = t}
             in do
                  put new
                  unless (timePassedSinceSortOrderChange new <= sum (replicate 2 sortOrderChangeHighlightPeriod)) B.continueWithoutRedraw
          -- ignore otherwise
          _ ->
            return (),
      B.appStartEvent = return (),
      B.appAttrMap = \_ ->
        B.attrMap
          V.defAttr
          [ (B.listSelectedFocusedAttr, V.currentAttr `V.withStyle` V.reverseVideo),
            (attrTerminal, B.fg V.blue),
            (attrUnderlined, V.currentAttr `V.withStyle` V.underline)
          ]
    }
  where
    allowedSearchChars :: Set Char
    allowedSearchChars =
      Set.fromList
        ( mconcat
            [ ['a' .. 'z'],
              ['A' .. 'Z'],
              ['0' .. '9'],
              "+-.=?_"
            ]
        )

yankToClipboard :: StoreName -> IO (Either Notice ())
yankToClipboard p =
  Clipboard.copy (toText $ storeNameToPath p)
    <&> \case
      Right () -> Right ()
      Left errs ->
        Left $
          Notice
            "Error"
            ( T.intercalate "\n" $
                "Cannot copy to clipboard: "
                  : map ("  " <>) errs
                  ++ ["Please report this as a bug."]
            )

timePassedSinceSortOrderChange :: AppEnv s -> Clock.TimeSpec
timePassedSinceSortOrderChange env = Clock.diffTimeSpec (aeCurrTime env) (aeSortOrderLastChanged env)

renderMainScreen :: AppEnv s -> B.Widget Widgets
renderMainScreen env@AppEnv {aePrevPane, aeCurrPane, aeNextPane} =
  (B.joinBorders . B.border)
    ( B.hBox
        [ renderList Nothing True aePrevPane,
          B.vBorder,
          renderList shouldHighlightSortOrder True aeCurrPane,
          B.vBorder,
          renderList Nothing False aeNextPane
        ]
    )
    B.<=> renderInfoPane env
  where
    shouldHighlightSortOrder =
      if timePassedSinceSortOrderChange env < sortOrderChangeHighlightPeriod
        then Just (aeSortOrder env)
        else Nothing

renderInfoPane :: AppEnv s -> B.Widget Widgets
renderInfoPane env =
  let selected = selectedPath env
      immediateParents = psImmediateParents $ spPayload selected
      signatures = spSignatures selected
   in B.vBox
        [ let (f, s) = storeNameToSplitShortText (spName selected)
           in B.txt f B.<+> underlineWhen SortOrderAlphabetical (B.txt s),
          [ B.txt $ "NAR Size: " <> prettySize (spSize selected),
            underlineWhen SortOrderClosureSize . B.txt $ "Closure Size: " <> prettySize (psTotalSize $ spPayload selected),
            underlineWhen SortOrderAddedSize . B.txt $ "Added Size: " <> prettySize (psAddedSize $ spPayload selected)
          ]
            & intersperse (B.txt " | ")
            & B.hBox,
          B.txt $
            "Signatures: "
              <> if null signatures
                then "✗"
                else
                  ( signatures
                      & map npsKeyName
                      & T.intercalate ", "
                  ),
          B.txt $
            if null immediateParents
              then "Immediate Parents: -"
              else
                "Immediate Parents ("
                  <> T.pack (show $ length immediateParents)
                  <> "): "
                  <> T.intercalate ", " (map storeNameToShortText immediateParents)
        ]
  where
    underlineWhen so =
      if so == aeSortOrder env
        then B.withAttr attrUnderlined
        else identity

renderModal :: Text -> B.Widget a -> B.Widget a
renderModal title widget =
  widget
    & B.borderWithLabel (B.txt title)
    & B.hLimitPercent 90
    & B.vLimitPercent 60
    & B.centerLayer

helpText :: Text
helpText =
  T.intercalate
    "\n"
    [ "hjkl/Arrow Keys : Navigate",
      "w               : Open why-depends modal",
      "/               : Open search modal",
      "s               : Change sort order",
      "y               : Yank selected path to clipboard",
      "?               : Show help",
      "q/Esc           : Quit / close modal"
    ]

helpNotice :: Notice
helpNotice = Notice "Help" helpText

renderNotice :: Notice -> B.Widget a
renderNotice (Notice title txt) = renderModal title (B.txt txt)

renderWhyDependsModal ::
  B.GenericList Widgets Seq (NonEmpty Path) ->
  B.Widget Widgets
renderWhyDependsModal l =
  B.renderList renderDepends True l
    & B.hLimitPercent 100 -- This limit seems pointless, but otherwise render list takes infinite
    -- amount of horizontal space and 'viewport' below complains.
    & B.viewport WidgetWhyDependsViewport B.Horizontal
    & renderModal "why-depends"
  where
    renderDepends _ =
      B.txt . whyDependsPathToText

whyDependsPathToText :: NonEmpty Path -> Text
whyDependsPathToText xs =
  xs
    & NE.toList
    & fmap (storeNameToShortText . spName)
    & T.intercalate " → "

{- We could use 'B.hScrollToEnd', but it jumps too much when some lines are longer than others.
This is more fine grained in that it jumps only to the end of the currently selected line.
-}
scrollToSelectedLineEnd ::
  B.GenericList Widgets Seq (NonEmpty Path) ->
  B.EventM Widgets (AppEnv s) ()
scrollToSelectedLineEnd l = do
  mViewport <- B.lookupViewport WidgetWhyDependsViewport
  case (mViewport, B.listSelectedElement l) of
    (Just vp, Just (_, selectedPath')) -> do
      let textWidth = B.textWidth (whyDependsPathToText selectedPath')
          viewportWidth = vp ^. B.vpSize . _1
          currentScroll = vp ^. B.vpLeft
          -- Calculate the scroll position needed to right-align the selected line
          -- We want: scrollPos + viewportWidth >= textWidth
          -- So: scrollPos = textWidth - viewportWidth (but not less than 0)
          targetScroll = max 0 (textWidth - viewportWidth)
          scrollDelta = targetScroll - currentScroll
      B.hScrollBy (B.viewportScroll WidgetWhyDependsViewport) scrollDelta
    _ -> pass

showWhyDepends :: AppEnv s -> AppEnv s
showWhyDepends env@AppEnv {aeActualStoreEnv} =
  env
    { aeOpenModal =
        Just . ModalWhyDepends $
          let selected = selectedPath env
              route = selectedPaths env
              xs = S.fromList $ whyDepends aeActualStoreEnv (spName selected)
           in B.list WidgetWhyDepends xs 1
                & B.listMoveTo
                  (fromMaybe 0 $ ((==) `on` fmap spName) route `S.findIndexL` xs)
    }

renderSearchModal :: Text -> Text -> B.GenericList Widgets Seq Path -> B.Widget Widgets
renderSearchModal left right l =
  renderModal "Search" window
  where
    window =
      B.txt left
        B.<+> B.txt "|"
        B.<+> B.txt right
        B.<=> B.hBorder
        B.<=> renderList Nothing True l

showAndUpdateSearch :: Text -> Text -> AppEnv s -> AppEnv s
showAndUpdateSearch left right env@AppEnv {aeInvertedIndex} =
  env {aeOpenModal = Just $ ModalSearch left right results}
  where
    results =
      let xs =
            iiSearch (left <> right) aeInvertedIndex
              & Map.elems
              & S.fromList
       in B.list WidgetSearch xs 1

move :: (List -> List) -> B.EventM n (AppEnv s) ()
move = moveF . modify

moveF :: B.EventM n List () -> B.EventM n (AppEnv s) ()
moveF f = do
  B.zoom aeCurrPaneL f
  modify repopulateNextPane

moveLeft :: AppEnv s -> AppEnv s
moveLeft env@AppEnv {aeParents = []} = env
moveLeft env@AppEnv {aePrevPane, aeCurrPane, aeParents = parent : grandparents} =
  env
    { aeParents = grandparents,
      aePrevPane = parent,
      aeCurrPane = aePrevPane {B.listName = WidgetCurrPane},
      aeNextPane = aeCurrPane {B.listName = WidgetNextPane}
    }

moveRight :: AppEnv s -> AppEnv s
moveRight env@AppEnv {aePrevPane, aeCurrPane, aeNextPane, aeParents}
  | null (B.listElements aeNextPane) = env
  | otherwise =
      env
        { aePrevPane = aeCurrPane {B.listName = WidgetPrevPane},
          aeCurrPane = aeNextPane {B.listName = WidgetCurrPane},
          aeParents = aePrevPane : aeParents
        }
        & repopulateNextPane

repopulateNextPane :: AppEnv s -> AppEnv s
repopulateNextPane env@AppEnv {aeActualStoreEnv, aeNextPane, aeSortOrder} =
  let ref = selectedPath env
   in env
        { aeNextPane =
            B.listReplace
              ( S.sortBy (compareBySortOrder aeSortOrder)
                  . S.fromList
                  . map (seLookup aeActualStoreEnv)
                  $ spRefs ref
              )
              (Just 0)
              aeNextPane
        }

sortPane :: SortOrder -> List -> List
sortPane so l =
  let selected = B.listSelectedElement l
      elems =
        B.listElements l
          & S.sortBy (compareBySortOrder so)
      name = B.getName l
   in mkList so name elems (snd <$> selected)

sortPanes :: AppEnv s -> AppEnv s
sortPanes env@AppEnv {aeParents, aePrevPane, aeCurrPane, aeNextPane, aeSortOrder} =
  env
    { aeCurrPane = sortPane aeSortOrder aeCurrPane,
      aeNextPane = sortPane aeSortOrder aeNextPane,
      aeParents = sortPane aeSortOrder <$> aeParents,
      aePrevPane = sortPane aeSortOrder aePrevPane
    }

selectedPath :: AppEnv s -> Path
selectedPath = NE.head . selectedPaths

selectedPaths :: AppEnv s -> NonEmpty Path
selectedPaths AppEnv {aePrevPane, aeCurrPane, aeParents} =
  let parents =
        mapMaybe
          (fmap snd . B.listSelectedElement)
          (aePrevPane : aeParents)
   in case B.listSelectedElement aeCurrPane of
        Nothing -> error "invariant violation: no selected element"
        Just (_, p) -> p :| parents

selectPath :: NonEmpty Path -> AppEnv s -> AppEnv s
selectPath path env
  | (spName <$> path) == (spName <$> selectedPaths env) =
      env
selectPath path env@AppEnv {aeActualStoreEnv} =
  let root :| children = NE.reverse path
      lists =
        NE.scanl
          ( \(_, prev) curr ->
              ( map (seLookup aeActualStoreEnv) $
                  spRefs prev,
                curr
              )
          )
          (NE.toList (seGetRoots aeActualStoreEnv), root)
          children
          & NE.reverse
          & fmap (\(possible, selected) -> mkList (aeSortOrder env) WidgetPrevPane (S.fromList possible) (Just selected))
          & (<> (emptyPane :| []))
   in case lists of
        (curr :| prevs) ->
          let (prev, parents) = case prevs of
                [] -> (emptyPane, [])
                p : ps -> (p, ps)
           in env
                { aeParents = parents,
                  aePrevPane = prev,
                  aeCurrPane = curr {B.listName = WidgetCurrPane}
                }
                & repopulateNextPane
  where
    emptyPane =
      B.list WidgetPrevPane S.empty 0

mkList ::
  SortOrder ->
  n ->
  Seq Path ->
  Maybe Path ->
  B.GenericList n Seq Path
mkList sortOrder name possible selected =
  let contents = S.sortBy (compareBySortOrder sortOrder) possible
   in B.list name contents 1
        & B.listMoveTo
          (fromMaybe 0 $ selected >>= \s -> ((==) `on` spName) s `S.findIndexL` contents)

-- Utils

prettySize :: Int -> T.Text
prettySize size = case HRF.convertSize $ fromIntegral size of
  HRF.Bytes d -> T.pack (show d)
  HRF.KiB d -> T.pack (show d) <> " KiB"
  HRF.MiB d -> T.pack (show d) <> " MiB"
  HRF.GiB d -> T.pack (show d) <> " GiB"
  HRF.TiB d -> T.pack (show d) <> " TiB"