packages feed

talash-0.3.0: src/Talash/Brick/Internal.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ExistentialQuantification #-}

module Talash.Brick.Internal (twoColumnText , columns , searchWidget , searchWidgetAux , headingAndBody , listWithHighlights , columnsWithHighlights
                             , MatchSetG (..) , Searcher (..) , SearchEvent (..) , SearchEnv (..) ,  EventHooks (..) , AppSettingsG (..)
                             , SearchEventSized (..) , SearcherSized (..)
                             , queryEditor , matches , matcher , numMatches , eventSource , theme , hooks , chunkSize , maximumMatches , eventStrategy
                             , matchedTop , totMatches , term , getQuery , defHooks , theMain , initialSearcher , generateSearchEvent
                             , handleKeyEvent , handleSearch , selectedElement , renderList , module Export) where

import Brick as Export
import Brick.BChan as Export (BChan , newBChan , writeBChan)
import Brick.Widgets.Border as Export (border, vBorder, hBorder, borderAttr)
import Brick.Widgets.Border.Style as Export
import Brick.Widgets.Center as Export (vCenter, center)
import Brick.Widgets.Edit  as Export (editor , editorText, renderEditor, Editor, handleEditorEvent, getEditContents, applyEdit )
import Brick.Widgets.List as Export (Splittable (..) , List, list ,handleListEvent, handleListEventVi, listAttr, listSelectedAttr, listSelectedElement , listSelectedL
                                    , listReplace , listElements, GenericList (listElements, listSelected) , listMoveUp , listMoveDown , slice)
import qualified Brick.Widgets.List as L
import qualified Data.Set as DS
import qualified Data.Text as T
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Sized as S
import Graphics.Vty as Export (defAttr, cyan, white, blue, withStyle, bold, brightMagenta, black, magenta, brightBlue, Attr, defaultConfig, mkVty, green, standardIOConfig)
import qualified Graphics.Vty as V
import Graphics.Vty.Config (inputFd)
import Graphics.Vty.Input.Events as Export
import Lens.Micro (_head , (^?))
import Lens.Micro.TH as Export ( makeLenses )
import System.Posix.IO
import System.Posix.Terminal
import Talash.Chunked as Export
import Talash.Intro

newtype MatchSetG a = MatchSetG (DS.Set a) deriving Foldable

instance Splittable MatchSetG where
  splitAt n (MatchSetG s) = (MatchSetG p , MatchSetG q)
    where
      (p,q) = DS.splitAt n s

