packages feed

tinytools-vty-0.1.0.0: src/Potato/Reflex/Vty/Widget/FileExplorer.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo     #-}

module Potato.Reflex.Vty.Widget.FileExplorer (
  FileExplorerWidgetConfig(..)
  , FileExplorerWidget(..)
  , holdFileExplorerWidget
) where

import           Relude

import           Potato.Flow
import           Potato.Flow.Controller
import           Potato.Flow.Vty.Attrs
import           Potato.Flow.Vty.Input
import           Potato.Reflex.Vty.Helpers
import           Potato.Reflex.Vty.Widget
import Potato.Flow.Vty.PotatoReader
import Potato.Reflex.Vty.Widget.TextInputHelpers
import           Potato.Reflex.Vty.Widget.ScrollBar



import Control.Exception (catch)
import           Control.Monad.Fix
import           Data.Align
import           Data.Dependent.Sum          (DSum ((:=>)))
import qualified Data.IntMap.Strict          as IM
import qualified Data.List                   as L
import qualified Data.Sequence               as Seq
import qualified Data.Text                   as T
import           Data.Text.Zipper
import qualified Data.Text.Zipper            as TZ
import           Data.These
import qualified System.Directory as FP
import qualified System.FilePath as FP

import qualified Graphics.Vty                as V
import           Reflex
import           Reflex.Network
import           Reflex.Potato.Helpers
import           Reflex.Vty


fetchDirectory :: forall t m. (MonadWidget t m) => Event t FP.FilePath -> m (Event t [(Bool, FP.FilePath)])
fetchDirectory ev = let
    catchfn :: SomeException -> IO [(Bool, FP.FilePath)]
    catchfn = const (return [])
  in performEvent $ ffor ev $ \dir -> liftIO $ (flip catch catchfn) $ do
    --workingDir <- FP.getCurrentDirectory
    contents <- FP.getDirectoryContents dir
    contentsWithFolder <- forM contents $ \d -> FP.doesDirectoryExist (FP.combine dir d) >>= return . (,FP.combine dir d)
    let
      sortfn (d1,_) (d2,_) = case (d1,d2) of
        (True, False) -> LT
        (False, True) -> GT
        _ -> EQ
    return $ (L.sortBy sortfn) contentsWithFolder

data FileExplorerWidgetConfig t = FileExplorerWidgetConfig {
  _fileExplorerWidgetConfig_clickDownStyle :: Behavior t V.Attr

  -- TODO we don't need full filepath
  , _fileExplorerWidgetConfig_fileFilter :: FP.FilePath -> Bool
  , _fileExplorerWidgetConfig_initialFile :: FP.FilePath
}

data FileExplorerWidget t = FileExplorerWidget {
  -- the filename in the entry area
  _fileExplorerWidget_filename :: Behavior t Text
  -- the directory <> filename in the entry area
  , _fileExplorerWidget_fullfilename :: Behavior t FP.FilePath
  , _fileExplorerWidget_doubleClickFile :: Event t FP.FilePath -- pretty sure this is just single click for now -__-
  , _fileExplorerWidget_directory :: Dynamic t FP.FilePath
  , _fileExplorerWidget_returnOnfilename :: Event t () -- fires when you hit the "return" key in file name input area
  , _fileExplorerWidget_doubleClick :: Event t () -- fires when you double click on an existing valid file
}

data FileClick = FileClick {
    _fileClick_isDouble :: Bool
    , _fileClick_isFolder :: Bool
    , _fileClick_file :: FP.FilePath
  }
-- TODO reduce constraints, don't use HasPotato
holdFileExplorerWidget :: forall t m. (MonadLayoutWidget t m, HasPotato t m)
  => FileExplorerWidgetConfig t
  -> m (FileExplorerWidget t)
