packages feed

brick 1.0 → 1.1

raw patch · 8 files changed

+248/−30 lines, 8 filesdep ~containersPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: containers

API changes (from Hackage documentation)

+ Brick.Keybindings.KeyConfig: keyEventMappings :: (Ord k, Eq k) => KeyConfig k -> [(Binding, Set k)]
+ Brick.Widgets.FileBrowser: instance Brick.Widgets.Core.Named (Brick.Widgets.FileBrowser.FileBrowser n) n
- Brick.BorderMap: eBottomL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.BorderMap: eBottomL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.BorderMap: eLeftL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.BorderMap: eLeftL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.BorderMap: eRightL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.BorderMap: eRightL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.BorderMap: eTopL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.BorderMap: eTopL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.Keybindings.KeyDispatcher: keyDispatcher :: Ord k => KeyConfig k -> [KeyEventHandler k m] -> KeyDispatcher k m
+ Brick.Keybindings.KeyDispatcher: keyDispatcher :: Ord k => KeyConfig k -> [KeyEventHandler k m] -> Either [(Binding, [KeyHandler k m])] (KeyDispatcher k m)
- Brick.Types: availHeightL :: forall n_axz2. Lens' (Context n_axz2) Int
+ Brick.Types: availHeightL :: forall n_axFd. Lens' (Context n_axFd) Int
- Brick.Types: availWidthL :: forall n_axz2. Lens' (Context n_axz2) Int
+ Brick.Types: availWidthL :: forall n_axFd. Lens' (Context n_axFd) Int
- Brick.Types: bordersL :: forall n_axz6. Lens' (Result n_axz6) (BorderMap DynBorder)
+ Brick.Types: bordersL :: forall n_axFh. Lens' (Result n_axFh) (BorderMap DynBorder)
- Brick.Types: ctxAttrMapL :: forall n_axz2. Lens' (Context n_axz2) AttrMap
+ Brick.Types: ctxAttrMapL :: forall n_axFd. Lens' (Context n_axFd) AttrMap
- Brick.Types: ctxAttrNameL :: forall n_axz2. Lens' (Context n_axz2) AttrName
+ Brick.Types: ctxAttrNameL :: forall n_axFd. Lens' (Context n_axFd) AttrName
- Brick.Types: ctxBorderStyleL :: forall n_axz2. Lens' (Context n_axz2) BorderStyle
+ Brick.Types: ctxBorderStyleL :: forall n_axFd. Lens' (Context n_axFd) BorderStyle
- Brick.Types: ctxDynBordersL :: forall n_axz2. Lens' (Context n_axz2) Bool
+ Brick.Types: ctxDynBordersL :: forall n_axFd. Lens' (Context n_axFd) Bool
- Brick.Types: ctxHScrollBarOrientationL :: forall n_axz2. Lens' (Context n_axz2) (Maybe HScrollBarOrientation)
+ Brick.Types: ctxHScrollBarOrientationL :: forall n_axFd. Lens' (Context n_axFd) (Maybe HScrollBarOrientation)
- Brick.Types: ctxHScrollBarRendererL :: forall n_axz2. Lens' (Context n_axz2) (Maybe (ScrollbarRenderer n_axz2))
+ Brick.Types: ctxHScrollBarRendererL :: forall n_axFd. Lens' (Context n_axFd) (Maybe (ScrollbarRenderer n_axFd))
- Brick.Types: ctxVScrollBarOrientationL :: forall n_axz2. Lens' (Context n_axz2) (Maybe VScrollBarOrientation)
+ Brick.Types: ctxVScrollBarOrientationL :: forall n_axFd. Lens' (Context n_axFd) (Maybe VScrollBarOrientation)
- Brick.Types: ctxVScrollBarRendererL :: forall n_axz2. Lens' (Context n_axz2) (Maybe (ScrollbarRenderer n_axz2))
+ Brick.Types: ctxVScrollBarRendererL :: forall n_axFd. Lens' (Context n_axFd) (Maybe (ScrollbarRenderer n_axFd))
- Brick.Types: cursorLocationL :: forall n_axz7. Lens' (CursorLocation n_axz7) Location
+ Brick.Types: cursorLocationL :: forall n_axFi. Lens' (CursorLocation n_axFi) Location
- Brick.Types: cursorLocationNameL :: forall n_axz7 n_azHU. Lens (CursorLocation n_axz7) (CursorLocation n_azHU) (Maybe n_axz7) (Maybe n_azHU)
+ Brick.Types: cursorLocationNameL :: forall n_axFi n_azO5. Lens (CursorLocation n_axFi) (CursorLocation n_azO5) (Maybe n_axFi) (Maybe n_azO5)
- Brick.Types: cursorsL :: forall n_axz6. Lens' (Result n_axz6) [CursorLocation n_axz6]
+ Brick.Types: cursorsL :: forall n_axFh. Lens' (Result n_axFh) [CursorLocation n_axFh]
- Brick.Types: eBottomL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.Types: eBottomL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.Types: eLeftL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.Types: eLeftL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.Types: eRightL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.Types: eRightL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.Types: eTopL :: forall a_aeiU. Lens' (Edges a_aeiU) a_aeiU
+ Brick.Types: eTopL :: forall a_aeqk. Lens' (Edges a_aeqk) a_aeqk
- Brick.Types: extentsL :: forall n_axz6. Lens' (Result n_axz6) [Extent n_axz6]
+ Brick.Types: extentsL :: forall n_axFh. Lens' (Result n_axFh) [Extent n_axFh]
- Brick.Types: imageL :: forall n_axz6. Lens' (Result n_axz6) Image
+ Brick.Types: imageL :: forall n_axFh. Lens' (Result n_axFh) Image
- Brick.Types: visibilityRequestsL :: forall n_axz6. Lens' (Result n_axz6) [VisibilityRequest]
+ Brick.Types: visibilityRequestsL :: forall n_axFh. Lens' (Result n_axFh) [VisibilityRequest]
- Brick.Types: windowHeightL :: forall n_axz2. Lens' (Context n_axz2) Int
+ Brick.Types: windowHeightL :: forall n_axFd. Lens' (Context n_axFd) Int
- Brick.Types: windowWidthL :: forall n_axz2. Lens' (Context n_axz2) Int
+ Brick.Types: windowWidthL :: forall n_axFd. Lens' (Context n_axFd) Int
- Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1hXl a_a1hXZ. Lens (Dialog a_a1hXl) (Dialog a_a1hXZ) [(String, a_a1hXl)] [(String, a_a1hXZ)]
+ Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1i3s a_a1i46. Lens (Dialog a_a1i3s) (Dialog a_a1i46) [(String, a_a1i3s)] [(String, a_a1i46)]
- Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1hXl. Lens' (Dialog a_a1hXl) (Maybe Int)
+ Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1i3s. Lens' (Dialog a_a1i3s) (Maybe Int)
- Brick.Widgets.Dialog: dialogTitleL :: forall a_a1hXl. Lens' (Dialog a_a1hXl) (Maybe String)
+ Brick.Widgets.Dialog: dialogTitleL :: forall a_a1i3s. Lens' (Dialog a_a1i3s) (Maybe String)
- Brick.Widgets.Dialog: dialogWidthL :: forall a_a1hXl. Lens' (Dialog a_a1hXl) Int
+ Brick.Widgets.Dialog: dialogWidthL :: forall a_a1i3s. Lens' (Dialog a_a1i3s) Int
- Brick.Widgets.Edit: editContentsL :: forall t_a1c0h n_a1c0i t_a1c0W. Lens (Editor t_a1c0h n_a1c0i) (Editor t_a1c0W n_a1c0i) (TextZipper t_a1c0h) (TextZipper t_a1c0W)
+ Brick.Widgets.Edit: editContentsL :: forall t_a1c6o n_a1c6p t_a1c73. Lens (Editor t_a1c6o n_a1c6p) (Editor t_a1c73 n_a1c6p) (TextZipper t_a1c6o) (TextZipper t_a1c73)
- Brick.Widgets.FileBrowser: fileBrowserEntriesG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) (List n_a1tbq FileInfo)
+ Brick.Widgets.FileBrowser: fileBrowserEntriesG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) (List n_a1thx FileInfo)
- Brick.Widgets.FileBrowser: fileBrowserEntryFilterG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) (Maybe (FileInfo -> Bool))
+ Brick.Widgets.FileBrowser: fileBrowserEntryFilterG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) (Maybe (FileInfo -> Bool))
- Brick.Widgets.FileBrowser: fileBrowserExceptionG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) (Maybe IOException)
+ Brick.Widgets.FileBrowser: fileBrowserExceptionG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) (Maybe IOException)
- Brick.Widgets.FileBrowser: fileBrowserLatestResultsG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) [FileInfo]
+ Brick.Widgets.FileBrowser: fileBrowserLatestResultsG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) [FileInfo]
- Brick.Widgets.FileBrowser: fileBrowserNameG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) n_a1tbq
+ Brick.Widgets.FileBrowser: fileBrowserNameG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) n_a1thx
- Brick.Widgets.FileBrowser: fileBrowserSearchStringG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) (Maybe Text)
+ Brick.Widgets.FileBrowser: fileBrowserSearchStringG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) (Maybe Text)
- Brick.Widgets.FileBrowser: fileBrowserSelectableG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) (FileInfo -> Bool)
+ Brick.Widgets.FileBrowser: fileBrowserSelectableG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) (FileInfo -> Bool)
- Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1tbq. Lens' (FileBrowser n_a1tbq) (FileInfo -> Bool)
+ Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1thx. Lens' (FileBrowser n_a1thx) (FileInfo -> Bool)
- Brick.Widgets.FileBrowser: fileBrowserSelectedFilesG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) (Set String)
+ Brick.Widgets.FileBrowser: fileBrowserSelectedFilesG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) (Set String)
- Brick.Widgets.FileBrowser: fileBrowserWorkingDirectoryG :: forall n_a1tbq. SimpleGetter (FileBrowser n_a1tbq) FilePath
+ Brick.Widgets.FileBrowser: fileBrowserWorkingDirectoryG :: forall n_a1thx. SimpleGetter (FileBrowser n_a1thx) FilePath
- Brick.Widgets.List: listElementsL :: forall n_a1nZK t_a1nZL e_a1nZM t_a1odX e_a1odY. Lens (GenericList n_a1nZK t_a1nZL e_a1nZM) (GenericList n_a1nZK t_a1odX e_a1odY) (t_a1nZL e_a1nZM) (t_a1odX e_a1odY)
+ Brick.Widgets.List: listElementsL :: forall n_a1o5R t_a1o5S e_a1o5T t_a1ok4 e_a1ok5. Lens (GenericList n_a1o5R t_a1o5S e_a1o5T) (GenericList n_a1o5R t_a1ok4 e_a1ok5) (t_a1o5S e_a1o5T) (t_a1ok4 e_a1ok5)
- Brick.Widgets.List: listItemHeightL :: forall n_a1nZK t_a1nZL e_a1nZM. Lens' (GenericList n_a1nZK t_a1nZL e_a1nZM) Int
+ Brick.Widgets.List: listItemHeightL :: forall n_a1o5R t_a1o5S e_a1o5T. Lens' (GenericList n_a1o5R t_a1o5S e_a1o5T) Int
- Brick.Widgets.List: listNameL :: forall n_a1nZK t_a1nZL e_a1nZM n_a1odZ. Lens (GenericList n_a1nZK t_a1nZL e_a1nZM) (GenericList n_a1odZ t_a1nZL e_a1nZM) n_a1nZK n_a1odZ
+ Brick.Widgets.List: listNameL :: forall n_a1o5R t_a1o5S e_a1o5T n_a1ok6. Lens (GenericList n_a1o5R t_a1o5S e_a1o5T) (GenericList n_a1ok6 t_a1o5S e_a1o5T) n_a1o5R n_a1ok6
- Brick.Widgets.List: listSelectedL :: forall n_a1nZK t_a1nZL e_a1nZM. Lens' (GenericList n_a1nZK t_a1nZL e_a1nZM) (Maybe Int)
+ Brick.Widgets.List: listSelectedL :: forall n_a1o5R t_a1o5S e_a1o5T. Lens' (GenericList n_a1o5R t_a1o5S e_a1o5T) (Maybe Int)

