packages feed

ghc-debug-brick-0.8.0.0: src/Brick/Widgets/ListPicker.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module Brick.Widgets.ListPicker (
  -- * ListPicker
  ListPicker(..),
  -- * Lenses
  listPickerInput,
  listPickerElements,
  listPickerAllElements,
  listPickerSelect,
  listPickerDescription,
  listPickerRenderRow,
  listPickerLabel,
  listPickerHighlightAttrName,
  -- * Render and Update
  Update(..),
  renderListPicker,
  handleListPicker,
) where

import Brick
import Brick.Forms
import Brick.Widgets.Border
import Brick.Widgets.List
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Graphics.Vty.Input as Vty
import Lens.Micro
import Lens.Micro.Platform (makeLenses)

data ListPicker name state ev element = ListPicker
  { _listPickerInput :: Form Text ev name
  , _listPickerElements :: GenericList name Seq element
  , _listPickerAllElements :: Seq element
  , _listPickerSelect :: element -> EventM name state (Update name state ev element)
  , _listPickerDescription :: element -> Text
  , _listPickerRenderRow :: Int -> element -> Widget name
  , _listPickerLabel :: Text
  , _listPickerHighlightAttrName :: AttrName
  }

data Update name state ev element
  = Changed (ListPicker name state ev element)
  | Unchanged
  | Exit

makeLenses ''ListPicker

renderListPicker :: (Ord name, Show name) => ListPicker name state ev element -> Widget name
renderListPicker picker =
  hLimit vWidth $ vLimit vHeight $
    borderWithLabel (txt (_listPickerLabel picker)) $ vBox $
      [ renderForm (_listPickerInput picker)
      , renderList
          (\elIsSelected ->
            if elIsSelected
              then withHighlighting picker . renderLine
              else renderLine
          )
          False
          (_listPickerElements picker)
      ]
  where
    renderLine = _listPickerRenderRow picker actual_width

    vHeight = length (_listPickerAllElements picker) + 4
    vWidth = actual_width + 2

    maximum_size_ = maximum (fmap (Text.length . (_listPickerDescription picker)) (_listPickerAllElements picker))
    -- we limit the size to 120 to avoid row overflow
    maximum_size = min maximum_size_ 120

    actual_width =
      maximum_size
        + 5  -- 5, maximum width of rendering a key
        + 1  -- 1, at least one padding

handleListPicker :: (Ord n) => BrickEvent n e -> ListPicker n a e element -> EventM n a (Update n a e element)
handleListPicker e picker = do
  -- Overlapping commands are up/down so handle those just via list, otherwise both
  let handle_form = nestEventM' form (handleFormEvent e)
      handle_list =
        case e of
          VtyEvent vty_e -> nestEventM' cmd_list (handleListEvent vty_e)
          _ -> return cmd_list
      k form' cmd_list' =
        if (formState form /= formState form') then do
            let filter_string = formState form'
                new_elems = Seq.filter (\cmd -> fuzzyMatcher filter_string (_listPickerDescription picker cmd)) orig_cmds
                cmd_list'' = cmd_list'
                                  & listElementsL .~ new_elems
                                  & listSelectedL .~ if Seq.null new_elems then Nothing else Just 0
            pure $ Changed $ picker
              & listPickerInput .~ form'
              & listPickerElements .~ cmd_list''
          else
            pure $ Changed $ picker
              & listPickerInput .~ form'
              & listPickerElements .~ cmd_list'

  case e of
    VtyEvent (Vty.EvKey Vty.KUp _) -> do
      list' <- handle_list
      k form list'
    VtyEvent (Vty.EvKey Vty.KDown _) -> do
      list' <- handle_list
      k form list'
    VtyEvent (Vty.EvKey Vty.KEsc _) -> do
      pure Exit
    VtyEvent (Vty.EvKey Vty.KEnter _) -> do
      case listSelectedElement cmd_list of
        Just (_, cmd) -> do
          _listPickerSelect picker cmd
        Nothing  ->
          pure Unchanged
    _ -> do
      form' <- handle_form
      list' <- handle_list
      k form' list'
  where
    form = _listPickerInput picker
    cmd_list = _listPickerElements picker
    orig_cmds = _listPickerAllElements picker

-- | Check whether the search string occurs with sufficient likelihood in the
-- given string.
fuzzyMatcher :: Text -> Text -> Bool
fuzzyMatcher filterString haystack =
  -- TODO: This should be a fuzzy search, not 'Text.isInfixOf'
  Text.toLower filterString `Text.isInfixOf` Text.toLower haystack

withHighlighting :: ListPicker name state ev element -> Widget n -> Widget n
withHighlighting picker = forceAttr (_listPickerHighlightAttrName picker)