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 -> " " 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