diff options
| author | Akshay Nair <phenax5@gmail.com> | 2025-10-04 18:07:30 +0530 |
|---|---|---|
| committer | Akshay Nair <phenax5@gmail.com> | 2025-10-04 19:10:06 +0530 |
| commit | a302dfc2aabda53446fb38e035e61ae91b28f84a (patch) | |
| tree | 67ca03f33ebf31e198ac5a883100b96cc387f264 | |
| parent | b05be850349dbb813d2af6f3ee7a2fc3bf98b8ef (diff) | |
| download | daffm-a302dfc2aabda53446fb38e035e61ae91b28f84a.tar.gz daffm-a302dfc2aabda53446fb38e035e61ae91b28f84a.zip | |
Add multi-key handling + command parsing + shell/shell! aliases
Diffstat (limited to '')
| -rw-r--r-- | daffm.cabal | 13 | ||||
| -rw-r--r-- | flake.nix | 5 | ||||
| -rw-r--r-- | justfile | 6 | ||||
| -rw-r--r-- | lib/Daffm/Action/Cmdline.hs | 62 | ||||
| -rw-r--r-- | lib/Daffm/Action/Commands.hs | 95 | ||||
| -rw-r--r-- | lib/Daffm/Action/Core.hs | 7 | ||||
| -rw-r--r-- | lib/Daffm/Event.hs | 68 | ||||
| -rw-r--r-- | lib/Daffm/State.hs | 29 | ||||
| -rw-r--r-- | lib/Daffm/Types.hs | 29 | ||||
| -rw-r--r-- | lib/Daffm/Utils.hs | 13 | ||||
| -rw-r--r-- | notes.org | 42 | ||||
| -rw-r--r-- | specs/Specs/FooSpec.hs | 75 |
12 files changed, 327 insertions, 117 deletions
diff --git a/daffm.cabal b/daffm.cabal index 379e91d..8b37971 100644 --- a/daffm.cabal +++ b/daffm.cabal @@ -27,8 +27,7 @@ common common-config build-depends: array, base, - brick <= 2.10, - containers, + brick == 2.4, data-default <= 0.8.0.1, directory <= 1.3.9.0, filepath <= 1.5.4.0, @@ -60,13 +59,15 @@ library lib-daffm hs-source-dirs: lib exposed-modules: Daffm - Daffm.View - Daffm.Types + Daffm.Action.Cmdline + Daffm.Action.Commands + Daffm.Action.Core Daffm.Attrs Daffm.Event Daffm.State - Daffm.Action.Core - Daffm.Action.Cmdline + Daffm.Types + Daffm.Utils + Daffm.View test-suite specs import: common-config, warnings @@ -30,10 +30,7 @@ pkg-config ]; - devPackages = with pkgs; [ - just - nodemon - ]; + devPackages = with pkgs; [just]; in { haskellProjects.default = { inherit projectRoot; @@ -6,9 +6,3 @@ run *args: test *args: cabal test {{args}} - -testw *args: - nodemon -e .hs -w lib -w specs --exec 'clear && just test {{args}}' - -build: - nix build diff --git a/lib/Daffm/Action/Cmdline.hs b/lib/Daffm/Action/Cmdline.hs index 12f7a9d..34e5e41 100644 --- a/lib/Daffm/Action/Cmdline.hs +++ b/lib/Daffm/Action/Cmdline.hs @@ -1,18 +1,18 @@ module Daffm.Action.Cmdline where -import Brick (suspendAndResume') import qualified Brick.Widgets.Edit as Editor -import qualified Brick.Widgets.List as L -import Control.Monad (unless, void) -import Control.Monad.State (get, gets, modify) -import Daffm.Action.Core (reloadDir) -import Daffm.Types (AppEvent, AppState (..), FileInfo (..), FocusTarget (..)) -import Data.Char (isSpace) -import qualified Data.Set as Set +import Control.Monad.State (gets, modify) +import qualified Control.Monad.State.Strict as StateStrict +import Daffm.Types +import Daffm.Utils (trimStart) import qualified Data.Text as Text import qualified Data.Text.Zipper as Z import qualified Data.Text.Zipper as Zipper -import System.Process (callCommand, callProcess) + +getCmdlineText :: AppEvent Text.Text +getCmdlineText = StateStrict.gets cmdtext + where + cmdtext = trimStart . Text.unlines . Editor.getEditContents . stateCmdlineEditor leaveCmdline :: AppEvent () leaveCmdline = clearCmdline >> modify (\st -> st {stateFocusTarget = FocusMain}) @@ -27,50 +27,6 @@ setCmdlineText text = clearCmdline :: AppEvent () clearCmdline = applyCmdlineEdit Z.clearZipper -runCmdline :: AppEvent () -runCmdline = do - cmd <- gets (trimCmd . Editor.getEditContents . stateCmdlineEditor) - evaluateCommand cmd - leaveCmdline - where - trimCmd = Text.dropWhile isSpace . Text.dropWhileEnd isSpace . Text.unlines - -evaluateCommand :: Text.Text -> AppEvent () -evaluateCommand (Text.splitAt 2 -> ("!!", cmd)) = do - cmd' <- Text.unpack <$> cmdSubstitutions cmd - suspendAndResume' $ do - callCommand cmd' - putStrLn "Press any key to continue" >> void getChar - reloadDir -evaluateCommand (Text.splitAt 1 -> ("!", cmd)) = do - cmd' <- Text.unpack <$> cmdSubstitutions cmd - suspendAndResume' $ callCommand cmd' - reloadDir -evaluateCommand "delete" = do - (AppState {stateFileSelections, stateFiles}) <- get - let files = - if Set.null stateFileSelections - then maybe [] ((: []) . filePath . snd) $ L.listSelectedElement stateFiles - else Set.elems stateFileSelections - unless (null files) $ do - suspendAndResume' $ callProcess "rm" ("-rfi" : map Text.unpack files) - reloadDir -evaluateCommand _cmd = pure () - -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 - -- TODO: Escaping % - let subst = - Text.replace "%" file - . Text.replace "%d" stateCwd - . Text.replace "%s" (Text.unwords $ map escape selections) - . Text.replace "%S" (Text.dropWhileEnd (== '\n') $ Text.unlines selections) - pure . subst $ cmd - applyCmdlineEdit :: (Zipper.TextZipper Text.Text -> Zipper.TextZipper Text.Text) -> AppEvent () applyCmdlineEdit zipper = do editor <- gets stateCmdlineEditor diff --git a/lib/Daffm/Action/Commands.hs b/lib/Daffm/Action/Commands.hs new file mode 100644 index 0000000..91e21b6 --- /dev/null +++ b/lib/Daffm/Action/Commands.hs @@ -0,0 +1,95 @@ +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} + +{-# HLINT ignore "Use for_" #-} +module Daffm.Action.Commands where + +import Brick (suspendAndResume') +import qualified Brick as M +import qualified Brick.Widgets.List as L +import Control.Monad (void) +import Control.Monad.State (get) +import Daffm.Action.Cmdline +import Daffm.Action.Core +import Daffm.Types +import Daffm.Utils (trimStart) +import Data.Bifunctor (Bifunctor (second)) +import Data.Char (isSpace) +import Data.Maybe (fromMaybe) +import qualified Data.Set as Set +import qualified Data.Text as Text +import System.Process (callCommand) + +parseCommand :: Text.Text -> Maybe Command +parseCommand (Text.splitAt 2 -> ("!!", cmd)) = Just $ CmdShell True cmd +parseCommand (Text.splitAt 1 -> ("!", cmd)) = Just $ CmdShell False cmd +parseCommand cmd = mkCmd . splitCmdArgs $ trimStart cmd + where + splitCmdArgs = second trimStart . Text.splitAt cmdEndIdx + cmdEndIdx = fromMaybe (Text.length cmd) $ Text.findIndex isSpace cmd + mkCmd = \case + ("q", _) -> Just CmdQuit + ("quit", _) -> Just CmdQuit + ("shell!", cmd') -> Just $ CmdShell True cmd' + ("shell", cmd') -> Just $ CmdShell False cmd' + ("back", _) -> Just CmdGoBack + ("open", _) -> Just CmdOpenSelection + ("reload", _) -> Just CmdReload + ("cd", dir) -> Just $ CmdChangeDir dir + ("noop", _) -> Just CmdNoop + ("cmdline-enter", _) -> Just CmdEnterCmdline + ("cmdline-leave", _) -> Just CmdLeaveCmdline + ("cmdline-set", txt) -> Just $ CmdSetCmdline txt + ("selection-toggle", _) -> Just CmdToggleSelection + ("selection-clear", _) -> Just CmdClearSelection + _ -> Nothing + +processCommand :: Command -> AppEvent () +processCommand (CmdShell True cmd) = do + cmd' <- Text.unpack <$> cmdSubstitutions cmd + suspendAndResume' $ do + callCommand cmd' + putStrLn "Press any key to continue" >> void getChar + reloadDir +processCommand (CmdShell False cmd) = do + cmd' <- Text.unpack <$> cmdSubstitutions cmd + suspendAndResume' $ callCommand cmd' + reloadDir +processCommand CmdQuit = M.halt +processCommand (CmdSetCmdline txt) = enterCmdline >> setCmdlineText txt +processCommand CmdEnterCmdline = enterCmdline +processCommand CmdLeaveCmdline = leaveCmdline +processCommand CmdOpenSelection = openSelectedFile +processCommand (CmdChangeDir dir) = changeDir dir +processCommand CmdReload = reloadDir +processCommand CmdToggleSelection = toggleCurrentFileSelection +processCommand CmdClearSelection = clearFileSelections +processCommand CmdGoBack = goBackToParentDir +processCommand CmdNoop = pure () + +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 "%" file + . Text.replace "%d" 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 + +runCmdline :: AppEvent () +runCmdline = do + cmd <- getCmdlineText + leaveCmdline + evaluateCommand cmd + +evaluateCommand :: Text.Text -> AppEvent () +evaluateCommand cmdtxt = + case parseCommand cmdtxt of + Just cmd -> processCommand cmd + Nothing -> pure () diff --git a/lib/Daffm/Action/Core.hs b/lib/Daffm/Action/Core.hs index 337801e..ce04073 100644 --- a/lib/Daffm/Action/Core.hs +++ b/lib/Daffm/Action/Core.hs @@ -32,10 +32,13 @@ goBackToParentDir = do dir <- gets stateParentDir loadDir dir (Text.pack . takeDirectory $ Text.unpack dir) +changeDir :: FilePathText -> AppEvent () +changeDir dir = do + loadDir dir (Text.pack $ takeDirectory $ Text.unpack dir) + goHome :: AppEvent () goHome = do - dir <- liftIO getHomeDirectory - loadDir (Text.pack dir) (Text.pack $ takeDirectory dir) + liftIO getHomeDirectory >>= changeDir . Text.pack openSelectedFile :: AppEvent () openSelectedFile = do diff --git a/lib/Daffm/Event.hs b/lib/Daffm/Event.hs index 247719b..262686b 100644 --- a/lib/Daffm/Event.hs +++ b/lib/Daffm/Event.hs @@ -1,42 +1,68 @@ module Daffm.Event where -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 (gets, modify) +import Control.Monad.State (get, gets, modify) import Daffm.Action.Cmdline -import Daffm.Action.Core +import Daffm.Action.Commands import Daffm.State (cacheDirPosition) -import Daffm.Types (AppEvent, AppState (..), FocusTarget (..)) +import Daffm.Types (AppEvent, AppState (..), Command (..), FocusTarget (..), Key, KeyMatchResult (..), KeySequence, Keymap) +import qualified Data.Map as Map +import Data.Maybe (fromMaybe) import qualified Graphics.Vty as V appEvent :: T.BrickEvent FocusTarget e -> AppEvent () appEvent brickevent@(T.VtyEvent event) = do focusTarget <- gets stateFocusTarget case (focusTarget, event) of - (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, V.EvKey (V.KChar '~') []) -> goHome - (FocusMain, V.EvKey (V.KChar ':') []) -> enterCmdline - (FocusMain, V.EvKey (V.KChar '!') []) -> setCmdlineText "!" >> enterCmdline - (FocusMain, V.EvKey (V.KChar 'q') []) -> M.halt - (FocusMain, V.EvKey (V.KChar 'r') [V.MCtrl]) -> reloadDir - (FocusMain, V.EvKey (V.KChar 'v') []) -> toggleCurrentFileSelection - (FocusMain, V.EvKey (V.KChar 'C') []) -> clearFileSelections - -- Just for testing - (FocusMain, V.EvKey (V.KChar 'p') [V.MCtrl]) -> evaluateCommand "!!chafa -f kitty %" (FocusCmdline, V.EvKey V.KEsc []) -> leaveCmdline (FocusCmdline, V.EvKey V.KEnter []) -> runCmdline - (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}) + (FocusMain, V.EvKey key []) -> do + appendToKeySequence key + processKeySequence >>= \case + MatchFailure -> do + files <- gets stateFiles + newFiles <- T.nestEventM' files (L.handleListEventVi L.handleListEvent event) + modify (\appState -> appState {stateFiles = newFiles}) + _ -> pure () + (FocusMain, _) -> do + files <- gets stateFiles + newFiles <- T.nestEventM' files (L.handleListEventVi L.handleListEvent event) + modify (\appState -> appState {stateFiles = newFiles}) modify cacheDirPosition appEvent _ = pure () + +matchKeySequence :: Keymap -> KeySequence -> KeyMatchResult +matchKeySequence keymaps keys + | Map.member keys keymaps = + MatchSuccess . fromMaybe CmdNoop $ Map.lookup keys keymaps + | otherwise = partial keymaps keys + where + partial _ [] = MatchFailure + partial (Map.null -> True) _ = MatchFailure + partial keymaps' keys' = if hasMatch then MatchPartial else MatchFailure + where + hasMatch = any (startsWith keys' . fst) (Map.toList keymaps') + startsWith ls1 ls2 = ls1 == take (length ls1) ls2 + +processKeySequence :: AppEvent KeyMatchResult +processKeySequence = do + (AppState {stateKeyMap, stateKeySequence}) <- get + let match = matchKeySequence stateKeyMap stateKeySequence + case match of + MatchSuccess cmd -> do + processCommand cmd + modify (\st -> st {stateKeySequence = []}) + MatchPartial -> pure () + MatchFailure -> do + modify (\st -> st {stateKeySequence = []}) + pure match + +appendToKeySequence :: Key -> AppEvent () +appendToKeySequence key = + modify (\st -> st {stateKeySequence = stateKeySequence st <> [key]}) diff --git a/lib/Daffm/State.hs b/lib/Daffm/State.hs index beeb646..f45a154 100644 --- a/lib/Daffm/State.hs +++ b/lib/Daffm/State.hs @@ -4,7 +4,7 @@ import qualified Brick.Widgets.Edit as Editor import qualified Brick.Widgets.List as L import Control.Applicative ((<|>)) import Control.Monad (filterM, forM) -import Daffm.Types (AppState (..), FileInfo (..), FilePathText, FileType (..), FocusTarget (..)) +import Daffm.Types (AppState (..), Command (..), FileInfo (..), FilePathText, FileType (..), FocusTarget (..)) import Data.List (findIndex, sortBy) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) @@ -12,6 +12,7 @@ import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.Text.Zipper.Generic as Zipper import qualified Data.Vector as Vec +import qualified Graphics.Vty as K import System.Directory (listDirectory, makeAbsolute, setCurrentDirectory) import System.PosixCompat (fileExist) import qualified System.PosixCompat as Posix @@ -28,8 +29,32 @@ mkEmptyAppState = stateListPositionCache = Map.empty, stateFileSelections = Set.empty, stateCwd = "", - stateParentDir = "" + stateParentDir = "", + stateKeyMap = defaultKeymaps, + stateKeySequence = [] } + where + defaultKeymaps = + Map.fromList + [ ([K.KChar 'q'], CmdQuit), + ([K.KChar 'r', K.KChar 'r'], CmdReload), + ([K.KChar '!'], CmdSetCmdline "!"), + ([K.KChar ':'], CmdEnterCmdline), + ([K.KChar 'l'], CmdOpenSelection), + ([K.KChar 'h'], CmdGoBack), + ([K.KEnter], CmdOpenSelection), + ([K.KBS], CmdGoBack), + ([K.KChar 'v'], CmdToggleSelection), + ([K.KChar '\t'], CmdToggleSelection), + ([K.KChar 'C'], CmdClearSelection), + ([K.KChar '~'], CmdChangeDir "/home/imsohexy"), + ([K.KChar 'g', K.KChar 'h'], CmdChangeDir "/home/imsohexy"), + ([K.KChar 'g', K.KChar 'd', K.KChar 'c'], CmdChangeDir "/home/imsohexy/Documents"), + ([K.KChar 'g', K.KChar 'd', K.KChar 'l'], CmdChangeDir "/home/imsohexy/Downloads"), + ([K.KChar 'g', K.KChar 'p'], CmdChangeDir "/home/imsohexy/Pictures"), + -- Just for testing + ([K.KChar 'p'], CmdShell True "chafa -f kitty %") + ] toggleSetItem :: (Ord a) => a -> Set.Set a -> Set.Set a toggleSetItem val set = diff --git a/lib/Daffm/Types.hs b/lib/Daffm/Types.hs index ffff4d0..953e3a8 100644 --- a/lib/Daffm/Types.hs +++ b/lib/Daffm/Types.hs @@ -6,6 +6,7 @@ import qualified Brick.Widgets.List as L import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as Text +import qualified Graphics.Vty as V import System.Posix.Types (FileMode, FileOffset) data FileType @@ -39,10 +40,36 @@ data AppState = AppState stateFocusTarget :: FocusTarget, stateCwd :: FilePathText, stateListPositionCache :: Map.Map Text.Text Int, - stateParentDir :: FilePathText + stateParentDir :: FilePathText, + stateKeySequence :: KeySequence, + stateKeyMap :: Keymap } deriving (Show) type AppEvent = EventM FocusTarget AppState type CmdlineEditor = Editor.Editor Text.Text FocusTarget + +data KeyMatchResult = MatchSuccess Command | MatchPartial | MatchFailure + deriving (Show, Eq) + +data Command + = CmdShell Bool Text.Text + | CmdQuit + | CmdSetCmdline Text.Text + | CmdEnterCmdline + | CmdLeaveCmdline + | CmdOpenSelection + | CmdChangeDir Text.Text + | CmdReload + | CmdToggleSelection + | CmdClearSelection + | CmdGoBack + | CmdNoop + deriving (Show, Eq) + +type Key = V.Key + +type Keymap = Map.Map [Key] Command + +type KeySequence = [Key] diff --git a/lib/Daffm/Utils.hs b/lib/Daffm/Utils.hs new file mode 100644 index 0000000..af4e0b2 --- /dev/null +++ b/lib/Daffm/Utils.hs @@ -0,0 +1,13 @@ +module Daffm.Utils where + +import Data.Char (isSpace) +import qualified Data.Text as Text + +trimStart :: Text.Text -> Text.Text +trimStart = Text.dropWhile isSpace + +trimEnd :: Text.Text -> Text.Text +trimEnd = Text.dropWhileEnd isSpace + +trim :: Text.Text -> Text.Text +trim = trimStart . trimEnd @@ -1,28 +1,34 @@ -** Current -- [X] Preserve cursor position per dir while navigating -- [X] Cmdline must be single line -- [X] Reload -- [X] Run shell command -- [X] Command substitution % -- [X] Command substitution %d:cwd -- [X] Show file permissions +** Essentials - [X] select multiple files +- [X] Support multikey bindings +- [X] Show file permissions +- [X] Run shell command +- [X] Reload +- [X] Preserve cursor position per dir while navigating +- [X] Command: shell & shell! (alias for !, !!) - [X] Command substitution %s:selections +- [X] Command substitution %d:cwd +- [X] Command substitution % +- [X] Command parsing +- [X] Cmdline must be single line +- [X] Internal commands? - [ ] Error handling -- [ ] Command parsing -- [ ] pipe command: Pipe selections/current to stdin of command -- [ ] Cli arg parsing -** Later -- [ ] handle on open (for external integrations) - [ ] Cmdline history +- [ ] parsing key sequence +- [ ] configuration file (toml?) +- [ ] Cli arg parsing (dir arg) +** Right after - [ ] cd into dir symlinks +- [ ] handle on open (for external integrations) - [ ] copy/paste across instances - user-land solution (write selections to file and read from second instance) - socket -- [ ] Internal commands -- [ ] configuration file (toml?) -- [ ] support multikey bindings? -- [ ] bind command: define keybindings -- [ ] watch for changes +- [ ] Command: pipe (think about this) (pipe selection file names/file contents) +- [ ] Allow escaping % in commands +- [ ] Rethink % substituions +** Later - [ ] cmdline tab completion - [ ] Allow escaping % in commands +- [ ] watch for changes +- [ ] bind command: define keybindings +- [ ] some caching for file list? diff --git a/specs/Specs/FooSpec.hs b/specs/Specs/FooSpec.hs index 295ef79..4c5f640 100644 --- a/specs/Specs/FooSpec.hs +++ b/specs/Specs/FooSpec.hs @@ -1,10 +1,77 @@ module Specs.FooSpec where +import Daffm.Action.Commands (parseCommand) +import Daffm.Event (matchKeySequence) +import Daffm.Types +import qualified Data.Map as Map +import qualified Graphics.Vty as K import Test.Hspec test :: SpecWith () test = do - describe "stuff" $ do - context "when things" $ do - it "does stuf" $ do - 1 `shouldBe` 1 + describe "matchKeySequence" $ do + context "when key sequence is empty" $ do + it "returns MatchFailure" $ do + let keymap = Map.fromList [([K.KChar 'a', K.KChar 'b', K.KChar 'c'], CmdNoop)] + matchKeySequence keymap [] `shouldBe` MatchFailure + + context "when keymap is empty" $ do + it "returns MatchFailure" $ do + matchKeySequence Map.empty [K.KChar 'a'] `shouldBe` MatchFailure + + context "when key sequence does not match any of the keymaps" $ do + it "returns MatchPartial" $ do + let keymap = + Map.fromList + [ ([K.KChar 'a', K.KChar 'b', K.KChar 'c'], CmdShell True "1"), + ([K.KChar 'a', K.KChar 'd', K.KChar 'e'], CmdShell True "2") + ] + let keys = [K.KChar 'a', K.KChar 'x'] + matchKeySequence keymap keys `shouldBe` MatchFailure + + context "when key sequence partially matches one of the keymaps" $ do + it "returns MatchPartial" $ do + let keymap = + Map.fromList + [ ([K.KChar 'a', K.KChar 'b', K.KChar 'c'], CmdShell True "1"), + ([K.KChar 'a', K.KChar 'd', K.KChar 'e'], CmdShell True "2") + ] + let keys = [K.KChar 'a', K.KChar 'b'] + matchKeySequence keymap keys `shouldBe` MatchPartial + + context "when key sequence matches with one of the keymaps" $ do + it "returns MatchPartial" $ do + let keymap = + Map.fromList + [ ([K.KChar 'a', K.KChar 'b', K.KChar 'c'], CmdShell True "1"), + ([K.KChar 'a', K.KChar 'd', K.KChar 'e'], CmdShell True "2") + ] + let keys = [K.KChar 'a', K.KChar 'd', K.KChar 'e'] + matchKeySequence keymap keys `shouldBe` MatchSuccess (CmdShell True "2") + + describe "parseCommand" $ do + context "when given an invalid command" $ do + it "returns Nothing" $ do + parseCommand "aklsdjijm" `shouldBe` Nothing + context "when empty command" $ do + it "returns Nothing" $ do + parseCommand "" `shouldBe` Nothing + + context "when command prefixed with !" $ do + it "parses as shell command without wait for key" $ do + parseCommand "!ls -la /" `shouldBe` Just (CmdShell False "ls -la /") + + context "when command prefixed with !!" $ do + it "parses as shell command with wait for key" $ do + parseCommand "!!ls -la /" `shouldBe` Just (CmdShell True "ls -la /") + + context "when given quit" $ do + it "parses correctly" $ do + parseCommand "quit invalid args" `shouldBe` Just CmdQuit + parseCommand "quit" `shouldBe` Just CmdQuit + + context "when given set-cmdline" $ do + it "parses correctly" $ do + parseCommand "cmdline-set hello" `shouldBe` Just (CmdSetCmdline "hello") + parseCommand "cmdline-set" `shouldBe` Just (CmdSetCmdline "") + parseCommand "cmdline-set somespaces " `shouldBe` Just (CmdSetCmdline "somespaces ") |
