From a3cc21ebb749c1e5071d857990c6aaed0d182840 Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Fri, 24 Oct 2025 00:41:56 +0530 Subject: Add move command and gj gk keys --- lib/Daffm/Action/Commands.hs | 27 +++++++-- lib/Daffm/Action/Core.hs | 134 +++++++++++++++++++++---------------------- 2 files changed, 87 insertions(+), 74 deletions(-) (limited to 'lib/Daffm/Action') diff --git a/lib/Daffm/Action/Commands.hs b/lib/Daffm/Action/Commands.hs index a6a55e6..669ff3c 100644 --- a/lib/Daffm/Action/Commands.hs +++ b/lib/Daffm/Action/Commands.hs @@ -4,9 +4,10 @@ module Daffm.Action.Commands where import qualified Brick as M +import qualified Brick.Widgets.List as L import Control.Monad (forM_) import Control.Monad.IO.Class (MonadIO (liftIO)) -import Control.Monad.State (modify) +import Control.Monad.State (gets, modify) import Daffm.Action.Cmdline import Daffm.Action.Core import Daffm.Keymap (parseKeySequence) @@ -27,8 +28,9 @@ runCmdline = do evaluateCommand cmd parseCommand :: Text.Text -> Maybe Command -parseCommand (Text.splitAt 2 -> ("!!", cmd)) = Just $ CmdShell True cmd -parseCommand (Text.splitAt 1 -> ("!", cmd)) = Just $ CmdShell False cmd +parseCommand (Text.stripPrefix "!!" -> Just cmd) = Just $ CmdShell True cmd +parseCommand (Text.stripPrefix "!" -> Just cmd) = Just $ CmdShell False cmd +parseCommand (Text.stripPrefix "/" -> Just term) = Just $ CmdSearch $ trim term parseCommand cmd = mkCmd . splitCmdArgs $ trimStart cmd where splitCmdArgs = second trimStart . Text.break isSpace @@ -37,7 +39,7 @@ parseCommand cmd = mkCmd . splitCmdArgs $ trimStart cmd ("quit", _) -> Just CmdQuit ("shell!", cmd') -> Just $ CmdShell True cmd' ("shell", cmd') -> Just $ CmdShell False cmd' - ("command-shell", cmd') -> Just $ CmdCommandShell cmd' + ("eval", cmd') -> Just $ CmdCommandShell cmd' ("back", _) -> Just CmdGoBack ("open", _) -> Just CmdOpenSelection ("reload", _) -> Just CmdReload @@ -51,6 +53,10 @@ parseCommand cmd = mkCmd . splitCmdArgs $ trimStart cmd ("search", term) -> Just $ CmdSearch $ trim term ("search-next", _) -> Just $ CmdSearchNext 1 ("search-prev", _) -> Just $ CmdSearchNext (-1) + ("move", Text.stripPrefix "$" -> Just _) -> Just $ CmdMove MoveToEnd + ("move", Text.stripPrefix "+" -> Just inc) -> Just . CmdMove . MoveDown . read $ Text.unpack inc + ("move", Text.stripPrefix "-" -> Just inc) -> Just . CmdMove . MoveUp . read $ Text.unpack inc + ("move", pos) -> Just . CmdMove . MoveTo . read $ Text.unpack pos ("map", Text.break isSpace -> (keysraw, cmdraw)) -> do keys <- parseKeySequence keysraw cmd' <- parseCommand $ trimStart cmdraw @@ -95,7 +101,18 @@ processCommand CmdGoBack = goBackToParentDir processCommand (CmdChain chain) = forM_ chain processCommand processCommand (CmdSearch term) = setSearchTerm term >> applySearch >> nextSearchMatch processCommand (CmdSearchNext change) = updateSearchIndex (+ change) >> nextSearchMatch -processCommand (CmdKeymapSet keys command) = modify $ \s -> s {stateKeyMap = Map.insert keys command $ stateKeyMap s} +processCommand (CmdKeymapSet keys command) = + modify $ \st -> st {stateKeyMap = Map.insert keys command $ stateKeyMap st} +processCommand (CmdMove move) = moveCursor $ toUpdater move + where + toUpdater MoveToEnd = L.listMoveToEnd + toUpdater (MoveTo pos) = L.listMoveTo pos + toUpdater (MoveUp inc) = L.listMoveBy $ - inc + toUpdater (MoveDown inc) = L.listMoveBy inc + moveCursor :: (L.List FocusTarget FileInfo -> L.List FocusTarget FileInfo) -> AppEvent () + moveCursor updater = do + files <- gets $ updater . stateFiles + modify $ \st -> st {stateFiles = files} processCommand CmdNoop = pure () evaluateCommand :: Text.Text -> AppEvent () diff --git a/lib/Daffm/Action/Core.hs b/lib/Daffm/Action/Core.hs index 72bccef..dc68c5d 100644 --- a/lib/Daffm/Action/Core.hs +++ b/lib/Daffm/Action/Core.hs @@ -25,24 +25,21 @@ modifyM f = get >>= f >>= put loadDir :: FilePathText -> AppEvent () loadDir dir = do modifyM (liftIO . (>>= filterInvalidSelections) . loadDirToState dir) - applySearch + applySearch -- Apply search after loading dir to update match indexes reloadDir :: AppEvent () -reloadDir = do - AppState {stateCwd} <- get - loadDir stateCwd +reloadDir = gets stateCwd >>= loadDir goBackToParentDir :: AppEvent () goBackToParentDir = do - dir <- gets (Text.pack . takeDirectory . Text.unpack . stateCwd) - loadDir dir + parentDir <- gets (Text.pack . takeDirectory . Text.unpack . stateCwd) + loadDir parentDir changeDir :: FilePathText -> AppEvent () changeDir = loadDir goHome :: AppEvent () -goHome = do - liftIO getHomeDirectory >>= changeDir . Text.pack +goHome = liftIO getHomeDirectory >>= changeDir . Text.pack openSelectedFile :: AppEvent () openSelectedFile = do @@ -54,99 +51,98 @@ openSelectedFile = do cmdSubstitutions opener >>= suspendAndRunShellCommand False Nothing -> pure () -shellCommand :: String -> IO Proc.ExitCode -shellCommand cmd = do - Proc.withCreateProcess - (Proc.shell cmd) {Proc.delegate_ctlc = True} - $ \_ _ _ p -> Proc.waitForProcess p - cmdSubstitutions :: Text.Text -> AppEvent Text.Text -cmdSubstitutions cmd = do - (AppState {stateFiles, stateCwd, stateFileSelections}) <- get - let file = maybe "" (filePath . snd) . L.listSelectedElement $ stateFiles - let escape = (\s -> "'" <> s <> "'") . Text.replace "'" "\\'" - let selections = Set.elems stateFileSelections - let selectionsOrCurrent = if Set.null stateFileSelections then [file] else selections - let subst = - Text.replace "%" (escape file) - . Text.replace "%d" (escape stateCwd) - . Text.replace "%s" (Text.unwords $ map escape selections) - . Text.replace "%S" (Text.dropWhileEnd (== '\n') $ Text.unlines selections) - . Text.replace "%f" (Text.unwords $ map escape selectionsOrCurrent) - . Text.replace "%F" (Text.dropWhileEnd (== '\n') $ Text.unlines selectionsOrCurrent) - pure . subst $ cmd +cmdSubstitutions cmd = gets (`substitute` cmd) + where + escape = (\s -> "'" <> s <> "'") . Text.replace "'" "\\'" + substitute (AppState {stateFiles, stateCwd, stateFileSelections}) = + Text.replace "%" (escape cursorFile) + . Text.replace "%d" (escape stateCwd) + . Text.replace "%s" (Text.unwords $ map escape selections) + . Text.replace "%S" (Text.dropWhileEnd (== '\n') $ Text.unlines selections) + . Text.replace "%f" (Text.unwords $ map escape selectionsOrCursor) + . Text.replace "%F" (Text.dropWhileEnd (== '\n') $ Text.unlines selectionsOrCursor) + where + cursorFile = maybe "" (filePath . snd) . L.listSelectedElement $ stateFiles + selections = Set.elems stateFileSelections + selectionsOrCursor = if Set.null stateFileSelections then [cursorFile] else selections -- Suspend tui and run shell command -- When waitForKey is true, it will prompt for a key press on success -- When exit code is non-zero, it will print it and prompt for key press regardless of waitForKey suspendAndRunShellCommand :: Bool -> Text.Text -> AppEvent () suspendAndRunShellCommand waitForKey cmd = do - suspendAndResume' $ do - exitCode <- shellCommand $ Text.unpack cmd - case exitCode of + suspendAndResume' $ + shellCommand (Text.unpack cmd) >>= \case Proc.ExitFailure code -> do putStrLn $ "Process exited with " <> show code putStrLn "Press any key to continue" >> void getChar _ | waitForKey -> putStrLn "Press any key to continue" >> void getChar _ -> pure () +shellCommand :: String -> IO Proc.ExitCode +shellCommand cmd = do + Proc.withCreateProcess + (Proc.shell cmd) {Proc.delegate_ctlc = True} + $ \_ _ _ p -> Proc.waitForProcess p + currentFile :: AppEvent (Maybe FileInfo) -currentFile = do - gets (fmap snd . L.listSelectedElement . stateFiles) +currentFile = gets (fmap snd . L.listSelectedElement . stateFiles) toggleCurrentFileSelection :: AppEvent () toggleCurrentFileSelection = do - currentFile >>= maybe (pure ()) (modify . toggleFileSelection . filePath) + currentFile >>= \case + Just fileInfo -> modify . toggleFileSelection . filePath $ fileInfo + Nothing -> pure () moveCurrent 1 clearFileSelections :: AppEvent () clearFileSelections = - modify $ \s -> s {stateFileSelections = Set.empty} + modify $ \st -> st {stateFileSelections = Set.empty} moveCurrent :: Int -> AppEvent () -moveCurrent count = do - files <- gets stateFiles - modify $ \s -> s {stateFiles = L.listMoveBy count files} +moveCurrent count = + modify $ \st -> st {stateFiles = L.listMoveBy count $ stateFiles st} setSearchTerm :: Text.Text -> AppEvent () -setSearchTerm "" = modify (\st -> st {stateSearchTerm = Nothing, stateSearchIndex = 0}) -setSearchTerm term = modify (\st -> st {stateSearchTerm = Just term, stateSearchIndex = 0}) +setSearchTerm "" = modify $ \st -> st {stateSearchTerm = Nothing, stateSearchIndex = 0} +setSearchTerm term = modify $ \st -> st {stateSearchTerm = Just term, stateSearchIndex = 0} applySearch :: AppEvent () -applySearch = get >>= apply +applySearch = get >>= search where - apply :: AppState -> AppEvent () - apply (AppState {stateSearchTerm = Nothing}) = - modify - (\st -> st {stateSearchMatches = Vec.empty, stateSearchIndex = 0}) - apply (AppState {stateSearchTerm = Just term, stateFiles}) = do - let search (_, FileInfo {fileName}) = Text.toLower term `Text.isInfixOf` Text.toLower fileName - let matches = Vec.map fst . Vec.filter search . Vec.indexed $ L.listElements stateFiles - modify - ( \st -> - st - { stateSearchMatches = matches, - stateSearchIndex = wrapSearchIndex st (stateSearchIndex st) - } - ) + search :: AppState -> AppEvent () + search (AppState {stateSearchTerm = Nothing}) = + modify $ \st -> st {stateSearchMatches = Vec.empty, stateSearchIndex = 0} + search (AppState {stateSearchTerm = Just term, stateFiles}) = + modify $ + \st -> + st + { stateSearchMatches = searchFiles stateFiles, + stateSearchIndex = wrapSearchIndex st $ stateSearchIndex st + } + where + isAMatch (FileInfo {fileName}) = Text.toLower term `Text.isInfixOf` Text.toLower fileName + searchFiles = Vec.map fst . Vec.filter (isAMatch . snd) . Vec.indexed . L.listElements nextSearchMatch :: AppEvent () -nextSearchMatch = do - st@(AppState {stateSearchMatches, stateFiles, stateSearchIndex}) <- get - let nextFiles = - if Vec.null stateSearchMatches - then stateFiles - else L.listMoveTo (stateSearchMatches Vec.! wrapSearchIndex st stateSearchIndex) stateFiles - modify (\st' -> st' {stateFiles = nextFiles}) +nextSearchMatch = + modify (\st -> st {stateFiles = forwardSearch st}) + where + forwardSearch st@(AppState {stateSearchMatches, stateFiles, stateSearchIndex}) = + if Vec.null stateSearchMatches + then stateFiles + else L.listMoveTo (stateSearchMatches Vec.! wrapSearchIndex st stateSearchIndex) stateFiles wrapSearchIndex :: AppState -> Int -> Int wrapSearchIndex (AppState {stateSearchMatches}) nextIndex = - let matchCount = length stateSearchMatches - in if - | nextIndex < 0 -> matchCount - 1 - | nextIndex >= matchCount && matchCount /= 0 -> nextIndex `mod` matchCount - | otherwise -> nextIndex + if + | nextIndex < 0 -> matchCount - 1 + | nextIndex >= matchCount && matchCount /= 0 -> nextIndex `mod` matchCount + | otherwise -> nextIndex + where + matchCount = length stateSearchMatches updateSearchIndex :: (Int -> Int) -> AppEvent () -updateSearchIndex upd = - modify (\st -> st {stateSearchIndex = wrapSearchIndex st $ upd $ stateSearchIndex st}) +updateSearchIndex update = + modify $ \st -> st {stateSearchIndex = wrapSearchIndex st $ update $ stateSearchIndex st} -- cgit v1.3.1