holdFileExplorerWidget FileExplorerWidgetConfig {..} = mdo

  baseStyle <- theme
  isInitialFileDir <- liftIO (FP.doesDirectoryExist _fileExplorerWidgetConfig_initialFile)

  -- select + click is one way to do choose file to save to but double click is prob better...
  --selectedDyn :: Dynamic t (Maybe Int)
  --selectedDyn <- holdDyn Nothing $ leftmost [selectEv, ]

  -- set up directory/filename text field stuff
  pb <- getPostBuild
  let
    initialDirFileEv :: Event t (FilePath, FilePath) = flip pushAlways pb $ \_ -> do
      let
        dir = FP.takeDirectory _fileExplorerWidgetConfig_initialFile
        file = FP.takeFileName _fileExplorerWidgetConfig_initialFile
      return $ if isInitialFileDir then (_fileExplorerWidgetConfig_initialFile, "") else (dir, file)
    foldDirDynFn new old = case FP.takeFileName new of
      "." -> old
      ".." -> FP.takeDirectory old
      _ -> new
  dirDyn <- foldDyn foldDirDynFn "" (leftmost [fmap fst initialDirFileEv, clickFolderEvent, setFolderEvent])
  fetchDirComplete <- fetchDirectory (updated dirDyn)
  dirContentsDyn <- holdDyn [] fetchDirComplete

  let
    -- set up directory list widget
    dirWidget filenameinentryfielddyn xs = (grout . stretch) 1 $ row $ mdo
      r <- (grout . stretch) 1 $ col $ do
        let 
          scrolledContents = ffor vScrollDyn $ \vscroll -> fmap leftmost $ forM (drop vscroll xs) $ \(isFolder, path) -> do
            (grout . fixed) 1 $ row $ do
              let
                clickable = _fileExplorerWidgetConfig_fileFilter path

                -- TODO design proper styling... (maybe prefix folders with > instead of style differently?)

              -- TODO make it so you need to click on the name???
              (singleClick', downDyn) <- singleClickWithDownState V.BLeft
              doubleClick <- doubleClickSimple 

              -- TODO highlight if matches the file entry box
              let
                styleBeh = join $ ffor2 (current filenameinentryfielddyn) (current downDyn) $ \fn d -> if d || T.unpack fn == filename then _fileExplorerWidgetConfig_clickDownStyle else baseStyle
                filename = FP.takeFileName path
                pathtext' = T.pack filename
                pathtext = if isFolder
                  then "> " <> pathtext'
                  else if clickable
                    then " *" <> pathtext'
                    else "  " <> pathtext'

              localTheme (const styleBeh) $ do
                text (constant pathtext)

              let 
                singleClick = ffilter (not . _singleClick_didDragOff) singleClick'
                makefileclick isdouble = FileClick {
                    _fileClick_isDouble = isdouble
                    , _fileClick_isFolder = isFolder
                    , _fileClick_file = path
                  }
                alignWithFn th = case th of
                  This _ -> makefileclick False
                  That _ -> makefileclick True
                  These _ _ -> makefileclick True
                fileclickev = alignWith alignWithFn singleClick (traceEvent "double" doubleClick)

              if isFolder || clickable
                then return fileclickev
                else return never
        join . fmap (switchHold never) . networkView $ scrolledContents
      vScrollDyn <- (grout . fixed) 1 $ col $ do
        vScrollBar (constDyn (length xs))
      return r

  -- put it all together
  (clickEvent, setFolderRawEvent, filenameDyn, enterEv) <- col $ do
    -- TODO consider combining filename and directory into one...
    let
      setFileEv = fmap T.pack $ leftmost [fmap snd initialDirFileEv, fmap FP.takeFileName clickFileEvent]


    -- input for filename
    (fninputfid, (filenameDyn', enterEv')) <- (tile . fixed) 1 $ row $ do
      (grout . fixed) 10 $ text "filename"
      (tile' . stretch) 1 $ (,) <$> filenameInput "" setFileEv <*> key V.KEnter

    -- focus the filename input
    requestFocus (pb $> Refocus_Id fninputfid)

    -- input for directory
    setFolderRawEvent' <- (tile . fixed) 1 $ row $ do
      (grout . fixed) 10 $ text "directory"
      (tile . stretch) 1 $ do
        let indirev = (updated (fmap T.pack dirDyn))
        dirdyn <- filenameInput "" indirev
        return $ difference (updated $ fmap T.unpack dirdyn) indirev

    -- click in dir list event
    (clickEvent') <- (grout . stretch) 5 $ box (constant singleBoxStyle) $ do
      join . fmap (switchHold never) $ networkView (ffor dirContentsDyn (dirWidget filenameDyn'))

    return (clickEvent', setFolderRawEvent', filenameDyn', enterEv')

  -- perform the IO query to get the folder contents
  mSetFolderEvent <- performEvent $ ffor setFolderRawEvent $ \dir -> liftIO $ do
    exists <- FP.doesDirectoryExist dir
    if exists then return $ Just dir else return Nothing
  let
    setFolderEvent = fmapMaybe id mSetFolderEvent

  let
    -- TODO consider using doubleClickEvent (you might want to implement highlight selection in this case)
    maybeClickFolder fc = if _fileClick_isFolder fc && not (_fileClick_isDouble fc) then Just (_fileClick_file fc) else Nothing
    maybeClickFile fc = if not (_fileClick_isFolder fc) && not (_fileClick_isDouble fc) then Just (_fileClick_file fc) else Nothing 
    maybeDoubleClickFile fc = if not (_fileClick_isFolder fc) && _fileClick_isDouble fc then Just (_fileClick_file fc) else Nothing

    clickFolderEvent = fmapMaybe maybeClickFolder clickEvent
    clickFileEvent = fmapMaybe maybeClickFile clickEvent
    doubleClickFileEvent = fmapMaybe maybeDoubleClickFile clickEvent

    fullfilenameDyn = ffor2 dirDyn filenameDyn $ \dir fn -> FP.combine dir (T.unpack fn)

  return $ FileExplorerWidget {
      _fileExplorerWidget_filename  = current filenameDyn
      , _fileExplorerWidget_fullfilename = current fullfilenameDyn
      , _fileExplorerWidget_doubleClickFile = clickFileEvent
      , _fileExplorerWidget_directory = dirDyn
      , _fileExplorerWidget_returnOnfilename = void enterEv
      , _fileExplorerWidget_doubleClick = void doubleClickFileEvent
    }