packages feed

talash-0.3.0: src/Talash/Brick.hs

{-# LANGUAGE TemplateHaskell #-}

-- | A simple brick app to search among the candidates from a vector of text and get the selection. By default the app doesn't do anything except
-- return a single selection but more complicated actions can be performed by using the `_hooks` which allow abitrary IO actions (due to `EventM` being a `MonadIO`)
-- in response to input events. The most convenient function to use the brick app are `selected` and related functions. `runApp` provides some more flexibility.
module Talash.Brick (-- * Types
                     Searcher (..) , SearchEvent (..) , SearchEnv (..) ,  EventHooks (..) , AppTheme (..) , AppSettings (..) , AppSettingsG (..) , CaseSensitivity (..)
                     -- * The Brick App and Helpers
                    , searchApp , defSettings , fuzzyFunctions , orderlessFunctions , runApp , runAppFromHandle , selected , selectedFromHandle
                    , selectedFromHandleWith , selectedFromFileNamesSorted , selectedFromFiles , selectedUsing , runSearch , makeChunks
                    -- * Default program
                    , run , run'
                     -- * Lenses
                     -- ** Searcher
                    , queryEditor , allMatches , matches , numMatches
                     -- ** SearchEvent
                    , matchedTop , totalMatches , term
                     -- ** SearchEnv
                    , candidates , eventSource
                     -- ** SearchFunctions
                    , makeMatcher , match , display
                     -- ** AppTheme
                    , prompt , themeAttrs , borderStyle
                     -- ** SearchSettings
                    , theme , hooks , defTheme , defHooks
                     -- * Exposed Internals
                    , handleKeyEvent , handleSearch , searcherWidget , initialSearcher , readVectorHandleWith)
where

import Data.Monoid.Colorful as C
import Data.Vector (unsafeIndex , elemIndex)
import GHC.Compact (Compact , compact , getCompact)
import qualified System.IO.Streams as I
import Talash.Brick.Internal
import Talash.Files
import Talash.Intro hiding (sort, on , take)

data AppTheme = AppTheme { _prompt :: Text -- ^ The prompt to display next to the editor.
                         , _themeAttrs :: [(AttrName, Attr)]  -- ^ This is used to construct the `attrMap` for the app. By default the used attarNmaes are
                                                              --  `listSelectedAttr` , `borderAttr` , \"Prompt\" , \"Highlight\" and \"Stats\"
                         , _borderStyle :: BorderStyle -- ^ The border style to use. By default `unicodeRounded`
                         }
makeLenses ''AppTheme

type AppSettings n a = AppSettingsG n a (Widget Bool) AppTheme

-- | The brick widget used to display the editor and the search result.
searcherWidget :: (KnownNat n , KnownNat m) => SearchEnv n a (Widget Bool) -> Text -> SearcherSized m a -> Widget Bool
searcherWidget env p s = joinBorders . border $ vBox [searchWidgetAux True p (s ^. queryEditor) (withAttr (attrName "Stats") . txt  $ pack (show $ s ^. numMatches))
                                                     , hBorder , listWithHighlights env "➜ " (s ^. matcher) False (s ^. matches)]

defThemeAttrs :: [(AttrName, Attr)]
defThemeAttrs = [ (listSelectedAttr, withStyle (bg white) bold) , (attrName "Prompt" , withStyle (white `on` blue) bold)
                , (attrName "Highlight" , withStyle (fg blue) bold) ,  (attrName "Stats" , fg blue) ,  (borderAttr , fg cyan)]

defTheme ::AppTheme
defTheme = AppTheme {_prompt = "Find: " , _themeAttrs = defThemeAttrs , _borderStyle = unicodeRounded}

-- | Default settings. Uses blue for various highlights and cyan for borders. All the hooks except keyHook which is `handleKeyEvent` are trivial.
{-# INLINE defSettings#-}
defSettings :: KnownNat n => AppSettings n a
defSettings = AppSettings defTheme (ReaderT (\e -> defHooks {keyHook = handleKeyEvent e})) Proxy 4096
                          (\r -> r ^. ocassion == QueryDone)

-- | Tha app itself. `selected` and the related functions are probably more convenient for embedding into a larger program.
searchApp ::KnownNat n => AppSettings n a -> SearchEnv n a (Widget Bool) -> App (Searcher a) (SearchEvent a) Bool
searchApp (AppSettings th hks _ _ _) env  = App {appDraw = ad , appChooseCursor = showFirstCursor , appHandleEvent = he , appStartEvent = as , appAttrMap = am}
  where
    ad (Searcher s)                                  = (:[]) . withBorderStyle (th ^. borderStyle) . searcherWidget env (th ^. prompt) $ s
    as                                               = liftIO (sendQuery env "")
    am                                               = const $ attrMap defAttr (th ^. themeAttrs)
    hk                                               = runReaderT hks env
    he (VtyEvent (EvKey k m))                      = keyHook hk k m
    he (VtyEvent (EvMouseDown i j b m))            = mouseDownHook   hk i j b m
    he (VtyEvent (EvMouseUp   i j b  ))            = mouseUpHook     hk i j b
    he (VtyEvent (EvPaste     b      ))            = pasteHook       hk     b
    he (VtyEvent  EvGainedFocus       )            = focusGainedHook hk
    he (VtyEvent  EvLostFocus         )            = focusLostHook   hk
    he (AppEvent e@(SearchEvent e'))               = handleSearch e
    he _                                           = pure ()

-- | Run app with given settings and return the final Searcher state.
runApp :: KnownNat n => AppSettings n a -> SearchFunctions a (Widget Bool) -> Chunks n -> IO (Searcher a)
runApp s f c =     (\b -> (\env -> startSearcher env *> finally (theMain (searchApp s env) b . Searcher . initialSearcher env $ b) (stopSearcher env))
               =<< searchEnv f (s ^. maximumMatches) (generateSearchEvent (s ^. eventStrategy) b) c) =<< newBChan 8

-- | Run app with a vector that contains lines read from a handle and return the final Searcher state.
runAppFromHandle :: KnownNat n => AppSettings n a -> SearchFunctions a (Widget Bool) -> Handle -> IO (Searcher a)
runAppFromHandle s f = runApp s f . getCompact <=< compact . forceChunks <=< chunksFromHandle (s ^. chunkSize)

-- | Run app with a vector that contains lines read from a handle and return the final Searcher state.
runAppFromVector :: KnownNat n =>  AppSettings n a -> SearchFunctions a (Widget Bool) -> Vector Text -> IO (Searcher a)
runAppFromVector s f = runApp s f . getCompact <=< compact . forceChunks . makeChunks

-- | Run app and return the text of the selection if there is one else Nothing.
selected :: KnownNat n => AppSettings n a -> SearchFunctions a (Widget Bool) -> Chunks n -> IO (Maybe Text)
selected s f = (\c -> map (selectedElement c) . runApp s f $ c) . getCompact <=< compact . forceChunks

-- | Same as `selected` but reads the vector from the supplied handle.
selectedFromHandle :: KnownNat n => AppSettings n a -> SearchFunctions a (Widget Bool) -> Handle -> IO (Maybe Text)
selectedFromHandle s f = selected s f <=< chunksFromHandle (s ^. chunkSize)

-- | Same as `selectedFromHandle` but allows for transforming the lines read and the final vector with supplied functions. See also `readVectorHandleWith`.
selectedFromHandleWith :: KnownNat n => (Text -> Text) -> (Vector Text -> Vector Text) -> AppSettings n a -> SearchFunctions a (Widget Bool) -> Handle -> IO (Maybe Text)
selectedFromHandleWith w t s f = selected s f . makeChunks <=< readVectorHandleWith w t

-- | Another variation on `selectedFromHandle`. See `fileNamesSorted` for what happens to read vector.
selectedFromFileNamesSorted :: KnownNat n => AppSettings n a -> SearchFunctions a (Widget Bool) -> Handle -> IO (Maybe Text)
selectedFromFileNamesSorted s f = selected s f .  makeChunks <=< fileNamesSorted

-- | Version of `selected` for file search using a simple implementation of searching file trees from "Talash.Files". Better to use either other
-- libraries like @unix-recursive@ or external programs like @fd@ for more complicated tasks.
selectedFromFiles :: KnownNat n => AppSettings n a -> SearchFunctions a (Widget Bool) -> [FindInDirs] -> IO (Maybe Text)
selectedFromFiles s f = selected s f . forceChunks . makeChunks . (flatten =<<) <=< findFilesInDirs

selectedUsing :: KnownNat n => AppSettings n a -> SearchFunctions a (Widget Bool) -> (a -> Text) -> Vector a -> IO (Maybe a)
selectedUsing s f t v = map (map (unsafeIndex v) . (`elemIndex` w) =<<) . selected s f . makeChunks $ w
  where
    w = map t v

-- | A version of `selected` that puts the selected text on the stdout.
runSearch :: AppSettings 64 a -> SearchFunctions a (Widget Bool) -> IO ()
runSearch s f = maybe (pure ()) putStrLn =<< selected s f =<< chunksFromStream =<< I.decodeUtf8 =<< I.lines I.stdin

-- | The backend for `run`
run' :: [String] -> IO ()
run' []                 = runSearch defSettings (orderlessFunctions IgnoreCase)
run' ["fuzzy"]          = runSearch defSettings (fuzzyFunctions IgnoreCase)
run' ["orderless"]      = runSearch defSettings (orderlessFunctions IgnoreCase)
run' xs                 = (\t -> C.printColored putStr t usageString) =<< C.getTerm

usageString :: Colored Text
usageString =    "talash tui is a set of command for a tui searcher/selector interface. It reads the input from the stdin to generate candidates to search for,"
              <> " one from each line and outputs the selected candidate (if there is one) on the stdout.\n"
              <> C.Fg C.Blue "talash tui" <> ": Run the tui with the default orderless style of searching.\n"
              <> C.Fg C.Blue "talash tui fuzzy" <> ": Run the tui with fuzzy style for searching.\n"
              <> C.Fg C.Blue "talash tui orderless" <>  ": Run the tui with the default orderless style of searching.\n"

-- | Defualt program for the brick app that reads candidates from stdin and prints the selected text to the stdout. Can be called from the executable with
-- @talash tui@ which uses the orderless style. The search style can be set explicitly by calling @talash tui fuzzy@ or @talash tui orderless@
run :: IO ()
run = run' =<< getArgs