{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE CPP #-} module Main where import Control.Monad (void) import Lens.Micro import Lens.Micro.TH import Lens.Micro.Mtl import qualified Graphics.Vty as V #if !(MIN_VERSION_base(4,11,0)) import Data.Semigroup ((<>)) #endif import qualified Brick.Main as M import qualified Brick.Types as T import Brick.Widgets.Core ( (<+>) , vBox , hLimit , vLimit , str , visible , viewport , withDefAttr ) import qualified Brick.Widgets.Center as C import qualified Brick.Widgets.Edit as E import qualified Brick.AttrMap as A import Brick.Util (on, fg) data Name = Edit | EditLines deriving (Ord, Show, Eq) data St = St { _edit :: E.Editor String Name } makeLenses ''St drawUI :: St -> [T.Widget Name] drawUI st = [ui] where e = renderWithLineNumbers (st^.edit) ui = C.center $ hLimit 50 $ vLimit 10 e -- | Given an editor, render the editor with line numbers to the left of -- the editor. -- -- This essentially exploits knowledge of how the editor is implemented: -- we make a viewport containing line numbers that is just as high as -- the editor, then request that the line number associated with the -- editor's current line position be made visible, thus scrolling it -- into view. This is slightly brittle, however, because it relies on -- essentially keeping the line number viewport and the editor viewport -- in the same vertical scrolling state; with direct scrolling requests -- from EventM it is easily possible to put the two viewports into a -- state where they do not have the same vertical scrolling offset. That -- means that visibility requests made with 'visible' won't necessarily -- have the same effect in each viewport in that case. So this is -- only really usable in the case where you're sure that the editor's -- viewport and the line number viewports will not be managed by direct -- viewport operations in EventM. That's what I'd recommend anyway, but -- still, this is an important caveat. -- -- There's another important caveat here: this particular implementation -- has @O(n)@ performance for editor height @n@ because we generate -- the entire list of line numbers on each rendering depending on the -- height of the editor. That means that for sufficiently large files, -- it will get more expensive to render the line numbers. There is a way -- around this problem, which is to take the approach that the @List@ -- implementation takes: only render a region of visible line numbers -- around the currently-edited line that is just large enough to be -- guaranteed to fill the viewport, then translate that so that it -- appears at the right viewport offset, thus faking a viewport filled -- with line numbers when in fact we'd only ever render at most @2 * K + -- 1@ line numbers for a viewport height of @K@. That's more involved, -- so I didn't do it here, but that would be the way to go for a Real -- Application. renderWithLineNumbers :: E.Editor String Name -> T.Widget Name renderWithLineNumbers e = lineNumbersVp <+> editorVp where lineNumbersVp = hLimit (maxNumWidth + 1) $ viewport EditLines T.Vertical body editorVp = E.renderEditor (str . unlines) True e body = withDefAttr lineNumberAttr $ vBox numWidgets numWidgets = mkNumWidget <$> numbers mkNumWidget i = maybeVisible i $ str $ show i maybeVisible i | i == curLine + 1 = visible . withDefAttr currentLineNumberAttr | otherwise = id numbers = [1..h] contents = E.getEditContents e h = length contents curLine = fst $ E.getCursorPosition e maxNumWidth = length $ show h appEvent :: T.BrickEvent Name e -> T.EventM Name St () appEvent (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt appEvent ev = do zoom edit $ E.handleEditorEvent ev initialState :: St initialState = St (E.editor Edit Nothing "") lineNumberAttr :: A.AttrName lineNumberAttr = A.attrName "lineNumber" currentLineNumberAttr :: A.AttrName currentLineNumberAttr = lineNumberAttr <> A.attrName "current" theMap :: A.AttrMap theMap = A.attrMap V.defAttr [ (E.editAttr, V.white `on` V.blue) , (E.editFocusedAttr, V.black `on` V.yellow) , (lineNumberAttr, fg V.cyan) , (currentLineNumberAttr, V.defAttr `V.withStyle` V.bold) ] theApp :: M.App St e Name theApp = M.App { M.appDraw = drawUI , M.appChooseCursor = const $ M.showCursorNamed Edit , M.appHandleEvent = appEvent , M.appStartEvent = return () , M.appAttrMap = const theMap } main :: IO () main = do void $ M.defaultMain theApp initialState