Files

CHANGELOG.md view
@@ -2,6 +2,26 @@ Brick changelog --------------- +1.1+---++API changes:+* `keyDispatcher` now returns `Either` to fail with collision+  information if collisions are detected due to overloaded keybindings.+  This fixes a critical problem in `KeyDispatcher` where it would+  previously silently ignore all but one handler for a specified key+  if the key configuration resulted in the same key being mapped to+  multiple handlers (either by event or by statically specified key).+* Added `Brick.Keybindings.KeyConfig.keyEventMappings` to allow+  applications to check for colliding bindings at the key configuration+  level.++Other changes:+* The User Guide got a new subsection on keybinding collisions.+* `programs/CustomKeybindingDemo.hs` got additional code to demonstrate+  how to check for and deal with keybinding collisions.+* `FileBrowser` got a `Named` instance.+ 1.0 --- @@ -80,6 +100,8 @@ * `Brick.Widgets.List` got `listSelectedElementL`, a traversal for   accessing the currently selected element of a list. (Thanks Fraser   Tweedale)+* The `MonadFail` derived instance for `EventM` was removed for GHC >=+  8.8.  0.73 ----
brick.cabal view
@@ -1,5 +1,5 @@ name:                brick-version:             1.0+version:             1.1 synopsis:            A declarative terminal user interface library description:   Write terminal user interfaces (TUIs) painlessly with 'brick'! You@@ -155,6 +155,7 @@                        brick,                        text,                        vty,+                       containers,                        microlens,                        microlens-mtl,                        microlens-th
docs/guide.rst view
@@ -1687,6 +1687,87 @@ |                     |                        |    handler is run.      | +---------------------+------------------------+-------------------------+ +Keybinding Collisions+---------------------++An important issue to consider in using the custom keybinding API is+that it is possible for the user to map the same key to more than one+event. We refer to this situation as a *keybinding collision*. Whether+the collision represents a problem depends on how the events in question+are going to be handled by the application. This section provides an+example scenario and describes a way to deal with this situation.++Suppose an application has two key events:++.. code:: haskell++   data KeyEvent = QuitEvent+                 | CloseWindowEvent++   allKeyEvents :: KeyEvents KeyEvent+   allKeyEvents =+       K.keyEvents [ ("quit",         QuitEvent)+                   , ("close-window", CloseWindowEvent)+                   ]++   defaultBindings :: [(KeyEvent, [Binding])]+   defaultBindings =+       [ (QuitEvent,        [ctrl 'q'])+       , (CloseWindowEvent, [bind KEsc])+       ]++Suppose also that the application using the above key events has a+feature that opens a window, and that ``CloseWindowEvent`` is used to+close the window, while ``QuitEvent`` is used to quit the application.++A user might then provide a custom INI file to rebind keys as follows::++   [keybindings]+   quit = Esc+   close-window = Esc++While this is a valid configuration for the user to provide, it would+result in a keybinding collision for ``Esc`` since it is now bound+to two events. Whether that's a problem depends entirely on how+``QuitEvent`` and ``CloseWindowEvent`` are handled:++* If the application handles both events in the same event handler,+  the ``KeyDispatcher`` for those events would fail to construct since+  ``Esc`` maps to more than one event. Building a ``KeyDispatcher``+  from a ``KeyConfig`` with such a collision would fail and return+  information about the collisions.+* If the application handles the two events in different dispatchers+  then the collision has no effect and is not a problem since different+  ``KeyDispatcher`` values would be constructed to handle the events+  separately. This could happen, for instance, if the application only+  ever handled ``CloseWindowEvent`` when the window in question was+  open and only handled ``QuitEvent`` when the window had been closed.+  This kind of "modal" approach to handling events means that we only+  consider a key to have a collision if it is bound to two or more+  events that are handled in the same event handler.++There's also another situation that would be problematic, which is when+an abstract event like ``QuitEvent`` has a key mapping that+collides with a key handler that is bound to a specific key using+``Brick.Keybindings.KeyDispatcher.onKey`` rather than an event:++.. code:: haskell++    K.onKey (K.bind '\t') "Increment the counter by 10" $+        counter %= (+ 10)++If ``onKey`` is used, the handler it creates is only triggered by the+specified key (``Tab`` in the example above). But the handler may be+included alongside handlers in the same dispatcher that are *also*+triggered by ``Tab``, so if those event handlers were provided together+when creating a ``KeyDispatcher`` then it would fail to construct due to+the collision.++Brick provides ``Brick.Keybindings.KeyConfig.keyEventMappings`` to help+finding collisions at the key configuration level. Finding out about+collisions at the dispatcher level is possible by handling the failure+case when calling ``Brick.Keybindings.KeyDispatcher.keyDispatcher``.+ Joining Borders =============== 
programs/CustomKeybindingDemo.hs view
@@ -6,7 +6,9 @@ import Lens.Micro ((^.)) import Lens.Micro.TH (makeLenses) import Lens.Micro.Mtl ((<~), (.=), (%=), use)-import Control.Monad (void)+import Control.Monad (void, forM_, when)+import qualified Data.Set as S+import Data.Maybe (fromJust) import qualified Data.Text as Text import qualified Data.Text.IO as Text #if !(MIN_VERSION_base(4,11,0))@@ -60,6 +62,8 @@        , _customBindingsPath :: Maybe FilePath        -- ^ Set if the application found custom keybindings in the        -- specified file.+       , _dispatcher :: K.KeyDispatcher KeyEvent (T.EventM () St)+       -- ^ The key dispatcher we'll use to dispatch input events.        }  makeLenses ''St@@ -90,8 +94,7 @@     -- Dispatch the key to the event handler to which the key is mapped,     -- if any. Also record in lastKeyHandled whether the dispatcher     -- found a matching handler.-    kc <- use keyConfig-    let d = K.keyDispatcher kc handlers+    d <- use dispatcher     lastKey .= Just (k, mods)     lastKeyHandled <~ K.handleKey d k mods appEvent _ =@@ -187,11 +190,52 @@     -- the custom bindings we loaded from the INI file, if any.     let kc = K.newKeyConfig allKeyEvents defaultBindings customBindings +    -- Before starting the application, check on whether any events have+    -- colliding bindings. Exit if so.+    --+    -- Note that in a Real Application, we would more than likely+    -- want to check for collisions among specific sets of+    -- events. For example, if 'Esc' was bound to both 'quit' and+    -- 'close-dialog-box', we might not care about such a collision+    -- if the application only ever handled the 'close-dialog-box'+    -- event in a separate mode and only ever handled 'quit' at the+    -- top-level of the event handler. But if we had two events such as+    -- 'dialog-box-okay' and 'dialog-box-cancel' that were intended to+    -- be handled in the same mode, we might want to check that those+    -- two events did not have the same binding.+    forM_ (K.keyEventMappings kc) $ \(b, evs) -> do+        when (S.size evs > 1) $ do+            Text.putStrLn $ "Error: key '" <> K.ppBinding b <> "' is bound to multiple events:"+            forM_ evs $ \e ->+                Text.putStrLn $ "  " <> Text.pack (show e) <> " (" <> fromJust (K.keyEventName allKeyEvents e) <> ")"+            exitFailure++    -- Now build a key dispatcher for our event handlers. If this fails+    -- due to key collision detection, we'll print out info about the+    -- collisions.+    d <- case K.keyDispatcher kc handlers of+        Right d -> return d+        Left collisions -> do+            putStrLn "Error: some key events have the same keys bound to them."++            forM_ collisions $ \(b, hs) -> do+                Text.putStrLn $ "Handlers with the '" <> K.ppBinding b <> "' binding:"+                forM_ hs $ \h -> do+                    let trigger = case K.kehEventTrigger $ K.khHandler h of+                            K.ByKey k   -> "triggered by the key '" <> K.ppBinding k <> "'"+                            K.ByEvent e -> "triggered by the event '" <> fromJust (K.keyEventName allKeyEvents e) <> "'"+                        desc = K.handlerDescription $ K.kehHandler $ K.khHandler h++                    Text.putStrLn $ "  " <> desc <> " (" <> trigger <> ")"++            exitFailure+     void $ M.defaultMain app $ St { _keyConfig = kc                                   , _lastKey = Nothing                                   , _lastKeyHandled = False                                   , _counter = 0                                   , _customBindingsPath = customFile+                                  , _dispatcher = d                                   }      -- Now demonstrate how the library's generated key binding help text
src/Brick/Keybindings/KeyConfig.hs view
@@ -4,6 +4,12 @@ -- -- To get started, see 'newKeyConfig'. Once a 'KeyConfig' has been -- constructed, see 'Brick.Keybindings.KeyHandlerMap.keyDispatcher'.+--+-- Since a key configuration can have keys bound to multiple events, it+-- is the application author's responsibility to check for collisions+-- since the nature of the collisions will depend on how the application+-- is implemented. To check for collisions, use the result of+-- 'keyEventMappings'. module Brick.Keybindings.KeyConfig   ( KeyConfig   , newKeyConfig@@ -23,6 +29,7 @@   , firstActiveBinding   , allDefaultBindings   , allActiveBindings+  , keyEventMappings    -- * Misc   , keyConfigEvents@@ -32,8 +39,11 @@  import Data.List (nub) import qualified Data.Map.Strict as M+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif import qualified Data.Set as S-import Data.Maybe (fromMaybe, listToMaybe)+import Data.Maybe (fromMaybe, listToMaybe, catMaybes) import qualified Graphics.Vty as Vty  import Brick.Keybindings.KeyEvents@@ -90,9 +100,14 @@ --   event to either 'Unbound' or providing explicit overridden bindings --   with 'BindingList'. data KeyConfig k =-    KeyConfig { keyConfigBindingMap :: M.Map k BindingState-              -- ^ The map of custom bindings for events with custom-              -- bindings.+    KeyConfig { keyConfigCustomBindings :: [(k, BindingState)]+              -- ^ The list of custom binding states for events with+              -- custom bindings. We use a list to ensure that we+              -- preserve key bindings for keys that are mapped to more+              -- than one event. This may be valid or invalid depending+              -- on the events in question; whether those bindings+              -- constitute a collision is up to the application+              -- developer to check.               , keyConfigEvents :: KeyEvents k               -- ^ The base mapping of events and their names that is               -- used in this configuration.@@ -116,19 +131,50 @@              -- ^ Custom bindings by key event, such as from a              -- configuration file. Explicitly setting an event to              -- 'Unbound' here has the effect of disabling its default-             -- bindings. Optional on a per-event basis.+             -- bindings. Optional on a per-event basis. Note that this+             -- function does not check for collisions since it is up to+             -- the application to determine whether a key bound to more+             -- than one event constitutes a collision!              -> KeyConfig k newKeyConfig evs defaults bindings =-    KeyConfig { keyConfigBindingMap = M.fromList bindings+    KeyConfig { keyConfigCustomBindings = bindings               , keyConfigEvents = evs               , keyConfigDefaultBindings = M.fromList defaults               } +-- | Return a list of mappings including each key bound to any event+-- combined with the list of events to which it is bound. This is useful+-- for identifying problematic key binding collisions. Since key binding+-- collisions cannot be determined in general, we leave it up to the+-- application author to determine which key-to-event bindings are+-- problematic.+keyEventMappings :: (Ord k, Eq k) => KeyConfig k -> [(Binding, S.Set k)]+keyEventMappings kc = M.toList resultMap+    where+        -- Get all default bindings+        defaultBindings = M.toList $ keyConfigDefaultBindings kc+        -- Get all explicitly unbound events+        explicitlyUnboundEvents = fmap fst $ filter ((== Unbound) . snd) $ keyConfigCustomBindings kc+        -- Remove explicitly unbound events from the default set of+        -- bindings+        defaultBindingsWithoutUnbound = filter ((`notElem` explicitlyUnboundEvents) . fst) defaultBindings+        -- Now get customized binding lists+        customizedKeybindingLists = catMaybes $ (flip fmap) (keyConfigCustomBindings kc) $ \(k, bState) -> do+            case bState of+                Unbound -> Nothing+                BindingList bs -> Just (k, bs)+        -- Now build a map from binding to event list+        allPairs = defaultBindingsWithoutUnbound <>+                   customizedKeybindingLists+        addBindings m (ev, bs) =+            M.unionWith S.union m $ M.fromList [(b, S.singleton ev) | b <- bs]+        resultMap = foldl addBindings mempty allPairs+ -- | Look up the binding state for the specified event. This returns -- 'Nothing' when the event has no explicitly configured custom -- 'BindingState'. lookupKeyConfigBindings :: (Ord k) => KeyConfig k -> k -> Maybe BindingState-lookupKeyConfigBindings kc e = M.lookup e $ keyConfigBindingMap kc+lookupKeyConfigBindings kc e = lookup e $ keyConfigCustomBindings kc  -- | A convenience function to return the first result of -- 'allDefaultBindings', if any.
src/Brick/Keybindings/KeyDispatcher.hs view
@@ -15,14 +15,17 @@ --   If desired, provide custom keybindings to 'newKeyConfig' from --   within the program or load them from an INI file with routines like --   'Brick.Keybindings.Parse.keybindingsFromFile'.+-- * Optionally check for configuration-wide keybinding collisions with+--   'Brick.Keybindings.KeyConfig.keyEventMappings'. -- * Implement application event handlers that will be run in response --   to either specific hard-coded keys or events @k@, both in some --   monad @m@ of your choosing, using constructors 'onKey' and --   'onEvent'. -- * Use the created 'KeyConfig' and handlers to create a---   'KeyDispatcher' with 'keyDispatcher'.+--   'KeyDispatcher' with 'keyDispatcher', dealing with collisions if+--   they arise. -- * As user input events arrive, dispatch them to the appropriate---   handler using 'handleKey'.+--   handler in the dispatcher using 'handleKey'. module Brick.Keybindings.KeyDispatcher   ( -- * Key dispatching     KeyDispatcher@@ -49,14 +52,16 @@ import qualified Data.Set as S import qualified Data.Text as T import qualified Graphics.Vty as Vty+import Data.Function (on)+import Data.List (groupBy, sortBy)  import Brick.Keybindings.KeyConfig --- | A set of handlers for specific keys whose handlers run in the monad--- @m@.+-- | A dispatcher keys that map to abstract events @k@ and whose+-- handlers run in the monad @m@. newtype KeyDispatcher k m = KeyDispatcher (M.Map Binding (KeyHandler k m)) --- | An 'Handler' represents a handler implementation to be invoked in+-- | A 'Handler' represents a handler implementation to be invoked in -- response to some event that runs in the monad @m@. -- -- In general, you should never need to make one of these manually.@@ -71,8 +76,8 @@  -- | A handler for a specific key. ----- In general, you should never need to create one of these. The--- internals are exposed to make inspection easy.+-- In general, you should never need to create one of these manually.+-- The internals are exposed to make inspection easy. data KeyHandler k m =     KeyHandler { khHandler :: KeyEventHandler k m                -- ^ The handler to invoke. Note that this maintains@@ -119,7 +124,13 @@         Nothing -> return False  -- | Build a 'KeyDispatcher' to dispatch keys to handle events of type--- @k@ using actions in a Monad @m@.+-- @k@ using actions in a Monad @m@. If any collisions are detected,+-- this fails with 'Left' and returns the list of colliding event+-- handlers for each overloaded binding. (Each returned 'KeyHandler'+-- contains the original 'KeyEventHandler' that was used to build it so+-- those can be inspected to understand which handlers are mapped to the+-- same key, either via an abstract key event using 'onEvent' or via a+-- statically configured key using 'onKey'.) -- -- This works by taking a list of abstract 'KeyEventHandler's and -- building a 'KeyDispatcher' of event handlers based on specific Vty@@ -134,8 +145,18 @@ keyDispatcher :: (Ord k)               => KeyConfig k               -> [KeyEventHandler k m]-              -> KeyDispatcher k m-keyDispatcher conf ks = KeyDispatcher $ M.fromList $ buildKeyDispatcherPairs ks conf+              -> Either [(Binding, [KeyHandler k m])] (KeyDispatcher k m)+keyDispatcher conf ks =+    let pairs = buildKeyDispatcherPairs ks conf+        groups = groupBy ((==) `on` fst) $ sortBy (compare `on` fst) pairs+        badGroups = filter ((> 1) . length) groups+        combine :: [(Binding, KeyHandler k m)] -> (Binding, [KeyHandler k m])+        combine as =+            let b = fst $ head as+            in (b, snd <$> as)+    in if null badGroups+       then Right $ KeyDispatcher $ M.fromList pairs+       else Left $ combine <$> badGroups  -- | Convert a key dispatcher to a list of pairs of bindings and their -- handlers.
src/Brick/Main.hs view
@@ -110,11 +110,8 @@         -- application state is what you probably want to use to decide         -- which one wins.         , appHandleEvent :: BrickEvent n e -> EventM n s ()-        -- ^ This function takes the current application state and an-        -- event and returns an action to be taken and a corresponding-        -- transformed application state. Possible options are-        -- 'continue', 'continueWithoutRedraw', 'suspendAndResume', and-        -- 'halt'.+        -- ^ This function handles an event and updates the current+        -- application state.         , appStartEvent :: EventM n s ()         -- ^ This function gets called once just prior to the first         -- drawing of your application. Here is where you can make@@ -588,10 +585,11 @@ -- state without redrawing the screen. This is faster than 'continue' -- because it skips the redraw, but the drawback is that you need to -- be really sure that you don't want a screen redraw. If your state--- changed in a way that needs to be reflected on the screen, use--- 'continue'. This function is for cases where you know that you did--- something that won't have an impact on the screen state and you want--- to save on redraw cost.+-- changed in a way that needs to be reflected on the screen, just don't+-- call this; 'EventM' blocks default to triggering redraws when they+-- finish executing. This function is for cases where you know that you+-- did something that won't have an impact on the screen state and you+-- want to save on redraw cost. continueWithoutRedraw :: EventM n s () continueWithoutRedraw =     EventM $ lift $ lift $ modify $ \es -> es { nextAction = ContinueWithoutRedraw }
src/Brick/Widgets/FileBrowser.hs view
@@ -3,6 +3,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} -- | This module provides a file browser widget that allows users to -- navigate directory trees, search for files and directories, and -- select entries of interest. For a complete working demonstration of@@ -198,6 +200,9 @@                 -- Note that this is a record field so it can be used to                 -- change the selection function.                 }++instance Named (FileBrowser n) n where+    getName = getName . fileBrowserEntries  -- | File status information. data FileStatus =