├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── Setup.hs ├── app └── Main.hs ├── bhoogle.cabal ├── stack.yaml └── ui.png /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.0 3 | 4 | jobs: 5 | build: 6 | docker: 7 | - image: fpco/stack-build:lts-11.3 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | keys: 12 | - stack-{{ .Branch }}-{{ checksum "stack.yaml" }} 13 | - stack-{{ .Branch }} 14 | - stack- 15 | - run: 16 | name: Dependencies 17 | command: make setup 18 | - run: 19 | name: Build 20 | command: make build 21 | - save_cache: 22 | key: stack-{{ .Branch }}-{{ checksum "stack.yaml" }} 23 | paths: 24 | - ~/.stack 25 | - ./.stack-work 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | *#*.hs# 3 | dist-newstyle 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Dual licenced as either BSD-3-Clause or Apache 2.0 2 | 3 | -------------------------------------------------------------------------------- 4 | BSD-3-Clause 5 | -------------------------------------------------------------------------------- 6 | Copyright Andre Van Der Merwe (c) 2017/2018 7 | 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above 17 | copyright notice, this list of conditions and the following 18 | disclaimer in the documentation and/or other materials provided 19 | with the distribution. 20 | 21 | * Neither the name of Author name here nor the names of other 22 | contributors may be used to endorse or promote products derived 23 | from this software without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- 37 | 38 | 39 | -------------------------------------------------------------------------------- 40 | Apache 2.0 41 | -------------------------------------------------------------------------------- 42 | Copyright Andre Van Der Merwe (c) 2017/2018 43 | 44 | Licensed under the Apache License, Version 2.0 (the "License"); 45 | you may not use this file except in compliance with the License. 46 | You may obtain a copy of the License at 47 | 48 | http://www.apache.org/licenses/LICENSE-2.0 49 | 50 | Unless required by applicable law or agreed to in writing, software 51 | distributed under the License is distributed on an "AS IS" BASIS, 52 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 53 | See the License for the specific language governing permissions and 54 | limitations under the License. 55 | -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | package = bhoogle 2 | exe = bhoogle 3 | 4 | run: 5 | cabal run $(exe) 6 | 7 | build: 8 | cabal build $(package) --ghc-options "-j6 +RTS -A128m -n2m -qg -RTS" 9 | 10 | build-fast: 11 | cabal build $(package) --disable-optimisation --ghc-options "-O0 -j6 +RTS -A128m -n2m -qg -RTS" 12 | 13 | ghcid: 14 | ghcid --lint -c "cabal repl --repl-options='-ignore-dot-ghci' --repl-options='-fobject-code' --repl-options='-fno-warn-unused-do-bind' --repl-options='-j6' " 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hoogle terminal GUI. 2 | 3 | **bhoogle** is a simple terminal GUI wrapper over [hoogle](https://hackage.haskell.org/package/hoogle). 4 | 5 | 6 | ![ui](http://www.andrevdm.com/images/bhoogle.png) 7 | 8 | 9 | ## Setup 10 | - Make sure you have a local hoogle database created 11 | - If you don't already, then 12 | 1. Install hoogle (e.g. ```stack install hoogle``` or ```cabal install hoogle```) 13 | 1. Generate the default database (```hoogle generate```) 14 | 15 | ## Usage 16 | 1. Enter a string in the "Type" edit box. You can filter the results to specific packages appending the `+packagename` syntax just like with Hoogle. 17 | 1. Press enter to search: focus goes directly to the results list 18 | 1. Or press tab to search and focus will go to the "text" edit box 19 | 1. You can then filter the results by typing in the "text" edit box, any result containing the sub-string typed will be shown 20 | 1. Navigate the results by using arrow or vi (hjkl) keys 21 | 1. Pressing **'s'** in the results list will toggle the sort order 22 | 1. Escape to exit 23 | 1. Search-ahead is enable for any type search longer than ~3 characters 24 | 1. When a result is selected `p` yanks the package name 25 | 1. When a result is selected `m` yanks the module name 26 | 27 | 28 | ## Settings 29 | 30 | Location: ~/.config/bhoogle/bhoogle.conf 31 | 32 | Eg: 33 | 34 | yank=xclip 35 | yankArgs=-selection c 36 | 37 | 38 | Note that the version described in the [blog](http://www.andrevdm.com/posts/2022-09-07-bhoogle.html) is on the [blog](https://github.com/andrevdm/bhoogle/tree/blog) branch. 39 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | {-# LANGUAGE LambdaCase #-} 5 | 6 | module Main where 7 | 8 | import Verset -- replacing protolude until it works with GHC 9.12+ 9 | import Control.Lens ((^.), (.~), (%~)) 10 | import Control.Lens.TH (makeLenses) 11 | import qualified Data.Map as Map 12 | import qualified Data.List as Lst 13 | import qualified Data.Time as Tm 14 | import qualified Data.Text as Txt 15 | import qualified Data.Text.Encoding as TxtE 16 | import qualified Data.ByteString as BS 17 | import qualified Data.Vector as Vec 18 | import Brick ((<+>), (<=>)) 19 | import qualified Brick as B 20 | import qualified Brick.BChan as BCh 21 | import qualified Brick.Focus as BF 22 | import qualified Brick.AttrMap as BA 23 | import qualified Brick.Widgets.List as BL 24 | import qualified Brick.Widgets.Edit as BE 25 | import qualified Brick.Widgets.Border as BB 26 | import qualified Brick.Widgets.Border.Style as BBS 27 | import qualified Graphics.Vty as V 28 | import qualified Graphics.Vty.Input.Events as K 29 | import qualified Graphics.Vty.CrossPlatform 30 | import qualified Graphics.Vty.Config 31 | import System.FilePath (()) 32 | import qualified System.Directory as Dir 33 | import qualified Hoogle as H 34 | import qualified System.Process.Typed as PT 35 | import qualified Data.ByteString.Lazy as BSL 36 | 37 | 38 | -- | Events that can be sent 39 | -- | Here there is just one event for updating the time 40 | newtype Event = EventUpdateTime Tm.LocalTime 41 | 42 | -- | Names use to identify each of the controls 43 | data Name = TypeSearch 44 | | TextSearch 45 | | ListResults 46 | deriving (Show, Eq, Ord) 47 | 48 | -- | Sort order 49 | data SortBy = SortNone 50 | | SortAsc 51 | | SortDec 52 | deriving (Eq) 53 | 54 | 55 | -- | State of the brick app. Contains the controls and any other required state 56 | data BrickState = BrickState { _stEditType :: !(BE.Editor Text Name) -- ^ Editor for the type to search for 57 | , _stEditText :: !(BE.Editor Text Name) -- ^ Editor for a text search in the results 58 | , _stTime :: !Tm.LocalTime -- ^ The current time 59 | , _stFocus :: !(BF.FocusRing Name) -- ^ Focus ring - a circular list of focusable controls 60 | , _stResults :: [H.Target] -- ^ The last set of search results from hoohle 61 | , _stResultsList :: !(BL.List Name H.Target) -- ^ List for the search results 62 | , _stSortResults :: SortBy -- ^ Current sort order for the results 63 | , _stDbPath :: FilePath -- ^ Hoogle DB path 64 | , _yankCommand :: Text -- ^ Command to run to copy text to the clipboard 65 | , _yankArgs :: Text -- ^ Args for the yank command 66 | } 67 | 68 | makeLenses ''BrickState 69 | 70 | 71 | -- | Defines how the brick application will work / handle events 72 | app :: B.App BrickState Event Name 73 | app = 74 | B.App { B.appDraw = drawUI 75 | , B.appChooseCursor = B.showFirstCursor 76 | , B.appHandleEvent = handleEvent 77 | , B.appStartEvent = pass 78 | , B.appAttrMap = const theMap 79 | } 80 | 81 | 82 | main :: IO () 83 | main = do 84 | -- Use the default hoogle DB. This may not exist because 85 | -- 1) hoogle generate was never called 86 | -- 2) the system hoogle is a different version from the package used here 87 | dbPath <- H.defaultDatabaseLocation 88 | Dir.doesFileExist dbPath >>= \case 89 | True -> runBHoogle dbPath 90 | False -> do 91 | putText "" 92 | putText "bhoogle error: " 93 | putText " default hoogle database not found" 94 | putText $ " at " <> Txt.pack dbPath 95 | putText " You can create the database by installing hoogle and running" 96 | putText " hoogle generate" 97 | putText "" 98 | 99 | 100 | runBHoogle :: FilePath -> IO () 101 | runBHoogle dbPath = do 102 | chan <- BCh.newBChan 5 -- ^ create a bounded channel for events 103 | 104 | -- Send a tick event every 1 seconds with the current time 105 | -- Brick will send this to our event handler which can then update the stTime field 106 | void . forkIO $ forever $ do 107 | t <- getTime 108 | BCh.writeBChan chan $ EventUpdateTime t 109 | threadDelay $ 1 * 1000000 110 | 111 | -- Initial current time value 112 | t <- getTime 113 | 114 | -- Settings 115 | settings <- loadSettings 116 | 117 | -- Construct the initial state values 118 | let st = BrickState { _stEditType = BE.editor TypeSearch (Just 1) "" 119 | , _stEditText = BE.editor TextSearch (Just 1) "" 120 | , _stResultsList = BL.list ListResults Vec.empty 1 121 | , _stTime = t 122 | , _stFocus = BF.focusRing [TypeSearch, TextSearch, ListResults] 123 | , _stResults = [] 124 | , _stSortResults = SortNone 125 | , _stDbPath = dbPath 126 | , _yankCommand = Map.findWithDefault "xclip" "yank" settings 127 | , _yankArgs = Map.findWithDefault "" "yankArgs" settings 128 | } 129 | 130 | -- And run brick 131 | let vtyBuilder = Graphics.Vty.CrossPlatform.mkVty Graphics.Vty.Config.defaultConfig 132 | initialVty <- vtyBuilder 133 | 134 | void $ B.customMain initialVty vtyBuilder (Just chan) app st 135 | 136 | where 137 | -- | Get the local time 138 | getTime = do 139 | t <- Tm.getCurrentTime 140 | tz <- Tm.getCurrentTimeZone 141 | pure $ Tm.utcToLocalTime tz t 142 | 143 | 144 | -- | Main even handler for brick events 145 | handleEvent :: B.BrickEvent Name Event -> B.EventM Name BrickState () 146 | handleEvent ev = 147 | case ev of 148 | -- Handle keyboard events 149 | -- k is the key 150 | -- ms are the modifier keys 151 | (B.VtyEvent ve@(V.EvKey k ms)) -> 152 | case (k, ms) of 153 | -- Escape quits the app, no matter what control has focus 154 | (K.KEsc, []) -> B.halt 155 | 156 | _ -> do 157 | st' <- B.get 158 | -- How to interpret the key press depends on which control is focused 159 | case BF.focusGetCurrent $ st' ^. stFocus of 160 | Just TypeSearch -> 161 | case k of 162 | K.KChar '\t' -> do 163 | -- Search, clear sort order, focus next 164 | found <- liftIO $ doSearch st' 165 | B.modify $ \st -> filterResults $ st & stFocus %~ BF.focusNext 166 | & stResults .~ found 167 | & stSortResults .~ SortNone 168 | 169 | K.KBackTab ->do 170 | -- Search, clear sort order, focus prev 171 | found <- liftIO $ doSearch st' 172 | B.modify $ \st -> filterResults $ st & stFocus %~ BF.focusPrev 173 | & stResults .~ found 174 | & stSortResults .~ SortNone 175 | 176 | K.KEnter -> do 177 | -- Search, clear sort order, focus on results 178 | -- This makes it faster if you want to search and navigate results without tabing through the text search box 179 | found <- liftIO $ doSearch st' 180 | B.modify $ \st -> filterResults $ st & stResults .~ found 181 | & stSortResults .~ SortNone 182 | & stFocus %~ BF.focusNext & stFocus %~ BF.focusNext 183 | -- TODO with brick >= 0.33, rather than 2x focus next: & stFocus %~ BF.focusSetCurrent ListResults 184 | 185 | _ -> do 186 | -- Let the editor handle all other events 187 | B.zoom stEditType $ BE.handleEditorEvent ev 188 | st <- B.get 189 | st2 <- liftIO $ searchAhead doSearch st 190 | B.put st2 191 | 192 | 193 | Just TextSearch -> 194 | case k of 195 | K.KChar '\t' -> B.modify $ \st -> st & stFocus %~ BF.focusNext -- Focus next 196 | K.KBackTab -> B.modify $ \st -> st & stFocus %~ BF.focusPrev -- Focus previous 197 | _ -> do 198 | -- Let the editor handle all other events 199 | B.zoom stEditText $ BE.handleEditorEvent ev 200 | B.modify filterResults 201 | 202 | 203 | Just ListResults -> 204 | case k of 205 | K.KChar '\t' -> B.modify $ \st -> st & stFocus %~ BF.focusNext -- Focus next 206 | K.KBackTab -> B.modify $ \st -> st & stFocus %~ BF.focusPrev -- Focus previous 207 | K.KChar 's' -> 208 | -- Toggle the search order between ascending and descending, use asc if sort order was 'none' 209 | let sortDir = if (st' ^. stSortResults) == SortAsc then SortDec else SortAsc in 210 | let sorter = if sortDir == SortDec then Lst.sortBy (flip compareType) else Lst.sortBy compareType in 211 | B.modify $ \st -> filterResults $ st & stResults %~ sorter 212 | & stSortResults .~ sortDir 213 | K.KChar 'p' -> do 214 | let selected = BL.listSelectedElement $ st' ^. stResultsList 215 | B.suspendAndResume (yankPackage st' selected) 216 | K.KChar 'm' -> do 217 | let selected = BL.listSelectedElement $ st' ^. stResultsList 218 | B.suspendAndResume (yankModule st' selected) 219 | _ -> do 220 | -- Let the list handle all other events 221 | -- Using handleListEventVi which adds vi-style keybindings for navigation 222 | -- and the standard handleListEvent as a fallback for all other events 223 | B.zoom stResultsList $ BL.handleListEventVi BL.handleListEvent ve 224 | 225 | _ -> pass 226 | 227 | (B.AppEvent (EventUpdateTime time)) -> 228 | -- Update the time in the state 229 | B.modify $ \st -> st & stTime .~ time 230 | 231 | _ -> pass 232 | 233 | where 234 | doSearch :: BrickState -> IO [H.Target] 235 | doSearch st' = 236 | searchHoogle (st' ^. stDbPath) (Txt.strip . Txt.concat $ BE.getEditContents (st' ^. stEditType)) 237 | 238 | 239 | yank :: (H.Target -> Maybe Text) -> BrickState -> Maybe (Int, H.Target) -> IO BrickState 240 | yank getText st selected = 241 | case getText <<$>> selected of 242 | Just (_, Just s') -> do 243 | let cmd = (st ^. yankCommand) <> " " <> (st ^. yankArgs) 244 | PT.runProcess_ $ PT.setStdin (PT.byteStringInput (BSL.fromStrict . TxtE.encodeUtf8 $ s')) (PT.shell (Txt.unpack cmd)) 245 | pure st 246 | 247 | _ -> 248 | pure st 249 | 250 | 251 | yankPackage :: BrickState -> Maybe (Int, H.Target) -> IO BrickState 252 | yankPackage st selected = 253 | yank (\t -> Txt.pack . fst <$> H.targetPackage t) st selected 254 | 255 | 256 | yankModule :: BrickState -> Maybe (Int, H.Target) -> IO BrickState 257 | yankModule st selected = 258 | yank (\t -> Txt.pack . fst <$> H.targetModule t) st selected 259 | 260 | 261 | -- | Search ahead for type strings longer than 3 chars. 262 | searchAhead :: (BrickState -> IO [H.Target]) -> BrickState -> IO BrickState 263 | searchAhead search st = 264 | let searchText = Txt.strip . Txt.concat . BE.getEditContents $ st ^. stEditType in 265 | 266 | if Txt.length (Txt.filter (`notElem` [' ', '\t', '(', ')', '=']) searchText) > 3 267 | then do 268 | -- Search 269 | found <- search st 270 | pure . filterResults $ st & stResults .~ found 271 | & stSortResults .~ SortNone 272 | else 273 | -- Just clear 274 | pure $ st & stResults .~ [] 275 | & stResultsList %~ BL.listClear 276 | 277 | 278 | -- | Filter the results from hoogle using the search text 279 | filterResults :: BrickState -> BrickState 280 | filterResults st = 281 | let allResults = st ^. stResults in 282 | let filterText = Txt.toLower . Txt.strip . Txt.concat . BE.getEditContents $ st ^. stEditText in 283 | 284 | let results = 285 | if Txt.null filterText 286 | then allResults 287 | else filter (Txt.isInfixOf filterText . Txt.toLower . formatResult) allResults 288 | in 289 | st & stResultsList .~ BL.list ListResults (Vec.fromList results) 1 290 | 291 | 292 | -- | Draw the UI 293 | drawUI :: BrickState -> [B.Widget Name] 294 | drawUI st = 295 | [B.padAll 1 contentBlock] 296 | 297 | where 298 | contentBlock = 299 | (B.withBorderStyle BBS.unicode $ BB.border searchBlock) 300 | <=> 301 | B.padTop (B.Pad 1) resultsBlock 302 | 303 | resultsBlock = 304 | let total = show . length $ st ^. stResults in 305 | let showing = show . length $ st ^. stResultsList ^. BL.listElementsL in 306 | (B.withAttr (B.attrName "infoTitle") $ B.txt "Results: ") <+> B.txt (showing <> "/" <> total) 307 | <=> 308 | (B.padTop (B.Pad 1) $ 309 | resultsContent <+> resultsDetail 310 | ) 311 | 312 | resultsContent = 313 | BL.renderList (\_ e -> B.txt $ formatResult e) False (st ^. stResultsList) 314 | 315 | resultsDetail = 316 | B.padLeft (B.Pad 1) $ 317 | B.hLimit 60 $ 318 | vtitle "package:" 319 | <=> 320 | B.padLeft (B.Pad 2) (B.txt $ getSelectedDetail (\t -> maybe "" (Txt.pack . fst) (H.targetPackage t))) 321 | <=> 322 | vtitle "module:" 323 | <=> 324 | B.padLeft (B.Pad 2) (B.txt $ getSelectedDetail (\t -> maybe "" (Txt.pack . fst) (H.targetModule t))) 325 | <=> 326 | vtitle "docs:" 327 | <=> 328 | B.padLeft (B.Pad 2) (B.txtWrap . reflow $ getSelectedDetail (Txt.pack . clean . H.targetDocs)) 329 | <=> 330 | B.fill ' ' 331 | 332 | searchBlock = 333 | ((htitle "Type: " <+> editor TypeSearch (st ^. stEditType)) <+> time (st ^. stTime)) 334 | <=> 335 | (htitle "Text: " <+> editor TextSearch (st ^. stEditText)) 336 | 337 | htitle t = 338 | B.hLimit 20 $ 339 | B.withAttr (B.attrName "infoTitle") $ 340 | B.txt t 341 | 342 | vtitle t = 343 | B.withAttr (B.attrName "infoTitle") $ 344 | B.txt t 345 | 346 | editor n e = 347 | B.vLimit 1 $ 348 | BE.renderEditor (B.txt . Txt.unlines) (BF.focusGetCurrent (st ^. stFocus) == Just n) e 349 | 350 | time t = 351 | B.padLeft (B.Pad 1) $ 352 | B.hLimit 20 $ 353 | B.withAttr (B.attrName "time") $ 354 | B.str (Tm.formatTime Tm.defaultTimeLocale "%H-%M-%S" t) 355 | 356 | getSelectedDetail fn = 357 | case BL.listSelectedElement $ st ^. stResultsList of 358 | Nothing -> "" 359 | Just (_, e) -> fn e 360 | 361 | 362 | -- | Reformat the text so that it can be wrapped nicely 363 | reflow :: Text -> Text 364 | reflow = Txt.replace "\n" " " . Txt.replace "\n\n" "\n" . Txt.replace "\0" "\n" 365 | 366 | 367 | theMap :: BA.AttrMap 368 | theMap = BA.attrMap V.defAttr [ (BE.editAttr , V.black `B.on` V.cyan) 369 | , (BE.editFocusedAttr , V.black `B.on` V.yellow) 370 | , (BL.listAttr , V.white `B.on` V.blue) 371 | , (BL.listSelectedAttr , V.blue `B.on` V.white) 372 | , (B.attrName "infoTitle", B.fg V.cyan) 373 | , (B.attrName "time" , B.fg V.yellow) 374 | ] 375 | 376 | 377 | getFiles :: FilePath -> IO [FilePath] 378 | getFiles p = do 379 | entries <- (p ) <<$>> Dir.listDirectory p 380 | filterM Dir.doesFileExist entries 381 | 382 | ---------------------------------------------------------------------------------------------- 383 | -- | Compare two hoogle results for sorting 384 | compareType :: H.Target -> H.Target -> Ordering 385 | compareType a b = 386 | compare (formatResult a) (formatResult b) 387 | 388 | 389 | -- | Search hoogle using the default hoogle database 390 | searchHoogle :: FilePath -> Text -> IO [H.Target] 391 | searchHoogle path f = 392 | H.withDatabase path (\x -> pure $ H.searchDatabase x (Txt.unpack f)) 393 | 394 | 395 | -- | Format the hoogle results so they roughly match what the terminal app would show 396 | formatResult :: H.Target -> Text 397 | formatResult t = 398 | let typ = clean $ H.targetItem t in 399 | let m = (clean . fst) <$> H.targetModule t in 400 | Txt.pack $ fromMaybe "" m <> " :: " <> typ 401 | 402 | 403 | clean :: [Char] -> [Char] 404 | clean = unescapeHTML . stripTags 405 | 406 | 407 | -- | From hoogle source: https://hackage.haskell.org/package/hoogle-5.0.16/docs/src/General-Util.html 408 | unescapeHTML :: [Char] -> [Char] 409 | unescapeHTML ('&':xs) 410 | | Just x <- Lst.stripPrefix "lt;" xs = '<' : unescapeHTML x 411 | | Just x <- Lst.stripPrefix "gt;" xs = '>' : unescapeHTML x 412 | | Just x <- Lst.stripPrefix "amp;" xs = '&' : unescapeHTML x 413 | | Just x <- Lst.stripPrefix "quot;" xs = '\"' : unescapeHTML x 414 | unescapeHTML (x:xs) = x : unescapeHTML xs 415 | unescapeHTML [] = [] 416 | 417 | 418 | -- | From hakyll source: https://hackage.haskell.org/package/hakyll-4.1.2.1/docs/src/Hakyll-Web-Html.html#stripTags 419 | stripTags :: [Char] -> [Char] 420 | stripTags [] = [] 421 | stripTags ('<' : xs) = stripTags $ drop 1 $ dropWhile (/= '>') xs 422 | stripTags (x : xs) = x : stripTags xs 423 | 424 | ---------------------------------------------------------------------------------------------------- 425 | 426 | loadSettings :: IO (Map Text Text) 427 | loadSettings = do 428 | p <- getSettingsFilePath 429 | 430 | Dir.doesFileExist p >>= \case 431 | True -> do 432 | cfgLines1 <- Txt.lines . TxtE.decodeUtf8 <$> BS.readFile p 433 | let cfgLines2 = Txt.strip <$> cfgLines1 434 | let cfgLines3 = filter (not . Txt.isPrefixOf "#") cfgLines2 435 | let cfg1 = Txt.breakOn "=" <$> cfgLines3 436 | let cfg2 = filter (not . Txt.null . snd) cfg1 437 | let cfg3 = (\(k, v) -> (Txt.strip k, Txt.strip . Txt.drop 1 $ v)) <$> cfg2 438 | pure $ Map.fromList cfg3 439 | False -> 440 | pure Map.empty 441 | 442 | saveSettings :: Map Text Text -> IO () 443 | saveSettings settings = do 444 | p <- getSettingsFilePath 445 | let ss = Txt.intercalate "\n" $ (\(k, v) -> k <> "=" <> v) <$> Map.toList settings 446 | BS.writeFile p . TxtE.encodeUtf8 $ ss 447 | 448 | 449 | getSettingsFilePath :: IO FilePath 450 | getSettingsFilePath = do 451 | p <- getSettingsRootPath 452 | pure $ p "bhoogle.conf" 453 | 454 | 455 | getSettingsRootPath :: IO FilePath 456 | getSettingsRootPath = do 457 | p <- Dir.getXdgDirectory Dir.XdgConfig "bhoogle" 458 | Dir.createDirectoryIfMissing True p 459 | pure p 460 | -------------------------------------------------------------------------------- /bhoogle.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: bhoogle 3 | version: 0.1.4.4 4 | synopsis: Simple terminal GUI for local hoogle. 5 | 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. 6 | homepage: https://github.com/andrevdm/bhoogle#readme 7 | license: BSD-3-Clause OR Apache-2.0 8 | license-file: LICENSE 9 | author: Andre Van Der Merwe 10 | maintainer: andre@andrevdm.com 11 | copyright: 2018-2025 Andre Van Der Merwe 12 | category: Development, Terminal 13 | build-type: Simple 14 | extra-source-files: README.md 15 | 16 | executable bhoogle 17 | hs-source-dirs: app 18 | main-is: Main.hs 19 | ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wimplicit-prelude 20 | build-depends: base >= 4.9.1.0 && <5 21 | , verset >= 0.0.1 && < 0.1 22 | --protolude 23 | , brick >= 2.1 && < 2.9 24 | , bytestring >= 0.12.1 && < 0.13 25 | , containers >= 0.6 && < 0.9 26 | , directory >= 1.3.8 && < 1.4 27 | , filepath >= 1.4 && < 1.6 28 | , hoogle >= 5.0 && < 5.1 29 | , lens >= 5.3.4 && < 5.4 30 | , process >= 1.6.2 && < 1.7 31 | , text >= 2.1.1 && < 2.2 32 | , time >= 1.12.2 && < 1.15 33 | , typed-process >= 0.2.12 && < 0.3 34 | , vector >= 0.13.2 && < 0.14 35 | , vty >= 6.2 && < 6.5 36 | , vty-crossplatform >= 0.4.0 && < 0.5 37 | default-language: Haskell2010 38 | 39 | source-repository head 40 | type: git 41 | location: https://github.com/andrevdm/bhoogle 42 | 43 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-23.18 2 | 3 | packages: 4 | - . 5 | -------------------------------------------------------------------------------- /ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrevdm/bhoogle/222ecd6d1c86d2fb0ffb271b0388ac1956811a73/ui.png --------------------------------------------------------------------------------