brick-list-skip (empty) → 0.1.0.0
raw patch · 7 files changed
+447/−0 lines, 7 filesdep +basedep +brickdep +brick-list-skipbinary-added
Dependencies added: base, brick, brick-list-skip, containers, microlens, vector, vty
Files
- CHANGELOG.md +18/−0
- LICENSE +3/−0
- brick-list-skip.cabal +61/−0
- demo-01.png binary
- demo-02.png binary
- demo/BrickListSkip.hs +85/−0
- src/Brick/Widgets/List/Skip.hs +280/−0
+ CHANGELOG.md view
@@ -0,0 +1,18 @@+# Revision history for brick-list-search++## 0.1.2.1 -- 2023-02-26++* Improved documentation+* Made `listSearchBy` and `listSearchByPages` a bit more efficient++## 0.1.2.0 -- 2023-02-22++* Added `listShowTheTop` and `listShowTheBottom`.++## 0.1.1.0 -- 2023-02-21++* Added `listSearchBy` and `listSearchByPages`.++## 0.1.0.0 -- 2023-02-20++* First version with proper documentation.
+ LICENSE view
@@ -0,0 +1,3 @@+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ brick-list-skip.cabal view
@@ -0,0 +1,61 @@+cabal-version: 3.0+name: brick-list-skip+version: 0.1.0.0++synopsis: Skip a certain kind of items when moving in brick list++description:+ This package contains functions that can be used in brick event handlers to skip a certain kind of items when+ moving in brick list.++ For example, you can skip a separator because selecting a separator doesn't make sense.++ You can run demo programs to see how it works.++homepage: https://codeberg.org/amano.kenji/brick-list-skip+bug-reports: https://codeberg.org/amano.kenji/brick-list-skip/issues++license: 0BSD+license-file: LICENSE+author: amano.kenji+maintainer: amano.kenji@proton.me++category: User Interface+extra-source-files: CHANGELOG.md+extra-doc-files:+ demo-01.png+ , demo-02.png++source-repository head+ type: git+ location: https://codeberg.org/amano.kenji/brick-list-skip.git++Flag demo+ description: Build demonstration programs+ Default: False++common all+ build-depends:+ base >=4.15.1.0 && <5+ , brick >=1.5 && <1.7+ , microlens >=0.4.13 && <0.5+ , vector >=0.12.3 && <0.13+ default-language: Haskell2010++library+ import: all+ exposed-modules: Brick.Widgets.List.Skip+ build-depends:+ containers >=0.6.4 && <0.7+ hs-source-dirs: src++executable brick-list-skip+ import: all+ if !flag(demo)+ buildable: False+ main-is: BrickListSkip.hs+ build-depends:+ brick-list-skip+ , vty >=5.36 && <5.37+ hs-source-dirs: demo+ ghc-options: -threaded
+ demo-01.png view
binary file changed (absent → 21772 bytes)
+ demo-02.png view
binary file changed (absent → 25710 bytes)
+ demo/BrickListSkip.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE LambdaCase #-}+module Main where++import Brick.Widgets.List.Skip+-- base+import Control.Monad (void)+-- Third party libraries+import Lens.Micro+import Data.Vector hiding ((++), map, modify)+-- brick & vty+import Brick.Main+import Brick.AttrMap+import Brick.Types+import Brick.Widgets.Core+import Brick.Util+import Brick.Widgets.Border+import Brick.Widgets.List+import Brick.Widgets.Center+import Graphics.Vty (defAttr, Event(..), Key(..), Modifier(..), black, white)++data Name = TheList deriving (Eq, Ord, Show)++data ListElem = Sep (Maybe String) | Str String deriving Show++isSep :: ListElem -> Bool+isSep = \case+ Str _ -> False+ Sep _ -> True++handleEvent :: BrickEvent Name () -> EventM Name (GenericList Name Vector ListElem) ()+handleEvent e = do+ case e of+ VtyEvent (EvKey KEsc []) -> halt+ VtyEvent (EvKey (KChar 'q') []) -> halt+ _ -> listSkip isSep $ case e of+ VtyEvent (EvKey KUp []) -> Move One Bwd+ VtyEvent (EvKey KDown []) -> Move One Fwd+ VtyEvent (EvKey KHome []) -> Move Most Bwd+ VtyEvent (EvKey KEnd []) -> Move Most Fwd+ VtyEvent (EvKey KPageUp []) -> Move Page Bwd+ VtyEvent (EvKey KPageDown []) -> Move Page Fwd+ VtyEvent (EvKey (KChar 'u') [MCtrl]) -> Move HalfPage Bwd+ VtyEvent (EvKey (KChar 'd') [MCtrl]) -> Move HalfPage Fwd+ _ -> NoMove++renderListElement :: Bool -> ListElem -> Widget Name+renderListElement lf e = case e of+ Str s -> padRight Max $ str s+ Sep Nothing -> hBorder+ Sep (Just title) -> hBorderWithLabel $ str title++drawUi :: GenericList Name Vector ListElem -> [Widget Name]+drawUi l = let msgs = [ "Press q or Esc to quit"+ , "Press Up arrow to move up"+ , "Press Down arrow to move down"+ , "Press Home to move to the beginning"+ , "Press End to move to the end"+ , "Press PageUp to move up one page"+ , "Press PageDown to move down one page"+ , "Press Ctrl+u to move up half page"+ , "Press Ctrl+d to move down half page" ]+ in [vCenter $ vBox $+ hCenter (border $ vLimit 20 $ hLimit 30 $ renderList renderListElement True l) : map (hCenter . str) msgs]++listElems :: Vector ListElem+listElems = fromList $+ [Sep (Just "First separator"), Str "New account", Sep (Just "Accounts")]+ ++ map (Str . ("Account "<>) . show) [1..1000]+ ++ [Sep Nothing, Str "Show account statistics.", Sep (Just "Yet Another Separator"), Str "Last list element",+ Sep (Just "Last separator")]++theList :: GenericList Name Vector ListElem+theList = list TheList listElems 1++main :: IO ()+main = do+ let app = App {+ appDraw = drawUi+ , appChooseCursor = neverShowCursor+ , appHandleEvent = handleEvent+ -- If the first element is a separator, searching forward for an item from the first element is necessary.+ , appStartEvent = listSearchFromCurrent isSep Fwd+ , appAttrMap = const $ attrMap defAttr [(listSelectedAttr, black `on` white)]+ }+ void $ defaultMain app theList
+ src/Brick/Widgets/List/Skip.hs view
@@ -0,0 +1,280 @@+{-# LANGUAGE KindSignatures #-}+-- | Functions that brick event handlers can use to move up or down while skipping a certain kind of elements.+--+--  +--+-- For example, in the above demo program, you can move in the list while skipping separators.+module Brick.Widgets.List.Skip (+-- * Types+ Dir(..)+, Amount(..)+, Move(..)+-- * Functions+, listSkip+, listSearchFromCurrent+-- * Classes+, Searchable(..)+) where++-- base+import Prelude hiding (take, drop)+-- third party packages+import Lens.Micro+import qualified Data.Vector as V+import Data.Vector hiding (take, drop, modify)+import qualified Data.Sequence as S+import Data.Sequence hiding (take, drop)+-- brick+import qualified Brick.Widgets.List as L+import Brick.Types hiding (Direction(..))+import Brick.Main++-- | Should the search include the current location?+data IncludeCurrent = IncludeCurrent | ExcludeCurrent deriving (Show, Eq)++-- | The direction to move in+data Dir =+ -- | Backward, or Up+ Bwd |+ -- | Forward, or Down+ Fwd+ deriving (Show, Eq)++-- | The amount to move by+data Amount =+ -- | Move to the next element that fails the test.+ One |+ -- | Move forward or backward as much as possible, and start searching in the opposite direction.+ Most |+ -- | Move by one page. From there, start searching. If the search fails, start searching in the opposite direction.+ Page |+ -- | Move by half page. From there, start searching. If the search fails, start searching in the opposite direction.+ HalfPage+ deriving (Show, Eq)++-- | Description of a movement+data Move =+ Move Amount Dir |+ NoMove+ deriving (Show, Eq)++-- | Move by a specified amount. Skip elements that pass the test. After moving, if there is no more element that fails+-- the test in the specified direction, this function tries to show as many elements that pass the test as possible in+-- the direction.+listSkip :: (Foldable t, L.Splittable t, Searchable t, Ord n)+ => (e -> Bool) -- ^ The test+ -> Move+ -> EventM n (L.GenericList n t e) ()+listSkip t m = case m of+ Move a Bwd -> do+ case a of+ One -> modify $ listSkipBackward t+ Most -> modify $ listSkipToBeginning t+ Page -> listSkipByPages t (-1.0)+ HalfPage -> listSkipByPages t (-0.5)+ listShowAbove t+ Move a Fwd -> do+ case a of+ One -> modify $ listSkipForward t+ Most -> modify $ listSkipToEnd t+ Page -> listSkipByPages t 1.0+ HalfPage -> listSkipByPages t 0.5+ listShowBelow t+ NoMove -> return ()++-- | From the current element, search for an element that fails the test, and go to it.+listSearchFromCurrent :: Searchable t+ => (e -> Bool) -- ^ The test+ -> Dir+ -> EventM n (L.GenericList n t e) ()+listSearchFromCurrent t d = let+ searchList = case d of+ Bwd -> searchListBackward+ Fwd -> searchListForward+ in modify $ \l -> case searchList IncludeCurrent t l of+ Nothing -> l+ Just ix -> l & L.listSelectedL ?~ ix++-- | Search forward for the first element index that fails the test+searchListForward :: Searchable t+ => IncludeCurrent+ -> (e -> Bool) -- ^ The test+ -> L.GenericList n t e+ -> Maybe Int -- ^ The first element index that fails the test+searchListForward incCur test l = let+ skipForward idx es = case viewHead es of+ Nothing -> Nothing+ Just (e, es') -> if test e+ then skipForward (idx+1) es'+ else Just idx+ -- Start with the current list element index. If the current element is excluded, then add 1 to the current index.+ start = case l ^. L.listSelectedL of+ Nothing -> 0+ Just i -> i + case incCur of+ IncludeCurrent -> 0+ ExcludeCurrent -> 1+ es = drop start $ l ^. L.listElementsL+ in skipForward start es++-- | Search backward for the first element index that fails the test+searchListBackward :: Searchable t+ => IncludeCurrent+ -> (e -> Bool) -- ^ The test+ -> L.GenericList n t e+ -> Maybe Int -- ^ The first element index that fails the test+searchListBackward incCur test l = let+ skipBackward idx es = case viewLast es of+ Nothing -> Nothing+ Just (es', e) -> if test e+ then skipBackward (idx-1) es'+ else Just idx+ -- Start with the current list element index + 1 so that the current index is included. If the current element is+ -- excluded, don't add 1 to the current index.+ start = case l ^. L.listSelectedL of+ Nothing -> 0+ Just i -> i + case incCur of+ IncludeCurrent -> 1+ ExcludeCurrent -> 0+ es = take start $ l ^. L.listElementsL+ in skipBackward (start-1) es++-- | Search backward for the first element that fails the test.+--+-- The current element is not included in the search.+--+-- If backward search fails, no change is made.+listSkipBackward :: Searchable t+ => (e -> Bool) -- ^ The test+ -> L.GenericList n t e+ -> L.GenericList n t e+listSkipBackward test l = case searchListBackward ExcludeCurrent test l of+ Nothing -> l+ Just idx -> l & L.listSelectedL ?~ idx++-- | Search forward for the first element that fails the test.+--+-- The current element is not included in the search.+--+-- If forward search fails, no change is made.+listSkipForward :: Searchable t+ => (e -> Bool) -- ^ The test+ -> L.GenericList n t e+ -> L.GenericList n t e+listSkipForward test l = case searchListForward ExcludeCurrent test l of+ Nothing -> l+ Just idx -> l & L.listSelectedL ?~ idx++-- | Move by a (fractional) number of pages in the list. If the number of pages to move by is 0, no change is made.+-- Otherwise, call 'L.listMoveByPages' with the number of pages to move by.+--+-- The element chosen by 'L.listMoveByPages' is included in the search.+--+-- After calling 'L.listMoveByPages', if the number of pages to move by was positive, search forward for the first+-- element that fails the test. If forward search fails, then search backward for the first such element. If backward+-- search fails too, cancel movements made by this function.+--+-- After calling 'L.listMoveByPages', if the number of pages to move by was negative, search backward for the first+-- element that fails the test. If backward search fails, then search forward for the first such element. If forward+-- search fails too, cancel movements made by this function.+listSkipByPages :: (Searchable t, Foldable t, L.Splittable t, Ord n, RealFrac pages)+ => (e -> Bool) -- ^ The test+ -> pages -- ^ Pages to move by+ -> EventM n (L.GenericList n t e) ()+listSkipByPages test p = if p == 0+ then return ()+ else do+ origL <- get+ L.listMoveByPages p+ let (searchList, searchListOpposite) = if p < 0+ then (searchListBackward, searchListForward)+ else (searchListForward, searchListBackward)+ modify $ \l -> case searchList IncludeCurrent test l of+ Nothing -> case searchListOpposite ExcludeCurrent test l of+ Nothing -> origL+ Just idx -> l & L.listSelectedL ?~ idx+ Just idx -> l & L.listSelectedL ?~ idx++-- | From the first element, search forward for the first element that fails the test.+--+-- The first element is included in the search.+--+-- If forward search fails, no change is made.+listSkipToBeginning :: (Searchable t, Foldable t, L.Splittable t)+ => (e -> Bool) -- ^ The test+ -> L.GenericList n t e+ -> L.GenericList n t e+listSkipToBeginning test l = case searchListForward IncludeCurrent test (L.listMoveToBeginning l) of+ Nothing -> l+ Just idx -> l & L.listSelectedL ?~ idx++-- | From the last element, search backward for the first element that fails the test.+--+-- The last element is included in the search.+--+-- If backward search fails, no change is made.+listSkipToEnd :: (Searchable t, Foldable t, L.Splittable t)+ => (e -> Bool) -- ^ The test+ -> L.GenericList n t e+ -> L.GenericList n t e+listSkipToEnd test l = case searchListBackward IncludeCurrent test (L.listMoveToEnd l) of+ Nothing -> l+ Just idx -> l & L.listSelectedL ?~ idx++-- | If searching backward for a list element that fails the test fails, show as many list elements as possible above+-- the current element.+--+-- The current list element is not included in the backward search.+--+-- Call this after moving backward by calling one of list manipulation functions.+listShowAbove :: Searchable t+ => (e -> Bool) -- ^ The test+ -> EventM n (L.GenericList n t e) ()+listShowAbove test = do+ l <- get+ case searchListBackward ExcludeCurrent test l of+ Nothing -> vScrollToBeginning $ viewportScroll $ l ^. L.listNameL+ Just _ -> return ()++-- | If searching forward for a list element that fails the test fails, show as many list elements as possible below+-- the current element.+--+-- The current list element is not included in the forward search.+--+-- Call this after moving forward by calling one of list manipulation functions.+listShowBelow :: Searchable t+ => (e -> Bool) -- ^ The test+ -> EventM n (L.GenericList n t e) ()+listShowBelow test = do+ l <- get+ case searchListForward ExcludeCurrent test l of+ Nothing -> vScrollToEnd $ viewportScroll $ l ^. L.listNameL+ Just _ -> return ()++-- | Functions for searching elements.+class Searchable (t :: * -> *) where+ -- | Get the head and the rest+ viewHead :: t a -> Maybe (a, t a)+ -- | Get the last element and the rest+ viewLast :: t a -> Maybe (t a, a)+ -- | Take a number of elements from the beginning+ take :: Int -> t a -> t a+ -- | Drop a number of elements from the beginning+ drop :: Int -> t a -> t a++-- | O(1) for all operations+instance Searchable Vector where+ viewHead = uncons+ viewLast = unsnoc+ take = V.take+ drop = V.drop++-- | O(1) for viewHead and viewLast. O(log(min(i,n-i))) for take and drop.+instance Searchable Seq where+ viewHead s = case S.viewl s of+ EmptyL -> Nothing+ a :< s' -> Just (a, s')+ viewLast s = case S.viewr s of+ EmptyR -> Nothing+ s' :> a -> Just (s', a)+ take = S.take+ drop = S.drop