packages feed

clifm-0.5.2.1: src/Widgets/Tab.hs

module Widgets.Tab where
import Commons
import qualified Widgets.Entry as Entry

import Control.Lens hiding (Empty)
import Data.List (sortOn, isInfixOf, elemIndex)
import Data.Char (toLower)
import Data.Maybe (fromMaybe, fromJust)
import Data.Time.Clock (UTCTime(..), secondsToDiffTime)
import Data.Time.Calendar (Day(ModifiedJulianDay))
import Conduit
import System.FilePath (takeFileName, takeDirectory, (</>))
import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
import Brick.Types (Widget, EventM)
import Brick.Widgets.Core (hLimit, hBox, vBox, (<+>), str, strWrap, fill, withBorderStyle, visible)
import Brick.Widgets.List (List, list, renderList, handleListEvent, listMoveTo,
  listSelectedElement, listReplace, listElements, listElementsL, listSelectedL, listNameL, listItemHeightL)
import Brick.Widgets.Border (hBorder, vBorder, borderElem, border)
import Brick.Widgets.Border.Style (unicodeRounded, unicodeBold, bsHorizontal, bsCornerTL, bsCornerTR)
import Graphics.Vty (Event(EvKey), Key(..), Modifier(MCtrl))
import Data.Foldable (toList)
import qualified Data.Vector as Vect

data Tab = Dir {_name :: String, _path :: FilePath, _entryList :: List Name Entry.Entry, _entryOrder :: EntryOrder} |
  Search {_name :: String, _path :: FilePath, _query :: String, _entryList :: List Name Entry.Entry, _entryOrder :: EntryOrder} |
  Empty
data EntryOrder = EntryOrder {_orderType :: OrderType, _inverted :: Bool}
data OrderType = FileName | FileSize | AccessTime | ModificationTime deriving (Eq, Enum, Bounded)

instance Show Tab where
  show Empty = "\x276f -new tab-"
  show Dir {_name = n} = "\x2636 " ++ n
  show Search {_name = n} = "\x26B2 " ++ n

instance Show EntryOrder where
  show order = show (_orderType order) ++ (if _inverted order then " \x2193 " else " \x2191 ")

instance Show OrderType where
  show FileName = "name"
  show FileSize = "size"
  show AccessTime = "access"
  show ModificationTime = "modified"

-- lenses
name :: Traversal' Tab String
name = unlessEmpty . lens _name (\tab n -> tab {_name = n})

path :: Traversal' Tab FilePath
path = unlessEmpty . lens _path (\tab n -> tab {_path = n})

entryList :: Traversal' Tab (List Name Entry.Entry)
entryList = unlessEmpty . lens _entryList (\tab n -> tab {_entryList = n})

entryOrder :: Traversal' Tab EntryOrder
entryOrder = unlessEmpty . lens _entryOrder (\tab n -> tab {_entryOrder = n})

query :: Traversal' Tab String
query = onSearch . lens _query (\tab n -> tab {_query = n})

orderType :: Traversal' Tab OrderType
orderType = entryOrder.entryOrderType

orderInverted :: Traversal' Tab Bool
orderInverted = entryOrder.entryOrderInverted

entryOrderType :: Lens' EntryOrder OrderType
entryOrderType = lens _orderType (\tab n -> tab {_orderType = n})

entryOrderInverted :: Lens' EntryOrder Bool
entryOrderInverted = lens _inverted (\tab n -> tab {_inverted = n})

onSearch :: Traversal' Tab Tab
onSearch = filtered isSearch

unlessEmpty :: Traversal' Tab Tab
unlessEmpty = filtered (not . isEmpty)

entries :: Traversal' Tab Entry.Entry
entries = entryList.traverse

-- creation
empty :: Tab
empty = Empty

makeDirTab :: PaneName -> FilePath -> IO Tab
makeDirTab pName filePath = do
  isFile <- doesFileExist filePath
  isDir <- doesDirectoryExist filePath
  if isDir && not isFile then do
    let fName = takeFileName filePath
        order = EntryOrder FileName False
    entryLst <- makeDirEntryList pName order filePath
    return $ Dir (if null fName then "-root-" else fName) filePath entryLst order
  else return empty

makeDirEntryList :: PaneName -> EntryOrder -> FilePath -> IO (List Name Entry.Entry)
makeDirEntryList pName order dir = do
  sub <- listDirectory dir
  entries <- mapM (Entry.make . (dir </>)) sub
  upDir <- Entry.makeBackDir $ takeDirectory dir
  return $ list EntryList {pnName = pName} (Vect.fromList . (upDir :) $ sortEntries order entries) 1

makeSearchTab :: PaneName -> FilePath -> String -> IO Tab
makeSearchTab pName filePath searchQuery = do
  isFile <- doesFileExist filePath
  isDir <- doesDirectoryExist filePath
  if isDir && not isFile then do
    let order = EntryOrder FileName False
    entryLst <- makeSearchEntryList pName order filePath searchQuery
    return $ Search searchQuery filePath searchQuery entryLst order
  else return empty

makeSearchEntryList :: PaneName -> EntryOrder -> FilePath -> String -> IO (List Name Entry.Entry)
makeSearchEntryList pName order dir searchQuery = do
  searchResult <- searchRecursive dir searchQuery
  entries <- mapM Entry.make searchResult
  searchDir <- Entry.makeBackDir dir
  return $ list EntryList {pnName = pName} (Vect.fromList . (searchDir :) $ sortEntries order entries) 1

