bhoogle - Building a simple hoogle GUI with brick

Posted on January 15, 2018

NB. This is using an old version of brick. Please see this post for a brick 1.1 updated version


Overview

bhoogle is a simple hoogle terminal GUI written using brick. This post is the annotated source code that should give you an idea of how to use brick and how easy brick makes building terminal UIs.

bhoogle

bhoogle is possibly useful as a local hoogle UI as well as a demo app. You can get the full code from github.

Setup

You will need an existing local hoogle database. If you do not already have one or are unsure, then do this

  1. Install hoogle (e.g. stack install hoogle)
  2. Generate the default database (hoogle generate)

Build

You can then clone the code, or download one of the pre-build linux releases

Usage

  1. Enter a type search in the “type” edit box
  2. Press enter to search: focus goes directly to the results list
  3. Or press tab to search and focus will go to the “text” edit box
  4. You can then filter the results by typing in the “text” edit box, any result containing the sub-string typed will be shown
  5. Navigate the results by using arrow or vi (hjkl) keys
  6. Pressing ‘s’ in the results list will toggle the sort order
  7. Escape to exit
  8. Search-ahead is enable for any type search longer than three characters

Brick

There are a few conventions to get used to when building a brick UI, but I don’t think it should take you too long to get the hang of things.

The brick user guide and documentation are fantastic. Brick comes with multiple example apps that show controls and features being used. There are also third party tutorials e.g. Samuel Tay’s brick tutorial

bhoogle 0.1.1.0 source

If you have looked at the user guide or Samuel Tay’s tutorial you’ll already have some idea of the fundamental concepts. Below is the annotated source for bhoogle. As always feel free to email or contact me on twitter if anything is unclear and I’ll do my best to assist.

Import all the modules we’ll need. I’m using protolude as my custom prelude, changing to one of the others e.g. classy should be pretty simple if you prefer that.

I’m also using lens. The brick examples use lens so its worth getting used to. However I’m only using three of the simpler lenses, so if you don’t like lens or template haskell it should be easy enough to remove them.

Next we need to define the type of custom events that our brick application can handle and a sum type defining the “name” for each control we want to use.

In this example there is only a single event EventUpdateTime. It is sent once a second with the current time. This gets displayed by brick in the top right corner

There are three controls

  1. The edit box for the type to search for
  2. The edit box for the substring search
  3. The results listbox

BrickState contains the current state of the brick application. Any event e.g. the custom update time event, or any key press event can result in the state being updated. There is a separate draw function that renders the state.

I.e. one part of the code deals with events, roughly state -> event -> state and another handles the drawing state -> GUI

Here the state contains

  1. The three controls mentioned above (two edit + one listbox)
  2. A focus ring. (A focus ring is a circular list of control names that helps your code keep track of which control has the current focus).
  3. The last updated current time
  4. The last search result
  5. The current sort order, so that it can be toggled between ascending and descending

The App type defines how the brick app operates, but defining how events are handled (appHandleEvent) and how the GUI is drawn (appDraw)

In main some setup is preformed and then brick is started by calling customMain.

For bhoogle the steps are

  1. Construct the channel for brick events (passed to customMain)
  2. Create a new thread to send the current time every second
  3. Construct an initial state, with empty controls and search results
  4. B.customMain to run brick

handleEvent gets all the brick events, updates the state and decides how to continue.

Here the code matches the custom (B.AppEvent) event looking for our update time event (EventUpdateTime) and then updates the state with the current time. B.continue means that brick continues after updating the state. Note that the UI is not changed in any way here, we are just altering the current state.

Then the code matches any keyboard event (B.VtyEvent) here matching on the escape key (K.KEsc). So when the user clicks the escape key this handler will call B.halt which will terminate the app. As this is done at the top level, this means that no matter which control has the focus, escape will exit.

For the rest of the key press logic, what bhoogle does depends on which control has the focus. BF.focusGetCurrent is used to get that from the state’s focus ring.

If the user is typing in the “type” edit box and tabs out (either tab or shift-tab) then

  1. Perform the search (see doSearch below)
  2. Update the current set of results
  3. Reset the sort order, default to the order that hoogle uses
  4. Move the focus to the next/previous control

If the user presses enter while in the type search edit box, then

  1. Perform the search (see doSearch below)
  2. Update the current set of results
  3. Reset the sort order, default to the order that hoogle uses
  4. Move the focus directly to the results lisbox so they can navigate and see the current item’s details & help text

For all other key events for the type search, let the editor control handle the key press. This gives us editing, navigation etc for free.

For the text edit box

  1. Change focus on tab / shift-tab
  2. For all other keys
    1. Let the editor handle the key press
    2. Filter the hoogle results

For the results listbox

And finally for handleEvent the doSearch function which calls the searchHoogle function (below) to search on the text from the type editbox.

searchAhead is a helper function that searches hoogle as the user types. As long as there are more than three characters being searched for. Without this limit hoogle seems a bit slow on my machine because of the large number of results.

Filter the hoogle results by doing a sub-string search if the user has entered one

-- | 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.txt $ 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

drawUI renders the state and creates the GUI. At first this may take some getting used to, but you will soon be able to see the GUI structure from the code.

The attribute map is where attributes for the controls and custom attributes are defined. This makes it easy to change how the GUI looks. There is even support for themes and basic markup.

----------------------------------------------------------------------------------------------
-- | 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

The remainder of the code is non-brick code for searching and formatting hoogle results

Hopefully this example helps you get started with brick and demonstrates how easy brick makes creating terminal UIs

Links