packages feed

escoger (empty) → 0.1.0.0

raw patch · 11 files changed

+374/−0 lines, 11 filesdep +HUnitdep +basedep +bytestringsetup-changed

Dependencies added: HUnit, base, bytestring, criterion, escoger, mtl, test-framework, test-framework-hunit, unix, vector, vector-algorithms, vty

Files

+ Benchmarks/Bench.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}++import Escoger.Matches+import Criterion.Main+import qualified Data.ByteString.Char8 as BC+import qualified Data.Vector as V++main :: IO ()+main = do+  content <- BC.readFile "Benchmarks/Random.txt"+  let content' = V.fromList $ BC.split '\n' content+  defaultMain [+      bench "sortByScore" $ nf (sortByScore "aeu") content'+    ]
+ Exec/Main.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE OverloadedStrings #-}++import           Control.Monad.Reader+import           Control.Monad.State.Strict+import qualified Data.ByteString.Char8 as BC+import qualified Data.Vector as V+import           Escoger.Interactive+import           Escoger.Internal+import           Graphics.Vty+import           System.Posix.IO++main :: IO ()+main = do+  content <- BC.getContents+  let content' = (V.filter (not . BC.null) . V.fromList . BC.split '\n') content+  ifd <- openFd "/dev/tty" ReadOnly Nothing defaultFileFlags+  ofd <- openFd "/dev/tty" WriteOnly Nothing defaultFileFlags+  vty <- mkVty defaultConfig { inputFd = Just ifd, outputFd = Just ofd }+  selection <- flip evalStateT (SearchState content' "" 1) $+    flip runReaderT (SearchData content' vty) $+      interactiveLoop+  BC.putStrLn selection
+ LICENSE view
@@ -0,0 +1,20 @@+The MIT License (MIT)++Copyright (c) 2014 Travis Staton++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.
+ Lib/Escoger/Interactive.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}++module Escoger.Interactive (interactiveLoop) where++import           Control.Monad.Reader+import           Control.Monad.State.Strict+import           Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import           Data.Maybe (fromMaybe)+import qualified Data.Vector as V+import           Escoger.Internal+import           Escoger.Matches (sortByScore)+import           Escoger.Utils+import           Graphics.Vty+import           System.Exit (exitSuccess)++interactiveLoop :: SearchM ByteString+interactiveLoop = do+  render+  loop+  where+    loop :: SearchM ByteString+    loop = do+      vty <- asks _vty+      st <- get+      e <- liftIO $ nextEvent vty+      let result = handleKeyPress e st+      case result of+        Right selectionIndex -> do+          liftIO $ shutdown vty+          if selectionIndex > 0+          then do+            matches <- gets _matches+            return $ fromMaybe "" $ matches V.!? (selectionIndex - 1)+          else liftIO exitSuccess+        Left ss -> do+          updateMatches ss+          render+          loop++render :: SearchM ()+render = do+  vty <- asks _vty+  term <- gets _term+  index <- gets _index+  matches <- gets _matches+  let termLine = utf8Bytestring' defAttr $ mconcat ["> ", term]+      imatches = (V.map (formatMatch index) . V.zip [1..maxRows] . V.take maxRows) matches+      img = V.foldl' vertJoin termLine imatches+      pic = picForImage img+  liftIO $ update vty pic+  where+    formatMatch :: Index -> (Index,ByteString) -> Image+    formatMatch i (x,y) = if i == x+                            then utf8Bytestring' (defAttr `withBackColor` white `withForeColor` black) y+                            else utf8Bytestring' defAttr y++handleKeyPress :: Event -> SearchState -> Either SearchState Index+handleKeyPress e ss@(SearchState m t i) = do+  case e of+    (EvKey KBS []) -> Left $ ss { _term = safeInit t }+    (EvKey KDel []) -> Left $ ss { _term = safeInit t }+    (EvKey KUp []) -> Left $ ss { _index = safePred i }+    (EvKey KDown []) -> Left $ ss { _index = safeSucc' i }+    (EvKey KEnter []) -> Right i+    (EvKey (KChar 'p') [MCtrl]) -> Left $ ss { _index = safePred i }+    (EvKey (KChar 'n') [MCtrl]) -> Left $ ss { _index = safeSucc' i }+    (EvKey (KChar 'u') [MCtrl]) -> Left $ ss { _term = "" }+    (EvKey (KChar 'w') [MCtrl]) -> Left $ ss { _term = (BC.unwords . init . BC.words) t }+    (EvKey (KChar 'c') [MCtrl]) -> Right (-1)+    (EvKey (KChar c) []) -> Left $ ss { _index = 1, _term = BC.snoc t c }+    (EvKey KEsc []) -> Right (-1)+    _ -> Left ss+  where+    safeSucc' = safeSucc (V.length m)++updateMatches :: SearchState -> SearchM ()+updateMatches (SearchState _ t i) = do+  st <- get+  term <- gets _term+  if B.length t /= B.length term+    then do+      terms <- if BC.length term < BC.length t then gets _matches else asks _content+      put $ st { _index = i, _term = t, _matches = sortByScore t terms }+    else put $ st { _index = i }
+ Lib/Escoger/Internal.hs view
@@ -0,0 +1,23 @@+module Escoger.Internal where++import Control.Monad.State.Strict+import Control.Monad.Reader+import Data.ByteString (ByteString)+import Data.Vector (Vector)+import Graphics.Vty (Vty)++data SearchState = SearchState { _matches :: Vector ByteString+                               , _term    :: ByteString+                               , _index   :: Int+                               } deriving (Show)++data SearchData = SearchData { _content :: Vector ByteString+                             , _vty     :: Vty+                             }++type SearchM a = ReaderT SearchData (StateT SearchState IO) a++type Index = Int++maxRows :: Int+maxRows = 100
+ Lib/Escoger/Matches.hs view
@@ -0,0 +1,61 @@+module Escoger.Matches+( sortByScore+, score+, matchLength+, findEndOfMatch+) where++import           Control.Applicative+import           Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import           Data.Char (ord, chr)+import           Data.Maybe (mapMaybe)+import           Data.Ord (comparing)+import           Data.Vector (Vector)+import qualified Data.Vector as V+import qualified Data.Vector.Algorithms.Intro as VA+import           GHC.Unicode (isAsciiUpper)++sortByScore :: ByteString -> Vector ByteString -> Vector ByteString+sortByScore query choices = sortFilter choiceScores+  where+    sortFilter = V.map fst . V.modify (VA.sortBy $ comparing (negate . snd)) . V.filter (\(_,y) -> y /= 0)+    choiceScores = V.map (\x -> (x, score x query)) choices++score :: ByteString -> ByteString -> Double+score choice query+  | B.null query = 1+  | B.null choice = 0+  | otherwise = case matchLength c q of+    Nothing -> 0+    Just len -> let tmpScore = fromIntegral (B.length q) / fromIntegral len+                    choiceLength = fromIntegral $ B.length c+                in tmpScore / choiceLength+    where+      c = BC.map toAsciiLower choice+      q = BC.map toAsciiLower query++matchLength :: ByteString -> ByteString -> Maybe Int+matchLength choice query =+  if null matches then Nothing else Just $ minimum matches+  where+    matches :: [Int]+    matches = (filter (0<=) . mapMaybe findNorm) indices+    findNorm i = findEndOfMatch choice query i >>= normalize i+    normalize i m = return $ 1 + subtract i m+    indices :: [Int]+    indices = B.elemIndices (B.head query) choice++findEndOfMatch :: ByteString -> ByteString -> Int -> Maybe Int+findEndOfMatch choice query index =+  B.foldl' func (Just index) (B.tail query)+  where+    func m_lastIndex q = let findNextMatch i = B.findIndex (q ==) (B.drop (i+1) choice)+                             acc = ((+) <$> ((+1) <$> m_lastIndex) <*>)+                    in m_lastIndex >>= findNextMatch >>= acc . return++toAsciiLower :: Char -> Char+toAsciiLower c = if isAsciiUpper c+                   then chr (ord c + 32)+                   else c
+ Lib/Escoger/Utils.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE OverloadedStrings #-}++module Escoger.Utils where++import Control.Applicative+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BC+import Escoger.Internal (maxRows)++safeInit :: ByteString -> ByteString+safeInit "" = ""+safeInit bs = BC.init bs++safePred :: Int -> Int+safePred 1 = 1+safePred x = pred x++safeSucc :: Int -> Int -> Int+safeSucc x y = if c then y else succ y+  where c = or $ [(x==), (maxRows==)] <*> [y]
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ Tests/Test.hs view
@@ -0,0 +1,7 @@+import Test.Framework (defaultMain)+import Unit.Matches++tests = [unit_matches]++main :: IO ()+main = defaultMain tests
+ Tests/Unit/Matches.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}++module Unit.Matches (unit_matches) where++import qualified Data.Vector as V+import           Escoger.Matches+import           Test.Framework (defaultMain, testGroup)+import           Test.Framework.Providers.HUnit (testCase)+import           Test.HUnit++shorter_terms = assertEqual "favors shorter terms" correctOrder resultOrder+  where+    correctOrder = ["kite", "kitch", "kitten"]+    resultOrder = V.toList . sortByScore "kit" $ V.fromList ["kitten", "kitch", "kite"]++ordered_terms = assertEqual "ignores out of order terms" correctOrder resultOrder+  where+    correctOrder = ["ectwdaokxycaojxh"]+    resultOrder = V.toList . sortByScore "each" $ V.fromList ["aech", "ectwdaokxycaojxh"]++ignores_case = assertEqual "ignores case" correctOrder resultOrder+  where+    correctOrder = ["Each", "eachr"]+    resultOrder = V.toList . sortByScore "each" $ V.fromList ["eachr", "Each"]++unit_matches = testGroup "matches"+               [ testCase "favor shorter terms" shorter_terms+               , testCase "ignore out of order terms" ordered_terms+               , testCase "ignores case" ignores_case+               ]
+ escoger.cabal view
@@ -0,0 +1,88 @@+name:                escoger+version:             0.1.0.0+license:             MIT+license-file:        LICENSE+author:              Travis Staton+maintainer:          hello@travisstaton.com+bug-reports:         https://github.com/tstat/escoger/issues+synopsis:            Terminal fuzzy selector+description:         Interactive fuzzy selector for the terminal. Escoger accepts+                     a newline separated input on stdin, and provides an interactive+                     prompt to select one of these lines to output to stdout.+category:            Tools+build-type:          Simple+cabal-version:       2.0++Source-Repository head+    Type: git+    Location: https://github.com/tstat/escoger++library escoger-lib+  exposed-modules:     Escoger.Interactive+                       Escoger.Internal+                       Escoger.Matches+                       Escoger.Utils++  build-depends:         base >= 4.6 && < 5.0+                       , vty  >= 5.19 && <= 5.20+                       , unix >= 2.7 && <= 2.8+                       , bytestring+                       , vector+                       , vector-algorithms+                       , mtl++  hs-source-dirs:      Lib+  default-language:    Haskell2010+  ghc-options:         -Wall+                       -O2++executable escoger+  main-is:             Main.hs+  hs-source-dirs:      Exec++  build-depends:       base >=4.6 && <= 5.0+                       , escoger-lib+                       , vty+                       , unix+                       , bytestring+                       , vector+                       , mtl++  default-language:    Haskell2010+  ghc-options:         -Wall+                       -threaded+                       -rtsopts+                       -with-rtsopts=-N++benchmark bench+  type:                exitcode-stdio-1.0+  hs-source-dirs:      Exec, Benchmarks+  main-is:             Bench.hs+  build-depends:       base+                       , escoger-lib+                       , vty+                       , unix+                       , bytestring+                       , vector++  build-depends:       criterion++  default-language:    Haskell2010+  ghc-options:         -Wall+                       -threaded+                       -rtsopts+                       -with-rtsopts=-N++test-suite tests+  type:                exitcode-stdio-1.0+  hs-source-dirs:      Exec, Tests+  main-is:             Test.hs+  other-modules:       Unit.Matches+  build-depends:         base+                       , escoger-lib+                       , vector+                       , HUnit+                       , test-framework+                       , test-framework-hunit++  default-language:    Haskell2010