-- rendering
renderLabel :: (Tab, Bool) -> Widget Name
renderLabel (tab, hasFoc) = modifs . hLimit (wdt + 2) $ vBox [top, middle]
  where
    modifs = if hasFoc then withBorderStyle unicodeBold . visible
      else withBorderStyle unicodeRounded
    txt = show tab
    wdt = min 14 $ length txt
    top = hBox [borderElem bsCornerTL, hBorder, borderElem bsCornerTR]
    middle = hBox [vBorder, str $ take wdt txt, fill ' ', vBorder]

renderSeparator :: Tab -> Widget Name
renderSeparator tab = hBox [
    borderElem bsHorizontal,
    renderPath tab,
    hBorder,
    renderEntryOrder tab,
    borderElem bsHorizontal
  ]

renderEntryOrder :: Tab -> Widget Name
renderEntryOrder = str . maybe "" (" by " ++) . previews entryOrder show

renderPath :: Tab -> Widget Name
renderPath tab = str $ case tab of
  Empty -> " <empty tab> "
  Dir {_path = p} -> " " ++ p ++ " "
  Search {_path = p, _query = q} -> " search for " ++ q ++ " in " ++ takeFileName p

renderContent :: Bool -> Tab -> Widget Name
renderContent hasFoc = maybe emptyContent (renderList Entry.render hasFoc) . preview entryList

emptyContent :: Widget Name
emptyContent = vBox (lns ++ [fill ' '])
  where lns = map strWrap $ lines "Command Line Interface File Manager\n \n\
    \clifm allows you to explore directories on multiple tabs.\nIf your terminal\
    \ has mouse support you can click on some elements to interact with them, \
    \but you can perform every action with your keyboard.\n \nInside each tab \
    \you can move to a different entry using the up and down arrow keys \
    \(Home/End to jump to top or bottom) and Enter to move into a selected \
    \directory.\n \nYou can move to a different tab using... the Tab and the \
    \BackTab key or use Ctrl + Left or Right arrow key to swap them.\n \nYou can \
    \see every other possible action as a button in the bottom, or you can use \
    \them as Keys combination.\n \nTo see them all please refer to the README"

-- event handling and state-changing
handleEvent :: Event -> Tab -> EventM Name Tab
handleEvent event = case event of
  EvKey (KChar 'o') [] -> return . changeOrder
  EvKey (KChar 'i') [] -> return . invertOrder
  _ -> entryList (handleListEvent event)

changeOrder :: Tab -> Tab
changeOrder tab = tab
  & entryOrder .~ newOrder
  & entryList.listElementsL._tail .~ sorted
  & entryList.listSelectedL ?~ maybe 0 (+1) ((`Vect.elemIndex` sorted) =<< selectedEntry tab)
  where
    newOrder = over entryOrderType nextOrderType . fromJust $ preview entryOrder tab
    sorted = Vect.fromList . sortEntries newOrder . toList $ view (entryList.listElementsL._tail) tab

invertOrder :: Tab -> Tab
invertOrder tab = tab
  & orderInverted %~ not 
  & entryList.listElementsL._tail %~ Vect.reverse
  & entryList.listSelectedL %~ fmap (\idx -> if idx == 0 then 0 else size - idx)
  where size = fromJust $ previews (entryList.listElementsL) Vect.length tab

reload :: PaneName -> Tab -> IO Tab
reload pName tab = case tab of
  Empty -> return Empty
  Dir {_path=p, _entryOrder=o} -> keepSelection tab <$> makeDirEntryList pName o p
  Search {_path=p, _entryOrder=o, _query=q} -> keepSelection tab <$> makeSearchEntryList pName o p q

keepSelection :: Tab -> List Name Entry.Entry -> Tab
keepSelection tab newList = tab 
  & entryList.listElementsL .~ newElems
  & entryList.listSelectedL ?~ fromMaybe 0 ((`Vect.elemIndex` newElems) =<< selectedEntry tab)
  where newElems = listElements newList

moveToRow :: Int -> Tab -> Tab
moveToRow row = over entryList (listMoveTo row)

-- utility
isDir :: Tab -> Bool
isDir Dir {} = True
isDir _ = False

isSearch :: Tab -> Bool
isSearch Search {} = True
isSearch _ = False

isEmpty :: Tab -> Bool
isEmpty Empty = True
isEmpty _ = False

selectedEntry :: Tab -> Maybe Entry.Entry
selectedEntry Empty = Nothing
selectedEntry tab = snd <$> listSelectedElement (fromJust $ preview entryList tab)

nextOrderType :: OrderType -> OrderType
nextOrderType order
  | order == maxBound = minBound
  | otherwise = succ order

sortEntries :: EntryOrder -> [Entry.Entry] -> [Entry.Entry]
sortEntries order
  | view entryOrderInverted order = reverse . orderOn
  | otherwise = orderOn
  where 
    orderOn = case view entryOrderType order of
      FileName -> sortOn (map toLower . view Entry.name)
      FileSize -> sortOn (view Entry.size)
      AccessTime -> sortOn (fromMaybe zeroTime . preview Entry.accessTime)
      ModificationTime -> sortOn (fromMaybe zeroTime . preview Entry.modifTime)

zeroTime :: UTCTime
zeroTime = UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)

searchRecursive :: FilePath -> String -> IO [FilePath]
searchRecursive filePath searchQuery = runConduitRes
  $ sourceDirectoryDeep False filePath
  .| filterC (isInfixOf searchQuery . takeFileName)
  .| sinkList