data SearchEventSized n a = SearchEventSized  { _matcherEv :: {-# UNPACK #-} !(MatcherSized n a)
                                                -- | The (maximum possible) number of matches. See the note on `_numMatches`.
                                              , _totMatches :: {-# UNPACK #-} !Int
                                              -- | The term which was searched for.
                                              , _term :: {-# UNPACK #-} !Text
                                              , -- | The matches received.
                                                _matchedTop :: !(MatchSetSized n)}
makeLenses ''SearchEventSized

data SearchEvent a = forall n. KnownNat n => SearchEvent (SearchEventSized n a)

data SearcherSized n a = SearcherSized { -- | The editor to get the query from.
                                         _queryEditor :: Editor Text Bool
                                       , _matches :: GenericList Bool MatchSetG (ScoredMatchSized n)
                                       , _matcher :: MatcherSized n a
                                       , _numMatches :: Int
                                       , _eventSource :: BChan (SearchEvent a) -- ^ The BChan from which the app receives search events.
                                       }
makeLenses ''SearcherSized

data Searcher a = forall n. KnownNat n => Searcher {getSearcher :: SearcherSized n a}

{-# INLINE generateSearchEvent #-}
generateSearchEvent :: forall m a b c. KnownNat m => (SearchReport -> Bool) -> BChan (SearchEvent a) -> c -> SearchReport -> MatcherSized m a -> MatchSetSized m -> IO ()
generateSearchEvent p b _ = go
  where
    go r m = when (p r) . writeBChan b . SearchEvent . SearchEventSized m (r ^. nummatches) (r ^. searchedTerm)

-- | Event hooks are almost direct translations of the events from vty i.e. see `Event`.
data EventHooks a = EventHooks { keyHook :: Key -> [Modifier] -> EventM Bool a ()
                               , pasteHook :: ByteString -> EventM Bool a ()
                               , resizeHook :: Int -> Int -> EventM Bool a ()
                               , mouseDownHook :: Int -> Int -> Button -> [Modifier] -> EventM Bool a ()
                               , mouseUpHook   :: Int -> Int -> Maybe Button -> EventM Bool a ()
                               , focusLostHook :: EventM Bool a ()
                               , focusGainedHook :: EventM Bool a ()}

data AppSettingsG (n :: Nat) a b t   =   AppSettings { _theme :: t
                                                     , _hooks :: ReaderT (SearchEnv n a b) EventHooks (Searcher a) -- ^ The event hooks which can make use of the search environment.
                                                     , _chunkSize :: Proxy n
                                                     , _maximumMatches :: Int
                                                     , _eventStrategy :: SearchReport -> Bool}
makeLenses ''AppSettingsG

defHooks :: EventHooks a
defHooks = EventHooks (const . const (pure ())) (const (pure ())) (const . const (pure ())) (const . const . const . const (pure ()))
                                (const . const . const (pure ())) (pure ()) (pure ())

getQuery :: SearcherSized n a -> Text
getQuery s = fromMaybe "" . listToMaybe . getEditContents $ s ^. queryEditor

twoColumnText :: Int -> Text -> Text -> Widget n
twoColumnText n t1 t2 =  joinBorders . vLimit 1 $ go n t1 <+> go 100 t2
  where
    go m t = hLimitPercent m $ padLeftRight 2 (txt t) <+> fill ' '

columns :: (a -> Widget n) -> [AttrName] -> [Int] -> [a] -> Widget n
columns f as ls = vLimit 1 . hBox . zipWith3 (\a l t -> hLimitPercent l $ (padRight (Pad 2) . withAttr a . f $ t) <+> fill ' ') as ls

searchWidget :: (Ord n , Show n) => Bool -> Text -> Editor Text n -> Widget n
searchWidget b p e = hBox [withAttr (attrName "Prompt") (padLeftRight 2 . txt $ p) , padLeftRight 2 (renderEditor (hBox . map txt) b e)]

searchWidgetAux :: (Ord n , Show n) => Bool -> Text -> Editor Text n -> Widget n -> Widget n
searchWidgetAux b p e w = hBox [withAttr (attrName "Prompt") (padLeftRight 2 . txt $ p) , padLeftRight 2 (renderEditor (hBox . map txt) b e) , w]

highlightAlternate :: (a -> Widget n) -> [a] -> Widget n
highlightAlternate f = hBox . zipWith (\b e -> if b then withAttr (attrName "Highlight") (f e) else f e) (cycle [False , True])

headingAndBody :: Text -> Text -> Widget n
headingAndBody h b = withAttr (attrName "Heading") (txt h) <=> txtWrap b

listWithHighlights :: (Ord n , Show n , KnownNat m , KnownNat l) => SearchEnv l a (Widget n) -> Text
                                                        -> MatcherSized m a -> Bool -> GenericList n MatchSetG (ScoredMatchSized m) -> Widget n
listWithHighlights env c m = renderList (\s e -> hBox [txtLine (if s then c else "  ") , hBox . go $! e])
  where
    go (ScoredMatchSized _ i v) = (env ^. searchFunctions . display) (\b e -> if b then withAttr (attrName "Highlight") (txtLine e) else txtLine e) m ((env ^. candidates) ! i) v

columnsWithHighlights :: Text -> (a -> [[Text]]) -> [AttrName] -> [Int] -> Bool -> a -> Widget n
columnsWithHighlights c f as ls s e = (txt (if s then c else "  ") <+>) . columns (highlightAlternate txt) as ls . f $! e

theMain :: Ord n => App b e n -> BChan e -> b -> IO b
theMain a b s = (\v -> customMain v (pure v) (Just b) a s) =<< mkVty =<<(\c -> (\fd -> c {inputFd = Just fd}) <$> termFd) =<< standardIOConfig
  where
    termFd = (\f -> openFd f ReadOnly Nothing (OpenFileFlags False False False False False)) =<< getControllingTerminalName

drawListElements :: (Ord n, Show n, Foldable t, Splittable t) => (Int -> Bool -> a -> Widget n) -> Bool -> GenericList n t a  -> Widget n
drawListElements drawElem foc l =
    Widget Greedy Greedy $ do
        c <- getContext
        let es = slice start (numPerHeight * 2) (l^. L.listElementsL)
            idx = fromMaybe 0 (l^. L.listSelectedL)
            start = max 0 $ idx - numPerHeight + 1
            numPerHeight = 1 + (c^.availHeightL - 1) `div` (l^. L.listItemHeightL)
            off = start * (l^. L.listItemHeightL)
            drawElement j e =
                let isSelected = Just j == l^.listSelectedL
                    elemWidget = drawElem j isSelected e
                    selItemAttr = withDefAttr listSelectedAttr
                    makeVisible = if isSelected
                                  then visible . selItemAttr
                                  else id
                in makeVisible elemWidget
        render $ viewport (l^. L.listNameL) Vertical $
                 translateBy (Location (0, off)) $ vBox . zipWith drawElement [start ..] . toList $ es

selectedElementInternal :: (Foldable t, Splittable t) => GenericList n t a -> Maybe (Int, a)
selectedElementInternal l = do
    sel <- l^.listSelectedL
    let (_, xs) = L.splitAt sel (l ^. L.listElementsL)
    (sel,) <$> toList xs ^? _head

{-# INLINE renderListWithIndex #-}
renderListWithIndex :: (Ord n, Show n, Foldable t, Splittable t) => (Int -> Bool -> a -> Widget n) -> Bool -> GenericList n t a -> Widget n
renderListWithIndex drawElem foc = withDefAttr listAttr . drawListElements drawElem foc

renderList :: (Foldable t, Splittable t, Ord n, Show n) => (Bool -> e -> Widget n) -> Bool -> GenericList n t e -> Widget n
renderList drawElem = renderListWithIndex $ const drawElem

handleQuery :: KnownNat n => SearchEnv n a c -> Key -> [Modifier] -> Searcher b -> EventM Bool (Searcher b) ()
handleQuery env k m (Searcher s) = put . Searcher =<< editStep =<< (nestEventM' s . zoom queryEditor . handleEditorEvent . VtyEvent $ EvKey k m)
  where
    editStep ns = sendStep (getQuery ns) $> ns
    sendStep nq = unless (nq == getQuery s) $ liftIO (sendQuery env nq)

-- | Handling of keypresses. The default bindings are
--  @Enter@ exits the app with the current selection.
--  @Esc@ exits without any selection
--  @Up@ , @Down@ , @PageUp@ and @PageDown@ move through the matches.
-- All others keys are used for editing the query. See `handleEditorEvent` for details.
{-# INLINE handleKeyEvent #-}
handleKeyEvent :: (KnownNat n) => SearchEnv n a c -> Key -> [Modifier] -> EventM Bool (Searcher b) ()
handleKeyEvent env k m
  | k == KEnter                                  , null m = halt
  | k == KEsc                                    , null m = modify (\(Searcher s) -> Searcher . ((matches . listSelectedL) .~ Nothing) $ s) >> halt
  | k == KChar '\t'                              , null m = modify (\(Searcher s) -> Searcher . over matches listMoveDown $ s)
  | k == KBackTab                                , null m = modify (\(Searcher s) -> Searcher . over matches listMoveUp $ s)
  | k `elem` [KUp , KDown , KPageUp , KPageDown] , null m = (\(Searcher s) -> put . Searcher =<< (nestEventM' s . zoom matches . handleListEvent $ EvKey k m)) =<< get
  | otherwise                                             = handleQuery env k m =<< get

-- | The initial state of the searcher. The editor is empty.
initialSearcher :: SearchEnv n a c -> BChan (SearchEvent a) -> SearcherSized 0 a
initialSearcher env = SearcherSized (editorText True (Just 1) "") (list False (MatchSetG DS.empty) 0) emptyMatcher 0

{-# INLINE  handleSearchSized #-}
handleSearchSized :: (KnownNat n , KnownNat m) => SearcherSized n a -> SearchEventSized m a -> SearcherSized m a
handleSearchSized s e = SearcherSized (s ^. queryEditor) (list False (MatchSetG (e ^. matchedTop)) 0)
                                      (e ^. matcherEv) (e ^. totMatches) (s ^. eventSource)

{-# INLINE  handleSearch #-}
handleSearch :: SearchEvent a -> EventM Bool (Searcher a) ()
handleSearch (SearchEvent e) = modify (\(Searcher s) -> if (e ^. term == getQuery s) then Searcher (handleSearchSized s e) else Searcher s)

selectedElement :: KnownNat n => Chunks n -> Searcher a -> Maybe Text
selectedElement c (Searcher s) = (map ((c !) . chunkIndex . snd) . selectedElementInternal . (^. matches)) s

txtLine :: Text -> Widget n
txtLine s =
    Widget Fixed Fixed $ do
      c <- getContext
      return $ (imageL .~ (V.text' (c^.attrL) s)) emptyResult