From 2d0abaeb779ef63ed59de136e55c63ed2fd0a4ba Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Fri, 3 Oct 2025 11:05:17 +0530 Subject: Preserve list position while navigating through directories --- lib/Daffm/Event.hs | 62 +++++++++++++++++++++++++++++++++++++++ lib/Daffm/State.hs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/Daffm/Types.hs | 4 ++- lib/Daffm/View.hs | 11 +++---- 4 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 lib/Daffm/Event.hs create mode 100644 lib/Daffm/State.hs (limited to 'lib/Daffm') diff --git a/lib/Daffm/Event.hs b/lib/Daffm/Event.hs new file mode 100644 index 0000000..f14ebd8 --- /dev/null +++ b/lib/Daffm/Event.hs @@ -0,0 +1,62 @@ +module Daffm.Event where + +import Brick (suspendAndResume') +import qualified Brick.Main as M +import qualified Brick.Types as T +import qualified Brick.Widgets.Edit as Editor +import qualified Brick.Widgets.List as L +import Control.Monad.State (MonadIO (liftIO), MonadState, get, gets, modify, put) +import Daffm.State (cacheDirPosition, loadDirInAppState) +import Daffm.Types (AppState (..), FileInfo (..), FileType (..), FocusTarget (FocusCmdline, FocusMain)) +import Data.Vector ((!?)) +import qualified Graphics.Vty as V +import System.FilePath (takeDirectory) +import System.Process (callProcess) + +type AppEvent = T.EventM FocusTarget AppState + +modifyM :: (MonadState s m) => (s -> m s) -> m () +modifyM f = get >>= f >>= put + +appEvent :: T.BrickEvent FocusTarget e -> AppEvent () +appEvent brickevent@(T.VtyEvent event) = do + focusTarget <- gets stateFocusTarget + case (focusTarget, event) of + (FocusCmdline, V.EvKey V.KEsc []) -> modify (\st -> st {stateFocusTarget = FocusMain}) + (FocusMain, V.EvKey (V.KChar ':') []) -> modify (\st -> st {stateFocusTarget = FocusCmdline}) + (FocusMain, V.EvKey (V.KChar 'q') []) -> M.halt + (FocusMain, V.EvKey (V.KChar 'l') []) -> openSelectedFile + (FocusMain, V.EvKey (V.KChar 'h') []) -> goBackToParentDir + (FocusMain, V.EvKey V.KEnter []) -> openSelectedFile + (FocusMain, V.EvKey V.KBS []) -> goBackToParentDir + (FocusMain, _) -> do + files <- gets stateFiles + newFiles <- T.nestEventM' files (L.handleListEventVi L.handleListEvent event) + modify (\appState -> appState {stateFiles = newFiles}) + (FocusCmdline, _) -> do + editor <- gets stateCmdlineEditor + newEditor <- T.nestEventM' editor (Editor.handleEditorEvent brickevent) + modify (\appState -> appState {stateCmdlineEditor = newEditor}) + modify cacheDirPosition +appEvent _ = pure () + +openSelectedFile :: AppEvent () +openSelectedFile = do + AppState {stateFiles, stateCwd} <- get + let indexM = L.listSelected stateFiles + let files = L.listElements stateFiles + case indexM >>= (files !?) of + Just (FileInfo {filePath, fileType = Directory}) -> + modifyM (liftIO . loadDirInAppState filePath stateCwd) + Just (FileInfo {filePath, fileType}) -> do + suspendAndResume' $ do + putStrLn $ "Opening " <> show fileType <> ": " <> filePath + callProcess "nvim" [filePath] + pure () + Nothing -> pure () + pure () + +goBackToParentDir :: AppEvent () +goBackToParentDir = do + dir <- gets stateParentDir + modifyM (liftIO . loadDirInAppState dir (takeDirectory dir)) diff --git a/lib/Daffm/State.hs b/lib/Daffm/State.hs new file mode 100644 index 0000000..cfa9aeb --- /dev/null +++ b/lib/Daffm/State.hs @@ -0,0 +1,85 @@ +module Daffm.State where + +import qualified Brick.Widgets.Edit as Editor +import qualified Brick.Widgets.List as L +import Control.Applicative ((<|>)) +import Control.Monad (forM) +import Daffm.Types (AppState (..), FileInfo (..), FileType (..), FocusTarget (..)) +import Data.Char (toLower) +import Data.List (findIndex, sortBy) +import qualified Data.Map.Strict as Map +import Data.Maybe (fromMaybe) +import qualified Data.Vector as Vec +import System.Directory (listDirectory, makeAbsolute, setCurrentDirectory) +import qualified System.PosixCompat as Posix + +mkEmptyAppState :: AppState +mkEmptyAppState = + AppState + { stateFiles = L.list FocusMain (Vec.fromList []) 1, + stateCmdlineEditor = Editor.editor FocusCmdline Nothing "", + stateFocusTarget = FocusMain, + stateListPositionCache = Map.empty, + stateCwd = "", + stateParentDir = "" + } + +loadDirInAppState :: FilePath -> FilePath -> AppState -> IO AppState +loadDirInAppState dir parentDir appState@(AppState {stateCwd, stateListPositionCache}) = do + setCurrentDirectory dir + files <- listFilesInDir dir + let prevDirPosM = findIndex ((== stateCwd) . filePath) files + let cachedPosM = Map.lookup dir stateListPositionCache + let pos = fromMaybe 0 (cachedPosM <|> prevDirPosM) + let list = L.listMoveTo pos $ L.list FocusMain (Vec.fromList files) 1 + pure $ + appState + { stateFiles = list, + stateCwd = dir, + stateParentDir = parentDir + } + +fileTypeFromStatus :: Posix.FileStatus -> FileType +fileTypeFromStatus s = + if + | Posix.isBlockDevice s -> BlockDevice + | Posix.isCharacterDevice s -> CharacterDevice + | Posix.isNamedPipe s -> NamedPipe + | Posix.isRegularFile s -> RegularFile + | Posix.isDirectory s -> Directory + | Posix.isSocket s -> UnixSocket + | Posix.isSymbolicLink s -> SymbolicLink + | otherwise -> UnknownFileType + +getFileInfo :: FilePath -> IO FileInfo +getFileInfo name = do + path <- makeAbsolute name + stat <- Posix.getSymbolicLinkStatus path + pure $ + FileInfo + { filePath = path, + fileName = name, + fileSize = Posix.fileSize stat, + fileType = fileTypeFromStatus stat + } + +fileSorter :: FileInfo -> FileInfo -> Ordering +fileSorter (FileInfo {fileType = Directory, fileName = fa}) (FileInfo {fileType = Directory, fileName = fb}) = + compare (toLower <$> fa) (toLower <$> fb) +fileSorter (FileInfo {fileType = Directory}) _ = LT +fileSorter _ (FileInfo {fileType = Directory}) = GT +fileSorter (FileInfo {fileName = fa}) (FileInfo {fileName = fb}) = + compare (toLower <$> fa) (toLower <$> fb) + +listFilesInDir :: FilePath -> IO [FileInfo] +listFilesInDir dir = do + files <- listDirectory dir + sortBy fileSorter <$> forM files getFileInfo + +cacheDirPosition :: AppState -> AppState +cacheDirPosition appState@(AppState {stateListPositionCache, stateCwd, stateFiles}) = + appState + { stateListPositionCache = Map.insert stateCwd pos stateListPositionCache + } + where + pos = fromMaybe 0 $ L.listSelected stateFiles diff --git a/lib/Daffm/Types.hs b/lib/Daffm/Types.hs index 5462851..aaf6083 100644 --- a/lib/Daffm/Types.hs +++ b/lib/Daffm/Types.hs @@ -2,6 +2,7 @@ module Daffm.Types where import qualified Brick.Widgets.Edit as Editor import qualified Brick.Widgets.List as L +import qualified Data.Map as Map import System.Posix.Types (FileOffset) data FileType @@ -12,6 +13,7 @@ data FileType | Directory | SymbolicLink | UnixSocket + | UnknownFileType deriving (Show) data FileInfo = FileInfo @@ -28,8 +30,8 @@ data AppState = AppState { stateFiles :: L.List FocusTarget FileInfo, stateCmdlineEditor :: Editor.Editor String FocusTarget, stateFocusTarget :: FocusTarget, - -- stateFocusRing :: FocusRing FocusTarget, stateCwd :: FilePath, + stateListPositionCache :: Map.Map String Int, stateParentDir :: FilePath } deriving (Show) diff --git a/lib/Daffm/View.hs b/lib/Daffm/View.hs index db26a7d..2bfb7dc 100644 --- a/lib/Daffm/View.hs +++ b/lib/Daffm/View.hs @@ -1,7 +1,7 @@ module Daffm.View where import Brick.Types (Widget) -import Brick.Widgets.Core (Padding (Max, Pad), TextWidth (textWidth), hBox, hLimit, padLeft, padRight, str, vBox, vLimit, withAttr, (<+>)) +import Brick.Widgets.Core (Padding (Max, Pad), hBox, hLimit, padLeft, padRight, str, vBox, vLimit, withAttr, (<+>)) import Brick.Widgets.Edit (renderEditor) import qualified Brick.Widgets.List as L import Daffm.Attrs (directoryAttr, directorySelectedAttr, fileAttr, fileSelectedAttr) @@ -20,14 +20,14 @@ appView appState@(AppState {stateFiles, stateCwd}) = [ui] box :: Widget FocusTarget box = L.renderList fileItemView True stateFiles -fixedColumnsStr :: Int -> Widget n -> Widget n -fixedColumnsStr w s = hLimit w $ padRight Max s +hFixed :: Int -> Widget n -> Widget n +hFixed w = hLimit w . padRight Max fileItemView :: Bool -> FileInfo -> Widget FocusTarget fileItemView sel fileInfo@(FileInfo {fileSize, fileType}) = hBox - [ fixedColumnsStr 5 (fileTypeView fileType), - fixedColumnsStr 7 (fileSizeView fileSize), + [ hFixed 5 (fileTypeView fileType), + hFixed 7 (fileSizeView fileSize), fileNameView sel fileInfo ] where @@ -40,6 +40,7 @@ fileItemView sel fileInfo@(FileInfo {fileSize, fileType}) = showFileType CharacterDevice = "cdev" showFileType BlockDevice = "bdev" showFileType RegularFile = "file" + showFileType UnknownFileType = "?" fileNameView :: Bool -> FileInfo -> Widget FocusTarget fileNameView True (FileInfo {fileName, fileType = Directory}) = withAttr directorySelectedAttr $ str $ fileName <> "/" -- cgit v1.3.1