bhoogle (empty) → 0.1.1.1
raw patch · 5 files changed
+446/−0 lines, 5 filesdep +basedep +brickdep +bytestringsetup-changed
Dependencies added: base, brick, bytestring, containers, directory, filepath, hoogle, lens, process, protolude, safe-exceptions, text, time, vector, vty
Files
- LICENSE +30/−0
- README.md +26/−0
- Setup.hs +2/−0
- app/Main.hs +349/−0
- bhoogle.cabal +39/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Andre Van Der Merwe (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,26 @@+# Hoogle terminal GUI.++**bhoogle** is a simple terminal GUI wrapper over [hoogle](https://hackage.haskell.org/package/hoogle). ++++++## Setup+ - Make sure you have a local hoogle database created+ - If you don't already, then+ 1. Install hoogle (e.g. ```stack install hoogle``` or ```cabal install hoogle```)+ 1. Generate the default database (```hoogle generate```)++## Usage+ 1. Enter a search in the "type" edit box+ 1. Press enter to search: focus goes directly to the results list+ 1. Or press tab to search and focus will go to the "text" edit box+ 1. You can then filter the results by typing in the "text" edit box, any result containing the sub-string typed will be shown+ 1. Navigate the results by using arrow or vi (hjkl) keys+ 1. Pressing **'s'** in the results list will toggle the sort order+ 1. Escape to exit+ 1. Search-ahead is enable for any type search longer than ~3 characters+++Note that the version described in the [blog](http://www.andrevdm.com/posts/2018-01-15-bhoogle.html) is on the [blog](https://github.com/andrevdm/bhoogle/tree/blog) branch.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,349 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Main where++import Protolude+import Control.Lens ((^.), (.~), (%~))+import Control.Lens.TH (makeLenses)+import qualified Data.List as Lst+import qualified Data.Time as Tm+import qualified Data.Text as Txt+import qualified Data.Vector as Vec+import Brick ((<+>), (<=>))+import qualified Brick as B+import qualified Brick.BChan as BCh+import qualified Brick.Focus as BF+import qualified Brick.AttrMap as BA+import qualified Brick.Widgets.List as BL+import qualified Brick.Widgets.Edit as BE+import qualified Brick.Widgets.Border as BB+import qualified Brick.Widgets.Border.Style as BBS+import Control.Concurrent (threadDelay, forkIO)+import qualified Graphics.Vty as V+import qualified Graphics.Vty.Input.Events as K+import qualified Hoogle as H++++-- | Events that can be sent+-- | Here there is just one event for updating the time+newtype Event = EventUpdateTime Tm.LocalTime++-- | Names use to identify each of the controls+data Name = TypeSearch+ | TextSearch+ | ListResults+ deriving (Show, Eq, Ord)++-- | Sort order+data SortBy = SortNone+ | SortAsc+ | SortDec+ deriving (Eq)+++-- | State of the brick app. Contains the controls and any other required state+data BrickState = BrickState { _stEditType :: !(BE.Editor Text Name) -- ^ Editor for the type to search for+ , _stEditText :: !(BE.Editor Text Name) -- ^ Editor for a text search in the results+ , _stTime :: !Tm.LocalTime -- ^ The current time+ , _stFocus :: !(BF.FocusRing Name) -- ^ Focus ring - a circular list of focusable controls+ , _stResults :: [H.Target] -- ^ The last set of search results from hoohle+ , _stResultsList :: !(BL.List Name H.Target) -- ^ List for the search results+ , _stSortResults :: SortBy -- ^ Current sort order for the results+ }++makeLenses ''BrickState+++-- | Defines how the brick application will work / handle events+app :: B.App BrickState Event Name+app = B.App { B.appDraw = drawUI+ , B.appChooseCursor = B.showFirstCursor+ , B.appHandleEvent = handleEvent+ , B.appStartEvent = pure+ , B.appAttrMap = const theMap+ }+++main :: IO ()+main = do+ chan <- BCh.newBChan 5 -- ^ create a bounded channel for events++ -- Send a tick event every 1 seconds with the current time+ -- Brick will send this to our event handler which can then update the stTime field+ void . forkIO $ forever $ do+ t <- getTime + BCh.writeBChan chan $ EventUpdateTime t+ threadDelay $ 1 * 1000000++ -- Initial current time value+ t <- getTime++ -- Construct the initial state values+ let st = BrickState { _stEditType = BE.editor TypeSearch (Just 1) ""+ , _stEditText = BE.editor TextSearch (Just 1) ""+ , _stResultsList = BL.list ListResults Vec.empty 1+ , _stTime = t+ , _stFocus = BF.focusRing [TypeSearch, TextSearch, ListResults]+ , _stResults = []+ , _stSortResults = SortNone+ }+ + -- And run brick+ void $ B.customMain (V.mkVty V.defaultConfig) (Just chan) app st++ where+ -- | Get the local time+ getTime = do+ t <- Tm.getCurrentTime+ tz <- Tm.getCurrentTimeZone+ pure $ Tm.utcToLocalTime tz t+++-- | Main even handler for brick events+handleEvent :: BrickState -> B.BrickEvent Name Event -> B.EventM Name (B.Next BrickState)+handleEvent st ev =+ case ev of+ -- Handle keyboard events+ -- k is the key+ -- ms are the modifier keys+ (B.VtyEvent ve@(V.EvKey k ms)) ->+ case (k, ms) of+ -- Escape quits the app, no matter what control has focus+ (K.KEsc, []) -> B.halt st++ _ ->+ -- How to interpret the key press depends on which control is focused+ case BF.focusGetCurrent $ st ^. stFocus of+ Just TypeSearch ->+ case k of+ K.KChar '\t' -> do+ -- Search, clear sort order, focus next+ found <- doSearch st+ B.continue . filterResults $ st & stFocus %~ BF.focusNext+ & stResults .~ found+ & stSortResults .~ SortNone++ K.KBackTab ->do+ -- Search, clear sort order, focus prev+ found <- doSearch st+ B.continue . filterResults $ st & stFocus %~ BF.focusPrev+ & stResults .~ found+ & stSortResults .~ SortNone++ K.KEnter -> do+ -- Search, clear sort order, focus on results+ -- This makes it faster if you want to search and navigate results without tabing through the text search box+ found <- doSearch st+ B.continue . filterResults $ st & stResults .~ found+ & stSortResults .~ SortNone+ & stFocus %~ BF.focusNext & stFocus %~ BF.focusNext+ -- TODO with brick >= 0.33, rather than 2x focus next: & stFocus %~ BF.focusSetCurrent ListResults++ _ -> do+ -- Let the editor handle all other events+ r <- BE.handleEditorEvent ve $ st ^. stEditType+ next <- liftIO . searchAhead doSearch $ st & stEditType .~ r + B.continue next+++ Just TextSearch ->+ case k of+ K.KChar '\t' -> B.continue $ st & stFocus %~ BF.focusNext -- Focus next+ K.KBackTab -> B.continue $ st & stFocus %~ BF.focusPrev -- Focus previous+ _ -> do+ -- Let the editor handle all other events+ r <- BE.handleEditorEvent ve $ st ^. stEditText+ B.continue . filterResults $ st & stEditText .~ r++ Just ListResults ->+ case k of+ K.KChar '\t' -> B.continue $ st & stFocus %~ BF.focusNext -- Focus next+ K.KBackTab -> B.continue $ st & stFocus %~ BF.focusPrev -- Focus previous+ K.KChar 's' ->+ -- Toggle the search order between ascending and descending, use asc if sort order was 'none'+ let sortDir = if (st ^. stSortResults) == SortAsc then SortDec else SortAsc in+ let sorter = if sortDir == SortDec then (Lst.sortBy $ flip compareType) else (Lst.sortBy compareType) in+ B.continue . filterResults $ st & stResults %~ sorter+ & stSortResults .~ sortDir++ _ -> do+ -- Let the list handle all other events+ -- Using handleListEventVi which adds vi-style keybindings for navigation+ -- and the standard handleListEvent as a fallback for all other events+ r <- BL.handleListEventVi BL.handleListEvent ve $ st ^. stResultsList+ B.continue $ st & stResultsList .~ r++ _ -> B.continue st++ (B.AppEvent (EventUpdateTime time)) ->+ -- Update the time in the state+ B.continue $ st & stTime .~ time+ + _ -> B.continue st++ where+ doSearch st' = + liftIO $ searchHoogle (Txt.strip . Txt.concat $ BE.getEditContents (st' ^. stEditType))+++-- | Search ahead for type strings longer than 3 chars.+searchAhead :: (BrickState -> IO [H.Target]) -> BrickState -> IO BrickState+searchAhead search st =+ let searchText = Txt.strip . Txt.concat . BE.getEditContents $ st ^. stEditType in++ if Txt.length (Txt.filter (`notElem` [' ', '\t', '(', ')', '=']) searchText) > 3+ then do+ -- Search+ found <- search st+ pure . filterResults $ st & stResults .~ found+ & stSortResults .~ SortNone+ else+ -- Just clear+ pure $ st & stResults .~ []+ & stResultsList %~ BL.listClear+++-- | Filter the results from hoogle using the search text+filterResults :: BrickState -> BrickState+filterResults st =+ let allResults = st ^. stResults in+ let filterText = Txt.toLower . Txt.strip . Txt.concat . BE.getEditContents $ st ^. stEditText in++ let results =+ if Txt.null filterText+ then allResults+ else filter (\t -> Txt.isInfixOf filterText . Txt.toLower $ formatResult t) allResults+ in+ st & stResultsList .~ BL.list ListResults (Vec.fromList results) 1+ ++-- | Draw the UI+drawUI :: BrickState -> [B.Widget Name]+drawUI st =+ [B.padAll 1 contentBlock] ++ where+ contentBlock =+ (B.withBorderStyle BBS.unicode $ BB.border searchBlock)+ <=>+ B.padTop (B.Pad 1) resultsBlock+ + resultsBlock =+ let total = show . length $ st ^. stResults in+ let showing = show . length $ st ^. stResultsList ^. BL.listElementsL in+ (B.withAttr "infoTitle" $ B.txt "Results: ") <+> B.txt (showing <> "/" <> total)+ <=>+ (B.padTop (B.Pad 1) $+ resultsContent <+> resultsDetail+ )++ resultsContent =+ BL.renderList (\_ e -> B.txt $ formatResult e) False (st ^. stResultsList)++ resultsDetail =+ B.padLeft (B.Pad 1) $+ B.hLimit 60 $+ vtitle "package:"+ <=>+ B.padLeft (B.Pad 2) (B.txt $ getSelectedDetail (\t -> maybe "" (Txt.pack . fst) (H.targetPackage t)))+ <=>+ vtitle "module:"+ <=>+ B.padLeft (B.Pad 2) (B.txt $ getSelectedDetail (\t -> maybe "" (Txt.pack . fst) (H.targetModule t)))+ <=>+ vtitle "docs:"+ <=>+ B.padLeft (B.Pad 2) (B.txtWrap . reflow $ getSelectedDetail (Txt.pack . clean . H.targetDocs))+ <=>+ B.fill ' '+ + searchBlock =+ ((htitle "Type: " <+> editor TypeSearch (st ^. stEditType)) <+> time (st ^. stTime))+ <=>+ (htitle "Text: " <+> editor TextSearch (st ^. stEditText))++ htitle t =+ B.hLimit 20 $+ B.withAttr "infoTitle" $+ B.txt t+ + vtitle t =+ B.withAttr "infoTitle" $+ B.txt t++ editor n e =+ B.vLimit 1 $+ BE.renderEditor (B.txt . Txt.unlines) (BF.focusGetCurrent (st ^. stFocus) == Just n) e++ time t =+ B.padLeft (B.Pad 1) $+ B.hLimit 20 $+ B.withAttr "time" $+ B.str (Tm.formatTime Tm.defaultTimeLocale "%H-%M-%S" t)++ getSelectedDetail fn =+ case BL.listSelectedElement $ st ^. stResultsList of+ Nothing -> ""+ Just (_, e) -> fn e+++-- | Reformat the text so that it can be wrapped nicely+reflow :: Text -> Text+reflow = Txt.replace "\0" "\n\n" . Txt.replace "\n" "" . Txt.replace "\n\n" "\0" +++theMap :: BA.AttrMap+theMap = BA.attrMap V.defAttr [ (BE.editAttr , V.black `B.on` V.cyan)+ , (BE.editFocusedAttr , V.black `B.on` V.yellow)+ , (BL.listAttr , V.white `B.on` V.blue)+ , (BL.listSelectedAttr, V.blue `B.on` V.white)+ , ("infoTitle" , B.fg V.cyan)+ , ("time" , B.fg V.yellow)+ ]+++----------------------------------------------------------------------------------------------+-- | Compare two hoogle results for sorting+compareType :: H.Target -> H.Target -> Ordering+compareType a b =+ compare (formatResult a) (formatResult b)++ +-- | Search hoogle using the default hoogle database+searchHoogle :: Text -> IO [H.Target]+searchHoogle f = do+ d <- H.defaultDatabaseLocation + H.withDatabase d (\x -> pure $ H.searchDatabase x (Txt.unpack f))+ ++-- | Format the hoogle results so they roughly match what the terminal app would show+formatResult :: H.Target -> Text+formatResult t =+ let typ = clean $ H.targetItem t in+ let m = (clean . fst) <$> H.targetModule t in+ Txt.pack $ fromMaybe "" m <> " :: " <> typ+ ++clean :: [Char] -> [Char]+clean = unescapeHTML . stripTags+++-- | From hoogle source: https://hackage.haskell.org/package/hoogle-5.0.16/docs/src/General-Util.html+unescapeHTML :: [Char] -> [Char]+unescapeHTML ('&':xs)+ | Just x <- Lst.stripPrefix "lt;" xs = '<' : unescapeHTML x+ | Just x <- Lst.stripPrefix "gt;" xs = '>' : unescapeHTML x+ | Just x <- Lst.stripPrefix "amp;" xs = '&' : unescapeHTML x+ | Just x <- Lst.stripPrefix "quot;" xs = '\"' : unescapeHTML x+unescapeHTML (x:xs) = x : unescapeHTML xs+unescapeHTML [] = []+ ++-- | From hakyll source: https://hackage.haskell.org/package/hakyll-4.1.2.1/docs/src/Hakyll-Web-Html.html#stripTags+stripTags :: [Char] -> [Char]+stripTags [] = []+stripTags ('<' : xs) = stripTags $ drop 1 $ dropWhile (/= '>') xs+stripTags (x : xs) = x : stripTags xs
+ bhoogle.cabal view
@@ -0,0 +1,39 @@+name: bhoogle+version: 0.1.1.1+synopsis: Simple terminal GUI for local hoogle.+description: bhoogle is a terminal GUI layer over local hoogle. It provides search ahead and sub-string filtering in addition to the usual type-search.+homepage: https://github.com/githubuser/bhoogle#readme+license: BSD3+license-file: LICENSE+author: Andre Van Der Merwe+maintainer: andre@andrevdm.com+copyright: 2018 Andre Van Der Merwe+category: Development, Terminal+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++executable bhoogle+ hs-source-dirs: app+ main-is: Main.hs+ ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wimplicit-prelude+ build-depends: base >=4.7 && <5+ , protolude+ , text+ , containers+ , brick+ , safe-exceptions+ , filepath+ , directory+ , vty+ , vector+ , process+ , lens+ , bytestring+ , time+ , hoogle+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/githubuser/bhoogle