packages feed

fcd (empty) → 1.0.0.0

raw patch · 6 files changed

+330/−0 lines, 6 filesdep +arraydep +basedep +directorysetup-changed

Dependencies added: array, base, directory, fcd, process, tasty, tasty-hunit, text, unix, vty, vty-ui

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Benoît Faucon++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ fcd.cabal view
@@ -0,0 +1,41 @@+name:                fcd+version:             1.0.0.0+synopsis:            A faster way to navigate directories using the command line.+description:         Bookmark directories, then select a bookmark by fuzzy matching. You need to set up a small shell function for this to work properly (see the README).+license:             MIT+license-file:        LICENSE+author:              Benoît Faucon+maintainer:          faucon.benoit@cegetel.net+homepage:            https://github.com/Neki/fcd+category:            Utility+build-type:          Simple+cabal-version:       >=1.10+bug-reports:         https://github.com/Neki/fcd/issues++source-repository    head+  type: git+  location: git@github.com:Neki/fcd.git++executable fcd+  main-is:             Main.hs+  build-depends:       base, fcd+  hs-source-dirs:      src-exec+  default-language:    Haskell2010++library+  hs-source-dirs:      src+  default-language:    Haskell2010+  build-depends:       base >= 4.6 && < 5 , array, vty, vty-ui >= 1.7.1 && < 2, unix, directory, text, process >= 1.2.0.0+  exposed-modules:     Fcd+  other-modules:       Paths_fcd++test-suite test+  default-language:    Haskell2010+  type:                exitcode-stdio-1.0+  hs-source-dirs:      tests+  main-is:             Tests.hs+  build-depends:+    base >= 4.6 && < 5+    , tasty+    , tasty-hunit >= 0.9+    , fcd
+ src-exec/Main.hs view
@@ -0,0 +1,5 @@+module Main (main) where++import qualified Fcd+main :: IO ()+main = Fcd.run
+ src/Fcd.hs view
@@ -0,0 +1,231 @@+module Fcd (+    run+  , lcs+  , sortCandidates+) where++import Control.Monad+import Control.Concurrent.MVar+import Data.Array+import Data.List+import qualified Data.Text as T+import qualified Data.Text.IO as T.IO+import System.Directory+import System.Posix.Signals+import System.Environment (getArgs)++import Graphics.Vty.Widgets.List+import Graphics.Vty.Widgets.Text+import Graphics.Vty.Widgets.Edit+import Graphics.Vty.Widgets.Box+import Graphics.Vty.Attributes+import Graphics.Vty.Widgets.Core+import Graphics.Vty.Widgets.EventLoop+import Graphics.Vty.LLInput++import Paths_fcd (version)+import Data.Version++data Command = Add | List | PrintVersion | Help | Select | Delete deriving (Eq, Show)+data Args = Args { command :: Command, name :: String, shortcut :: String, description :: String, execute :: [String] -> IO ()}++availableCommands :: [Args]+availableCommands = [ Args { command = Add, name = "add", shortcut = "a", description = "Add new bookmarks. If no additional parameter is provided, bookmark the current directory.", execute = addBookmarks }+                    , Args { command = List, name = "list", shortcut = "l", description = "List the available bookmarks.", execute = const listBookmarks}+                    , Args { command = PrintVersion, name = "version", shortcut = "v", description = "Print version number.", execute = const printVersion}+                    , Args { command = Help, name = "help", shortcut = "h", description = "Print this help.", execute = const printHelp}+                    , Args { command = Select, name = "select", shortcut = "s", description = "Select a bookmark. This is equivalent to calling fcd without arguments.", execute = selectBookmark }+                    , Args { command = Delete, name = "delete", shortcut = "d", description = "Delete a bookmark.", execute = deleteBookmark }+                    ]++-- |Main entry point for the program+run :: IO ()+run = do+  args <- getArgs+  let (cmdName:opts) = if null args then ["s"] else args+  case parseCommand cmdName of+    Nothing -> error $ "Unrecognized command: " ++ cmdName ++ ".\nAvailable commands are: " ++ commandList+    Just cmd -> execute cmd opts++-- |Given a command name or a short-hand version of a command name, returns the+-- corresponding command+parseCommand :: String -> Maybe Args+parseCommand cmd = find (\arg -> name arg == cmd || shortcut arg == cmd) availableCommands++-- |Add each path in the provided list to the bookmarks file.+addBookmarks :: [String] -> IO ()+addBookmarks [] = addBookmarks ["."]+addBookmarks xs = do+  allBookmarks <- readBookmarks+  mapM_ (addBookmark allBookmarks . T.pack) xs++-- |Print to stdout the list of bookmarks.+listBookmarks :: IO ()+listBookmarks = liftM (T.unpack . T.intercalate (T.pack "\n")) readBookmarks >>= putStrLn++-- |Print to stdout the version number.+printVersion :: IO ()+printVersion = putStrLn $ "fcd version " ++ showVersion version++-- |Print to stdout a short help about the program.+printHelp :: IO ()+printHelp = do+  printVersion+  putStrLn "Usage: fcd [select [PATH]] | fcd add [PATH1 [...]] | fcd delete [PATH] | fcd list | fcd help | ..."+  putStrLn ""+  putStrLn "Available commands:"+  forM_ availableCommands $ \cmd ->+    putStrLn $ name cmd ++ " (" ++ shortcut cmd ++ ") -- " ++ description cmd+  putStrLn ""+  putStrLn "Each command can be abbreviated to its first letter."+  putStrLn "When in prompt mode, use C-h and C-l to navigate the result list."++-- |Prompt the user to select a bookmark, then write the selected bookmark in+-- the bookmark file.+selectBookmark :: [String] -> IO ()+selectBookmark opts = displayPrompt (T.pack $ unwords opts) >>= writeResult++-- |Prompt the user to select a bookmark, then delete the selected bookmark from+-- the bookmark file.+deleteBookmark :: [String] -> IO ()+deleteBookmark opts = do+  toDelete <- displayPrompt $ T.pack $ unwords opts+  path <- bookmarkFile+  bookmarks <- liftM T.lines (T.IO.readFile path)+  let filtered = filter (/= toDelete) bookmarks+  T.IO.writeFile path $ T.concat $ map (flip T.append $ T.pack "\n") filtered++-- |Return the path to the bookmark file.+bookmarkFile :: IO String+bookmarkFile = fmap (++ "/.fcdbookmarks") getHomeDirectory++-- |Return the path to the result file.+resultFile :: IO String+resultFile = fmap (++ "/.fcdresult") getHomeDirectory++-- |Return the list of the available commands (list, add, etc.)+commandList :: String+commandList = intercalate ", " $ map name availableCommands++-- |Add a bookmark (a directory path) to the bookmark file.+-- If the bookmark already exists, then do nothing.+addBookmark :: [T.Text] -> T.Text -> IO ()+addBookmark allBookmarks bookmark = do+  path <- bookmarkFile+  bookmarkClean <- canonicalizePath $ T.unpack bookmark+  unless (T.pack bookmarkClean `elem` allBookmarks) $+    appendFile path (bookmarkClean ++ "\n")++-- |Prompt the user to select a bookmark. The given parameter is used to+-- prepopulate the prompt. Return the user selection.+displayPrompt :: T.Text -> IO T.Text+displayPrompt prefill = do+  result <- newMVar $ T.pack "."+  inputLine <- editWidget+  setEditText inputLine prefill++  candidates <- readBookmarks+  candidatesList <- newTextList def_attr [] 1+  box <- vBox inputLine candidatesList++  mainFocusGroup <- newFocusGroup+  mainFocusGroup `onKeyPressed` \_ key modifiers ->+    if key == KASCII 'c' && modifiers == [MCtrl] then+      raiseSignal sigINT >> return True+    else if key == KASCII 'd' && modifiers == [MCtrl] then+      raiseSignal sigKILL >> return True+    else if key == KASCII 'h' && modifiers == [MCtrl] then+      moveSelectionDown candidatesList >> return True+    else if key == KASCII 'l' && modifiers == [MCtrl] then+      moveSelectionUp candidatesList >> return True+    else return False++  _ <- addToFocusGroup mainFocusGroup box++  collection <- newCollection+  _ <- addToCollection collection box mainFocusGroup++  inputLine `onChange` \inputText -> updateCandidates inputText candidates candidatesList++  inputLine `onActivate` \_ -> do+    choice <- getSelected candidatesList+    let selection = case choice of+                  Nothing -> T.pack "."+                  Just (_pos, (entry, _) ) -> entry+    modifyMVar_ result (const $ return selection)+    shutdownUi++  updateCandidates prefill candidates candidatesList++  runUi collection defaultContext+  takeMVar result++moveSelectionDown :: Widget (List T.Text FormattedText) -> IO ()+moveSelectionDown listWidget = do+  selected <- getSelected listWidget+  case selected of+    Nothing -> return ()+    Just (pos, _) -> unless (pos == 0) (setSelected listWidget (pos - 1))++moveSelectionUp :: Widget (List T.Text FormattedText) -> IO ()+moveSelectionUp listWidget = do+  selected <- getSelected listWidget+  case selected of+    Nothing -> return ()+    Just (pos, _) -> setSelected listWidget (pos + 1) -- vty-ui takes care of not going out of bounds++-- |Update a list widget by sorting the candidates list according to a reference+updateCandidates :: T.Text                              -- a reference (user provided input)+                  -> [T.Text]                           -- a list of candidates+                  -> Widget (List T.Text FormattedText) -- the list widget to update+                  -> IO ()+updateCandidates inputText candidates candidatesList = do+  let sortedCandidates = sortCandidates (map T.unpack candidates) (T.unpack inputText)+      displayedCandidates = map T.pack sortedCandidates+  setCandidates displayedCandidates candidatesList++-- |Write the selected bookmark to the result file+writeResult :: T.Text -> IO ()+writeResult selection = resultFile >>= flip T.IO.writeFile selection++-- |Retrieve the bookmark list from disk+readBookmarks :: IO [T.Text]+readBookmarks = do+  path <- bookmarkFile+  exist <-  doesFileExist path+  if exist+  then liftM T.lines $ T.IO.readFile path+  else return []++setCandidates :: [T.Text] -> Widget (List T.Text FormattedText) -> IO ()+setCandidates candidates list = do+  clearList list+  forM_ candidates (\el -> addToList list el =<< plainText el)++-- |Compute the length of the longest common subsequence of two lists.+-- This is a simple implementation with memoization that uses quadratic space+-- (O(n*m) where n and m are the length of the inputs).+-- Note: a possible optimization would be to cache the memoized matrix as a lot+-- of it is still relevant when a new character is added.+lcs :: (Eq a) => [a] -> [a] -> Int+lcs xs ys = memoized ! (n,m)+  where memoized = array ((0,0),(n,m)) [((i,j), lcs' i j) | i <- [0..n], j <- [0..m] ]+        n = length xs+        m = length ys+        as = listArray (1, n) xs+        bs = listArray (1, m) ys+        lcs' _ 0 = 0+        lcs' 0 _ = 0+        lcs' u v = if as ! u == bs ! v+                   then memoized ! (u - 1, v - 1) + 1+                   else max (memoized ! (u - 1, v)) (memoized ! (u, v - 1))++sortCandidates :: (Eq a) => [[a]] -> [a] -> [[a]]+sortCandidates candidates reference = sortBy comparator candidates+  where comparator x y =+         let distRefToX = distance x reference+             distRefToY = distance y reference+             distance a b  = - lcs a b+         in compare distRefToX distRefToY++
+ tests/Tests.hs view
@@ -0,0 +1,31 @@+module Main (main) where++import Test.Tasty+import Test.Tasty.HUnit++import Fcd++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests" [unitTests]++unitTests :: TestTree+unitTests = testGroup "Unit tests" [distanceTests]++distanceTests :: TestTree+distanceTests = testGroup "Longest common subsequence tests"+  [ testCase "Some LCS" $ do+      0 @=? lcs "" "42"+      0 @=? lcs "abc" "def"+      1 @=? lcs "ab" "b"+      1 @=? lcs "ab" "ba"+      4 @=? lcs "hello" "hullo"+      5 @=? lcs "my word" "their words"+      4 @=? lcs "abcdef" "acedf"+  , testCase "Sort candidates according to the distance to a reference" $+      ["abcd", "ab", "efg"] @=? sortCandidates ["ab", "abcd", "efg"] "abdc"+  ]++