brick 0.2.3 → 2.12
raw patch · 79 files changed
Files
- CHANGELOG.md +1959/−94
- LICENSE +1/−1
- README.md +184/−69
- brick.cabal +386/−100
- docs/guide.rst +2144/−864
- docs/snake-demo.gif binary
- programs/AnimationDemo.hs +223/−0
- programs/AttrDemo.hs +36/−21
- programs/BorderDemo.hs +34/−28
- programs/CacheDemo.hs +81/−0
- programs/CroppingDemo.hs +61/−0
- programs/CustomEventDemo.hs +35/−26
- programs/CustomKeybindingDemo.hs +253/−0
- programs/DialogDemo.hs +24/−14
- programs/DynamicBorderDemo.hs +71/−0
- programs/EditDemo.hs +43/−43
- programs/EditorLineNumbersDemo.hs +135/−0
- programs/FileBrowserDemo.hs +109/−0
- programs/FillDemo.hs +16/−0
- programs/FormDemo.hs +166/−0
- programs/HelloWorldDemo.hs +1/−1
- programs/LayerDemo.hs +72/−34
- programs/ListDemo.hs +33/−27
- programs/ListViDemo.hs +96/−0
- programs/MarkupDemo.hs +0/−42
- programs/MouseDemo.hs +147/−0
- programs/PaddingDemo.hs +13/−15
- programs/ProgressBarDemo.hs +153/−0
- programs/ReadmeDemo.hs +16/−0
- programs/SuspendAndResumeDemo.hs +21/−15
- programs/TableDemo.hs +63/−0
- programs/TabularListDemo.hs +146/−0
- programs/TailDemo.hs +151/−0
- programs/TextWrapDemo.hs +25/−0
- programs/ThemeDemo.hs +86/−0
- programs/ViewportScrollDemo.hs +35/−37
- programs/ViewportScrollbarsDemo.hs +176/−0
- programs/VisibilityDemo.hs +35/−36
- programs/custom_keys.ini +4/−0
- src/Brick.hs +7/−1
- src/Brick/Animation.hs +588/−0
- src/Brick/Animation/Clock.hs +64/−0
- src/Brick/AttrMap.hs +96/−44
- src/Brick/BChan.hs +44/−0
- src/Brick/BorderMap.hs +225/−0
- src/Brick/Focus.hs +86/−42
- src/Brick/Forms.hs +970/−0
- src/Brick/Keybindings.hs +20/−0
- src/Brick/Keybindings/KeyConfig.hs +258/−0
- src/Brick/Keybindings/KeyDispatcher.hs +241/−0
- src/Brick/Keybindings/KeyEvents.hs +52/−0
- src/Brick/Keybindings/Normalize.hs +14/−0
- src/Brick/Keybindings/Parse.hs +182/−0
- src/Brick/Keybindings/Pretty.hs +248/−0
- src/Brick/Main.hs +512/−151
- src/Brick/Markup.hs +0/−54
- src/Brick/Themes.hs +410/−0
- src/Brick/Types.hs +92/−105
- src/Brick/Types/Common.hs +66/−0
- src/Brick/Types/EventM.hs +45/−0
- src/Brick/Types/Internal.hs +382/−83
- src/Brick/Types/TH.hs +11/−4
- src/Brick/Util.hs +11/−3
- src/Brick/Widgets/Border.hs +106/−66
- src/Brick/Widgets/Border/Style.hs +9/−7
- src/Brick/Widgets/Center.hs +80/−20
- src/Brick/Widgets/Core.hs +1984/−739
- src/Brick/Widgets/Dialog.hs +97/−71
- src/Brick/Widgets/Edit.hs +187/−59
- src/Brick/Widgets/FileBrowser.hs +948/−0
- src/Brick/Widgets/Internal.hs +174/−20
- src/Brick/Widgets/List.hs +580/−144
- src/Brick/Widgets/ProgressBar.hs +47/−12
- src/Brick/Widgets/Table.hs +389/−0
- src/Data/IMap.hs +185/−0
- src/Data/Text/Markup.hs +0/−74
- tests/List.hs +375/−0
- tests/Main.hs +117/−0
- tests/Render.hs +62/−0
CHANGELOG.md view
@@ -2,98 +2,1963 @@ Brick changelog --------------- -0.2.3--------Bug fixes:-* Fixed viewport behavior when the image in a viewport reduces its size- enough to render the viewport offsets invalid. Before, this behavior- caused a crash during image croppin in vty; now the behavior is- handled sanely (fixes #22; reported by Hans-Peter Deifel)--0.2.2--------Demo changes:-* Improved the list demo by using characters instead of integers in the- demo list and cleaned up item-adding code (thanks Jøhannes Lippmann- <code@schauderbasis.de>)--0.2.1--------Bug fixes:-* List:- * Fixed size policy of lists so that rather than being Fixed/Fixed,- they are Greedy/Greedy. This resolves issues that arise when the box- layout widget renders a list widget alongside a Fixed/Fixed one.- (Closes issue #17, thanks Karl Voelker)-* Scrolling:- * vScrollPage actually scrolls vertically now rather than horizontally- (Thanks Hans-Peter Deifel <hpd@hpdeifel.de>)--0.2------API changes:-* Added top-level `Brick` module that re-exports the most important- modules in the library.-* List:- * Now instead of passing the item-drawing function to the `list` state- constructor, it is passed to `renderList`- * `renderList` now takes the row height of the list's item widgets.- The list item-drawing function must respect this in order for- scrolling to work properly. This change made it possible to optimize- the list so that it only draws widgets visible in the viewport- rather than rendering all of the list's items (even the ones- off-screen). But to do this we must be able to tell in advance- how high each one is, so we require this parameter. In addition- this change means that lists no longer support items of different- heights.- * The list now uses Data.Vector instead of [a] to store items; this- permits efficient slicing so we can do the optimized rendering- described above.-* The `HandleEvent` type class `handleEvent` method now runs in- `EventM`. This permits event-handling code implemented in terms of- `HandleEvent` to do get access to viewport state and to run IO code,- making it just as powerful as code in the top-level `EventM` handler.-* Many types were moved from `Brick.Widgets.Core` and `Brick.Main` to- `Brick.Types`, making the former module merely a home for `Widget`- constructors and combinators.-* The `IsString` instance for `Widget` was removed; this might be- reinstated later, but this package provides enough `IsString`- instances that things can get confusing.-* `EventM` is now reader monad over the most recent rendering pass's- viewport state, in addition to being a state monad over viewport- requests for the renderer. Added the `lookupViewport` function to- provide access to the most recent viewport state. Exported the- `Viewport` type and lenses.-* Now that `handleEvent` is now an `EventM` action, composition with- `continue` et al got a little messier when using lenses to- update the application state. To help with this, there is now- `handleEventLensed`.--Bugfixes:-* Lists now perform well with 10 items or a million (see above; fixes- #7, thanks Simon Michael)-* Added more haddock notes to `Brick.Widgets.Core` about growth- policies.-* Forced evaluation of render states to address a space leak in the- renderer (fixes #14, thanks Sebastian Reuße <seb@wirrsal.net>)-* str: only reference string content that can be shown (eliminates a- space leak, fixes #14, thanks Sebastian Reuße <seb@wirrsal.net>)--Misc:-* Added a makefile for the user guide.-* List: added support for Home and End keys (thanks Simon Michael)-* Viewports: when rendering viewports, scroll requests from `EventM` are- processed before visibility requests from the rendering process; this- reverses this previous order of operations but permits user-supplied- event handlers to reset viewports when desired.--Package changes:-* Added `deepseq` dependency--0.1----+2.12+----++Package changes:++* Raised upper bound on microlens to allow building with 0.5.++2.11+----++Bug fixes:++* Fixed a bug in FileBrowser: if a user pressed Enter when the cursor+ was on a selected entry, it was omitted from the list of+ selected browser entries. As part of this change, the function+ `actionFileBrowserSelectCurrent` previously toggled the selection+ of the entry at the cursor, but should have selected it instead.+ It now does so, and a new function for toggling was introduced:+ `actionFileBrowserToggleCurrent`.++Other changes:++* Upper bounds on `base` and `microlens` were adjusted.++2.10+----++* Updated `brick` to build with `microlens == 0.5.0.0` which moved its+ Field* classes to `Lens.Micro.FieldN`.++2.9+---++API changes:+* Added `Brick.Widgets.List.listFindFirst` function.++2.8.3+-----++Bug fixes:++* Fixed a bug that completely broke `makeVisible` that was introduced+ in brick 2.6.+* Fixed context cropping in `cropRightBy` and `cropBottomBy`.++2.8.2+-----++* Updated `Brick.Widgets.Core` functions `cropBottomBy`, `cropToBy`,+ `cropLeftBy`, and `cropRightBy` to properly perform result cropping to+ actually address the internal bug fixed in 2.8.1.++2.8.1+-----++* Fixed a long-standing bug in `cropToContext` that resulted in some+ extents getting left around when they should be dropped, possibly+ leading to application bugs when handling mouse clicks in extent+ regions that should have been removed from the rendering result.++2.8+---++Behavior changes:+* `FileBrowser` file marking with `Space` now honors the file browser's+ configured file selector predicate.+* `FileBrowser` file marking with `Space` and `Enter` now toggles file+ selection rather than just selecting files, allowing for selected+ files to be unselected.++2.7+---++This release adds `Brick.Animation`, a module providing infrastructure+for adding animations to Brick interfaces. See the Haddock documentation+in `Brick.Animation` for full details; see `programs/AnimationDemo.hs`+for a working example.++2.6+---++Behavior changes:+ * `Brick.Widgets.Core.relativeTo` now draws nothing if the requested+ extent is not found. Previously it would draw the specified widget in+ the upper-left corner of the layer.++Bug fixes:+ * Fixed the conditional import in `BorderMap` (#519)+ * `Brick.Widgets.Center.hCenterWith` now properly accounts for centered+ image width when computing additional right padding (#520)+ * The Brick renderer now properly resets some render-specific state+ in between renderings that was previously kept around, avoiding+ preservation of stale extents across renderings+ * `brick-tail-demo` and `brick-custom-event-demo` now shut down Vty+ properly++2.5+---++New features:+* `Brick.Widgets.ProgressBar` got a new function, `customProgressBar`,+ which allows the customization of the fill characters used to draw a+ progress bar. (Thanks @sectore)++2.4+---++Changes:+* The `Keybindings` API now normalizes keybindings+ to lowercase when modifiers are present. (See also+ https://github.com/jtdaugherty/brick/issues/512) This means that,+ for example, a constructed binding for `C-X` would be normalized to+ `C-x`, and a binding from a configuration file written `C-X` would be+ parsed and then normalized to `C-x`. This is because, in general, when+ modifiers are present, input events are received for the lowercase+ version of the character in question. Prior to changing this, Brick+ would silently parse (or permit the construction of) uppercase-mapped+ key bindings, but in practice those bindings were unusable because+ they are not generated by terminals.++2.3.2+-----++Bug fixes:+* `FileBrowser`: if the `FileBrowser` was initialized with a `FilePath`+ that ended in a slash, then if the user hit `Enter` on the `../` entry+ to move to the parent directory, the only effect was the removal of+ that trailing slash. This change trims the trailing slash so that the+ expected move occurs whenever the `../` entry is selected.+* `Brick.Keybindings.Pretty.keybindingHelpWidget`: fixed a problem where+ a key event with no name in a `KeyEvents` would cause a `fromJust`+ exception. The pretty-printer now falls back to a placeholder+ representation for such unnamed key events.++2.3.1+-----++Bug fixes:+* Form field rendering now correctly checks for form field focus when+ its visibility mode is `ShowAugmentedField`.++2.3+---++API changes:+* `FormFieldVisibilityMode`'s `ShowAugmentedField` was renamed to+ `ShowCompositeField` to be clearer about what it does, and a new+ `ShowAugmentedField` constructor was added to support a mode where+ field augmentations applied with `@@=` are made visible as well.++2.2+---++Enhancements:+* `Brick.Forms` got a new `FormFieldVisibilityMode` type and a+ `setFieldVisibilityMode` function to allow greater control over+ how form field collections are brought into view when forms are+ rendered in viewports. Form fields will default to using the+ `ShowFocusedFieldOnly` mode which preserves functionality prior to+ this release. To get the new behavior, set a field's visibility mode+ to `ShowAugmentedField`.++2.1.1+-----++Bug fixes:+* `defaultMain` now properly shuts down Vty before it returns, fixing+ a bug where the terminal would be in an unclean state on return from+ `defaultMain`.++2.1+---++API changes:++* Added `Brick.Main.customMainWithDefaultVty` as an alternative way to+ initialize Brick.++2.0+---++This release updates Brick to support Vty 6, which includes support for+Windows.++Package changes:+* Increased lower bound on `vty` to 6.0.+* Added dependency on `vty-crossplatform`.+* Migrated from `unix` dependency to `unix-compat`.++Other changes:+* Update core library and demo programs to use `vty-crossplatform` to+ initialize the terminal.++1.10+----++API changes:+* The `ScrollbarRenderer` type got split up into vertical and horizontal+ versions, `VScrollbarRenderer` and `HScrollbarRenderer`, respectively.+ Their fields are nearly identical to the original `ScrollbarRenderer`+ fields except that many fields now have a `V` or `H` in them as+ appropriate. As part of this change, the various `Brick.Widgets.Core`+ functions that deal with the renderers got their types updated, and+ the types of the default scroll bar renderers changed, too.+* The scroll bar renderers now have a field to control how much space+ is allocated to a scroll bar. Previously, all scroll bars were+ assumed to be exactly one row in height or one column in width. This+ change is motivated by a desire to be able to control how scroll+ bars are rendered adjacent to viewport contents. It isn't always+ desirable to render them right up against the contents; sometimes,+ spacing would be nice between the bar and contents, for example.+ As part of this change, `VScrollbarRenderer` got a field called+ `scrollbarWidthAllocation` and `HScrollbarRenderer` got a field called+ `scrollbarHeightAllocation`. The fields specify the height (for+ horizontal scroll bars) or width (for vertical ones) of the region+ in which the bar is rendered, allowing scroll bar element widgets+ to take up more than one row in height (for horizontal scroll bars)+ or more than one column in width (for vertical ones) as desired. If+ the widgets take up less space, padding is added between the scroll+ bar and the viewport contents to pad the scroll bar to take up the+ specified allocation.++1.9+---++API changes:+* `FocusRing` got a `Show` instance.++1.8+---++API changes:+* Added `Brick.Widgets.Core.forceAttrAllowStyle`, which is like+ `forceAttr` but allows styles to be preserved rather than overridden.++Other improvements:+* The `Brick.Forms` documentation was updated to clarify how attributes+ get used for form fields.++1.7+---++Package changes:+* Allow building with `base` 4.18 (GHC 9.6) (thanks Mario Lang)++API changes:+* Added a new function, `Brick.Util.style`, to create a Vty `Attr` from+ a style value (thanks Amir Dekel)++Other improvements:+* `Brick.Forms.renderForm` now issues a visibility request for the+ focused form field, which makes forms usable within viewports.++1.6+---++Package changes:+* Support `mtl` 2.3 (thanks Daniel Firth)++API changes:+* `Brick.Widgets.Table` got a new `alignColumns` function that can be+ used to do column layout of a list of widgets using `ColumnAlignment`+ values from the table API.+* `Brick.Widgets.Table` got a new low-level table-rendering API for use+ in applications that want to use the table layout machinery without+ using `Table` itself. This includes:+ * `tableCellLayout` - does table cell layout using table configuration+ settings,+ * `addBorders` - adds row, column, and surrounding borders using table+ border-drawing settings, and+ * `RenderedTableCells` and `BorderConfiguration` - the low-level types+ used for the new functions.++Other changes:+* Added a new `EditorLineNumbersDemo` demo program.++1.5+---++This release focuses on API improvements in `Brick.Widgets.Dialog`:++* `Dialog` got an additional type argument, `n`, for resource names.+* The `dialog` constructor now takes `[(String, n, a)]` rather than+ `[(String, a)]`; this allows the caller to associate a resource name+ with each dialog button.+* Dialog buttons now report click events under their associated resource+ names.+* Dialog buttons now `putCursor` when they are focused in order to work+ better with screen readers.+* The `Dialog` module got `getDialogFocus` and `setDialogFocus`+ functions to help with focus management, and as part of this change,+ the `dialogSelectedIndex` function and its lens `dialogSelectedIndexL`+ were removed.++1.4+---++API changes:+* `Brick.Widgets.Border` got `hBorderAttr` and `vBorderAttr` for use by+ `hBorder` and `vBorder` respectively. The new attributes inherit from+ `borderAttr`, so applications that just specify `borderAttr` will not+ see any change in behavior for those specific border elements.++Performance improvements:+* `Brick.Widgets.Core.txt` had its performance improved. (thanks Fraser+ Tweedale)+* `Brick.Widgets.Core.hBox` and `vBox` had their performance improved.+ (thanks Fraser Tweedale)++1.3+---++Package changes:+* Removed dependency on `dlist`.++Performance improvements:+* Improved the performance of `vBox` and `hBox` (thanks Fraser Tweedale)++1.2+---++Package changes:+* Supports base 4.17 (GHC 9.4).++Bug fixes:+* `newFileBrowser` now normalizes its initial path (#387).++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+---++Version 1.0 of `brick` comes with some improvements that will require+you to update your programs. This section details the list of API+changes in 1.0 that are likely to introduce breakage and how to deal+with each one. You can also consult the demonstration+programs to see working examples of the new API. For those+interested in a bit of discussion on the changes, see [this+ticket](https://github.com/jtdaugherty/brick/issues/379).++* The event-handling monad `EventM` was improved and changed in some+ substantial ways, all aimed at making `EventM` code cleaner, more+ composable, and more amenable to lens updates to the application+ state.+ * The type has changed from `EventM n a` to `EventM n s a` and is now+ an `mtl`-compatible state monad over `s`. Some consequences and+ related changes are:+ * Event handlers no longer take and return an explicit state value;+ an event handler that formerly had the type `handler :: s ->+ BrickEvent n e -> EventM n (Next s)` now has type `handler ::+ BrickEvent n e -> EventM n s ()`. This also affected all of+ Brick's built-in event handler functions for `List`, `Editor`,+ etc.+ * The `appHandleEvent` and `appStartEvent` fields of `App` changed+ types to reflect the new structure of `EventM`. `appStartEvent`+ will just be `return ()` rather than `return` for most+ applications.+ * `EventM` can be used with the `MonadState` API from `mtl` as well+ as with the very nice lens combinators in `microlens-mtl`.+ * The `Next` type was removed.+ * State-specific event handlers like `handleListEvent` and+ `handleEditorEvent` are now statically typed to be scoped to+ just the states they manage, so `zoom` from `microlens-mtl` must+ be used to invoke them. `Brick.Types` re-exports `zoom` for+ convenience. `handleEventLensed` was removed from the API in lieu+ of the new `zoom` behavior. Code that previously handled events+ with `handleEventLensed s someLens someHandler e` is now just+ written `zoom someLens $ someHandler e`.+ * If an `EventM` block needs to operate on some state `s` that is+ not accessible via a lens into the application state, the `EventM`+ block can be set up with `Brick.Types.nestEventM`.+ * Since `Next` was removed, control flow is now as follows:+ * Without any explicit specification, an `EventM` block always+ continues execution of the `brick` event loop when it finishes.+ `continue` was removed from the API. What was previously `continue+ $ s & someLens .~ value` will become `someLens .= value`.+ * `halt` is still used to indicate that the event loop should halt+ after the calling handler is finished, but `halt` no longer takes+ an explicit state value argument.+ * `suspendAndResume` is now immediate; previously,+ `suspendAndResume` indicated that the specified action should run+ once the event handler finished. Now, the event handler is paused+ while the specified action is run. This allows `EventM` code to+ continue to run after `suspendAndResume` is called and before+ control is returned to `brick`.+ * Brick now depends on `mtl` rather than `transformers`.+* The `IsString` instance for `AttrName` was removed.+ * This change is motivated by the API wart that resulted from the+ overloading of both `<>` and string literals (via+ `OverloadedStrings`) that resulted in code such as `someAttrName+ = "blah" <> "things"`. While that worked to create an `AttrName`+ with two segments, it was far too easy to read as two strings+ concatenated. The overloading hid what is really going on with the+ segments of the attribute name. The way to write the above example+ after this change is `someAttrName = attrName "blah" <> attrName+ "things"`.++Other changes in this release:++* Brick now provides an optional API for user-defined keybindings+ for applications! See the User Guide section "Customizable+ Keybindings", the Haddock for `Brick.Keybindings.KeyDispatcher`,+ and the new demo program `programs/CustomKeybindingDemo.hs` to get+ started.+* `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+----++API changes:+ * Added `Brick.Widgets.Edit.getCursorPosition` (thanks+ @TristanCacqueray)++0.72+----++Package changes:+ * Increased lower bound on `text-zipper` to `0.12`.++API changes:+ * `handleEditorEvent` now takes a `BrickEvent` rather than just a Vty+ `Event`.+ * Brick editors now handle mouse clicks to change their cursor+ positions.++0.71.1+------++Bug fixes:+ * Fixed an issue where `tests/Render.hs` did not gracefully exit in the+ presence of an unknown terminal.++0.71+----++Package changes:+ * Increased `vty` lower bound to `5.36`.++API changes:+ * Added `tests/Render.hs` to provide a simple test of+ `Brick.Main.renderWidget` (thanks @valyagolev)+ * Added `Brick.Main.renderWidget` to help in golden testing contexts+ (thanks @valyagolev)++Other changes:+ * Various `table` documentation improvements.++0.70.1+------++Build fixes:+ * Added a missing import for GHC 8.2.2.++0.70+----++Enhancements:+ * The table widget now behaves much better when some or all cells are+ empty.++Bug fixes:+ * BorderMaps got fixed to ensure that smart borders connect even in the+ presence of empty widgets (#370). Thanks to Daniel Wagner for this+ fix!++0.69.1+------++Bug fixes:+ * `table` can now deal properly with empty cells that are in left- and+ top-aligned settings. Previously, empty cells in those settings would+ break table rendering. (#369)++0.69+----++New features:+ * `Brick.Widgets.Core`: added `relativeTo` to support relative+ positioning across layers. This allows elements in higher layers+ to be positioned relative to elements in lower layers as long as+ those elements have had their extents reported with `reportExtent` or+ `clickable`.++0.68.1+------++Bug fixes:+ * Brick's internal book-keeping got a bug fix that caused mouse-click+ coordinates to be wrong for clickable regions that were translated+ partially off of the left or top edges of a rendered region.++0.68+----++API changes:+ * Removed the "markup" feature, which included `Data.Text.Markup`,+ `Brick.Markup`, and `brick-markup-demo`. This feature never performed+ well and was awkward to use. I considered it experimental from the+ initial release of this library. Some recent incompatibilities with+ Vty changes made me realize that it was time to finally get rid of+ this. If this affects you, please let me know and I am happy to work+ with you to figure out an alternative. Granted, anyone is welcome to+ dig up the previous code and re-use it in their own projects!++0.67+----++API changes:+ * `Brick.Widgets.FileBrowser` now exports getters for all+ `FileBrowser` fields. These getters are lens-like accessors+ with the `G` suffix.+ * `Brick.Widgets.FileBrowser` no longer exports the+ `fileBrowserEntryFilterL` lens. The lens broke the API+ because it allowed modification of internal state that could+ lead to inconsistency in the UI. Users who needed to use+ `fileBrowserEntryFilterL` before this change should use+ `setFileBrowserEntryFilter` instead.++0.66.1+------++Bug fixes:+ * `Brick.Widgets.Core.cached` no longer caches the visibility requests+ generated by the cached image. This fixes a bug where re-use of a+ cached rendering would cause undesired viewport scrolling of those+ requested regions into view when the cached renderings got re-used.++0.66+----++New features:+ * Added `Brick.Main.makeVisible`, a function to request visible regions+ from `EventM`. This, together with `Brick.Widgets.Core.reportExtent`,+ can be used to request that a viewport be scrolled to make a+ specified named region visible on the next redraw. The region must be+ known to the renderer with `reportExtent` (or something that calls+ it, like `clickable`). Due to the `Ord` constraint on some of the API+ calls required to implement this, an `Ord` constraint on the resource+ name type (`n`) got propagated to various places in the API. But that+ shouldn't present a problem since other fundamental API calls already+ required that instance.++0.65.1+------++Bug fixes:+ * `Brick.Widgets.Core.viewport`: fixed non-scroll+ direction width/height in the presence of scroll bars (see+ e41ad936ebe8b49e259a72ff7a34765d5a587aaa).++0.65+----++New features and API changes:+ * Viewports got support for built-in scroll bar rendering. This+ includes additions of types and functions to manage the feature+ behavior. These changes enable viewports to automatically get+ scroll bars drawn next to them (on any side) with customizable+ attributes and drawings. As part of this change, a new demo program,+ `ViewportScrollbarsDemo.hs`, was added to show off these new+ features. Here are the new types and functions that got added (mostly+ to `Brick.Widgets.Core`):+ * `withVScrollBars` - enable display of vertical scroll bars+ * `withHScrollBars` - enable display of horizontal scroll bars+ * `withClickableVScrollBars` - enable mouse click reporting on+ vertical scroll bar elements+ * `withClickableHScrollBars` - enable mouse click reporting on+ horizontal scroll bar elements+ * `ClickableScrollbarElement` - the type of elements of a scroll bar+ that can be clicked on and provided to the application+ * `withVScrollBarHandles` - enable vertical scroll bar handle drawing+ * `withHScrollBarHandles` - enable horizontal scroll bar handle+ drawing+ * `withVScrollBarRenderer` - customize the renderer used for vertical+ scroll bars+ * `withHScrollBarRenderer` - customize the renderer used for+ horizontal scroll bars+ * `ScrollbarRenderer(..)` - the type of scroll bar renderer+ implementations+ * `verticalScrollbarRenderer` - the default renderer for vertical+ scrollbars, customizable with `withVScrollBarRenderer`+ * `horizontalScrollbarRenderer` - the default renderer for horizontal+ scrollbars, customizable with `withHScrollBarRenderer`+ * `scrollbarAttr` - the base attribute of scroll bars+ * `scrollbarTroughAttr` - the attribute of scroll bar troughs+ * `scrollbarHandleAttr` - the attribute of scroll bar handles+ * The `Context` type got the `n` type argument that is used for+ `Result`, `EventM`, etc.++Package changes:+ * Raised `base` bounds to allow building with GHC 9.2.1 (thanks Mario+ Lang)+ * Stopped supporting GHC 7.10.++0.64.2+------++Bug fixes:+ * `Brick.Themes.saveTheme` now correctly saves background colors (#338)+ * `Brick.Widgets.List.listMoveToEnd` now uses the correct destination+ index (#337)++0.64.1+------++Bug fixes:+ * Fixed a bug where mouse clicks could fail to be noticed if+ "continueWithoutRedraw" was called.++0.64+----++API changes:+ * Added `Brick.Main.continueWithoutRedraw`, an alternative to+ `Brick.Main.continue` that does not trigger a screen redraw. See the+ Haddock and User Guide for details.+ * Added `Brick.Widgets.Core.putCursor` to support Vty's new (as of+ 5.33) API for placing cursors without visually representing+ them. This change also updated `Brick.Forms.renderCheckbox` and+ `Brick.Forms.renderRadio` to use `putCursor` (thanks to Mario Lang+ for this work).++Other improvements:+ * `Brick.Widgets.Edit` now supports a few more Emacs-style keybindings+ (thanks Mario Lang):+ * `M-b` and `M-f` to navigate by word+ * `C-b` and `C-f` for consistency+ * `M-d` to delete word under cursor+ * `C-t` to transpose previous character with current character+ * `M-<` and `M->` to goto-beginning-of-file and end of file,+ respectively++0.63+----++API changes:+ * The `Viewport` type got a new field, `_vpContentSize` (and a+ corresponding lens `vpContentSize`) to get the size of the viewport's+ contents.++0.62+----++API changes:+ * `Brick.Widgets.Core` got new functions+ `crop{Left,Right,Bottom,Top}To`. Unlike the `crop...By` functions,+ which crop on the specified side by a particular amount, these+ `crop...To` functions crop on the specified side and take a desired+ overall width of the final result and use that to determine how much+ to crop. A widget `x` of width `w` could thus be cropped equivalently+ with `cropLeftBy a x` and `cropLeftTo (w - a) x`.++Other changes:+ * Added `programs/CroppingDemo.hs` to demonstrate the new (and+ preexisting) cropping functions.++0.61+----++API changes:+ * Brick.Forms got `editShowableFieldWithValidate`, a generalization+ of `editShowableField` that allows the caller to specify an+ additional validation function (thanks Ben Selfridge)++0.60.2+------++Bug fixes:+ * Widgets reported as `clickable` are now reported as clickable even+ when their renderings are cached with `cached` (#307; thanks Hari+ Menon)++0.60.1+------++Bug fixes:+ * `table []` no longer raises `TEUnequalRowSizes`.++0.60+----++New features:+ * Added `Brick.Widgets.Table` to support drawing basic tables. See+ `programs/TableDemo.hs` for a demonstration (`cabal new-run -f demos+ brick-table-demo`).++0.59+----++API changes:+ * `Brick.Widgets.List` got `listMoveToBeginning` and `listMoveToEnd`+ functions+ * `Extent`: removed the unused `extentOffset` field++Bug fixes:+ * Fixed a crash in the border rewriting code that attempted to rewrite+ empty images (#305) (thanks @dmwit)++0.58.1+------++Bug fixes:+ * Removed a defunct failing test from the List test suite++0.58+----++Package changes:+ * Updated dependency constraints to build on GHC 9.0.1 (thanks Ondřej+ Súkup)++API changes:+ * The FileBrowser module now exports individual functions for+ each of the events that it handles. This allows end users to+ trigger the behaviors directly rather than relying on the built-in+ `handleFileBrowserEvent` function. The documentation has been updated+ to indicate which functions are triggered by each key event. (Thanks+ David B. Lamkins)++Other changes:+ * The `List` module's `listFindBy` function now attempts to find a+ match anywhere in the list rather than just somewhere between the+ cursor and the end of the list.+ * The `FileBrowser` now positions a cursor at the beginning of the+ selected entry when the file browser is focused. (thanks Mario Lang)+ * The user guide's viewport visibility example got an important+ syntactic fix. (thanks Mario Lang)++0.57.1+------++Bug fixes:+ * Fixed a small space leak in the main rendering loop (#260)+ * Get `TailDemo` building on more versions of GHC++0.57+----++Package changes:+ * Raised lower bound on `vty` to 5.31 to get the new `strikethrough`+ style.++New features:+ * Added support for the `strikethrough` style in Brick theme+ customization files.++0.56+----++Package changes:+ * Increased upper bound for `base` to support GHC 8.10.2 (thanks Ryan+ Scott)++API changes:+ * Added `Brick.Forms.updateFormState` to update the state contained+ within (and managed by) a Form. This function takes care of the+ details of updating the form fields themselves to be consistent with+ the change in underlying state.+ * Added the overall window width (`windowWidth`) and height+ (`windowHeight`) to `Context`, the rendering context type (thanks Tom+ McLaughlin)++Other changes:+ * Added `brick-tail-demo`, a demonstration program for writing a+ `tail`-style output-following interface.+ * Updated `Brick.Widgets.ProgressBar` so that it handles near-endpoint+ cases more naturally (fixes #281)++0.55+----++Package changes:+ * Increased lower bound on `vty` dependency to 5.29.++Bug fixes:+ * `customMain` now restores the initial terminal input state on+ shutdown. This means that changes to the input state flags in the last+ `suspendAndResume` before program exit are no longer propagated to the+ end user's terminal environment (which could lead to broken or garbled+ terminal I/O).++0.54+----++API changes:+ * Exported `Brick.Widgets.FileBrowser.maybeSelectCurrentEntry` (thanks+ Róman Joost)++Other changes:+ * Added handlers for the `Home` and `End` keys to+ `Brick.Widgets.Edit.handleEditorEvent` (thanks Róman Joost)++0.53+----++Package changes:+ * Relaxed base bounds to allow building with GHC 8.10 (thanks Joshua+ Chia)++Bug fixes:+ * `vLimitPercent`: use correct horizontal size policy from child+ (thanks Janek Spaderna)+ * `str`: be more aggressive in determining how many characters to+ display (attempt to display as many zero-width characters as+ possible)++0.52.1+------++Bug fixes:+ * Attribute map lookups now merge styles in addition to merging colors+ (see `eb857e6bb176e119ac76f5e2af475f1b49812088`).+ * `txtWrapWith` now pads in the single-line case (see also+ `926d317c46b19d4e576748891a1702080287aa03`, #234, and #263)++0.52+----++API changes:+ * EventM now provides a MonadFail instance+ * EventM now provides MonadMask, MonadCatch, and MonadThrow instances+ (thanks Fraser Tweedale)++Other changes:+ * The FileBrowser now has support for vi-style bindings in addition to+ its previous bindings. New bindings include:+ * `j`/`k`: next/previous element+ * `C-n`/`C-p`: page down/up+ * `C-d`/`C-u`: half page down/up+ * `g`: select first entry+ * `G`: select last entry++0.51+----++API changes:+ * Added Brick.Focus.focusRingToList, which returns all of the elements+ in a focus ring as a list, starting with the focused entry and+ wrapping around (#257; thanks @4eUeP)++Bug fixes:+ * Fix Brick.Widgets.FileBrowser.fileExtensionMatch to match directories+ and also match symlinks that link to directories (thanks @YVee1)++Other changes:+ * Added demonstration program screenshot gallery (thanks @drola)++0.50.1+------++Bug fixes:+ * Fixed a bug where a self-referential symlink would cause the file+ browser to get into a loop and ultimately crash. (Thanks Kevin Quick)++API changes:+ * Added `Brick.Focus.focusRingLength` to get the size of a focus ring.+ (Thanks Róman Joost)++Other changes:+ * Updated Travis configuration and base dependency to support GHC+ 8.8.1. (thanks Brandon Hamilton)++0.50+----++API changes:+ * Added `writeBChanNonBlocking`, which does a non-blocking write to a+ `BChan` and returns whether the write succeeded. This required+ raising the STM lower bound to 2.4.3.++0.49+----++New features:+ * The `FileBrowser` now supports navigation of directories via+ symlinks, so `Enter` on a symlink will descend into the target path+ of the symlink if that path is a directory. Part of this change is+ that the `FileInfo` type got a new file, `fileInfoLinkTargetType`,+ that indicates the type of file that the link points to, if any.++0.48+----++New features:+ * The `Edit` widget now supports `EvPaste` Vty events by default,+ assuming UTF-8 encoding of pasted bytes. If pasted bytes are not+ UTF-8-decodable, the pastes will be ignored. In any case, users can+ still intercept `EvPaste` events as before and handle them as desired+ if the default behavior is not desirable.++Other changes:+ * `txtWrapWith` now always pads its output to the available width to+ obey its `Greedy` requirement.++0.47.1+------++Bug fixes:+ * userguide: update stale Result construction+ * Added test case for List initial selection (thanks Fraser Tweedale)+ * Fixed build on GHC 7.10 due to RULES pragma formatting issue (thanks+ Fraser Tweedale)+ * Various CI-related fixes (thanks Fraser Tweedale)++0.47+----++API changes:+ * Changed `Brick.Main.customMain` so that it now takes an additional+ (first) argument: the initial `Vty` handle to use. This lets the+ caller have more control over the terminal state when, for example,+ they have previously set up Vty to do other work before calling+ `customMain`.+ * Added `Brick.Main.customMainWithVty`. This function is the same as+ `customMain` except that it also returns the final `Vty` handle that+ it used internally *without* shutting that Vty handle down. This+ allows the caller to continue using the terminal without resetting it+ after `customMainWithVty` finishes executing.++0.46+----++Performance improvements:+ * The box combinators `<=>`, `<+>`, `vBox`, and `hBox` got GHC rewrite+ rules that will optimize away redundant boxes. This change improves+ performance for chains of `<+>` or `<=>` as well as nested boxes+ using `hBox` and `vBox`. Previously chains of e.g. `<+>` produced+ binary trees of boxes that incurred more rendering overhead. Those+ are now optimized away.++API changes:+ * Data.Text.Markup: renamed `empty` to `isEmpty`++0.45+----++API changes:+ * List got a new `listFindBy` function (thanks Fraser Tweedale). This+ function uses a predicate to find a matching element in the list and+ move the cursor to that item.+ * Data.Text.Markup got a new `empty` function (#213)++0.44.1+------++Bug fixes:+ * `Brick.Markup` now properly renders empty lines in markup (#209)++0.44+----++API changes:+ * The `List` type got its container type generalized thanks to a lot of+ work by Fraser Tweedale. Note that this change is+ backwards-compatible with older Brick programs that use the `List`.+ Thanks to this work, the `List` now supports both `Data.Vector`+ and `Data.Sequence` as its container types out of the box and can+ be extended to support other sequence types with some simple type+ class instances. In addition, property tests are provided for `List`+ and its asymptotics are noted in the documentation. Along the way,+ various bugs in some of the list movement functions got fixed to+ bring them in line with the advertised behavior in the documentation.+ Thanks, Fraser!++0.43+----++API changes:+ * The FileBrowser module got the ability to select multiple files+ (#204). This means that the `fileBrowserSelection` function now+ returns a list of `FileInfo` rather than at most one via `Maybe`.+ The module also now uses a new attribute, `fileBrowserSelectedAttr`,+ to indicate entries that are currently selected (in addition to+ displaying an asterisk after their filenames). Lastly, the file+ size and type fields of `FileInfo` have been replaced with a+ new type, `FileStatus`, and `FileInfo` now carries an `Either+ IOException FileStatus`. As part of that safety improvement,+ `setWorkingDirectory` now no longer clobbers the entire entry listing+ if any of the listings fail to stat. In addition, the FileBrowser now+ uses the correct file stat routines to deal with symbolic links.++Package changes:+ * Added lower bound on `directory` (thanks Fraser Tweedale)++Test suite changes:+ * Test suite now propagates success/failure to exit status (thanks+ Fraser Tweedale)++0.42.1+------++Behavior changes:+ * File browsers in search mode now terminate search mode when `Enter`+ is pressed, resulting in better behavior.++0.42+----++New features:+ * Added `Brick.Widgets.FileBrowser`, which provides a filesystem+ browser for selecting files and directories. Read the Haddock+ module documentation and see the included demo program,+ `programs/FileBrowserDemo.hs`, for information on using the new+ functionality.++0.41.5+------++Miscellaneous:+ * `suspendAndResume` now empties the rendering cache when returning to+ the rendering event loop. This ensures that the state returned by the+ `IO` action is rendered completely rather than relying on potentially+ stale cache entries.++0.41.4+------++API changes:+ * Forms: added `setFormFocus` function to set focus for a form+ * Added `NFData` instances for `AttrMap` and `Theme` types (thanks+ Fraser Tweedale)++0.41.3+------++Bug fixes:+ * Lists now draw correctly without crashing due to a vector slice+ bounds check failure if their rendering area is too small (#195;+ thanks @andrevdm)++Other changes:+ * Relaxed base bounds to support GHC 8.6 (thanks @maoe)+ * Added towerHanoi to the featured projects list++0.41.2+------++Bug fixes:+ * Support STM 2.5 by allowing for `Natural` argument to `newTBQueue`+ (thanks @osa1)++0.41.1+------++New features:+ * `Forms`: added `checkboxCustomField` and `radioCustomField` to permit+ customization of characters used to draw selection state for such+ fields.++0.41+----++New features:+ * `Brick.Forms` got a new field constructor, `listField`, that provides+ a form field using a `List`.+ * `List`: added the `listMoveToElement` function for changing the list+ selection to the specified element, if it exists.++Package changes:+ * Now depends on vty >= 5.24.++Other changes:+ * `viewport`: fixed failable patterns for forward compatibility with+ GHC 8.6 (#183)+ * Add `Generic`, `NFData`, and `Read` instances for some types++0.40+----++New features:+ * Brick.Widgets.Core: added new functions `hLimitPercent` and+ `vLimitPercent`. These behave similarly to `hLimit` and `vLimit`+ except that instead of taking absolute numbers of columns or rows,+ they take percentages. (Thanks Roman Joost)++0.39+----++New features:+ * The `italic` keyword is now supported in theme customization file+ style lists. This requires `vty >= 5.23.1`++0.38+----++New features:+ * Added support for parsing `#RRGGBB` color values in theme+ customization files in addition to the color names already supported+ (thanks Brent Carmer). These values are mapped to the nearest+ reasonable entry in the 240-color space.++0.37.2+------++Bug fixes:+ * Theme customization files can now use empty lists for style+ customization.++0.37.1+------++API changes:+ * Exposed `Brick.Forms.renderFormFieldState`.++0.37+----++Behavior changes:+ * `listMoveBy` now automatically moves to the first or last position+ in the list if called when the list is non-empty but has no selected+ element (thanks Philip Kamenarsky)++API changes:+ * Added `Brick.Widgets.List.renderListWithIndex` that passes+ the index of each element to the item rendering function (thanks+ liam@magicseaweed.com)++0.36.3+------++Bug fixes:+ * Fixed a bug where mouse-up events in viewports were not translated+ into the global coordinate space, unlike mouse-down events (#173)++0.36.2+------++API changes:+ * The Forms API got two new functions, `setFormConcat` and+ `setFieldConcat`, used for controlling the previously hard-coded+ concatenation behavior of form fields. These are optional and both+ concatenation settings default to their former hard-coded values,+ `vBox` (#172).++0.36.1+------++Package changes:+ * Raised upper bound to support GHC 8.4.2 (#171)++Other changes:+ * Improved List accessor documentation (thanks liam <liam@magicseaweed.com>)+ * Brick.Main now uses a Set instead a list to track invalidation+ requests to avoid duplicates.++0.36+----++New features:+ * Dynamic border support: adjacent widgets that use borders can make+ those borders seamlessly connect to each other! Thanks+ so much to Daniel Wagner for this feature! Please see+ `programs/DynamicBorderDemo.hs` for a demonstration. Also see the+ "Joinable Borders" section of the User Guide.++0.35.1+------++ * Conditionally depend on semigroups for GHC before 8++0.35+----++ * Added support for GHC 8.4.+ * Updated travis build to test on all 8.x releases (thanks Peter+ Simons)++0.34.1+------++Bug fixes:+ * Fixed a bug where the "reverseVideo" style could not be parsed in a+ theme customization when it was all lowercase (thanks Yuriy Lazarev)++Documentation changes:+ * Guide: added more complete example of creating a default theme+ (thanks Mark Wales)+ * Guide: added offset to Extent pattern matching (thanks Mark Wales)++0.34+----++API changes:+ * Core: vLimit and hLimit now *bound* sizes rather than setting them.+ This was the original intention of these combinators. The change in+ behavior means that now `vLimit N` means that *at most* `N` rows will+ be available; if the context has less, then the smaller constraint in+ the context is used instead. Programs affected by this behavior will+ be those that assume that `vLimit` doesn't do this, but that should+ be very few or zero.++Other changes:+ * Dialog: now arrow keys no longer wrap around available buttons but+ stop at rightmost or leftmost button to avoid confusion when+ attempting to tell which button is selected in two-button dialogs+ (thanks to Karl Ostmo for this change)++Documentation changes:+ * Updated Haddocks for str/txt in Core to mention tab character+ considerations++0.33+----++API changes:+ * Forms: added support for external validation of form fields using+ `setFieldValid`. See the Haddock, User Guide, and FormDemo.hs for+ details.+ * Borders: removed all attribute names except `borderAttr` to simplify+ border attribute assignment.++0.32.1+------++Bug fixes:+ * Core: make all text wrap widgets Greedy horizontally++Miscellaneous:+ * Dialog: clarify purpose in documentation (w.r.t. #149)++0.32+----++API changes:+ * This release adds the new `Brick.Forms` module, which provides an API+ for type-safe input forms with automatic rendering, event handling,+ and state management! See the Haddock and the "Input Forms" section+ of the Brick User Guide for information on this killer feature! Many+ thanks to Kevin Quick for feedback on this new functionality.++0.31+----++Behavior changes:+ * `viewport` now implicitly causes generation of mouse events for the+ viewport when mouse mode is enabled. The mouse events are expressed+ in the coordinate system of the contents of the viewport. The+ consequence and intention of this change is to enable mouse event+ reporting for editors when clicks occur outside the known text area.++0.30+----++API changes:+ * `Brick.Focus`: added `focusSetCurrent` to make it easy to set the+ focus of a focus ring+ * `Brick.Main`: added a simple polymorphic `App` value, `simpleApp`++0.29.1+------++Bug fixes:+ * Mixed-case color names like "brightBlue" can now be parsed in theme+ customization files.++0.29+----++API changes:+ * Added Ord instances for `Location` and `BrickEvent` (thanks Tom+ Sydney Kerckhove)+ * `Brick.AttrMap`: attribute name components are now exposed via the+ `attrNameComponents` function. Also added a Read instance for+ AttrName.++New features:+ * This release adds user-customizable theme support. Please see the+ "Attribute Themes" section of the User Guide for an introduction; see+ the Haddock documentation for `Brick.Themes` for full details. Also,+ see the new `programs/ThemeDemo.hs` for a working demonstration.++0.28+----++API changes:+ * Brick.AttrMap.setDefault was renamed to setDefaultAttr.+ * Added Brick.AttrMap.getDefaultAttr: get the default attribute from an+ attribute map.+ * Added Brick.Widgets.Core.modifyDefAttr to modify the default+ attribute of the rendering context.++Other changes:+ * Updated AttrDemo to show usage of modifyDefAttr.++0.27+----++API changes:+ * Brick.Widgets.Core: added `hyperlink` combinator (thanks Getty Ritter+ for hyperlinking support)++Other changes:+ * Updated AttrDemo to show how to use hyperlinking+ * README: Added `herms` to featured projects++0.26.1+------++ * Fixed haddock for listHandleEventVi.++0.26+----++API changes:+ * Added Brick.Widgets.List.handleListEventVi to add support for+ vi-style movements to lists (thanks Richard Alex Hofer)++Other changes:+ * Added ListViDemo.hs to demonstrate the Vi-style handler for lists+ (thanks Richard Alex Hofer)++0.25+----++API changes:+ * List: added page movement functions `listMoveByPages`,+ `listMovePageUp`, and `listMovePageDown` (thanks Richard Alex Hofer)++Miscellaneous:+ * Fixed a spelling mistake in the AttrMap haddock (thanks Edward Betts)++0.24.2+------++Miscellaneous:+ * Minor documentation updates including a clarification for #135++0.24.1+------++Bug fixes:+ * vBox/hBox: when there is leftover space and all elements are greedy,+ spread it amongst the elements as evenly as possible instead of+ assigning it all to the first element (fixes #133)++Package changes:+ * Include Sam Tay's brick tutorial files in extra-doc-files++0.24+----++API changes:+ * Added Brick.Widgets.Core.setAvailableSize to control rendering+ context size in cases where the screen size is too constraining (e.g.+ for a floating layer that might be bigger than the screen).++Documentation changes:+ * Samuel Tay has contributed his wonderful Brick tutorial to this+ package in docs/samtay-tutorial.md. Thank you!++0.23+----++API changes:+ * getVtyHandle: always return a Vty handle rather than Maybe+ (Previously, in appStartEvent you'd get Nothing because Vty had+ not been initialized yet. This made various use cases impossible+ to satisfy because appStartEvent is a natural place to get initial+ terminal state from Vty. This change makes it so that a Vty handle is+ always available, even in appStartEvent.)+ * txtWrapWith: added missing haddock++0.22+----++API changes:+ * Core: added txtWrapWith and strWrapWith functions to provide control+ over wrapping behavior by specifying custom wrapping settings.++Other changes:+ * Updated TextWrapDemo.hs to demonstrate customizing wrapping settings.++0.21+----++Package changes:+ * Upgrade to word-wrap 0.2++Other changes:+ * Brick.Types.Internal: improve mouse constructor haddock+ * Add a basic fill demonstration program (FillDemo.hs)++0.20.1+------++Bug fixes:+ * str: fixed an IsString constraint confusion on GHC 7.10.1++0.20+----++Package changes:+ * Added a dependency on "word-wrap" for text-wrapping.+ * Added a new TextWrapDemo demo program to illustrate text wrapping+ support++API changes:+ * Brick.Widgets.Core: added new functions txtWrap and strWrap to do+ wrapping of long lines of text.++Miscellaneous:+ * Guide: fixed event type (#126)++0.19+----++API changes:+ * The editor content drawing function is now passed to renderEditor,+ not the constructor, to improve separation of presentation and+ representation concerns. The corresponding Editor drawing function+ lens and accessor were removed.++0.18+----++Package changes:+ * Added a dependency on data-clist.++API changes:+ * Brick.Focus: removed the Functor instance for FocusRing.+ * Brick.Focus: re-implemented FocusRing in terms of the circular list+ data structure from data-clist. In addition, this change introduced+ "focusRingModify", which permits the user to use the data-clist API+ to directly manipulate the FocusRing's internals. This way brick+ doesn't have to re-invent the wheel on the focus ring behavior.++0.17.2+------++Package changes:+ * Added programs/ReadmeDemo.hs and featured its output and code in the+ README to provide an early demonstration++Library changes:+ * centerAbout now right- and bottom-pads its operand to behave+ consistently with h/vCenter++0.17.1+------++Package changes:+ * Use Extra-Doc-Files instead of Data-Files for documentation files++Bug fixes:+ * List: correctly update selected index in listInsert+ * Update example program in brick.cabal (thanks @timbod7)++0.17+----++Package changes:+* Updated to depend on Vty 5.15.+* Updated to remove dependency on data-default.+* Discontinued support for GHC versions prior to 7.10.1.++API changes:+* Removed Data.Default instances for AttrName, AttrMap, Result, and+ BorderStyle (use Monoid instances instead where possible).+* Added defaultBorderStyle :: BorderStyle.+* Added emptyResult :: Result n.++0.16+----++This release includes a breaking API change:+* Brick now uses bounded channels (Brick.BChan.BChan) for event+ communication rather than Control.Concurrent.Chan's unbounded channels+ to improve memory consumption for programs with runaway event+ production (thanks Joshua Chia)++Other API changes:+* Brick.List got a new function, listModify, for modifying the selected+ element (thanks @diegospd)++Performance improvements:+* hBox and vBox now use the more efficient DList data structure when+ rendering to improve performance for boxes with many elements (thanks+ Mitsutoshi Aoe)++0.15.2+------++Bug fixes:+* viewport: do not cull cursor locations on empty viewport contents+ (fixes #105)+* User guide CounterEvent type fix (thanks @diegospd)++0.15.1+------++Bug fixes:+* List: fixed empty list validation in listReplace (thanks Joshua Chia)++0.15+----++Demo changes:+* MouseDemo: add an editor and use mouse events to move the cursor+* MouseDemo: Enhance MouseDemo to show interaction between 'clickable'+ and viewports (thanks Kevin Quick)++New features:+* Editors now report mouse click events++API changes:+* Rename TerminalLocation row/column fields to avoid commonplace name+ clashes; rename row/column to locationRow/locationColumn (fixes #96)++Bug fixes:+* Core: make cropToContext also crop extents (fixes #101)+* viewport: if the sub-widget is not rendered, also cull all extents and+ cursor locations++Documentation changes:+* User Guide updates: minor fixes, updates to content on custom widgets,+ wide character support, and examples (thanks skapazzo@inventati.org,+ Kevin Quick)++0.14+----++This release added support for wide characters. In particular, wide+characters can now be entered into the text editor widget and used in+'str' and 'txt' widgets.++0.13+----++API changes:+ * Mouse mode is no longer enabled by default.+ * customMain's event channel parameter is now optional+ * FocusRing now provides a Functor instance (thanks Ian Jeffries)++0.12+----++This release primarily adds support for mouse interaction. For details,+see the Mouse Support section of the User Guide. This release also+includes breaking API changes for the App type. Here's a migration+guide:++ * Event handlers now take "BrickEvent n e" instead of "e", where "e"+ was the custom event type used before this change. To recover your+ own custom events, pattern-match on "AppEvent"; to recover Vty input+ events, pattern-match on "VtyEvent".+ * appLiftVtyEvent went away and can just be removed from your App+ record constructor.+ * If you aren't using the custom event type or were just using Vty's+ "Event" type as your App's event type, you can set your event type to+ just "e" because you'll now be able to get Vty events regardless of+ whether you use a custom event type.++API changes:+ * Added the Widget combinator "clickable" to indicate that a widget+ should generate mouse click events+ * Added the Extent data type and the "reportExtent" widget combinator+ to report the positions and sizes of widgets+ * Rendering "Result" values now include reported extents and update+ their offsets (adds "extents" field and "extentsL" lens)+ * Added "lookupExtent", "findClickedExtents", and "clickedExtent" in+ EventM to find extents and check them for mouse clicks+ * Removed appLiftVtyEvent. Instead of wrapping Vty's events in your own+ type, you now get a "BrickEvent" that always contains Vty events but+ has the ability to embed *your* custom events. See the User Guide for+ details.+ * Added demo program MouseDemo.hs+ * Added demo program ProgressBarDemo.hs (thanks Kevin Quick)+ * Added mapAttrname, mapAttrNames, and overrideAttr functions (thanks+ Kevin Quick)+ * Make handleEventLensed polymorphic over event type to allow use with+ custom events (thanks Kevin Quick)+ * Added Ord constraint to some library startup functions++Bug fixes:+ * Added Show instance for Editor, List (fixes #63)++Documentation changes:+ * Updated documentation to use new "resource name" terminology to+ reduce confusion and better explain the purpose of names.+ * Updated user guide with sections on mouse support, the rendering+ cache, resource names, paste mode, and extents++Package changes:+ * Depend on Vty 5.11.3 to get mouse mode support++0.11+----++API changes:+ * Added getVtyHandle in EventM for obtaining the current Vty context.+ It returns Nothing when calling the appStartEvent handler but after+ that a context is always available.++0.10+----++New features:+ * Added a rendering cache. To use the rendering cache, use the 'cached'+ widget combinator. This causes drawings of the specified widget to+ re-use a cached rendering until the rendering cache is invalidated+ with 'invalidateCacheEntry' or 'invalidateCache'. This change also+ includes programs/CacheDemo.hs. This change introduced an Ord+ constraint on the name type variable 'n'.+ * Added setTop and setLeft for setting viewport offsets directly in+ EventM.+ * Dialog event handlers now support left and right arrow keys (thanks+ Grégoire Charvet)++Library changes:+ * On resizes brick now draws the application twice before handling the+ resize event. This change makes it possible for event handlers to+ get the latest viewport states on a resize rather than getting the+ most recent (but stale) versions as before, at the cost of a second+ redraw.++Bug fixes:+ * We now use the most recent rendering state when setting up event handler+ viewport data. This mostly won't matter to anyone except in cases+ where a viewport name was expected to be in the viewport map but+ wasn't due to using stale rendering state to set up EventM.++0.9+---++Package changes:+ * Depend on text-zipper 0.7.1++API changes:+ * The editor widget state value is now polymorphic over the type of+ "string" value that can be edited, so you can now create editors over+ Text values as well as Strings. This is a breaking change but it only+ requires the addition of the string type variable to any uses of+ Editor. (thanks Jason Dagit and Getty Ritter)+ * Added some missing Eq and Show instances (thanks Grégoire Charvet)++New features:+ * The editor now binds Control-U to delete to beginning of line (thanks+ Hans-Peter Deifel)++Bug fixes:+ * List: avoid runtime exception by ensuring item height is always at+ least 1++0.8+---++API changes:+ * Center: added layer-friendly centering functions centerLayer,+ hCenterLayer, and vCenterLayer.++Functionality changes:+ * Dialog now uses new layer-friendly centering functions. This makes it+ possible to overlay a Dialog on top of your UI when you use a Dialog+ rendering as a separate layer.+ * Updated the LayerDemo to demonstrate a centered layer.+ * The renderer now uses a default Vty Picture background+ of spaces with the default attribute, rather than using+ ClearBackground (the Vty default). This is to compensate for an+ unexpected attribute behavior in Vty when ClearBackgrounds (see+ https://github.com/coreyoconnor/vty/issues/95)++0.7+---++NOTE: this release includes many API changes. Please see the "Widget+Names" section of the Brick User Guide for details on the fundamentals!++API changes:+ * The "Name" type was removed. In its place we now have a name type+ variable ("n") attached to many types (including EventM,+ CursorLocation, App, Editor, List, and FocusRing). This change makes+ it possible to:+ * Avoid runtime errors due to name typos+ * Achieve compile-time guarantees about name matching and usage+ * Force widget functions to be name-agnostic by being polymorphic+ in their name type+ * Clean up focus handling by making it possible to pattern-match+ on cursor location names+ * The EditDemo demonstration program was updated to use a FocusRing.+ * Added the "Named" type class to Brick.Widgets.Core for types that+ store names. This type class is used to streamline the Focus+ interface; see Brick.Focus.withFocusRing and EditDemo.hs.+ * The List and Editor types are now parameterized on names.+ * The List widget is now focus-aware; its rendering function now takes+ a boolean indicating whether it should be rendered with focus. The+ List uses the following attributes now:+ * When not focused, the cursor is rendered with listSelectedAttr.+ * When focused, the cursor is rendered with listSelectedFocusedAttr.+ * The Editor widget is now focus-aware; its rendering function now+ takes a boolean indicating whether it should be rendered with focus.+ The Editor uses the following attributes now:+ * When not focused, the widget is rendered with editAttr.+ * When focused, the widget is rendered with editFocusedAttr.+ * The Dialog's name constructor parameter and lens were removed.+ * The 'viewport' function was modified to raise a runtime exception if+ the widget name it receives is used more than once during the+ rendering of a single frame.++Miscellaneous:+ * Many modules now use conditional imports to silence redundancy+ warnings on GHCs with newer Preludes (e.g. including Monoid,+ Foldable, Traversable, Applicative, etc.)++0.6.4+-----++Bug fixes:+ * Add missing Functor instance for Next type (thanks Markus Hauck)++0.6.3+-----++Bug fixes:+ * List: the list now properly renders when the available height is not+ a multiple of the item height. Previously the list size would+ decrease relative to the available height. Now the list renders+ enough items to fill the space even if the top-most or bottom-most+ item is partially visible, which is the expected behavior.++0.6.2+-----++Bug fixes:+ * Editor: the 'editor' initial content parameter is now correctly split+ on newlines to ensure that the underlying editor zipper is+ initialized properly. (fixes #56; thanks @listx)++0.6.1+-----++Package changes:+ * Added lower bound for microlens >= 0.3.0.0 to fix build failure due+ to Field1 not being defined (thanks Markus Hauck)++Documentation changes:+ * Updated user guide and README to link to and mention microlens+ instead of lens++Misc:+ * Fixed a qualified import in the List demo to avoid ambiguity (thanks+ Alan Gilbert)++0.6+---++API changes:+ * Brick now uses the microlens family of packages instead of lens. This+ version of brick also depends on vty 5.5.0, which was modified to use+ microlens instead of lens. This change shouldn't impact functionality+ but will greatly reduce build times.++0.5.1+-----++Bug fixes:+ * Fix negative cropping in hCenter, vCenter, and cropResultToContext+ (fixes #52)+ * Remove unnecessary Eq constraint from listReplace (fixes #48; thanks+ sifmelcara)+ * Mention Google Group in README++0.5+---++Functionality changes:+ * Markup: make markup support multi-line strings (fixes #41)+ * brick-edit-demo: support shift-tab to switch editors+ * Core: improve box layout algorithm (when rendering boxes, track+ remaining space while rendering high-priority children to use+ successively more constrained primary dimensions)+ * Core: make fixed padding take precedence over padded widgets (fixes #42)+ Prior to this commit, padding a widget meant that if there was room+ after rendering the widget, the specified amount of padding would be+ added. This meant that under tight layout constraints padding would+ disappear before a padded widget would. This is often a desirable+ outcome but it also led to unexpected behavior when adding padding+ to a widget that grows greedily: fixed padding would never show up+ because it was placed in a box adjacent to the widget in question,+ and boxes always render greedy children before fixed ones. As a+ result fixed padding would disappear under these conditions. Instead,+ in the case of fixed padding, since we often intend to *guarantee*+ that padding is present, all of the padding combinators have been+ modified so that when the padded widget is rendered with fixed+ padding in the amount V, the widget is given V fewer rows/columns+ when it is rendered so that the padding always has room.++0.4.1+-----++Bug fixes:+* Fixed a bug in the 'visible' combinator: If the size of the visibility+ request was larger than the available space, then the rendering of a+ viewport was toggling between two states, one with aligning on the+ end of the visibility request, and another one aligning on the start.+ This commit fixes it so that a visibility request is always aligned+ on the start if not enough space is available. (thanks Thomas Strobel+ <ts468@cam.ac.uk>)++Behavior changes:+* Honor multiple 'visible' markers in a single viewport with preference+ on the innermost request (thanks Thomas Strobel <ts468@cam.ac.uk>)++0.4+---++API changes:+* Added Brick.Widgets.Core.unsafeLookupViewport to make certain kinds+ of custom widget implementations easier when viewport states are needed+ (thanks Markus Hauck <markus1189@gmail.com>)+* List: added listClear and listReverse functions (thanks Markus Hauck)+* List: Derive instances for Functor, Foldable, Traversable (thanks+ Markus Hauck)++Documentation changes:+* Hyperlink "Data.Text.Markup" inside Brick.Markup haddock (thanks+ Markus Hauck)+* Fix typo in 'Attribute Management' section of user guide (thanks+ Markus Hauck)++0.3.1+-----++Bug fixes:+* EventM newtype again instances MonadIO (thanks Andrew Rademacher)++0.3+---++API changes:+* Made EventM a newtype instead of a type alias+* List: listReplace now takes the new selected index and no longer does+element diffing++Package changes:+* Removed the dependency on the Diff package++Misc:+* Applied some hlint hints (thanks Markus Hauck <markus1189@gmail.com>)+* Fixed a typo in the README (thanks Markus Hauck <markus1189@gmail.com>)+* Improved the renderList documentation (thanks Profpatsch <mail@profpatsch.de>)+* Types: added an explicit import of Applicative for older GHCs++0.2.3+-----++Bug fixes:+* Fixed viewport behavior when the image in a viewport reduces its size+ enough to render the viewport offsets invalid. Before, this behavior+ caused a crash during image cropping in vty; now the behavior is+ handled sanely (fixes #22; reported by Hans-Peter Deifel)++0.2.2+-----++Demo changes:+* Improved the list demo by using characters instead of integers in the+ demo list and cleaned up item-adding code (thanks Jøhannes Lippmann+ <code@schauderbasis.de>)++0.2.1+-----++Bug fixes:+* List:+ * Fixed size policy of lists so that rather than being Fixed/Fixed,+ they are Greedy/Greedy. This resolves issues that arise when the box+ layout widget renders a list widget alongside a Fixed/Fixed one.+ (Closes issue #17, thanks Karl Voelker)+* Scrolling:+ * vScrollPage actually scrolls vertically now rather than horizontally+ (Thanks Hans-Peter Deifel <hpd@hpdeifel.de>)++0.2+---++API changes:+* Added top-level `Brick` module that re-exports the most important+ modules in the library.+* List:+ * Now instead of passing the item-drawing function to the `list` state+ constructor, it is passed to `renderList`+ * `renderList` now takes the row height of the list's item widgets.+ The list item-drawing function must respect this in order for+ scrolling to work properly. This change made it possible to optimize+ the list so that it only draws widgets visible in the viewport+ rather than rendering all of the list's items (even the ones+ off-screen). But to do this we must be able to tell in advance+ how high each one is, so we require this parameter. In addition+ this change means that lists no longer support items of different+ heights.+ * The list now uses Data.Vector instead of [a] to store items; this+ permits efficient slicing so we can do the optimized rendering+ described above.+* The `HandleEvent` type class `handleEvent` method now runs in+ `EventM`. This permits event-handling code implemented in terms of+ `HandleEvent` to do get access to viewport state and to run IO code,+ making it just as powerful as code in the top-level `EventM` handler.+* Many types were moved from `Brick.Widgets.Core` and `Brick.Main` to+ `Brick.Types`, making the former module merely a home for `Widget`+ constructors and combinators.+* The `IsString` instance for `Widget` was removed; this might be+ reinstated later, but this package provides enough `IsString`+ instances that things can get confusing.+* `EventM` is now reader monad over the most recent rendering pass's+ viewport state, in addition to being a state monad over viewport+ requests for the renderer. Added the `lookupViewport` function to+ provide access to the most recent viewport state. Exported the+ `Viewport` type and lenses.+* Now that `handleEvent` is now an `EventM` action, composition with+ `continue` et al got a little messier when using lenses to+ update the application state. To help with this, there is now+ `handleEventLensed`.++Bugfixes:+* Lists now perform well with 10 items or a million (see above; fixes+ #7, thanks Simon Michael)+* Added more haddock notes to `Brick.Widgets.Core` about growth+ policies.+* Forced evaluation of render states to address a space leak in the+ renderer (fixes #14, thanks Sebastian Reuße <seb@wirrsal.net>)+* str: only reference string content that can be shown (eliminates a+ space leak, fixes #14, thanks Sebastian Reuße <seb@wirrsal.net>)++Misc:+* Added a makefile for the user guide.+* List: added support for Home and End keys (thanks Simon Michael)+* Viewports: when rendering viewports, scroll requests from `EventM` are+ processed before visibility requests from the rendering process; this+ reverses this previous order of operations but permits user-supplied+ event handlers to reset viewports when desired.++Package changes:+* Added `deepseq` dependency++0.1+---+ Initial release
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2015, Jonathan Daugherty.+Copyright (c) 2015-2025, Jonathan Daugherty. All rights reserved. Redistribution and use in source and binary forms, with or without
README.md view
@@ -1,108 +1,211 @@-brick--------[](https://travis-ci.org/jtdaugherty/brick)+ -`brick` is a terminal user interface programming-library written in Haskell, in the style of-[gloss](http://hackage.haskell.org/package/gloss). This means you write-a function that describes how your user interface should look, but the-library takes care of a lot of the book-keeping that so commonly goes-into writing such programs.+`brick` is a Haskell terminal user interface (TUI) programming toolkit.+To use it, you write a pure function that describes how your user+interface should be drawn based on your current application state and+you provide a state transformation function to handle events. `brick` exposes a declarative API. Unlike most GUI toolkits which-require you to write a long and tedious sequence of "create a widget,-now bind an event handler", `brick` just requires you to describe-your interface -- even the bits that are stateful -- using a set of-declarative combinators. Then you provide a function to transform your-own application state when input (or other kinds of) events arrive.+require you to write a long and tedious sequence of widget creations+and layout setup, `brick` just requires you to describe your interface+using a set of declarative layout combinators. Event-handling is done by+pattern-matching on incoming events and updating your application state. -Under the hood, this library builds upon [vty](http://hackage.haskell.org/package/vty).+Under the hood, this library builds upon+[vty](http://hackage.haskell.org/package/vty), so some knowledge of Vty+will be necessary to use this library. Brick depends on+`vty-crossplatform`, so Brick should work anywhere Vty works (Unix and+Windows). Brick releases prior to 2.0 only support Unix-based systems. -This library deprecates [vty-ui](https://github.com/jtdaugherty/vty-ui).+Example+------- -Feature Overview-----------------+Here's an example interface (see `programs/ReadmeDemo.hs`): -`brick` comes with a bunch of widget types to get you started:+```+joinBorders $+withBorderStyle unicode $+borderWithLabel (str "Hello!") $+(center (str "Left") <+> vBorder <+> center (str "Right"))+``` - * Vertical and horizontal box layout widgets- * Basic single- and multi-line text editor widgets- * List widget- * Progress bar widget- * Simple dialog box widget- * Border-drawing widgets (put borders around or in between things)- * Generic scrollable viewports- * Extensible widget-building API- * (And many more general-purpose layout control combinators)+Result: -In addition, some of `brick`'s more powerful features may not be obvious-right away:+```+┌─────────Hello!─────────┐+│ │ │+│ │ │+│ Left │ Right │+│ │ │+│ │ │+└───────────┴────────────┘+``` - * All widgets can be arranged in predictable layouts so you don't have- to worry about terminal resizes.- * Most widgets can be made scrollable *for free*.- * Attribute management is flexible and can be customized at runtime on- a per-widget basis.+Featured Projects+----------------- -`brick` exports [lens](http://github.com/ekmett/lens) and non-`lens`-interfaces for most things, so you can get the full power of `lens` if-you want it or use plain Haskell if you don't. If a `brick` library-function named `thing` has a `lens` version, the `lens` version is named-`thingL`.+To get an idea of what some people have done with `brick`, check out+these projects. If you have made something and would like me to include+it, get in touch! +| Project | Description |+| ------- | ----------- |+| [`2048Haskell`](https://github.com/8Gitbrix/2048Haskell) | An implementation of the 2048 game |+| [`babel-cards`](https://github.com/srhoulam/babel-cards) | A TUI spaced-repetition memorization tool. Similar to Anki. |+| [`bhoogle`](https://github.com/andrevdm/bhoogle) | A [Hoogle](https://www.haskell.org/hoogle/) client |+| [`bollama`](https://github.com/andrevdm/bollama) | A simple [Ollama](https://ollama.com/) TUI |+| [`brewsage`](https://github.com/gerdreiss/brewsage#readme) | A TUI for Homebrew |+| [`brick-trading-journal`](https://codeberg.org/amano.kenji/brick-trading-journal) | A TUI program that calculates basic statistics from trades |+| [`Brickudoku`](https://github.com/Thecentury/brickudoku) | A hybrid of Tetris and Sudoku |+| [`cbookview`](https://github.com/mlang/cbookview) | A TUI for exploring polyglot chess opening book files |+| [`clifm`](https://github.com/pasqu4le/clifm) | A file manager |+| [`codenames-haskell`](https://github.com/VigneshN1997/codenames-haskell) | An implementation of the Codenames game |+| [`fifteen`](https://github.com/benjaminselfridge/fifteen) | An implementation of the [15 puzzle](https://en.wikipedia.org/wiki/15_puzzle) |+| [`ghcup`](https://www.haskell.org/ghcup/) | A TUI for `ghcup`, the Haskell toolchain manager |+| [`git-brunch`](https://github.com/andys8/git-brunch) | A git branch checkout utility |+| [`Giter`](https://gitlab.com/refaelsh/giter) | A UI wrapper around Git CLI inspired by [Magit](https://magit.vc/). |+| [`gotta-go-fast`](https://github.com/callum-oakley/gotta-go-fast) | A typing tutor |+| [`haradict`](https://github.com/srhoulam/haradict) | A TUI Arabic dictionary powered by [ElixirFM](https://github.com/otakar-smrz/elixir-fm) |+| [`hascard`](https://github.com/Yvee1/hascard) | A program for reviewing "flash card" notes |+| [`haskell-player`](https://github.com/potomak/haskell-player) | An `afplay` frontend |+| [`herms`](https://github.com/jackkiefer/herms) | A command-line tool for managing kitchen recipes |+| [`hic-hac-hoe`](https://github.com/blastwind/hic-hac-hoe) | Play tic tac toe in terminal! |+| [`hledger-iadd`](http://github.com/rootzlevel/hledger-iadd) | An interactive terminal UI for adding hledger journal entries |+| [`hledger-ui`](https://github.com/simonmichael/hledger) | A terminal UI for the hledger accounting system. |+| [`homodoro`](https://github.com/c0nradLC/homodoro) | A terminal application to use the pomodoro technique and keep track of daily tasks |+| [`hskanban`](https://github.com/vincentaxhe/hskanban) | A Kanban organizer |+| [`htyper`](https://github.com/Simon-Hostettler/htyper) | A typing speed test program |+| [`hyahtzee2`](https://github.com/DamienCassou/hyahtzee2#readme) | Famous Yahtzee dice game |+| [`kpxhs`](https://github.com/akazukin5151/kpxhs) | An interactive [Keepass](https://github.com/keepassxreboot/keepassxc/) database viewer |+| [`matterhorn`](https://github.com/matterhorn-chat/matterhorn) | A client for [Mattermost](https://about.mattermost.com/) |+| [`maze`](https://github.com/benjaminselfridge/maze) | A Brick-based maze game |+| [`monad-torrent`](https://github.com/davorluc/monad-torrent) | A simple and minimal torrent client |+| [`monalog`](https://github.com/goosedb/Monalog) | Terminal logs observer |+| [`mushu`](https://github.com/elaye/mushu) | An `MPD` client |+| [`mywork`](https://github.com/kquick/mywork) [[Hackage]](https://hackage.haskell.org/package/mywork) | A tool to keep track of the projects you are working on |+| [`pboy`](https://github.com/2mol/pboy) | A tiny PDF organizer |+| [`purebred`](https://github.com/purebred-mua/purebred) | A mail user agent |+| [`sandwich`](https://codedownio.github.io/sandwich/) | A test framework with a TUI interface |+| [`silly-joy`](https://github.com/rootmos/silly-joy) | An interpreter for Joy |+| [`solitaire`](https://github.com/ambuc/solitaire) | The card game |+| [`sudoku-tui`](https://github.com/evanrelf/sudoku-tui) | A Sudoku implementation |+| [`summoner-tui`](https://github.com/kowainik/summoner/tree/master/summoner-tui) | An interactive frontend to the Summoner tool |+| [`swarm`](https://github.com/byorgey/swarm/) | A 2D programming and resource gathering game |+| [`tart`](https://github.com/jtdaugherty/tart) | A mouse-driven ASCII art drawing program |+| [`tick-tock-tui`](https://github.com/sectore/tick-tock-tui) | A stylish TUI app to handle Bitcoin data provided by [Mempool REST API](https://mempool.space/docs/api/rest) incl. blocks, fees and price converter. |+| [`tetris`](https://github.com/SamTay/tetris) | An implementation of the Tetris game |+| [`thock`](https://github.com/rmehri01/thock) | A modern TUI typing game featuring online racing against friends |+| [`timeloop`](https://github.com/cdupont/timeloop) | A time-travelling demonstrator |+| [`towerHanoi`](https://github.com/shajenM/projects/tree/master/towerHanoi) | Animated solutions to The Tower of Hanoi |+| [`ttyme`](https://github.com/evuez/ttyme) | A TUI for [Harvest](https://www.getharvest.com/) |+| [`ullekha`](https://github.com/ajithnn/ullekha) | An interactive terminal notes/todo app with file/redis persistence |+| [`viewprof`](https://github.com/maoe/viewprof) | A GHC profile viewer |+| [`VOIDSPACE`](https://github.com/ChrisPenner/void-space) | A space-themed typing-tutor game |+| [`wordle`](https://github.com/ivanjermakov/wordle) | An implementation of the Wordle game |+| [`wrapping-editor`](https://github.com/ta0kira/wrapping-editor) | An embeddable editor with support for Brick |+| [`youbrick`](https://github.com/florentc/youbrick) | A feed aggregator and launcher for Youtube channels |++These additional packages also extend `brick`:++| Project | Description | Hackage |+| ------- | ----------- | ------- |+| [`brick-filetree`](https://github.com/ChrisPenner/brick-filetree) | A widget for exploring a directory tree and selecting or flagging files and directories | [Hackage](https://hackage.haskell.org/package/brick-filetree) |+| [`brick-panes`](https://github.com/kquick/brick-panes) | A Brick overlay library providing composition and isolation of screen areas for TUI apps. | [Hackage](https://hackage.haskell.org/package/brick-panes) |+| [`brick-calendar`](https://github.com/ldgrp/brick-calendar) | A library providing a calendar widget for Brick-based applications. | [Hackage](https://hackage.haskell.org/package/brick-calendar) |+| [`brick-skylighting`](https://github.com/jtdaugherty/brick-skylighting) | A library providing integration support for [Skylighting](https://hackage.haskell.org/package/skylighting)-based syntax highlighting. | [Hackage](https://hackage.haskell.org/package/brick-skylighting) |+ Getting Started --------------- -TLDR:+Check out the many demo programs to get a feel for different aspects of+the library: ```-$ cabal sandbox init-$ cabal install -j -f demos-$ .cabal-sandbox/bin/brick-???-demo+$ cabal new-build -f demos+$ find dist-newstyle -type f -name \*-demo ``` -To get started, see the [first few sections of the brick-user guide](docs/guide.rst).+To get started, see the [user guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst). Documentation ------------- -Your documentation options, in recommended order, are:+Documentation for `brick` comes in a variety of forms: -* [FAQ](https://github.com/jtdaugherty/brick/blob/master/FAQ.md)-* [The brick user guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst)-* Haddock (all modules)+* [The official brick user guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst)+* [Haddock documentation](https://hackage.haskell.org/package/brick) * [Demo programs](https://github.com/jtdaugherty/brick/blob/master/programs)+* [FAQ](https://github.com/jtdaugherty/brick/blob/master/FAQ.md) +Feature Overview+----------------++`brick` comes with a bunch of batteries included:++ * Vertical and horizontal box layout widgets+ * Basic single- and multi-line text editor widgets+ * List and table widgets+ * Progress bar widget+ * Simple dialog box widget+ * Border-drawing widgets (put borders around or in between things)+ * Animation support+ * Generic scrollable viewports and viewport scroll bars+ * General-purpose layout control combinators+ * Extensible widget-building API+ * User-customizable attribute themes+ * Type-safe, validated input form API (see the `Brick.Forms` module)+ * A filesystem browser for file and directory selection+ * Borders can be configured to automatically connect!++Brick Discussion+----------------++There are two forums for discussing brick-related things:++1. The [Discussions page](https://github.com/jtdaugherty/brick/discussions) on the github repo, and+1. The `brick-users` Google Group / e-mail list. You can subscribe+ [here](https://groups.google.com/group/brick-users).+ Status ------ -`brick` is young and may be missing some essential features. There are-some places were I have deliberately chosen to worry about performance-later for the sake of spending more time on the design (and to wait on-performance issues to arise first). `brick` exports an extension API-that makes it possible to make your own packages and widgets. If you-use that, you'll also be helping to test whether the exported interface-is usable and complete!+There are some places were I have deliberately chosen to worry about+performance later for the sake of spending more time on the design+(and to wait on performance issues to arise first). `brick` is also+something of an experimental project of mine and some aspects of the+design involve trade-offs that might not be right for your application.+Brick is not intended to be all things to all people; rather, I want it+to provide a good foundation for building complex terminal interfaces+in a declarative style to take away specific headaches of building,+modifying, and working with such interfaces, all while seeing how far we+can get with a pure function to specify the interface. -The development of this library has revealed some bugs in `vty`, and-I've tried to report those as I go. If they haven't been resolved,-you'll see them arise when using `brick`.+`brick` exports an extension API that makes it possible to make your own+packages and widgets. If you use that, you'll also be helping to test+whether the exported interface is usable and complete! +A note on Windows support+-------------------------++Brick supports Windows implicitly by way of Vty's Windows support.+While I don't (and can't) personally test Brick on Windows hosts,+it should be possible to use Brick on Windows. If you have any+trouble, report any issues here. If needed, we'll migrate them to the+[vty-windows](https://github.com/chhackett/vty-windows) repository if+they need to be fixed there.+ Reporting bugs -------------- Please file bug reports as GitHub issues. For best results: - Include the versions of relevant software packages: your terminal- emulator, `brick`, `ghc`, and `vty` will be the most important- ones. Even better, the output of `cabal freeze` would probably be- helpful in making the problem reproducible.+ emulator, `brick`, `ghc`, `vty`, and Vty platform packages will be+ the most important ones. - Clearly describe the behavior you expected ... - - ... and include a mininal demonstration program that exhibits the+ - ... and include a minimal demonstration program that exhibits the behavior you actually observed. Contributing@@ -111,10 +214,22 @@ If you decide to contribute, that's great! Here are some guidelines you should consider to make submitting patches easier for all concerned: + - Patches written completely or partially by AI are unlikely to be+ accepted. - If you want to take on big things, talk to me first; let's have a design/vision discussion before you start coding. Create a GitHub issue and we can use that as the place to hash things out.- - If you make changes, try to make them consistent with the syntactic- conventions I've used in the codebase.- - Please provide Haddock and/or user guide documentation for any- changes you make.+ - Please make changes consistent with the conventions I've used in the+ codebase.+ - Please adjust or provide Haddock and/or user guide documentation+ relevant to any changes you make.+ - Please ensure that commits are `-Wall` clean.+ - Please ensure that each commit makes a single, logical, isolated+ change as much as possible.+ - Please do not submit changes that your linter told you to make. I+ will probably decline them. Relatedly: please do not submit changes+ that change only style without changing functionality.+ - Please do NOT include package version changes in your patches.+ Package version changes are only done at release time when the full+ scope of a release's changes can be evaluated to determine the+ appropriate version change.
brick.cabal view
@@ -1,16 +1,17 @@ name: brick-version: 0.2.3+version: 2.12 synopsis: A declarative terminal user interface library description:- Write terminal applications painlessly with 'brick'! You write an- event handler and a drawing function and the library does the rest.+ Write terminal user interfaces (TUIs) painlessly with 'brick'! You+ write an event handler and a drawing function and the library does the+ rest. . . > module Main where > > import Brick >- > ui :: Widget+ > ui :: Widget () > ui = str "Hello, world!" > > main :: IO ()@@ -31,20 +32,34 @@ license-file: LICENSE author: Jonathan Daugherty <cygnus@foobox.com> maintainer: Jonathan Daugherty <cygnus@foobox.com>-copyright: (c) Jonathan Daugherty 2015+copyright: (c) Jonathan Daugherty 2015-2025 category: Graphics build-type: Simple-cabal-version: >=1.10+cabal-version: 1.18 Homepage: https://github.com/jtdaugherty/brick/ Bug-reports: https://github.com/jtdaugherty/brick/issues+tested-with: GHC == 8.2.2+ || == 8.4.4+ || == 8.6.5+ || == 8.8.4+ || == 8.10.7+ || == 9.0.2+ || == 9.2.8+ || == 9.4.8+ || == 9.6.7+ || == 9.8.4+ || == 9.10.3+ || == 9.12.2 -data-files: README.md,+extra-doc-files: README.md, docs/guide.rst,- CHANGELOG.md+ docs/snake-demo.gif,+ CHANGELOG.md,+ programs/custom_keys.ini Source-Repository head type: git- location: git://github.com/jtdaugherty/brick.git+ location: http://github.com/jtdaugherty/brick Flag demos Description: Build demonstration programs@@ -52,14 +67,26 @@ library default-language: Haskell2010- ghc-options: -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -Wall -Wcompat -O2 -Wunused-packages+ default-extensions: CPP hs-source-dirs: src exposed-modules: Brick+ Brick.Animation Brick.AttrMap+ Brick.BChan+ Brick.BorderMap+ Brick.Keybindings+ Brick.Keybindings.KeyConfig+ Brick.Keybindings.KeyEvents+ Brick.Keybindings.KeyDispatcher+ Brick.Keybindings.Normalize+ Brick.Keybindings.Parse+ Brick.Keybindings.Pretty Brick.Focus+ Brick.Forms Brick.Main- Brick.Markup+ Brick.Themes Brick.Types Brick.Util Brick.Widgets.Border@@ -68,207 +95,466 @@ Brick.Widgets.Core Brick.Widgets.Dialog Brick.Widgets.Edit+ Brick.Widgets.FileBrowser Brick.Widgets.List Brick.Widgets.ProgressBar- Data.Text.Markup+ Brick.Widgets.Table+ Data.IMap other-modules:+ Brick.Animation.Clock+ Brick.Types.Common Brick.Types.TH+ Brick.Types.EventM Brick.Types.Internal Brick.Widgets.Internal - build-depends: base <= 5,- vty >= 5.3.1,- transformers,- data-default,- Diff,- containers,- lens,+ build-depends: base >= 4.9.0.0 && < 4.23.0.0,+ vty >= 6.0,+ vty-crossplatform,+ bimap >= 0.5 && < 0.6,+ data-clist >= 0.1,+ directory >= 1.2.5.0,+ exceptions >= 0.10.0,+ filepath,+ containers >= 0.5.7,+ microlens >= 0.3.0.0 && < 0.6,+ microlens-th,+ microlens-mtl,+ mtl,+ config-ini, vector,- contravariant,+ stm >= 2.4.3, text,- text-zipper >= 0.2.1,+ text-zipper >= 0.13, template-haskell,- deepseq >= 1.3 && < 1.5+ deepseq >= 1.3 && < 1.6,+ unix-compat,+ bytestring,+ word-wrap >= 0.2,+ unordered-containers,+ hashable,+ time +executable brick-custom-keybinding-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: CustomKeybindingDemo.hs+ build-depends: base,+ brick,+ text,+ vty,+ containers,+ microlens,+ microlens-mtl,+ microlens-th++executable brick-table-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: TableDemo.hs+ build-depends: base,+ brick++executable brick-tail-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: TailDemo.hs+ build-depends: base,+ brick,+ text,+ vty,+ random,+ microlens-th,+ microlens-mtl++executable brick-readme-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: ReadmeDemo.hs+ build-depends: base,+ brick++executable brick-file-browser-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: FileBrowserDemo.hs+ build-depends: base,+ vty,+ brick,+ text,+ mtl++executable brick-form-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: FormDemo.hs+ build-depends: base,+ brick,+ text,+ microlens,+ microlens-th,+ vty-crossplatform,+ vty++executable brick-text-wrap-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: TextWrapDemo.hs+ build-depends: base,+ brick,+ word-wrap++executable brick-cache-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: CacheDemo.hs+ build-depends: base,+ brick,+ vty,+ mtl+ executable brick-visibility-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: VisibilityDemo.hs build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty,+ microlens >= 0.3.0.0,+ microlens-th,+ microlens-mtl +executable brick-viewport-scrollbars-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ default-extensions: CPP+ main-is: ViewportScrollbarsDemo.hs+ build-depends: base,+ brick,+ vty,+ vty-crossplatform,+ microlens-mtl,+ microlens-th+ executable brick-viewport-scroll-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010+ default-extensions: CPP main-is: ViewportScrollDemo.hs build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty executable brick-dialog-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: DialogDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty +executable brick-mouse-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ main-is: MouseDemo.hs+ build-depends: base,+ brick,+ vty,+ microlens >= 0.3.0.0,+ microlens-th,+ microlens-mtl,+ mtl+ executable brick-layer-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: LayerDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty,+ microlens >= 0.3.0.0,+ microlens-th,+ microlens-mtl executable brick-suspend-resume-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: SuspendAndResumeDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty,+ microlens >= 0.3.0.0,+ microlens-th +executable brick-cropping-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ main-is: CroppingDemo.hs+ build-depends: base,+ brick,+ vty+ executable brick-padding-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: PaddingDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty +executable brick-theme-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ main-is: ThemeDemo.hs+ build-depends: base,+ brick,+ vty,+ mtl+ executable brick-attr-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: AttrDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty -executable brick-markup-demo+executable brick-tabular-list-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010- main-is: MarkupDemo.hs- build-depends: base <= 5,+ main-is: TabularListDemo.hs+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty,+ microlens >= 0.3.0.0,+ microlens-mtl,+ microlens-th,+ vector executable brick-list-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: ListDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens,+ vty,+ microlens >= 0.3.0.0,+ microlens-mtl,+ mtl, vector +executable brick-list-vi-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ main-is: ListViDemo.hs+ build-depends: base,+ brick,+ vty,+ microlens >= 0.3.0.0,+ microlens-mtl,+ mtl,+ vector++executable brick-animation-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ main-is: AnimationDemo.hs+ build-depends: base,+ brick,+ vty,+ vty-crossplatform,+ containers,+ microlens-platform+ executable brick-custom-event-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: CustomEventDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty,+ microlens >= 0.3.0.0,+ microlens-th,+ microlens-mtl +executable brick-fill-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ main-is: FillDemo.hs+ build-depends: base,+ brick+ executable brick-hello-world-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: HelloWorldDemo.hs- build-depends: base <= 5,- brick,- vty >= 5.3.1,- data-default,- text,- lens+ build-depends: base,+ brick executable brick-edit-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: EditDemo.hs- build-depends: base <= 5,+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty,+ microlens >= 0.3.0.0,+ microlens-th,+ microlens-mtl +executable brick-editor-line-numbers-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-language: Haskell2010+ main-is: EditorLineNumbersDemo.hs+ build-depends: base,+ brick,+ vty,+ microlens >= 0.3.0.0,+ microlens-th,+ microlens-mtl+ executable brick-border-demo if !flag(demos) Buildable: False hs-source-dirs: programs- ghc-options: -threaded -Wall -fno-warn-unused-do-bind -O3+ ghc-options: -threaded -Wall -Wcompat -O2+ default-extensions: CPP default-language: Haskell2010 main-is: BorderDemo.hs+ build-depends: base,+ brick,+ vty,+ text++executable brick-dynamic-border-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-extensions: CPP+ default-language: Haskell2010+ main-is: DynamicBorderDemo.hs build-depends: base <= 5,+ brick++executable brick-progressbar-demo+ if !flag(demos)+ Buildable: False+ hs-source-dirs: programs+ ghc-options: -threaded -Wall -Wcompat -O2+ default-extensions: CPP+ default-language: Haskell2010+ main-is: ProgressBarDemo.hs+ build-depends: base, brick,- vty >= 5.3.1,- data-default,- text,- lens+ vty,+ microlens-mtl,+ microlens-th++test-suite brick-tests+ type: exitcode-stdio-1.0+ hs-source-dirs: tests+ ghc-options: -Wall -Wcompat -Wno-orphans -O2+ default-language: Haskell2010+ main-is: Main.hs+ other-modules: List Render+ build-depends: base <=5,+ brick,+ containers,+ microlens,+ vector,+ vty,+ vty-crossplatform,+ QuickCheck
docs/guide.rst view
@@ -11,867 +11,2147 @@ and as direct as possible. ``brick`` builds on `vty`_; `vty` provides the terminal input and output interface and drawing primitives, while ``brick`` builds on those to provide a high-level application-abstraction and combinators for expressing user interface layouts.--This documentation is intended to provide a high-level overview of-the library's design along with guidance for using it, but details on-specific functions can be found in the Haddock documentation.--The process of writing an application using ``brick`` entails writing-two important functions:--- A *drawing function* that turns your application state into a- specification of how your interface should look, and-- An *event handler* that takes your application state and an input- event and decides whether to change the state or quit the program.--We write drawing functions in ``brick`` using an extensive set of-primitives and combinators to place text on the screen, set its-attributes (e.g. foreground color), and express layout constraints (e.g.-padding, centering, box layouts, scrolling viewports, etc.).--These functions get packaged into a structure that we hand off to the-``brick`` library's main event loop. We'll cover that in detail in `The-App Type`_.--Installation---------------``brick`` can be installed in the "usual way," either by installing-the latest `Hackage`_ release or by cloning the GitHub repository and-building locally.--To install from Hackage::-- $ cabal update- $ cabal install brick--To clone and build locally::-- $ git clone https://github.com/jtdaugherty/brick.git- $ cd brick- $ cabal sandbox init- $ cabal install -j--Building the Demonstration Programs--------------------------------------``brick`` includes a large collection of feature-specific demonstration-programs. These programs are not built by default but can be built by-passing the ``demos`` flag to `cabal install`, e.g.::-- $ cabal install brick -f demos--Conventions-===========--``brick`` has some API conventions worth knowing about as you read this-documentation and as you explore the library source and write your own-programs.--- Use of `lens`_: ``brick`` uses ``lens`` functions internally and also- exposes lenses for many types in the library. However, if you prefer- not to use the ``lens`` interface in your program, all ``lens``- interfaces have non-`lens` equivalents exported by the same module. In- general, the "``L``" suffix on something tells you it is a ``lens``;- the name without the "``L``" suffix is the non-`lens` version. You can- get by without using ``brick``'s ``lens`` interface but your life will- probably be much more pleasant once your application state becomes- sufficiently complex if you use lenses to modify it (see- `appHandleEvent: Handling Events`_).-- Attribute names: some modules export attribute names (see `How- Attributes Work`_) associated with user interface elements. These tend- to end in an "``Attr``" suffix (e.g. ``borderAttr``). In addition,- hierarchical relationships between attributes are documented in- Haddock documentation.-- Use of qualified names: in this document, where sensible, I will use- fully-qualified names whenever I mention something for the first time- or whenever I use something that is not part of ``brick``. Use of- names in this way is not intended to produce executable examples, but- rather to guide you in writing your ``import`` statements.--The App Type-============--To use the library we must provide it with a value of type-``Brick.Main.App``. This type is a record type whose fields perform-various functions:--.. code:: haskell-- data App s e =- App { appDraw :: s -> [Widget]- , appChooseCursor :: s -> [CursorLocation] -> Maybe CursorLocation- , appHandleEvent :: s -> e -> EventM (Next s)- , appStartEvent :: s -> EventM s- , appAttrMap :: s -> AttrMap- , appLiftVtyEvent :: Event -> e- }--The ``App`` type is polymorphic over two types: your application state-type ``s`` and event type ``e``.--The application state type is the type of data that will evolve over the-course of the application's execution; we will provide the library with-its starting value and event handling will transform it as the program-executes.--The event type is the type of events that your event handler-(``appHandleEvent``) will handle. The underlying ``vty`` library-provides ``Graphics.Vty.Event`` and this forms the basis of all events-we will handle with ``brick`` applications. The-``Brick.Main.defaultMain`` function expects an ``App s Event`` since-this is a common case.--However, we often need to extend our notion of events beyond those-originating from the keyboard. Imagine an application with multiple-threads and network or disk I/O. Such an application will need to have-its own internal events to pass to the event handler as (for example)-network data arrives. To accommodate this we allow an ``App`` to use an-event type of your own design, so long as it provides a constructor for-``vty``'s ``Event`` type (``appLiftVtyEvent``). For more details, see-`Using Your Own Event Type`_.--The various fields of ``App`` will be described in the sections below.--To run an ``App``, we pass it to ``Brick.Main.defaultMain`` or-``Brick.Main.customMain`` along with an initial application state value.--appDraw: Drawing an Interface--------------------------------The value of ``appDraw`` is a function that turns the current-application state into a list of *layers* of type ``Widget``, listed-topmost first, that will make up the interface. Each ``Widget`` gets-turned into a ``vty`` layer and the resulting layers are drawn to the-terminal.--The ``Widget`` type is the type of *drawing instructions*. The body of-your drawing function will use one or more drawing functions to build or-transform ``Widget`` values to describe your interface. These-instructions will then be executed with respect to three things:--- The size of the terminal: the size of the terminal determines how many- ``Widget`` values behave. For example, fixed-size ``Widget`` values- such as text strings behave the same under all conditions (and get- cropped if the terminal is too small) but layout combinators such as- ``Brick.Widgets.Core.vBox`` or ``Brick.Widgets.Center.center`` use the- size of the terminal to determine how to lay other widgets out. See- `How Widgets and Rendering Work`_.-- The application's attribute map (``appAttrMap``): drawing functions- requesting the use of attributes cause the attribute map to be- consulted. See `How Attributes Work`_.-- The state of scrollable viewports: the state of any scrollable- viewports on the *previous* drawing will be considered. For more- details, see `Viewports`_.--The ``appDraw`` function is called when the event loop begins to draw-the application as it initially appears. It is also called right after-an event is processed by ``appHandleEvent``. Even though the function-returns a specification of how to draw the entire screen, the underlying-``vty`` library goes to some trouble to efficiently update only the-parts of the screen that have changed so you don't need to worry about-this.--Where do I find drawing functions?-**********************************--The most important module providing drawing functions is-``Brick.Widgets.Core``. Beyond that, any module in the ``Brick.Widgets``-namespace provides specific kinds of functionality.--appHandleEvent: Handling Events----------------------------------The value of ``appHandleEvent`` is a function that decides how to modify-the application state as a result of an event. It also decides whether-to continue program execution. The function takes the current-application state and the event and returns the *next application-state*:--.. code:: haskell-- appHandleEvent :: s -> e -> EventM (Next s)--The ``EventM`` monad is the event-handling monad. This monad is a-transformer around ``IO``, so you are free to do I/O in this monad by-using ``liftIO``. Beyond I/O, this monad is just used to make scrolling-requests to the renderer (see `Viewports`_). Keep in mind that time-spent blocking in your event handler is time during which your UI is-unresponsive, so consider this when deciding whether to have background-threads do work instead of inlining the work in the event handler.--The ``Next s`` value describes what should happen after the event-handler is finished. We have three choices:--* ``Brick.Main.continue s``: continue executing the event loop with the- specified application state ``s`` as the next value. Commonly this is- where you'd modify the state based on the event and return it.-* ``Brick.Main.halt s``: halt the event loop and return the final- application state value ``s``. This state value is returned to the- caller of ``defaultMain`` or ``customMain`` where it can be used prior- to finally exiting ``main``.-* ``Brick.Main.suspendAndResume act``: suspend the ``brick`` event loop- and execute the specified ``IO`` action ``act``. The action ``act``- must be of type ``IO s``, so when it executes it must return the next- application state. When ``suspendAndResume`` is used, the ``brick``- event loop is shut down and the terminal state is restored to its- state when the ``brick`` event loop began execution. When it finishes- executing, the event loop will be resumed using the returned state- value. This is useful for situations where your program needs to- suspend your interface and execute some other program that needs to- gain control of the terminal (such as an external editor).--The HandleEvent Type Class-**************************--Event handlers are responsible for transforming the application state.-While you can use ordinary methods to do this such as pattern matching-and pure function calls, some widget state types such as the ones-provided by the ``Brick.Widgets.List`` and ``Brick.Widgets.Edit``-modules support another interface: the ``Brick.Types.HandleEvent`` type-class.--The ``HandleEvent`` type class has only one method:--.. code:: haskell-- handleEvent :: Event -> a -> EventM a--Instances of ``HandleEvent`` provide reasonable default behavior for-handling input events; for example, arrow keys change the ``List``-selection and input keys modify the text in ``Editor`` states.--Since ``handleEvent`` runs in ``EventM``, event handlers-written this way have access to rendering viewport states via-``Brick.Main.lookupViewport`` and the ``IO`` monad via ``liftIO``.--To use ``handleEvent`` in your program, invoke it on the relevant piece-of state in your event handler, e.g.,--.. code:: haskell-- myEvent :: s -> e -> EventM (Next s)- myEvent s e = continue =<< handleEvent e s--This pattern works fine when your application state instances-``HandleEvent``, but it can become unpleasant if the value on which-you want to invoke ``handleEvent`` is embedded deeply within your-application state. If you have chosen to generate lenses for your-application state fields, you can use the convenience function-``handleEventLensed`` by specifying your state, a lens, and the event:--.. code:: haskell-- myEvent :: s -> e -> EventM (Next s)- myEvent s e = continue =<< handleEventLensed s someLens e--Compare that with the more verbose explicit version:--.. code:: haskell-- myEvent :: s -> e -> EventM (Next s)- myEvent s e = do- newVal <- handleEvent e (s^.someLens)- continue $ s & someLens .~ newVal--Using Your Own Event Type-*************************--Since we often need to communicate application-specific events-beyond input events to the event handler, the ``App`` type is-polymorphic over the event type we want to handle. If we use-``Brick.Main.defaultMain`` to run our ``App``, we have to use-``Graphics.Vty.Event`` as our event type. But if our application has-other event-handling needs, we need to use our own event type.--To do this, we first define an event type:--.. code:: haskell-- data CustomEvent =- VtyEvent Graphics.Vty.Event- | CustomEvent1- | CustomEvent2--Our custom event type *must* provide a constructor capable of taking-a ``Graphics.Vty.Event`` value. This allows the ``brick`` event loop-to send us ``vty`` events in the midst of our custom ones. To allow-``brick`` to do this, we provide this constructor as the value of-``appLiftVtyEvent``. This way, ``brick`` can wrap a ``vty`` event using-our custom event type and then pass it to our event handler (which takes-``CustomEvent`` values). In this case we'd set ``appLiftVtyEvent =-VtyEvent``.--Once we have set ``appLiftVtyEvent`` in this way, we also need to set up-a mechanism for getting our custom events into the ``brick`` event loop-from other threads. To do this we use a ``Control.Concurrent.Chan`` and-call ``Brick.Main.customMain`` instead of ``Brick.Main.defaultMain``:--.. code:: haskell-- main :: IO ()- main = do- eventChan <- Control.Concurrent.newChan- finalState <- customMain (Graphics.Vty.mkVty Data.Default.def) eventChan app initialState- -- Use finalState and exit--Beyond just the application and its initial state, the ``customMain``-function lets us have control over how the ``vty`` library is-initialized and how ``brick`` gets custom events to give to our event-handler. ``customMain`` is the entry point into ``brick`` when you need-to use your own event type.--Starting up: appStartEvent-**************************--When an application starts, it may be desirable to perform some of-the duties typically only possible when an event has arrived, such as-setting up initial scrolling viewport state. Since such actions can only-be performed in ``EventM`` and since we do not want to wait until the-first event arrives to do this work in ``appHandleEvent``, the ``App``-type provides ``appStartEvent`` function for this purpose:--.. code:: haskell-- appStartEvent :: s -> EventM s--This function takes the initial application state and returns it in-``EventM``, possibly changing it and possibly making viewport requests.-For more details, see `Viewports`_. You will probably just want to use-``return`` as the implementation of this function for most applications.--appChooseCursor: Placing the Cursor--------------------------------------The rendering process for a ``Widget`` may return information about-where that widget would like to place the cursor. For example, a text-editor will need to report a cursor position. However, since a-``Widget`` may be a composite of many such cursor-placing widgets, we-have to have a way of choosing which of the reported cursor positions,-if any, is the one we actually want to honor.--To decide which cursor placement to use, or to decide not to show one at-all, we set the ``App`` type's ``appChooseCursor`` function:--.. code:: haskell-- appChooseCursor :: s -> [CursorLocation] -> Maybe CursorLocation--The event loop renders the interface and collects the-``Brick.Types.CursorLocation`` values produced by the rendering process-and passes those, along with the current application state, to this-function. Using your application state (to track which text input box-is "focused," say) you can decide which of the locations to return or-return ``Nothing`` if you do not want to show a cursor.--We decide which location to show by looking at the ``Brick.Types.Name``-value contained in the ``cursorLocationName`` field. The ``Name``-value associated with a cursor location will be the ``Name`` of the-``Widget`` that requested it; this is why constructors for widgets like-``Brick.Widgets.Edit.editor`` require a ``Name`` parameter. The ``Name``-lets us distinguish between many cursor-placing widgets of the same-type.--``Brick.Main`` provides various convenience functions to make cursor-selection easy in common cases:--* ``neverShowCursor``: never show any cursor.-* ``showFirstCursor``: always show the first cursor request given; good- for applications with only one cursor-placing widget.-* ``showCursorNamed``: show the cursor with the specified name or- ``Nothing`` if it is not requested.--Widgets request cursor placement by using the-``Brick.Widgets.Core.showCursor`` combinator. For example, this widget-places a cursor on the first "``o``" in "``foo``" assocated with the-cursor name "``myCursor``":--.. code:: haskell-- let w = showCursor (Name "myCursor") (Brick.Types.Location (1, 0))- (Brick.Widgets.Core.str "foobar")--appAttrMap: Managing Attributes----------------------------------In ``brick`` we use an *attribute map* to assign attibutes to elements-of the interface. Rather than specifying specific attributes when-drawing a widget (e.g. red-on-black text) we specify an *attribute name*-that is an abstract name for the kind of thing we are drawing, e.g.-"keyword" or "e-mail address." We then provide an attribute map which-maps those attribute names to actual attributes. This approach lets us:--* Change the attributes at runtime, letting the user change the- attributes of any element of the application arbitrarily without- forcing anyone to build special machinery to make this configurable;-* Write routines to load saved attribute maps from disk;-* Provide modular attribute behavior for third-party components, where- we would not want to have to recompile third-party code just to change- attributes, and where we would not want to have to pass in attribute- arguments to third-party drawing functions.--This lets us put the attribute mapping for an entire app, regardless of-use of third-party widgets, in one place.--To create a map we use ``Brick.AttrMap.attrMap``, e.g.,--.. code:: haskell-- App { ...- , appAttrMap = const $ attrMap Graphics.Vty.defAttr [(someAttrName, fg blue)]- }--To use an attribute map, we specify the ``App`` field ``appAttrMap`` as-the function to return the current attribute map each time rendering-occurs. This function takes the current application state, so you may-choose to store the attribute map in your application state. You may-also choose not to bother with that and to just set ``appAttrMap = const-someMap``.--To draw a widget using an attribute name in the map, use-``Brick.Widgets.Core.withAttr``. For example, this draws a string with a-``blue`` background:--.. code:: haskell-- let w = withAttr blueBg $ str "foobar"- blueBg = attrName "blueBg"- myMap = attrMap defAttr [ (blueBg, Brick.Util.bg Graphics.Vty.blue)- ]--For complete details on how attribute maps and attribute names work, see-the Haddock documentation for the ``Brick.AttrMap`` module. See also-`How Attributes Work`_.--How Widgets and Rendering Work-==============================--When ``brick`` renders a ``Widget``, the widget's rendering routine is-evaluated to produce a ``vty`` ``Image`` of the widget. The widget's-rendering routine runs with some information called the *rendering-context* that contains:--* The size of the area in which to draw things-* The name of the current attribute to use to draw things-* The map of attributes to use to look up attribute names-* The active border style to use when drawing borders--Available Rendering Area---------------------------The most important element in the rendering context is the rendering-area: This part of the context tells the widget being drawn how many-rows and columns are available for it to consume. When rendering begins,-the widget being rendered (i.e. a layer returned by an ``appDraw``-function) gets a rendering context whose rendering area is the size of-the terminal. This size information is used to let widgets take up that-space if they so choose. For example, a string "Hello, world!" will-always take up one row and 13 columns, but the string "Hello, world!"-*centered* will always take up one row and *all available columns*.--How widgets use space when rendered is described in two pieces of-information in each ``Widget``: the widget's horizontal and vertical-growth policies. These fields have type ``Brick.Types.Size`` and can-have the values ``Fixed`` and ``Greedy``.--A widget advertising a ``Fixed`` size in a given dimension is a widget-that will always consume the same number of rows or columns no-matter how many it is given. Widgets can advertise different-vertical and horizontal growth policies for example, the-``Brick.Widgets.Border.hCenter`` function centers a widget and is-``Greedy`` horizontally and defers to the widget it centers for vertical-growth behavior.--These size policies govern the box layout algorithm that is at-the heart of every non-trivial drawing specification. When we use-``Brick.Widgets.Core.vBox`` and ``Brick.Widgets.Core.hBox`` to-lay things out (or use their binary synonyms ``<=>`` and ``<+>``,-respectively), the box layout algorithm looks at the growth policies of-the widgets it receives to determine how to allocate the available space-to them.--For example, imagine that the terminal window is currently 10 rows high-and 50 columns wide. We wish to render the following widget:--.. code:: haskell-- let w = (str "Hello," <=> str "World!")--Rendering this to the terminal will result in "Hello," and "World!"-underneath it, with 8 rows unoccupied by anything. But if we wished to-render a vertical border underneath those strings, we would write:--.. code:: haskell-- let w = (str "Hello," <=> str "World!" <=> vBorder)--Rendering this to the terminal will result in "Hello," and "World!"-underneath it, with 8 rows remaining occupied by vertical border-characters ("``|``") one column wide. The vertical border widget is-designed to take up however many rows it was given, but rendering the-box layout algorithm has to be careful about rendering such ``Greedy``-widgets because the won't leave room for anything else. Since the box-widget cannot know the sizes of its sub-widgets until they are rendered,-the ``Fixed`` widgets get rendered and their sizes are used to determine-how much space is left for ``Greedy`` widgets.--When using widgets it is important to understand their horizontal and-vertical space behavior by knowing their ``Size`` values. Those should-be made clear in the Haddock documentation.--Limiting Rendering Area--------------------------If you'd like to use a ``Greedy`` widget but want to limit how much-space it consumes, you can turn it into a ``Fixed`` widget by using-one of the *limiting combinators*, ``Brick.Widgets.Core.hLimit`` and-``Brick.Widgets.Core.vLimit``. These combinators take widgets and turn-them into widgets with a ``Fixed`` size (in the relevant dimension) and-run their rendering functions in a modified rendering context with a-restricted rendering area.--For example, the following will center a string in 30 columns, leaving-room for something to be placed next to it as the terminal width-changes:--.. code:: haskell-- let w = hLimit 30 $ hCenter $ str "Hello, world!"--The Attribute Map--------------------The rendering context contains an attribute map (see `How Attributes-Work`_ and `appAttrMap: Managing Attributes`_) which is used to look up-attribute names from the drawing specification. The map originates from-``Brick.Main.appAttrMap`` and can be manipulated on a per-widget basis-using ``Brick.Widgets.Core.updateAttrMap``.--The Active Border Style--------------------------Widgets in the ``Brick.Widgets.Border`` module draw border characters-(horizontal, vertical, and boxes) between and around other widgets. To-ensure that widgets across your application share a consistent visual-style, border widgets consult the rendering context's *active border-style*, a value of type ``Brick.Widgets.Border.Style``, to get the-characters used to draw borders.--The default border style is ``Brick.Widgets.Border.Style.unicode``. To-change border styles, use the ``Brick.Widgets.Core.withBorderStyle``-combinator to wrap a widget and change the border style it uses when-rendering. For example, this will use the ``ascii`` border style instead-of ``unicode``:--.. code:: haskell-- let w = withBorderStyle Brick.Widgets.Border.Style.ascii $- Brick.Widgets.Border.border $ str "Hello, world!"--How Attributes Work-===================--In addition to letting us map names to attributes, attribute maps-provide hierarchical attribute inheritance: a more specific attribute-derives any properties (e.g. background color) that it does not specify-from more general attributes in hierarchical relationship to it, letting-us customize only the parts of attributes that we want to change without-having to repeat ourselves.--For example, this draws a string with a foreground color of ``white`` on-a background color of ``blue``:--.. code:: haskell-- let w = withAttr specificAttr $ str "foobar"- generalAttr = attrName "general"- specificAttr = attrName "general" <> attrName "specific"- myMap = attrMap defAttr [ (generalAttr, bg blue)- , (specificAttr, fg white)- ]--Functions ``Brick.Util.fg`` and ``Brick.Util.bg`` specify-partial attributes, and map lookups start with the desired name-(``general/specific`` in this case) and walk up the name hierarchy (to-``general``), merging partial attribute settings as they go, letting-already-specified attribute settings take precedence. Finally, any-attribute settings not specified by map lookups fall back to the map's-*default attribute*, specified above as ``Graphics.Vty.defAttr``. In-this way, if you want everything in your application to have a ``blue``-background color, you only need to specify it *once*: in the attribute-map's default attribute. Any other attribute names can merely customize-the foreground color.--In addition to using the attribute map provided by ``appAttrMap``,-the map can be customized on a per-widget basis by using the attribute-map combinators:--* ``Brick.Widgets.Core.updateAttrMap``-* ``Brick.Widgets.Core.forceAttr``-* ``Brick.Widgets.Core.withDefAttr``--Viewports-=========--A *viewport* is a scrollable window onto another widget. Viewports have-a *scrolling direction* of type ``Brick.Types.ViewportType`` which can-be one of:--* ``Horizontal``: the viewport can only scroll horizontally.-* ``Vertical``: the viewport can only scroll vertically.-* ``Both``: the viewport can scroll both horizontally and vertically.--The ``Brick.Widgets.Core.viewport`` combinator takes another widget and-embeds it in a named viewport. We name the viewport so that we can-keep track of its scrolling state in the renderer, and so that you can-make scrolling requests. The viewport's name is its handle for these-operations (see `Scrolling Viewports in Event Handlers`_). *The viewport-name must be unique across your interface.*--For example, the following puts a string in a horizontally-scrollable-viewport:--.. code:: haskell-- let w = viewport (Name "myViewport") Horizontal $ str "Hello, world!"--The above example is incomplete. A ``viewport`` specification means that-the widget in the viewport will be placed in a viewport window that is-``Greedy`` in both directions (see `Available Rendering Area`_). This-is suitable if we want the viewport size to be the size of the entire-terminal window, but if we want to embed this scrollable viewport-somewhere in our interface, we want to control its dimensions. To do so,-we use the limiting combinators (see `Limiting Rendering Area`_):--.. code:: haskell-- let w = hLimit 5 $- vLimit 1 $- viewport (Name "myViewport") Horizontal $ str "Hello, world!"--Now the example produces a scrollable window one row high and five-columns wide initially showing "Hello". The next two sections discuss-the two ways in which this viewport can be scrolled.--Scrolling Viewports in Event Handlers----------------------------------------The most direct way to scroll a viewport is to make *scrolling requests*-in the ``EventM`` event-handling monad. Scrolling requests ask the-render to update the state of a viewport the next time the user-interface is rendered. Those state updates will be made with respect to-the *previous* viewport state. This approach is the best approach to use-to scroll widgets that have no notion of a cursor. For cursor-based-scrolling, see `Scrolling Viewports With Visibility Requests`_.--To make scrolling requests, we first create a-``Brick.Main.ViewportScroll`` from a viewport name with-``Brick.Main.viewportScroll``:--.. code:: haskell-- let vp = viewportScroll (Name "myViewport")--The ``ViewportScroll`` record type contains a number of scrolling-functions for making scrolling requests:--.. code:: haskell-- hScrollPage :: Direction -> EventM ()- hScrollBy :: Int -> EventM ()- hScrollToBeginning :: EventM ()- hScrollToEnd :: EventM ()- vScrollPage :: Direction -> EventM ()- vScrollBy :: Int -> EventM ()- vScrollToBeginning :: EventM ()- vScrollToEnd :: EventM ()--In each case the scrolling function scrolls the viewport by the-specified amount in the specified direction; functions prefixed with-``h`` scroll horizontally and functions prefixed with ``v`` scroll-vertically.--Scrolling operations do nothing when they don't make sense for the-specified viewport; scrolling a ``Vertical`` viewport horizontally is a-no-op, for example.--Using ``viewportScroll`` and the ``myViewport`` example given above, we-can write an event handler that scrolls the "Hello, world!" viewport one-column to the right:--.. code:: haskell-- myHandler :: s -> e -> EventM (Next s)- myHandler s e = do- let vp = viewportScroll (Name "myViewport")- hScrollBy vp 1- continue s--Scrolling Viewports With Visibility Requests-----------------------------------------------When we need to scroll widgets only when a cursor in the viewport leaves-the viewport's bounds, we need to use *visibility requests*. A-visibility request is a hint to the renderer that some element of a-widget inside a viewport should be made visible, i.e., that the viewport-should be scrolled to bring the requested element into view.--To use a visibility request to make a widget in a viewport visible, we-simply wrap it with ``visible``:--.. code:: haskell-- let w = viewport (Name "myViewport") Horizontal $- (visible $ str "Hello," <+> (str " world!")--This example requests that the "``myViewport``" viewport be scrolled so-that "Hello," is visible. We could extend this example with a value-in the application state indicating which word in our string should-be visible and then use that to change which string gets wrapped with-``visible``; this is the basis of cursor-based scrolling.--Note that a visibility request does not change the state of a viewport-*if the requested widget is already visible*! This important detail is-what makes visibility requests so powerful, because they can be used to-capture various cursor-based scenarios:--* The ``Brick.Widgets.Edit`` widget uses a visibility request to make its- 1x1 cursor position visible, thus making the text editing widget fully- scrollable *while being entirely scrolling-unaware*.-* The ``Brick.Widgets.List`` widget uses a visibility request to make- its selected item visible regardless of its size, which makes- the list widget both scrolling-unaware and also makes it support- variable-height items for free.--Viewport Restrictions------------------------Viewports impose one restriction: a viewport that is scrollable in some-direction can only embed a widget that has a ``Fixed`` size in that-direction. This extends to ``Both`` type viewports: they can only embed-widgets that are ``Fixed`` in both directions. This restriction is-because when viewports embed a widget, they relax the rendering area-constraint in the rendering context, but doing so to a large enough-number for ``Greedy`` widgets would result in a widget that is too big-and not scrollable in a useful way.--Violating this restriction will result in a runtime exception.--Implementing Your Own Widgets-=============================--``brick`` exposes all of the internals you need to implement your own-widgets. Those internals, together with ``Graphics.Vty``, can be used to-create widgets from the ground up. We start by writing a constructor-function:--.. code:: haskell-- myWidget :: ... -> Widget- myWidget ... =- Widget Fixed Fixed $ do- ...--We specify the horizontal and vertical growth policies of the widget-as ``Fixed`` in this example, although they should be specified-appropriately (see `How Widgets and Rendering Work`_). Lastly we specify-the *rendering function*, a function of type--.. code:: haskell-- render :: RenderM Result--which is a function returning a ``Brick.Types.Result``:--.. code:: haskell-- data Result =- Result { image :: Graphics.Vty.Image- , cursors :: [Brick.Types.CursorLocation]- , visibilityRequests :: [Brick.Types.VisibilityRequest]- }--The ``RenderM`` monad gives us access to the rendering context (see `How-Widgets and Rendering Work`_) via the ``Brick.Types.getContext``-function. The context type is:--.. code:: haskell-- data Context =- Context { ctxAttrName :: AttrName- , availWidth :: Int- , availHeight :: Int- , ctxBorderStyle :: BorderStyle- , ctxAttrMap :: AttrMap- }--and has `lens` fields exported as described in `Conventions`_.--The job of the rendering function is to return a rendering result which,-at a minimum, means producing a ``vty`` ``Image``. In addition, if you-so choose, you can also return one or more cursor positions in the-``cursors`` field of the ``Result`` as well as visibility requests (see-`Viewports`_) in the ``visibilityRequests`` field. Returned visibility-requests and cursor positions should be relative to the upper-left-corner of your widget, ``Location (0, 0)``. When your widget is placed-in others, such as boxes, the ``Result`` data you returned will be-offset (as described in `Rendering Sub-Widgets`_) to result in correct-coordinates once the entire interface has been rendered.--Using the Rendering Context------------------------------The most important fields of the context are the rendering area fields-``availWidth`` and ``availHeight``. These fields must be used to-determine how much space your widget has to render.--To perform an attribute lookup in the attribute map for the context's-current attribute, use ``Brick.Types.attrL``.--For example, to build a widget that always fills the available width and-height with a fill character using the current attribute, we could-write:--.. code:: haskell-- myFill :: Char -> Widget- myFill ch =- Widget Greedy Greedy $ do- ctx <- getContext- let a = ctx^.attrL- return $ Result (Graphics.Vty.charFill ch a (ctx^.availWidth) (ctx^.availHeight))- [] []--Rendering Sub-Widgets------------------------If your custom widget wraps another, then in addition to rendering the-wrapped widget and augmenting its returned ``Result`` *it must also-translate the resulting cursor locations and visibility requests*.-This is vital to maintaining the correctness of cursor locations and-visbility locations as widget layout proceeds. To do so, use the-``Brick.Widgets.Core.addResultOffset`` function to offset the elements-of a ``Result`` by a specified amount. The amount depends on the nature-of the offset introduced by your wrapper widget's logic.--Widgets are not required to respect the rendering context's width and-height restrictions. Widgets may be embedded in viewports or translated-so they must render without cropping to work in those scenarios.-However, widgets rendering other widgets *should* enforce the rendering-context's constraints to avoid using more space than is available. The-``Brick.Widgets.Core.cropToContext`` function is provided to make this-easy:--.. code:: haskell-- let w = cropToContext someWidget--Widgets wrapped with ``cropToContext`` can be safely embedded in other-widgets. If you don't want to crop in this way, you can use any of-``vty``'s cropping functions to operate on the ``Result`` image as-desired.--.. _vty: https://github.com/coreyoconnor/vty-.. _Hackage: http://hackage.haskell.org/-.. _lens: http://hackage.haskell.org/package/lens+abstraction, combinators for expressing user interface layouts, and+infrastructure for handling events.++This documentation is intended to provide a high-level overview of+the library's design along with guidance for using it, but details on+specific functions can be found in the Haddock documentation.++The process of writing an application using ``brick`` entails writing+two important functions:++- A *drawing function* that turns your application state into a+ specification of how your interface should be drawn, and+- An *event handler* that takes your application state and an input+ event and decides whether to change the state or quit the program.++We write drawing functions in ``brick`` using an extensive set of+primitives and combinators to place text on the screen, set its+attributes (e.g. foreground color), and express layout constraints (e.g.+padding, centering, box layouts, scrolling viewports, etc.).++These functions get packaged into an ``App`` structure that we hand off+to the ``brick`` library's main event loop. We'll cover that in detail+in `The App Type`_.++Installation+------------++``brick`` can be installed in the "usual way," either by installing+the latest `Hackage`_ release or by cloning the GitHub repository and+building locally.++To install from Hackage::++ $ cabal update+ $ cabal install brick++To clone and build locally::++ $ git clone https://github.com/jtdaugherty/brick.git+ $ cd brick+ $ cabal new-build++Your package will need some dependencies:++* ``brick``,+* ``vty >= 6.0``, and+* ``vty-crossplatform`` or ``vty-unix`` or ``vty-windows``, depending+ on which platform(s) your application supports.++Building the Demonstration Programs+-----------------------------------++``brick`` includes a large collection of feature-specific demonstration+programs. These programs are not built by default but can be built by+passing the ``demos`` flag to ``cabal install``, e.g.::++ $ cabal install brick -f demos++Conventions+===========++``brick`` has some API conventions worth knowing about as you read this+documentation and as you explore the library source and write your own+programs.++- Use of `microlens`_ packages: ``brick`` uses the ``microlens`` family+ of packages internally and also exposes lenses for many types in the+ library. However, if you prefer not to use the lens interface in your+ program, all lens interfaces have non-lens equivalents exported by+ the same module. In general, the "``L``" suffix on something tells+ you it is a lens; the name without the "``L``" suffix is the non-lens+ version. You can get by without using ``brick``'s lens interface+ but your life will probably be much more pleasant if you use lenses+ to modify your application state once it becomes sufficiently+ complex (see `appHandleEvent: Handling Events`_ and `Event Handlers+ for Component State`_).+- Attribute names: some modules export attribute names (see `How+ Attributes Work`_) associated with user interface elements. These tend+ to end in an "``Attr``" suffix (e.g. ``borderAttr``). In addition,+ hierarchical relationships between attributes are documented in+ Haddock documentation.+- Use of qualified Haskell identifiers: in this document, where+ sensible, I will use fully-qualified identifiers whenever I mention+ something for the first time or whenever I use something that is+ not part of ``brick``. Use of qualified names is not intended to+ produce executable examples, but rather to guide you in writing your+ ``import`` statements.++Compiling Brick Applications+============================++Brick applications must be compiled with the threaded RTS using the GHC+``-threaded`` option.++The ``App`` Type+================++To use the library we must provide it with a value of type+``Brick.Main.App``. This type is a record type whose fields perform+various functions:++.. code:: haskell++ data App s e n =+ App { appDraw :: s -> [Widget n]+ , appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)+ , appHandleEvent :: BrickEvent n e -> EventM n s ()+ , appStartEvent :: EventM n s ()+ , appAttrMap :: s -> AttrMap+ }++The ``App`` type is parameterized over three types. These type variables+will appear in the signatures of many library functions and types. They+are:++- The **application state type** ``s``: the type of data that will+ evolve over the course of the application's execution. Your+ application will provide the library with its starting value and event+ handling will transform it as the program executes. When a ``brick``+ application exits, the final application state will be returned.+- The **event type** ``e``: the type of custom application events+ that your application will need to produce and handle in+ ``appHandleEvent``. All applications will be provided with events from+ the underlying ``vty`` library, such as keyboard events or resize+ events; this type variable indicates the type of *additional* events+ the application will need. For more details, see `Using Your Own Event+ Type`_.+- The **resource name type** ``n``: during application execution we+ sometimes need a way to refer to rendering state, such as the space+ taken up by a given widget, the state for a scrollable viewport, a+ mouse click, or a cursor position. For these situations we need a+ unique handle called a *resource name*. The type ``n`` specifies the+ name type the application will use to identify these bits of state+ produced and managed by the renderer. The resource name type must be+ provided by your application; for more details, see `Resource Names`_.++The various fields of ``App`` will be described in the sections below.++Running an Application+----------------------++To run an ``App``, we pass it to ``Brick.Main.defaultMain`` or+``Brick.Main.customMain`` along with an initial application state value:++.. code:: haskell++ main :: IO ()+ main = do+ let app = App { ... }+ initialState = ...+ finalState <- defaultMain app initialState+ -- Use finalState and exit++The ``customMain`` function is for more advanced uses; for details see+`Using Your Own Event Type`_.++``appDraw``: Drawing an Interface+---------------------------------++The value of ``appDraw`` is a function that turns the current+application state into a list of *layers* of type ``Widget``, listed+topmost first, that will make up the interface. Each ``Widget`` gets+turned into a ``vty`` layer and the resulting layers are drawn to the+terminal.++The ``Widget`` type is the type of *drawing instructions*. The body of+your drawing function will use one or more drawing functions to build or+transform ``Widget`` values to describe your interface. These+instructions will then be executed with respect to three things:++- The size of the terminal: the size of the terminal determines how many+ ``Widget`` values behave. For example, fixed-size ``Widget`` values+ such as text strings behave the same under all conditions (and get+ cropped if the terminal is too small) but layout combinators such as+ ``Brick.Widgets.Core.vBox`` or ``Brick.Widgets.Center.center`` use the+ size of the terminal to determine how to lay other widgets out. See+ `How Widgets and Rendering Work`_.+- The application's attribute map (``appAttrMap``): drawing functions+ requesting the use of attributes cause the attribute map to be+ consulted. See `How Attributes Work`_.+- The state of scrollable viewports: the state of any scrollable+ viewports on the *previous* drawing will be considered. For more+ details, see `Viewports`_.++The ``appDraw`` function is called when the event loop begins to draw+the application as it initially appears. It is also called right after+an event is processed by ``appHandleEvent``. Even though the function+returns a specification of how to draw the entire screen, the underlying+``vty`` library goes to some trouble to efficiently update only the+parts of the screen that have changed so you don't need to worry about+this.++Where do I find drawing functions?+**********************************++The most important module providing drawing functions is+``Brick.Widgets.Core``. Beyond that, any module in the ``Brick.Widgets``+namespace provides specific kinds of functionality.++``appHandleEvent``: Handling Events+-----------------------------------++The value of ``appHandleEvent`` is a function that decides how to modify+the application state as a result of an event:++.. code:: haskell++ appHandleEvent :: BrickEvent n e -> EventM n s ()++``appHandleEvent`` is responsible for deciding how to change the state+based on incoming events. The single parameter to the event handler is+the event to be handled. Its type variables ``n`` and ``e`` correspond+to the *resource name type* and *event type* of your application,+respectively, and must match the corresponding types in ``App`` and+``EventM``.++The ``EventM`` monad is parameterized on the *resource name type*+``n`` and your application's state type ``s``. The ``EventM`` monad+is a state monad over ``s``, so one way to access and modify your+application's state in an event handler is to use the ``MonadState``+type class and associated operations from the ``mtl`` package. The+recommended approach, however, is to use the lens operations from the+``microlens-mtl`` package with lenses to perform concise state updates.+We'll cover this topic in more detail in `Event Handlers for Component+State`_.++Once the event handler has performed any relevant state updates, it can+also indicate what should happen once the event handler has finished+executing. By default, after an event handler has completed, Brick will+redraw the screen with the application state (by calling ``appDraw``)+and wait for the next input event. However, there are two other options:++* ``Brick.Main.halt``: halt the event loop. The application state as it+ exists after the event handler completes is returned to the caller+ of ``defaultMain`` or ``customMain``.+* ``Brick.Main.continueWithoutRedraw``: continue executing the event+ loop, but do not redraw the screen using the new state before waiting+ for another input event. This is faster than the default continue+ behavior since it doesn't redraw the screen; it just leaves up the+ previous screen contents. This function is only useful when you know+ that your event handler's state change(s) won't cause anything on+ the screen to change. Use this only when you are certain that no+ redraw of the screen is needed *and* when you are trying to address a+ performance problem. (See also `The Rendering Cache`_ for details on+ how to deal with rendering performance issues.)++The ``EventM`` monad is a transformer around ``IO`` so I/O is possible+in this monad by using ``liftIO``. Keep in mind, however, that event+handlers should execute as quickly as possible to avoid introducing+screen redraw latency. Consider using background threads to work+asynchronously when handling an event would otherwise cause redraw+latency.++``EventM`` is also used to make scrolling requests to the renderer (see+`Viewports`_), obtain named extents (see `Extents`_), and other duties.++Event Handlers for Component State+**********************************++The top-level ``appHandleEvent`` handler is responsible for managing+the application state, but it also needs to be able to update the state+associated with UI components such as those that come with Brick.++For example, consider an application that uses Brick's built-in text+editor from ``Brick.Widgets.Edit``. The built-in editor is similar to+the main application in that it has three important elements:++* The editor state of type ``Editor t n``: this stores the editor's+ contents, cursor position, etc.+* The editor's drawing function, ``renderEditor``: this is responsible+ for drawing the editor in the UI.+* The editor's event handler, ``handleEditorEvent``: this is responsible+ for updating the editor's contents and cursor position in response to+ key events.++To use the built-in editor, the application must:++* Embed an ``Editor t n`` somewhere in the application state ``s``,+* Render the editor's state at the appropriate place in ``appDraw`` with+ ``renderEditor``, and+* Dispatch events to the editor in the ``appHandleEvent`` with+ ``handleEditorEvent``.++An example application state using an editor might look like this:++.. code:: haskell++ data MyState n = MyState { _editor :: Editor Text n }+ makeLenses ''MyState++This declares the ``MyState`` type with an ``Editor`` contained within+it and uses Template Haskell to generate a lens, ``editor``, to allow us+to easily update the editor state in our event handler.++To dispatch events to the ``editor`` we'd start by writing the+application event handler:++.. code:: haskell++ handleEvent :: BrickEvent n e -> EventM n MyState ()+ handleEvent e = do+ ...++But there's a problem: ``handleEditorEvent``'s type indicates that it+can only run over a state of type ``Editor t n``, but our handler runs+on ``MyState``. Specifically, ``handleEditorEvent`` has this type:++.. code:: haskell++ handleEditorEvent :: BrickEvent n e -> EventM n (Editor t n) ()++This means that to use ``handleEditorEvent``, it must be composed+into the application's event handler, but since the state types ``s``+and ``Editor t n`` do not match, we need a way to compose these event+handlers. There are two ways to do this:++* Use ``Lens.Micro.Mtl.zoom`` from the ``microlens-mtl`` package+ (re-exported by ``Brick.Types`` for convenience). This function is+ required when you want to change the state type to a field embedded in+ your application state using a lens. For example:++.. code:: haskell++ handleEvent :: BrickEvent n e -> EventM n MyState ()+ handleEvent e = do+ zoom editor $ handleEditorEvent e++* Use ``Brick.Types.nestEventM``: this function lets you provide a state+ value and run ``EventM`` using that state. The following+ ``nestEventM`` example is equivalent to the ``zoom`` example above:++.. code:: haskell++ import Lens.Micro (_1)+ import Lens.Micro.Mtl (use, (.=))++ handleEvent :: BrickEvent n e -> EventM n MyState ()+ handleEvent e = do+ editorState <- use editor+ (newEditorState, ()) <- nestEventM editorState $ do+ handleEditorEvent e+ editor .= newEditorState++The ``zoom`` function, together with lenses for your application state's+fields, is by far the best way to manage your state in ``EventM``. As+you can see from the examples above, the ``zoom`` approach avoids a lot+of boilerplate. The ``nestEventM`` approach is provided in cases where+the state that you need to mutate is not easily accessed by ``zoom``.++Finally, if you prefer to avoid the use of lenses, you can always use+the ``MonadState`` API to get, put, and modify your state. Keep in+mind that the ``MonadState`` approach will still require the use of+``nestEventM`` when events scoped to widget states such as ``Editor``+need to be handled.++Using Your Own Event Type+*************************++Since we often need to communicate application-specific events beyond+Vty input events to the event handler, brick supports embedding your+application's custom events in the stream of ``BrickEvent``-s that+your handler will receive. The type of these events is the type ``e``+mentioned in ``BrickEvent n e`` and ``App s e n``.++Note: ordinarily your application will not have its own custom event+type, so you can leave this type unused (e.g. ``App MyState e MyName``)+or just set it to unit (``App MyState () MyName``).++Here's an example of using a custom event type. Suppose that you'd like+to be able to handle counter events in your event handler. First we+define the counter event type:++.. code:: haskell++ data CounterEvent = Counter Int++With this type declaration we can now use counter events in our app by+using the application type ``App s CounterEvent n``. To handle these+events we'll just need to check for ``AppEvent`` values in the event+handler:++.. code:: haskell++ myEvent :: BrickEvent n CounterEvent -> EventM n s ()+ myEvent (AppEvent (Counter i)) = ...++The next step is to actually *generate* our custom events and+inject them into the ``brick`` event stream so they make it to the+event handler. To do that we need to create a ``BChan`` for our+custom events, provide that ``BChan`` to ``brick``, and then send+our events over that channel. Once we've created the channel with+``Brick.BChan.newBChan``, we provide it to ``brick`` with+``customMain`` instead of ``defaultMain``:++.. code:: haskell++ main :: IO ()+ main = do+ eventChan <- Brick.BChan.newBChan 10+ let buildVty = Graphics.Vty.CrossPlatform.mkVty Graphics.Vty.Config.defaultConfig+ initialVty <- buildVty+ finalState <- customMain initialVty buildVty+ (Just eventChan) app initialState+ -- Use finalState and exit++The ``customMain`` function lets us have control over how the ``vty``+library is initialized *and* how ``brick`` gets custom events to give to+our event handler. ``customMain`` is the entry point into ``brick`` when+you need to use your own event type as shown here. In this example we're+using ``mkVty`` provided by the ``vty-crossplatform`` package, which+provides build-time support for both Unix and Windows. If you prefer,+you can use either the ``vty-unix`` package or the ``vty-windows``+package directly instead if you only want to support one platform.++With all of this in place, sending our custom events to the event+handler is straightforward:++.. code:: haskell++ counterThread :: Brick.BChan.BChan CounterEvent -> IO ()+ counterThread chan = do+ Brick.BChan.writeBChan chan $ Counter 1++Bounded Channels+****************++A ``BChan``, or *bounded channel*, can hold a limited number of+items before attempts to write new items will block. In the call to+``newBChan`` above, the created channel has a capacity of 10 items.+Use of a bounded channel ensures that if the program cannot process+events quickly enough then there is a limit to how much memory will+be used to store unprocessed events. Thus the chosen capacity should+be large enough to buffer occasional spikes in event handling latency+without inadvertently blocking custom event producers. Each application+will have its own performance characteristics that determine the best+bound for the event channel. In general, consider the performance of+your event handler when choosing the channel capacity and design event+producers so that they can block if the channel is full.++``appStartEvent``: Starting up+------------------------------++When an application starts, it may be desirable to perform some of+the duties typically only possible when an event has arrived, such as+setting up initial scrolling viewport state. Since such actions can only+be performed in ``EventM`` and since we do not want to wait until the+first event arrives to do this work in ``appHandleEvent``, the ``App``+type provides ``appStartEvent`` function for this purpose:++.. code:: haskell++ appStartEvent :: EventM n s ()++This function is a handler action to run on the initial application+state. This function is invoked once and only once, at application+startup. This might be a place to make initial viewport scroll requests+or make changes to the Vty environment. You will probably just want+to use ``return ()`` as the implementation of this function for most+applications.++``appChooseCursor``: Placing the Cursor+---------------------------------------++The rendering process for a ``Widget`` may return information about+where that widget would like to place the cursor. For example, a text+editor will need to report a cursor position. However, since a+``Widget`` may be a composite of many such cursor-placing widgets, we+have to have a way of choosing which of the reported cursor positions,+if any, is the one we actually want to honor.++To decide which cursor placement to use, or to decide not to show one at+all, we set the ``App`` type's ``appChooseCursor`` function:++.. code:: haskell++ appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)++The event loop renders the interface and collects the+``Brick.Types.CursorLocation`` values produced by the rendering process+and passes those, along with the current application state, to this+function. Using your application state (to track which text input box+is "focused," say) you can decide which of the locations to return or+return ``Nothing`` if you do not want to show a cursor.++Many widgets in the rendering process can request cursor placements, but+it is up to our application to determine which one (if any) should be+used. Since we can only show at most a single cursor in the terminal,+we need to decide which location to show. One way is by looking at the+resource name contained in the ``cursorLocationName`` field. The name+value associated with a cursor location will be the name used to request+the cursor position with ``Brick.Widgets.Core.showCursor``.++``Brick.Main`` provides various convenience functions to make cursor+selection easy in common cases:++* ``neverShowCursor``: never show any cursor.+* ``showFirstCursor``: always show the first cursor request given; good+ for applications with only one cursor-placing widget.+* ``showCursorNamed``: show the cursor with the specified resource name+ or show no cursor if the name was not associated with any requested+ cursor position.++For example, this widget requests a cursor placement on the first+"``o``" in "``foo``" associated with the cursor name ``CustomName``:++.. code:: haskell++ data MyName = CustomName++ let w = showCursor CustomName (Brick.Types.Location (1, 0))+ (Brick.Widgets.Core.str "foobar")++The event handler for this application would use ``MyName`` as its+resource name type ``n`` and would be able to pattern-match on+``CustomName`` to match cursor requests when this widget is rendered:++.. code:: haskell++ myApp =+ App { ...+ , appChooseCursor = \_ -> showCursorNamed CustomName+ }++See the next section for more information on using names.++Resource Names+--------------++We saw above in `appChooseCursor: Placing the Cursor`_ that resource+names are used to describe cursor locations. Resource names are also+used to name other kinds of resources:++* viewports (see `Viewports`_)+* rendering extents (see `Extents`_)+* mouse events (see `Mouse Support`_)++Assigning names to these resource types allows us to distinguish between+events based on the part of the interface to which an event is related.++Your application must provide some type of name. For simple applications+that don't make use of resource names, you may use ``()``. But if your+application has more than one named resource, you *must* provide a type+capable of assigning a unique name to every resource that needs one.++A Note of Caution+*****************++Resource names can be assigned to any of the resource types mentioned+above, but some resource types--viewports, extents, the render cache,+and cursor locations--form separate resource namespaces. So, for+example, the same name can be assigned to both a viewport and an extent,+since the ``brick`` API provides access to viewports and extents using+separate APIs and data structures. However, if the same name is used for+two resources of the same kind, it is undefined *which* of those you'll+be getting access to when you go to use one of those resources in your+event handler.++For example, if the same name is assigned to two viewports:++.. code:: haskell++ data Name = Viewport1++ ui :: Widget Name+ ui = (viewport Viewport1 Vertical $ str "Foo") <+>+ (viewport Viewport1 Vertical $ str "Bar") <+>++then in ``EventM`` when we attempt to scroll the viewport ``Viewport1``+we don't know which of the two uses of ``Viewport1`` will be affected:++.. code:: haskell++ let vp = viewportScroll Viewport1+ vScrollBy vp 1++The solution is to ensure that for a given resource type (in this case+viewport), a unique name is assigned in each use.++.. code:: haskell++ data Name = Viewport1 | Viewport2++ ui :: Widget Name+ ui = (viewport Viewport1 Vertical $ str "Foo") <+>+ (viewport Viewport2 Vertical $ str "Bar") <+>++``appAttrMap``: Managing Attributes+-----------------------------------++In ``brick`` we use an *attribute map* to assign attributes to elements+of the interface. Rather than specifying specific attributes when+drawing a widget (e.g. red-on-black text) we specify an *attribute name*+that is an abstract name for the kind of thing we are drawing, e.g.+"keyword" or "e-mail address." We then provide an attribute map which+maps those attribute names to actual attributes. This approach lets us:++* Change the attributes at runtime, letting the user change the+ attributes of any element of the application arbitrarily without+ forcing anyone to build special machinery to make this configurable;+* Write routines to load saved attribute maps from disk;+* Provide modular attribute behavior for third-party components, where+ we would not want to have to recompile third-party code just to change+ attributes, and where we would not want to have to pass in attribute+ arguments to third-party drawing functions.++This lets us put the attribute mapping for an entire app, regardless of+use of third-party widgets, in one place.++To create a map we use ``Brick.AttrMap.attrMap``, e.g.,++.. code:: haskell++ App { ...+ , appAttrMap = const $ attrMap Graphics.Vty.defAttr [(someAttrName, fg blue)]+ }++To use an attribute map, we specify the ``App`` field ``appAttrMap`` as+the function to return the current attribute map each time rendering+occurs. This function takes the current application state, so you may+choose to store the attribute map in your application state. You may+also choose not to bother with that and to just set ``appAttrMap = const+someMap``.++To draw a widget using an attribute name in the map, use+``Brick.Widgets.Core.withAttr``. For example, this draws a string with a+``blue`` background:++.. code:: haskell++ let w = withAttr blueBg $ str "foobar"+ blueBg = attrName "blueBg"+ myMap = attrMap defAttr [ (blueBg, Brick.Util.bg Graphics.Vty.blue)+ ]++For complete details on how attribute maps and attribute names work, see+the Haddock documentation for the ``Brick.AttrMap`` module. See also+`How Attributes Work`_.++How Widgets and Rendering Work+==============================++When ``brick`` renders a ``Widget``, the widget's rendering routine is+evaluated to produce a ``vty`` ``Image`` of the widget. The widget's+rendering routine runs with some information called the *rendering+context* that contains:++* The size of the area in which to draw things+* The name of the current attribute to use to draw things+* The map of attributes to use to look up attribute names+* The active border style to use when drawing borders++Available Rendering Area+------------------------++The most important element in the rendering context is the rendering+area: This part of the context tells the widget being drawn how many+rows and columns are available for it to consume. When rendering begins,+the widget being rendered (i.e. a layer returned by an ``appDraw``+function) gets a rendering context whose rendering area is the size of+the terminal. This size information is used to let widgets take up that+space if they so choose. For example, a string "Hello, world!" will+always take up one row and 13 columns, but the string "Hello, world!"+*centered* will always take up one row and *all available columns*.++How widgets use space when rendered is described in two pieces of+information in each ``Widget``: the widget's horizontal and vertical+growth policies. These fields have type ``Brick.Types.Size`` and can+have the values ``Fixed`` and ``Greedy``. Note that these values are+merely *descriptive hints* about the behavior of the rendering function,+so it's important that they accurately describe the widget's use of+space.++A widget advertising a ``Fixed`` size in a given dimension is a widget+that will always consume the same number of rows or columns no+matter how many it is given. Widgets can advertise different+vertical and horizontal growth policies for example, the+``Brick.Widgets.Center.hCenter`` function centers a widget and is+``Greedy`` horizontally and defers to the widget it centers for vertical+growth behavior.++These size policies govern the box layout algorithm that is at+the heart of every non-trivial drawing specification. When we use+``Brick.Widgets.Core.vBox`` and ``Brick.Widgets.Core.hBox`` to+lay things out (or use their binary synonyms ``<=>`` and ``<+>``,+respectively), the box layout algorithm looks at the growth policies of+the widgets it receives to determine how to allocate the available space+to them.++For example, imagine that the terminal window is currently 10 rows high+and 50 columns wide. We wish to render the following widget:++.. code:: haskell++ let w = (str "Hello," <=> str "World!")++Rendering this to the terminal will result in "Hello," and "World!"+underneath it, with 8 rows unoccupied by anything. But if we wished to+render a vertical border underneath those strings, we would write:++.. code:: haskell++ let w = (str "Hello," <=> str "World!" <=> vBorder)++Rendering this to the terminal will result in "Hello," and "World!"+underneath it, with 8 rows remaining occupied by vertical border+characters ("``|``") one column wide. The vertical border widget is+designed to take up however many rows it was given, but rendering the+box layout algorithm has to be careful about rendering such ``Greedy``+widgets because they won't leave room for anything else. Since the box+widget cannot know the sizes of its sub-widgets until they are rendered,+the ``Fixed`` widgets get rendered and their sizes are used to determine+how much space is left for ``Greedy`` widgets.++When using widgets it is important to understand their horizontal and+vertical space behavior by knowing their ``Size`` values. Those should+be made clear in the Haddock documentation.++The rendering context's specification of available space will also+govern how widgets get cropped, since all widgets are required to render+to an image no larger than the rendering context specifies. If they do,+they will be forcibly cropped.++Limiting Rendering Area+-----------------------++If you'd like to use a ``Greedy`` widget but want to limit how much+space it consumes, you can turn it into a ``Fixed`` widget by using+one of the *limiting combinators*, ``Brick.Widgets.Core.hLimit`` and+``Brick.Widgets.Core.vLimit``. These combinators take widgets and turn+them into widgets with a ``Fixed`` size (in the relevant dimension) and+run their rendering functions in a modified rendering context with a+restricted rendering area.++For example, the following will center a string in 30 columns, leaving+room for something to be placed next to it as the terminal width+changes:++.. code:: haskell++ let w = hLimit 30 $ hCenter $ str "Hello, world!"++The Attribute Map+-----------------++The rendering context contains an attribute map (see `How Attributes+Work`_ and `appAttrMap: Managing Attributes`_) which is used to look up+attribute names from the drawing specification. The map originates from+``Brick.Main.appAttrMap`` and can be manipulated on a per-widget basis+using ``Brick.Widgets.Core.updateAttrMap``.++The Active Border Style+-----------------------++Widgets in the ``Brick.Widgets.Border`` module draw border characters+(horizontal, vertical, and boxes) between and around other widgets. To+ensure that widgets across your application share a consistent visual+style, border widgets consult the rendering context's *active border+style*, a value of type ``Brick.Widgets.Border.Style``, to get the+characters used to draw borders.++The default border style is ``Brick.Widgets.Border.Style.unicode``. To+change border styles, use the ``Brick.Widgets.Core.withBorderStyle``+combinator to wrap a widget and change the border style it uses when+rendering. For example, this will use the ``ascii`` border style instead+of ``unicode``:++.. code:: haskell++ let w = withBorderStyle Brick.Widgets.Border.Style.ascii $+ Brick.Widgets.Border.border $ str "Hello, world!"++By default, borders in adjacent widgets do not connect to each other.+This can lead to visual oddities, for example, when horizontal borders+are drawn next to vertical borders by leaving a small gap like this:++.. code:: text++ │─++You can request that adjacent borders connect to each other with+``Brick.Widgets.Core.joinBorders``. Two borders drawn with the+same attribute and border style, and both under the influence of+``joinBorders``, will produce a border like this instead:++.. code:: text++ ├─++See `Joining Borders`_ for further details.++How Attributes Work+===================++In addition to letting us map names to attributes, attribute maps+provide hierarchical attribute inheritance: a more specific attribute+derives any properties (e.g. background color) that it does not specify+from more general attributes in hierarchical relationship to it, letting+us customize only the parts of attributes that we want to change without+having to repeat ourselves.++For example, this draws a string with a foreground color of ``white`` on+a background color of ``blue``:++.. code:: haskell++ let w = withAttr specificAttr $ str "foobar"+ generalAttr = attrName "general"+ specificAttr = attrName "general" <> attrName "specific"+ myMap = attrMap defAttr [ (generalAttr, bg blue)+ , (specificAttr, fg white)+ ]++When drawing a widget, Brick keeps track of the current attribute it+is using to draw to the screen. The attribute it tracks is specified+by its *attribute name*, which is a hierarchical name referring to the+attribute in the attribute map. In the example above, the map contains+two attribute names: ``generalAttr`` and ``specificAttr``. Both names+are made up of segments: ``general`` is the first segment for both+names, and ``specific`` is the second segment for ``specificAttr``.+This tells Brick that ``specificAttr`` is a more specialized version+of ``generalAttr``. We'll see below how that affects the resulting+attributes that Brick uses.++When it comes to drawing something on the screen with either of these+attributes, Brick looks up the desired attribute name in the map+and uses the result to draw to the screen. In the example above,+``withAttr`` is used to tell Brick that when drawing ``str "foobar"``,+the attribute ``specificAttr`` should be used. Brick looks that name+up in the attribute map and finds a match: an attribute with a white+foreground color. However, what happens next is important: Brick then+looks up the more general attribute name derived from ``specificAttr``,+which it gets by removing the last segment in the name, ``specific``.+The resulting name, ``general``, is then looked up. The new result is+then *merged* with the initial lookup, yielding an attribute with a+white foreground color and a blue background color. This happens because+the ``specificAttr`` entry did not specify a background color. If it+had, that would have been used instead. In this way, we can create+inheritance relationships between attributes, much the same way CSS+supports inheritance of styles based on rule specificity.++Brick uses Vty's attribute type, ``Attr``, which has three components:+foreground color, background color, and style. These three components+can be independently specified to have an explicit value, and any+component not explicitly specified can default to whatever the terminal+is currently using. Vty styles can be combined together, e.g. underline+and bold, so styles are cumulative.++What if a widget attempts to draw with an attribute name that is not+specified in the map at all? In that case, the attribute map's "default+attribute" is used. In the example above, the default attribute for the+map is Vty's ``defAttr`` value, which means that the terminal's default+colors and style should be used. But that attribute can be customized+as well, and any attribute map lookup results will get merged with the+default attribute for the map. So, for example, if you'd like your+entire application background to be blue unless otherwise specified, you+could create an attribute map as follows:++.. code:: haskell++ let myMap = attrMap (bg blue) [ ... ]++This way, we can avoid repeating the desired background color and all of+the other map entries can just set foreground colors and styles where+needed.++In addition to using the attribute map provided by ``appAttrMap``, the+map and attribute lookup behavior can be customized on a per-widget+basis by using various functions from ``Brick.Widgets.Core``:++* ``updateAttrMap`` -- allows transformations of the attribute map,+* ``forceAttr`` -- forces all attribute lookups to map to the value of+ the specified attribute name,+* ``withDefAttr`` -- changes the default attribute for the attribute map+ to the one with the specified name, and+* ``overrideAttr`` -- creates attribute map lookup synonyms between+ attribute names.++Attribute Themes+================++Brick provides support for customizable attribute themes. This works as+follows:++* The application provides a default theme built in to the program.+* The application customizes the theme by loading theme customizations+ from a user-specified customization file.+* The application can save new customizations to files for later+ re-loading.++Customizations are written in an INI-style file. Here's an example:++.. code:: ini++ [default]+ default.fg = blue+ default.bg = black++ [other]+ someAttribute.fg = red+ someAttribute.style = underline+ otherAttribute.style = [underline, bold]+ otherAttribute.inner.fg = white++In the above example, the theme's *default attribute* -- the one that is+used when no other attributes are used -- is customized. Its foreground+and background colors are set. Then, other attributes specified by+the theme -- ``someAttribute`` and ``otherAttribute`` -- are also+customized. This example shows that styles can be customized, too, and+that a custom style can either be a single style (in this example,+``underline``) or a collection of styles to be applied simultaneously+(in this example, ``underline`` and ``bold``). Lastly, the hierarchical+attribute name ``otherAttribute.inner`` refers to an attribute name+with two components, ``otherAttribute <> inner``, similar to the+``specificAttr`` attribute described in `How Attributes Work`_. Full+documentation for the format of theme customization files can be found+in the module documentation for ``Brick.Themes``.++The above example can be used in a ``brick`` application as follows.+First, the application provides a default theme:++.. code:: haskell++ import Brick.Themes (Theme, newTheme)+ import Brick (attrName)+ import Brick.Util (fg, on)+ import Graphics.Vty (defAttr, white, blue, yellow, magenta)++ defaultTheme :: Theme+ defaultTheme =+ newTheme (white `on` blue)+ [ (attrName "someAttribute", fg yellow)+ , (attrName "otherAttribute", fg magenta)+ ]++Notice that the attributes in the theme have defaults: ``someAttribute``+will default to a yellow foreground color if it is not customized. (And+its background will default to the theme's default background color,+blue, if it not customized either.) Then, the application can customize+the theme with the user's customization file:++.. code:: haskell++ import Brick.Themes (loadCustomizations)++ main :: IO ()+ main = do+ customizedTheme <- loadCustomizations "custom.ini" defaultTheme++Now we have a customized theme based on ``defaultTheme``. The next step+is to build an ``AttrMap`` from the theme:++.. code:: haskell++ import Brick.Themes (themeToAttrMap)++ main :: IO ()+ main = do+ customizedTheme <- loadCustomizations "custom.ini" defaultTheme+ let mapping = themeToAttrMap customizedTheme++The resulting ``AttrMap`` can then be returned by ``appAttrMap``+as described in `How Attributes Work`_ and `appAttrMap: Managing+Attributes`_.++If the theme is further customized at runtime, any changes can be saved+with ``Brick.Themes.saveCustomizations``.++Wide Character Support and the ``TextWidth`` class+==================================================++Brick attempts to support rendering wide characters in all widgets,+and the brick editor supports entering and editing wide characters.+Wide characters are those such as many Asian characters and emoji+that need more than a single terminal column to be displayed.++Unfortunately, there is not a fully correct solution to determining+the character width that the user's terminal will use for a given+character. The current recommendation is to avoid use of wide characters+due to these issues. If you still must use them, you can read `vty`_'s+documentation for options that will affect character width calculations.++As a result of supporting wide characters, it is important to know that+computing the length of a string to determine its screen width will+*only* work for single-column characters. So, for example, if you want+to support wide characters in your application, this will not work:++.. code:: haskell++ let width = Data.Text.length t++If the string contains any wide characters, their widths will not be+counted properly. In order to get this right, use the ``TextWidth`` type+class to compute the width:++.. code:: haskell++ let width = Brick.Widgets.Core.textWidth t++The ``TextWidth`` type class uses Vty's character width routine to+compute the width by looking up the string's characters in a Unicode+width table. If you need to compute the width of a single character, use+``Graphics.Text.wcwidth``.++Extents+=======++When an application needs to know where a particular widget was drawn+by the renderer, the application can request that the renderer record+the *extent* of the widget--its upper-left corner and size--and provide+access to it in an event handler. Extents are represented using Brick's+``Brick.Types.Extent`` type. In the following example, the application+needs to know where the bordered box containing "Foo" is rendered:++.. code:: haskell++ ui = center $ border $ str "Foo"++We don't want to have to care about the particulars of the layout to+find out where the bordered box got placed during rendering. To get this+information we request that the extent of the box be reported to us by+the renderer using a resource name:++.. code:: haskell++ data Name = FooBox++ ui = center $+ reportExtent FooBox $+ border $ str "Foo"++Now, whenever the ``ui`` is rendered, the extent of the bordered box+containing "Foo" will be recorded. We can then look it up in event+handlers in ``EventM``:++.. code:: haskell++ mExtent <- Brick.Main.lookupExtent FooBox+ case mExtent of+ Nothing -> ...+ Just (Extent _ upperLeft (width, height)) -> ...++Paste Support+=============++Some terminal emulators support "bracketed paste" mode. This feature+enables OS-level paste operations to send the pasted content as a+single chunk of data and bypass the usual input processing that the+application does. This enables more secure handling of pasted data since+the application can detect that a paste occurred and avoid processing+the pasted data as ordinary keyboard input. For more information, see+`bracketed paste mode`_.++The Vty library used by brick provides support for bracketed pastes, but+this mode must be enabled. To enable paste mode, we need to get access+to the Vty library handle in ``EventM`` (in e.g. ``appHandleEvent``):++.. code:: haskell++ import Control.Monad (when)+ import qualified Graphics.Vty as V++ do+ vty <- Brick.Main.getVtyHandle+ let output = V.outputIface vty+ when (V.supportsMode output V.BracketedPaste) $+ liftIO $ V.setMode output V.BracketedPaste True++Once enabled, paste mode will generate Vty ``EvPaste`` events. These+events will give you the entire pasted content as a ``ByteString`` which+you must decode yourself if, for example, you expect it to contain UTF-8+text data.++Mouse Support+=============++Some terminal emulators support mouse interaction. The Vty library used+by brick provides these low-level events if mouse mode has been enabled.+To enable mouse mode, we need to get access to the Vty library handle in+``EventM``:++.. code:: haskell++ do+ vty <- Brick.Main.getVtyHandle+ let output = outputIface vty+ when (supportsMode output Mouse) $+ liftIO $ setMode output Mouse True++Bear in mind that some terminals do not support mouse interaction, so+use Vty's ``getModeStatus`` to find out whether your terminal will+provide mouse events.++Also bear in mind that terminal users will usually expect to be able+to interact with your application entirely without a mouse, so if you+do choose to enable mouse interaction, consider using it to improve+existing interactions rather than provide new functionality that cannot+already be managed with a keyboard.++Low-level Mouse Events+----------------------++Once mouse events have been enabled, Vty will generate ``EvMouseDown``+and ``EvMouseUp`` events containing the mouse button clicked, the+location in the terminal, and any modifier keys pressed.++.. code:: haskell++ handleEvent (VtyEvent (EvMouseDown col row button mods) = ...++Brick Mouse Events+------------------++Although these events may be adequate for your needs, ``brick`` provides+a higher-level mouse event interface that ties into the drawing+language. The disadvantage to the low-level interface described above is+that you still need to determine *what* was clicked, i.e., the part of+the interface that was under the mouse cursor. There are two ways to do+this with ``brick``: with *click reporting* and *extent checking*.++Click reporting+***************++The *click reporting* approach is the most high-level approach offered+by ``brick`` and the one that we recommend you use. In this approach,+we use ``Brick.Widgets.Core.clickable`` when drawing the interface to+request that a given widget generate ``MouseDown`` and ``MouseUp``+events when it is clicked.++.. code:: haskell++ data Name = MyButton++ ui :: Widget Name+ ui = center $+ clickable MyButton $+ border $+ str "Click me"++ handleEvent (MouseDown MyButton button modifiers coords) = ...+ handleEvent (MouseUp MyButton button coords) = ...++This approach enables event handlers to use pattern matching to check+for mouse clicks on specific regions; this uses `Extent checking`_+under the hood but makes it possible to denote which widgets are+clickable in the interface description. The event's click coordinates+are local to the widget being clicked. In the above example, a click+on the upper-left corner of the border would result in coordinates of+``(0,0)``.++Extent checking+***************++The *extent checking* approach entails requesting extents (see+`Extents`_) for parts of your interface, then checking the Vty mouse+click event's coordinates against one or more extents. This approach+is slightly lower-level than the direct mouse click reporting approach+above but is provided in case you need more control over how mouse+clicks are dealt with.++The most direct way to do this is to check a specific extent:++.. code:: haskell++ handleEvent (VtyEvent (EvMouseDown col row _ _)) = do+ mExtent <- lookupExtent SomeExtent+ case mExtent of+ Nothing -> return ()+ Just e -> do+ if Brick.Main.clickedExtent (col, row) e+ then ...+ else ...++This approach works well enough if you know which extent you're+interested in checking, but what if there are many extents and you+want to know which one was clicked? And what if those extents are in+different layers? The next approach is to find all clicked extents:++.. code:: haskell++ handleEvent (VtyEvent (EvMouseDown col row _ _)) = do+ extents <- Brick.Main.findClickedExtents (col, row)+ -- Then check to see if a specific extent is in the list, or just+ -- take the first one in the list.++This approach finds all clicked extents and returns them in a list with+the following properties:++* For extents ``A`` and ``B``, if ``A``'s layer is higher than ``B``'s+ layer, ``A`` comes before ``B`` in the list.+* For extents ``A`` and ``B``, if ``A`` and ``B`` are in the same layer+ and ``A`` is contained within ``B``, ``A`` comes before ``B`` in the+ list.++As a result, the extents are ordered in a natural way, starting with the+most specific extents and proceeding to the most general.++Viewports+=========++A *viewport* is a scrollable window onto a widget. Viewports have a+*scrolling direction* of type ``Brick.Types.ViewportType`` which can be+one of:++* ``Horizontal``: the viewport can only scroll horizontally.+* ``Vertical``: the viewport can only scroll vertically.+* ``Both``: the viewport can scroll both horizontally and vertically.++The ``Brick.Widgets.Core.viewport`` combinator takes another widget+and embeds it in a named viewport. We name the viewport so that we can+keep track of its scrolling state in the renderer, and so that you can+make scrolling requests. The viewport's name is its handle for these+operations (see `Scrolling Viewports in Event Handlers`_ and `Resource+Names`_). **The viewport name must be unique across your application.**++For example, the following puts a string in a horizontally-scrollable+viewport:++.. code:: haskell++ -- Assuming that App uses 'Name' for its resource names:+ data Name = Viewport1+ let w = viewport Viewport1 Horizontal $ str "Hello, world!"++A ``viewport`` specification means that the widget in the viewport will+be placed in a viewport window that is ``Greedy`` in both directions+(see `Available Rendering Area`_). This is suitable if we want the+viewport size to be the size of the entire terminal window, but if+we want to limit the size of the viewport, we might use limiting+combinators (see `Limiting Rendering Area`_):++.. code:: haskell++ let w = hLimit 5 $+ vLimit 1 $+ viewport Viewport1 Horizontal $ str "Hello, world!"++Now the example produces a scrollable window one row high and five+columns wide initially showing "Hello". The next two sections discuss+the two ways in which this viewport can be scrolled.++Scrolling Viewports in Event Handlers+-------------------------------------++The most direct way to scroll a viewport is to make *scrolling requests*+in the ``EventM`` event-handling monad. Scrolling requests ask the+renderer to update the state of a viewport the next time the user+interface is rendered. Those state updates will be made with respect+to the *previous* viewport state, i.e., the state of the viewports as+of the end of the most recent rendering. This approach is the best+approach to use to scroll widgets that have no notion of a cursor.+For cursor-based scrolling, see `Scrolling Viewports With Visibility+Requests`_.++To make scrolling requests, we first create a+``Brick.Main.ViewportScroll`` from a viewport name with+``Brick.Main.viewportScroll``:++.. code:: haskell++ -- Assuming that App uses 'Name' for its resource names:+ data Name = Viewport1+ let vp = viewportScroll Viewport1++The ``ViewportScroll`` record type contains a number of scrolling+functions for making scrolling requests:++.. code:: haskell++ hScrollPage :: Direction -> EventM n s ()+ hScrollBy :: Int -> EventM n s ()+ hScrollToBeginning :: EventM n s ()+ hScrollToEnd :: EventM n s ()+ vScrollPage :: Direction -> EventM n s ()+ vScrollBy :: Int -> EventM n s ()+ vScrollToBeginning :: EventM n s ()+ vScrollToEnd :: EventM n s ()++In each case the scrolling function scrolls the viewport by the+specified amount in the specified direction; functions prefixed with+``h`` scroll horizontally and functions prefixed with ``v`` scroll+vertically.++Scrolling operations do nothing when they don't make sense for the+specified viewport; scrolling a ``Vertical`` viewport horizontally is a+no-op, for example.++Using ``viewportScroll`` we can write an event handler that scrolls the+``Viewport1`` viewport one column to the right:++.. code:: haskell++ myHandler :: e -> EventM n s ()+ myHandler e = do+ let vp = viewportScroll Viewport1+ hScrollBy vp 1++Scrolling Viewports With Visibility Requests+--------------------------------------------++When we need to scroll widgets only when a cursor in the viewport+leaves the viewport's bounds, we need to use *visibility requests*. A+visibility request is a hint to the renderer that some element of a+widget inside a viewport should be made visible, i.e., that the viewport+should be scrolled to bring the requested element into view.++To use a visibility request to make a widget in a viewport visible, we+simply wrap it with ``visible``:++.. code:: haskell++ -- Assuming that App uses 'Name' for its resource names:+ data Name = Viewport1+ let w = viewport Viewport1 Horizontal $+ (visible $ str "Hello,") <+> (str " world!")++This example requests that the ``Viewport1`` viewport be scrolled so+that "Hello," is visible. We could extend this example with a value+in the application state indicating which word in our string should+be visible and then use that to change which string gets wrapped with+``visible``; this is the basis of cursor-based scrolling.++Note that a visibility request does not change the state of a viewport+*if the requested widget is already visible*! This important detail is+what makes visibility requests so powerful, because they can be used to+capture various cursor-based scenarios:++* The ``Brick.Widgets.Edit`` widget uses a visibility request to make its+ 1x1 cursor position visible, thus making the text editing widget fully+ scrollable *while being entirely scrolling-unaware*.+* The ``Brick.Widgets.List`` widget uses a visibility request to make+ its selected item visible regardless of its size, which makes+ the list widget scrolling-unaware.++Showing Scroll Bars on Viewports+--------------------------------++Brick supports drawing both vertical and horizontal scroll bars on+viewports. To enable scroll bars, wrap your call to ``viewport`` with+a call to ``withVScrollBars`` and/or ``withHScrollBars``. If you don't+like the appearance of the resulting scroll bars, you can customize+how they are drawn by making your own ``ScrollbarRenderer`` and using+``withVScrollBarRenderer`` and/or ``withHScrollBarRenderer``. Note that+when you enable scrollbars, the content of your viewport will lose one+column of available space if vertical scroll bars are enabled and one+row of available space if horizontal scroll bars are enabled.++Scroll bars can also be configured to draw "handles" with+``withHScrollBarHandles`` and ``withVScrollBarHandles``.++Lastly, scroll bars can be configured to report mouse events on+each scroll bar element. To enable mouse click reporting, use+``withClickableHScrollBars`` and ``withClickableVScrollBars``.++For a demonstration of the scroll bar API in action, see the+``ViewportScrollbarsDemo.hs`` demonstration program.++Viewport Restrictions+---------------------++Viewports impose one restriction: a viewport that is scrollable in+some direction can only embed a widget that has a ``Fixed`` size in+that direction. This extends to ``Both`` type viewports: they can only+embed widgets that are ``Fixed`` in both directions. This restriction+is because when viewports embed a widget, they relax the rendering area+constraint in the rendering context, but doing so to a large enough+number for ``Greedy`` widgets would result in a widget that is too big+and not scrollable in a useful way.++Violating this restriction will result in a runtime exception.++Input Forms+===========++While it's possible to construct interfaces with editors and other+interactive inputs manually, this process is somewhat tedious: all of+the event dispatching has to be written by hand, a focus ring or other+construct needs to be managed, and most of the rendering code needs to+be written. Furthermore, this process makes it difficult to follow some+common patterns:++* We typically want to validate the user's input, and only collect it+ once it has been validated.+* We typically want to notify the user when a particular field's+ contents are invalid.+* It is often helpful to be able to create a new data type to represent+ the fields in an input interface, and use it to initialize the input+ elements and later collect the (validated) results.+* A lot of the rendering and event-handling work to be done is+ repetitive.++The ``Brick.Forms`` module provides a high-level API to automate all of+the above work in a type-safe manner.++A Form Example+--------------++Let's consider an example data type that we'd want to use as the+basis for an input interface. This example comes directly from the+``FormDemo.hs`` demonstration program.++.. code:: haskell++ data UserInfo =+ FormState { _name :: T.Text+ , _age :: Int+ , _address :: T.Text+ , _ridesBike :: Bool+ , _handed :: Handedness+ , _password :: T.Text+ } deriving (Show)++ data Handedness = LeftHanded+ | RightHanded+ | Ambidextrous+ deriving (Show, Eq)++Suppose we want to build an input form for the above data. We might want+to use an editor to allow the user to enter a name and an age. We'll+need to ensure that the user's input for age is a valid integer. For+``_ridesBike`` we might want a checkbox-style input, and for ``_handed``+we might want a radio button input. For ``_password``, we'd definitely+like a password input box that conceals the input.++If we were to build an interface for this data manually, we'd need to+deal with converting the data above to the right types for inputs. For+example, for ``_age`` we'd need to convert an initial age value to+``Text``, put it in an editor with ``Brick.Widgets.Edit.editor``, and+then at a later time, parse the value and reconstruct an age from the+editor's contents. We'd also need to tell the user if the age value was+invalid.++Brick's ``Forms`` API provides input field types for all of the above+use cases. Here's the form that we can use to allow the user to edit a+``UserInfo`` value:++.. code:: haskell++ mkForm :: UserInfo -> Form UserInfo e Name+ mkForm =+ newForm [ editTextField name NameField (Just 1)+ , editTextField address AddressField (Just 3)+ , editShowableField age AgeField+ , editPasswordField password PasswordField+ , radioField handed [ (LeftHanded, LeftHandField, "Left")+ , (RightHanded, RightHandField, "Right")+ , (Ambidextrous, AmbiField, "Both")+ ]+ , checkboxField ridesBike BikeField "Do you ride a bicycle?"+ ]++A form is represented using a ``Form s e n`` value and is parameterized+with some types:++* ``s`` - the type of *form state* managed by the form (in this case+ ``UserInfo``)+* ``e`` - the event type of the application (must match the event type+ used with ``App``)+* ``n`` - the resource name type of the application (must match the+ resource name type used with ``App``)++First of all, the above code assumes we've derived lenses for+``UserInfo`` using ``Lens.Micro.TH.makeLenses``. Once we've done+that, each field that we specify in the form must provide a lens into+``UserInfo`` so that we can declare the particular field of ``UserInfo``+that will be edited by the field. For example, to edit the ``_name``+field we use the ``name`` lens to create a text field editor with+``editTextField``. All of the field constructors above are provided by+``Brick.Forms``.++Each form field also needs a resource name (see `Resource Names`_). The+resource names are assigned to the individual form inputs so the form+can automatically track input focus and handle mouse click events.++The form carries with it the value of ``UserInfo`` that reflects the+contents of the form. Whenever an input field in the form handles an+event, its contents are validated and rewritten to the form state (in+this case, a ``UserInfo`` record).++The ``mkForm`` function takes a ``UserInfo`` value, which is really+just an argument to ``newForm``. This ``UserInfo`` value will be used+to initialize all of the form fields. Each form field will use the lens+provided to extract the initial value from the ``UserInfo`` record,+convert it into an appropriate state type for the field in question, and+later validate that state and convert it back into the appropriate type+for storage in ``UserInfo``.++The form value itself -- of type ``Form`` -- must be stored in your+application state. You should only ever call ``newForm`` when you need+to initialize a totally new form. Once initialized, the form needs to be+kept around and updated by event handlers in order to work.++For example, if the initial ``UserInfo`` value's ``_age`` field has the+value ``0``, the ``editShowableField`` will call ``show`` on ``0``,+convert that to ``Text``, and initialize the editor for ``_age`` with+the text string ``"0"``. Later, if the user enters more text -- changing+the editor contents to ``"10"``, say -- the ``Read`` instance for+``Int`` (the type of ``_age``) will be used to parse ``"10"``. The+successfully-parsed value ``10`` will then be written to the ``_age``+field of the form's ``UserInfo`` state using the ``age`` lens. The use+of ``Show`` and ``Read`` here is a feature of the field type we have+chosen for ``_age``, ``editShowableField``.++For other field types we may have other needs. For instance,+``Handedness`` is a data type representing all the possible choices+we want to provide for a user's handedness. We wouldn't want the user+to have to type in a text string for this option. A more appropriate+input interface is a list of radio buttons to choose from amongst+the available options. For that we have ``radioField``. This field+constructor takes a list of all of the available options, and updates+the form state with the value of the currently-selected option.++Rendering Forms+---------------++Rendering forms is done easily using the ``Brick.Forms.renderForm``+function. However, as written above, the form will not look especially+nice. We'll see a few text editors followed by some radio buttons and a+check box. But we'll need to customize the output a bit to make the form+easier to use. For that, we have the ``Brick.Forms.@@=`` operator. This+operator lets us provide a function to augment the ``Widget`` generated+by the field's rendering function so we can do things like add labels,+control layout, or change attributes:++.. code:: haskell++ (str "Name: " <+>) @@=+ editTextField name NameField (Just 1)++Now when we invoke ``renderForm`` on a form using the above example,+we'll see a ``"Name:"`` label to the left of the editor field for+the ``_name`` field of ``UserInfo``.++Brick provides this interface to controlling per-field rendering because+many form fields either won't have labels or will have different layout+requirements, so an alternative API such as building the label into the+field API doesn't always make sense.++Brick defaults to rendering individual fields' inputs, and the entire+form, in a vertical box using ``vBox``. Use ``setFormConcat`` and+``setFieldConcat`` to change this behavior to, e.g., ``hBox``.++Form Attributes+---------------++The ``Brick.Forms`` module uses and exports two attribute names (see+`How Attributes Work`_):++* ``focusedFormInputAttr`` - this attribute is used to render the form+ field that has the focus.+* ``invalidFormInputAttr`` - this attribute is used to render any form+ field that has user input that has invalid validation.++Your application should set both of these. Some good mappings in the+attribute map are:++* ``focusedFormInputAttr`` - ``black `on` yellow``+* ``invalidFormInputAttr`` - ``white `on` red``++Handling Form Events+--------------------++Handling form events is easy: we just use ``zoom`` to call+``Brick.Forms.handleFormEvent`` with the ``BrickEvent`` and a lens+to access the ``Form`` in the application state. This automatically+dispatches input events to the currently-focused input field, and it+also manages focus changes with ``Tab`` and ``Shift-Tab`` keybindings.+(For details on all of its behaviors, see the Haddock documentation for+``handleFormEvent``.) It's still up to the application to decide when+events should go to the form in the first place.++Since the form field handlers take ``BrickEvent`` values, that means+that custom fields could even handle application-specific events (of the+type ``e`` above).++Once the application has decided that the user should be done with the+form editing session, the current state of the form can be obtained+with ``Brick.Forms.formState``. In the example above, this would+return a ``UserInfo`` record containing the values for each field in+the form *as of the last time it was valid input*. This means that+the user might have provided invalid input to a form field that is+not reflected in the form state due to failing validation.++Since the ``formState`` is always a valid set of values, it might+be surprising to the user if the values used do not match the last+values they saw on the screen; the ``Brick.Forms.allFieldsValid``+can be used to determine if the last visual state of the form had+any invalid entries and doesn't match the value of ``formState``. A+list of any fields which had invalid values can be retrieved with the+``Brick.Forms.invalidFields`` function.++While each form field type provides a validator function to validate+its current user input value, that function is pure. As a result it's+not suitable for doing validation that requires I/O such as searching+a database or making network requests. If your application requires+that kind of validation, you can use the ``Brick.Forms.setFieldValid``+function to set the validation state of any form field as you see+fit. The validation state set by that function will be considered by+``allFieldsValid`` and ``invalidFields``. See ``FormDemo.hs`` for an+example of this API.++Note that if mouse events are enabled in your application (see `Mouse+Support`_), all built-in form fields will respond to mouse interaction.+Radio buttons and check boxes change selection on mouse clicks and+editors change cursor position on mouse clicks.++Writing Custom Form Field Types+-------------------------------++If the built-in form field types don't meet your needs, ``Brick.Forms``+exposes all of the data types needed to implement your own field types.+For more details on how to do this, see the Haddock documentation for+the ``FormFieldState`` and ``FormField`` data types along with the+implementations of the built-in form field types.++Customizable Keybindings+========================++Brick applications typically start out by explicitly checking incoming+events for specific keys in ``appHandleEvent``. While this works well+enough, it results in *tight coupling* between the input key events and+the event handlers that get run. As applications evolve, it becomes+important to decouple the input key events and their handlers to allow+the input keys to be customized by the user. That's where Brick's+customizable keybindings API comes in.++The customizable keybindings API provides:++* ``Brick.Keybindings.Parse``: parsing and loading user-provided+ keybinding configuration files,+* ``Brick.Keybindings.Pretty``: pretty-printing keybindings and+ generating keybinding help text in ``Widget``, plain text, and+ Markdown formats so you can provide help to users both within the+ program and outside of it,+* ``Brick.Keybindings.KeyEvents``: specifying the application's abstract+ key events and their configuration names,+* ``Brick.Keybindings.KeyConfig``: bundling default and customized+ keybindings for each abstract event into a structure for use by the+ dispatcher, and+* ``Brick.Keybindings.KeyDispatcher``: specifying handlers and+ dispatching incoming key events to them.++This section of the User Guide describes the API at a high level,+but Brick also provides a complete working example of the custom+keybinding API in ``programs/CustomKeybindingDemo.hs`` and+provides detailed documentation on how to use the API, including a+step-by-step process for using it, in the module documentation for+``Brick.Keybindings.KeyDispatcher``.++The following table compares Brick application design decisions and+runtime behaviors in a typical application to those of an application+that uses the customizable keybindings API:+++---------------------+------------------------+-------------------------++| **Approach** | **Before runtime** | **At runtime** |++---------------------+------------------------+-------------------------++| Typical application | The application author | #. An input event |+| (no custom | decides which keys will| arrives when the user|+| keybindings) | trigger application | presses a key. |+| | behaviors. The event | #. The event handler |+| | handler is written to | pattern-matches on |+| | pattern-match on | the input event to |+| | specific keys. | check for a match and|+| | | then runs the |+| | | corresponding |+| | | handler. |++---------------------+------------------------+-------------------------++| Application with | The application author | #. A Vty input event |+| custom keybindings | specifies the possible | arrives when the user|+| API integrated | *abstract events* that | presses a key. |+| | correspond to the | #. The input event is |+| | application's | provided to |+| | behaviors. The events | ``appHandleEvent``. |+| | are given default | #. ``appHandleEvent`` |+| | keybindings. The | passes the event on |+| | application provides | to a |+| | event handlers for the | ``KeyDispatcher``. |+| | abstract events, not | #. The key dispatcher |+| | specific keys. If | checks to see whether|+| | desired, the | the input key event |+| | application can load | maps to an abstract |+| | user-defined custom | event. |+| | keybindings from an INI| #. If the dispatcher |+| | file at startup to | finds a match, the |+| | override the | corresponding |+| | application's defaults.| abstract event's key |+| | | 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 handling context.++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 abstract 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+===============++Brick supports a feature called "joinable borders" which means that+borders drawn in adjacent widgets can be configured to automatically+"join" with each other using the appropriate intersection characters.+This feature is helpful for creating seamless connected borders without+the need for manual calculations to determine where to draw intersection+characters.++Under normal circumstances, widgets are self-contained in that their+renderings do not interact with the appearance of adjacent widgets. This+is unfortunate for borders: one often wants to draw a T-shaped character+at the intersection of a vertical and horizontal border, for example.+To facilitate automatically adding such characters, ``brick`` offers+some border-specific capabilities for widgets to re-render themselves+as information about neighboring widgets becomes available during the+rendering process.++Border-joining works by iteratively *redrawing* the edges of widgets as+those edges come into contact with other widgets during rendering. If+the adjacent edge locations of two widgets both use joinable borders,+the Brick will re-draw one of the characters so that it connects+seamlessly with the adjacent border.++How Joining Works+-----------------++When a widget is rendered, it can report supplementary information+about each position on its edges. Each position has four notional line+segments extending from its center, arranged like this:++.. code:: text++ top+ |+ |+ left ----+---- right+ |+ |+ bottom++These segments can independently be *drawn*, *accepting*, and+*offering*, as captured in the ``Brick.Types.BorderSegment`` type:++.. code:: haskell++ data BorderSegment = BorderSegment+ { bsAccept :: Bool+ , bsOffer :: Bool+ , bsDraw :: Bool+ }++If no information is reported for a position, it assumed that it is+not drawn, not accepting, and not offering -- and so it will never+be rewritten. This situation is the ordinary situation where an edge+location is not a border at all, or is a border that we don't want to+join to other borders.++Line segments that are *drawn* are used for deciding which part of the+``BorderStyle`` to use if this position needs to be updated. (See also+`The Active Border Style`_.) For example, suppose a position needs to+be redrawn, and already has the left and bottom segments drawn; then it+will replace the current character with the upper-right corner drawing+character ``bsCornerTR`` from its border style.++The *accepting* and *offering* properties are used to perform a small+handshake between neighboring widgets; when the handshake is successful,+one segment will transition to being drawn. For example, suppose a+horizontal and vertical border widget are drawn next to each other:++.. code:: text++ top+ (offering) top+ |+ |+ left + right left ----+---- right+ | (offering) (offering)+ |+ bottom bottom+ (offering)++These borders are accepting in all directions, drawn in the directions+signified by visible lines, and offering in the directions written.+Since the horizontal border on the right is offering towards the+vertical border, and the vertical border is accepting from the direction+towards the horizontal border, the right segment of the vertical+border will transition to being drawn. This will trigger an update of+the ``Image`` associated with the left widget, overwriting whatever+character is there currently with a ``bsIntersectL`` character instead.+The state of the segments afterwards will be the same, but the fact that+there is one more segment drawn will be recorded:++.. code:: text++ top+ (offering) top+ |+ |+ left +---- right left ----+---- right+ | (offering) (offering)+ |+ bottom bottom+ (offering)++It is important that this be recorded: we may later place this combined+widget to the right of another horizontal border, in which case we+would want to transition again from a ``bsIntersectL`` character to a+``bsIntersectFull`` character that represents all four segments being+drawn.++Because this involves an interaction between multiple widgets, we+may find that the two widgets involved were rendered under different+rendering contexts. To avoid mixing and matching border styles and+drawing attributes, each location records not just the state of its+four segments but also the border style and attribute that were active+at the time the border was drawn. This information is stored in+``Brick.Types.DynBorder``.++.. code:: haskell++ data DynBorder = DynBorder+ { dbStyle :: BorderStyle+ , dbAttr :: Attr+ , dbSegments :: Edges BorderSegment+ }++The ``Brick.Types.Edges`` type has one field for each direction:++.. code:: haskell++ data Edges a = Edges { eTop, eBottom, eLeft, eRight :: a }++In addition to the offer/accept handshake described above, segments also+check that their neighbor's ``BorderStyle`` and ``Attr`` match their own+before transitioning from undrawn to drawn to avoid visual glitches from+trying to connect e.g. ``unicode`` borders to ``ascii`` ones or green+borders to red ones.++The above description applies to a single location; any given widget's+result may report information about any location on its border using the+``Brick.BorderMap.BorderMap`` type. A ``BorderMap a`` is close kin to a+``Data.Map.Map Location a`` except that each ``BorderMap`` has a fixed+rectangle on which keys are retained. Values inserted at other keys are+silently discarded.++For backwards compatibility, all the widgets that ship with ``brick``+avoid reporting any border information by default, but ``brick`` offers+three ways of modifying the border-joining behavior of a widget.++* ``Brick.Widgets.Core.joinBorders`` instructs any borders drawn in its+ child widget to report their edge information. It does this+ by setting a flag in the rendering context that tells the+ ``Brick.Widgets.Border`` widgets to report the information described+ above. Consequently, widgets drawn in this context will join their+ borders with neighbors.+* ``Brick.Widgets.Core.separateBorders`` does the opposite of+ ``joinBorders`` by unsetting the same context flag, preventing border+ widgets from attempting to connect.+* ``Brick.Widgets.Core.freezeBorders`` lets its child widget connect its+ borders internally but prevents it from connecting with anything+ outside the ``freezeBorders`` call. It does this by deleting the edge+ metadata about its child widget. This means that any connections+ already made within the child widget will stay as they are but no new+ connections will be made to adjacent widgets. For example, one might+ use this to create a box with internal but no external connections:++ .. code:: haskell++ joinBorders . freezeBorders . border . hBox $+ [str "left", vBorder, str "right"]++ Or to create a box that allows external connections but not internal+ ones:++ .. code:: haskell++ joinBorders . border . freezeBorders . hBox $+ [str "left", vBorder, str "right"]++When creating new widgets, if you would like ``joinBorders`` and+``separateBorders`` to affect the behavior of your widget, you may do+so by consulting the ``ctxDynBorders`` field of the rendering context+before writing to your ``Result``'s ``borders`` field.++Animations+==========++Brick provides animation support in ``Brick.Animation``. See the Haddock+documentation in that module for a complete explanation of the API; see+``programs/AnimationDemo.hs`` (``brick-animation-demo``) for a working+example.++The Rendering Cache+===================++When widgets become expensive to render, ``brick`` provides a *rendering+cache* that automatically caches and re-uses stored Vty images from+previous renderings to avoid expensive renderings. To cache the+rendering of a widget, just wrap it in the ``Brick.Widgets.Core.cached``+function:++.. code:: haskell++ data Name = ExpensiveThing++ ui :: Widget Name+ ui = center $+ cached ExpensiveThing $+ border $+ str "This will be cached"++In the example above, the first time the ``border $ str "This will be cached"``+widget is rendered, the resulting Vty image will be stored+in the rendering cache under the key ``ExpensiveThing``. On subsequent+renderings the cached Vty image will be used instead of re-rendering the+widget. This example doesn't need caching to improve performance, but+more sophisticated widgets might.++Once ``cached`` has been used to store something in the rendering cache,+periodic cache invalidation may be required. For example, if the cached+widget is built from application state, the cache will need to be+invalidated when the relevant state changes. The cache may also need to+be invalidated when the terminal is resized. To invalidate the cache, we+use the cache invalidation functions in ``EventM``:++.. code:: haskell++ handleEvent ... = do+ -- Invalidate just a single cache entry:+ Brick.Main.invalidateCacheEntry ExpensiveThing++ -- Invalidate the entire cache (useful on a resize):+ Brick.Main.invalidateCache++Implementing Custom Widgets+===========================++``brick`` exposes all of the internals you need to implement your+own widgets. Those internals, together with ``Graphics.Vty``, can be+used to create widgets from the ground up. You'll need to implement+your own widget if you can't write what you need in terms of existing+combinators. For example, an ordinary widget like++.. code:: haskell++ myWidget :: Widget n+ myWidget = str "Above" <=> str "Below"++can be expressed with ``<=>`` and ``str`` and needs no custom behavior.+But suppose we want to write a widget that renders some string followed+by the number of columns in the space available to the widget. We can't+do this without writing a custom widget because we need access to the+rendering context. We can write such a widget as follows:++.. code:: haskell++ customWidget :: String -> Widget n+ customWidget s =+ Widget Fixed Fixed $ do+ ctx <- getContext+ render $ str (s <> " " <> show (ctx^.availWidthL))++The ``Widget`` constructor takes the horizontal and vertical growth+policies as described in `How Widgets and Rendering Work`_. Here we just+provide ``Fixed`` for both because the widget will not change behavior+if we give it more space. We then get the rendering context and append+the context's available columns to the provided string. Lastly we call+``render`` to render the widget we made with ``str``. The ``render``+function returns a ``Brick.Types.Result`` value:++.. code:: haskell++ data Result n =+ Result { image :: Graphics.Vty.Image+ , cursors :: [Brick.Types.CursorLocation n]+ , visibilityRequests :: [Brick.Types.VisibilityRequest]+ , extents :: [Extent n]+ , borders :: BorderMap DynBorder+ }++The rendering function runs in the ``RenderM`` monad, which gives us+access to the rendering context (see `How Widgets and Rendering Work`_)+via the ``Brick.Types.getContext`` function as shown above. The context+tells us about the dimensions of the rendering area and the current+attribute state of the renderer, among other things:++.. code:: haskell++ data Context =+ Context { ctxAttrName :: AttrName+ , availWidth :: Int+ , availHeight :: Int+ , ctxBorderStyle :: BorderStyle+ , ctxAttrMap :: AttrMap+ , ctxDynBorders :: Bool+ }++and has lens fields exported as described in `Conventions`_.++As shown here, the job of the rendering function is to return a+rendering result which means producing a ``vty`` ``Image``. In addition,+if you so choose, you can also return one or more cursor positions in+the ``cursors`` field of the ``Result`` as well as visibility requests+(see `Viewports`_) in the ``visibilityRequests`` field. Returned+visibility requests and cursor positions should be relative to the+upper-left corner of your widget, ``Location (0, 0)``. When your widget+is placed in others, such as boxes, the ``Result`` data you returned+will be offset (as described in `Rendering Sub-Widgets`_) to result in+correct coordinates once the entire interface has been rendered.++Using the Rendering Context+---------------------------++The most important fields of the context are the rendering area fields+``availWidth`` and ``availHeight``. These fields must be used to+determine how much space your widget has to render.++To perform an attribute lookup in the attribute map for the context's+current attribute, use ``Brick.Types.attrL``.++For example, to build a widget that always fills the available width and+height with a fill character using the current attribute, we could+write:++.. code:: haskell++ myFill :: Char -> Widget n+ myFill ch =+ Widget Greedy Greedy $ do+ ctx <- getContext+ let a = ctx^.attrL+ return $ Result (Graphics.Vty.charFill a ch (ctx^.availWidthL) (ctx^.availHeightL))+ [] [] [] Brick.BorderMap.empty++Rendering Sub-Widgets+---------------------++If your custom widget wraps another, then in addition to rendering+the wrapped widget and augmenting its returned ``Result`` *it must+also translate the resulting cursor locations, visibility requests,+and extents*. This is vital to maintaining the correctness of+rendering metadata as widget layout proceeds. To do so, use the+``Brick.Widgets.Core.addResultOffset`` function to offset the elements+of a ``Result`` by a specified amount. The amount depends on the nature+of the offset introduced by your wrapper widget's logic.++Widgets are not required to respect the rendering context's width and+height restrictions. Widgets may be embedded in viewports or translated+so they must render without cropping to work in those scenarios.+However, widgets rendering other widgets *should* enforce the rendering+context's constraints to avoid using more space than is available. The+``Brick.Widgets.Core.cropToContext`` function is provided to make this+easy:++.. code:: haskell++ let w = cropToContext someWidget++Widgets wrapped with ``cropToContext`` can be safely embedded in other+widgets. If you don't want to crop in this way, you can use any of+``vty``'s cropping functions to operate on the ``Result`` image as+desired.++Sub-widgets may specify specific attribute name values influencing+that sub-widget. If the custom widget utilizes its own attribute+names but needs to render the sub-widget, it can use ``overrideAttr``+or ``mapAttrNames`` to convert its custom names to the names that the+sub-widget uses for rendering its output.++.. _vty: https://github.com/jtdaugherty/vty+.. _Hackage: http://hackage.haskell.org/+.. _microlens: http://hackage.haskell.org/package/microlens+.. _bracketed paste mode: https://cirw.in/blog/bracketed-paste
+ docs/snake-demo.gif view
binary file changed (absent → 406517 bytes)
+ programs/AnimationDemo.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RankNTypes #-}+module Main where++import Control.Monad (void)+import Lens.Micro.Platform+import Data.List (intersperse)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Data.Map as M+import qualified Graphics.Vty as V+import Graphics.Vty.CrossPlatform (mkVty)++import Brick.BChan+import Brick.Util (fg)+import Brick.Main (App(..), neverShowCursor, customMain, halt)+import Brick.AttrMap (AttrName, AttrMap, attrMap, attrName)+import Brick.Types (Widget, EventM, BrickEvent(..), Location(..))+import Brick.Widgets.Border (border)+import Brick.Widgets.Center (center)+import Brick.Widgets.Core ((<+>), str, vBox, hBox, hLimit, vLimit, translateBy, withDefAttr)+import qualified Brick.Animation as A++data CustomEvent =+ AnimationUpdate (EventM () St ())+ -- ^ The state update constructor required by the animation API++data St =+ St { _stAnimationManager :: A.AnimationManager St CustomEvent ()+ -- ^ The animation manager that will run all of our animations+ , _animation1 :: Maybe (A.Animation St ())+ , _animation2 :: Maybe (A.Animation St ())+ , _animation3 :: Maybe (A.Animation St ())+ , _clickAnimations :: M.Map Location (A.Animation St ())+ -- ^ The various fields for storing animation states. For mouse+ -- animations, we store animations for each screen location that+ -- was clicked.+ }++makeLenses ''St++drawUI :: St -> [Widget ()]+drawUI st = drawClickAnimations st <> [drawAnimations st]++drawClickAnimations :: St -> [Widget ()]+drawClickAnimations st =+ drawClickAnimation st <$> M.toList (st^.clickAnimations)++drawClickAnimation :: St -> (Location, A.Animation St ()) -> Widget ()+drawClickAnimation st (l, a) =+ translateBy l $+ A.renderAnimation (const $ str " ") st (Just a)++drawAnimations :: St -> Widget ()+drawAnimations st =+ let animStatus label key a =+ str (label <> ": ") <+>+ maybe (str "Not running") (const $ str "Running") a <+>+ str (" (Press " <> key <> " to toggle)")+ statusMessages = statusMessage <$> zip [(0::Int)..] animations+ statusMessage (i, (c, config)) =+ animStatus ("Animation #" <> (show $ i + 1)) [c]+ (st^.(animationTarget config))+ animationDrawings = hBox $ intersperse (str " ") $+ drawSingleAnimation <$> animations+ drawSingleAnimation (_, config) =+ A.renderAnimation (const $ str " ") st (st^.(animationTarget config))+ in vBox [ str "Click and drag the mouse or press keys to start animations."+ , str " "+ , vBox statusMessages+ , animationDrawings+ ]++clip1 :: A.Clip a ()+clip1 = A.newClip_ $ str <$> [".", "o", "O", "^", " "]++clip2 :: A.Clip a ()+clip2 = A.newClip_ $ str <$> ["|", "/", "-", "\\"]++clip3 :: A.Clip a ()+clip3 =+ A.newClip_ $+ (hLimit 9 . vLimit 9 . border . center) <$>+ [ border $ str " "+ , border $ vBox $ replicate 3 $ str $ replicate 3 ' '+ , border $ vBox $ replicate 5 $ str $ replicate 5 ' '+ ]++mouseClickClip :: A.Clip a ()+mouseClickClip =+ A.newClip_+ [ withDefAttr attr6 $ str "0"+ , withDefAttr attr5 $ str "O"+ , withDefAttr attr4 $ str "o"+ , withDefAttr attr3 $ str "•"+ , withDefAttr attr2 $ str "*"+ , withDefAttr attr2 $ str "."+ ]++attr6 :: AttrName+attr6 = attrName "attr6"++attr5 :: AttrName+attr5 = attrName "attr5"++attr4 :: AttrName+attr4 = attrName "attr4"++attr3 :: AttrName+attr3 = attrName "attr3"++attr2 :: AttrName+attr2 = attrName "attr2"++attr1 :: AttrName+attr1 = attrName "attr1"++attrs :: AttrMap+attrs =+ attrMap V.defAttr+ [ (attr6, fg V.white)+ , (attr5, fg V.brightYellow)+ , (attr4, fg V.brightGreen)+ , (attr3, fg V.cyan)+ , (attr2, fg V.blue)+ , (attr1, fg V.black)+ ]++-- | Animation settings grouped together for lookup by keystroke.+data AnimationConfig =+ AnimationConfig { animationTarget :: Lens' St (Maybe (A.Animation St ()))+ , animationClip :: A.Clip St ()+ , animationFrameTime :: Integer+ , animationMode :: A.RunMode+ }++animations :: [(Char, AnimationConfig)]+animations =+ [ ('1', AnimationConfig animation1 clip1 1000 A.Loop)+ , ('2', AnimationConfig animation2 clip2 100 A.Loop)+ , ('3', AnimationConfig animation3 clip3 100 A.Once)+ ]++-- | Start the animation specified by this config.+startAnimationFromConfig :: AnimationConfig -> EventM () St ()+startAnimationFromConfig config = do+ mgr <- use stAnimationManager+ A.startAnimation mgr (animationClip config)+ (animationFrameTime config)+ (animationMode config)+ (animationTarget config)++-- | If the animation specified in this config is not running, start it.+-- Otherwise stop it.+toggleAnimationFromConfig :: AnimationConfig -> EventM () St ()+toggleAnimationFromConfig config = do+ mgr <- use stAnimationManager+ mOld <- use (animationTarget config)+ case mOld of+ Just a -> A.stopAnimation mgr a+ Nothing -> startAnimationFromConfig config++-- | Start a new mouse click animation at the specified location if one+-- is not already running there.+startMouseClickAnimation :: Location -> EventM () St ()+startMouseClickAnimation l = do+ mgr <- use stAnimationManager+ a <- use (clickAnimations.at l)+ case a of+ Just {} -> return ()+ Nothing -> A.startAnimation mgr mouseClickClip 100 A.Once (clickAnimations.at l)++appEvent :: BrickEvent () CustomEvent -> EventM () St ()+appEvent e = do+ case e of+ -- A mouse click starts an animation at the click location.+ VtyEvent (V.EvMouseDown col row _ _) ->+ startMouseClickAnimation (Location (col, row))++ -- If we got a character keystroke, see if there is a specific+ -- animation mapped to that character and toggle the resulting+ -- animation.+ VtyEvent (V.EvKey (V.KChar c) [])+ | Just aConfig <- lookup c animations ->+ toggleAnimationFromConfig aConfig++ -- Apply a state update from the animation manager.+ AppEvent (AnimationUpdate act) -> act++ VtyEvent (V.EvKey V.KEsc []) -> halt++ _ -> return ()++theApp :: App St CustomEvent ()+theApp =+ App { appDraw = drawUI+ , appChooseCursor = neverShowCursor+ , appHandleEvent = appEvent+ , appStartEvent = return ()+ , appAttrMap = const attrs+ }++main :: IO ()+main = do+ chan <- newBChan 10+ mgr <- A.startAnimationManager 50 chan AnimationUpdate++ let initialState =+ St { _stAnimationManager = mgr+ , _animation1 = Nothing+ , _animation2 = Nothing+ , _animation3 = Nothing+ , _clickAnimations = mempty+ }+ buildVty = do+ v <- mkVty V.defaultConfig+ V.setMode (V.outputIface v) V.Mouse True+ return v++ initialVty <- buildVty+ void $ customMain initialVty buildVty (Just chan) theApp initialState
programs/AttrDemo.hs view
@@ -1,10 +1,13 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} module Main where +#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Graphics.Vty- ( Event, Attr, white, blue, cyan, green, red, yellow- , black+ ( Attr, white, blue, cyan, green, red, yellow+ , black, withURL ) import Brick.Main@@ -16,28 +19,39 @@ , withAttr , vBox , str+ , hyperlink+ , modifyDefAttr ) import Brick.Util (on, fg)-import Brick.AttrMap (attrMap, AttrMap)+import Brick.AttrMap (attrMap, AttrMap, attrName) -ui :: Widget+ui :: Widget n ui = vBox [ str "This text uses the global default attribute."- , withAttr "foundFull" $+ , withAttr (attrName "foundFull") $ str "Specifying an attribute name means we look it up in the attribute tree."- , (withAttr "foundFgOnly" $- str ("When we find a value, we merge it with its parent in the attribute")- <=> str "name tree all the way to the root (the global default).")- , withAttr "missing" $+ , withAttr (attrName "foundFgOnly") $+ str "When we find a value, we merge it with its parent in the attribute"+ <=> str "name tree all the way to the root (the global default)."+ , withAttr (attrName "missing") $ str "A missing attribute name just resumes the search at its parent."- , withAttr ("general" <> "specific") $+ , withAttr (attrName "general" <> attrName "specific") $ str "In this way we build complete attribute values by using an inheritance scheme."- , withAttr "foundFull" $+ , withAttr (attrName "foundFull") $ str "You can override everything ..."- , withAttr "foundFgOnly" $- str "... or only you want to change and inherit the rest."+ , withAttr (attrName "foundFgOnly") $+ str "... or only what you want to change and inherit the rest." , str "Attribute names are assembled with the Monoid append operation to indicate"- , str "hierarchy levels, e.g. \"window\" <> \"title\"."+ , str "hierarchy levels, e.g. attrName \"window\" <> attrName \"title\"."+ , str " "+ , withAttr (attrName "linked") $+ str "This text is hyperlinked in terminals that support hyperlinking."+ , str " "+ , hyperlink "http://www.google.com/" $+ str "This text is also hyperlinked in terminals that support hyperlinking."+ , str " "+ , modifyDefAttr (`withURL` "http://www.google.com/") $+ str "This text is hyperlinked by modifying the default attribute." ] globalDefault :: Attr@@ -45,20 +59,21 @@ theMap :: AttrMap theMap = attrMap globalDefault- [ ("foundFull", white `on` green)- , ("foundFgOnly", fg red)- , ("general", yellow `on` black)- , ("general" <> "specific", fg cyan)+ [ (attrName "foundFull", white `on` green)+ , (attrName "foundFgOnly", fg red)+ , (attrName "general", yellow `on` black)+ , (attrName "general" <> attrName "specific",+ fg cyan)+ , (attrName "linked", fg yellow `withURL` "http://www.google.com/") ] -app :: App () Event+app :: App () e () app = App { appDraw = const [ui] , appHandleEvent = resizeOrQuit- , appStartEvent = return+ , appStartEvent = return () , appAttrMap = const theMap , appChooseCursor = neverShowCursor- , appLiftVtyEvent = id } main :: IO ()
programs/BorderDemo.hs view
@@ -1,27 +1,32 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Main where -import Control.Applicative ((<$>))-import Data.Monoid+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+ import qualified Data.Text as T import qualified Graphics.Vty as V import qualified Brick.Main as M-import Brick.Util (fg, bg, on)+import Brick.Util (fg, on) import qualified Brick.AttrMap as A import Brick.Types ( Widget ) import Brick.Widgets.Core- ( (<=>)- , (<+>)+ ( (<+>)+ , withAttr , vLimit , hLimit , hBox+ , vBox , updateAttrMap , withBorderStyle , txt , str+ , padLeftRight ) import qualified Brick.Widgets.Center as C import qualified Brick.Widgets.Border as B@@ -52,47 +57,48 @@ , BS.bsVertical = '!' } -borderDemos :: [Widget]+borderDemos :: [Widget ()] borderDemos = mkBorderDemo <$> styles -mkBorderDemo :: (T.Text, BS.BorderStyle) -> Widget+mkBorderDemo :: (T.Text, BS.BorderStyle) -> Widget () mkBorderDemo (styleName, sty) = withBorderStyle sty $ B.borderWithLabel (str "label") $ vLimit 5 $ C.vCenter $- txt $ " " <> styleName <> " style "+ padLeftRight 2 $+ txt $ styleName <> " style" -borderMappings :: [(A.AttrName, V.Attr)]-borderMappings =+titleAttr :: A.AttrName+titleAttr = A.attrName "title"++attrs :: [(A.AttrName, V.Attr)]+attrs = [ (B.borderAttr, V.yellow `on` V.black)- , (B.vBorderAttr, V.green `on` V.red)- , (B.hBorderAttr, V.white `on` V.green)- , (B.hBorderLabelAttr, fg V.blue)- , (B.tlCornerAttr, bg V.red)- , (B.trCornerAttr, bg V.blue)- , (B.blCornerAttr, bg V.yellow)- , (B.brCornerAttr, bg V.green)+ , (B.vBorderAttr, fg V.cyan)+ , (B.hBorderAttr, fg V.magenta)+ , (titleAttr, fg V.cyan) ] -colorDemo :: Widget+colorDemo :: Widget () colorDemo =- updateAttrMap (A.applyAttrMappings borderMappings) $- B.borderWithLabel (str "title") $+ updateAttrMap (A.applyAttrMappings attrs) $+ B.borderWithLabel (withAttr titleAttr $ str "title") $ hLimit 20 $ vLimit 5 $ C.center $ str "colors!" -ui :: Widget+ui :: Widget () ui =- hBox borderDemos- <=> B.hBorder- <=> colorDemo- <=> B.hBorderWithLabel (str "horizontal border label")- <=> (C.center (str "Left of vertical border")- <+> B.vBorder- <+> C.center (str "Right of vertical border"))+ vBox [ hBox borderDemos+ , B.hBorder+ , colorDemo+ , B.hBorderWithLabel (str "horizontal border label")+ , (C.center (str "Left of vertical border")+ <+> B.vBorder+ <+> C.center (str "Right of vertical border"))+ ] main :: IO () main = M.simpleMain ui
+ programs/CacheDemo.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE CPP #-}+module Main where++import Control.Monad (void)+import Control.Monad.State (modify)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Graphics.Vty as V++import qualified Brick.Types as T+import qualified Brick.Main as M+import qualified Brick.Widgets.Center as C+import Brick.Types+ ( Widget+ , BrickEvent(..)+ )+import Brick.Widgets.Core+ ( Padding(..)+ , vBox+ , padTopBottom+ , withDefAttr+ , cached+ , padBottom+ , str+ )+import Brick (on)+import Brick.Widgets.Center+ ( hCenter+ )+import Brick.AttrMap+ ( AttrName+ , attrName+ , attrMap+ )++data Name = ExpensiveWidget+ deriving (Ord, Show, Eq)++drawUi :: Int -> [Widget Name]+drawUi i = [ui]+ where+ ui = C.vCenter $+ vBox $ hCenter <$>+ [ str "This demo shows how cached widgets behave. The top widget below"+ , str "is cacheable, so once it's rendered, brick re-uses the rendering"+ , str "each time it is drawn. The bottom widget is not cacheable so it is"+ , str "drawn on every request. Brick supports cache invalidation to force"+ , str "a redraw of cached widgets; we can trigger that here with 'i'. Notice"+ , str "how state changes with '+' aren't reflected in the cached widget"+ , str "until the cache is invalidated with 'i'."+ , padTopBottom 1 $+ cached ExpensiveWidget $+ withDefAttr emphAttr $ str $ "This widget is cached (state = " <> show i <> ")"+ , padBottom (Pad 1) $+ withDefAttr emphAttr $ str $ "This widget is not cached (state = " <> show i <> ")"+ , hCenter $ str "Press 'i' to invalidate the cache,"+ , str "'+' to change the state value, and"+ , str "'Esc' to quit."+ ]++appEvent :: BrickEvent Name e -> T.EventM Name Int ()+appEvent (VtyEvent (V.EvKey (V.KChar '+') [])) = modify (+ 1)+appEvent (VtyEvent (V.EvKey (V.KChar 'i') [])) = M.invalidateCacheEntry ExpensiveWidget+appEvent (VtyEvent (V.EvKey V.KEsc [])) = M.halt+appEvent _ = return ()++emphAttr :: AttrName+emphAttr = attrName "emphasis"++app :: M.App Int e Name+app =+ M.App { M.appDraw = drawUi+ , M.appStartEvent = return ()+ , M.appHandleEvent = appEvent+ , M.appAttrMap = const $ attrMap V.defAttr [(emphAttr, V.white `on` V.blue)]+ , M.appChooseCursor = M.neverShowCursor+ }++main :: IO ()+main = void $ M.defaultMain app 0
+ programs/CroppingDemo.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Brick.Main (App(..), neverShowCursor, resizeOrQuit, defaultMain)+import Brick.Types+ ( Widget+ )+import Brick.Widgets.Core+ ( vBox+ , hBox+ , txt+ , (<=>)+ , padRight+ , cropLeftBy+ , cropRightBy+ , cropTopBy+ , cropBottomBy+ , cropLeftTo+ , cropRightTo+ , cropTopTo+ , cropBottomTo+ , Padding(..)+ )+import Brick.Widgets.Border (border)+import Brick.AttrMap (attrMap)+import qualified Graphics.Vty as V++example :: Widget n+example =+ border+ (txt "Example" <=> txt "Widget")++mkExample :: Widget n -> Widget n+mkExample = padRight (Pad 2)++ui :: Widget ()+ui =+ vBox [ txt "Uncropped" <=> example+ , hBox [ mkExample $ txt "cropLeftBy 2" <=> cropLeftBy 2 example+ , mkExample $ txt "cropRightBy 2" <=> cropRightBy 2 example+ , mkExample $ txt "cropTopBy 2" <=> cropTopBy 2 example+ , mkExample $ txt "cropBottomBy 2" <=> cropBottomBy 2 example+ ]+ , hBox [ mkExample $ txt "cropLeftTo 4" <=> cropLeftTo 4 example+ , mkExample $ txt "cropRightTo 4" <=> cropRightTo 4 example+ , mkExample $ txt "cropTopTo 1" <=> cropTopTo 1 example+ , mkExample $ txt "cropBottomTo 1" <=> cropBottomTo 1 example+ ]+ ]++app :: App () e ()+app =+ App { appDraw = const [ui]+ , appHandleEvent = resizeOrQuit+ , appStartEvent = return ()+ , appAttrMap = const $ attrMap V.defAttr []+ , appChooseCursor = neverShowCursor+ }++main :: IO ()+main = defaultMain app ()
programs/CustomEventDemo.hs view
@@ -1,77 +1,86 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE CPP #-} module Main where -import Control.Lens (makeLenses, (^.), (&), (.~), (%~))+import Lens.Micro ((^.))+import Lens.Micro.TH (makeLenses)+import Lens.Micro.Mtl import Control.Monad (void, forever)-import Control.Concurrent (newChan, writeChan, threadDelay, forkIO)-import Data.Default+import Control.Concurrent (threadDelay, forkIO)+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import qualified Graphics.Vty as V +import Brick.BChan import Brick.Main ( App(..) , showFirstCursor- , customMain- , continue+ , customMainWithDefaultVty , halt )+import Brick.AttrMap+ ( attrMap+ ) import Brick.Types ( Widget- , Next , EventM+ , BrickEvent(..) ) import Brick.Widgets.Core ( (<=>) , str ) +data CustomEvent = Counter deriving Show+ data St =- St { _stLastVtyEvent :: Maybe V.Event+ St { _stLastBrickEvent :: Maybe (BrickEvent () CustomEvent) , _stCounter :: Int } makeLenses ''St -data CustomEvent = VtyEvent V.Event- | Counter--drawUI :: St -> [Widget]+drawUI :: St -> [Widget ()] drawUI st = [a] where- a = (str $ "Last Vty event: " <> (show $ st^.stLastVtyEvent))+ a = (str $ "Last event: " <> (show $ st^.stLastBrickEvent)) <=> (str $ "Counter value is: " <> (show $ st^.stCounter)) -appEvent :: St -> CustomEvent -> EventM (Next St)-appEvent st e =+appEvent :: BrickEvent () CustomEvent -> EventM () St ()+appEvent e = case e of- VtyEvent (V.EvKey V.KEsc []) -> halt st- VtyEvent ev -> continue $ st & stLastVtyEvent .~ (Just ev)- Counter -> continue $ st & stCounter %~ (+1)+ VtyEvent (V.EvKey V.KEsc []) -> halt+ VtyEvent _ -> stLastBrickEvent .= (Just e)+ AppEvent Counter -> do+ stCounter %= (+1)+ stLastBrickEvent .= (Just e)+ _ -> return () initialState :: St initialState =- St { _stLastVtyEvent = Nothing+ St { _stLastBrickEvent = Nothing , _stCounter = 0 } -theApp :: App St CustomEvent+theApp :: App St CustomEvent () theApp = App { appDraw = drawUI , appChooseCursor = showFirstCursor , appHandleEvent = appEvent- , appStartEvent = return- , appAttrMap = def- , appLiftVtyEvent = VtyEvent+ , appStartEvent = return ()+ , appAttrMap = const $ attrMap V.defAttr [] } main :: IO () main = do- chan <- newChan+ chan <- newBChan 10 - forkIO $ forever $ do- writeChan chan Counter+ void $ forkIO $ forever $ do+ writeBChan chan Counter threadDelay 1000000 - void $ customMain (V.mkVty def) chan theApp initialState+ (_, vty) <- customMainWithDefaultVty (Just chan) theApp initialState+ V.shutdown vty
+ programs/CustomKeybindingDemo.hs view
@@ -0,0 +1,253 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+module Main where++import Lens.Micro ((^.))+import Lens.Micro.TH (makeLenses)+import Lens.Micro.Mtl ((<~), (.=), (%=), use)+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))+import Data.Monoid ((<>))+#endif+import qualified Graphics.Vty as V+import System.Environment (getArgs)+import System.Exit (exitFailure)++import qualified Brick.Types as T+import Brick.Types (Widget)+import qualified Brick.Keybindings as K+import Brick.AttrMap+import Brick.Util (fg)+import qualified Brick.Main as M+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Center as C+import Brick.Widgets.Core++-- | The abstract key events for the application.+data KeyEvent = QuitEvent+ | IncrementEvent+ | DecrementEvent+ deriving (Ord, Eq, Show)++-- | The mapping of key events to their configuration field names.+allKeyEvents :: K.KeyEvents KeyEvent+allKeyEvents =+ K.keyEvents [ ("quit", QuitEvent)+ , ("increment", IncrementEvent)+ , ("decrement", DecrementEvent)+ ]++-- | Default key bindings for each abstract key event.+defaultBindings :: [(KeyEvent, [K.Binding])]+defaultBindings =+ [ (QuitEvent, [K.ctrl 'q', K.bind V.KEsc])+ , (IncrementEvent, [K.bind '+'])+ , (DecrementEvent, [K.bind '-'])+ ]++data St =+ St { _keyConfig :: K.KeyConfig KeyEvent+ -- ^ The key config to use.+ , _lastKey :: Maybe (V.Key, [V.Modifier])+ -- ^ The last key that was pressed.+ , _lastKeyHandled :: Bool+ -- ^ Whether the last key was handled by a handler.+ , _counter :: Int+ -- ^ The counter value to manipulate in the handlers.+ , _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++-- | Key event handlers for our application.+handlers :: [K.KeyEventHandler KeyEvent (T.EventM () St)]+handlers =+ -- The first three handlers are triggered by keys mapped to abstract+ -- events, thus decoupling the configured key bindings from these+ -- handlers.+ [ K.onEvent QuitEvent "Quit the program"+ M.halt+ , K.onEvent IncrementEvent "Increment the counter" $+ counter %= succ+ , K.onEvent DecrementEvent "Decrement the counter" $+ counter %= subtract 1++ -- These handlers are always triggered by specific keys and thus+ -- cannot be rebound.+ , K.onKey (K.bind '\t') "Increment the counter by 10" $+ counter %= (+ 10)+ , K.onKey (K.bind V.KBackTab) "Decrement the counter by 10" $+ counter %= subtract 10+ ]++appEvent :: T.BrickEvent () e -> T.EventM () St ()+appEvent (T.VtyEvent (V.EvKey k mods)) = do+ -- 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.+ d <- use dispatcher+ lastKey .= Just (k, mods)+ lastKeyHandled <~ K.handleKey d k mods+appEvent _ =+ return ()++drawUi :: St -> [Widget ()]+drawUi st = [body]+ where+ binding = uncurry K.binding <$> st^.lastKey++ -- Generate key binding help using the library so we can embed+ -- it in the UI.+ keybindingHelp = B.borderWithLabel (txt "Active Keybindings") $+ K.keybindingHelpWidget (st^.keyConfig) handlers++ lastKeyDisplay = withDefAttr lastKeyAttr $+ txt $ maybe "(none)" K.ppBinding binding++ -- Show the status of the last pressed key, whether we handled+ -- it, and other bits of the application state.+ status = B.borderWithLabel (txt "Status") $+ hLimit 40 $+ padRight Max $+ vBox [ txt "Last key: " <+> lastKeyDisplay+ , str $ "Last key handled: " <> show (st^.lastKeyHandled)+ , str $ "Counter: " <> show (st^.counter)+ ]++ -- Show info about whether the application is currently using+ -- custom bindings loaded from an INI file.+ customBindingInfo =+ B.borderWithLabel (txt "Custom Bindings") $+ case st^.customBindingsPath of+ Nothing ->+ hLimit 40 $+ txtWrap $ "No custom bindings loaded. Create an INI file with a " <>+ (Text.pack $ show sectionName) <>+ " section or use 'programs/custom_keys.ini'. " <>+ "Pass its path to this program on the command line."+ Just f -> str "Loaded custom bindings from:" <=> str (show f)++ body = C.center $+ (padRight (Pad 2) $ status <=> customBindingInfo) <+>+ keybindingHelp++lastKeyAttr :: AttrName+lastKeyAttr = attrName "lastKey"++app :: M.App St e ()+app =+ M.App { M.appDraw = drawUi+ , M.appStartEvent = return ()+ , M.appHandleEvent = appEvent+ , M.appAttrMap = const $ attrMap V.defAttr [ (K.eventNameAttr, fg V.magenta)+ , (K.eventDescriptionAttr, fg V.cyan)+ , (K.keybindingAttr, fg V.yellow)+ , (lastKeyAttr, fg V.white `V.withStyle` V.bold)+ ]+ , M.appChooseCursor = M.showFirstCursor+ }++sectionName :: Text.Text+sectionName = "keybindings"++main :: IO ()+main = do+ args <- getArgs++ -- If the command line specified the path to an INI file with custom+ -- bindings, attempt to load it.+ (customBindings, customFile) <- case args of+ [iniFilePath] -> do+ result <- K.keybindingsFromFile allKeyEvents sectionName iniFilePath+ case result of+ -- A section was found and had zero more bindings.+ Right (Just bindings) ->+ return (bindings, Just iniFilePath)++ -- No section was found at all.+ Right Nothing -> do+ putStrLn $ "Error: found no section " <> show sectionName <> " in " <> show iniFilePath+ exitFailure++ -- There was some problem parsing the file as an INI+ -- file.+ Left e -> do+ putStrLn $ "Error reading keybindings file " <> show iniFilePath <> ": " <> e+ exitFailure++ _ -> return ([], Nothing)++ -- Create a key config that includes the default bindings as well as+ -- 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+ -- looks in plain text and Markdown formats. These can be used to+ -- generate documentation for users. Note that the output generated+ -- here takes the active bindings into account! If you don't want+ -- that, use a key config that doesn't have any custom bindings+ -- applied.+ let sections = [("Main", handlers)]++ putStrLn "Generated plain text help:"+ Text.putStrLn $ K.keybindingTextTable kc sections++ putStrLn "Generated Markdown help:"+ Text.putStrLn $ K.keybindingMarkdownTable kc sections
programs/DialogDemo.hs view
@@ -1,12 +1,16 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} module Main where +#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import qualified Graphics.Vty as V import qualified Brick.Main as M import Brick.Types ( Widget+ , BrickEvent(..) ) import Brick.Widgets.Core ( padAll@@ -21,24 +25,31 @@ data Choice = Red | Blue | Green deriving Show -drawUI :: D.Dialog Choice -> [Widget]+data Name =+ RedButton+ | BlueButton+ | GreenButton+ deriving (Show, Eq, Ord)++drawUI :: D.Dialog Choice Name -> [Widget Name] drawUI d = [ui] where ui = D.renderDialog d $ C.hCenter $ padAll 1 $ str "This is the dialog body." -appEvent :: D.Dialog Choice -> V.Event -> T.EventM (T.Next (D.Dialog Choice))-appEvent d ev =+appEvent :: BrickEvent Name e -> T.EventM Name (D.Dialog Choice Name) ()+appEvent (VtyEvent ev) = case ev of- V.EvKey V.KEsc [] -> M.halt d- V.EvKey V.KEnter [] -> M.halt d- _ -> M.continue =<< T.handleEvent ev d+ V.EvKey V.KEsc [] -> M.halt+ V.EvKey V.KEnter [] -> M.halt+ _ -> D.handleDialogEvent ev+appEvent _ = return () -initialState :: D.Dialog Choice-initialState = D.dialog "dialog" (Just "Title") (Just (0, choices)) 50+initialState :: D.Dialog Choice Name+initialState = D.dialog (Just $ str "Title") (Just (RedButton, choices)) 50 where- choices = [ ("Red", Red)- , ("Blue", Blue)- , ("Green", Green)+ choices = [ ("Red", RedButton, Red)+ , ("Blue", BlueButton, Blue)+ , ("Green", GreenButton, Green) ] theMap :: A.AttrMap@@ -48,14 +59,13 @@ , (D.buttonSelectedAttr, bg V.yellow) ] -theApp :: M.App (D.Dialog Choice) V.Event+theApp :: M.App (D.Dialog Choice Name) e Name theApp = M.App { M.appDraw = drawUI , M.appChooseCursor = M.showFirstCursor , M.appHandleEvent = appEvent- , M.appStartEvent = return+ , M.appStartEvent = return () , M.appAttrMap = const theMap- , M.appLiftVtyEvent = id } main :: IO ()
+ programs/DynamicBorderDemo.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Brick.Main as M+import Brick.Types+ ( Widget+ )+import qualified Brick.Widgets.Center as C+import qualified Brick.Widgets.Core as C+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Border.Style as BS++doubleHorizontal :: BS.BorderStyle+doubleHorizontal = BS.BorderStyle+ { BS.bsCornerTL = '╒'+ , BS.bsCornerTR = '╕'+ , BS.bsCornerBR = '╛'+ , BS.bsCornerBL = '╘'+ , BS.bsIntersectL = '╞'+ , BS.bsIntersectR = '╡'+ , BS.bsIntersectT = '╤'+ , BS.bsIntersectB = '╧'+ , BS.bsIntersectFull = '╪'+ , BS.bsHorizontal = '═'+ , BS.bsVertical = '│'+ }++box1 :: Widget ()+box1+ = C.withBorderStyle doubleHorizontal . B.border+ . C.withBorderStyle BS.unicodeRounded . B.border+ $ C.str "25 kg"++weights :: Widget ()+weights = C.withBorderStyle doubleHorizontal $ C.hBox+ [ box1+ , C.str "\n\n" C.<=> B.hBorder+ , box1+ ]++box2 :: Widget ()+box2 = C.freezeBorders $ C.vBox+ [ C.hBox+ [ C.vLimit 3 B.vBorder+ , C.str "Resize horizontally to\nmove across the label\nbelow"+ , C.vLimit 3 B.vBorder+ ]+ , B.borderWithLabel (B.vBorder C.<+> C.str " Label " C.<+> B.vBorder) $ C.hBox+ [ C.str " "+ , C.vBox [B.vBorder, C.str "L\na\nb\ne\nl", C.vLimit 3 B.vBorder]+ , C.str "\n\n\n Resize vertically to\n move across the label\n to the left\n\n\n\n\n" C.<=> B.hBorder+ ]+ ]++-- BYOB: build your own border+byob :: Widget ()+byob = C.vBox+ [ C.hBox [ corner , top , corner ]+ , C.vLimit 6 $ C.hBox [ B.vBorder, mid , B.vBorder]+ , C.hBox [ corner , B.hBorder, corner ]+ ]+ where+ top = B.hBorderWithLabel (C.str "BYOB")+ mid = C.center (C.str "If `border` is too easy,\nyou can build it yourself")+ corner = B.joinableBorder (pure False)++ui :: Widget ()+ui = C.vBox [weights, box2, byob]++main :: IO ()+main = M.simpleMain (C.joinBorders ui)
programs/EditDemo.hs view
@@ -3,7 +3,9 @@ {-# LANGUAGE RankNTypes #-} module Main where -import Control.Lens+import Lens.Micro+import Lens.Micro.TH+import Lens.Micro.Mtl import qualified Graphics.Vty as V import qualified Brick.Main as M@@ -18,72 +20,70 @@ import qualified Brick.Widgets.Center as C import qualified Brick.Widgets.Edit as E import qualified Brick.AttrMap as A+import qualified Brick.Focus as F import Brick.Util (on) +data Name = Edit1+ | Edit2+ deriving (Ord, Show, Eq)+ data St =- St { _currentEditor :: T.Name- , _edit1 :: E.Editor- , _edit2 :: E.Editor+ St { _focusRing :: F.FocusRing Name+ , _edit1 :: E.Editor String Name+ , _edit2 :: E.Editor String Name } makeLenses ''St -firstEditor :: T.Name-firstEditor = "edit1"--secondEditor :: T.Name-secondEditor = "edit2"--switchEditors :: St -> St-switchEditors st =- let next = if st^.currentEditor == firstEditor- then secondEditor else firstEditor- in st & currentEditor .~ next--currentEditorL :: St -> Lens' St E.Editor-currentEditorL st =- if st^.currentEditor == firstEditor- then edit1- else edit2--drawUI :: St -> [T.Widget]+drawUI :: St -> [T.Widget Name] drawUI st = [ui] where- ui = C.center $ (str "Input 1 (unlimited): " <+> (hLimit 30 $ vLimit 5 $ E.renderEditor $ st^.edit1)) <=>- str " " <=>- (str "Input 2 (limited to 2 lines): " <+> (hLimit 30 $ E.renderEditor $ st^.edit2)) <=>- str " " <=>- str "Press Tab to switch between editors, Esc to quit."+ e1 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit1)+ e2 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit2) -appEvent :: St -> V.Event -> T.EventM (T.Next St)-appEvent st ev =- case ev of- V.EvKey V.KEsc [] -> M.halt st- V.EvKey (V.KChar '\t') [] -> M.continue $ switchEditors st- _ -> M.continue =<< T.handleEventLensed st (currentEditorL st) ev+ ui = C.center $+ (str "Input 1 (unlimited): " <+> (hLimit 30 $ vLimit 5 e1)) <=>+ str " " <=>+ (str "Input 2 (limited to 2 lines): " <+> (hLimit 30 e2)) <=>+ str " " <=>+ str "Press Tab to switch between editors, Esc to quit." +appEvent :: T.BrickEvent Name e -> T.EventM Name St ()+appEvent (T.VtyEvent (V.EvKey V.KEsc [])) =+ M.halt+appEvent (T.VtyEvent (V.EvKey (V.KChar '\t') [])) =+ focusRing %= F.focusNext+appEvent (T.VtyEvent (V.EvKey V.KBackTab [])) =+ focusRing %= F.focusPrev+appEvent ev = do+ r <- use focusRing+ case F.focusGetCurrent r of+ Just Edit1 -> zoom edit1 $ E.handleEditorEvent ev+ Just Edit2 -> zoom edit2 $ E.handleEditorEvent ev+ Nothing -> return ()+ initialState :: St initialState =- St firstEditor- (E.editor firstEditor (str . unlines) Nothing "")- (E.editor secondEditor (str . unlines) (Just 2) "")+ St (F.focusRing [Edit1, Edit2])+ (E.editor Edit1 Nothing "")+ (E.editor Edit2 (Just 2) "") theMap :: A.AttrMap theMap = A.attrMap V.defAttr- [ (E.editAttr, V.white `on` V.blue)+ [ (E.editAttr, V.white `on` V.blue)+ , (E.editFocusedAttr, V.black `on` V.yellow) ] -appCursor :: St -> [T.CursorLocation] -> Maybe T.CursorLocation-appCursor st = M.showCursorNamed (st^.currentEditor)+appCursor :: St -> [T.CursorLocation Name] -> Maybe (T.CursorLocation Name)+appCursor = F.focusRingCursor (^.focusRing) -theApp :: M.App St V.Event+theApp :: M.App St e Name theApp = M.App { M.appDraw = drawUI , M.appChooseCursor = appCursor , M.appHandleEvent = appEvent- , M.appStartEvent = return+ , M.appStartEvent = return () , M.appAttrMap = const theMap- , M.appLiftVtyEvent = id } main :: IO ()
+ programs/EditorLineNumbersDemo.hs view
@@ -0,0 +1,135 @@+{-# 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
+ programs/FileBrowserDemo.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+module Main where++import qualified Control.Exception as E+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Graphics.Vty as V++import Control.Monad.State (get)+import qualified Data.Text as Text+import qualified Brick.Main as M+import qualified Brick.Widgets.List as L+import Brick.AttrMap (AttrName, attrName)+import Brick.Types+ ( Widget+ , BrickEvent(..)+ )+import Brick.Widgets.Center+ ( center+ , hCenter+ )+import Brick.Widgets.Border+ ( borderWithLabel+ )+import Brick.Widgets.Core+ ( vBox, (<=>), padTop+ , hLimit, vLimit, txt+ , withDefAttr, emptyWidget+ , Padding(..)+ )+import qualified Brick.Widgets.FileBrowser as FB+import qualified Brick.AttrMap as A+import Brick.Util (on, fg)+import qualified Brick.Types as T++data Name = FileBrowser1+ deriving (Eq, Show, Ord)++drawUI :: FB.FileBrowser Name -> [Widget Name]+drawUI b = [center $ ui <=> help]+ where+ ui = hCenter $+ vLimit 15 $+ hLimit 50 $+ borderWithLabel (txt "Choose a file") $+ FB.renderFileBrowser True b+ help = padTop (Pad 1) $+ vBox [ case FB.fileBrowserException b of+ Nothing -> emptyWidget+ Just e -> hCenter $ withDefAttr errorAttr $+ txt $ Text.pack $ E.displayException e+ , hCenter $ txt "Up/Down: select"+ , hCenter $ txt "/: search, Ctrl-C or Esc: cancel search"+ , hCenter $ txt "Enter: change directory or select file"+ , hCenter $ txt "Esc: quit"+ ]++appEvent :: BrickEvent Name e -> T.EventM Name (FB.FileBrowser Name) ()+appEvent (VtyEvent ev) = do+ b <- get+ case ev of+ V.EvKey V.KEsc [] | not (FB.fileBrowserIsSearching b) ->+ M.halt+ _ -> do+ FB.handleFileBrowserEvent ev+ -- If the browser has a selected file after handling the+ -- event (because the user pressed Enter), shut down.+ case ev of+ V.EvKey V.KEnter [] -> do+ b' <- get+ case FB.fileBrowserSelection b' of+ [] -> return ()+ _ -> M.halt+ _ -> return ()+appEvent _ = return ()++errorAttr :: AttrName+errorAttr = attrName "error"++theMap :: A.AttrMap+theMap = A.attrMap V.defAttr+ [ (L.listSelectedFocusedAttr, V.black `on` V.yellow)+ , (FB.fileBrowserCurrentDirectoryAttr, V.white `on` V.blue)+ , (FB.fileBrowserSelectionInfoAttr, V.white `on` V.blue)+ , (FB.fileBrowserDirectoryAttr, fg V.blue)+ , (FB.fileBrowserBlockDeviceAttr, fg V.magenta)+ , (FB.fileBrowserCharacterDeviceAttr, fg V.green)+ , (FB.fileBrowserNamedPipeAttr, fg V.yellow)+ , (FB.fileBrowserSymbolicLinkAttr, fg V.cyan)+ , (FB.fileBrowserUnixSocketAttr, fg V.red)+ , (FB.fileBrowserSelectedAttr, V.white `on` V.magenta)+ , (errorAttr, fg V.red)+ ]++theApp :: M.App (FB.FileBrowser Name) e Name+theApp =+ M.App { M.appDraw = drawUI+ , M.appChooseCursor = M.showFirstCursor+ , M.appHandleEvent = appEvent+ , M.appStartEvent = return ()+ , M.appAttrMap = const theMap+ }++main :: IO ()+main = do+ b <- M.defaultMain theApp =<< FB.newFileBrowser FB.selectNonDirectories FileBrowser1 Nothing+ putStrLn $ "Selected entry: " <> show (FB.fileBrowserSelection b)
+ programs/FillDemo.hs view
@@ -0,0 +1,16 @@+module Main where++import Brick+import Brick.Widgets.Border++ui :: Widget ()+ui = vBox [ vLimitPercent 20 $ vBox [ str "This text is in the top 20% of the window due to a fill and vLimitPercent."+ , fill ' '+ , hBorder+ ]+ , str "This text is at the bottom with another fill beneath it."+ , fill 'x'+ ]++main :: IO ()+main = simpleMain ui
+ programs/FormDemo.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Data.Text as T+import Lens.Micro ((^.))+import Lens.Micro.TH+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif++import qualified Graphics.Vty as V+import Graphics.Vty.CrossPlatform (mkVty)++import Brick+import Brick.Forms+ ( Form+ , newForm+ , formState+ , formFocus+ , setFieldValid+ , renderForm+ , handleFormEvent+ , invalidFields+ , allFieldsValid+ , focusedFormInputAttr+ , invalidFormInputAttr+ , checkboxField+ , radioField+ , editShowableField+ , editTextField+ , editPasswordField+ , (@@=)+ )+import Brick.Focus+ ( focusGetCurrent+ , focusRingCursor+ )+import qualified Brick.Widgets.Edit as E+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Center as C++data Name = NameField+ | AgeField+ | BikeField+ | HandedField+ | PasswordField+ | LeftHandField+ | RightHandField+ | AmbiField+ | AddressField+ deriving (Eq, Ord, Show)++data Handedness = LeftHanded | RightHanded | Ambidextrous+ deriving (Show, Eq)++data UserInfo =+ UserInfo { _name :: T.Text+ , _age :: Int+ , _address :: T.Text+ , _ridesBike :: Bool+ , _handed :: Handedness+ , _password :: T.Text+ }+ deriving (Show)++makeLenses ''UserInfo++-- This form is covered in the Brick User Guide; see the "Input Forms"+-- section.+mkForm :: UserInfo -> Form UserInfo e Name+mkForm =+ let label s w = padBottom (Pad 1) $+ (vLimit 1 $ hLimit 15 $ str s <+> fill ' ') <+> w+ in newForm [ label "Name" @@=+ editTextField name NameField (Just 1)+ , label "Address" @@=+ B.borderWithLabel (str "Mailing") @@=+ editTextField address AddressField (Just 3)+ , label "Age" @@=+ editShowableField age AgeField+ , label "Password" @@=+ editPasswordField password PasswordField+ , label "Dominant hand" @@=+ radioField handed [ (LeftHanded, LeftHandField, "Left")+ , (RightHanded, RightHandField, "Right")+ , (Ambidextrous, AmbiField, "Both")+ ]+ , label "" @@=+ checkboxField ridesBike BikeField "Do you ride a bicycle?"+ ]++theMap :: AttrMap+theMap = attrMap V.defAttr+ [ (E.editAttr, V.white `on` V.black)+ , (E.editFocusedAttr, V.black `on` V.yellow)+ , (invalidFormInputAttr, V.white `on` V.red)+ , (focusedFormInputAttr, V.black `on` V.yellow)+ ]++draw :: Form UserInfo e Name -> [Widget Name]+draw f = [C.vCenter $ C.hCenter form <=> C.hCenter help]+ where+ form = B.border $ padTop (Pad 1) $ hLimit 50 $ renderForm f+ help = padTop (Pad 1) $ B.borderWithLabel (str "Help") body+ body = str $ "- Name is free-form text\n" <>+ "- Age must be an integer (try entering an\n" <>+ " invalid age!)\n" <>+ "- Handedness selects from a list of options\n" <>+ "- The last option is a checkbox\n" <>+ "- Enter/Esc quit, mouse interacts with fields"++app :: App (Form UserInfo e Name) e Name+app =+ App { appDraw = draw+ , appHandleEvent = \ev -> do+ f <- gets formFocus+ case ev of+ VtyEvent (V.EvResize {}) -> return ()+ VtyEvent (V.EvKey V.KEsc []) -> halt+ -- Enter quits only when we aren't in the multi-line editor.+ VtyEvent (V.EvKey V.KEnter [])+ | focusGetCurrent f /= Just AddressField -> halt+ _ -> do+ handleFormEvent ev++ -- Example of external validation:+ -- Require age field to contain a value that is at least 18.+ st <- gets formState+ modify $ setFieldValid (st^.age >= 18) AgeField++ , appChooseCursor = focusRingCursor formFocus+ , appStartEvent = return ()+ , appAttrMap = const theMap+ }++main :: IO ()+main = do+ let buildVty = do+ v <- mkVty V.defaultConfig+ V.setMode (V.outputIface v) V.Mouse True+ return v++ initialUserInfo = UserInfo { _name = ""+ , _address = ""+ , _age = 0+ , _handed = RightHanded+ , _ridesBike = False+ , _password = ""+ }+ f = setFieldValid False AgeField $+ mkForm initialUserInfo++ initialVty <- buildVty+ f' <- customMain initialVty buildVty Nothing app f++ putStrLn "The starting form state was:"+ print initialUserInfo++ putStrLn "The final form state was:"+ print $ formState f'++ if allFieldsValid f'+ then putStrLn "The final form inputs were valid."+ else putStrLn $ "The final form had invalid inputs: " <> show (invalidFields f')
programs/HelloWorldDemo.hs view
@@ -2,7 +2,7 @@ import Brick -ui :: Widget+ui :: Widget () ui = str "Hello, world!" main :: IO ()
programs/LayerDemo.hs view
@@ -1,70 +1,108 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} module Main where -import Control.Lens (makeLenses, (^.), (&), (%~))+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import Lens.Micro ((^.))+import Lens.Micro.TH (makeLenses)+import Lens.Micro.Mtl import Control.Monad (void)-import Data.Default import qualified Graphics.Vty as V import qualified Brick.Types as T-import Brick.Types (rowL, columnL)+import Brick.Types (locationRowL, locationColumnL, Location(..), Widget) import qualified Brick.Main as M import qualified Brick.Widgets.Border as B-import Brick.Types- ( Widget- )+import qualified Brick.Widgets.Center as C import Brick.Widgets.Core ( translateBy , str+ , relativeTo+ , reportExtent+ , withDefAttr )+import Brick.Util (fg)+import Brick.AttrMap+ ( attrMap+ , AttrName+ , attrName+ ) data St =- St { _topLayerLocation :: T.Location+ St { _middleLayerLocation :: T.Location , _bottomLayerLocation :: T.Location } makeLenses ''St -drawUi :: St -> [Widget]+data Name =+ MiddleLayerElement+ deriving (Ord, Eq, Show)++drawUi :: St -> [Widget Name] drawUi st =- [ topLayer st- , bottomLayer st+ [ C.centerLayer $+ B.border $ str "This layer is centered but other\nlayers are placed underneath it."+ , arrowLayer+ , middleLayer (st^.middleLayerLocation)+ , bottomLayer (st^.bottomLayerLocation) ] -topLayer :: St -> Widget-topLayer st =- translateBy (st^.topLayerLocation) $- B.border $ str "Top layer\n(Arrow keys move)"+arrowLayer :: Widget Name+arrowLayer =+ let msg = "Relatively\n" <>+ "positioned\n" <>+ "arrow---->"+ in relativeTo MiddleLayerElement (Location (-10, -2)) $+ withDefAttr arrowAttr $+ str msg -bottomLayer :: St -> Widget-bottomLayer st =- translateBy (st^.bottomLayerLocation) $+middleLayer :: Location -> Widget Name+middleLayer l =+ translateBy l $+ reportExtent MiddleLayerElement $+ B.border $ str "Middle layer\n(Arrow keys move)"++bottomLayer :: Location -> Widget Name+bottomLayer l =+ translateBy l $ B.border $ str "Bottom layer\n(Ctrl-arrow keys move)" -appEvent :: St -> V.Event -> T.EventM (T.Next St)-appEvent st (V.EvKey V.KDown []) = M.continue $ st & topLayerLocation.rowL %~ (+ 1)-appEvent st (V.EvKey V.KUp []) = M.continue $ st & topLayerLocation.rowL %~ (subtract 1)-appEvent st (V.EvKey V.KRight []) = M.continue $ st & topLayerLocation.columnL %~ (+ 1)-appEvent st (V.EvKey V.KLeft []) = M.continue $ st & topLayerLocation.columnL %~ (subtract 1)+appEvent :: T.BrickEvent Name e -> T.EventM Name St ()+appEvent (T.VtyEvent (V.EvKey V.KDown [])) =+ middleLayerLocation.locationRowL %= (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KUp [])) =+ middleLayerLocation.locationRowL %= (subtract 1)+appEvent (T.VtyEvent (V.EvKey V.KRight [])) =+ middleLayerLocation.locationColumnL %= (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KLeft [])) =+ middleLayerLocation.locationColumnL %= (subtract 1) -appEvent st (V.EvKey V.KDown [V.MCtrl]) = M.continue $ st & bottomLayerLocation.rowL %~ (+ 1)-appEvent st (V.EvKey V.KUp [V.MCtrl]) = M.continue $ st & bottomLayerLocation.rowL %~ (subtract 1)-appEvent st (V.EvKey V.KRight [V.MCtrl]) = M.continue $ st & bottomLayerLocation.columnL %~ (+ 1)-appEvent st (V.EvKey V.KLeft [V.MCtrl]) = M.continue $ st & bottomLayerLocation.columnL %~ (subtract 1)+appEvent (T.VtyEvent (V.EvKey V.KDown [V.MCtrl])) =+ bottomLayerLocation.locationRowL %= (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KUp [V.MCtrl])) =+ bottomLayerLocation.locationRowL %= (subtract 1)+appEvent (T.VtyEvent (V.EvKey V.KRight [V.MCtrl])) =+ bottomLayerLocation.locationColumnL %= (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KLeft [V.MCtrl])) =+ bottomLayerLocation.locationColumnL %= (subtract 1) -appEvent st (V.EvKey V.KEsc []) = M.halt st-appEvent st _ = M.continue st+appEvent (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt+appEvent _ = return () -app :: M.App St V.Event+arrowAttr :: AttrName+arrowAttr = attrName "attr"++app :: M.App St e Name app = M.App { M.appDraw = drawUi- , M.appStartEvent = return+ , M.appStartEvent = return () , M.appHandleEvent = appEvent- , M.appAttrMap = const def- , M.appLiftVtyEvent = id+ , M.appAttrMap = const $ attrMap V.defAttr [(arrowAttr, fg V.cyan)] , M.appChooseCursor = M.neverShowCursor } main :: IO ()-main = void $ M.defaultMain app $ St (T.Location (0, 0)) (T.Location (0, 0))+main = void $ M.defaultMain app $ St (T.Location (20, 5)) (T.Location (0, 0))
programs/ListDemo.hs view
@@ -1,9 +1,13 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} module Main where -import Control.Lens ((^.))+import Lens.Micro ((^.))+import Lens.Micro.Mtl import Control.Monad (void)+import Control.Monad.State (modify)+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import Data.Maybe (fromMaybe) import qualified Graphics.Vty as V @@ -13,7 +17,7 @@ import qualified Brick.Widgets.List as L import qualified Brick.Widgets.Center as C import qualified Brick.AttrMap as A-import qualified Data.Vector as V+import qualified Data.Vector as Vec import Brick.Types ( Widget )@@ -27,56 +31,59 @@ ) import Brick.Util (fg, on) -drawUI :: (Show a) => L.List a -> [Widget]+drawUI :: (Show a) => L.List () a -> [Widget ()] drawUI l = [ui] where label = str "Item " <+> cur <+> str " of " <+> total cur = case l^.(L.listSelectedL) of Nothing -> str "-" Just i -> str (show (i + 1))- total = str $ show $ V.length $ l^.(L.listElementsL)+ total = str $ show $ Vec.length $ l^.(L.listElementsL) box = B.borderWithLabel label $ hLimit 25 $ vLimit 15 $- L.renderList l listDrawElement+ L.renderList listDrawElement True l ui = C.vCenter $ vBox [ C.hCenter box , str " " , C.hCenter $ str "Press +/- to add/remove list elements." , C.hCenter $ str "Press Esc to exit." ] -appEvent :: L.List Char -> V.Event -> T.EventM (T.Next (L.List Char))-appEvent l e =+appEvent :: T.BrickEvent () e -> T.EventM () (L.List () Char) ()+appEvent (T.VtyEvent e) = case e of- V.EvKey (V.KChar '+') [] ->- let el = nextElement (L.listElements l)- pos = V.length $ l^.(L.listElementsL)- in M.continue $ L.listInsert pos el l+ V.EvKey (V.KChar '+') [] -> do+ els <- use L.listElementsL+ let el = nextElement els+ pos = Vec.length els+ modify $ L.listInsert pos el - V.EvKey (V.KChar '-') [] ->- case l^.(L.listSelectedL) of- Nothing -> M.continue l- Just i -> M.continue $ L.listRemove i l+ V.EvKey (V.KChar '-') [] -> do+ sel <- use L.listSelectedL+ case sel of+ Nothing -> return ()+ Just i -> modify $ L.listRemove i - V.EvKey V.KEsc [] -> M.halt l+ V.EvKey V.KEsc [] -> M.halt - ev -> M.continue =<< T.handleEvent ev l+ ev -> L.handleListEvent ev where- nextElement :: V.Vector Char -> Char- nextElement v = fromMaybe '?' $ V.find (flip V.notElem v) (V.fromList ['a' .. 'z'])+ nextElement :: Vec.Vector Char -> Char+ nextElement v = fromMaybe '?' $ Vec.find (flip Vec.notElem v) (Vec.fromList ['a' .. 'z'])+appEvent _ = return () -listDrawElement :: (Show a) => Bool -> a -> Widget+listDrawElement :: (Show a) => Bool -> a -> Widget () listDrawElement sel a = let selStr s = if sel then withAttr customAttr (str $ "<" <> s <> ">") else str s in C.hCenter $ str "Item " <+> (selStr $ show a) -initialState :: L.List Char-initialState = L.list (T.Name "list") (V.fromList ['a','b','c']) 1+initialState :: L.List () Char+initialState = L.list () (Vec.fromList ['a','b','c']) 1 customAttr :: A.AttrName-customAttr = L.listSelectedAttr <> "custom"+customAttr = L.listSelectedAttr <> A.attrName "custom" theMap :: A.AttrMap theMap = A.attrMap V.defAttr@@ -85,14 +92,13 @@ , (customAttr, fg V.cyan) ] -theApp :: M.App (L.List Char) V.Event+theApp :: M.App (L.List () Char) e () theApp = M.App { M.appDraw = drawUI , M.appChooseCursor = M.showFirstCursor , M.appHandleEvent = appEvent- , M.appStartEvent = return+ , M.appStartEvent = return () , M.appAttrMap = const theMap- , M.appLiftVtyEvent = id } main :: IO ()
+ programs/ListViDemo.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE CPP #-}+module Main where++import Control.Monad (void)+import Control.Monad.State (modify)+import Data.Maybe (fromMaybe)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Graphics.Vty as V+import Lens.Micro ((^.))+import Lens.Micro.Mtl++import qualified Brick.AttrMap as A+import qualified Brick.Main as M+import Brick.Types (Widget)+import qualified Brick.Types as T+import Brick.Util (fg, on)+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Center as C+import Brick.Widgets.Core (hLimit, str, vBox, vLimit, withAttr, (<+>))+import qualified Brick.Widgets.List as L+import qualified Data.Vector as Vec++drawUI :: (Show a) => L.List () a -> [Widget ()]+drawUI l = [ui]+ where+ label = str "Item " <+> cur <+> str " of " <+> total+ cur = case l^.(L.listSelectedL) of+ Nothing -> str "-"+ Just i -> str (show (i + 1))+ total = str $ show $ Vec.length $ l^.(L.listElementsL)+ box = B.borderWithLabel label $+ hLimit 25 $+ vLimit 15 $+ L.renderList listDrawElement True l+ ui = C.vCenter $ vBox [ C.hCenter box+ , str " "+ , C.hCenter $ str "Press +/- to add/remove list elements."+ , C.hCenter $ str "Press Esc to exit."+ ]++appEvent :: T.BrickEvent () e -> T.EventM () (L.List () Char) ()+appEvent (T.VtyEvent e) =+ case e of+ V.EvKey (V.KChar '+') [] -> do+ els <- use L.listElementsL+ let el = nextElement els+ pos = Vec.length els+ modify $ L.listInsert pos el++ V.EvKey (V.KChar '-') [] -> do+ sel <- use L.listSelectedL+ case sel of+ Nothing -> return ()+ Just i -> modify $ L.listRemove i++ V.EvKey V.KEsc [] -> M.halt++ ev -> L.handleListEventVi L.handleListEvent ev+ where+ nextElement :: Vec.Vector Char -> Char+ nextElement v = fromMaybe '?' $ Vec.find (flip Vec.notElem v) (Vec.fromList ['a' .. 'z'])+appEvent _ = return ()++listDrawElement :: (Show a) => Bool -> a -> Widget ()+listDrawElement sel a =+ let selStr s = if sel+ then withAttr customAttr (str $ "<" <> s <> ">")+ else str s+ in C.hCenter $ str "Item " <+> (selStr $ show a)++initialState :: L.List () Char+initialState = L.list () (Vec.fromList ['a','b','c']) 1++customAttr :: A.AttrName+customAttr = L.listSelectedAttr <> A.attrName "custom"++theMap :: A.AttrMap+theMap = A.attrMap V.defAttr+ [ (L.listAttr, V.white `on` V.blue)+ , (L.listSelectedAttr, V.blue `on` V.white)+ , (customAttr, fg V.cyan)+ ]++theApp :: M.App (L.List () Char) e ()+theApp =+ M.App { M.appDraw = drawUI+ , M.appChooseCursor = M.showFirstCursor+ , M.appHandleEvent = appEvent+ , M.appStartEvent = return ()+ , M.appAttrMap = const theMap+ }++main :: IO ()+main = void $ M.defaultMain theApp initialState
− programs/MarkupDemo.hs
@@ -1,42 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Main where--import Data.Monoid ((<>))-import qualified Graphics.Vty as V--import Brick.Main (App(..), defaultMain, resizeOrQuit, neverShowCursor)-import Brick.Types- ( Widget- )-import Brick.Widgets.Core- ( (<=>)- )-import Brick.Util (on, fg)-import Brick.Markup (markup, (@?))-import Brick.AttrMap (attrMap, AttrMap)-import Data.Text.Markup ((@@))--ui :: Widget-ui = m1 <=> m2- where- m1 = markup $ ("Hello" @@ fg V.blue) <> ", " <> ("world!" @@ fg V.red)- m2 = markup $ ("Hello" @? "keyword1") <> ", " <> ("world!" @? "keyword2")--theMap :: AttrMap-theMap = attrMap V.defAttr- [ ("keyword1", fg V.magenta)- , ("keyword2", V.white `on` V.blue)- ]--app :: App () V.Event-app =- App { appDraw = const [ui]- , appHandleEvent = resizeOrQuit- , appAttrMap = const theMap- , appStartEvent = return- , appChooseCursor = neverShowCursor- , appLiftVtyEvent = id- }--main :: IO ()-main = defaultMain app ()
+ programs/MouseDemo.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}+module Main where++import Lens.Micro ((^.))+import Lens.Micro.TH (makeLenses)+import Lens.Micro.Mtl+import Control.Monad (void)+import Control.Monad.Trans (liftIO)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Graphics.Vty as V++import qualified Brick.Types as T+import Brick.AttrMap+import Brick.Util+import Brick.Types (Widget, ViewportType(Vertical))+import qualified Brick.Main as M+import qualified Brick.Widgets.Edit as E+import qualified Brick.Widgets.Center as C+import qualified Brick.Widgets.Border as B+import Brick.Widgets.Core++data Name = Info | Button1 | Button2 | Button3 | Prose | TextBox+ deriving (Show, Ord, Eq)++data St =+ St { _clicked :: [T.Extent Name]+ , _lastReportedClick :: Maybe (Name, T.Location)+ , _prose :: String+ , _edit :: E.Editor String Name+ }++makeLenses ''St++drawUi :: St -> [Widget Name]+drawUi st =+ [ buttonLayer st+ , proseLayer st+ , infoLayer st+ ]++buttonLayer :: St -> Widget Name+buttonLayer st =+ C.vCenterLayer $+ C.hCenterLayer (padBottom (Pad 1) $ str "Click a button:") <=>+ C.hCenterLayer (hBox $ padLeftRight 1 <$> buttons) <=>+ C.hCenterLayer (padTopBottom 1 $ str "Or enter text and then click in this editor:") <=>+ C.hCenterLayer (vLimit 3 $ hLimit 50 $ E.renderEditor (str . unlines) True (st^.edit))+ where+ buttons = mkButton <$> buttonData+ buttonData = [ (Button1, "Button 1", attrName "button1")+ , (Button2, "Button 2", attrName "button2")+ , (Button3, "Button 3", attrName "button3")+ ]+ mkButton (name, label, attr) =+ let wasClicked = (fst <$> st^.lastReportedClick) == Just name+ in clickable name $+ withDefAttr attr $+ B.border $+ padTopBottom 1 $+ padLeftRight (if wasClicked then 2 else 3) $+ str (if wasClicked then "<" <> label <> ">" else label)++proseLayer :: St -> Widget Name+proseLayer st =+ B.border $+ C.hCenterLayer $+ vLimit 8 $+ viewport Prose Vertical $+ vBox $ map str $ lines (st^.prose)++infoLayer :: St -> Widget Name+infoLayer st = T.Widget T.Fixed T.Fixed $ do+ c <- T.getContext+ let h = c^.T.availHeightL+ msg = case st^.lastReportedClick of+ Nothing ->+ "Click and hold/drag to report a mouse click"+ Just (name, T.Location l) ->+ "Mouse down at " <> show name <> " @ " <> show l+ T.render $ translateBy (T.Location (0, h-1)) $ clickable Info $+ withDefAttr (attrName "info") $+ C.hCenter $ str msg++appEvent :: T.BrickEvent Name e -> T.EventM Name St ()+appEvent ev@(T.MouseDown n _ _ loc) = do+ lastReportedClick .= Just (n, loc)+ zoom edit $ E.handleEditorEvent ev+appEvent (T.MouseUp {}) =+ lastReportedClick .= Nothing+appEvent (T.VtyEvent (V.EvMouseUp {})) =+ lastReportedClick .= Nothing+appEvent (T.VtyEvent (V.EvKey V.KUp [V.MCtrl])) =+ M.vScrollBy (M.viewportScroll Prose) (-1)+appEvent (T.VtyEvent (V.EvKey V.KDown [V.MCtrl])) =+ M.vScrollBy (M.viewportScroll Prose) 1+appEvent (T.VtyEvent (V.EvKey V.KEsc [])) =+ M.halt+appEvent ev =+ zoom edit $ E.handleEditorEvent ev++aMap :: AttrMap+aMap = attrMap V.defAttr+ [ (attrName "info", V.white `on` V.magenta)+ , (attrName "button1", V.white `on` V.cyan)+ , (attrName "button2", V.white `on` V.green)+ , (attrName "button3", V.white `on` V.blue)+ , (E.editFocusedAttr, V.black `on` V.yellow)+ ]++app :: M.App St e Name+app =+ M.App { M.appDraw = drawUi+ , M.appStartEvent = do+ vty <- M.getVtyHandle+ liftIO $ V.setMode (V.outputIface vty) V.Mouse True+ , M.appHandleEvent = appEvent+ , M.appAttrMap = const aMap+ , M.appChooseCursor = M.showFirstCursor+ }++main :: IO ()+main = do+ void $ M.defaultMain app $ St [] Nothing+ (unlines [ "Try clicking on various UI elements."+ , "Observe that the click coordinates identify the"+ , "underlying widget coordinates."+ , ""+ , "Lorem ipsum dolor sit amet,"+ , "consectetur adipiscing elit,"+ , "sed do eiusmod tempor incididunt ut labore"+ , "et dolore magna aliqua."+ , ""+ , "Ut enim ad minim veniam"+ , "quis nostrud exercitation ullamco laboris"+ , "isi ut aliquip ex ea commodo consequat."+ , ""+ , "Duis aute irure dolor in reprehenderit"+ , "in voluptate velit esse cillum dolore eu fugiat nulla pariatur."+ , ""+ , "Excepteur sint occaecat cupidatat not proident,"+ , "sunt in culpa qui officia deserunt mollit"+ , "anim id est laborum."+ ])+ (E.editor TextBox Nothing "")
programs/PaddingDemo.hs view
@@ -1,13 +1,9 @@ {-# LANGUAGE OverloadedStrings #-} module Main where -import Data.Default-import qualified Graphics.Vty as V- import Brick.Main (App(..), neverShowCursor, resizeOrQuit, defaultMain) import Brick.Types ( Widget- , Padding(..) ) import Brick.Widgets.Core ( vBox@@ -20,20 +16,23 @@ , padBottom , padTopBottom , padLeftRight+ , Padding(..) )-import Brick.Widgets.Border as B-import Brick.Widgets.Center as C+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Center as C+import Brick.AttrMap (attrMap)+import qualified Graphics.Vty as V -ui :: Widget+ui :: Widget () ui =- vBox [ hBox [ padLeft Max $ vCenter $ str "Left-padded"+ vBox [ hBox [ padLeft Max $ C.vCenter $ str "Left-padded" , B.vBorder- , padRight Max $ vCenter $ str "Right-padded"+ , padRight Max $ C.vCenter $ str "Right-padded" ] , B.hBorder- , hBox [ padTop Max $ hCenter $ str "Top-padded"+ , hBox [ padTop Max $ C.hCenter $ str "Top-padded" , B.vBorder- , padBottom Max $ hCenter $ str "Bottom-padded"+ , padBottom Max $ C.hCenter $ str "Bottom-padded" ] , B.hBorder , hBox [ padLeftRight 2 $ str "Padded by 2 on left/right"@@ -46,14 +45,13 @@ , padAll 2 $ str "Padded by 2 on all sides" ] -app :: App () V.Event+app :: App () e () app = App { appDraw = const [ui] , appHandleEvent = resizeOrQuit- , appStartEvent = return- , appAttrMap = const def+ , appStartEvent = return ()+ , appAttrMap = const $ attrMap V.defAttr [] , appChooseCursor = neverShowCursor- , appLiftVtyEvent = id } main :: IO ()
+ programs/ProgressBarDemo.hs view
@@ -0,0 +1,153 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}+module Main where++import Control.Monad (void)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Graphics.Vty as V+import Lens.Micro.Mtl+import Lens.Micro.TH++import qualified Brick.AttrMap as A+import qualified Brick.Main as M+import qualified Brick.Types as T+import qualified Brick.Widgets.ProgressBar as P+import Brick.Types+ ( Widget+ )+import Brick.Widgets.Core+ ( (<+>), (<=>)+ , str+ , strWrap+ , updateAttrMap+ , overrideAttr+ )+import Brick.Util (fg, bg, on, clamp)++data MyAppState n = MyAppState { _x, _y, _z :: Float, _showLabel :: Bool }++makeLenses ''MyAppState++drawUI :: MyAppState () -> [Widget ()]+drawUI p = [ui]+ where+ -- use mapAttrNames+ xBar = updateAttrMap+ (A.mapAttrNames [ (xDoneAttr, P.progressCompleteAttr)+ , (xToDoAttr, P.progressIncompleteAttr)+ ]+ ) $ bar $ _x p+ -- or use individual mapAttrName calls+ yBar = updateAttrMap+ (A.mapAttrName yDoneAttr P.progressCompleteAttr .+ A.mapAttrName yToDoAttr P.progressIncompleteAttr) $+ bar $ _y p+ -- or use overrideAttr calls+ zBar = overrideAttr P.progressCompleteAttr zDoneAttr $+ overrideAttr P.progressIncompleteAttr zToDoAttr $+ bar $ _z p+ -- custom bars+ cBar1 = overrideAttr P.progressCompleteAttr cDoneAttr1 $+ overrideAttr P.progressIncompleteAttr cToDoAttr1+ $ bar' '▰' '▱' $ _x p+ cBar2 = overrideAttr P.progressCompleteAttr cDoneAttr2 $+ overrideAttr P.progressIncompleteAttr cToDoAttr2+ $ bar' '|' '─' $ _y p+ cBar3 = overrideAttr P.progressCompleteAttr cDoneAttr $+ overrideAttr P.progressIncompleteAttr cToDoAttr+ $ bar' '⣿' '⠶' $ _z p+ lbl c = if _showLabel p+ then Just $ " " ++ (show $ fromEnum $ c * 100) ++ " "+ else Nothing+ bar v = P.progressBar (lbl v) v+ bar' cc ic v = P.customProgressBar cc ic (lbl v) v+ ui = (str "X: " <+> xBar) <=>+ (str "Y: " <+> yBar) <=>+ (str "Z: " <+> zBar) <=>+ (str "X: " <+> cBar1) <=>+ (str "Y: " <+> cBar2) <=>+ (str "Z: " <+> cBar3) <=>+ str "" <=>+ (strWrap $ concat [ "Hit 'x', 'y', or 'z' to advance progress,"+ , "'t' to toggle labels, 'r' to revert values, "+ , "'Ctrl + r' to reset values or 'q' to quit"+ ])++appEvent :: T.BrickEvent () e -> T.EventM () (MyAppState ()) ()+appEvent (T.VtyEvent e) =+ let valid = clamp (0.0 :: Float) 1.0+ in case e of+ V.EvKey (V.KChar 'x') [] -> x %= valid . (+ 0.05)+ V.EvKey (V.KChar 'y') [] -> y %= valid . (+ 0.03)+ V.EvKey (V.KChar 'z') [] -> z %= valid . (+ 0.02)+ V.EvKey (V.KChar 't') [] -> showLabel %= not+ V.EvKey (V.KChar 'r') [V.MCtrl] -> do+ x .= 0+ y .= 0+ z .= 0+ V.EvKey (V.KChar 'r') [] -> T.put initialState+ V.EvKey (V.KChar 'q') [] -> M.halt+ _ -> return ()+appEvent _ = return ()++initialState :: MyAppState ()+initialState = MyAppState 0.25 0.18 0.63 True++theBaseAttr :: A.AttrName+theBaseAttr = A.attrName "theBase"++xDoneAttr, xToDoAttr :: A.AttrName+xDoneAttr = theBaseAttr <> A.attrName "X:done"+xToDoAttr = theBaseAttr <> A.attrName "X:remaining"++yDoneAttr, yToDoAttr :: A.AttrName+yDoneAttr = theBaseAttr <> A.attrName "Y:done"+yToDoAttr = theBaseAttr <> A.attrName "Y:remaining"++zDoneAttr, zToDoAttr :: A.AttrName+zDoneAttr = theBaseAttr <> A.attrName "Z:done"+zToDoAttr = theBaseAttr <> A.attrName "Z:remaining"++cDoneAttr, cToDoAttr :: A.AttrName+cDoneAttr = A.attrName "C:done"+cToDoAttr = A.attrName "C:remaining"++cDoneAttr1, cToDoAttr1 :: A.AttrName+cDoneAttr1 = A.attrName "C1:done"+cToDoAttr1 = A.attrName "C1:remaining"++cDoneAttr2, cToDoAttr2 :: A.AttrName+cDoneAttr2 = A.attrName "C2:done"+cToDoAttr2 = A.attrName "C2:remaining"++theMap :: A.AttrMap+theMap = A.attrMap V.defAttr+ [ (theBaseAttr, bg V.brightBlack)+ , (xDoneAttr, V.black `on` V.white)+ , (xToDoAttr, V.white `on` V.black)+ , (yDoneAttr, V.magenta `on` V.yellow)+ , (zDoneAttr, V.blue `on` V.green)+ , (zToDoAttr, V.blue `on` V.red)+ , (cDoneAttr, fg V.blue)+ , (cToDoAttr, fg V.blue)+ , (cDoneAttr1, fg V.red)+ , (cToDoAttr1, fg V.brightWhite)+ , (cDoneAttr2, fg V.green)+ , (cToDoAttr2, fg V.brightGreen)+ , (P.progressIncompleteAttr, fg V.yellow)+ ]++theApp :: M.App (MyAppState ()) e ()+theApp =+ M.App { M.appDraw = drawUI+ , M.appChooseCursor = M.showFirstCursor+ , M.appHandleEvent = appEvent+ , M.appStartEvent = return ()+ , M.appAttrMap = const theMap+ }++main :: IO ()+main = void $ M.defaultMain theApp initialState
+ programs/ReadmeDemo.hs view
@@ -0,0 +1,16 @@+module Main where++import Brick (Widget, simpleMain, (<+>), str, withBorderStyle, joinBorders)+import Brick.Widgets.Center (center)+import Brick.Widgets.Border (borderWithLabel, vBorder)+import Brick.Widgets.Border.Style (unicode)++ui :: Widget ()+ui =+ joinBorders $+ withBorderStyle unicode $+ borderWithLabel (str "Hello!") $+ (center (str "Left") <+> vBorder <+> center (str "Right"))++main :: IO ()+main = simpleMain ui
programs/SuspendAndResumeDemo.hs view
@@ -1,21 +1,27 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE CPP #-} module Main where -import Control.Lens (makeLenses, (.~), (^.), (&))+import Lens.Micro ((^.))+import Lens.Micro.TH (makeLenses) import Control.Monad (void)+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid-import Data.Default+#endif import qualified Graphics.Vty as V import Brick.Main ( App(..), neverShowCursor, defaultMain- , suspendAndResume, halt, continue+ , suspendAndResume, halt )+import Brick.AttrMap+ ( attrMap+ ) import Brick.Types ( Widget , EventM- , Next+ , BrickEvent(..) ) import Brick.Widgets.Core ( vBox@@ -28,38 +34,38 @@ makeLenses ''St -drawUI :: St -> [Widget]+drawUI :: St -> [Widget ()] drawUI st = [ui] where ui = vBox [ str $ "External input: \"" <> st^.stExternalInput <> "\"" , str "(Press Esc to quit or Space to ask for input)" ] -appEvent :: St -> V.Event -> EventM (Next St)-appEvent st e =+appEvent :: BrickEvent () e -> EventM () St ()+appEvent (VtyEvent e) = case e of- V.EvKey V.KEsc [] -> halt st+ V.EvKey V.KEsc [] -> halt V.EvKey (V.KChar ' ') [] -> suspendAndResume $ do putStrLn "Suspended. Please enter something and press enter to resume:" s <- getLine- return $ st & stExternalInput .~ s- _ -> continue st+ return $ St { _stExternalInput = s }+ _ -> return ()+appEvent _ = return () initialState :: St initialState = St { _stExternalInput = "" } -theApp :: App St V.Event+theApp :: App St e () theApp = App { appDraw = drawUI , appChooseCursor = neverShowCursor , appHandleEvent = appEvent- , appStartEvent = return- , appAttrMap = const def- , appLiftVtyEvent = id+ , appStartEvent = return ()+ , appAttrMap = const $ attrMap V.defAttr [] } main :: IO ()-main = do+main = void $ defaultMain theApp initialState
+ programs/TableDemo.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+module Main where++#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import Brick+import Brick.Widgets.Table+import Brick.Widgets.Center (center)++ui :: Widget ()+ui = center $ renderTable leftTable <+>+ padLeft (Pad 5) (renderTable rightTableA <=>+ renderTable rightTableB <=>+ renderTable rightTableC)++innerTable :: Table ()+innerTable =+ surroundingBorder False $+ table [ [txt "inner", txt "table"]+ , [txt "is", txt "here"]+ ]++leftTable :: Table ()+leftTable =+ alignCenter 1 $+ alignRight 2 $+ alignMiddle 2 $+ table [ [txt "Left", txt "Center", txt "Right"]+ , [txt "X", txt "Some things", txt "A"]+ , [renderTable innerTable, txt "are", txt "B"]+ , [txt "Z", txt "centered", txt "C"]+ ]++rightTableA :: Table ()+rightTableA =+ rowBorders False $+ setDefaultColAlignment AlignCenter $+ table [ [txt "A", txt "without"]+ , [txt "table", txt "row borders"]+ ]++rightTableB :: Table ()+rightTableB =+ columnBorders False $+ setDefaultColAlignment AlignCenter $+ table [ [txt "A", txt "table"]+ , [txt "without", txt "column borders"]+ ]++rightTableC :: Table ()+rightTableC =+ surroundingBorder False $+ rowBorders False $+ columnBorders False $+ setDefaultColAlignment AlignCenter $+ table [ [txt "A", txt "table"]+ , [txt "without", txt "any borders"]+ ]++main :: IO ()+main = simpleMain ui
+ programs/TabularListDemo.hs view
@@ -0,0 +1,146 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE CPP #-}+module Main where++import Lens.Micro ((^.))+import Lens.Micro.Mtl+import Lens.Micro.TH+import Control.Monad (void)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Graphics.Vty as V++import qualified Brick.Main as M+import qualified Brick.Types as T+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.List as L+import qualified Brick.Widgets.Center as C+import qualified Brick.Widgets.Table as Table+import qualified Brick.AttrMap as A+import qualified Data.Vector as Vec+import Brick.Types+ ( Widget+ )+import Brick.Widgets.Core+ ( (<=>)+ , str+ , vLimit+ , hLimit+ , vBox+ , hBox+ , withDefAttr+ )+import Brick.Util (on)++data Row = Row String String String++data AppState =+ AppState { _tabularList :: L.List () Row+ , _colIndex :: Int+ }++makeLenses ''AppState++drawUI :: AppState -> [Widget ()]+drawUI s = [ui]+ where+ l = s^.tabularList+ label = str $ "Row " <> cur <> " / col " <> show (s^.colIndex + 1)+ cur = case l^.(L.listSelectedL) of+ Nothing -> "-"+ Just i -> show (i + 1)+ box = B.borderWithLabel label $+ hLimit totalWidth $+ vLimit 15 $+ listDrawElement 0 False headerRow <=>+ L.renderList (listDrawElement (s^.colIndex)) True l+ ui = C.vCenter $ vBox [ C.hCenter box+ , str " "+ , C.hCenter $ str "Press +/- to add/remove list elements."+ , C.hCenter $ str "Use arrow keys to change selection."+ , C.hCenter $ str "Press Esc to exit."+ ]++appEvent :: T.BrickEvent () e -> T.EventM () AppState ()+appEvent (T.VtyEvent e) =+ case e of+ V.EvKey (V.KChar '+') [] -> do+ els <- use (tabularList.L.listElementsL)+ let el = Row (show pos) (show $ pos * 3) (show $ pos * 9)+ pos = Vec.length els+ tabularList %= L.listInsert pos el++ V.EvKey (V.KChar '-') [] -> do+ sel <- use (tabularList.L.listSelectedL)+ case sel of+ Nothing -> return ()+ Just i -> tabularList %= L.listRemove i++ V.EvKey V.KLeft [] ->+ colIndex %= (\i -> max 0 (i - 1))+ V.EvKey V.KRight [] ->+ colIndex %= (\i -> min (length columnAlignments - 1) (i + 1))++ V.EvKey V.KEsc [] -> M.halt++ ev -> T.zoom tabularList $ L.handleListEvent ev+appEvent _ = return ()++listDrawElement :: Int -> Bool -> Row -> Widget ()+listDrawElement colIdx sel (Row a b c) =+ let ws = [str a, str b, str c]+ maybeSelect es = selectCell <$> zip [0..] es+ selectCell (i, w) = if sel && i == colIdx+ then withDefAttr selectedCellAttr w+ else w+ in hLimit totalWidth $+ hBox $+ maybeSelect $+ Table.alignColumns columnAlignments columnWidths ws++initialState :: AppState+initialState =+ AppState { _tabularList = L.list () (Vec.fromList initialRows) 1+ , _colIndex = 0+ }++selectedCellAttr :: A.AttrName+selectedCellAttr = A.attrName "selectedCell"++theMap :: A.AttrMap+theMap = A.attrMap V.defAttr+ [ (L.listAttr, V.white `on` V.blue)+ , (selectedCellAttr, V.blue `on` V.white)+ ]++columnWidths :: [Int]+columnWidths = [10, 15, 20]++totalWidth :: Int+totalWidth = sum columnWidths++headerRow :: Row+headerRow = Row "Col 1" "Col 2" "Col 3"++columnAlignments :: [Table.ColumnAlignment]+columnAlignments = [Table.AlignLeft, Table.AlignCenter, Table.AlignRight]++initialRows :: [Row]+initialRows =+ [ Row "one" "two" "three"+ , Row "foo" "bar" "baz"+ , Row "stuff" "things" "blah"+ ]++theApp :: M.App AppState e ()+theApp =+ M.App { M.appDraw = drawUI+ , M.appChooseCursor = M.showFirstCursor+ , M.appHandleEvent = appEvent+ , M.appStartEvent = return ()+ , M.appAttrMap = const theMap+ }++main :: IO ()+main = void $ M.defaultMain theApp initialState
+ programs/TailDemo.hs view
@@ -0,0 +1,151 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+module Main where++#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Data.Text as T+import Control.Monad (void)+import Control.Concurrent+import Lens.Micro.TH+import Lens.Micro.Mtl+import System.Random++import Brick+import Brick.BChan+import Brick.Widgets.Border+import qualified Graphics.Vty as V++data AppState =+ AppState { _textAreaHeight :: Int+ , _textAreaWidth :: Int+ , _textAreaContents :: [T.Text]+ }++makeLenses ''AppState++draw :: AppState -> Widget n+draw st =+ header st <=> box st++header :: AppState -> Widget n+header st =+ padBottom (Pad 1) $+ hBox [ padRight (Pad 7) $+ (str $ "Max width: " <> show (_textAreaWidth st)) <=>+ (str "Left(-)/Right(+)")+ , (str $ "Max height: " <> show (_textAreaHeight st)) <=>+ (str "Down(-)/Up(+)")+ ]++box :: AppState -> Widget n+box st =+ border $+ hLimit (_textAreaWidth st) $+ vLimit (_textAreaHeight st) $+ (renderBottomUp (txtWrap <$> _textAreaContents st))++-- | Given a list of widgets, draw them bottom-up in a vertical+-- arrangement, i.e., the first widget in this list will appear at the+-- bottom of the rendering area. Rendering stops when the rendering area+-- is full, i.e., items that cannot be rendered are never evaluated or+-- drawn.+renderBottomUp :: [Widget n] -> Widget n+renderBottomUp ws =+ Widget Greedy Greedy $ do+ let go _ [] = return V.emptyImage+ go remainingHeight (c:cs) = do+ cResult <- render c+ let img = image cResult+ newRemainingHeight = remainingHeight - V.imageHeight img+ if newRemainingHeight == 0+ then return img+ else if newRemainingHeight < 0+ then return $ V.cropTop remainingHeight img+ else do+ rest <- go newRemainingHeight cs+ return $ V.vertCat [rest, img]++ ctx <- getContext+ img <- go (availHeight ctx) ws+ render $ fill ' ' <=> raw img++textLines :: [T.Text]+textLines =+ [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut"+ , "labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco"+ , "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit"+ , "in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat"+ , "cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."+ ]++handleEvent :: BrickEvent n CustomEvent -> EventM n AppState ()+handleEvent (AppEvent (NewLine l)) =+ textAreaContents %= (l :)+handleEvent (VtyEvent (V.EvKey V.KUp [])) =+ textAreaHeight %= (+ 1)+handleEvent (VtyEvent (V.EvKey V.KDown [])) =+ textAreaHeight %= max 0 . subtract 1+handleEvent (VtyEvent (V.EvKey V.KRight [])) =+ textAreaWidth %= (+ 1)+handleEvent (VtyEvent (V.EvKey V.KLeft [])) =+ textAreaWidth %= max 0 . subtract 1+handleEvent (VtyEvent (V.EvKey V.KEsc [])) =+ halt+handleEvent _ =+ return ()++data CustomEvent =+ NewLine T.Text++app :: App AppState CustomEvent ()+app =+ App { appDraw = (:[]) . draw+ , appChooseCursor = neverShowCursor+ , appHandleEvent = handleEvent+ , appAttrMap = const $ attrMap V.defAttr []+ , appStartEvent = return ()+ }++initialState :: AppState+initialState =+ AppState { _textAreaHeight = 20+ , _textAreaWidth = 40+ , _textAreaContents = []+ }++-- | Run forever, generating new lines of text for the application+-- window, prefixed with a line number. This function simulates the kind+-- of behavior that you'd get from running 'tail -f' on a file.+generateLines :: BChan CustomEvent -> IO ()+generateLines chan = go (1::Integer)+ where+ go lineNum = do+ -- Wait a random amount of time (in milliseconds)+ let delayOptions = [500, 1000, 2000]+ delay <- randomVal delayOptions+ threadDelay $ delay * 1000++ -- Choose a random line of text from our collection+ l <- randomVal textLines++ -- Send it to the application to be added to the UI+ writeBChan chan $ NewLine $ (T.pack $ "Line " <> show lineNum <> " - ") <> l++ go $ lineNum + 1++randomVal :: [a] -> IO a+randomVal as = do+ idx <- randomRIO (0, length as - 1)+ return $ as !! idx++main :: IO ()+main = do+ chan <- newBChan 10++ -- Run thread to simulate incoming data+ void $ forkIO $ generateLines chan++ (_, vty) <- customMainWithDefaultVty (Just chan) app initialState+ V.shutdown vty
+ programs/TextWrapDemo.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE CPP #-}+module Main where++#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import Brick+import Text.Wrap (defaultWrapSettings, preserveIndentation)++ui :: Widget ()+ui =+ t1 <=> (padTop (Pad 1) t2)+ where+ t1 = strWrap $ "Hello, world! This line is long enough that " <>+ "it's likely to wrap on your terminal if your window " <>+ "isn't especially wide. Try narrowing and widening " <>+ "the window to see what happens to this text."+ settings = defaultWrapSettings { preserveIndentation = True }+ t2 = strWrapWith settings $+ "This text wraps\n" <>+ " with different settings to preserve indentation\n" <>+ " so that long lines wrap in nicer way."++main :: IO ()+main = simpleMain ui
+ programs/ThemeDemo.hs view
@@ -0,0 +1,86 @@+module Main where++import Control.Monad (void)+import Control.Monad.State (put)+import Graphics.Vty+ ( white, blue, green, yellow, black, magenta+ , Event(EvKey)+ , Key(KChar, KEsc)+ )++import Brick.Main+import Brick.Themes+ ( Theme+ , newTheme+ , themeToAttrMap+ )+import Brick.Types+ ( Widget+ , BrickEvent(VtyEvent)+ , EventM+ )+import Brick.Widgets.Center+ ( hCenter+ , center+ )+import Brick.Widgets.Core+ ( (<+>)+ , vBox+ , str+ , hLimit+ , withDefAttr+ )+import Brick.Util (on, fg)+import Brick.AttrMap (AttrName, attrName)++ui :: Widget n+ui =+ center $+ hLimit 40 $+ vBox $ hCenter <$>+ [ str "Press " <+> (withDefAttr keybindingAttr $ str "1") <+> str " to switch to theme 1."+ , str "Press " <+> (withDefAttr keybindingAttr $ str "2") <+> str " to switch to theme 2."+ ]++keybindingAttr :: AttrName+keybindingAttr = attrName "keybinding"++theme1 :: Theme+theme1 =+ newTheme (white `on` blue)+ [ (keybindingAttr, fg magenta)+ ]++theme2 :: Theme+theme2 =+ newTheme (green `on` black)+ [ (keybindingAttr, fg yellow)+ ]++appEvent :: BrickEvent () e -> EventM () Int ()+appEvent (VtyEvent (EvKey (KChar '1') [])) = put 1+appEvent (VtyEvent (EvKey (KChar '2') [])) = put 2+appEvent (VtyEvent (EvKey (KChar 'q') [])) = halt+appEvent (VtyEvent (EvKey KEsc [])) = halt+appEvent _ = return ()++app :: App Int e ()+app =+ App { appDraw = const [ui]+ , appHandleEvent = appEvent+ , appStartEvent = return ()+ , appAttrMap = \s ->+ -- Note that in practice this is not ideal: we don't want+ -- to build an attribute map from a theme every time this is+ -- invoked, because it gets invoked once per redraw. Instead+ -- we'd build the attribute map at startup and store it in+ -- the application state. Here I just use themeToAttrMap to+ -- show the mechanics of the API.+ themeToAttrMap $ if s == 1+ then theme1+ else theme2+ , appChooseCursor = neverShowCursor+ }++main :: IO ()+main = void $ defaultMain app 1
programs/ViewportScrollDemo.hs view
@@ -1,11 +1,11 @@-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Main where -import Control.Applicative import Control.Monad (void)-import Data.Monoid-import Data.Default+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif import qualified Graphics.Vty as V import qualified Brick.Types as T@@ -16,6 +16,9 @@ ( Widget , ViewportType(Horizontal, Vertical, Both) )+import Brick.AttrMap+ ( attrMap+ ) import Brick.Widgets.Core ( hLimit , vLimit@@ -25,60 +28,55 @@ , str ) -vp1Name :: T.Name-vp1Name = "demo1"--vp2Name :: T.Name-vp2Name = "demo2"--vp3Name :: T.Name-vp3Name = "demo3"+data Name = VP1+ | VP2+ | VP3+ deriving (Ord, Show, Eq) -drawUi :: () -> [Widget]+drawUi :: () -> [Widget Name] drawUi = const [ui] where ui = C.center $ B.border $ hLimit 60 $ vLimit 21 $ vBox [ pair, B.hBorder, singleton ]- singleton = viewport vp3Name Both $+ singleton = viewport VP3 Both $ vBox $ str "Press ctrl-arrow keys to scroll this viewport horizontally and vertically." : (str <$> [ "Line " <> show i | i <- [2..25::Int] ])- pair = hBox [ viewport vp1Name Vertical $+ pair = hBox [ viewport VP1 Vertical $ vBox $ str "Press up and down arrow keys" : str "to scroll this viewport." : (str <$> [ "Line " <> (show i) | i <- [3..50::Int] ]) , B.vBorder- , viewport vp2Name Horizontal $+ , viewport VP2 Horizontal $ str "Press left and right arrow keys to scroll this viewport." ] -vp1Scroll :: M.ViewportScroll-vp1Scroll = M.viewportScroll vp1Name+vp1Scroll :: M.ViewportScroll Name+vp1Scroll = M.viewportScroll VP1 -vp2Scroll :: M.ViewportScroll-vp2Scroll = M.viewportScroll vp2Name+vp2Scroll :: M.ViewportScroll Name+vp2Scroll = M.viewportScroll VP2 -vp3Scroll :: M.ViewportScroll-vp3Scroll = M.viewportScroll vp3Name+vp3Scroll :: M.ViewportScroll Name+vp3Scroll = M.viewportScroll VP3 -appEvent :: () -> V.Event -> T.EventM (T.Next ())-appEvent _ (V.EvKey V.KDown [V.MCtrl]) = M.vScrollBy vp3Scroll 1 >> M.continue ()-appEvent _ (V.EvKey V.KUp [V.MCtrl]) = M.vScrollBy vp3Scroll (-1) >> M.continue ()-appEvent _ (V.EvKey V.KRight [V.MCtrl]) = M.hScrollBy vp3Scroll 1 >> M.continue ()-appEvent _ (V.EvKey V.KLeft [V.MCtrl]) = M.hScrollBy vp3Scroll (-1) >> M.continue ()-appEvent _ (V.EvKey V.KDown []) = M.vScrollBy vp1Scroll 1 >> M.continue ()-appEvent _ (V.EvKey V.KUp []) = M.vScrollBy vp1Scroll (-1) >> M.continue ()-appEvent _ (V.EvKey V.KRight []) = M.hScrollBy vp2Scroll 1 >> M.continue ()-appEvent _ (V.EvKey V.KLeft []) = M.hScrollBy vp2Scroll (-1) >> M.continue ()-appEvent _ (V.EvKey V.KEsc []) = M.halt ()-appEvent _ _ = M.continue ()+appEvent :: T.BrickEvent Name e -> T.EventM Name () ()+appEvent (T.VtyEvent (V.EvKey V.KDown [V.MCtrl])) = M.vScrollBy vp3Scroll 1+appEvent (T.VtyEvent (V.EvKey V.KUp [V.MCtrl])) = M.vScrollBy vp3Scroll (-1)+appEvent (T.VtyEvent (V.EvKey V.KRight [V.MCtrl])) = M.hScrollBy vp3Scroll 1+appEvent (T.VtyEvent (V.EvKey V.KLeft [V.MCtrl])) = M.hScrollBy vp3Scroll (-1)+appEvent (T.VtyEvent (V.EvKey V.KDown [])) = M.vScrollBy vp1Scroll 1+appEvent (T.VtyEvent (V.EvKey V.KUp [])) = M.vScrollBy vp1Scroll (-1)+appEvent (T.VtyEvent (V.EvKey V.KRight [])) = M.hScrollBy vp2Scroll 1+appEvent (T.VtyEvent (V.EvKey V.KLeft [])) = M.hScrollBy vp2Scroll (-1)+appEvent (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt+appEvent _ = return () -app :: M.App () V.Event+app :: M.App () e Name app = M.App { M.appDraw = drawUi- , M.appStartEvent = return+ , M.appStartEvent = return () , M.appHandleEvent = appEvent- , M.appAttrMap = const def- , M.appLiftVtyEvent = id+ , M.appAttrMap = const $ attrMap V.defAttr [] , M.appChooseCursor = M.neverShowCursor }
+ programs/ViewportScrollbarsDemo.hs view
@@ -0,0 +1,176 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+module Main where++import Lens.Micro.TH+import Lens.Micro.Mtl+import Control.Monad (void)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Graphics.Vty as V+import Graphics.Vty.CrossPlatform (mkVty)++import qualified Brick.Types as T+import qualified Brick.Main as M+import qualified Brick.Widgets.Center as C+import qualified Brick.Widgets.Border as B+import Brick.Types+ ( Widget+ , ViewportType(Horizontal, Both)+ , VScrollBarOrientation(..)+ , HScrollBarOrientation(..)+ )+import Brick.Util+ ( fg+ )+import Brick.AttrMap+ ( AttrMap+ , attrMap+ )+import Brick.Widgets.Core+ ( Padding(..)+ , hLimit+ , vLimit+ , padRight+ , hBox+ , vBox+ , viewport+ , str+ , fill+ , withVScrollBars+ , withHScrollBars+ , withHScrollBarRenderer+ , withVScrollBarRenderer+ , withVScrollBarHandles+ , withHScrollBarHandles+ , withClickableHScrollBars+ , withClickableVScrollBars+ , VScrollbarRenderer(..)+ , HScrollbarRenderer(..)+ , scrollbarAttr+ , scrollbarHandleAttr+ )++customHScrollbars :: HScrollbarRenderer n+customHScrollbars =+ HScrollbarRenderer { renderHScrollbar = vLimit 1 $ fill '^'+ , renderHScrollbarTrough = vLimit 1 $ fill ' '+ , renderHScrollbarHandleBefore = str "<<"+ , renderHScrollbarHandleAfter = str ">>"+ , scrollbarHeightAllocation = 2+ }++customVScrollbars :: VScrollbarRenderer n+customVScrollbars =+ VScrollbarRenderer { renderVScrollbar = C.hCenter $ hLimit 1 $ fill '*'+ , renderVScrollbarTrough = fill ' '+ , renderVScrollbarHandleBefore = C.hCenter $ str "-^-"+ , renderVScrollbarHandleAfter = C.hCenter $ str "-v-"+ , scrollbarWidthAllocation = 5+ }++data Name = VP1 | VP2 | SBClick T.ClickableScrollbarElement Name+ deriving (Ord, Show, Eq)++data St = St { _lastClickedElement :: Maybe (T.ClickableScrollbarElement, Name) }++makeLenses ''St++drawUi :: St -> [Widget Name]+drawUi st = [ui]+ where+ ui = C.center $ hLimit 80 $ vLimit 21 $+ (vBox [ pair+ , C.hCenter (str "Last clicked scroll bar element:")+ , str $ show $ _lastClickedElement st+ ])+ pair = hBox [ padRight (Pad 5) $+ B.border $+ withClickableHScrollBars SBClick $+ withHScrollBars OnBottom $+ withHScrollBarRenderer customHScrollbars $+ withHScrollBarHandles $+ viewport VP1 Horizontal $+ str $ "Press left and right arrow keys to scroll this viewport.\n" <>+ "This viewport uses a\n" <>+ "custom scroll bar renderer!"+ , B.border $+ withClickableVScrollBars SBClick $+ withVScrollBars OnLeft $+ withVScrollBarRenderer customVScrollbars $+ withVScrollBarHandles $+ viewport VP2 Both $+ vBox $+ (str $ unlines $+ [ "Press up and down arrow keys to"+ , "scroll this viewport vertically."+ , "This viewport uses a custom"+ , "scroll bar renderer with"+ , "a larger space allocation and"+ , "even more fancy rendering."+ ])+ : (str <$> [ "Line " <> show i | i <- [2..55::Int] ])+ ]++vp1Scroll :: M.ViewportScroll Name+vp1Scroll = M.viewportScroll VP1++vp2Scroll :: M.ViewportScroll Name+vp2Scroll = M.viewportScroll VP2++appEvent :: T.BrickEvent Name e -> T.EventM Name St ()+appEvent (T.VtyEvent (V.EvKey V.KRight [])) = M.hScrollBy vp1Scroll 1+appEvent (T.VtyEvent (V.EvKey V.KLeft [])) = M.hScrollBy vp1Scroll (-1)+appEvent (T.VtyEvent (V.EvKey V.KDown [])) = M.vScrollBy vp2Scroll 1+appEvent (T.VtyEvent (V.EvKey V.KUp [])) = M.vScrollBy vp2Scroll (-1)+appEvent (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt+appEvent (T.MouseDown (SBClick el n) _ _ _) = do+ lastClickedElement .= Just (el, n)+ case n of+ VP1 -> do+ let vp = M.viewportScroll VP1+ case el of+ T.SBHandleBefore -> M.hScrollBy vp (-1)+ T.SBHandleAfter -> M.hScrollBy vp 1+ T.SBTroughBefore -> M.hScrollBy vp (-10)+ T.SBTroughAfter -> M.hScrollBy vp 10+ T.SBBar -> return ()+ VP2 -> do+ let vp = M.viewportScroll VP2+ case el of+ T.SBHandleBefore -> M.vScrollBy vp (-1)+ T.SBHandleAfter -> M.vScrollBy vp 1+ T.SBTroughBefore -> M.vScrollBy vp (-10)+ T.SBTroughAfter -> M.vScrollBy vp 10+ T.SBBar -> return ()+ _ ->+ return ()+appEvent _ = return ()++theme :: AttrMap+theme =+ attrMap V.defAttr+ [ (scrollbarAttr, fg V.white)+ , (scrollbarHandleAttr, fg V.brightYellow)+ ]++app :: M.App St e Name+app =+ M.App { M.appDraw = drawUi+ , M.appStartEvent = return ()+ , M.appHandleEvent = appEvent+ , M.appAttrMap = const theme+ , M.appChooseCursor = M.neverShowCursor+ }++main :: IO ()+main = do+ let buildVty = do+ v <- mkVty V.defaultConfig+ V.setMode (V.outputIface v) V.Mouse True+ return v++ initialVty <- buildVty+ void $ M.customMain initialVty buildVty Nothing app (St Nothing)
programs/VisibilityDemo.hs view
@@ -1,17 +1,21 @@-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE CPP #-} module Main where import Control.Monad (void)-import Control.Lens+import Lens.Micro+import Lens.Micro.TH+import Lens.Micro.Mtl+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif import qualified Graphics.Vty as V import qualified Brick.Types as T import qualified Brick.Main as M import qualified Brick.Widgets.Center as C import qualified Brick.Widgets.Border as B-import Brick.AttrMap (AttrMap, AttrName, attrMap)+import Brick.AttrMap (AttrMap, AttrName, attrMap, attrName) import Brick.Util (on) import Brick.Types ( Widget@@ -36,28 +40,24 @@ makeLenses ''St -vp1Name :: T.Name-vp1Name = "demo1"+data Name = VP1+ | VP2+ | VP3+ deriving (Show, Ord, Eq) vp1Size :: Int vp1Size = 15 -vp2Name :: T.Name-vp2Name = "demo2"- vp2Size :: Int vp2Size = 15 -vp3Name :: T.Name-vp3Name = "demo3"- vp3Size :: (Int, Int) vp3Size = (25, 25) selectedAttr :: AttrName-selectedAttr = "selected"+selectedAttr = attrName "selected" -drawUi :: St -> [Widget]+drawUi :: St -> [Widget Name] drawUi st = [ui] where ui = C.center $ hLimit 60 $ vLimit 30 $@@ -66,7 +66,7 @@ "- Left/right arrow keys scroll the top-right viewport\n" <> "- Ctrl-arrow keys move the bottom viewport" ]- singleton = viewport vp3Name Both $+ singleton = viewport VP3 Both $ vBox $ do i <- [1..vp3Size^._1] let row = do@@ -78,14 +78,14 @@ return $ hBox row pair = hBox [ vp1, B.vBorder, vp2 ]- vp1 = viewport vp1Name Vertical $+ vp1 = viewport VP1 Vertical $ vBox $ do i <- [1..vp1Size] let mkItem = if i == st^.vp1Index then withAttr selectedAttr . visible else id return $ mkItem $ str $ "Item " <> show i- vp2 = viewport vp2Name Horizontal $+ vp2 = viewport VP2 Horizontal $ hBox $ do i <- [1..vp2Size] let mkItem = if i == st^.vp2Index@@ -93,39 +93,38 @@ else id return $ mkItem $ str $ "Item " <> show i <> " " -vp1Scroll :: M.ViewportScroll-vp1Scroll = M.viewportScroll vp1Name+vp1Scroll :: M.ViewportScroll Name+vp1Scroll = M.viewportScroll VP1 -vp2Scroll :: M.ViewportScroll-vp2Scroll = M.viewportScroll vp2Name+vp2Scroll :: M.ViewportScroll Name+vp2Scroll = M.viewportScroll VP2 -vp3Scroll :: M.ViewportScroll-vp3Scroll = M.viewportScroll vp3Name+vp3Scroll :: M.ViewportScroll Name+vp3Scroll = M.viewportScroll VP3 -appEvent :: St -> V.Event -> T.EventM (T.Next St)-appEvent st (V.EvKey V.KDown [V.MCtrl]) = M.continue $ st & vp3Index._1 %~ min (vp3Size^._1) . (+ 1)-appEvent st (V.EvKey V.KUp [V.MCtrl]) = M.continue $ st & vp3Index._1 %~ max 1 . subtract 1-appEvent st (V.EvKey V.KRight [V.MCtrl]) = M.continue $ st & vp3Index._2 %~ min (vp3Size^._1) . (+ 1)-appEvent st (V.EvKey V.KLeft [V.MCtrl]) = M.continue $ st & vp3Index._2 %~ max 1 . subtract 1-appEvent st (V.EvKey V.KDown []) = M.continue $ st & vp1Index %~ min vp1Size . (+ 1)-appEvent st (V.EvKey V.KUp []) = M.continue $ st & vp1Index %~ max 1 . subtract 1-appEvent st (V.EvKey V.KRight []) = M.continue $ st & vp2Index %~ min vp2Size . (+ 1)-appEvent st (V.EvKey V.KLeft []) = M.continue $ st & vp2Index %~ max 1 . subtract 1-appEvent st (V.EvKey V.KEsc []) = M.halt st-appEvent st _ = M.continue st+appEvent :: T.BrickEvent Name e -> T.EventM Name St ()+appEvent (T.VtyEvent (V.EvKey V.KDown [V.MCtrl])) = vp3Index._1 %= min (vp3Size^._1) . (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KUp [V.MCtrl])) = vp3Index._1 %= max 1 . subtract 1+appEvent (T.VtyEvent (V.EvKey V.KRight [V.MCtrl])) = vp3Index._2 %= min (vp3Size^._1) . (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KLeft [V.MCtrl])) = vp3Index._2 %= max 1 . subtract 1+appEvent (T.VtyEvent (V.EvKey V.KDown [])) = vp1Index %= min vp1Size . (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KUp [])) = vp1Index %= max 1 . subtract 1+appEvent (T.VtyEvent (V.EvKey V.KRight [])) = vp2Index %= min vp2Size . (+ 1)+appEvent (T.VtyEvent (V.EvKey V.KLeft [])) = vp2Index %= max 1 . subtract 1+appEvent (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt+appEvent _ = return () theMap :: AttrMap theMap = attrMap V.defAttr [ (selectedAttr, V.black `on` V.yellow) ] -app :: M.App St V.Event+app :: M.App St e Name app = M.App { M.appDraw = drawUi- , M.appStartEvent = return+ , M.appStartEvent = return () , M.appHandleEvent = appEvent , M.appAttrMap = const theMap- , M.appLiftVtyEvent = id , M.appChooseCursor = M.neverShowCursor }
+ programs/custom_keys.ini view
@@ -0,0 +1,4 @@+[keybindings]+quit = x+increment = i+decrement = d
src/Brick.hs view
@@ -1,5 +1,11 @@ -- | This module is provided as a convenience to import the most--- important parts of the API all at once.+-- important parts of the API all at once. If you are new to Brick and+-- are looking to learn it, the best place to start is the+-- [Brick User Guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst).+-- The README also has links to other learning resources. Unlike+-- most Haskell libraries that only have API documentation, Brick+-- is best learned by reading the User Guide and other materials and+-- referring to the API docs only as needed. Enjoy! module Brick ( module Brick.Main , module Brick.Types
+ src/Brick/Animation.hs view
@@ -0,0 +1,588 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RankNTypes #-}+-- | This module provides some infrastructure for adding animations to+-- Brick applications. See @programs/AnimationDemo.hs@ for a complete+-- working example of this API.+--+-- At a high level, this works as follows:+--+-- This module provides a threaded animation manager that manages a set+-- of running animations. The application creates the manager and starts+-- animations, which automatically loop or run once, depending on their+-- configuration. Each animation has some state in the application's+-- state that is automatically managed by the animation manager using a+-- lens-based API. Whenever animations need to be redrawn, the animation+-- manager sends a custom event with a state update to the application,+-- which must be evaluated by the main event loop to update animation+-- states. Each animation is associated with a 'Clip' -- sequence of+-- frames -- which may be static or may be built from the application+-- state at rendering time.+--+-- To use this module:+--+-- * Use a custom event type @e@ in your 'Brick.Main.App' and give the+-- event type a constructor @EventM n s () -> e@ (where @s@ and+-- @n@ are those in @App s e n@). This will require the use of+-- 'Brick.Main.customMain' and will also require the creation of a+-- 'Brick.BChan.BChan' for custom events.+--+-- * Add an 'AnimationManager' field to the application state @s@.+--+-- * Create an 'AnimationManager' at startup with+-- 'startAnimationManager', providing the custom event constructor and+-- 'BChan' created above. Store the manager in the application state.+--+-- * For each animation you want to run at any given time, add a field+-- to the application state of type @Maybe (Animation s n)@,+-- initialized to 'Nothing'. A value of 'Nothing' indicates that the+-- animation is not running.+--+-- * Ensure that each animation state field in @s@ has a lens, usually+-- by using 'Lens.Micro.TH.makeLenses'.+--+-- * Start new animations in 'EventM' with 'startAnimation'; stop them+-- with 'stopAnimation'. Supply clips for new animations with+-- 'newClip', 'newClip_', and the clip transformation functions.+--+-- * Call 'renderAnimation' in 'Brick.Main.appDraw' for each animation in the+-- application state.+--+-- * If needed, stop the animation manager with 'stopAnimationManager'.+--+-- See 'AnimationManager' and the docs for the rest of this module for+-- details.+module Brick.Animation+ ( -- * Animation managers+ AnimationManager+ , startAnimationManager+ , stopAnimationManager+ , minTickTime++ -- * Animations+ , Animation+ , animationFrameIndex++ -- * Starting and stopping animations+ , RunMode(..)+ , startAnimation+ , stopAnimation++ -- * Rendering animations+ , renderAnimation++ -- * Creating clips+ , Clip+ , newClip+ , newClip_+ , clipLength++ -- * Transforming clips+ , pingPongClip+ , reverseClip+ )+where++import Control.Concurrent (threadDelay, forkIO, ThreadId, killThread, myThreadId)+import qualified Control.Concurrent.STM as STM+import Control.Monad (forever, when)+import Control.Monad.State.Strict+import Data.Foldable (foldrM)+import Data.Hashable (Hashable)+import qualified Data.HashMap.Strict as HM+import Data.Maybe (fromMaybe)+import qualified Data.Vector as V+import Lens.Micro ((^.), (%~), (.~), (&), Traversal', _Just)+import Lens.Micro.TH (makeLenses)+import Lens.Micro.Mtl++import Brick.BChan+import Brick.Types (EventM, Widget)+import qualified Brick.Animation.Clock as C++-- | A sequence of a animation frames.+newtype Clip s n = Clip (V.Vector (s -> Widget n))+ deriving (Semigroup)++-- | Get the number of frames in a clip.+clipLength :: Clip s n -> Int+clipLength (Clip fs) = V.length fs++-- | Build a clip.+--+-- Each frame in a clip is represented by a function from a state to a+-- 'Widget'. This allows applications to determine on a per-frame basis+-- what should be drawn in an animation based on application state, if+-- desired, in the same style as 'Brick.Main.appDraw'.+--+-- If the provided list is empty, this calls 'error'.+newClip :: [s -> Widget n] -> Clip s n+newClip [] = error "clip: got an empty list"+newClip fs = Clip $ V.fromList fs++-- | Like 'newClip' but for static frames.+newClip_ :: [Widget n] -> Clip s n+newClip_ ws = newClip $ const <$> ws++-- | Extend a clip so that when the end of the original clip is reached,+-- it continues in reverse order to create a loop.+--+-- For example, if this is given a clip with frames A, B, C, and D, then+-- this returns a clip with frames A, B, C, D, C, and B.+--+-- If the given clip contains less than two frames, this is equivalent+-- to 'id'.+pingPongClip :: Clip s n -> Clip s n+pingPongClip (Clip fs) | V.length fs >= 2 =+ Clip $ fs <> V.reverse (V.init $ V.tail fs)+pingPongClip c = c++-- | Reverse a clip.+reverseClip :: Clip s n -> Clip s n+reverseClip (Clip fs) = Clip $ V.reverse fs++data AnimationManagerRequest s n =+ Tick C.Time+ | StartAnimation (Clip s n) Integer RunMode (Traversal' s (Maybe (Animation s n)))+ -- ^ Clip, frame duration in milliseconds, run mode, updater+ | StopAnimation (Animation s n)+ | Shutdown++-- | The running mode for an animation.+data RunMode =+ Once+ -- ^ Run the animation once and then end+ | Loop+ -- ^ Run the animation in a loop forever+ deriving (Eq, Show, Ord)++newtype AnimationID = AnimationID Int+ deriving (Eq, Ord, Show, Hashable)++-- | The state of a running animation.+--+-- Put one of these (wrapped in 'Maybe') in your application state for+-- each animation that you'd like to run concurrently.+data Animation s n =+ Animation { animationFrameIndex :: Int+ -- ^ The animation's current frame index, provided for+ -- convenience. Applications won't need to access this in+ -- most situations; use 'renderAnimation' instead.+ , animationID :: AnimationID+ -- ^ The animation's internally-managed ID+ , animationClip :: Clip s n+ -- ^ The animation's clip+ }++-- | Render an animation.+renderAnimation :: (s -> Widget n)+ -- ^ The fallback function to use for drawing if the+ -- animation is not running+ -> s+ -- ^ The state to provide when rendering the animation's+ -- current frame+ -> Maybe (Animation s n)+ -- ^ The animation state itself+ -> Widget n+renderAnimation fallback input mAnim =+ draw input+ where+ draw = fromMaybe fallback $ do+ a <- mAnim+ let idx = animationFrameIndex a+ Clip fs = animationClip a+ fs V.!? idx++data AnimationState s n =+ AnimationState { _animationStateID :: AnimationID+ , _animationNumFrames :: Int+ , _animationCurrentFrame :: Int+ , _animationFrameMilliseconds :: Integer+ , _animationRunMode :: RunMode+ , animationFrameUpdater :: Traversal' s (Maybe (Animation s n))+ , _animationNextFrameTime :: C.Time+ }++makeLenses ''AnimationState++-- | A manager for animations. The type variables for this type are the+-- same as those for 'Brick.Main.App'.+--+-- This asynchronously manages a set of running animations, advancing+-- each one over time. When a running animation's current frame needs+-- to be changed, the manager sends an 'EventM' update for that+-- animation to the application's event loop to perform the update to+-- the animation in the application state. The manager will batch such+-- updates if more than one animation needs to be changed at a time.+--+-- The manager has a /tick duration/ in milliseconds which is the+-- resolution at which animations are checked to see if they should+-- be updated. Animations also have their own frame duration in+-- milliseconds. For example, if a manager has a tick duration of 50+-- milliseconds and is running an animation with a frame duration of 100+-- milliseconds, then the manager will advance that animation by one+-- frame every two ticks. On the other hand, if a manager has a tick+-- duration of 100 milliseconds and is running an animation with a frame+-- duration of 50 milliseconds, the manager will advance that animation+-- by two frames on each tick.+--+-- Animation managers are started with 'startAnimationManager' and+-- stopped with 'stopAnimationManager'.+--+-- Animations are started with 'startAnimation' and stopped with+-- 'stopAnimation'. Each animation must be associated with an+-- application state field accessible with a traversal given to+-- 'startAnimation'.+--+-- When an animation is started, every time it advances a frame, and+-- when it is ended, the manager communicates these changes to the+-- application by using the custom event constructor provided to+-- 'startAnimationManager'. The manager uses that to schedule a state+-- update which the application is responsible for evaluating. The state+-- updates are built from the traversals provided to 'startAnimation'.+--+-- The manager-updated 'Animation' values in the application state are+-- then drawn with 'renderAnimation'.+--+-- Animations in 'Loop' mode are run forever until stopped with+-- 'stopAnimation'; animations in 'Once' mode run once and are removed+-- from the application state (set to 'Nothing') when they finish. All+-- state updates to the application state are performed by the manager's+-- custom event mechanism; the application never needs to directly+-- modify the 'Animation' application state fields except to initialize+-- them to 'Nothing'.+--+-- There is nothing here to prevent an application from running multiple+-- managers, each at a different tick rate. That may have performance+-- consequences, though, due to the loss of batch efficiency in state+-- updates, so we recommend using only one manager per application at a+-- sufficiently short tick duration.+data AnimationManager s e n =+ AnimationManager { animationMgrRequestThreadId :: ThreadId+ , animationMgrTickThreadId :: ThreadId+ , animationMgrOutputChan :: BChan e+ , animationMgrInputChan :: STM.TChan (AnimationManagerRequest s n)+ , animationMgrEventConstructor :: EventM n s () -> e+ , animationMgrRunning :: STM.TVar Bool+ }++tickThreadBody :: Int+ -> STM.TChan (AnimationManagerRequest s n)+ -> IO ()+tickThreadBody tickMilliseconds outChan = do+ let nextTick = C.addOffset tickOffset+ tickOffset = C.offsetFromMs $ toInteger tickMilliseconds+ go targetTime = do+ now <- C.getTime+ STM.atomically $ STM.writeTChan outChan $ Tick now++ -- threadDelay does not guarantee that we will wake up on+ -- time; it only ensures that we won't wake up earlier than+ -- requested. Since we can therefore oversleep, instead of+ -- always sleeping for tickMilliseconds (which would cause+ -- us to drift off of schedule as delays accumulate) we+ -- determine sleep time by measuring the distance between+ -- now and the next scheduled tick. This is still unreliable+ -- as we can still oversleep, but it keeps the oversleeping+ -- under control over time. It means most ticks may be+ -- slightly late (about 1-2 milliseconds is common) but this+ -- will prevent that per-tick error from accumulating.+ let nextTickTime = nextTick targetTime+ sleepMs = fromInteger $+ C.offsetToMs $+ C.subtractTime nextTickTime now++ -- threadDelay works microseconds.+ threadDelay $ sleepMs * 1000+ go nextTickTime++ go =<< C.getTime++setNextFrameTime :: C.Time -> AnimationState s n -> AnimationState s n+setNextFrameTime t a = a & animationNextFrameTime .~ t++data ManagerState s e n =+ ManagerState { _managerStateInChan :: STM.TChan (AnimationManagerRequest s n)+ , _managerStateOutChan :: BChan e+ , _managerStateEventBuilder :: EventM n s () -> e+ , _managerStateAnimations :: HM.HashMap AnimationID (AnimationState s n)+ , _managerStateIDVar :: STM.TVar AnimationID+ }++makeLenses ''ManagerState++animationManagerThreadBody :: STM.TChan (AnimationManagerRequest s n)+ -> BChan e+ -> (EventM n s () -> e)+ -> IO ()+animationManagerThreadBody inChan outChan mkEvent = do+ idVar <- STM.newTVarIO $ AnimationID 1+ let initial = ManagerState { _managerStateInChan = inChan+ , _managerStateOutChan = outChan+ , _managerStateEventBuilder = mkEvent+ , _managerStateAnimations = mempty+ , _managerStateIDVar = idVar+ }+ evalStateT runManager initial++type ManagerM s e n a = StateT (ManagerState s e n) IO a++getNextManagerRequest :: ManagerM s e n (AnimationManagerRequest s n)+getNextManagerRequest = do+ inChan <- use managerStateInChan+ liftIO $ STM.atomically $ STM.readTChan inChan++sendApplicationStateUpdate :: EventM n s () -> ManagerM s e n ()+sendApplicationStateUpdate act = do+ outChan <- use managerStateOutChan+ mkEvent <- use managerStateEventBuilder+ liftIO $ writeBChan outChan $ mkEvent act++removeAnimation :: AnimationID -> ManagerM s e n ()+removeAnimation aId =+ managerStateAnimations %= HM.delete aId++lookupAnimation :: AnimationID -> ManagerM s e n (Maybe (AnimationState s n))+lookupAnimation aId =+ HM.lookup aId <$> use managerStateAnimations++insertAnimation :: AnimationState s n -> ManagerM s e n ()+insertAnimation a =+ managerStateAnimations %= HM.insert (a^.animationStateID) a++getNextAnimationID :: ManagerM s e n AnimationID+getNextAnimationID = do+ var <- use managerStateIDVar+ liftIO $ STM.atomically $ do+ AnimationID i <- STM.readTVar var+ let next = AnimationID $ i + 1+ STM.writeTVar var next+ return $ AnimationID i++runManager :: ManagerM s e n ()+runManager = forever $ do+ getNextManagerRequest >>= handleManagerRequest++handleManagerRequest :: AnimationManagerRequest s n -> ManagerM s e n ()+handleManagerRequest (StartAnimation clip frameMs runMode updater) = do+ aId <- getNextAnimationID+ now <- liftIO C.getTime+ let next = C.addOffset frameOffset now+ frameOffset = C.offsetFromMs frameMs+ a = AnimationState { _animationStateID = aId+ , _animationNumFrames = clipLength clip+ , _animationCurrentFrame = 0+ , _animationFrameMilliseconds = frameMs+ , _animationRunMode = runMode+ , animationFrameUpdater = updater+ , _animationNextFrameTime = next+ }++ insertAnimation a+ sendApplicationStateUpdate $ updater .= Just (Animation { animationID = aId+ , animationFrameIndex = 0+ , animationClip = clip+ })+handleManagerRequest (StopAnimation a) = do+ let aId = animationID a+ mA <- lookupAnimation aId+ case mA of+ Nothing -> return ()+ Just aState -> do+ -- Remove the animation from the manager+ removeAnimation aId++ -- Set the current animation state in the application state+ -- to none+ sendApplicationStateUpdate $ clearStateAction aState+handleManagerRequest Shutdown = do+ as <- HM.elems <$> use managerStateAnimations++ let updater = sequence_ $ clearStateAction <$> as+ when (not $ null as) $ do+ sendApplicationStateUpdate updater++ liftIO $ myThreadId >>= killThread+handleManagerRequest (Tick tickTime) = do+ -- Check all animation states for frame advances+ -- based on the relationship between the tick time+ -- and each animation's next frame time+ mUpdateAct <- checkAnimations tickTime+ case mUpdateAct of+ Nothing -> return ()+ Just act -> sendApplicationStateUpdate act++clearStateAction :: AnimationState s n -> EventM n s ()+clearStateAction a = animationFrameUpdater a .= Nothing++frameUpdateAction :: AnimationState s n -> EventM n s ()+frameUpdateAction a =+ animationFrameUpdater a._Just %=+ (\an -> an { animationFrameIndex = a^.animationCurrentFrame })++updateAnimationState :: C.Time -> AnimationState s n -> AnimationState s n+updateAnimationState now a =+ let differenceMs = C.offsetToMs $+ C.subtractTime now (a^.animationNextFrameTime)+ numFrames = 1 + (differenceMs `div` (a^.animationFrameMilliseconds))+ newNextTime = C.addOffset (C.offsetFromMs $ numFrames * (a^.animationFrameMilliseconds))+ (a^.animationNextFrameTime)++ -- The new frame is obtained by advancing from the current frame by+ -- numFrames.+ in setNextFrameTime newNextTime $ advanceBy numFrames a++checkAnimations :: C.Time -> ManagerM s e n (Maybe (EventM n s ()))+checkAnimations now = do+ let go a updaters = do+ result <- checkAnimation now a+ return $ case result of+ Nothing -> updaters+ Just u -> u : updaters++ anims <- use managerStateAnimations+ updaters <- foldrM go [] anims++ case updaters of+ [] -> return Nothing+ _ -> return $ Just $ sequence_ updaters++-- For each active animation, check to see if the animation's next frame+-- time has passed. If it has, advance its frame counter as appropriate+-- and schedule its frame index to be updated in the application state.+checkAnimation :: C.Time -> AnimationState s n -> ManagerM s e n (Maybe (EventM n s ()))+checkAnimation now a+ | isFinished a = do+ -- This animation completed in a previous check, so clear it+ -- from the manager and the application state.+ removeAnimation (a^.animationStateID)+ return $ Just $ clearStateAction a+ | (now < a^.animationNextFrameTime) =+ -- This animation is not due for an update, so don't do+ -- anything.+ return Nothing+ | otherwise = do+ -- This animation is still running, so determine how many frames+ -- have elapsed for it and then advance the frame index based+ -- the elapsed time. Also set its next frame time.+ let a' = updateAnimationState now a+ managerStateAnimations %= HM.insert (a'^.animationStateID) a'+ return $ Just $ frameUpdateAction a'++isFinished :: AnimationState s n -> Bool+isFinished a =+ case a^.animationRunMode of+ Once -> a^.animationCurrentFrame == a^.animationNumFrames - 1+ Loop -> False++advanceBy :: Integer -> AnimationState s n -> AnimationState s n+advanceBy n a+ | n <= 0 = a+ | otherwise =+ advanceBy (n - 1) $+ advanceByOne a++advanceByOne :: AnimationState s n -> AnimationState s n+advanceByOne a =+ if a^.animationCurrentFrame == a^.animationNumFrames - 1+ then case a^.animationRunMode of+ Loop -> a & animationCurrentFrame .~ 0+ Once -> a+ else a & animationCurrentFrame %~ (+ 1)++-- | The minimum tick duration in milliseconds allowed by+-- 'startAnimationManager'.+minTickTime :: Int+minTickTime = 25++-- | Start a new animation manager. For full details about how managers+-- work, see 'AnimationManager'.+--+-- If the specified tick duration is less than 'minTickTime', this will+-- call 'error'. This bound is in place to prevent API misuse leading to+-- ticking so fast that the terminal can't keep up with redraws.+startAnimationManager :: (MonadIO m)+ => Int+ -- ^ The tick duration for this manager in milliseconds+ -> BChan e+ -- ^ The event channel to use to send updates to+ -- the application (i.e. the same one given to+ -- e.g. 'Brick.Main.customVty')+ -> (EventM n s () -> e)+ -- ^ A constructor for building custom events+ -- that perform application state updates. The+ -- application must evaluate these custom events'+ -- 'EventM' actions in order to record animation+ -- updates in the application state.+ -> m (AnimationManager s e n)+startAnimationManager tickMilliseconds _ _ | tickMilliseconds < minTickTime =+ error $ "startAnimationManager: tick duration too small (minimum is " <> show minTickTime <> ")"+startAnimationManager tickMilliseconds outChan mkEvent = liftIO $ do+ inChan <- STM.newTChanIO+ reqTid <- forkIO $ animationManagerThreadBody inChan outChan mkEvent+ tickTid <- forkIO $ tickThreadBody tickMilliseconds inChan+ runningVar <- STM.newTVarIO True+ return $ AnimationManager { animationMgrRequestThreadId = reqTid+ , animationMgrTickThreadId = tickTid+ , animationMgrEventConstructor = mkEvent+ , animationMgrOutputChan = outChan+ , animationMgrInputChan = inChan+ , animationMgrRunning = runningVar+ }++-- | Execute the specified action only when this manager is running.+whenRunning :: (MonadIO m) => AnimationManager s e n -> IO () -> m ()+whenRunning mgr act = do+ running <- liftIO $ STM.atomically $ STM.readTVar (animationMgrRunning mgr)+ when running $ liftIO act++-- | Stop the animation manager, ending all running animations.+stopAnimationManager :: (MonadIO m) => AnimationManager s e n -> m ()+stopAnimationManager mgr =+ whenRunning mgr $ do+ tellAnimationManager mgr Shutdown+ killThread $ animationMgrTickThreadId mgr+ STM.atomically $ STM.writeTVar (animationMgrRunning mgr) False++-- | Send a request to an animation manager.+tellAnimationManager :: (MonadIO m)+ => AnimationManager s e n+ -- ^ The manager+ -> AnimationManagerRequest s n+ -- ^ The request to send+ -> m ()+tellAnimationManager mgr req =+ liftIO $+ STM.atomically $+ STM.writeTChan (animationMgrInputChan mgr) req++-- | Start a new animation at its first frame.+--+-- This will result in an application state update to initialize the+-- animation state at the provided traversal's location.+startAnimation :: (MonadIO m)+ => AnimationManager s e n+ -- ^ The manager to run the animation+ -> Clip s n+ -- ^ The frames for the animation+ -> Integer+ -- ^ The animation's frame duration in milliseconds+ -> RunMode+ -- ^ The animation's run mode+ -> Traversal' s (Maybe (Animation s n))+ -- ^ Where in the application state to manage this+ -- animation's state+ -> m ()+startAnimation mgr frames frameMs runMode updater =+ tellAnimationManager mgr $ StartAnimation frames frameMs runMode updater++-- | Stop an animation.+--+-- This will result in an application state update to remove the+-- animation state.+stopAnimation :: (MonadIO m)+ => AnimationManager s e n+ -> Animation s n+ -> m ()+stopAnimation mgr a =+ tellAnimationManager mgr $ StopAnimation a
+ src/Brick/Animation/Clock.hs view
@@ -0,0 +1,64 @@+-- | This module provides an API for working with+-- 'Data.Time.Clock.System.SystemTime' values similar to that of+-- 'Data.Time.Clock.UTCTime'. @SystemTime@s are more efficient to+-- obtain than @UTCTime@s, which is important to avoid animation+-- tick thread delays associated with expensive clock reads. In+-- addition, the @UTCTime@-based API provides unpleasant @Float@-based+-- conversions. Since the @SystemTime@-based API doesn't provide some+-- of the operations we need, and since it is easier to work with at+-- millisecond granularity, it is extended here for internal use.+module Brick.Animation.Clock+ ( Time+ , getTime+ , addOffset+ , subtractTime++ , Offset+ , offsetFromMs+ , offsetToMs+ )+where++import Control.Monad.IO.Class (MonadIO, liftIO)+import qualified Data.Time.Clock.System as C++newtype Time = Time C.SystemTime+ deriving (Ord, Eq)++-- | Signed difference in milliseconds+newtype Offset = Offset Integer+ deriving (Ord, Eq)++offsetFromMs :: Integer -> Offset+offsetFromMs = Offset++offsetToMs :: Offset -> Integer+offsetToMs (Offset ms) = ms++getTime :: (MonadIO m) => m Time+getTime = Time <$> liftIO C.getSystemTime++addOffset :: Offset -> Time -> Time+addOffset (Offset ms) (Time (C.MkSystemTime s ns)) =+ Time $ C.MkSystemTime (fromInteger s') (fromInteger ns')+ where+ -- Note that due to the behavior of divMod, this works even when+ -- the offset is negative: the number of seconds is decremented+ -- and the remainder of nanoseconds is correct.+ s' = newSec + toInteger s+ (newSec, ns') = (nsPerMs * ms + toInteger ns)+ `divMod` (msPerS * nsPerMs)++subtractTime :: Time -> Time -> Offset+subtractTime t1 t2 = Offset $ timeToMs t1 - timeToMs t2++timeToMs :: Time -> Integer+timeToMs (Time (C.MkSystemTime s ns)) =+ (toInteger s) * msPerS ++ (toInteger ns) `div` nsPerMs++nsPerMs :: Integer+nsPerMs = 1000000++msPerS :: Integer+msPerS = 1000
src/Brick/AttrMap.hs view
@@ -1,8 +1,7 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-} -- | This module provides types and functions for managing an attribute -- map which maps attribute names ('AttrName') to attributes ('Attr').--- This module is designed to be used with the 'OverloadedStrings'--- language extension to permit easy construction of 'AttrName' values--- and you should also use 'mappend' ('<>') to combine names. -- -- Attribute maps work by mapping hierarchical attribute names to -- attributes and inheriting parent names' attributes when child names@@ -15,7 +14,7 @@ -- Attribute names are mapped to attributes, but some attributes may -- be partial (specify only a foreground or background color). When -- attribute name lookups occur, the attribute corresponding to a more--- specific name ('parent <> child' as above) is sucessively merged with+-- specific name ('parent <> child' as above) is successively merged with -- the parent attribute ('parent' as above) all the way to the "root" -- of the attribute map, the map's default attribute. In this way, more -- specific attributes inherit what they don't specify from more general@@ -28,63 +27,69 @@ -- * Construction , attrMap , forceAttrMap+ , forceAttrMapAllowStyle , attrName+ -- * Inspection+ , attrNameComponents -- * Finding attributes from names , attrMapLookup -- * Manipulating attribute maps- , setDefault+ , setDefaultAttr+ , getDefaultAttr , applyAttrMappings , mergeWithDefault+ , mapAttrName+ , mapAttrNames ) where -import Control.Applicative ((<$>))+import qualified Data.Semigroup as Sem++import Control.DeepSeq+import Data.Bits ((.|.)) import qualified Data.Map as M-import Data.Monoid-import Data.Maybe (catMaybes)+import Data.Maybe (mapMaybe) import Data.List (inits)-import Data.String (IsString(..))-import Data.Default (Default(..))+import GHC.Generics (Generic) -import Graphics.Vty (Attr(..), MaybeDefault(..))+import Graphics.Vty (Attr(..), MaybeDefault(..), Style) -- | An attribute name. Attribute names are hierarchical; use 'mappend'--- ('<>') to assemble them. Hierachy in an attribute name is used to+-- ('<>') to assemble them. Hierarchy in an attribute name is used to -- represent increasing levels of specificity in referring to the -- attribute you want to use for a visual element, with names to the -- left being general and names to the right being more specific. For -- example: -- -- @--- "window" <> "border"--- "window" <> "title"--- "header" <> "clock" <> "seconds"+-- attrName "window" <> attrName "border"+-- attrName "window" <> attrName "title"+-- attrName "header" <> attrName "clock" <> attrName "seconds" -- @ data AttrName = AttrName [String]- deriving (Show, Eq, Ord)+ deriving (Show, Read, Eq, Ord, Generic, NFData) -instance Default AttrName where- def = mempty+instance Sem.Semigroup AttrName where+ (AttrName as) <> (AttrName bs) = AttrName $ as `mappend` bs instance Monoid AttrName where mempty = AttrName []- mappend (AttrName as) (AttrName bs) = AttrName $ as `mappend` bs--instance IsString AttrName where- fromString = AttrName . (:[])+ mappend = (Sem.<>) -- | An attribute map which maps 'AttrName' values to 'Attr' values. data AttrMap = AttrMap Attr (M.Map AttrName Attr) | ForceAttr Attr- deriving Show--instance Default AttrMap where- def = AttrMap def mempty+ | ForceAttrAllowStyle Attr AttrMap+ deriving (Show, Generic, NFData) -- | Create an attribute name from a string. attrName :: String -> AttrName attrName = AttrName . (:[]) +-- | Get the components of an attribute name.+attrNameComponents :: AttrName -> [String]+attrNameComponents (AttrName cs) = cs+ -- | Create an attribute map. attrMap :: Attr -- ^ The map's default attribute to be returned when a name@@ -96,10 +101,15 @@ attrMap theDefault pairs = AttrMap theDefault (M.fromList pairs) -- | Create an attribute map in which all lookups map to the same--- attribute.+-- attribute. This is functionally equivalent to @attrMap attr []@. forceAttrMap :: Attr -> AttrMap forceAttrMap = ForceAttr +-- | Create an attribute map in which all lookups map to the same+-- attribute. This is functionally equivalent to @attrMap attr []@.+forceAttrMapAllowStyle :: Attr -> AttrMap -> AttrMap+forceAttrMapAllowStyle = ForceAttrAllowStyle+ -- | Given an attribute and a map, merge the attribute with the map's -- default attribute. If the map is forcing all lookups to a specific -- attribute, the forced attribute is returned without merging it with@@ -119,50 +129,92 @@ -- @ mergeWithDefault :: Attr -> AttrMap -> Attr mergeWithDefault _ (ForceAttr a) = a+mergeWithDefault _ (ForceAttrAllowStyle f _) = f mergeWithDefault a (AttrMap d _) = combineAttrs d a -- | Look up the specified attribute name in the map. Map lookups -- proceed as follows. If the attribute map is forcing all lookups to a--- specific attribute, that attribute is returned. If the attribute name--- is empty, the map's default attribute is returned. If the attribute--- name is non-empty, very subsequence of names from the specified name--- are used to perform a lookup, and the results are combined as in--- 'mergeWithDefault', with more specific results taking precedence over--- less specific ones.+-- specific attribute, that attribute is returned along with its style+-- settings. If the attribute name is empty, the map's default attribute+-- is returned. If the attribute name is non-empty, every subsequence of+-- names from the specified name are used to perform a lookup and the+-- results are combined as in 'mergeWithDefault', with more specific+-- results taking precedence over less specific ones. As attributes are+-- merged, styles are also merged. If a more specific attribute name+-- introduces a style (underline, say) and a less specific attribute+-- name introduces an additional style (bold, say) then the final result+-- will include both styles. -- -- For example: -- -- @--- attrMapLookup ("foo" <> "bar") (attrMap a []) == a--- attrMapLookup ("foo" <> "bar") (attrMap (bg blue) [("foo" <> "bar", fg red)]) == red \`on\` blue--- attrMapLookup ("foo" <> "bar") (attrMap (bg blue) [("foo" <> "bar", red `on` cyan)]) == red \`on\` cyan--- attrMapLookup ("foo" <> "bar") (attrMap (bg blue) [("foo" <> "bar", fg red), ("foo", bg cyan)]) == red \`on\` cyan--- attrMapLookup ("foo" <> "bar") (attrMap (bg blue) [("foo", fg red)]) == red \`on\` blue+-- attrMapLookup (attrName "foo" <> attrName "bar") (attrMap a []) == a+-- attrMapLookup (attrName "foo" <> attrName "bar") (attrMap (bg blue) [(attrName "foo" <> attrName "bar", fg red)]) == red \`on\` blue+-- attrMapLookup (attrName "foo" <> attrName "bar") (attrMap (bg blue) [(attrName "foo" <> attrName "bar", red `on` cyan)]) == red \`on\` cyan+-- attrMapLookup (attrName "foo" <> attrName "bar") (attrMap (bg blue) [(attrName "foo" <> attrName "bar", fg red), ("foo", bg cyan)]) == red \`on\` cyan+-- attrMapLookup (attrName "foo" <> attrName "bar") (attrMap (bg blue) [(attrName "foo", fg red)]) == red \`on\` blue -- @ attrMapLookup :: AttrName -> AttrMap -> Attr attrMapLookup _ (ForceAttr a) = a+attrMapLookup a (ForceAttrAllowStyle forced m) =+ -- Look up the attribute in the contained map, then keep only its+ -- style.+ let result = attrMapLookup a m+ in forced { attrStyle = attrStyle forced `combineStyles` attrStyle result+ } attrMapLookup (AttrName []) (AttrMap theDefault _) = theDefault attrMapLookup (AttrName ns) (AttrMap theDefault m) =- let results = catMaybes $ (\n -> M.lookup n m) <$> (AttrName <$> (inits ns))+ let results = mapMaybe (\n -> M.lookup (AttrName n) m) (inits ns) in foldl combineAttrs theDefault results -- | Set the default attribute value in an attribute map.-setDefault :: Attr -> AttrMap -> AttrMap-setDefault _ (ForceAttr a) = ForceAttr a-setDefault newDefault (AttrMap _ m) = AttrMap newDefault m+setDefaultAttr :: Attr -> AttrMap -> AttrMap+setDefaultAttr _ (ForceAttr a) = ForceAttr a+setDefaultAttr newDefault (ForceAttrAllowStyle a m) =+ ForceAttrAllowStyle a (setDefaultAttr newDefault m)+setDefaultAttr newDefault (AttrMap _ m) = AttrMap newDefault m +-- | Get the default attribute value in an attribute map.+getDefaultAttr :: AttrMap -> Attr+getDefaultAttr (ForceAttr a) = a+getDefaultAttr (ForceAttrAllowStyle _ m) = getDefaultAttr m+getDefaultAttr (AttrMap d _) = d+ combineAttrs :: Attr -> Attr -> Attr-combineAttrs (Attr s1 f1 b1) (Attr s2 f2 b2) =- Attr (s1 `combineMDs` s2)+combineAttrs (Attr s1 f1 b1 u1) (Attr s2 f2 b2 u2) =+ Attr (s1 `combineStyles` s2) (f1 `combineMDs` f2) (b1 `combineMDs` b2)+ (u1 `combineMDs` u2) combineMDs :: MaybeDefault a -> MaybeDefault a -> MaybeDefault a combineMDs _ (SetTo v) = SetTo v combineMDs (SetTo v) _ = SetTo v combineMDs _ v = v +combineStyles :: MaybeDefault Style -> MaybeDefault Style -> MaybeDefault Style+combineStyles (SetTo a) (SetTo b) = SetTo $ a .|. b+combineStyles _ (SetTo v) = SetTo v+combineStyles (SetTo v) _ = SetTo v+combineStyles _ v = v+ -- | Insert a set of attribute mappings to an attribute map. applyAttrMappings :: [(AttrName, Attr)] -> AttrMap -> AttrMap applyAttrMappings _ (ForceAttr a) = ForceAttr a applyAttrMappings ms (AttrMap d m) = AttrMap d ((M.fromList ms) `M.union` m)+applyAttrMappings ms (ForceAttrAllowStyle a m) = ForceAttrAllowStyle a (applyAttrMappings ms m)++-- | Update an attribute map such that a lookup of 'ontoName' returns+-- the attribute value specified by 'fromName'. This is useful for+-- composite widgets with specific attribute names mapping those names+-- to the sub-widget's expected name when calling that sub-widget's+-- rendering function. See the ProgressBarDemo for an example usage,+-- and 'overrideAttr' for an alternate syntax.+mapAttrName :: AttrName -> AttrName -> AttrMap -> AttrMap+mapAttrName fromName ontoName inMap =+ applyAttrMappings [(ontoName, attrMapLookup fromName inMap)] inMap++-- | Map several attributes to return the value associated with an+-- alternate name. Applies 'mapAttrName' across a list of mappings.+mapAttrNames :: [(AttrName, AttrName)] -> AttrMap -> AttrMap+mapAttrNames names inMap = foldr (uncurry mapAttrName) inMap names
+ src/Brick/BChan.hs view
@@ -0,0 +1,44 @@+module Brick.BChan+ ( BChan+ , newBChan+ , writeBChan+ , writeBChanNonBlocking+ , readBChan+ , readBChan2+ )+where++import Control.Concurrent.STM.TBQueue+import Control.Monad.STM (atomically, orElse)++-- | @BChan@ is an abstract type representing a bounded FIFO channel.+data BChan a = BChan (TBQueue a)++-- | Builds and returns a new instance of @BChan@.+newBChan :: Int -- ^ maximum number of elements the channel can hold+ -> IO (BChan a)+newBChan size = atomically $ BChan <$> newTBQueue (fromIntegral size)++-- | Writes a value to a @BChan@; blocks if the channel is full.+writeBChan :: BChan a -> a -> IO ()+writeBChan (BChan q) a = atomically $ writeTBQueue q a++-- | Attempts to write a value to a @BChan@. If the channel has room,+-- the value is written and this returns 'True'. Otherwise this returns+-- 'False' and returns immediately.+writeBChanNonBlocking :: BChan a -> a -> IO Bool+writeBChanNonBlocking (BChan q) a = atomically $ do+ f <- isFullTBQueue q+ if f+ then return False+ else writeTBQueue q a >> return True++-- | Reads the next value from the @BChan@; blocks if necessary.+readBChan :: BChan a -> IO a+readBChan (BChan q) = atomically $ readTBQueue q++-- | Reads the next value from either @BChan@, prioritizing the first+-- @BChan@; blocks if necessary.+readBChan2 :: BChan a -> BChan b -> IO (Either a b)+readBChan2 (BChan q1) (BChan q2) = atomically $+ (Left <$> readTBQueue q1) `orElse` (Right <$> readTBQueue q2)
+ src/Brick/BorderMap.hs view
@@ -0,0 +1,225 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}+module Brick.BorderMap+ ( BorderMap+ , Edges(..)+ , eTopL, eBottomL, eRightL, eLeftL+ , empty, clear, emptyCoordinates, singleton+ , insertH, insertV, insert+ , unsafeUnion+ , coordinates, bounds+ , values+ , lookupRow, lookupCol, lookupH, lookupV, lookup+ , setCoordinates, crop, expand+ , translate+ ) where++import Brick.Types.Common (Edges(..), Location(..), eTopL, eBottomL, eRightL, eLeftL, origin)+#if !(MIN_VERSION_base(4,18,0))+import Control.Applicative (liftA2)+#endif+import Data.IMap (IMap, Run(Run))+import GHC.Generics+import Control.DeepSeq+import Prelude hiding (lookup)+import qualified Data.IMap as IM++-- | Internal use only.+neighbors :: Edges a -> Edges (a, a)+neighbors (Edges vt vb vl vr) = Edges horiz horiz vert vert where+ horiz = (vl, vr)+ vert = (vt, vb)++-- Invariant: corner values are present on all the edges incident on that+-- corner. Widthless or heightless rectangles replicate the IMaps exactly on+-- the two coincident edges.+--+-- Practically speaking, this means for lookup you can look on any edge that+-- could contain the key you care about, while for insertion you must insert on+-- every edge that could contain the keys being inserted.++-- | A @BorderMap a@ is like a @Map Location a@, except that there is a+-- rectangle, and only 'Location's on the border of this rectangle are+-- retained. The 'BorderMap' can be queried for the position and size of the+-- rectangle. There are also efficient bulk query and bulk update operations+-- for adjacent positions on the border.+data BorderMap a = BorderMap+ { _coordinates :: Edges Int+ , _values :: Edges (IMap a)+ } deriving (Eq, Ord, Show, Functor, Read, Generic, NFData)++-- | Given a rectangle (specified as the coordinates of the top, left, bottom,+-- and right sides), initialize an empty 'BorderMap'.+emptyCoordinates :: Edges Int -> BorderMap a+emptyCoordinates cs = BorderMap { _coordinates = cs, _values = pure IM.empty }++-- | An empty 'BorderMap' that tracks the same points as the input.+clear :: BorderMap a -> BorderMap b+clear = emptyCoordinates . coordinates++-- | An empty 'BorderMap' that does not track any points.+empty :: BorderMap a+empty = emptyCoordinates Edges+ { eTop = 0+ , eBottom = -1+ , eLeft = 0+ , eRight = -1+ }++-- | A 'BorderMap' that tracks only the given the point (and initially maps it+-- to the given value).+singleton :: Location -> a -> BorderMap a+singleton l v = translate l . insert origin v . emptyCoordinates $ pure 0++{-# INLINE coordinates #-}+-- | The positions of the edges of the rectangle whose border is retained in a+-- 'BorderMap'. For example, if @coordinates m = e@, then the top border+-- contains the 'Location's on row @eTop e@ and between columns @eLeft e@ to+-- @eRight e@ inclusive.+coordinates :: BorderMap a -> Edges Int+coordinates = _coordinates++-- | A complementary way to query the edges of the rectangle whose border is+-- retained in a 'BorderMap'. For example, if @bounds m = b@, then a+-- 'Location'\'s column must be between @fst (eTop b)@ and @snd (eTop b)@ to be+-- retained. See also 'coordinates', which is in most cases a more natural+-- border query.+bounds :: BorderMap a -> Edges (Int, Int)+bounds = neighbors . coordinates++{-# INLINE values #-}+-- | Maps giving the values along each edge. Corner values are replicated in+-- all relevant edges.+values :: BorderMap a -> Edges (IMap a)+values = _values++-- | Bulk insertion of horizontally-adjacent values. The 'Location' gives the+-- start point, and the 'Run' extends in the "larger columns" direction.+insertH :: Location -> Run a -> BorderMap a -> BorderMap a+insertH = insertDirAgnostic (Edges insertPar insertPar insertPerp insertPerp) . swapLoc+ where+ swapLoc (Location (col, row)) = Location (row, col)++-- | Bulk insertion of vertically-adjacent values. The 'Location' gives the+-- start point, and the 'Run' extends in the "larger rows" direction.+insertV :: Location -> Run a -> BorderMap a -> BorderMap a+insertV = insertDirAgnostic (Edges insertPerp insertPerp insertPar insertPar)++insertDirAgnostic+ :: Edges (Location -> Run a -> Int -> (Int, Int) -> IMap a -> IMap a)+ -> Location -> Run a -> BorderMap a -> BorderMap a+insertDirAgnostic insertions l r m =+ m { _values = insertions <*> pure l <*> pure r <*> coordinates m <*> bounds m <*> _values m }++insertPar, insertPerp :: Location -> Run a -> Int -> (Int, Int) -> IMap a -> IMap a+insertPar (Location (kPar, kPerp)) r herePar (loPerp, hiPerp)+ | kPar == herePar && loPerp <= kPerp + IM.len r - 1 && kPerp <= hiPerp+ = IM.insert beg r { IM.len = end - beg + 1 }+ | otherwise = id+ where+ beg = max kPerp loPerp+ end = min (kPerp + IM.len r - 1) hiPerp+insertPerp (Location (kPar, kPerp)) r herePerp (loPar, hiPar)+ | loPar <= kPar && kPar <= hiPar && kPerp <= herePerp && herePerp <= kPerp + IM.len r - 1+ = IM.insert kPar r { IM.len = 1 }+ | otherwise = id++-- | Insert a single value at the given location.+insert :: Location -> a -> BorderMap a -> BorderMap a+insert l = insertV l . Run 1++-- | Look up all values on a given row. The 'IMap' returned maps columns to+-- values.+lookupRow :: Int -> BorderMap a -> IMap a+lookupRow row m+ | row == eTop (coordinates m) = eTop (_values m)+ | row == eBottom (coordinates m) = eBottom (_values m)+ | otherwise = IM.fromList+ $ [(eLeft (coordinates m), Run 1 a) | Just a <- [IM.lookup row (eLeft (_values m))]]+ ++ [(eRight (coordinates m), Run 1 a) | Just a <- [IM.lookup row (eRight (_values m))]]++-- | Look up all values on a given column. The 'IMap' returned maps rows to+-- values.+lookupCol :: Int -> BorderMap a -> IMap a+lookupCol col m+ | col == eLeft (coordinates m) = eLeft (_values m)+ | col == eRight (coordinates m) = eRight (_values m)+ | otherwise = IM.fromList+ $ [(eTop (coordinates m), Run 1 a) | Just a <- [IM.lookup col (eTop (_values m))]]+ ++ [(eBottom (coordinates m), Run 1 a) | Just a <- [IM.lookup col (eBottom (_values m))]]++-- | Bulk lookup of horizontally-adjacent values. The 'Location' gives the+-- starting point, and the 'Run' extends in the "larger columns" direction. The+-- 'IMap' returned maps columns to values.+lookupH :: Location -> Run ignored -> BorderMap a -> IMap a+lookupH (Location (col, row)) r = IM.restrict col r . lookupRow row++-- | Bulk lookup of vertically-adjacent values. The 'Location' gives the+-- starting point, and the 'Run' extends in the "larger rows" direction. The+-- 'IMap' returned maps rows to values.+lookupV :: Location -> Run ignored -> BorderMap a -> IMap a+lookupV (Location (col, row)) r = IM.restrict row r . lookupCol col++-- | Look up a single position.+lookup :: Location -> BorderMap a -> Maybe a+lookup (Location (col, row)) = IM.lookup row . lookupCol col++-- | Set the rectangle being tracked by this 'BorderMap', throwing away any+-- values that do not lie on this new rectangle.+setCoordinates :: Edges Int -> BorderMap a -> BorderMap a+setCoordinates coordinates' m = BorderMap+ { _values = values'+ , _coordinates = coordinates'+ }+ where+ bounds' = neighbors coordinates'+ values' = gc+ <$> _coordinates m+ <*> coordinates'+ <*> bounds'+ <*> _values m+ <*> Edges { eTop = lookupRow, eBottom = lookupRow, eLeft = lookupCol, eRight = lookupCol }+ gc oldPar newPar (loPerp, hiPerp) imPar lookupPerp+ | oldPar == newPar = IM.restrict loPerp (Run (hiPerp-loPerp+1) ()) imPar+ | otherwise = lookupPerp newPar m++-- | Ensure that the rectangle being tracked by this 'BorderMap' extends no+-- farther than the given one.+crop :: Edges Int -> BorderMap a -> BorderMap a+crop cs m = setCoordinates (shrink <*> cs <*> coordinates m) m where+ shrink = Edges+ { eTop = max+ , eBottom = min+ , eLeft = max+ , eRight = min+ }++-- | Ensure that the rectangle being tracked by this 'BorderMap' extends at+-- least as far as the given one.+expand :: Edges Int -> BorderMap a -> BorderMap a+expand cs m = setCoordinates (grow <*> cs <*> coordinates m) m where+ grow = Edges+ { eTop = min+ , eBottom = max+ , eLeft = min+ , eRight = max+ }++-- | Move a 'BorderMap' by adding the given 'Location' to all keys in the map.+translate :: Location -> BorderMap a -> BorderMap a+-- fast path: do nothing for (0,0)+translate (Location (0, 0)) m = m+translate (Location (c, r)) m = BorderMap+ { _coordinates = liftA2 (+) cOffsets (_coordinates m)+ , _values = liftA2 IM.addToKeys vOffsets (_values m)+ }+ where+ cOffsets = Edges { eTop = r, eBottom = r, eLeft = c, eRight = c }+ vOffsets = Edges { eTop = c, eBottom = c, eLeft = r, eRight = r }++-- | Assumes the two 'BorderMap's are tracking the same rectangles, but have+-- disjoint keys. This property is not checked.+unsafeUnion :: BorderMap a -> BorderMap a -> BorderMap a+unsafeUnion m m' = m { _values = liftA2 IM.unsafeUnion (_values m) (_values m') }
src/Brick/Focus.hs view
@@ -1,68 +1,112 @@ -- | This module provides a type and functions for handling focus rings--- of widgets. Note that this interface is merely provided for managing--- the focus state for a sequence of widget names; it does not do--- anything beyond keep track of that.------ This interface is experimental.+-- of values. module Brick.Focus ( FocusRing , focusRing , focusNext , focusPrev , focusGetCurrent+ , focusSetCurrent+ , focusRingLength+ , focusRingToList , focusRingCursor+ , withFocusRing+ , focusRingModify ) where -import Control.Lens ((^.))-import Data.Maybe (listToMaybe)+import Lens.Micro ((^.))+import Data.List (find)+import qualified Data.CircularList as C import Brick.Types+import Brick.Widgets.Core (Named(..)) --- | A focus ring containing a sequence of widget names to focus and a--- currently-focused widget name.-data FocusRing = FocusRingEmpty- | FocusRingNonempty ![Name] !Int+-- | A focus ring containing a sequence of resource names to focus and a+-- currently-focused name.+newtype FocusRing n = FocusRing (C.CList n)+ deriving (Show) --- | Construct a focus ring from the list of names.-focusRing :: [Name] -> FocusRing-focusRing [] = FocusRingEmpty-focusRing names = FocusRingNonempty names 0+-- | Construct a focus ring from the list of resource names.+focusRing :: [n] -> FocusRing n+focusRing = FocusRing . C.fromList --- | Advance focus to the next widget in the ring.-focusNext :: FocusRing -> FocusRing-focusNext FocusRingEmpty = FocusRingEmpty-focusNext (FocusRingNonempty ns i) = FocusRingNonempty ns i'- where- i' = (i + 1) `mod` (length ns)+-- | Advance focus to the next value in the ring.+focusNext :: FocusRing n -> FocusRing n+focusNext r@(FocusRing l)+ | C.isEmpty l = r+ | otherwise = FocusRing $ C.rotR l --- | Advance focus to the previous widget in the ring.-focusPrev :: FocusRing -> FocusRing-focusPrev FocusRingEmpty = FocusRingEmpty-focusPrev (FocusRingNonempty ns i) = FocusRingNonempty ns i'- where- i' = (i + (length ns) - 1) `mod` (length ns)+-- | Advance focus to the previous value in the ring.+focusPrev :: FocusRing n -> FocusRing n+focusPrev r@(FocusRing l)+ | C.isEmpty l = r+ | otherwise = FocusRing $ C.rotL l --- | Get the currently-focused widget name from the ring. If the ring is--- emtpy, return 'Nothing'.-focusGetCurrent :: FocusRing -> Maybe Name-focusGetCurrent FocusRingEmpty = Nothing-focusGetCurrent (FocusRingNonempty ns i) = Just $ ns !! i+-- | This function is a convenience function to look up a widget state+-- value's resource name in a focus ring and set its focus setting+-- according to the focus ring's state. This function determines whether+-- a given widget state value is the focus of the ring and passes the+-- resulting boolean to a rendering function, along with the state value+-- (a), to produce whatever comes next (b).+--+-- Focus-aware widgets have rendering functions that should be+-- usable with this combinator; see 'Brick.Widgets.List.List' and+-- 'Brick.Widgets.Edit.Edit'.+withFocusRing :: (Eq n, Named a n)+ => FocusRing n+ -- ^ The focus ring to use as the source of focus state.+ -> (Bool -> a -> b)+ -- ^ A function that takes a value and its focus state.+ -> a+ -- ^ The widget state value that we need to check for focus.+ -> b+ -- ^ The rest of the computation.+withFocusRing ring f a = f (focusGetCurrent ring == Just (getName a)) a +-- | Get the currently-focused resource name from the ring. If the ring+-- is empty, return 'Nothing'.+focusGetCurrent :: FocusRing n -> Maybe n+focusGetCurrent (FocusRing l) = C.focus l++-- | Set the currently-focused resource name in the ring, provided the+-- name is in the ring. Otherwise return the ring unmodified.+focusSetCurrent :: (Eq n) => n -> FocusRing n -> FocusRing n+focusSetCurrent n r@(FocusRing l) =+ case C.rotateTo n l of+ Nothing -> r+ Just l' -> FocusRing l'++-- | Get the size of the FocusRing.+focusRingLength :: FocusRing n -> Int+focusRingLength (FocusRing l) = C.size l++-- | Return all of the entries in the focus ring, starting with the+-- currently-focused entry and wrapping around the ring.+--+-- For example, if a ring contains A, B, C, and D, and the current entry+-- is B, the result will be [B, C, D, A].+focusRingToList :: FocusRing n -> [n]+focusRingToList (FocusRing l) = C.rightElements l++-- | Modify the internal circular list structure of a focus ring+-- directly. This function permits modification of the circular list+-- using the rich Data.CircularList API.+focusRingModify :: (C.CList n -> C.CList n) -> FocusRing n -> FocusRing n+focusRingModify f (FocusRing l) = FocusRing $ f l+ -- | Cursor selection convenience function for use as an -- 'Brick.Main.appChooseCursor' value.-focusRingCursor :: (a -> FocusRing)+focusRingCursor :: (Eq n)+ => (a -> FocusRing n) -- ^ The function used to get the focus ring out of your -- application state. -> a -- ^ Your application state.- -> [CursorLocation]+ -> [CursorLocation n] -- ^ The list of available cursor positions.- -> Maybe CursorLocation- -- ^ The cursor position, if any, that matches the name- -- currently focused by the 'FocusRing'.-focusRingCursor getRing st ls =- listToMaybe $ filter isCurrent ls- where- isCurrent cl = cl^.cursorLocationNameL ==- (focusGetCurrent $ getRing st)+ -> Maybe (CursorLocation n)+ -- ^ The cursor position, if any, that matches the+ -- resource name currently focused by the 'FocusRing'.+focusRingCursor getRing st = find $ \cl ->+ cl^.cursorLocationNameL == focusGetCurrent (getRing st)
+ src/Brick/Forms.hs view
@@ -0,0 +1,970 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -fno-warn-unused-binds #-}+-- | Note - this API is designed to support a narrow (but common!) set+-- of use cases. If you find that you need more customization than this+-- offers, then you will need to consider building your own layout and+-- event handling for input fields.+--+-- For a fuller introduction to this API, see the "Input Forms" section+-- of the Brick User Guide. Also see the demonstration programs for+-- examples of forms in action.+--+-- This module provides an input form API. This API allows you to+-- construct an input interface based on a data type of your choice.+-- Each input in the form corresponds to a field in your data type. This+-- API then automatically dispatches keyboard and mouse input events to+-- each form input field, manages rendering of the form, notifies the+-- user when a form field's value is invalid, and stores valid inputs in+-- your data type when possible.+--+-- A form has both a visual representation and a corresponding data+-- structure representing the latest valid values for that form+-- (referred to as the "state" of the form). A 'FormField' is a single+-- input component in the form and a 'FormFieldState' defines the+-- linkage between that visual input and the corresponding portion+-- of the state represented by that visual; there may be multiple+-- 'FormField's combined for a single 'FormFieldState' (e.g. a radio+-- button sequence).+--+-- To use a 'Form', you must include it within your application state+-- type. You can use 'formState' to access the underlying state whenever+-- you need it. See @programs/FormDemo.hs@ for a complete working+-- example.+--+-- Also note that, by default, forms and their field inputs are+-- concatenated together in a 'vBox'. This can be customized on a+-- per-field basis and for the entire form by using the functions+-- 'setFieldConcat' and 'setFormConcat', respectively.+--+-- Bear in mind that for most uses, the 'FormField' and 'FormFieldState'+-- types will not be used directly. Instead, the constructors for+-- various field types (such as 'editTextField') will be used instead.+module Brick.Forms+ ( -- * Data types+ Form+ , FormFieldState(..)+ , FormField(..)+ , FormFieldVisibilityMode(..)++ -- * Creating and using forms+ , newForm+ , formFocus+ , formState+ , handleFormEvent+ , renderForm+ , renderFormFieldState+ , (@@=)+ , allFieldsValid+ , invalidFields+ , setFieldValid+ , setFormConcat+ , setFieldConcat+ , setFormFocus+ , updateFormState+ , setFieldVisibilityMode++ -- * Simple form field constructors+ , editTextField+ , editShowableField+ , editShowableFieldWithValidate+ , editPasswordField+ , radioField+ , checkboxField+ , listField++ -- * Advanced form field constructors+ , editField+ , radioCustomField+ , checkboxCustomField++ -- * Attributes+ , formAttr+ , invalidFormInputAttr+ , focusedFormInputAttr+ )+where++import Graphics.Vty hiding (showCursor)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import Data.Maybe (fromJust, isJust, isNothing)+import Data.List (elemIndex)+import Data.Vector (Vector)++import Brick+import Brick.Focus+import Brick.Widgets.Edit+import Brick.Widgets.List+import qualified Data.Text.Zipper as Z++import qualified Data.Text as T+import Text.Read (readMaybe)++import Lens.Micro+import Lens.Micro.Mtl++-- | A form field. This represents an interactive input field in the+-- form. Its user input is validated and thus converted into a type of+-- your choosing.+--+-- Type variables are as follows:+--+-- * @a@ - the type of the field in your form state that this field+-- manipulates+-- * @b@ - the form field's internal state type+-- * @e@ - your application's event type+-- * @n@ - your application's resource name type+data FormField a b e n =+ FormField { formFieldName :: n+ -- ^ The name identifying this form field.+ , formFieldValidate :: b -> Maybe a+ -- ^ A validation function converting this field's state+ -- into a value of your choosing. @Nothing@ indicates a+ -- validation failure. For example, this might validate+ -- an 'Editor' state value by parsing its text contents as+ -- an integer and return 'Maybe' 'Int'. This is for pure+ -- value validation; if additional validation is required+ -- (e.g. via 'IO'), use this field's state value in an+ -- external validation routine and use 'setFieldValid' to+ -- feed the result back into the form.+ , formFieldExternallyValid :: Bool+ -- ^ Whether the field is valid according to an external+ -- validation source. Defaults to always being 'True' and+ -- can be set with 'setFieldValid'. The value of this+ -- field also affects the behavior of 'allFieldsValid' and+ -- 'getInvalidFields'.+ , formFieldRender :: Bool -> b -> Widget n+ -- ^ A function to render this form field. Parameters are+ -- whether the field is currently focused, followed by the+ -- field state.+ , formFieldHandleEvent :: BrickEvent n e -> EventM n b ()+ -- ^ An event handler for this field.+ }++-- | How to bring form fields into view when a form is rendered in a+-- viewport with 'viewport'.+data FormFieldVisibilityMode =+ ShowFocusedFieldOnly+ -- ^ Make only the focused field's selected input visible. For+ -- composite fields this will not bring all options into view.+ | ShowCompositeField+ -- ^ Make all inputs in the focused field visible. For composite+ -- fields this will bring all options into view as long as the+ -- viewport is large enough to show them all.+ | ShowAugmentedField+ -- ^ Like 'ShowCompositeField' but includes rendering augmentations+ -- applied with '@@='.+ deriving (Eq, Show)++-- | A form field state accompanied by the fields that manipulate that+-- state. The idea is that some record field in your form state has+-- one or more form fields that manipulate that value. This data type+-- maps that state field (using a lens into your state) to the form+-- input fields responsible for managing that state field, along with+-- a current value for that state field and an optional function to+-- control how the form inputs are rendered.+--+-- Most form fields will just have one input, such as text editors, but+-- others, such as radio button collections, will have many, which is+-- why this type supports more than one input corresponding to a state+-- field.+--+-- Type variables are as follows:+--+-- * @s@ - the data type containing the value manipulated by these form+-- fields.+-- * @e@ - your application's event type+-- * @n@ - your application's resource name type+data FormFieldState s e n where+ FormFieldState :: { formFieldState :: b+ -- ^ The current state value associated with+ -- the field collection. Note that this type is+ -- existential. All form fields in the collection+ -- must validate to this type.+ , formFieldLens :: Lens' s a+ -- ^ A lens to extract and store a+ -- successfully-validated form input back into+ -- your form state.+ , formFieldUpdate :: a -> b -> b+ -- ^ Given a new form state value, update the form+ -- field state in place.+ , formFields :: [FormField a b e n]+ -- ^ The form fields, in order, that the user will+ -- interact with to manipulate this state value.+ , formFieldRenderHelper :: Widget n -> Widget n+ -- ^ A helper function to augment the rendered+ -- representation of this collection of form+ -- fields. It receives the default representation+ -- and can augment it, for example, by adding a+ -- label on the left.+ , formFieldConcat :: [Widget n] -> Widget n+ -- ^ Concatenation function for this field's input+ -- renderings.+ , formFieldVisibilityMode :: FormFieldVisibilityMode+ -- ^ This field's visibility mode for use in+ -- viewports.+ } -> FormFieldState s e n++-- | A form: a sequence of input fields that manipulate the fields of an+-- underlying state that you choose. This value must be stored in the+-- Brick application's state.+--+-- Type variables are as follows:+--+-- * @s@ - the data type of your choosing containing the values+-- manipulated by the fields in this form.+-- * @e@ - your application's event type+-- * @n@ - your application's resource name type+data Form s e n =+ Form { formFieldStates :: [FormFieldState s e n]+ , formFocus :: FocusRing n+ -- ^ The focus ring for the form, indicating which form field+ -- has input focus.+ , formState :: s+ -- ^ The current state of the form. Forms guarantee that only+ -- valid inputs ever get stored in the state, and that after+ -- each input event on a form field, if that field contains a+ -- valid state value then the value is immediately saved to its+ -- corresponding field in this state value using the form+ -- field's lens over @s@.+ , formConcatAll :: [Widget n] -> Widget n+ -- ^ Concatenation function for this form's field renderings.+ }++suffixLenses ''Form++-- | Compose a new rendering augmentation function with the one in the+-- form field collection. For example, we might put a label on the left+-- side of a form field:+--+-- > (str "Please check: " <+>) @@= checkboxField alive AliveField "Alive?"+--+-- This can also be used to add multiple augmentations and associates+-- right:+--+-- > (withDefAttr someAttribute) @@=+-- > (str "Please check: " <+>) @@=+-- > checkboxField alive AliveField "Alive?"+infixr 5 @@=+(@@=) :: (Widget n -> Widget n) -> (s -> FormFieldState s e n) -> s -> FormFieldState s e n+(@@=) h mkFs s =+ let v = mkFs s+ in v { formFieldRenderHelper = h . (formFieldRenderHelper v) }++-- | Update the state contained in a form.+--+-- This updates all form fields to be consistent with the new form+-- state. Where possible, this attempts to maintain other input state,+-- such as text editor cursor position.+--+-- Note that since this updates the form fields, this means that any+-- field values will be completely overwritten! This may or may not+-- be what you want, since a user actively using the form could get+-- confused if their edits go away. Use carefully.+updateFormState :: s -> Form s e n -> Form s e n+updateFormState newState f =+ let updateField fs = case fs of+ FormFieldState st l upd s rh concatAll visMode ->+ FormFieldState (upd (newState^.l) st) l upd s rh concatAll visMode+ in f { formState = newState+ , formFieldStates = updateField <$> formFieldStates f+ }++-- | Set the focused field of a form.+setFormFocus :: (Eq n) => n -> Form s e n -> Form s e n+setFormFocus n f = f { formFocus = focusSetCurrent n $ formFocus f }++-- | Set a form field's concatenation function.+setFieldConcat :: ([Widget n] -> Widget n) -> FormFieldState s e n -> FormFieldState s e n+setFieldConcat f s = s { formFieldConcat = f }++-- | Set a form's concatenation function.+setFormConcat :: ([Widget n] -> Widget n) -> Form s e n -> Form s e n+setFormConcat func f = f { formConcatAll = func }++-- | Create a new form with the specified input fields and an initial+-- form state. The fields are initialized from the state using their+-- state lenses and the first form input is focused initially.+newForm :: [s -> FormFieldState s e n]+ -- ^ The form field constructors. This is intended to be+ -- populated using the various field constructors in this+ -- module.+ -> s+ -- ^ The initial form state used to populate the fields.+ -> Form s e n+newForm mkEs s =+ let es = mkEs <*> pure s+ in Form { formFieldStates = es+ , formFocus = focusRing $ concatMap formFieldNames es+ , formState = s+ , formConcatAll = vBox+ }++formFieldNames :: FormFieldState s e n -> [n]+formFieldNames (FormFieldState _ _ _ fields _ _ _) = formFieldName <$> fields++-- | A form field for manipulating a boolean value. This represents+-- 'True' as @[X] label@ and 'False' as @[ ] label@.+--+-- This field responds to `Space` keypresses to toggle the checkbox and+-- to mouse clicks.+checkboxField :: (Ord n, Show n)+ => Lens' s Bool+ -- ^ The state lens for this value.+ -> n+ -- ^ The resource name for the input field.+ -> T.Text+ -- ^ The label for the check box, to appear at its right.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+checkboxField = checkboxCustomField '[' 'X' ']'++-- | A form field for manipulating a boolean value. This represents+-- 'True' as @[X] label@ and 'False' as @[ ] label@. This function+-- permits the customization of the @[X]@ notation characters.+--+-- This field responds to `Space` keypresses to toggle the checkbox and+-- to mouse clicks.+checkboxCustomField :: (Ord n, Show n)+ => Char+ -- ^ Left bracket character.+ -> Char+ -- ^ Checkmark character.+ -> Char+ -- ^ Right bracket character.+ -> Lens' s Bool+ -- ^ The state lens for this value.+ -> n+ -- ^ The resource name for the input field.+ -> T.Text+ -- ^ The label for the check box, to appear at its right.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+checkboxCustomField lb check rb stLens name label initialState =+ let initVal = initialState ^. stLens++ handleEvent (MouseDown n _ _ _) | n == name = modify not+ handleEvent (VtyEvent (EvKey (KChar ' ') [])) = modify not+ handleEvent _ = return ()++ in FormFieldState { formFieldState = initVal+ , formFields = [ FormField name Just True+ (renderCheckbox lb check rb label name)+ handleEvent+ ]+ , formFieldLens = stLens+ , formFieldUpdate =+ \val _ -> val+ , formFieldRenderHelper = id+ , formFieldConcat = vBox+ , formFieldVisibilityMode = ShowFocusedFieldOnly+ }++renderCheckbox :: (Ord n) => Char -> Char -> Char -> T.Text -> n -> Bool -> Bool -> Widget n+renderCheckbox lb check rb label n foc val =+ let addAttr = if foc then withDefAttr focusedFormInputAttr else id+ csr = if foc then putCursor n (Location (1,0)) else id+ in clickable n $+ addAttr $ csr $+ (txt $ T.singleton lb <> (if val then T.singleton check else " ") <>+ T.singleton rb <> " ") <+> txt label++-- | A form field for selecting a single choice from a set of possible+-- choices in a scrollable list. This uses a 'List' internally.+--+-- This field's attributes are governed by those exported from+-- 'Brick.Widgets.List'.+--+-- This field responds to the same input events that a 'List' does.+listField :: forall s e n a . (Ord n, Show n, Eq a)+ => (s -> Vector a)+ -- ^ Possible choices.+ -> Lens' s (Maybe a)+ -- ^ The state lens for the initially/finally selected+ -- element.+ -> (Bool -> a -> Widget n)+ -- ^ List item rendering function.+ -> Int+ -- ^ List item height in rows.+ -> n+ -- ^ The resource name for the input field.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+listField options stLens renderItem itemHeight name initialState =+ let optionsVector = options initialState+ initVal = initialState ^. customStLens++ customStLens :: Lens' s (List n a)+ customStLens = lens getList setList+ where+ getList s = let l = list name optionsVector itemHeight+ in case s ^. stLens of+ Nothing -> l+ Just e -> listMoveToElement e l+ setList s l = s & stLens .~ (snd <$> listSelectedElement l)++ handleEvent (VtyEvent e) = handleListEvent e+ handleEvent _ = return ()++ in FormFieldState { formFieldState = initVal+ , formFields = [ FormField name Just True+ (renderList renderItem)+ handleEvent+ ]+ , formFieldLens = customStLens+ , formFieldUpdate = \listState l ->+ case listSelectedElement listState of+ Nothing -> l+ Just (_, e) -> listMoveToElement e l+ , formFieldRenderHelper = id+ , formFieldConcat = vBox+ , formFieldVisibilityMode = ShowFocusedFieldOnly+ }++-- | A form field for selecting a single choice from a set of possible+-- choices. Each choice has an associated value and text label.+--+-- This field responds to `Space` keypresses to select a radio button+-- option and to mouse clicks.+radioField :: (Ord n, Show n, Eq a)+ => Lens' s a+ -- ^ The state lens for this value.+ -> [(a, n, T.Text)]+ -- ^ The available choices, in order. Each choice has a value+ -- of type @a@, a resource name, and a text label.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+radioField = radioCustomField '[' '*' ']'++-- | A form field for selecting a single choice from a set of possible+-- choices. Each choice has an associated value and text label. This+-- function permits the customization of the @[*]@ notation characters.+--+-- This field responds to `Space` keypresses to select a radio button+-- option and to mouse clicks.+radioCustomField :: (Ord n, Show n, Eq a)+ => Char+ -- ^ Left bracket character.+ -> Char+ -- ^ Checkmark character.+ -> Char+ -- ^ Right bracket character.+ -> Lens' s a+ -- ^ The state lens for this value.+ -> [(a, n, T.Text)]+ -- ^ The available choices, in order. Each choice has a value+ -- of type @a@, a resource name, and a text label.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+radioCustomField lb check rb stLens options initialState =+ let initVal = initialState ^. stLens++ lookupOptionValue n =+ let results = filter (\(_, n', _) -> n' == n) options+ in case results of+ [(val, _, _)] -> Just val+ _ -> Nothing++ handleEvent _ (MouseDown n _ _ _) =+ case lookupOptionValue n of+ Nothing -> return ()+ Just v -> put v+ handleEvent new (VtyEvent (EvKey (KChar ' ') [])) = put new+ handleEvent _ _ = return ()++ optionFields = mkOptionField <$> options+ mkOptionField (val, name, label) =+ FormField name+ Just+ True+ (renderRadio lb check rb val name label)+ (handleEvent val)++ in FormFieldState { formFieldState = initVal+ , formFields = optionFields+ , formFieldLens = stLens+ , formFieldUpdate = \val _ -> val+ , formFieldRenderHelper = id+ , formFieldConcat = vBox+ , formFieldVisibilityMode = ShowFocusedFieldOnly+ }++renderRadio :: (Eq a, Ord n) => Char -> Char -> Char -> a -> n -> T.Text -> Bool -> a -> Widget n+renderRadio lb check rb val name label foc cur =+ let addAttr = if foc+ then withDefAttr focusedFormInputAttr+ else id+ isSet = val == cur+ csr = if foc then putCursor name (Location (1,0)) else id+ in clickable name $+ addAttr $ csr $+ txt $ T.concat+ [ T.singleton lb+ , if isSet then T.singleton check else " "+ , T.singleton rb <> " " <> label+ ]++-- | A form field for using an editor to edit the text representation of+-- a value. The other editing fields in this module are special cases of+-- this function.+--+-- This field's attributes are governed by those exported from+-- 'Brick.Widgets.Edit'.+--+-- This field responds to all events handled by 'editor', including+-- mouse events.+editField :: (Ord n, Show n)+ => Lens' s a+ -- ^ The state lens for this value.+ -> n+ -- ^ The resource name for the input field.+ -> Maybe Int+ -- ^ The optional line limit for the editor (see 'editor').+ -> (a -> T.Text)+ -- ^ The initialization function that turns your value into+ -- the editor's initial contents. The resulting text may+ -- contain newlines.+ -> ([T.Text] -> Maybe a)+ -- ^ The validation function that converts the editor's+ -- contents into a valid value of type @a@.+ -> ([T.Text] -> Widget n)+ -- ^ The rendering function for the editor's contents (see+ -- 'renderEditor').+ -> (Widget n -> Widget n)+ -- ^ A rendering augmentation function to adjust the+ -- representation of the rendered editor.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+editField stLens n limit ini val renderText wrapEditor initialState =+ let initVal = applyEdit gotoEnd $+ editor n limit initialText+ gotoEnd = let ls = T.lines initialText+ pos = (length ls - 1, T.length (last ls))+ in if null ls+ then id+ else Z.moveCursor pos+ initialText = ini $ initialState ^. stLens++ in FormFieldState { formFieldState = initVal+ , formFields = [ FormField n+ (val . getEditContents)+ True+ (\b e -> wrapEditor $ renderEditor renderText b e)+ handleEditorEvent+ ]+ , formFieldLens = stLens+ , formFieldUpdate = \newVal e ->+ let newTxt = ini newVal+ in if newTxt == (T.unlines $ getEditContents e)+ then e+ else applyEdit (Z.insertMany newTxt . Z.clearZipper) e+ , formFieldRenderHelper = id+ , formFieldConcat = vBox+ , formFieldVisibilityMode = ShowFocusedFieldOnly+ }++-- | A form field using a single-line editor to edit the 'Show'+-- representation of a state field value of type @a@. This automatically+-- uses its 'Read' instance to validate the input. This field is mostly+-- useful in cases where the user-facing representation of a value+-- matches the 'Show' representation exactly, such as with 'Int'.+--+-- This field's attributes are governed by those exported from+-- 'Brick.Widgets.Edit'.+--+-- This field responds to all events handled by 'editor', including+-- mouse events.+editShowableField :: (Ord n, Show n, Read a, Show a)+ => Lens' s a+ -- ^ The state lens for this value.+ -> n+ -- ^ The resource name for the input field.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+editShowableField stLens n =+ editShowableFieldWithValidate stLens n (const True)++-- | A form field using a single-line editor to edit the 'Show' representation+-- of a state field value of type @a@. This automatically uses its 'Read'+-- instance to validate the input, and also accepts an additional user-defined+-- pass for validation. This field is mostly useful in cases where the+-- user-facing representation of a value matches the 'Show' representation+-- exactly, such as with 'Int', but you don't want to accept just /any/ 'Int'.+--+-- This field's attributes are governed by those exported from+-- 'Brick.Widgets.Edit'.+--+-- This field responds to all events handled by 'editor', including+-- mouse events.+editShowableFieldWithValidate :: (Ord n, Show n, Read a, Show a)+ => Lens' s a+ -- ^ The state lens for this value.+ -> n+ -- ^ The resource name for the input field.+ -> (a -> Bool)+ -- ^ Additional validation step for input.+ -- 'True' indicates that the value is+ -- valid.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+editShowableFieldWithValidate stLens n isValid =+ let ini = T.pack . show+ val ls = do+ v <- readMaybe $ T.unpack $ T.intercalate "\n" ls+ if isValid v+ then return v+ else Nothing+ limit = Just 1+ renderText = txt . T.unlines+ in editField stLens n limit ini val renderText id++-- | A form field using an editor to edit a text value. Since the value+-- is free-form text, it is always valid.+--+-- This field's attributes are governed by those exported from+-- 'Brick.Widgets.Edit'.+--+-- This field responds to all events handled by 'editor', including+-- mouse events.+editTextField :: (Ord n, Show n)+ => Lens' s T.Text+ -- ^ The state lens for this value.+ -> n+ -- ^ The resource name for the input field.+ -> Maybe Int+ -- ^ The optional line limit for the editor (see 'editor').+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+editTextField stLens n limit =+ let ini = id+ val = Just . T.intercalate "\n"+ renderText = txt . T.intercalate "\n"+ in editField stLens n limit ini val renderText id++-- | A form field using a single-line editor to edit a free-form text+-- value represented as a password. The value is always considered valid+-- and is always represented with one asterisk per password character.+--+-- This field's attributes are governed by those exported from+-- 'Brick.Widgets.Edit'.+--+-- This field responds to all events handled by 'editor', including+-- mouse events.+editPasswordField :: (Ord n, Show n)+ => Lens' s T.Text+ -- ^ The state lens for this value.+ -> n+ -- ^ The resource name for the input field.+ -> s+ -- ^ The initial form state.+ -> FormFieldState s e n+editPasswordField stLens n =+ let ini = id+ val = Just . T.concat+ limit = Just 1+ renderText = toPassword+ in editField stLens n limit ini val renderText id++toPassword :: [T.Text] -> Widget a+toPassword s = txt $ T.replicate (T.length $ T.concat s) "*"++-- | The namespace for the other form attributes.+formAttr :: AttrName+formAttr = attrName "brickForm"++-- | The attribute for form input fields with invalid values. Note that+-- this attribute will affect any field considered invalid and will take+-- priority over any attributes that the field uses to render itself.+invalidFormInputAttr :: AttrName+invalidFormInputAttr = formAttr <> attrName "invalidInput"++-- | The attribute for form input fields that have the focus. Note that+-- this attribute only affects fields that do not already use their own+-- attributes when rendering, such as editor- and list-based fields.+-- Those need to be styled by setting the appropriate attributes; see+-- the documentation for field constructors to find out which attributes+-- need to be configured.+focusedFormInputAttr :: AttrName+focusedFormInputAttr = formAttr <> attrName "focusedInput"++-- | Returns whether all form fields in the form currently have valid+-- values according to the fields' validation functions. This is useful+-- when we need to decide whether the form state is up to date with+-- respect to the form input fields.+allFieldsValid :: Form s e n -> Bool+allFieldsValid = null . invalidFields++-- | Returns the resource names associated with all form input fields+-- that currently have invalid inputs. This is useful when we need to+-- force the user to repair invalid inputs before moving on from a form+-- editing session.+invalidFields :: Form s e n -> [n]+invalidFields f = concatMap getInvalidFields (formFieldStates f)++-- | Set the visibility mode of the specified form field's collection+-- when the form is rendered in viewport. This is used to change how+-- focused fields are brought into view when they're outside of view+-- in a viewport and gain focus. In practice, this means this function+-- need only be called on one form field name in a collection in order+-- to affect the visibility behavior of that field's entire input+-- collection.+--+-- There are two visibility modes:+--+-- * 'ShowFocusedFieldOnly' - this is the default behavior. In this+-- mode, when a field receives focus, it is brought into view but+-- other inputs in the same field collection (e.g. a set of radio+-- buttons) will not be brought into view along with it.+--+-- * 'ShowCompositeField' - in this mode, when a field receives focus,+-- all of the inputs in its collection (e.g. a set of radio buttons)+-- are brought into view as long as the viewport is large enough to+-- show them all. If it isn't, the viewport will show as many as space+-- allows.+--+-- * 'ShowAugmentedField' - in this mode, when a field receives focus,+-- all of the inputs in its collection (e.g. a set of radio buttons)+-- and its rendering augmentations (as applied with '@@=') are brought+-- into view as long as the viewport is large enough to show them all.+setFieldVisibilityMode :: (Eq n)+ => n+ -- ^ The name of the form field whose visibility mode is to be set.+ -> FormFieldVisibilityMode+ -- ^ The mode to set.+ -> Form s e n+ -- ^ The form to modify.+ -> Form s e n+setFieldVisibilityMode n mode form =+ let go1 [] = []+ go1 (s:ss) =+ let s' = case s of+ FormFieldState st l upd fs rh concatAll _ ->+ if n `elem` formFieldNames s+ then FormFieldState st l upd fs rh concatAll mode+ else s+ in s' : go1 ss++ in form { formFieldStates = go1 (formFieldStates form) }++-- | Manually indicate that a field has invalid contents. This can be+-- useful in situations where validation beyond the form element's+-- validator needs to be performed and the result of that validation+-- needs to be fed back into the form state.+setFieldValid :: (Eq n)+ => Bool+ -- ^ Whether the field is considered valid.+ -> n+ -- ^ The name of the form field to set as (in)valid.+ -> Form s e n+ -- ^ The form to modify.+ -> Form s e n+setFieldValid v n form =+ let go1 [] = []+ go1 (s:ss) =+ let s' = case s of+ FormFieldState st l upd fs rh concatAll visMode ->+ let go2 [] = []+ go2 (f@(FormField fn val _ r h):ff)+ | n == fn = FormField fn val v r h : ff+ | otherwise = f : go2 ff+ in FormFieldState st l upd (go2 fs) rh concatAll visMode+ in s' : go1 ss++ in form { formFieldStates = go1 (formFieldStates form) }++getInvalidFields :: FormFieldState s e n -> [n]+getInvalidFields (FormFieldState st _ _ fs _ _ _) =+ let gather (FormField n validate extValid _ _) =+ if not extValid || isNothing (validate st) then [n] else []+ in concatMap gather fs++-- | Render a form.+--+-- For each form field, each input for the field is rendered using+-- the implementation provided by its 'FormField'. The inputs are+-- then concatenated with the field's concatenation function (see+-- 'setFieldConcat') and are then augmented using the form field's+-- rendering augmentation function (see '@@='). Fields with invalid+-- inputs (either due to built-in validator failure or due to external+-- validation failure via 'setFieldValid') will be displayed using the+-- 'invalidFormInputAttr' attribute.+--+-- Finally, all of the resulting field renderings are concatenated with+-- the form's concatenation function (see 'setFormConcat'). A visibility+-- request is also issued for the currently-focused form field in case+-- the form is rendered within a viewport.+renderForm :: (Eq n) => Form s e n -> Widget n+renderForm (Form es fr _ concatAll) =+ concatAll $ renderFormFieldState fr <$> es++-- | Render a single form field collection. This is called internally by+-- 'renderForm' but is exposed in cases where a form field state needs+-- to be rendered outside of a 'Form', so 'renderForm' is probably what+-- you want.+renderFormFieldState :: (Eq n)+ => FocusRing n+ -> FormFieldState s e n+ -> Widget n+renderFormFieldState fr (FormFieldState st _ _ fields helper concatFields visMode) =+ let curFocus = focusGetCurrent fr+ foc = case curFocus of+ Nothing -> False+ Just n -> n `elem` fieldNames+ maybeVisible = if foc && visMode == ShowCompositeField then visible else id+ renderFields [] = []+ renderFields ((FormField n validate extValid renderField _):fs) =+ let maybeInvalid = if (isJust $ validate st) && extValid+ then id+ else forceAttr invalidFormInputAttr+ fieldFoc = Just n == curFocus+ maybeFieldVisible = if fieldFoc && visMode == ShowFocusedFieldOnly then visible else id+ in (n, maybeFieldVisible $ maybeInvalid $ renderField fieldFoc st) : renderFields fs+ (fieldNames, renderedFields) = unzip $ renderFields fields+ maybeHelperVisible =+ if foc && visMode == ShowAugmentedField then visible else id+ in maybeHelperVisible $ helper $ maybeVisible $ concatFields renderedFields++-- | Dispatch an event to the currently focused form field. This handles+-- the following events in this order:+--+-- * On @Tab@ keypresses, this changes the focus to the next field in+-- the form.+-- * On @Shift-Tab@ keypresses, this changes the focus to the previous+-- field in the form.+-- * On mouse button presses (regardless of button or modifier), the+-- focus is changed to the clicked form field and the event is+-- forwarded to the event handler for the clicked form field.+-- * On @Left@ or @Up@, if the currently-focused field is part of a+-- collection (e.g. radio buttons), the previous entry in the+-- collection is focused.+-- * On @Right@ or @Down@, if the currently-focused field is part of a+-- collection (e.g. radio buttons), the next entry in the collection+-- is focused.+-- * All other events are forwarded to the currently focused form field.+--+-- In all cases where an event is forwarded to a form field, validation+-- of the field's input state is performed immediately after the+-- event has been handled. If the form field's input state succeeds+-- validation using the field's validator function, its value is+-- immediately stored in the form state using the form field's state+-- lens. The external validation flag is ignored during this step to+-- ensure that external validators have a chance to get the intermediate+-- validated value.+handleFormEvent :: (Eq n) => BrickEvent n e -> EventM n (Form s e n) ()+handleFormEvent (VtyEvent (EvKey (KChar '\t') [])) =+ formFocusL %= focusNext+handleFormEvent (VtyEvent (EvKey KBackTab [])) =+ formFocusL %= focusPrev+handleFormEvent e@(MouseDown n _ _ _) = do+ formFocusL %= focusSetCurrent n+ handleFormFieldEvent e n+handleFormEvent e@(MouseUp n _ _) = do+ formFocusL %= focusSetCurrent n+ handleFormFieldEvent e n+handleFormEvent e@(VtyEvent (EvKey KUp [])) =+ withFocusAndGrouping e $ \n grp ->+ formFocusL %= focusSetCurrent (entryBefore grp n)+handleFormEvent e@(VtyEvent (EvKey KDown [])) =+ withFocusAndGrouping e $ \n grp ->+ formFocusL %= focusSetCurrent (entryAfter grp n)+handleFormEvent e@(VtyEvent (EvKey KLeft [])) =+ withFocusAndGrouping e $ \n grp ->+ formFocusL %= focusSetCurrent (entryBefore grp n)+handleFormEvent e@(VtyEvent (EvKey KRight [])) =+ withFocusAndGrouping e $ \n grp ->+ formFocusL %= focusSetCurrent (entryAfter grp n)+handleFormEvent e =+ forwardToCurrent e++getFocusGrouping :: (Eq n) => Form s e n -> n -> Maybe [n]+getFocusGrouping f n = findGroup (formFieldStates f)+ where+ findGroup [] = Nothing+ findGroup (e:es) =+ let ns = formFieldNames e+ in if n `elem` ns && length ns > 1+ then Just ns+ else findGroup es++entryAfter :: (Eq a) => [a] -> a -> a+entryAfter as a =+ let i = fromJust $ elemIndex a as+ i' = if i == length as - 1 then 0 else i + 1+ in as !! i'++entryBefore :: (Eq a) => [a] -> a -> a+entryBefore as a =+ let i = fromJust $ elemIndex a as+ i' = if i == 0 then length as - 1 else i - 1+ in as !! i'++withFocusAndGrouping :: (Eq n)+ => BrickEvent n e+ -> (n -> [n] -> EventM n (Form s e n) ())+ -> EventM n (Form s e n) ()+withFocusAndGrouping e act = do+ foc <- gets formFocus+ case focusGetCurrent foc of+ Nothing -> return ()+ Just n -> do+ f <- get+ case getFocusGrouping f n of+ Nothing -> forwardToCurrent e+ Just grp -> act n grp++withFocus :: (n -> EventM n (Form s e n) ()) -> EventM n (Form s e n) ()+withFocus act = do+ foc <- gets formFocus+ case focusGetCurrent foc of+ Nothing -> return ()+ Just n -> act n++forwardToCurrent :: (Eq n) => BrickEvent n e -> EventM n (Form s e n) ()+forwardToCurrent =+ withFocus . handleFormFieldEvent++handleFormFieldEvent :: (Eq n) => BrickEvent n e -> n -> EventM n (Form s e n) ()+handleFormFieldEvent ev n = do+ let findFieldState _ [] = return ()+ findFieldState prev (e:es) =+ case e of+ FormFieldState st stLens upd fields helper concatAll visMode -> do+ let findField [] = return Nothing+ findField (field:rest) =+ case field of+ FormField n' validate _ _ handleFunc | n == n' -> do+ (nextSt, ()) <- nestEventM st (handleFunc ev)+ -- If the new state validates, go ahead and update+ -- the form state with it.+ case validate nextSt of+ Nothing -> return $ Just (nextSt, Nothing)+ Just newSt -> return $ Just (nextSt, Just newSt)+ _ -> findField rest++ result <- findField fields+ case result of+ Nothing -> findFieldState (prev <> [e]) es+ Just (newSt, maybeSt) -> do+ let newFieldState = FormFieldState newSt stLens upd fields helper concatAll visMode+ formFieldStatesL .= prev <> [newFieldState] <> es+ case maybeSt of+ Nothing -> return ()+ Just s -> formStateL.stLens .= s++ states <- gets formFieldStates+ findFieldState [] states
+ src/Brick/Keybindings.hs view
@@ -0,0 +1,20 @@+-- | The re-exporting catch-all module for the customizable keybindings+-- API.+--+-- To get started using this API, see the documentation in+-- @KeyDispatcher@ as well as the User Guide section on customizable+-- keybindings.+module Brick.Keybindings+ ( module Brick.Keybindings.KeyEvents+ , module Brick.Keybindings.KeyConfig+ , module Brick.Keybindings.KeyDispatcher+ , module Brick.Keybindings.Pretty+ , module Brick.Keybindings.Parse+ )+where++import Brick.Keybindings.KeyEvents+import Brick.Keybindings.KeyConfig+import Brick.Keybindings.KeyDispatcher+import Brick.Keybindings.Pretty+import Brick.Keybindings.Parse
+ src/Brick/Keybindings/KeyConfig.hs view
@@ -0,0 +1,258 @@+-- | This module provides 'KeyConfig' and associated functions. A+-- 'KeyConfig' is the basis for the custom keybinding system in this+-- library.+--+-- 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+ , BindingState(..)++ -- * Specifying bindings+ , Binding(..)+ , ToBinding(..)+ , binding+ , fn+ , meta+ , ctrl+ , shift++ -- * Querying KeyConfigs+ , firstDefaultBinding+ , firstActiveBinding+ , allDefaultBindings+ , allActiveBindings+ , keyEventMappings++ -- * Misc+ , keyConfigEvents+ , lookupKeyConfigBindings+ )+where++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, catMaybes)+import qualified Graphics.Vty as Vty++import Brick.Keybindings.KeyEvents+import Brick.Keybindings.Normalize++-- | A key binding.+--+-- The easiest way to express 'Binding's is to use the helper functions+-- in this module that work with instances of 'ToBinding', e.g.+--+-- @+-- let ctrlB = 'ctrl' \'b\'+-- shiftX = 'shift' \'x\'+-- ctrlMetaK = 'ctrl' $ 'meta' \'k\'+-- -- Or with Vty keys directly:+-- ctrlDown = 'ctrl' 'Graphics.Vty.Input.KDown'+-- @+data Binding =+ Binding { kbKey :: Vty.Key+ -- ^ The key itself.+ , kbMods :: S.Set Vty.Modifier+ -- ^ The set of modifiers.+ } deriving (Eq, Show, Ord)++-- | Construct a 'Binding'. Modifier order is ignored. If modifiers+-- are given and the binding is for a character key, it is forced to+-- lowercase.+binding :: Vty.Key -> [Vty.Modifier] -> Binding+binding k mods =+ Binding { kbKey = normalizeKey mods k+ , kbMods = S.fromList mods+ }++-- | An explicit configuration of key bindings for a key event.+data BindingState =+ BindingList [Binding]+ -- ^ Bind the event to the specified list of bindings.+ | Unbound+ -- ^ Disable all bindings for the event, including default bindings.+ deriving (Show, Eq, Ord)++-- | A configuration of custom key bindings. A 'KeyConfig'+-- stores everything needed to resolve a key event into one or+-- more key bindings. Make a 'KeyConfig' with 'newKeyConfig',+-- then use it to dispatch to 'KeyEventHandler's with+-- 'Brick.Keybindings.KeyHandlerMap.keyDispatcher'.+--+-- Make a new 'KeyConfig' with 'newKeyConfig'.+--+-- A 'KeyConfig' stores:+--+-- * A collection of named key events, mapping the event type @k@ to+-- 'Text' labels.+-- * For each event @k@, optionally store a list of default key bindings+-- for that event.+-- * An optional customized binding list for each event, setting the+-- event to either 'Unbound' or providing explicit overridden bindings+-- with 'BindingList'.+data KeyConfig k =+ 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.+ , keyConfigDefaultBindings :: M.Map k [Binding]+ -- ^ A mapping of events and their default key bindings,+ -- if any.+ }+ deriving (Show, Eq)++-- | Build a 'KeyConfig' with the specified 'KeyEvents' event-to-name+-- mapping, list of default bindings by event, and list of custom+-- bindings by event.+newKeyConfig :: (Ord k)+ => KeyEvents k+ -- ^ The base mapping of key events and names to use.+ -> [(k, [Binding])]+ -- ^ Default bindings by key event, such as from a+ -- configuration file or embedded code. Optional on a+ -- per-event basis.+ -> [(k, BindingState)]+ -- ^ 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. 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 { 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 = lookup e $ keyConfigCustomBindings kc++-- | A convenience function to return the first result of+-- 'allDefaultBindings', if any.+firstDefaultBinding :: (Show k, Ord k) => KeyConfig k -> k -> Maybe Binding+firstDefaultBinding kc ev = do+ bs <- M.lookup ev (keyConfigDefaultBindings kc)+ case bs of+ (b:_) -> Just b+ _ -> Nothing++-- | Returns the list of default bindings for the specified event,+-- irrespective of whether the event has been explicitly configured with+-- other bindings or set to 'Unbound'.+allDefaultBindings :: (Ord k) => KeyConfig k -> k -> [Binding]+allDefaultBindings kc ev =+ fromMaybe [] $ M.lookup ev (keyConfigDefaultBindings kc)++-- | A convenience function to return the first result of+-- 'allActiveBindings', if any.+firstActiveBinding :: (Show k, Ord k) => KeyConfig k -> k -> Maybe Binding+firstActiveBinding kc ev = listToMaybe $ allActiveBindings kc ev++-- | Return all active key bindings for the specified event. This+-- returns customized bindings if any have been set in the 'KeyConfig',+-- no bindings if the event has been explicitly set to 'Unbound', or the+-- default bindings if the event is absent from the customized bindings.+allActiveBindings :: (Show k, Ord k) => KeyConfig k -> k -> [Binding]+allActiveBindings kc ev = nub foundBindings+ where+ defaultBindings = allDefaultBindings kc ev+ foundBindings = case lookupKeyConfigBindings kc ev of+ Just (BindingList bs) -> bs+ Just Unbound -> []+ Nothing -> defaultBindings++-- | The class of types that can form the basis of 'Binding's.+--+-- This is provided to make it easy to write and modify bindings in less+-- verbose ways.+class ToBinding a where+ -- | Binding constructor.+ bind :: a -> Binding++instance ToBinding Vty.Key where+ bind k = Binding { kbMods = mempty, kbKey = k }++instance ToBinding Char where+ bind = bind . Vty.KChar++instance ToBinding Binding where+ bind = id++addModifier :: (ToBinding a) => Vty.Modifier -> a -> Binding+addModifier m val =+ let b = bind val+ newMods = S.insert m $ kbMods b+ in b { kbMods = newMods+ , kbKey = normalizeKey (S.toList newMods) $ kbKey b+ }++-- | Add Meta to a binding. If the binding is for a character key, force+-- it to lowercase.+meta :: (ToBinding a) => a -> Binding+meta = addModifier Vty.MMeta++-- | Add Ctrl to a binding. If the binding is for a character key, force+-- it to lowercase.+ctrl :: (ToBinding a) => a -> Binding+ctrl = addModifier Vty.MCtrl++-- | Add Shift to a binding. If the binding is for a character key, force+-- it to lowercase.+shift :: (ToBinding a) => a -> Binding+shift = addModifier Vty.MShift++-- | Function key binding.+fn :: Int -> Binding+fn = bind . Vty.KFun
+ src/Brick/Keybindings/KeyDispatcher.hs view
@@ -0,0 +1,241 @@+-- | This is the entry point into the keybinding infrastructure in+-- this library. Note that usage of this API is not required to create+-- working Brick applications; this API is provided for applications+-- that need to support custom keybindings that are less tightly coupled+-- to application behavior.+--+-- The workflow for this API is as follows:+--+-- * Create a data type @k@ with a constructor for each abstract+-- application event that you want to trigger with an input key.+-- * To each event @k@, assign a unique user-readable name (such as a+-- name you could imagine using in a configuration file to refer to+-- the event) and a list of default key bindings.+-- * Use the resulting data to create a 'KeyConfig' with 'newKeyConfig'.+-- 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', dealing with collisions if+-- they arise.+-- * As user input events arrive, dispatch them to the appropriate+-- handler in the dispatcher using 'handleKey'.+module Brick.Keybindings.KeyDispatcher+ ( -- * Key dispatching+ KeyDispatcher+ , keyDispatcher+ , handleKey++ -- * Building handlers+ , onEvent+ , onKey++ -- * Handlers and triggers+ , Handler(..)+ , KeyHandler(..)+ , KeyEventHandler(..)+ , EventTrigger(..)++ -- * Misc+ , keyDispatcherToList+ , lookupVtyEvent+ )+where++import qualified Data.Map.Strict as M+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 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))++-- | 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.+-- Instead, use 'onEvent' and 'onKey'. This type's internals are exposed+-- for easy inspection, not construction.+data Handler m =+ Handler { handlerDescription :: T.Text+ -- ^ The description of this handler's behavior.+ , handlerAction :: m ()+ -- ^ The action to take when this handler is invoked.+ }++-- | A handler for a specific key.+--+-- 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+ -- the original abstract key event handler; this allows+ -- us to obtain the original 'EventTrigger' for the+ -- 'KeyEventHandler' upon which this 'KeyHandler'+ -- is built. This can be important for keybinding+ -- consistency checks or collision checks as well as help+ -- text generation.+ , khBinding :: Binding+ -- ^ The specific key binding that should trigger this+ -- handler.+ }++-- | Find the key handler that matches a Vty key event, if any. Modifier+-- order is unimportant since the lookup for a matching binding ignores+-- modifier order.+--+-- This works by looking up an event handler whose binding is the+-- specified key and modifiers based on the 'KeyConfig' that was used to+-- build the 'KeyDispatcher'.+--+-- Ordinarily you will not need to use this function; use 'handleKey'+-- instead. This is provided for more direct access to the+-- 'KeyDispatcher' internals.+lookupVtyEvent :: Vty.Key -> [Vty.Modifier] -> KeyDispatcher k m -> Maybe (KeyHandler k m)+lookupVtyEvent k mods (KeyDispatcher m) = M.lookup (Binding k $ S.fromList mods) m++-- | Handle a keyboard event by looking it up in the 'KeyDispatcher'+-- and invoking the matching binding's handler if one is found. Return+-- @True@ if the a matching handler was found and run; return @False@ if+-- no matching binding was found.+handleKey :: (Monad m)+ => KeyDispatcher k m+ -- ^ The dispatcher to use.+ -> Vty.Key+ -- ^ The key to handle.+ -> [Vty.Modifier]+ -- ^ The modifiers for the key, if any.+ -> m Bool+handleKey d k mods = do+ case lookupVtyEvent k mods d of+ Just kh -> (handlerAction $ kehHandler $ khHandler kh) >> return True+ Nothing -> return False++-- | Build a 'KeyDispatcher' to dispatch keys to handle events of type+-- @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+-- keys using the provided 'KeyConfig' to map between abstract key+-- events of type @k@ and Vty keys. Event handlers triggered by an event+-- @k@ are set up to be triggered by either the customized bindings for+-- @k@ in the 'KeyConfig', no bindings at all if the 'KeyConfig' has+-- marked @k@ as 'Unbound', or the default bindings for @k@ otherwise.+--+-- Once you have a 'KeyDispatcher', you can dispatch an input key event+-- to it and invoke the corresponding handler (if any) with 'handleKey'.+keyDispatcher :: (Ord k)+ => KeyConfig k+ -> [KeyEventHandler k m]+ -> 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.+keyDispatcherToList :: KeyDispatcher k m+ -> [(Binding, KeyHandler k m)]+keyDispatcherToList (KeyDispatcher m) = M.toList m++buildKeyDispatcherPairs :: (Ord k)+ => [KeyEventHandler k m]+ -> KeyConfig k+ -> [(Binding, KeyHandler k m)]+buildKeyDispatcherPairs ks conf = pairs+ where+ pairs = mkPair <$> handlers+ mkPair h = (khBinding h, h)+ handlers = concatMap (keyHandlersFromConfig conf) ks++keyHandlersFromConfig :: (Ord k)+ => KeyConfig k+ -> KeyEventHandler k m+ -> [KeyHandler k m]+keyHandlersFromConfig kc eh =+ let allBindingsFor ev | Just (BindingList ks) <- lookupKeyConfigBindings kc ev = ks+ | Just Unbound <- lookupKeyConfigBindings kc ev = []+ | otherwise = allDefaultBindings kc ev+ bindings = case kehEventTrigger eh of+ ByKey b -> [b]+ ByEvent ev -> allBindingsFor ev+ in [ KeyHandler { khHandler = eh, khBinding = b } | b <- bindings ]++mkHandler :: T.Text -> m () -> Handler m+mkHandler msg action =+ Handler { handlerDescription = msg+ , handlerAction = action+ }++-- | Specify a handler for the specified key event.+onEvent :: k+ -- ^ The key event whose bindings should trigger this handler.+ -> T.Text+ -- ^ The description of the handler.+ -> m ()+ -- ^ The handler to invoke.+ -> KeyEventHandler k m+onEvent ev msg action =+ KeyEventHandler { kehHandler = mkHandler msg action+ , kehEventTrigger = ByEvent ev+ }++-- | Specify a handler for the specified key.+onKey :: (ToBinding a)+ => a+ -- ^ The binding that should trigger this handler.+ -> T.Text+ -- ^ The description of the handler.+ -> m ()+ -- ^ The handler to invoke.+ -> KeyEventHandler k m+onKey b msg action =+ KeyEventHandler { kehHandler = mkHandler msg action+ , kehEventTrigger = ByKey $ bind b+ }++-- | A trigger for an event handler.+data EventTrigger k =+ ByKey Binding+ -- ^ The key event is always triggered by a specific key.+ | ByEvent k+ -- ^ The trigger is an abstract key event.+ deriving (Show, Eq, Ord)++-- | A handler for an abstract key event.+--+-- In general, you should never need to create these manually. Instead,+-- use 'onEvent' and 'onKey'. The internals of this type are exposed to+-- allow inspection of handler data for e.g. custom help generation.+data KeyEventHandler k m =+ KeyEventHandler { kehHandler :: Handler m+ -- ^ The handler to invoke.+ , kehEventTrigger :: EventTrigger k+ -- ^ The trigger for the handler.+ }
+ src/Brick/Keybindings/KeyEvents.hs view
@@ -0,0 +1,52 @@+-- | This module provides 'KeyEvents', a data type for mapping+-- application-defined abstract events to user-facing names (e.g.+-- for use in configuration files and documentation). This data+-- structure gives you a place to define the correspondence between+-- your application's key events and their names. A 'KeyEvents' also+-- effectively tells the key binding system about the collection of+-- possible abstract events that can be handled.+--+-- A 'KeyEvents' is used to construct a+-- 'Brick.Keybindings.KeyConfig.KeyConfig' with+-- 'Brick.Keybindings.KeyConfig.newKeyConfig'.+module Brick.Keybindings.KeyEvents+ ( KeyEvents+ , keyEvents+ , keyEventsList+ , lookupKeyEvent+ , keyEventName+ )+where++import qualified Data.Bimap as B+import qualified Data.Text as T++-- | A bidirectional mapping between events @k@ and their user-readable+-- names.+data KeyEvents k = KeyEvents (B.Bimap T.Text k)+ deriving (Eq, Show)++-- | Build a new 'KeyEvents' map from the specified list of events and+-- names. Key event names are stored in lowercase.+--+-- Calls 'error' if any events have the same name (ignoring case) or if+-- multiple names map to the same event.+keyEvents :: (Ord k) => [(T.Text, k)] -> KeyEvents k+keyEvents pairs =+ let m = B.fromList [(T.strip $ T.toLower n, e) | (n, e) <- pairs]+ in if B.size m /= length pairs+ then error "keyEvents: input list contains duplicates by name or by event value"+ else KeyEvents $ B.fromList pairs++-- | Convert the 'KeyEvents' to a list.+keyEventsList :: KeyEvents k -> [(T.Text, k)]+keyEventsList (KeyEvents m) = B.toList m++-- | Look up the specified event name to get its abstract event. The+-- lookup ignores leading and trailing whitespace as well as case.+lookupKeyEvent :: (Ord k) => KeyEvents k -> T.Text -> Maybe k+lookupKeyEvent (KeyEvents m) name = B.lookup (T.strip $ T.toLower name) m++-- | Given an abstract event, get its event name.+keyEventName :: (Ord k) => KeyEvents k -> k -> Maybe T.Text+keyEventName (KeyEvents m) e = B.lookupR e m
+ src/Brick/Keybindings/Normalize.hs view
@@ -0,0 +1,14 @@+module Brick.Keybindings.Normalize+ ( normalizeKey+ )+where++import Data.Char (toLower)+import qualified Graphics.Vty as Vty++-- | A keybinding involving modifiers should have its key character+-- normalized to lowercase since it's impossible to get uppercase keys+-- from the terminal when modifiers are present.+normalizeKey :: [Vty.Modifier] -> Vty.Key -> Vty.Key+normalizeKey (_:_) (Vty.KChar c) = Vty.KChar $ toLower c+normalizeKey _ k = k
+ src/Brick/Keybindings/Parse.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE OverloadedStrings #-}+-- | This module provides key binding string parsing functions for use+-- in e.g. reading key bindings from configuration files.+module Brick.Keybindings.Parse+ ( parseBinding+ , parseBindingList+ , normalizeKey++ , keybindingsFromIni+ , keybindingsFromFile+ , keybindingIniParser+ )+where++import Control.Monad (forM)+import Data.Maybe (catMaybes)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import qualified Graphics.Vty as Vty+import Text.Read (readMaybe)+import qualified Data.Ini.Config as Ini++import Brick.Keybindings.KeyEvents+import Brick.Keybindings.KeyConfig+import Brick.Keybindings.Normalize++-- | Parse a key binding list into a 'BindingState'.+--+-- A key binding list either the string @"unbound"@ or is a+-- comma-separated list of 'Binding's parsed with 'parseBinding'.+parseBindingList :: T.Text -> Either String BindingState+parseBindingList t =+ if T.toLower t == "unbound"+ then return Unbound+ else BindingList <$> mapM (parseBinding . T.strip) (T.splitOn "," $ T.strip t)++-- | Parse a key binding string. Key binding strings specify zero or+-- more modifier keys and a base key, separated by hyphens.+--+-- @+-- (modifier "-")* key+-- @+--+-- e.g. @c-down@, @backspace@, @ctrl-shift-f1@.+--+-- where each @modifier@ is parsed case-insensitively as follows:+--+-- * @"s", "shift"@: 'Vty.MShift'+-- * @"m", "meta"@: 'Vty.MMeta'+-- * @"a", "alt"@: 'Vty.MAlt'+-- * @"c", "ctrl", "control"@: 'Vty.MCtrl'+--+-- and @key@ is parsed case-insensitively as follows:+--+-- * "f1", "f2", ...: 'Vty.KFun'+-- * "esc": 'Vty.KEsc'+-- * "backspace": 'Vty.KBS'+-- * "enter": 'Vty.KEnter'+-- * "left": 'Vty.KLeft'+-- * "right": 'Vty.KRight'+-- * "up": 'Vty.KUp'+-- * "down": 'Vty.KDown'+-- * "upleft": 'Vty.KUpLeft'+-- * "upright": 'Vty.KUpRight'+-- * "downleft": 'Vty.KDownLeft'+-- * "downright": 'Vty.KDownRight'+-- * "center": 'Vty.KCenter'+-- * "backtab": 'Vty.KBackTab'+-- * "printscreen": 'Vty.KPrtScr'+-- * "pause": 'Vty.KPause'+-- * "insert": 'Vty.KIns'+-- * "home": 'Vty.KHome'+-- * "pgup": 'Vty.KPageUp'+-- * "del": 'Vty.KDel'+-- * "end": 'Vty.KEnd'+-- * "pgdown": 'Vty.KPageDown'+-- * "begin": 'Vty.KBegin'+-- * "menu": 'Vty.KMenu'+-- * "space": @' '@+-- * "tab": @'\\t'@+-- * Otherwise, 'Vty.KChar'+parseBinding :: T.Text -> Either String Binding+parseBinding s = go (T.splitOn "-" $ T.toLower s) []+ where go [k] mods = do+ k' <- pKey k+ return $ binding k' mods+ go (k:ks) mods = do+ m <- case k of+ "s" -> return Vty.MShift+ "shift" -> return Vty.MShift+ "m" -> return Vty.MMeta+ "meta" -> return Vty.MMeta+ "a" -> return Vty.MAlt+ "alt" -> return Vty.MAlt+ "c" -> return Vty.MCtrl+ "ctrl" -> return Vty.MCtrl+ "control" -> return Vty.MCtrl+ _ -> Left ("Unknown modifier prefix: " ++ show k)+ go ks (m:mods)+ go [] _ = Left "Empty keybinding not allowed"+ pKey "esc" = return Vty.KEsc+ pKey "backspace" = return Vty.KBS+ pKey "enter" = return Vty.KEnter+ pKey "left" = return Vty.KLeft+ pKey "right" = return Vty.KRight+ pKey "up" = return Vty.KUp+ pKey "down" = return Vty.KDown+ pKey "upleft" = return Vty.KUpLeft+ pKey "upright" = return Vty.KUpRight+ pKey "downleft" = return Vty.KDownLeft+ pKey "downright" = return Vty.KDownRight+ pKey "center" = return Vty.KCenter+ pKey "backtab" = return Vty.KBackTab+ pKey "printscreen" = return Vty.KPrtScr+ pKey "pause" = return Vty.KPause+ pKey "insert" = return Vty.KIns+ pKey "home" = return Vty.KHome+ pKey "pgup" = return Vty.KPageUp+ pKey "del" = return Vty.KDel+ pKey "end" = return Vty.KEnd+ pKey "pgdown" = return Vty.KPageDown+ pKey "begin" = return Vty.KBegin+ pKey "menu" = return Vty.KMenu+ pKey "space" = return (Vty.KChar ' ')+ pKey "tab" = return (Vty.KChar '\t')+ pKey t+ | T.length t == 1 =+ return (Vty.KChar $ T.last s)+ | Just n <- T.stripPrefix "f" t =+ case readMaybe (T.unpack n) of+ Nothing -> Left ("Unknown keybinding: " ++ show t)+ Just i -> return (Vty.KFun i)+ | otherwise = Left ("Unknown keybinding: " ++ show t)++-- | Parse custom key bindings from the specified INI file using the+-- provided event name mapping.+--+-- Each line in the specified section can take the form+--+-- > <event-name> = <"unbound"|[binding,...]>+--+-- where the event name must be a valid event name in the specified+-- 'KeyEvents' and each binding is valid as parsed by 'parseBinding'.+--+-- Returns @Nothing@ if the named section was not found; otherwise+-- returns a (possibly empty) list of binding states for each event in+-- @evs@.+keybindingsFromIni :: KeyEvents k+ -- ^ The key event name mapping to use to parse the+ -- configuration data.+ -> T.Text+ -- ^ The name of the INI configuration section to+ -- read.+ -> T.Text+ -- ^ The text of the INI document to read.+ -> Either String (Maybe [(k, BindingState)])+keybindingsFromIni evs section doc =+ Ini.parseIniFile doc (keybindingIniParser evs section)++-- | Parse custom key bindings from the specified INI file path. This+-- does not catch or convert any exceptions resulting from I/O errors.+-- See 'keybindingsFromIni' for details.+keybindingsFromFile :: KeyEvents k+ -- ^ The key event name mapping to use to parse the+ -- configuration data.+ -> T.Text+ -- ^ The name of the INI configuration section to+ -- read.+ -> FilePath+ -- ^ The path to the INI file to read.+ -> IO (Either String (Maybe [(k, BindingState)]))+keybindingsFromFile evs section path =+ keybindingsFromIni evs section <$> T.readFile path++-- | The low-level INI parser for custom key bindings used by this+-- module, exported for applications that use the @config-ini@ package.+keybindingIniParser :: KeyEvents k -> T.Text -> Ini.IniParser (Maybe [(k, BindingState)])+keybindingIniParser evs section =+ Ini.sectionMb section $ do+ fmap catMaybes $ forM (keyEventsList evs) $ \(name, e) -> do+ fmap (e,) <$> Ini.fieldMbOf name parseBindingList
+ src/Brick/Keybindings/Pretty.hs view
@@ -0,0 +1,248 @@+{-# LANGUAGE OverloadedStrings #-}+-- | This module provides functions for pretty-printing key bindings+-- and for generating Markdown, plain text, and Brick displays of event+-- handler key binding configurations.+module Brick.Keybindings.Pretty+ (+ -- * Generating help output+ keybindingTextTable+ , keybindingMarkdownTable+ , keybindingHelpWidget++ -- * Pretty-printing primitives+ , ppBinding+ , ppMaybeBinding+ , ppKey+ , ppModifier++ -- * Attributes for Widget rendering+ , keybindingHelpBaseAttr+ , eventNameAttr+ , eventDescriptionAttr+ , keybindingAttr+ )+where++import Brick+import Data.List (sort, intersperse)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Data.Set as S+import qualified Data.Text as T+import qualified Graphics.Vty as Vty++import Brick.Keybindings.KeyEvents+import Brick.Keybindings.KeyConfig+import Brick.Keybindings.KeyDispatcher++data TextHunk = Verbatim T.Text+ | Comment T.Text++-- | Generate a Markdown document of sections indicating the key binding+-- state for each event handler.+keybindingMarkdownTable :: (Ord k)+ => KeyConfig k+ -- ^ The key binding configuration in use.+ -> [(T.Text, [KeyEventHandler k m])]+ -- ^ Key event handlers by named section.+ -> T.Text+keybindingMarkdownTable kc sections = title <> keybindSectionStrings+ where title = "# Keybindings\n"+ keybindSectionStrings = T.concat $ sectionText <$> sections+ sectionText (heading, handlers) =+ mkHeading heading <>+ mkKeybindEventSectionHelp kc keybindEventHelpMarkdown T.unlines handlers+ mkHeading n =+ "\n# " <> n <>+ "\n| Keybinding | Event Name | Description |" <>+ "\n| ---------- | ---------- | ----------- |\n"++-- | Generate a plain text document of sections indicating the key+-- binding state for each event handler.+keybindingTextTable :: (Ord k)+ => KeyConfig k+ -- ^ The key binding configuration in use.+ -> [(T.Text, [KeyEventHandler k m])]+ -- ^ Key event handlers by named section.+ -> T.Text+keybindingTextTable kc sections = title <> keybindSectionStrings+ where title = "Keybindings\n===========\n"+ keybindSectionStrings = T.concat $ sectionText <$> sections+ sectionText (heading, handlers) =+ mkHeading heading <>+ mkKeybindEventSectionHelp kc (keybindEventHelpText keybindingWidth eventNameWidth) T.unlines handlers+ keybindingWidth = 15+ eventNameWidth = 30+ mkHeading n =+ "\n" <> n <>+ "\n" <> (T.replicate (T.length n) "=") <>+ "\n"++keybindEventHelpText :: Int -> Int -> (TextHunk, T.Text, [TextHunk]) -> T.Text+keybindEventHelpText width eventNameWidth (evName, desc, evs) =+ let getText (Comment s) = s+ getText (Verbatim s) = s+ in padTo width (T.intercalate ", " $ getText <$> evs) <> " " <>+ padTo eventNameWidth (getText evName) <> " " <>+ desc++padTo :: Int -> T.Text -> T.Text+padTo n s = s <> T.replicate (n - T.length s) " "++mkKeybindEventSectionHelp :: (Ord k)+ => KeyConfig k+ -> ((TextHunk, T.Text, [TextHunk]) -> a)+ -> ([a] -> a)+ -> [KeyEventHandler k m]+ -> a+mkKeybindEventSectionHelp kc mkKeybindHelpFunc vertCat kbs =+ vertCat $ mkKeybindHelpFunc <$> (mkKeybindEventHelp kc <$> kbs)++keybindEventHelpMarkdown :: (TextHunk, T.Text, [TextHunk]) -> T.Text+keybindEventHelpMarkdown (evName, desc, evs) =+ let quote s = "`" <> s <> "`"+ format (Comment s) = s+ format (Verbatim s) = quote s+ name = case evName of+ Comment s -> s+ Verbatim s -> quote s+ in "| " <> (T.intercalate ", " $ format <$> evs) <>+ " | " <> name <>+ " | " <> desc <>+ " |"++mkKeybindEventHelp :: (Ord k)+ => KeyConfig k+ -> KeyEventHandler k m+ -> (TextHunk, T.Text, [TextHunk])+mkKeybindEventHelp kc h =+ let trig = kehEventTrigger h+ unbound = [Comment "(unbound)"]+ (label, evText) = case trig of+ ByKey b ->+ (Comment "(non-customizable key)", [Verbatim $ ppBinding b])+ ByEvent ev ->+ let name = maybe (Comment "(unnamed)") Verbatim $ keyEventName (keyConfigEvents kc) ev+ in case lookupKeyConfigBindings kc ev of+ Nothing ->+ if not (null (allDefaultBindings kc ev))+ then (name, Verbatim <$> ppBinding <$> allDefaultBindings kc ev)+ else (name, unbound)+ Just Unbound ->+ (name, unbound)+ Just (BindingList bs) ->+ let result = if not (null bs)+ then Verbatim <$> ppBinding <$> bs+ else unbound+ in (name, result)+ in (label, handlerDescription $ kehHandler h, evText)++-- | Build a 'Widget' displaying key binding information for a single+-- related group of event handlers. This is provided for convenience+-- so that basic help text for the application's event handlers can be+-- produced and embedded in the UI.+--+-- The resulting widget lists the key events (and keys) bound to the+-- specified handlers, along with the events' names and the list of+-- available key bindings for each handler.+keybindingHelpWidget :: (Ord k)+ => KeyConfig k+ -- ^ The key binding configuration in use.+ -> [KeyEventHandler k m]+ -- ^ The list of the event handlers to include in+ -- the help display.+ -> Widget n+keybindingHelpWidget kc =+ withDefAttr keybindingHelpBaseAttr .+ mkKeybindEventSectionHelp kc keybindEventHelpWidget (vBox . intersperse (str " "))++keybindEventHelpWidget :: (TextHunk, T.Text, [TextHunk]) -> Widget n+keybindEventHelpWidget (evName, desc, evs) =+ let evText = T.intercalate ", " (getText <$> evs)+ getText (Comment s) = s+ getText (Verbatim s) = s+ label = withDefAttr eventNameAttr $ case evName of+ Comment s -> txt s+ Verbatim s -> txt s+ in vBox [ withDefAttr eventDescriptionAttr $ txt desc+ , label <+> txt " = " <+> withDefAttr keybindingAttr (txt evText)+ ]++-- | Pretty-print a 'Binding' in the same format that is parsed by+-- 'Brick.Keybindings.Parse.parseBinding'.+ppBinding :: Binding -> T.Text+ppBinding (Binding k mods) =+ T.intercalate "-" $ (ppModifier <$> modifierList mods) <> [ppKey k]++modifierList :: S.Set Vty.Modifier -> [Vty.Modifier]+modifierList = sort . S.toList++-- | Pretty-print a 'Binding' in the same format that is parsed by+-- 'Brick.Keybindings.Parse.parseBinding'; if no binding is given,+-- produce a message indicating no binding.+ppMaybeBinding :: Maybe Binding -> T.Text+ppMaybeBinding Nothing =+ "(no binding)"+ppMaybeBinding (Just b) =+ ppBinding b++-- | Pretty-print a 'Vty.Key' in the same format that is parsed by+-- 'Brick.Keybindings.Parse.parseBinding'.+ppKey :: Vty.Key -> T.Text+ppKey (Vty.KChar c) = ppChar c+ppKey (Vty.KFun n) = "F" <> (T.pack $ show n)+ppKey Vty.KBackTab = "BackTab"+ppKey Vty.KEsc = "Esc"+ppKey Vty.KBS = "Backspace"+ppKey Vty.KEnter = "Enter"+ppKey Vty.KUp = "Up"+ppKey Vty.KDown = "Down"+ppKey Vty.KLeft = "Left"+ppKey Vty.KRight = "Right"+ppKey Vty.KHome = "Home"+ppKey Vty.KEnd = "End"+ppKey Vty.KPageUp = "PgUp"+ppKey Vty.KPageDown = "PgDown"+ppKey Vty.KDel = "Del"+ppKey Vty.KUpLeft = "UpLeft"+ppKey Vty.KUpRight = "UpRight"+ppKey Vty.KDownLeft = "DownLeft"+ppKey Vty.KDownRight = "DownRight"+ppKey Vty.KCenter = "Center"+ppKey Vty.KPrtScr = "PrintScreen"+ppKey Vty.KPause = "Pause"+ppKey Vty.KIns = "Insert"+ppKey Vty.KBegin = "Begin"+ppKey Vty.KMenu = "Menu"++-- | Pretty-print a character in the same format that is parsed by+-- 'Brick.Keybindings.Parse.parseBinding'.+ppChar :: Char -> T.Text+ppChar '\t' = "Tab"+ppChar ' ' = "Space"+ppChar c = T.singleton c++-- | Pretty-print a 'Vty.Modifier' in the same format that is parsed by+-- 'Brick.Keybindings.Parse.parseBinding'.+ppModifier :: Vty.Modifier -> T.Text+ppModifier Vty.MMeta = "M"+ppModifier Vty.MAlt = "A"+ppModifier Vty.MCtrl = "C"+ppModifier Vty.MShift = "S"++-- | The base attribute for 'Widget' keybinding help.+keybindingHelpBaseAttr :: AttrName+keybindingHelpBaseAttr = attrName "keybindingHelp"++-- | The attribute for event names in keybinding help 'Widget's.+eventNameAttr :: AttrName+eventNameAttr = keybindingHelpBaseAttr <> attrName "eventName"++-- | The attribute for event descriptions in keybinding help 'Widget's.+eventDescriptionAttr :: AttrName+eventDescriptionAttr = keybindingHelpBaseAttr <> attrName "eventDescription"++-- | The attribute for keybinding lists in keybinding help 'Widget's.+keybindingAttr :: AttrName+keybindingAttr = keybindingHelpBaseAttr <> attrName "keybinding"
src/Brick/Main.hs view
@@ -1,15 +1,26 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE RankNTypes #-} module Brick.Main ( App(..) , defaultMain , customMain+ , customMainWithVty+ , customMainWithDefaultVty , simpleMain , resizeOrQuit+ , simpleApp -- * Event handler functions- , continue+ , continueWithoutRedraw , halt , suspendAndResume+ , suspendAndResume'+ , makeVisible , lookupViewport+ , lookupExtent+ , findClickedExtents+ , clickedExtent+ , getVtyHandle -- ** Viewport scrolling , viewportScroll@@ -22,24 +33,37 @@ , hScrollPage , hScrollToBeginning , hScrollToEnd+ , setTop+ , setLeft -- * Cursor management functions , neverShowCursor , showFirstCursor , showCursorNamed++ -- * Rendering cache management+ , invalidateCacheEntry+ , invalidateCache++ -- * Renderer internals (for benchmarking)+ , renderFinal+ , getRenderState+ , resetRenderState+ , renderWidget ) where -import Control.Exception (finally)-import Control.Lens ((^.))-import Control.Monad (forever)-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.State-import Control.Monad.Trans.Reader-import Control.Concurrent (forkIO, Chan, newChan, readChan, writeChan, killThread)-import Data.Default+import qualified Control.Exception as E+import Lens.Micro ((^.), (&), (.~), (%~), _1, _2)+import Control.Monad+import Control.Monad.State.Strict+import Control.Monad.Reader+import Control.Concurrent (forkIO, killThread)+import qualified Data.Foldable as F+import Data.List (find) import Data.Maybe (listToMaybe) import qualified Data.Map as M+import qualified Data.Set as S import Graphics.Vty ( Vty , Picture(..)@@ -50,26 +74,36 @@ , displayBounds , shutdown , nextEvent- , mkVty+ , restoreInputState+ , inputIface )+import Graphics.Vty.CrossPlatform (mkVty)+import Graphics.Vty.Config (defaultConfig)+import Graphics.Vty.Attributes (defAttr) -import Brick.Types (Viewport, Direction, Widget, rowL, columnL, CursorLocation(..), cursorLocationNameL, Name(..), EventM)-import Brick.Types.Internal (ScrollRequest(..), RenderState(..), Next(..))-import Brick.Widgets.Internal (renderFinal)+import Brick.BChan (BChan, newBChan, readBChan, readBChan2, writeBChan)+import Brick.Types.EventM+import Brick.Types.Internal+import Brick.Widgets.Internal import Brick.AttrMap -- | The library application abstraction. Your application's operations--- are represented here and passed to one of the various main functions--- in this module. An application is in terms of an application state--- type 's' and an application event type 'e'. In the simplest case 'e' is--- vty's 'Event' type, but you may define your own event type, permitted--- that it has a constructor for wrapping Vty events, so that Vty events--- can be handled by your event loop.-data App s e =- App { appDraw :: s -> [Widget]+-- are provided in an @App@ and then the @App@ is provided to one of the+-- various main functions in this module. An application @App s e n@+-- is in terms of an application state type @s@, an application event+-- type @e@, and a resource name type @n@. In the simplest case 'e' is+-- unused (left polymorphic or set to @()@), but you may define your own+-- event type and use 'customMain' to provide custom events. The state+-- type @s@ is the type of application state to be provided by you and+-- iteratively modified by event handlers. The resource name type @n@+-- is the type of names you can assign to rendering resources such as+-- viewports and cursor locations. Your application must define this+-- type.+data App s e n =+ App { appDraw :: s -> [Widget n] -- ^ This function turns your application state into a list of -- widget layers. The layers are listed topmost first.- , appChooseCursor :: s -> [CursorLocation] -> Maybe CursorLocation+ , appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n) -- ^ This function chooses which of the zero or more cursor -- locations reported by the rendering process should be -- selected as the one to use to place the cursor. If this@@ -77,228 +111,555 @@ -- is that many widgets may request a cursor placement but your -- application state is what you probably want to use to decide -- which one wins.- , appHandleEvent :: s -> e -> EventM (Next 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', 'suspendAndResume', and 'halt'.- , appStartEvent :: s -> EventM s+ , appHandleEvent :: BrickEvent n e -> EventM n s ()+ -- ^ 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 -- initial scrolling requests, for example. , appAttrMap :: s -> AttrMap -- ^ The attribute map that should be used during rendering.- , appLiftVtyEvent :: Event -> e- -- ^ The event constructor to use to wrap Vty events in your own- -- event type. For example, if the application's event type is- -- 'Event', this is just 'id'. } -- | The default main entry point which takes an application and an--- initial state and returns the final state returned by a 'halt'--- operation.-defaultMain :: App s Event+-- initial state and returns the final state from 'EventM' once the+-- program exits.+defaultMain :: (Ord n)+ => App s e n -- ^ The application. -> s -- ^ The initial application state. -> IO s defaultMain app st = do- chan <- newChan- customMain (mkVty def) chan app st+ (s, vty) <- customMainWithDefaultVty Nothing app st+ shutdown vty+ return s -- | A simple main entry point which takes a widget and renders it. This -- event loop terminates when the user presses any key, but terminal -- resize events cause redraws.-simpleMain :: Widget+simpleMain :: (Ord n)+ => Widget n -- ^ The widget to draw. -> IO ()-simpleMain w =- let app = App { appDraw = const [w]- , appHandleEvent = resizeOrQuit- , appStartEvent = return- , appAttrMap = def- , appLiftVtyEvent = id- , appChooseCursor = neverShowCursor- }- in defaultMain app ()+simpleMain w = defaultMain (simpleApp w) () +-- | A simple application with reasonable defaults to be overridden as+-- desired:+--+-- * Draws only the specified widget+-- * Quits on any event other than resizes+-- * Has no start event handler+-- * Provides no attribute map+-- * Never shows any cursors+simpleApp :: Widget n -> App s e n+simpleApp w =+ App { appDraw = const [w]+ , appHandleEvent = resizeOrQuit+ , appStartEvent = return ()+ , appAttrMap = const $ attrMap defAttr []+ , appChooseCursor = neverShowCursor+ }+ -- | An event-handling function which continues execution of the event -- loop only when resize events occur; all other types of events trigger -- a halt. This is a convenience function useful as an 'appHandleEvent' -- value for simple applications using the 'Event' type that do not need -- to get more sophisticated user input.-resizeOrQuit :: s -> Event -> EventM (Next s)-resizeOrQuit s (EvResize _ _) = continue s-resizeOrQuit s _ = halt s+resizeOrQuit :: BrickEvent n e -> EventM n s ()+resizeOrQuit (VtyEvent (EvResize _ _)) = return ()+resizeOrQuit _ = halt -data InternalNext a = InternalSuspendAndResume RenderState (IO a)- | InternalHalt a+readBrickEvent :: BChan (BrickEvent n e) -> BChan e -> IO (BrickEvent n e)+readBrickEvent brickChan userChan = either id AppEvent <$> readBChan2 brickChan userChan -runWithNewVty :: IO Vty -> Chan e -> App s e -> RenderState -> s -> IO (InternalNext s)-runWithNewVty buildVty chan app initialRS initialSt = do- withVty buildVty $ \vty -> do- pid <- forkIO $ supplyVtyEvents vty (appLiftVtyEvent app) chan- let runInner rs st = do- (result, newRS) <- runVty vty chan app st rs- case result of- SuspendAndResume act -> do- killThread pid- return $ InternalSuspendAndResume newRS act- Halt s -> do- killThread pid- return $ InternalHalt s- Continue s -> runInner newRS s- runInner initialRS initialSt+runWithVty :: (Ord n)+ => VtyContext+ -> BChan (BrickEvent n e)+ -> Maybe (BChan e)+ -> App s e n+ -> RenderState n+ -> s+ -> IO (s, VtyContext)+runWithVty vtyCtx brickChan mUserChan app initialRS initialSt = do+ let readEvent = case mUserChan of+ Nothing -> readBChan brickChan+ Just uc -> readBrickEvent brickChan uc+ runInner ctx rs es draw st = do+ let nextRS = if draw+ then resetRenderState rs+ else rs+ (nextSt, result, newRS, newExtents, newCtx) <- runVty ctx readEvent app st nextRS es draw+ case result of+ Halt ->+ return (nextSt, newCtx)+ Continue ->+ runInner newCtx newRS newExtents True nextSt+ ContinueWithoutRedraw ->+ runInner newCtx newRS newExtents False nextSt + runInner vtyCtx initialRS mempty True initialSt+ -- | The custom event loop entry point to use when the simpler ones--- don't permit enough control.-customMain :: IO Vty- -- ^ An IO action to build a Vty handle. This is used to- -- build a Vty handle whenever the event loop begins or is- -- resumed after suspension.- -> Chan e+-- don't permit enough control. Returns the final application state+-- after the application halts.+--+-- Note that this function guarantees that the terminal input state+-- prior to the first Vty initialization is the terminal input state+-- that is restored on shutdown (regardless of exceptions).+customMain :: (Ord n)+ => Vty+ -- ^ The initial Vty handle to use.+ -> IO Vty+ -- ^ An IO action to build a Vty handle. This is used+ -- to build a Vty handle whenever the event loop needs+ -- to reinitialize the terminal, e.g. on resume after+ -- suspension.+ -> Maybe (BChan e) -- ^ An event channel for sending custom events to the event -- loop (you write to this channel, the event loop reads from- -- it).- -> App s e+ -- it). Provide 'Nothing' if you don't plan on sending custom+ -- events.+ -> App s e n -- ^ The application. -> s -- ^ The initial application state. -> IO s-customMain buildVty chan app initialAppState = do- let run rs st = do- result <- runWithNewVty buildVty chan app rs st- case result of- InternalHalt s -> return s- InternalSuspendAndResume newRS action -> do- newAppState <- action- run newRS newAppState+customMain initialVty buildVty mUserChan app initialAppState = do+ let restoreInitialState = restoreInputState $ inputIface initialVty - (st, initialScrollReqs) <- runStateT (runReaderT (appStartEvent app initialAppState) M.empty) []- let initialRS = RS M.empty initialScrollReqs- run initialRS st+ (s, vty) <- customMainWithVty initialVty buildVty mUserChan app initialAppState+ `E.catch` (\(e::E.SomeException) -> restoreInitialState >> E.throw e) -supplyVtyEvents :: Vty -> (Event -> e) -> Chan e -> IO ()-supplyVtyEvents vty mkEvent chan =- forever $ do- e <- nextEvent vty- writeChan chan $ mkEvent e+ shutdown vty+ restoreInitialState+ return s -runVty :: Vty -> Chan e -> App s e -> s -> RenderState -> IO (Next s, RenderState)-runVty vty chan app appState rs = do- firstRS <- renderApp vty app appState rs- e <- readChan chan- (next, scrollReqs) <- runStateT (runReaderT (appHandleEvent app appState e) (viewportMap rs)) []- return (next, firstRS { scrollRequests = scrollReqs })+-- | Like 'customMainWithVty', except that Vty is initialized with the+-- default configuration.+--+-- The returned 'Vty' handle still has control of the terminal. The+-- caller is responsible for calling 'shutdown' to restore the terminal+-- state.+customMainWithDefaultVty :: (Ord n)+ => Maybe (BChan e)+ -- ^ An event channel for sending custom+ -- events to the event loop (you write to this+ -- channel, the event loop reads from it).+ -- Provide 'Nothing' if you don't plan on+ -- sending custom events.+ -> App s e n+ -- ^ The application.+ -> s+ -- ^ The initial application state.+ -> IO (s, Vty)+customMainWithDefaultVty mUserChan app initialAppState = do+ let builder = mkVty defaultConfig+ vty <- builder+ customMainWithVty vty builder mUserChan app initialAppState +-- | Like 'customMain', except the last 'Vty' handle used by the+-- application is returned without being shut down with 'shutdown'. This+-- allows the caller to re-use the 'Vty' handle for something else, such+-- as another Brick application.+customMainWithVty :: (Ord n)+ => Vty+ -- ^ The initial Vty handle to use.+ -> IO Vty+ -- ^ An IO action to build a Vty handle. This is used+ -- to build a Vty handle whenever the event loop needs+ -- to reinitialize the terminal, e.g. on resume after+ -- suspension.+ -> Maybe (BChan e)+ -- ^ An event channel for sending custom events to the event+ -- loop (you write to this channel, the event loop reads from+ -- it). Provide 'Nothing' if you don't plan on sending custom+ -- events.+ -> App s e n+ -- ^ The application.+ -> s+ -- ^ The initial application state.+ -> IO (s, Vty)+customMainWithVty initialVty buildVty mUserChan app initialAppState = do+ brickChan <- newBChan 20+ vtyCtx <- newVtyContext buildVty (Just initialVty) (writeBChan brickChan . VtyEvent)++ let emptyES = ES { esScrollRequests = []+ , cacheInvalidateRequests = mempty+ , requestedVisibleNames = mempty+ , nextAction = Continue+ , vtyContext = vtyCtx+ }+ emptyRS = RS M.empty mempty S.empty mempty mempty mempty mempty+ eventRO = EventRO M.empty mempty emptyRS++ (((), appState), eState) <- runStateT (runStateT (runReaderT (runEventM (appStartEvent app)) eventRO) initialAppState) emptyES+ let initialRS = RS { viewportMap = M.empty+ , rsScrollRequests = esScrollRequests eState+ , observedNames = S.empty+ , renderCache = mempty+ , clickableNames = []+ , requestedVisibleNames_ = requestedVisibleNames eState+ , reportedExtents = mempty+ }++ (s, ctx) <- runWithVty vtyCtx brickChan mUserChan app initialRS appState+ `E.catch` (\(e::E.SomeException) -> shutdownVtyContext vtyCtx >> E.throw e)++ -- Shut down the context's event thread but do NOT shut down Vty+ -- itself because we want the handle to be live when we return it to+ -- the caller.+ shutdownVtyContextThread ctx+ return (s, vtyContextHandle ctx)++supplyVtyEvents :: Vty -> (Event -> IO ()) -> IO ()+supplyVtyEvents vty putEvent =+ forever $ putEvent =<< nextEvent vty++newVtyContextFrom :: VtyContext -> IO VtyContext+newVtyContextFrom old =+ newVtyContext (vtyContextBuilder old) Nothing (vtyContextPutEvent old)++newVtyContext :: IO Vty -> Maybe Vty -> (Event -> IO ()) -> IO VtyContext+newVtyContext builder handle putEvent = do+ vty <- case handle of+ Just h -> return h+ Nothing -> builder+ tId <- forkIO $ supplyVtyEvents vty putEvent+ return VtyContext { vtyContextHandle = vty+ , vtyContextBuilder = builder+ , vtyContextThread = tId+ , vtyContextPutEvent = putEvent+ }++shutdownVtyContext :: VtyContext -> IO ()+shutdownVtyContext ctx = do+ shutdown $ vtyContextHandle ctx+ shutdownVtyContextThread ctx++shutdownVtyContextThread :: VtyContext -> IO ()+shutdownVtyContextThread ctx =+ killThread $ vtyContextThread ctx++runVty :: (Ord n)+ => VtyContext+ -> IO (BrickEvent n e)+ -> App s e n+ -> s+ -> RenderState n+ -> [Extent n]+ -> Bool+ -> IO (s, NextAction, RenderState n, [Extent n], VtyContext)+runVty vtyCtx readEvent app appState rs prevExtents draw = do+ (firstRS, exts) <- if draw+ then renderApp vtyCtx app appState rs+ else return (rs, prevExtents)++ e <- readEvent++ (e', nextRS, nextExts) <- case e of+ -- If the event was a resize, redraw the UI to update the+ -- viewport states before we invoke the event handler since we+ -- want the event handler to have access to accurate viewport+ -- information.+ VtyEvent (EvResize _ _) -> do+ (rs', exts') <- renderApp vtyCtx app appState $ firstRS & observedNamesL .~ S.empty+ return (e, rs', exts')+ VtyEvent (EvMouseDown c r button mods) -> do+ let matching = findClickedExtents_ (c, r) exts+ case matching of+ (Extent n (Location (ec, er)) _:_) ->+ -- If the clicked extent was registered as+ -- clickable, send a click event. Otherwise, just+ -- send the raw mouse event+ if n `elem` firstRS^.clickableNamesL+ then do+ let localCoords = Location (lc, lr)+ lc = c - ec+ lr = r - er++ -- If the clicked extent was a viewport,+ -- adjust the local coordinates by+ -- adding the viewport upper-left corner+ -- offset.+ newCoords = case M.lookup n (viewportMap firstRS) of+ Nothing -> localCoords+ Just vp -> localCoords & _1 %~ (+ (vp^.vpLeft))+ & _2 %~ (+ (vp^.vpTop))++ return (MouseDown n button mods newCoords, firstRS, exts)+ else return (e, firstRS, exts)+ _ -> return (e, firstRS, exts)+ VtyEvent (EvMouseUp c r button) -> do+ let matching = findClickedExtents_ (c, r) exts+ case matching of+ (Extent n (Location (ec, er)) _:_) ->+ -- If the clicked extent was registered as+ -- clickable, send a click event. Otherwise, just+ -- send the raw mouse event+ if n `elem` firstRS^.clickableNamesL+ then do+ let localCoords = Location (lc, lr)+ lc = c - ec+ lr = r - er+ -- If the clicked extent was a viewport,+ -- adjust the local coordinates by+ -- adding the viewport upper-left corner+ -- offset.+ newCoords = case M.lookup n (viewportMap firstRS) of+ Nothing -> localCoords+ Just vp -> localCoords & _1 %~ (+ (vp^.vpLeft))+ & _2 %~ (+ (vp^.vpTop))+ return (MouseUp n button newCoords, firstRS, exts)+ else return (e, firstRS, exts)+ _ -> return (e, firstRS, exts)+ _ -> return (e, firstRS, exts)++ let emptyES = ES [] mempty mempty Continue vtyCtx+ eventRO = EventRO (viewportMap nextRS) nextExts nextRS++ (((), newAppState), eState) <- runStateT (runStateT (runReaderT (runEventM (appHandleEvent app e'))+ eventRO) appState) emptyES+ return ( newAppState+ , nextAction eState+ , nextRS { rsScrollRequests = esScrollRequests eState+ , renderCache = applyInvalidations (cacheInvalidateRequests eState) $+ renderCache nextRS+ , requestedVisibleNames_ = requestedVisibleNames eState+ }+ , nextExts+ , vtyContext eState+ )++applyInvalidations :: (Ord n) => S.Set (CacheInvalidateRequest n) -> M.Map n v -> M.Map n v+applyInvalidations ns cache =+ if InvalidateEntire `S.member` ns+ then mempty+ else foldr (.) id (mkFunc <$> F.toList ns) cache+ where+ mkFunc InvalidateEntire = const mempty+ mkFunc (InvalidateSingle n) = M.delete n+ -- | Given a viewport name, get the viewport's size and offset -- information from the most recent rendering. Returns 'Nothing' if -- no such state could be found, either because the name was invalid -- or because no rendering has occurred (e.g. in an 'appStartEvent'--- handler).-lookupViewport :: Name -> EventM (Maybe Viewport)-lookupViewport = asks . M.lookup+-- handler). An important consequence of this behavior is that if this+-- function is called before a viewport is rendered for the first+-- time, no state will be found because the renderer only knows about+-- viewports it has rendered in the most recent rendering. As a result,+-- if you need to make viewport transformations before they are drawn+-- for the first time, you may need to use 'viewportScroll' and its+-- associated functions without relying on this function. Those+-- functions queue up scrolling requests that can be made in advance of+-- the next rendering to affect the viewport.+lookupViewport :: (Ord n) => n -> EventM n s (Maybe Viewport)+lookupViewport n = EventM $ asks (M.lookup n . eventViewportMap) -withVty :: IO Vty -> (Vty -> IO a) -> IO a-withVty buildVty useVty = do- vty <- buildVty- useVty vty `finally` shutdown vty+-- | Did the specified mouse coordinates (column, row) intersect the+-- specified extent?+clickedExtent :: (Int, Int) -> Extent n -> Bool+clickedExtent (c, r) (Extent _ (Location (lc, lr)) (w, h)) =+ c >= lc && c < (lc + w) &&+ r >= lr && r < (lr + h) -renderApp :: Vty -> App s e -> s -> RenderState -> IO RenderState-renderApp vty app appState rs = do- sz <- displayBounds $ outputIface vty- let (newRS, pic, theCursor) = renderFinal (appAttrMap app appState)- (appDraw app appState)- sz- (appChooseCursor app appState)- rs+-- | Given a resource name, get the most recent rendering extent for the+-- name (if any).+lookupExtent :: (Eq n) => n -> EventM n s (Maybe (Extent n))+lookupExtent n = EventM $ asks (find f . latestExtents)+ where+ f (Extent n' _ _) = n == n'++-- | Given a mouse click location, return the extents intersected by the+-- click. The returned extents are sorted such that the first extent in+-- the list is the most specific extent and the last extent is the most+-- generic (top-level). So if two extents A and B both intersected the+-- mouse click but A contains B, then they would be returned [B, A].+findClickedExtents :: (Int, Int) -> EventM n s [Extent n]+findClickedExtents pos = EventM $ asks (findClickedExtents_ pos . latestExtents)++findClickedExtents_ :: (Int, Int) -> [Extent n] -> [Extent n]+findClickedExtents_ pos = reverse . filter (clickedExtent pos)++-- | Get the Vty handle currently in use.+getVtyHandle :: EventM n s Vty+getVtyHandle = vtyContextHandle <$> getVtyContext++setVtyContext :: VtyContext -> EventM n s ()+setVtyContext ctx =+ EventM $ lift $ lift $ modify $ \s -> s { vtyContext = ctx }++-- | Invalidate the rendering cache entry with the specified resource+-- name.+invalidateCacheEntry :: (Ord n) => n -> EventM n s ()+invalidateCacheEntry n = EventM $ do+ lift $ lift $ modify (\s -> s { cacheInvalidateRequests = S.insert (InvalidateSingle n) $ cacheInvalidateRequests s })++-- | Invalidate the entire rendering cache.+invalidateCache :: (Ord n) => EventM n s ()+invalidateCache = EventM $ do+ lift $ lift $ modify (\s -> s { cacheInvalidateRequests = S.insert InvalidateEntire $ cacheInvalidateRequests s })++getRenderState :: EventM n s (RenderState n)+getRenderState = EventM $ asks oldState++resetRenderState :: RenderState n -> RenderState n+resetRenderState s =+ s & observedNamesL .~ S.empty+ & clickableNamesL .~ mempty++renderApp :: (Ord n) => VtyContext -> App s e n -> s -> RenderState n -> IO (RenderState n, [Extent n])+renderApp vtyCtx app appState rs = do+ sz <- displayBounds $ outputIface $ vtyContextHandle vtyCtx+ let (newRS, pic, theCursor, exts) = renderFinal (appAttrMap app appState)+ (appDraw app appState)+ sz+ (appChooseCursor app appState)+ rs picWithCursor = case theCursor of Nothing -> pic { picCursor = NoCursor }- Just loc -> pic { picCursor = Cursor (loc^.columnL) (loc^.rowL) }+ Just cloc -> pic { picCursor = (if cursorLocationVisible cloc+ then AbsoluteCursor+ else PositionOnly True)+ (cloc^.locationColumnL)+ (cloc^.locationRowL)+ } - update vty picWithCursor+ update (vtyContextHandle vtyCtx) picWithCursor - return newRS+ return (newRS, exts) -- | Ignore all requested cursor positions returned by the rendering -- process. This is a convenience function useful as an -- 'appChooseCursor' value when a simple application has no need to -- position the cursor.-neverShowCursor :: s -> [CursorLocation] -> Maybe CursorLocation+neverShowCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n) neverShowCursor = const $ const Nothing -- | Always show the first cursor, if any, returned by the rendering -- process. This is a convenience function useful as an -- 'appChooseCursor' value when a simple program has zero or more -- widgets that advertise a cursor position.-showFirstCursor :: s -> [CursorLocation] -> Maybe CursorLocation+showFirstCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n) showFirstCursor = const listToMaybe --- | Show the cursor with the specified name, if such a cursor location--- has been reported.-showCursorNamed :: Name -> [CursorLocation] -> Maybe CursorLocation+-- | Show the cursor with the specified resource name, if such a cursor+-- location has been reported.+showCursorNamed :: (Eq n) => n -> [CursorLocation n] -> Maybe (CursorLocation n) showCursorNamed name locs =- let matches loc = loc^.cursorLocationNameL == Just name- in listToMaybe $ filter matches locs+ let matches l = l^.cursorLocationNameL == Just name+ in find matches locs -- | A viewport scrolling handle for managing the scroll state of -- viewports.-data ViewportScroll =- ViewportScroll { viewportName :: Name+data ViewportScroll n =+ ViewportScroll { viewportName :: n -- ^ The name of the viewport to be controlled by -- this scrolling handle.- , hScrollPage :: Direction -> EventM ()+ , hScrollPage :: forall s. Direction -> EventM n s () -- ^ Scroll the viewport horizontally by one page in -- the specified direction.- , hScrollBy :: Int -> EventM ()+ , hScrollBy :: forall s. Int -> EventM n s () -- ^ Scroll the viewport horizontally by the -- specified number of rows or columns depending on -- the orientation of the viewport.- , hScrollToBeginning :: EventM ()+ , hScrollToBeginning :: forall s. EventM n s () -- ^ Scroll horizontally to the beginning of the -- viewport.- , hScrollToEnd :: EventM ()+ , hScrollToEnd :: forall s. EventM n s () -- ^ Scroll horizontally to the end of the viewport.- , vScrollPage :: Direction -> EventM ()+ , vScrollPage :: forall s. Direction -> EventM n s () -- ^ Scroll the viewport vertically by one page in -- the specified direction.- , vScrollBy :: Int -> EventM ()+ , vScrollBy :: forall s. Int -> EventM n s () -- ^ Scroll the viewport vertically by the specified -- number of rows or columns depending on the -- orientation of the viewport.- , vScrollToBeginning :: EventM ()+ , vScrollToBeginning :: forall s. EventM n s () -- ^ Scroll vertically to the beginning of the viewport.- , vScrollToEnd :: EventM ()+ , vScrollToEnd :: forall s. EventM n s () -- ^ Scroll vertically to the end of the viewport.+ , setTop :: forall s. Int -> EventM n s ()+ -- ^ Set the top row offset of the viewport.+ , setLeft :: forall s. Int -> EventM n s ()+ -- ^ Set the left column offset of the viewport. } +addScrollRequest :: (n, ScrollRequest) -> EventM n s ()+addScrollRequest req = EventM $ do+ lift $ lift $ modify (\s -> s { esScrollRequests = req : esScrollRequests s })+ -- | Build a viewport scroller for the viewport with the specified name.-viewportScroll :: Name -> ViewportScroll+viewportScroll :: n -> ViewportScroll n viewportScroll n =- ViewportScroll { viewportName = n- , hScrollPage = \dir -> lift $ modify ((n, HScrollPage dir) :)- , hScrollBy = \i -> lift $ modify ((n, HScrollBy i) :)- , hScrollToBeginning = lift $ modify ((n, HScrollToBeginning) :)- , hScrollToEnd = lift $ modify ((n, HScrollToEnd) :)- , vScrollPage = \dir -> lift $ modify ((n, VScrollPage dir) :)- , vScrollBy = \i -> lift $ modify ((n, VScrollBy i) :)- , vScrollToBeginning = lift $ modify ((n, VScrollToBeginning) :)- , vScrollToEnd = lift $ modify ((n, VScrollToEnd) :)+ ViewportScroll { viewportName = n+ , hScrollPage = \dir -> addScrollRequest (n, HScrollPage dir)+ , hScrollBy = \i -> addScrollRequest (n, HScrollBy i)+ , hScrollToBeginning = addScrollRequest (n, HScrollToBeginning)+ , hScrollToEnd = addScrollRequest (n, HScrollToEnd)+ , vScrollPage = \dir -> addScrollRequest (n, VScrollPage dir)+ , vScrollBy = \i -> addScrollRequest (n, VScrollBy i)+ , vScrollToBeginning = addScrollRequest (n, VScrollToBeginning)+ , vScrollToEnd = addScrollRequest (n, VScrollToEnd)+ , setTop = \i -> addScrollRequest (n, SetTop i)+ , setLeft = \i -> addScrollRequest (n, SetLeft i) } -- | Continue running the event loop with the specified application--- state.-continue :: s -> EventM (Next s)-continue = return . Continue+-- 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, 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 } -- | Halt the event loop and return the specified application state as -- the final state value.-halt :: s -> EventM (Next s)-halt = return . Halt+halt :: EventM n s ()+halt =+ EventM $ lift $ lift $ modify $ \es -> es { nextAction = Halt } -- | Suspend the event loop, save the terminal state, and run the -- specified action. When it returns an application state value, restore--- the terminal state, redraw the application from the new state, and--- resume the event loop.-suspendAndResume :: IO s -> EventM (Next s)-suspendAndResume = return . SuspendAndResume+-- the terminal state, empty the rendering cache, update the application+-- state with the returned state, and continue execution of the event+-- handler that called this.+--+-- Note that any changes made to the terminal's input state are ignored+-- when Brick resumes execution and are not preserved in the final+-- terminal input state after the Brick application returns the terminal+-- to the user.+suspendAndResume :: (Ord n) => IO s -> EventM n s ()+suspendAndResume act = suspendAndResume' act >>= put++-- | Suspend the event loop, save the terminal state, and run the+-- specified action. When it completes, restore the terminal state,+-- empty the rendering cache, return the result, and continue execution+-- of the event handler that called this.+--+-- Note that any changes made to the terminal's input state are ignored+-- when Brick resumes execution and are not preserved in the final+-- terminal input state after the Brick application returns the terminal+-- to the user.+suspendAndResume' :: (Ord n) => IO a -> EventM n s a+suspendAndResume' act = do+ ctx <- getVtyContext+ liftIO $ shutdownVtyContext ctx+ result <- liftIO act+ setVtyContext =<< (liftIO $ newVtyContextFrom ctx)+ invalidateCache+ return result++-- | Request that the specified UI element be made visible on the+-- next rendering. This is provided to allow event handlers to make+-- visibility requests in the same way that the 'visible' function does+-- at rendering time.+makeVisible :: (Ord n) => n -> EventM n s ()+makeVisible n = EventM $ do+ lift $ lift $ modify (\s -> s { requestedVisibleNames = S.insert n $ requestedVisibleNames s })
− src/Brick/Markup.hs
@@ -1,54 +0,0 @@--- | This module provides an API for turning "markup" values into--- widgets. This module uses the Data.Text.Markup interface in this--- package to assign attributes to substrings in a text string; to--- manipulate markup using (for example) syntax highlighters, see that--- module.-module Brick.Markup- ( Markup- , markup- , (@?)- , GetAttr(..)- )-where--import Control.Lens ((.~), (&), (^.))-import Control.Monad (forM)-import qualified Data.Text as T-import Data.Text.Markup-import Data.Default (def)--import Graphics.Vty (Attr, horizCat, string)--import Brick.AttrMap-import Brick.Types---- | A type class for types that provide access to an attribute in the--- rendering monad. You probably won't need to instance this.-class GetAttr a where- -- | Where to get the attribute for this attribute metadata.- getAttr :: a -> RenderM Attr--instance GetAttr Attr where- getAttr a = do- c <- getContext- return $ mergeWithDefault a (c^.ctxAttrMapL)--instance GetAttr AttrName where- getAttr = lookupAttrName---- | Build a piece of markup from text with an assigned attribute name.--- When the markup is rendered, the attribute name will be looked up in--- the rendering context's 'AttrMap' to determine the attribute to use--- for this piece of text.-(@?) :: T.Text -> AttrName -> Markup AttrName-(@?) = (@@)---- | Build a widget from markup.-markup :: (Eq a, GetAttr a) => Markup a -> Widget-markup m =- Widget Fixed Fixed $ do- let pairs = markupToList m- imgs <- forM pairs $ \(t, aSrc) -> do- a <- getAttr aSrc- return $ string a $ T.unpack t- return $ def & imageL .~ horizCat imgs
+ src/Brick/Themes.hs view
@@ -0,0 +1,410 @@+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TemplateHaskell #-}+-- | Support for representing attribute themes and loading and saving+-- theme customizations in INI-style files.+--+-- Customization files are INI-style files with two sections, both+-- optional: @"default"@ and @"other"@.+--+-- The @"default"@ section specifies three optional fields:+--+-- * @"default.fg"@ - a color specification+-- * @"default.bg"@ - a color specification+-- * @"default.style"@ - a style specification+--+-- A color specification can be any of the strings @black@, @red@,+-- @green@, @yellow@, @blue@, @magenta@, @cyan@, @white@, @brightBlack@,+-- @brightRed@, @brightGreen@, @brightYellow@, @brightBlue@,+-- @brightMagenta@, @brightCyan@, @brightWhite@, or @default@.+--+-- We also support color specifications in the common hex format @#RRGGBB@, but+-- note that this specification is lossy: terminals can only display 256 colors,+-- but hex codes can specify @256^3 = 16777216@ colors.+--+-- A style specification can be either one of the following values+-- (without quotes) or a comma-delimited list of one or more of the+-- following values (e.g. @"[bold,underline]"@) indicating that all+-- of the specified styles be used. Valid styles are @standout@,+-- @underline@, @reverseVideo@, @blink@, @dim@, @italic@,+-- @strikethrough@, and @bold@.+--+-- The @other@ section specifies for each attribute name in the theme+-- the same @fg@, @bg@, and @style@ settings as for the default+-- attribute. Furthermore, if an attribute name has multiple components,+-- the fields in the INI file should use periods as delimiters. For+-- example, if a theme has an attribute name (@attrName "foo" <> attrName "bar"@), then+-- the file may specify three fields:+--+-- * @foo.bar.fg@ - a color specification+-- * @foo.bar.bg@ - a color specification+-- * @foo.bar.style@ - a style specification+--+-- Any color or style specifications omitted from the file mean that+-- those attribute or style settings will use the theme's default value+-- instead.+--+-- Attribute names with multiple components (e.g. @attr1 <> attr2@) can+-- be referenced in customization files by separating the names with+-- a dot. For example, the attribute name @attrName "list" <> attrName "selected"@ can be+-- referenced by using the string "list.selected".+module Brick.Themes+ ( CustomAttr(..)+ , customFgL+ , customBgL+ , customStyleL++ , Theme(..)+ , newTheme+ , themeDefaultAttrL+ , themeDefaultMappingL+ , themeCustomMappingL+ , themeCustomDefaultAttrL++ , ThemeDocumentation(..)+ , themeDescriptionsL++ , themeToAttrMap+ , applyCustomizations+ , loadCustomizations+ , saveCustomizations+ , saveTheme+ )+where++import GHC.Generics (Generic)+import Graphics.Vty hiding ((<|>))+import Control.DeepSeq+import Control.Monad (forM, join)+import Control.Applicative ((<|>))+import qualified Data.Text as T+import qualified Data.Text.Read as T+import qualified Data.Text.IO as T+import qualified Data.Map as M+import qualified Data.Semigroup as Sem+import Data.Tuple (swap)+import Data.List (intercalate)+import Data.Bits ((.|.), (.&.))+import Data.Maybe (fromMaybe, isNothing, catMaybes, mapMaybe)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import qualified Data.Foldable as F++import Data.Ini.Config++import Brick.AttrMap (AttrMap, AttrName, attrMap, attrNameComponents)+import Brick.Types.TH (suffixLenses)++import Text.Printf++-- | An attribute customization can specify which aspects of an+-- attribute to customize.+data CustomAttr =+ CustomAttr { customFg :: Maybe (MaybeDefault Color)+ -- ^ The customized foreground, if any.+ , customBg :: Maybe (MaybeDefault Color)+ -- ^ The customized background, if any.+ , customStyle :: Maybe Style+ -- ^ The customized style, if any.+ }+ deriving (Eq, Read, Show, Generic, NFData)++instance Sem.Semigroup CustomAttr where+ a <> b =+ CustomAttr { customFg = customFg a <|> customFg b+ , customBg = customBg a <|> customBg b+ , customStyle = customStyle a <|> customStyle b+ }++instance Monoid CustomAttr where+ mempty = CustomAttr Nothing Nothing Nothing+ mappend = (Sem.<>)++-- | Documentation for a theme's attributes.+data ThemeDocumentation =+ ThemeDocumentation { themeDescriptions :: M.Map AttrName T.Text+ -- ^ The per-attribute documentation for a theme+ -- so e.g. documentation for theme customization+ -- can be generated mechanically.+ }+ deriving (Eq, Read, Show, Generic, NFData)++-- | A theme provides a set of default attribute mappings, a default+-- attribute, and a set of customizations for the default mapping+-- and default attribute. The idea here is that the application will+-- always need to provide a complete specification of its attribute+-- mapping, but if the user wants to customize any aspect of that+-- default mapping, it can be contained here and then built into an+-- 'AttrMap' (see 'themeToAttrMap'). We keep the defaults separate+-- from customizations to permit users to serialize themes and their+-- customizations to, say, disk files.+data Theme =+ Theme { themeDefaultAttr :: Attr+ -- ^ The default attribute to use.+ , themeDefaultMapping :: M.Map AttrName Attr+ -- ^ The default attribute mapping to use.+ , themeCustomDefaultAttr :: Maybe CustomAttr+ -- ^ Customization for the theme's default attribute.+ , themeCustomMapping :: M.Map AttrName CustomAttr+ -- ^ Customizations for individual entries of the default+ -- mapping. Note that this will only affect entries in the+ -- default mapping; any attributes named here that are not+ -- present in the default mapping will not be considered.+ }+ deriving (Eq, Read, Show, Generic, NFData)++suffixLenses ''CustomAttr+suffixLenses ''Theme+suffixLenses ''ThemeDocumentation++defaultSectionName :: T.Text+defaultSectionName = "default"++otherSectionName :: T.Text+otherSectionName = "other"++-- | Create a new theme with the specified default attribute and+-- attribute mapping. The theme will have no customizations.+newTheme :: Attr -> [(AttrName, Attr)] -> Theme+newTheme def mapping =+ Theme { themeDefaultAttr = def+ , themeDefaultMapping = M.fromList mapping+ , themeCustomDefaultAttr = Nothing+ , themeCustomMapping = mempty+ }++-- | Build an 'AttrMap' from a 'Theme'. This applies all customizations+-- in the returned 'AttrMap'.+themeToAttrMap :: Theme -> AttrMap+themeToAttrMap t =+ attrMap (customizeAttr (themeCustomDefaultAttr t) (themeDefaultAttr t)) customMap+ where+ customMap = F.foldr f [] (M.toList $ themeDefaultMapping t)+ f (aName, attr) mapping =+ let a' = customizeAttr (M.lookup aName (themeCustomMapping t)) attr+ in (aName, a'):mapping++customizeAttr :: Maybe CustomAttr -> Attr -> Attr+customizeAttr Nothing a = a+customizeAttr (Just c) a =+ let fg = fromMaybe (attrForeColor a) (customFg c)+ bg = fromMaybe (attrBackColor a) (customBg c)+ sty = maybe (attrStyle a) SetTo (customStyle c)+ in a { attrForeColor = fg+ , attrBackColor = bg+ , attrStyle = sty+ }++isNullCustomization :: CustomAttr -> Bool+isNullCustomization c =+ isNothing (customFg c) &&+ isNothing (customBg c) &&+ isNothing (customStyle c)++-- | This function is lossy in the sense that we only internally support 240 colors but+-- the #RRGGBB format supports 16^3 colors.+parseColor :: T.Text -> Either String (MaybeDefault Color)+parseColor s =+ let stripped = T.strip $ T.toLower s+ normalize (t, c) = (T.toLower t, c)+ in if stripped == "default"+ then Right Default+ else case parseRGB stripped of+ Just c -> Right (SetTo c)+ Nothing -> maybe (Left $ "Invalid color: " <> show stripped) (Right . SetTo) $+ lookup stripped (normalize <$> swap <$> allColors)+ where+ parseRGB t = if T.head t /= '#'+ then Nothing+ else case mapMaybe readHex (T.chunksOf 2 (T.tail t)) of+ [r,g,b] -> Just (rgbColor r g b)+ _ -> Nothing++ readHex :: T.Text -> Maybe Int+ readHex t = either (const Nothing) (Just . fst) (T.hexadecimal t)++allColors :: [(Color, T.Text)]+allColors =+ [ (black, "black")+ , (red, "red")+ , (green, "green")+ , (yellow, "yellow")+ , (blue, "blue")+ , (magenta, "magenta")+ , (cyan, "cyan")+ , (white, "white")+ , (brightBlack, "brightBlack")+ , (brightRed, "brightRed")+ , (brightGreen, "brightGreen")+ , (brightYellow, "brightYellow")+ , (brightBlue, "brightBlue")+ , (brightMagenta, "brightMagenta")+ , (brightCyan, "brightCyan")+ , (brightWhite, "brightWhite")+ ]++allStyles :: [(T.Text, Style)]+allStyles =+ [ ("standout", standout)+ , ("underline", underline)+ , ("strikethrough", strikethrough)+ , ("reversevideo", reverseVideo)+ , ("blink", blink)+ , ("dim", dim)+ , ("bold", bold)+ , ("italic", italic)+ ]++parseStyle :: T.Text -> Either String Style+parseStyle s =+ let lookupStyle "" = Right Nothing+ lookupStyle n = case lookup n normalizedStyles of+ Just sty -> Right $ Just sty+ Nothing -> Left $ T.unpack $ "Invalid style: " <> n+ stripped = T.strip $ T.toLower s+ normalize (n, a) = (T.toLower n, a)+ normalizedStyles = normalize <$> allStyles+ bracketed = "[" `T.isPrefixOf` stripped &&+ "]" `T.isSuffixOf` stripped+ unbracketed = T.tail $ T.init stripped+ parseStyleList = do+ ss <- mapM lookupStyle $ T.strip <$> T.splitOn "," unbracketed+ return $ foldr (.|.) 0 $ catMaybes ss++ in if bracketed+ then parseStyleList+ else do+ result <- lookupStyle stripped+ case result of+ Nothing -> Left $ "Invalid style: " <> show stripped+ Just sty -> Right sty++themeParser :: Theme -> IniParser (Maybe CustomAttr, M.Map AttrName CustomAttr)+themeParser t = do+ let parseCustomAttr basename = do+ c <- CustomAttr <$> fieldMbOf (basename <> ".fg") parseColor+ <*> fieldMbOf (basename <> ".bg") parseColor+ <*> fieldMbOf (basename <> ".style") parseStyle+ return $ if isNullCustomization c then Nothing else Just c++ defCustom <- sectionMb defaultSectionName $ do+ parseCustomAttr "default"++ customMap <- sectionMb otherSectionName $ do+ catMaybes <$> (forM (M.keys $ themeDefaultMapping t) $ \an ->+ (fmap (an,)) <$> parseCustomAttr (makeFieldName $ attrNameComponents an)+ )++ return (join defCustom, M.fromList $ fromMaybe [] customMap)++-- | Apply customizations using a custom lookup function. Customizations+-- are obtained for each attribute name in the theme. Any customizations+-- already set are lost.+applyCustomizations :: Maybe CustomAttr+ -- ^ An optional customization for the theme's+ -- default attribute.+ -> (AttrName -> Maybe CustomAttr)+ -- ^ A function to obtain a customization for the+ -- specified attribute.+ -> Theme+ -- ^ The theme to customize.+ -> Theme+applyCustomizations customDefAttr lookupAttr t =+ let customMap = foldr nextAttr mempty (M.keys $ themeDefaultMapping t)+ nextAttr an m = case lookupAttr an of+ Nothing -> m+ Just custom -> M.insert an custom m+ in t { themeCustomDefaultAttr = customDefAttr+ , themeCustomMapping = customMap+ }++-- | Load an INI file containing theme customizations. Use the specified+-- theme to determine which customizations to load. Return the specified+-- theme with customizations set. See the module documentation for the+-- theme file format.+loadCustomizations :: FilePath -> Theme -> IO (Either String Theme)+loadCustomizations path t = do+ content <- T.readFile path+ case parseIniFile content (themeParser t) of+ Left e -> return $ Left e+ Right (customDef, customMap) ->+ return $ Right $ applyCustomizations customDef (flip M.lookup customMap) t++vtyColorName :: Color -> T.Text+vtyColorName c@(Color240 n) = case color240CodeToRGB (fromIntegral n) of+ Just (r,g,b) -> T.pack (printf "#%02x%02x%02x" r g b)+ Nothing -> (error $ "Invalid color: " <> show c)+vtyColorName c =+ fromMaybe (error $ "Invalid color: " <> show c)+ (lookup c allColors)++makeFieldName :: [String] -> T.Text+makeFieldName cs = T.pack $ intercalate "." cs++serializeCustomColor :: [String] -> MaybeDefault Color -> T.Text+serializeCustomColor cs cc =+ let cName = case cc of+ Default -> "default"+ SetTo c -> vtyColorName c+ KeepCurrent -> error "serializeCustomColor does not support KeepCurrent"+ in makeFieldName cs <> " = " <> cName++serializeCustomStyle :: [String] -> Style -> T.Text+serializeCustomStyle cs s =+ let activeStyles = filter (\(_, a) -> a .&. s == a) allStyles+ styleStr = case activeStyles of+ [(single, _)] -> single+ many -> "[" <> (T.intercalate ", " $ fst <$> many) <> "]"+ in makeFieldName cs <> " = " <> styleStr++serializeCustomAttr :: [String] -> CustomAttr -> [T.Text]+serializeCustomAttr cs c =+ catMaybes [ serializeCustomColor (cs <> ["fg"]) <$> customFg c+ , serializeCustomColor (cs <> ["bg"]) <$> customBg c+ , serializeCustomStyle (cs <> ["style"]) <$> customStyle c+ ]++emitSection :: T.Text -> [T.Text] -> [T.Text]+emitSection _ [] = []+emitSection secName ls = ("[" <> secName <> "]") : ls++-- | Save an INI file containing theme customizations. Use the specified+-- theme to determine which customizations to save. See the module+-- documentation for the theme file format.+saveCustomizations :: FilePath -> Theme -> IO ()+saveCustomizations path t = do+ let defSection = fromMaybe [] $+ serializeCustomAttr ["default"] <$> themeCustomDefaultAttr t+ mapSection = concat $ flip map (M.keys $ themeDefaultMapping t) $ \an ->+ maybe [] (serializeCustomAttr (attrNameComponents an)) $+ M.lookup an $ themeCustomMapping t+ content = T.unlines $ (emitSection defaultSectionName defSection) <>+ (emitSection otherSectionName mapSection)+ T.writeFile path content++-- | Save an INI file containing all attributes from the specified+-- theme. Customized attributes are saved, but if an attribute is not+-- customized, its default is saved instead. The file can later be+-- re-loaded as a customization file.+saveTheme :: FilePath -> Theme -> IO ()+saveTheme path t = do+ let defSection = serializeCustomAttr ["default"] $+ fromMaybe (attrToCustom $ themeDefaultAttr t) (themeCustomDefaultAttr t)+ mapSection = concat $ flip map (M.toList $ themeDefaultMapping t) $ \(an, def) ->+ serializeCustomAttr (attrNameComponents an) $+ fromMaybe (attrToCustom def) (M.lookup an $ themeCustomMapping t)+ content = T.unlines $ (emitSection defaultSectionName defSection) <>+ (emitSection otherSectionName mapSection)+ T.writeFile path content++attrToCustom :: Attr -> CustomAttr+attrToCustom a =+ CustomAttr { customFg = Just $ attrForeColor a+ , customBg = Just $ attrBackColor a+ , customStyle = case attrStyle a of+ SetTo s -> Just s+ _ -> Nothing+ }
src/Brick/Types.hs view
@@ -1,5 +1,4 @@ -- | Basic types used by this library.-{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Brick.Types@@ -20,34 +19,50 @@ , vpSize , vpTop , vpLeft+ , vpContentSize+ , VScrollBarOrientation(..)+ , HScrollBarOrientation(..)+ , VScrollbarRenderer(..)+ , HScrollbarRenderer(..)+ , ClickableScrollbarElement(..) - -- * Event-handling types+ -- * Event-handling types and functions , EventM- , Next- , HandleEvent(..)- , handleEventLensed+ , BrickEvent(..)+ , nestEventM+ , nestEventM' -- * Rendering infrastructure , RenderM , getContext -- ** The rendering context- , Context(ctxAttrName, availWidth, availHeight, ctxBorderStyle, ctxAttrMap)+ , Context(ctxAttrName, availWidth, availHeight, windowWidth, windowHeight, ctxBorderStyle, ctxAttrMap, ctxDynBorders) , attrL , availWidthL , availHeightL+ , windowWidthL+ , windowHeightL+ , ctxVScrollBarOrientationL+ , ctxVScrollBarRendererL+ , ctxHScrollBarOrientationL+ , ctxHScrollBarRendererL , ctxAttrMapL , ctxAttrNameL , ctxBorderStyleL+ , ctxDynBordersL -- ** Rendering results , Result(..)+ , emptyResult , lookupAttrName+ , Extent(..) -- ** Rendering result lenses , imageL , cursorsL , visibilityRequestsL+ , extentsL -- ** Visibility requests , VisibilityRequest(..)@@ -56,127 +71,99 @@ -- * Making lenses , suffixLenses+ , suffixLensesWith + -- * Dynamic borders+ , bordersL+ , DynBorder(..)+ , dbStyleL, dbAttrL, dbSegmentsL+ , BorderSegment(..)+ , bsAcceptL, bsOfferL, bsDrawL+ , Edges(..)+ , eTopL, eBottomL, eRightL, eLeftL+ -- * Miscellaneous , Size(..)- , Padding(..) , Direction(..)- , Name(..) + -- * Renderer internals (for benchmarking)+ , RenderState++ -- * Re-exports for convenience+ , get+ , gets+ , put+ , modify+ , zoom ) where -import Control.Lens (_1, _2, to, (^.), (&), (.~), Lens')-import Data.Monoid (Monoid(..))-import Control.Monad.Trans.State.Lazy-import Control.Monad.Trans.Reader-import Graphics.Vty (Event, Image, emptyImage, Attr)-import Data.Default (Default(..))-import Data.Functor.Contravariant-import qualified Data.Map as M+import Lens.Micro (_1, _2, to, (^.))+import Lens.Micro.Type (Getting)+import Lens.Micro.Mtl (zoom)+#if !MIN_VERSION_base(4,13,0)+import Control.Monad.Fail (MonadFail)+#endif+import Control.Monad.State.Strict+import Control.Monad.Reader+import Graphics.Vty (Attr) import Brick.Types.TH import Brick.Types.Internal+import Brick.Types.EventM import Brick.AttrMap (AttrName, attrMapLookup) --- | The type of padding.-data Padding = Pad Int- -- ^ Pad by the specified number of rows or columns.- | Max- -- ^ Pad up to the number of available rows or columns.---- | The class of types that provide some basic event-handling.-class HandleEvent a where- -- | Handle a Vty event- handleEvent :: Event -> a -> EventM a---- | A convenience function for handling events intended for values--- that are targets of lenses in your application state. This function--- obtains the target value of the specified lens, invokes 'handleEvent'--- on it, and stores the resulting transformed value back in the state--- using the lens.-handleEventLensed :: (HandleEvent b)- => a- -- ^ The state value.- -> Lens' a b- -- ^ The lens to use to extract and store the target- -- of the event.- -> Event- -- ^ The event to handle.- -> EventM a-handleEventLensed v target ev = do- newB <- handleEvent ev (v^.target)- return $ v & target .~ newB---- | The monad in which event handlers run. Although it may be tempting--- to dig into the reader value yourself, just use--- 'Brick.Main.lookupViewport'.-type EventM a = ReaderT (M.Map Name Viewport) (StateT EventState IO) a---- | Widget growth policies. These policies communicate to layout--- algorithms how a widget uses space when being rendered. These--- policies influence rendering order and space allocation in the box--- layout algorithm.-data Size = Fixed- -- ^ Fixed widgets take up the same amount of space no matter- -- how much they are given (non-greedy).- | Greedy- -- ^ Greedy widgets take up all the space they are given.- deriving (Show, Eq, Ord)---- | The type of widgets.-data Widget =- Widget { hSize :: Size- -- ^ This widget's horizontal growth policy- , vSize :: Size- -- ^ This widget's vertical growth policy- , render :: RenderM Result- -- ^ This widget's rendering function- }---- | The type of the rendering monad. This monad is used by the--- library's rendering routines to manage rendering state and--- communicate rendering parameters to widgets' rendering functions.-type RenderM a = ReaderT Context (State RenderState) a---- | The type of result returned by a widget's rendering function. The--- result provides the image, cursor positions, and visibility requests--- that resulted from the rendering process.-data Result =- Result { image :: Image- -- ^ The final rendered image for a widget- , cursors :: [CursorLocation]- -- ^ The list of reported cursor positions for the- -- application to choose from- , visibilityRequests :: [VisibilityRequest]- -- ^ The list of visibility requests made by widgets rendered- -- while rendering this one (used by viewports)- }- deriving Show--instance Default Result where- def = Result emptyImage [] []+-- | Given a state value and an 'EventM' that mutates that state, run+-- the specified action and return resulting modified state.+nestEventM' :: a+ -- ^ The initial state to use in the nested action.+ -> EventM n a b+ -- ^ The action to run.+ -> EventM n s a+nestEventM' s act = fst <$> nestEventM s act --- | Get the current rendering context.-getContext :: RenderM Context-getContext = ask+-- | Given a state value and an 'EventM' that mutates that state, run+-- the specified action and return both the resulting modified state and+-- the result of the action itself.+nestEventM :: a+ -- ^ The initial state to use in the nested action.+ -> EventM n a b+ -- ^ The action to run.+ -> EventM n s (a, b)+nestEventM s' act = do+ ro <- EventM ask+ es <- EventM $ lift $ lift get+ vtyCtx <- getVtyContext+ let stInner = ES { nextAction = Continue+ , esScrollRequests = esScrollRequests es+ , cacheInvalidateRequests = cacheInvalidateRequests es+ , requestedVisibleNames = requestedVisibleNames es+ , vtyContext = vtyCtx+ }+ ((actResult, newSt), stInnerFinal) <- liftIO $ runStateT (runStateT (runReaderT (runEventM act) ro) s') stInner -suffixLenses ''Context-suffixLenses ''Result+ EventM $ lift $ lift $ modify $+ \st -> st { nextAction = nextAction stInnerFinal+ , esScrollRequests = esScrollRequests stInnerFinal+ , cacheInvalidateRequests = cacheInvalidateRequests stInnerFinal+ , requestedVisibleNames = requestedVisibleNames stInnerFinal+ , vtyContext = vtyContext stInnerFinal+ }+ return (newSt, actResult) -- | The rendering context's current drawing attribute.-attrL :: (Contravariant f, Functor f) => (Attr -> f Attr) -> Context -> f Context+attrL :: forall r n. Getting r (Context n) Attr attrL = to (\c -> attrMapLookup (c^.ctxAttrNameL) (c^.ctxAttrMapL)) -instance TerminalLocation CursorLocation where- columnL = cursorLocationL._1- column = column . cursorLocation- rowL = cursorLocationL._2- row = row . cursorLocation+instance TerminalLocation (CursorLocation n) where+ locationColumnL = cursorLocationL._1+ locationColumn = locationColumn . cursorLocation+ locationRowL = cursorLocationL._2+ locationRow = locationRow . cursorLocation -- | Given an attribute name, obtain the attribute for the attribute -- name by consulting the context's attribute map.-lookupAttrName :: AttrName -> RenderM Attr+lookupAttrName :: AttrName -> RenderM n Attr lookupAttrName n = do c <- getContext return $ attrMapLookup n (c^.ctxAttrMapL)
+ src/Brick/Types/Common.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE CPP #-}+module Brick.Types.Common+ ( Location(..)+ , locL+ , origin+ , Edges(..)+ , eTopL, eBottomL, eRightL, eLeftL+ ) where++import Brick.Types.TH (suffixLenses)+import qualified Data.Semigroup as Sem+import GHC.Generics+import Control.DeepSeq+import Lens.Micro (_1, _2)+#if MIN_VERSION_microlens(0,5,0)+import Lens.Micro.FieldN (Field1, Field2)+#else+import Lens.Micro.Internal (Field1, Field2)+#endif++-- | A terminal screen location.+data Location = Location { loc :: !(Int, Int)+ -- ^ (Column, Row)+ }+ deriving (Show, Eq, Ord, Read, Generic, NFData)++suffixLenses ''Location++instance Field1 Location Location Int Int where+ _1 = locL._1++instance Field2 Location Location Int Int where+ _2 = locL._2++-- | The origin (upper-left corner).+origin :: Location+origin = Location (0, 0)++instance Sem.Semigroup Location where+ (Location (w1, h1)) <> (Location (w2, h2)) = Location (w1+w2, h1+h2)++instance Monoid Location where+ mempty = origin+ mappend = (Sem.<>)++data Edges a = Edges { eTop, eBottom, eLeft, eRight :: !a }+ deriving (Eq, Ord, Read, Show, Functor, Generic, NFData)++suffixLenses ''Edges++instance Applicative Edges where+ pure a = Edges a a a a+ Edges ft fb fl fr <*> Edges vt vb vl vr =+ Edges (ft vt) (fb vb) (fl vl) (fr vr)++instance Monad Edges where+ Edges vt vb vl vr >>= f = Edges+ (eTop (f vt))+ (eBottom (f vb))+ (eLeft (f vl))+ (eRight (f vr))
+ src/Brick/Types/EventM.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+module Brick.Types.EventM+ ( EventM(..)+ , getVtyContext+ )+where++import Control.Monad.Catch (MonadThrow, MonadCatch, MonadMask)+#if !MIN_VERSION_base(4,13,0)+import Control.Monad.Fail (MonadFail)+#endif+import Control.Monad.Reader+import Control.Monad.State.Strict+import Lens.Micro.Mtl+import Lens.Micro.Mtl.Internal++import Brick.Types.Internal++-- | The monad in which event handlers run.+newtype EventM n s a =+ EventM { runEventM :: ReaderT (EventRO n) (StateT s (StateT (EventState n) IO)) a+ }+ deriving ( Functor, Applicative, Monad, MonadIO+ , MonadThrow, MonadCatch, MonadMask+#if !MIN_VERSION_base(4,13,0)+ , MonadFail+#endif+ )++instance MonadState s (EventM n s) where+ get = EventM $ lift get+ put = EventM . lift . put++getVtyContext :: EventM n s VtyContext+getVtyContext = EventM $ lift $ lift $ gets vtyContext++type instance Zoomed (EventM n s) = Zoomed (StateT s (StateT (EventState n) IO))++instance Zoom (EventM n s) (EventM n t) s t where+ zoom l (EventM m) = EventM (zoom l m)
src/Brick/Types/Internal.hs view
@@ -1,11 +1,13 @@+{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-} module Brick.Types.Internal ( ScrollRequest(..) , VisibilityRequest(..) , vrPositionL , vrSizeL- , Name(..) , Location(..) , locL , origin@@ -17,150 +19,447 @@ , CursorLocation(..) , cursorLocationL , cursorLocationNameL+ , cursorLocationVisibleL+ , VScrollBarOrientation(..)+ , HScrollBarOrientation(..)+ , VScrollbarRenderer(..)+ , HScrollbarRenderer(..)+ , ClickableScrollbarElement(..) , Context(..)- , EventState- , Next(..)+ , ctxAttrMapL+ , ctxAttrNameL+ , ctxBorderStyleL+ , ctxDynBordersL+ , ctxVScrollBarOrientationL+ , ctxVScrollBarRendererL+ , ctxHScrollBarOrientationL+ , ctxHScrollBarRendererL+ , ctxVScrollBarShowHandlesL+ , ctxHScrollBarShowHandlesL+ , ctxVScrollBarClickableConstrL+ , ctxHScrollBarClickableConstrL+ , availWidthL+ , availHeightL+ , windowWidthL+ , windowHeightL - , scrollRequestsL+ , Size(..)++ , EventState(..)+ , VtyContext(..)+ , EventRO(..)+ , NextAction(..)+ , Result(..)+ , Extent(..)+ , Edges(..)+ , eTopL, eBottomL, eRightL, eLeftL+ , BorderSegment(..)+ , bsAcceptL, bsOfferL, bsDrawL+ , DynBorder(..)+ , dbStyleL, dbAttrL, dbSegmentsL+ , CacheInvalidateRequest(..)+ , BrickEvent(..)+ , RenderM+ , getContext+ , lookupReportedExtent+ , Widget(..)++ , rsScrollRequestsL , viewportMapL+ , clickableNamesL+ , reportedExtentsL+ , renderCacheL+ , observedNamesL+ , requestedVisibleNames_L , vpSize , vpLeft , vpTop+ , vpContentSize+ , imageL+ , cursorsL+ , extentsL+ , bordersL+ , visibilityRequestsL+ , emptyResult ) where -import Control.Lens (Field1, Field2, _1, _2, Lens', makeLenses)-import Data.String-import Data.Monoid+import Control.Concurrent (ThreadId)+import Control.Monad.Reader+import Control.Monad.State.Strict+import Lens.Micro (_1, _2, Lens')+import Lens.Micro.Mtl (use)+import Lens.Micro.TH (makeLenses)+import qualified Data.Set as S import qualified Data.Map as M-import Graphics.Vty (DisplayRegion)+import Graphics.Vty (Vty, Event, Button, Modifier, DisplayRegion, Image, Attr, emptyImage)+import GHC.Generics+import Control.DeepSeq (NFData) +import Brick.BorderMap (BorderMap)+import qualified Brick.BorderMap as BM+import Brick.Types.Common import Brick.Types.TH import Brick.AttrMap (AttrName, AttrMap) import Brick.Widgets.Border.Style (BorderStyle) --- | Names of things. Used to name cursor locations, widgets, and--- viewports.-newtype Name = Name String- deriving (Eq, Show, Ord)--instance IsString Name where- fromString = Name--data RenderState =- RS { viewportMap :: M.Map Name Viewport- , scrollRequests :: [(Name, ScrollRequest)]- }--data ScrollRequest = HScrollBy Int- | HScrollPage Direction+data ScrollRequest = HScrollBy !Int+ | HScrollPage !Direction | HScrollToBeginning | HScrollToEnd- | VScrollBy Int- | VScrollPage Direction+ | VScrollBy !Int+ | VScrollPage !Direction | VScrollToBeginning | VScrollToEnd+ | SetTop !Int+ | SetLeft !Int+ deriving (Read, Show, Generic, NFData) +-- | Widget size policies. These policies communicate how a widget uses+-- space when being rendered. These policies influence rendering order+-- and space allocation in the box layout algorithm for 'hBox' and+-- 'vBox'.+data Size = Fixed+ -- ^ Widgets advertising this size policy should take up the+ -- same amount of space no matter how much they are given,+ -- i.e. their size depends on their contents alone rather than+ -- on the size of the rendering area.+ | Greedy+ -- ^ Widgets advertising this size policy must take up all the+ -- space they are given.+ deriving (Show, Eq, Ord)++-- | The type of widgets.+data Widget n =+ Widget { hSize :: !Size+ -- ^ This widget's horizontal growth policy+ , vSize :: !Size+ -- ^ This widget's vertical growth policy+ , render :: RenderM n (Result n)+ -- ^ This widget's rendering function+ }++data RenderState n =+ RS { viewportMap :: !(M.Map n Viewport)+ , rsScrollRequests :: ![(n, ScrollRequest)]+ , observedNames :: !(S.Set n)+ , renderCache :: !(M.Map n ([n], Result n))+ , clickableNames :: ![n]+ , requestedVisibleNames_ :: !(S.Set n)+ , reportedExtents :: !(M.Map n (Extent n))+ } deriving (Read, Show, Generic, NFData)++-- | The type of the rendering monad. This monad is used by the+-- library's rendering routines to manage rendering state and+-- communicate rendering parameters to widgets' rendering functions.+type RenderM n a = ReaderT (Context n) (State (RenderState n)) a++-- | Get the current rendering context.+getContext :: RenderM n (Context n)+getContext = ask++-- | Orientations for vertical scroll bars.+data VScrollBarOrientation = OnLeft | OnRight+ deriving (Show, Eq)++-- | Orientations for horizontal scroll bars.+data HScrollBarOrientation = OnBottom | OnTop+ deriving (Show, Eq)++-- | A vertical scroll bar renderer.+data VScrollbarRenderer n =+ VScrollbarRenderer { renderVScrollbar :: Widget n+ -- ^ How to render the body of the scroll bar.+ -- This should provide a widget that expands in+ -- whatever direction(s) this renderer will be+ -- used for. So, for example, this widget would+ -- need to be one that expands vertically such as+ -- @fill@. The same goes for the trough widget.+ , renderVScrollbarTrough :: Widget n+ -- ^ How to render the "trough" of the scroll bar+ -- (the area to either side of the scroll bar+ -- body). This should expand as described in the+ -- documentation for the scroll bar field.+ , renderVScrollbarHandleBefore :: Widget n+ -- ^ How to render the handle that appears at+ -- the top or left of the scrollbar. The result+ -- will be allowed to be at most one row high.+ , renderVScrollbarHandleAfter :: Widget n+ -- ^ How to render the handle that appears at the+ -- bottom or right of the scrollbar. The result+ -- will be allowed to be at most one row high.+ , scrollbarWidthAllocation :: Int+ -- ^ The number of columns that will be allocated+ -- to the scroll bar. This determines how much+ -- space the widgets of the scroll bar elements+ -- can take up. If they use less than this+ -- amount, padding will be applied between the+ -- scroll bar and the viewport contents.+ }++-- | A horizontal scroll bar renderer.+data HScrollbarRenderer n =+ HScrollbarRenderer { renderHScrollbar :: Widget n+ -- ^ How to render the body of the scroll bar.+ -- This should provide a widget that expands+ -- in whatever direction(s) this renderer will+ -- be used for. So, for example, this widget+ -- would need to be one that expands horizontally+ -- such as @fill@. The same goes for the trough+ -- widget.+ , renderHScrollbarTrough :: Widget n+ -- ^ How to render the "trough" of the scroll bar+ -- (the area to either side of the scroll bar+ -- body). This should expand as described in the+ -- documentation for the scroll bar field.+ , renderHScrollbarHandleBefore :: Widget n+ -- ^ How to render the handle that appears at the+ -- top or left of the scrollbar. The result will+ -- be allowed to be at most one column wide.+ , renderHScrollbarHandleAfter :: Widget n+ -- ^ How to render the handle that appears at the+ -- bottom or right of the scrollbar. The result+ -- will be allowed to be at most one column wide.+ , scrollbarHeightAllocation :: Int+ -- ^ The number of rows that will be allocated to+ -- the scroll bar. This determines how much space+ -- the widgets of the scroll bar elements can+ -- take up. If they use less than this amount,+ -- padding will be applied between the scroll bar+ -- and the viewport contents.+ }+ data VisibilityRequest =- VR { vrPosition :: Location- , vrSize :: DisplayRegion+ VR { vrPosition :: !Location+ , vrSize :: !DisplayRegion }- deriving Show+ deriving (Show, Eq, Read, Generic, NFData) -- | Describes the state of a viewport as it appears as its most recent -- rendering. data Viewport =- VP { _vpLeft :: Int+ VP { _vpLeft :: !Int -- ^ The column offset of left side of the viewport.- , _vpTop :: Int+ , _vpTop :: !Int -- ^ The row offset of the top of the viewport.- , _vpSize :: DisplayRegion+ , _vpSize :: !DisplayRegion -- ^ The size of the viewport.+ , _vpContentSize :: !DisplayRegion+ -- ^ The size of the contents of the viewport. }- deriving Show+ deriving (Show, Read, Generic, NFData) -- | The type of viewports that indicates the direction(s) in which a -- viewport is scrollable.-data ViewportType = Vertical- -- ^ Viewports of this type are scrollable only vertically.- | Horizontal- -- ^ Viewports of this type are scrollable only horizontally.- | Both- -- ^ Viewports of this type are scrollable vertically and horizontally.- deriving Show+data ViewportType =+ Vertical+ -- ^ Viewports of this type are scrollable only vertically.+ | Horizontal+ -- ^ Viewports of this type are scrollable only horizontally.+ | Both+ -- ^ Viewports of this type are scrollable vertically and horizontally.+ deriving (Show, Eq) -type EventState = [(Name, ScrollRequest)]+data CacheInvalidateRequest n =+ InvalidateSingle n+ | InvalidateEntire+ deriving (Ord, Eq) +data EventState n =+ ES { esScrollRequests :: ![(n, ScrollRequest)]+ , cacheInvalidateRequests :: !(S.Set (CacheInvalidateRequest n))+ , requestedVisibleNames :: !(S.Set n)+ , nextAction :: !NextAction+ , vtyContext :: VtyContext+ }++data VtyContext =+ VtyContext { vtyContextBuilder :: !(IO Vty)+ , vtyContextHandle :: !Vty+ , vtyContextThread :: !ThreadId+ , vtyContextPutEvent :: Event -> IO ()+ }++-- | An extent of a named area: its size, location, and origin.+data Extent n = Extent { extentName :: !n+ , extentUpperLeft :: !Location+ , extentSize :: !(Int, Int)+ }+ deriving (Show, Read, Generic, NFData)+ -- | The type of actions to take upon completion of an event handler.-data Next a = Continue a- | SuspendAndResume (IO a)- | Halt a+data NextAction =+ Continue+ | ContinueWithoutRedraw+ | Halt -- | Scrolling direction. data Direction = Up -- ^ Up/left | Down -- ^ Down/right---- | A terminal screen location.-data Location = Location { loc :: (Int, Int)- -- ^ (Column, Row)- }- deriving Show--suffixLenses ''Location--instance Field1 Location Location Int Int where- _1 = locL._1--instance Field2 Location Location Int Int where- _2 = locL._2+ deriving (Show, Eq, Read, Generic, NFData) -- | The class of types that behave like terminal locations. class TerminalLocation a where -- | Get the column out of the value- columnL :: Lens' a Int- column :: a -> Int+ locationColumnL :: Lens' a Int+ locationColumn :: a -> Int+ -- | Get the row out of the value- rowL :: Lens' a Int- row :: a -> Int+ locationRowL :: Lens' a Int+ locationRow :: a -> Int instance TerminalLocation Location where- columnL = _1- column (Location t) = fst t- rowL = _2- row (Location t) = snd t---- | The origin (upper-left corner).-origin :: Location-origin = Location (0, 0)--instance Monoid Location where- mempty = origin- mappend (Location (w1, h1)) (Location (w2, h2)) = Location (w1+w2, h1+h2)+ locationColumnL = _1+ locationColumn (Location t) = fst t+ locationRowL = _2+ locationRow (Location t) = snd t -- | A cursor location. These are returned by the rendering process.-data CursorLocation =+data CursorLocation n = CursorLocation { cursorLocation :: !Location -- ^ The location- , cursorLocationName :: !(Maybe Name)+ , cursorLocationName :: !(Maybe n) -- ^ The name of the widget associated with the location+ , cursorLocationVisible :: !Bool+ -- ^ Whether the cursor should actually be visible }- deriving Show+ deriving (Read, Show, Generic, NFData) +-- | A border character has four segments, one extending in each direction+-- (horizontally and vertically) from the center of the character.+data BorderSegment = BorderSegment+ { bsAccept :: !Bool+ -- ^ Would this segment be willing to be drawn if a neighbor wanted to+ -- connect to it?+ , bsOffer :: !Bool+ -- ^ Does this segment want to connect to its neighbor?+ , bsDraw :: !Bool+ -- ^ Should this segment be represented visually?+ } deriving (Eq, Ord, Read, Show, Generic, NFData)++-- | Information about how to redraw a dynamic border character when it abuts+-- another dynamic border character.+data DynBorder = DynBorder+ { dbStyle :: !BorderStyle+ -- ^ The 'Char's to use when redrawing the border. Also used to filter+ -- connections: only dynamic borders with equal 'BorderStyle's will connect+ -- to each other.+ , dbAttr :: !Attr+ -- ^ What 'Attr' to use to redraw the border character. Also used to filter+ -- connections: only dynamic borders with equal 'Attr's will connect to+ -- each other.+ , dbSegments :: !(Edges BorderSegment)+ } deriving (Eq, Read, Show, Generic, NFData)++-- | The type of result returned by a widget's rendering function. The+-- result provides the image, cursor positions, and visibility requests+-- that resulted from the rendering process.+data Result n =+ Result { image :: !Image+ -- ^ The final rendered image for a widget+ , cursors :: ![CursorLocation n]+ -- ^ The list of reported cursor positions for the+ -- application to choose from+ , visibilityRequests :: ![VisibilityRequest]+ -- ^ The list of visibility requests made by widgets rendered+ -- while rendering this one (used by viewports)+ , extents :: ![Extent n]+ -- Programmer's note: we don't try to maintain the invariant that+ -- the size of the borders closely matches the size of the 'image'+ -- field. Most widgets don't need to care about borders, and so they+ -- use the empty 'BorderMap' that has a degenerate rectangle. Only+ -- border-drawing widgets and the hbox/vbox stuff try to set this+ -- carefully. Even then, in the boxes, we only make sure that the+ -- 'BorderMap' is no larger than the entire concatenation of boxes,+ -- and it's certainly possible for it to be smaller. (Resizing+ -- 'BorderMap's is lossy, so we try to do it as little as possible.)+ -- If you're writing a widget, this should make it easier for you to+ -- do so; but beware this lack of invariant if you are consuming+ -- widgets.+ , borders :: !(BorderMap DynBorder)+ -- ^ Places where we may rewrite the edge of the image when+ -- placing this widget next to another one.+ }+ deriving (Show, Read, Generic, NFData)++emptyResult :: Result n+emptyResult =+ Result { image = emptyImage+ , cursors = []+ , visibilityRequests = []+ , extents = []+ , borders = BM.empty+ }++-- | The type of events.+data BrickEvent n e = VtyEvent !Event+ -- ^ The event was a Vty event.+ | AppEvent !e+ -- ^ The event was an application event.+ | MouseDown !n !Button ![Modifier] !Location+ -- ^ A mouse-down event on the specified region was+ -- received. The 'n' value is the resource name of+ -- the clicked widget (see 'clickable').+ | MouseUp !n !(Maybe Button) !Location+ -- ^ A mouse-up event on the specified region was+ -- received. The 'n' value is the resource name of+ -- the clicked widget (see 'clickable').+ deriving (Show, Eq, Ord)++data EventRO n = EventRO { eventViewportMap :: !(M.Map n Viewport)+ , latestExtents :: ![Extent n]+ , oldState :: !(RenderState n)+ }++-- | Clickable elements of a scroll bar.+data ClickableScrollbarElement =+ SBHandleBefore+ -- ^ The handle at the beginning (left/top) of the scroll bar.+ | SBHandleAfter+ -- ^ The handle at the end (right/bottom) of the scroll bar.+ | SBBar+ -- ^ The scroll bar itself.+ | SBTroughBefore+ -- ^ The trough before the scroll bar.+ | SBTroughAfter+ -- ^ The trough after the scroll bar.+ deriving (Eq, Show, Ord)+ -- | The rendering context. This tells widgets how to render: how much -- space they have in which to render, which attribute they should use--- to render, which bordring style should be used, and the attribute map+-- to render, which bordering style should be used, and the attribute map -- available for rendering.-data Context =- Context { ctxAttrName :: AttrName- , availWidth :: Int- , availHeight :: Int- , ctxBorderStyle :: BorderStyle- , ctxAttrMap :: AttrMap+data Context n =+ Context { ctxAttrName :: !AttrName+ , availWidth :: !Int+ , availHeight :: !Int+ , windowWidth :: !Int+ , windowHeight :: !Int+ , ctxBorderStyle :: !BorderStyle+ , ctxAttrMap :: !AttrMap+ , ctxDynBorders :: !Bool+ , ctxVScrollBarOrientation :: !(Maybe VScrollBarOrientation)+ , ctxVScrollBarRenderer :: !(Maybe (VScrollbarRenderer n))+ , ctxHScrollBarOrientation :: !(Maybe HScrollBarOrientation)+ , ctxHScrollBarRenderer :: !(Maybe (HScrollbarRenderer n))+ , ctxVScrollBarShowHandles :: !Bool+ , ctxHScrollBarShowHandles :: !Bool+ , ctxVScrollBarClickableConstr :: !(Maybe (ClickableScrollbarElement -> n -> n))+ , ctxHScrollBarClickableConstr :: !(Maybe (ClickableScrollbarElement -> n -> n)) } suffixLenses ''RenderState suffixLenses ''VisibilityRequest suffixLenses ''CursorLocation+suffixLenses ''Context+suffixLenses ''DynBorder+suffixLenses ''Result+suffixLenses ''BorderSegment makeLenses ''Viewport++lookupReportedExtent :: (Ord n) => n -> RenderM n (Maybe (Extent n))+lookupReportedExtent n = do+ m <- lift $ use reportedExtentsL+ return $ M.lookup n m
src/Brick/Types/TH.hs view
@@ -1,18 +1,25 @@ module Brick.Types.TH ( suffixLenses+ , suffixLensesWith ) where import qualified Language.Haskell.TH.Syntax as TH import qualified Language.Haskell.TH.Lib as TH -import Control.Lens (DefName(..), makeLensesWith, lensRules, (&), (.~), lensField)+import Lens.Micro ((&), (.~))+import Lens.Micro.TH (DefName(..), LensRules, makeLensesWith, lensRules, lensField) -- | A template haskell function to build lenses for a record type. This--- function differs from the 'Control.Lens.makeLenses' function in that+-- function differs from the 'Lens.Micro.TH.makeLenses' function in that -- it does not require the record fields to be prefixed with underscores -- and it adds an "L" suffix to lens names to make it clear that they -- are lenses. suffixLenses :: TH.Name -> TH.DecsQ-suffixLenses = makeLensesWith $- lensRules & lensField .~ (\_ _ name -> [TopName $ TH.mkName $ TH.nameBase name ++ "L"])+suffixLenses = suffixLensesWith "L" lensRules++-- | A more general version of 'suffixLenses' that allows customization+-- of the lens-building rules and allows customization of the suffix.+suffixLensesWith :: String -> LensRules -> TH.Name -> TH.DecsQ+suffixLensesWith suffix rs = makeLensesWith $+ rs & lensField .~ (\_ _ name -> [TopName $ TH.mkName $ TH.nameBase name ++ suffix])
src/Brick/Util.hs view
@@ -4,12 +4,15 @@ , on , fg , bg+ , style , clOffset ) where -import Control.Lens ((&), (%~))+import Lens.Micro ((&), (%~))+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>))+#endif import Graphics.Vty import Brick.Types.Internal (Location(..), CursorLocation(..), cursorLocationL)@@ -50,10 +53,15 @@ fg = (defAttr `withForeColor`) -- | Create an attribute from the specified background color (the--- background color is the "default").+-- foreground color is the "default"). bg :: Color -> Attr bg = (defAttr `withBackColor`) +-- | Create an attribute from the specified style (the colors are the+-- "default").+style :: Style -> Attr+style = (defAttr `withStyle`)+ -- | Add a 'Location' offset to the specified 'CursorLocation'.-clOffset :: CursorLocation -> Location -> CursorLocation+clOffset :: CursorLocation n -> Location -> CursorLocation n clOffset cl off = cl & cursorLocationL %~ (<> off)
src/Brick/Widgets/Border.hs view
@@ -18,98 +18,89 @@ -- * Drawing single border elements , borderElem - -- * Border attribute names+ -- * Attribute names , borderAttr- , vBorderAttr , hBorderAttr- , hBorderLabelAttr- , tlCornerAttr- , trCornerAttr- , blCornerAttr- , brCornerAttr+ , vBorderAttr++ -- * Utility+ , joinableBorder ) where -import Control.Applicative ((<$>))-import Control.Lens ((^.), to)+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>))+#endif+import Lens.Micro ((^.), (&), (.~), to) import Graphics.Vty (imageHeight, imageWidth) import Brick.AttrMap import Brick.Types import Brick.Widgets.Core-import Brick.Widgets.Center (hCenterWith) import Brick.Widgets.Border.Style (BorderStyle(..))+import Brick.Widgets.Internal (renderDynBorder)+import Data.IMap (Run(..))+import qualified Brick.BorderMap as BM -- | The top-level border attribute name. borderAttr :: AttrName-borderAttr = "border"---- | The vertical border attribute name.-vBorderAttr :: AttrName-vBorderAttr = borderAttr <> "vertical"+borderAttr = attrName "border" --- | The horizontal border attribute name.+-- | The horizontal border attribute name. Inherits from 'borderAttr'. hBorderAttr :: AttrName-hBorderAttr = borderAttr <> "horizontal"---- | The attribute used for horizontal border labels.-hBorderLabelAttr :: AttrName-hBorderLabelAttr = hBorderAttr <> "label"---- | The attribute used for border box top-left corners.-tlCornerAttr :: AttrName-tlCornerAttr = borderAttr <> "corner" <> "tl"---- | The attribute used for border box top-right corners.-trCornerAttr :: AttrName-trCornerAttr = borderAttr <> "corner" <> "tr"---- | The attribute used for border box bottom-left corners.-blCornerAttr :: AttrName-blCornerAttr = borderAttr <> "corner" <> "bl"+hBorderAttr = borderAttr <> attrName "horizontal" --- | The attribute used for border box bottom-right corners.-brCornerAttr :: AttrName-brCornerAttr = borderAttr <> "corner" <> "br"+-- | The vertical border attribute name. Inherits from 'borderAttr'.+vBorderAttr :: AttrName+vBorderAttr = borderAttr <> attrName "vertical" -- | Draw the specified border element using the active border style -- using 'borderAttr'.-borderElem :: (BorderStyle -> Char) -> Widget+--+-- Does not participate in dynamic borders (due to the difficulty of+-- introspecting on the first argument); consider using 'joinableBorder'+-- instead.+borderElem :: (BorderStyle -> Char) -> Widget n borderElem f = Widget Fixed Fixed $ do bs <- ctxBorderStyle <$> getContext render $ withAttr borderAttr $ str [f bs] -- | Put a border around the specified widget.-border :: Widget -> Widget+border :: Widget n -> Widget n border = border_ Nothing -- | Put a border around the specified widget with the specified label -- widget placed in the middle of the top horizontal border.-borderWithLabel :: Widget+--+-- Note that a border will wrap its child widget as tightly as possible,+-- which means that if the child widget is narrower than the label+-- widget, the label widget will be truncated. If you want to avoid+-- this behavior, add a 'fill' or other space-filling wrapper to the+-- bordered widget so that it takes up enough room to make the border+-- horizontally able to avoid truncating the label.+borderWithLabel :: Widget n -- ^ The label widget- -> Widget+ -> Widget n -- ^ The widget to put a border around- -> Widget+ -> Widget n borderWithLabel label = border_ (Just label) -border_ :: Maybe Widget -> Widget -> Widget+border_ :: Maybe (Widget n) -> Widget n -> Widget n border_ label wrapped = Widget (hSize wrapped) (vSize wrapped) $ do- bs <- ctxBorderStyle <$> getContext c <- getContext middleResult <- render $ hLimit (c^.availWidthL - 2) $ vLimit (c^.availHeightL - 2) $ wrapped - let top = (withAttr tlCornerAttr $ str [bsCornerTL bs])- <+> hBorder_ label <+>- (withAttr trCornerAttr $ str [bsCornerTR bs])- bottom = (withAttr blCornerAttr $ str [bsCornerBL bs])- <+> hBorder <+>- (withAttr brCornerAttr $ str [bsCornerBR bs])+ let tl = joinableBorder (Edges False True False True)+ tr = joinableBorder (Edges False True True False)+ bl = joinableBorder (Edges True False False True)+ br = joinableBorder (Edges True False True False)+ top = tl <+> maybe hBorder hBorderWithLabel label <+> tr+ bottom = bl <+> hBorder <+> br middle = vBorder <+> (Widget Fixed Fixed $ return middleResult) <+> vBorder total = top <=> middle <=> bottom @@ -117,27 +108,76 @@ $ vLimit (middleResult^.imageL.to imageHeight + 2) $ total --- | A horizontal border. Fills all horizontal space.-hBorder :: Widget-hBorder = hBorder_ Nothing+-- | A horizontal border. Fills all horizontal space. Draws using+-- 'hBorderAttr'.+hBorder :: Widget n+hBorder =+ withAttr borderAttr $ Widget Greedy Fixed $ do+ ctx <- getContext+ let bs = ctxBorderStyle ctx+ w = availWidth ctx+ db <- dynBorderFromDirections (Edges False False True True)+ let dynBorders = BM.insertH mempty (Run w db)+ $ BM.emptyCoordinates (Edges 0 0 0 (w-1))+ setDynBorders dynBorders $ render $ withAttr hBorderAttr+ $ vLimit 1 $ fill (bsHorizontal bs) -- | A horizontal border with a label placed in the center of the -- border. Fills all horizontal space.-hBorderWithLabel :: Widget+hBorderWithLabel :: Widget n -- ^ The label widget- -> Widget-hBorderWithLabel label = hBorder_ (Just label)--hBorder_ :: Maybe Widget -> Widget-hBorder_ label =+ -> Widget n+hBorderWithLabel label = Widget Greedy Fixed $ do- bs <- ctxBorderStyle <$> getContext- let msg = maybe (str [bsHorizontal bs]) (withAttr hBorderLabelAttr) label- render $ vLimit 1 $ withAttr hBorderAttr $ hCenterWith (Just $ bsHorizontal bs) msg+ res <- render $ vLimit 1 label+ render $ hBox [hBorder, Widget Fixed Fixed (return res), hBorder] --- | A vertical border. Fills all vertical space.-vBorder :: Widget+-- | A vertical border. Fills all vertical space. Draws using+-- 'vBorderAttr'.+vBorder :: Widget n vBorder =- Widget Fixed Greedy $ do- bs <- ctxBorderStyle <$> getContext- render $ hLimit 1 $ withAttr vBorderAttr $ fill (bsVertical bs)+ withAttr borderAttr $ Widget Fixed Greedy $ do+ ctx <- getContext+ let bs = ctxBorderStyle ctx+ h = availHeight ctx+ db <- dynBorderFromDirections (Edges True True False False)+ let dynBorders = BM.insertV mempty (Run h db)+ $ BM.emptyCoordinates (Edges 0 (h-1) 0 0)+ setDynBorders dynBorders $ render $ withAttr vBorderAttr+ $ hLimit 1 $ fill (bsVertical bs)++-- | Initialize a 'DynBorder'. It will be 'bsDraw'n and 'bsOffer'ing+-- in the given directions to begin with, and accept join offers from+-- all directions. We consult the context to choose the 'dbStyle' and+-- 'dbAttr'.+--+-- This is likely to be useful only for custom widgets that need more+-- complicated dynamic border behavior than 'border', 'vBorder', or+-- 'hBorder' offer.+dynBorderFromDirections :: Edges Bool -> RenderM n DynBorder+dynBorderFromDirections dirs = do+ ctx <- getContext+ return DynBorder+ { dbStyle = ctxBorderStyle ctx+ , dbAttr = attrMapLookup (ctxAttrName ctx) (ctxAttrMap ctx)+ , dbSegments = (\draw -> BorderSegment True draw draw) <$> dirs+ }++-- | Replace the 'Result'\'s dynamic borders with the given one,+-- provided the context says to use dynamic borders at all.+setDynBorders :: BM.BorderMap DynBorder -> RenderM n (Result n) -> RenderM n (Result n)+setDynBorders newBorders act = do+ dyn <- ctxDynBorders <$> getContext+ res <- act+ return $ if dyn+ then res & bordersL .~ newBorders+ else res++-- | A single-character dynamic border that will react to neighboring+-- borders, initially connecting in the given directions.+joinableBorder :: Edges Bool -> Widget n+joinableBorder dirs = withAttr borderAttr . Widget Fixed Fixed $ do+ db <- dynBorderFromDirections dirs+ setDynBorders+ (BM.singleton mempty db)+ (render (raw (renderDynBorder db)))
src/Brick/Widgets/Border/Style.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-} -- | This module provides styles for borders as used in terminal -- applications. Your mileage may vary on some of the fancier styles -- due to varying support for some border characters in the fonts your@@ -8,8 +9,7 @@ -- -- To use these in your widgets, see -- 'Brick.Widgets.Core.withBorderStyle'. By default, widgets rendered--- without a specified border style use 'unicode' via the 'Default'--- instance provided by 'BorderStyle'.+-- without a specified border style use 'unicode' style. module Brick.Widgets.Border.Style ( BorderStyle(..) , borderStyleFromChar@@ -17,10 +17,12 @@ , unicode , unicodeBold , unicodeRounded+ , defaultBorderStyle ) where -import Data.Default+import GHC.Generics+import Control.DeepSeq -- | A border style for use in any widget that needs to render borders -- in a consistent style.@@ -48,10 +50,10 @@ , bsVertical :: Char -- ^ Vertical border character }- deriving (Show, Read)+ deriving (Show, Read, Eq, Generic, NFData) -instance Default BorderStyle where- def = unicode+defaultBorderStyle :: BorderStyle+defaultBorderStyle = unicode -- | Make a border style using the specified character everywhere. borderStyleFromChar :: Char -> BorderStyle
src/Brick/Widgets/Center.hs view
@@ -3,42 +3,66 @@ ( -- * Centering horizontally hCenter , hCenterWith+ , hCenterLayer -- * Centering vertically , vCenter , vCenterWith+ , vCenterLayer -- * Centering both horizontally and vertically , center , centerWith+ , centerLayer -- * Centering about an arbitrary origin , centerAbout ) where -import Control.Lens ((^.), (&), (.~), to)-import Graphics.Vty (imageWidth, imageHeight, horizCat, charFill, vertCat)+import Lens.Micro ((^.), (&), (.~), to)+import Data.Maybe (fromMaybe)+import Graphics.Vty (imageWidth, imageHeight, horizCat, charFill, vertCat,+ translateX, translateY) import Brick.Types import Brick.Widgets.Core -- | Center the specified widget horizontally. Consumes all available -- horizontal space.-hCenter :: Widget -> Widget+hCenter :: Widget n -> Widget n hCenter = hCenterWith Nothing +-- | Center the specified widget horizontally using a Vty image+-- translation. Consumes all available horizontal space. Unlike hCenter,+-- this does not fill the surrounding space so it is suitable for use+-- as a layer. Layers underneath this widget will be visible in regions+-- surrounding the centered widget.+hCenterLayer :: Widget n -> Widget n+hCenterLayer p =+ Widget Greedy (vSize p) $ do+ result <- render p+ c <- getContext+ let rWidth = result^.imageL.to imageWidth+ leftPaddingAmount = max 0 $ (c^.availWidthL - rWidth) `div` 2+ paddedImage = translateX leftPaddingAmount $ result^.imageL+ off = Location (leftPaddingAmount, 0)+ if leftPaddingAmount == 0 then+ return result else+ return $ addResultOffset off+ $ result & imageL .~ paddedImage+ -- | Center the specified widget horizontally. Consumes all available -- horizontal space. Uses the specified character to fill in the space -- to either side of the centered widget (defaults to space).-hCenterWith :: Maybe Char -> Widget -> Widget+hCenterWith :: Maybe Char -> Widget n -> Widget n hCenterWith mChar p =- let ch = maybe ' ' id mChar+ let ch = fromMaybe ' ' mChar in Widget Greedy (vSize p) $ do result <- render p c <- getContext let rWidth = result^.imageL.to imageWidth rHeight = result^.imageL.to imageHeight- remainder = c^.availWidthL - (leftPaddingAmount * 2)- leftPaddingAmount = (c^.availWidthL - rWidth) `div` 2- rightPaddingAmount = leftPaddingAmount + remainder+ remainder = max 0 $ c^.availWidthL - (rWidth + (leftPaddingAmount * 2))+ leftPaddingAmount = max 0 $ (c^.availWidthL - rWidth) `div` 2+ rightPaddingAmount = max 0 $ leftPaddingAmount + remainder leftPadding = charFill (c^.attrL) ch leftPaddingAmount rHeight rightPadding = charFill (c^.attrL) ch rightPaddingAmount rHeight paddedImage = horizCat [ leftPadding@@ -52,23 +76,42 @@ $ result & imageL .~ paddedImage -- | Center a widget vertically. Consumes all vertical space.-vCenter :: Widget -> Widget+vCenter :: Widget n -> Widget n vCenter = vCenterWith Nothing +-- | Center the specified widget vertically using a Vty image+-- translation. Consumes all available vertical space. Unlike vCenter,+-- this does not fill the surrounding space so it is suitable for use+-- as a layer. Layers underneath this widget will be visible in regions+-- surrounding the centered widget.+vCenterLayer :: Widget n -> Widget n+vCenterLayer p =+ Widget (hSize p) Greedy $ do+ result <- render p+ c <- getContext+ let rHeight = result^.imageL.to imageHeight+ topPaddingAmount = max 0 $ (c^.availHeightL - rHeight) `div` 2+ paddedImage = translateY topPaddingAmount $ result^.imageL+ off = Location (0, topPaddingAmount)+ if topPaddingAmount == 0 then+ return result else+ return $ addResultOffset off+ $ result & imageL .~ paddedImage+ -- | Center a widget vertically. Consumes all vertical space. Uses the -- specified character to fill in the space above and below the centered -- widget (defaults to space).-vCenterWith :: Maybe Char -> Widget -> Widget+vCenterWith :: Maybe Char -> Widget n -> Widget n vCenterWith mChar p =- let ch = maybe ' ' id mChar+ let ch = fromMaybe ' ' mChar in Widget (hSize p) Greedy $ do result <- render p c <- getContext let rWidth = result^.imageL.to imageWidth rHeight = result^.imageL.to imageHeight- remainder = c^.availHeightL - (topPaddingAmount * 2)- topPaddingAmount = (c^.availHeightL - rHeight) `div` 2- bottomPaddingAmount = topPaddingAmount + remainder+ remainder = max 0 $ c^.availHeightL - (topPaddingAmount * 2)+ topPaddingAmount = max 0 $ (c^.availHeightL - rHeight) `div` 2+ bottomPaddingAmount = max 0 $ topPaddingAmount + remainder topPadding = charFill (c^.attrL) ch rWidth topPaddingAmount bottomPadding = charFill (c^.attrL) ch rWidth bottomPaddingAmount paddedImage = vertCat [ topPadding@@ -83,18 +126,26 @@ -- | Center a widget both vertically and horizontally. Consumes all -- available vertical and horizontal space.-center :: Widget -> Widget+center :: Widget n -> Widget n center = centerWith Nothing -- | Center a widget both vertically and horizontally. Consumes all -- available vertical and horizontal space. Uses the specified character -- to fill in the space around the centered widget (defaults to space).-centerWith :: Maybe Char -> Widget -> Widget+centerWith :: Maybe Char -> Widget n -> Widget n centerWith c = vCenterWith c . hCenterWith c +-- | Center a widget both vertically and horizontally using a Vty image+-- translation. Consumes all available vertical and horizontal space.+-- Unlike center, this does not fill in the surrounding space with a+-- character so it is usable as a layer. Any widget underneath this one+-- will be visible in the region surrounding the centered widget.+centerLayer :: Widget n -> Widget n+centerLayer = vCenterLayer . hCenterLayer+ -- | Center the widget horizontally and vertically about the specified -- origin.-centerAbout :: Location -> Widget -> Widget+centerAbout :: Location -> Widget n -> Widget n centerAbout l p = Widget Greedy Greedy $ do -- Compute translation offset so that loc is in the middle of the@@ -102,7 +153,16 @@ c <- getContext let centerW = c^.availWidthL `div` 2 centerH = c^.availHeightL `div` 2- off = Location ( centerW - l^.columnL- , centerH - l^.rowL+ off = Location ( centerW - l^.locationColumnL+ , centerH - l^.locationRowL )- render $ translateBy off p+ result <- render $ translateBy off p++ -- Pad the result so it consumes available space+ let rightPaddingAmt = max 0 $ c^.availWidthL - imageWidth (result^.imageL)+ bottomPaddingAmt = max 0 $ c^.availHeightL - imageHeight (result^.imageL)+ rightPadding = charFill (c^.attrL) ' ' rightPaddingAmt (imageHeight $ result^.imageL)+ bottomPadding = charFill (c^.attrL) ' ' (imageWidth $ result^.imageL) bottomPaddingAmt+ paddedImg = horizCat [vertCat [result^.imageL, bottomPadding], rightPadding]++ return $ result & imageL .~ paddedImg
src/Brick/Widgets/Core.hs view
@@ -1,741 +1,1986 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TupleSections #-}--- | This module provides the core widget combinators and rendering--- routines. Everything this library does is in terms of these basic--- primitives.-module Brick.Widgets.Core- ( -- * Basic rendering primitives- emptyWidget- , raw- , txt- , str- , fill-- -- * Padding- , padLeft- , padRight- , padTop- , padBottom- , padLeftRight- , padTopBottom- , padAll-- -- * Box layout- , (<=>)- , (<+>)- , hBox- , vBox-- -- * Limits- , hLimit- , vLimit-- -- * Attribute mangement- , withDefAttr- , withAttr- , forceAttr- , updateAttrMap-- -- * Border style management- , withBorderStyle-- -- * Cursor placement- , showCursor-- -- * Translation- , translateBy-- -- * Cropping- , cropLeftBy- , cropRightBy- , cropTopBy- , cropBottomBy-- -- * Scrollable viewports- , viewport- , visible- , visibleRegion-- -- ** Adding offsets to cursor positions and visibility requests- , addResultOffset-- -- ** Cropping results- , cropToContext- )-where--import Control.Applicative-import Control.Lens ((^.), (.~), (&), (%~), to, _1, _2, each, to, ix)-import Control.Monad (when)-import Control.Monad.Trans.State.Lazy-import Control.Monad.Trans.Reader-import Control.Monad.Trans.Class (lift)-import qualified Data.Text as T-import Data.Default-import Data.Monoid ((<>), mempty)-import qualified Data.Map as M-import qualified Data.Function as DF-import Data.List (sortBy, partition)-import Control.Lens (Lens')-import qualified Graphics.Vty as V-import Control.DeepSeq--import Brick.Types-import Brick.Types.Internal-import Brick.Widgets.Border.Style-import Brick.Util (clOffset, clamp)-import Brick.AttrMap-import Brick.Widgets.Internal---- | When rendering the specified widget, use the specified border style--- for any border rendering.-withBorderStyle :: BorderStyle -> Widget -> Widget-withBorderStyle bs p = Widget (hSize p) (vSize p) $ withReaderT (& ctxBorderStyleL .~ bs) (render p)---- | The empty widget.-emptyWidget :: Widget-emptyWidget = raw V.emptyImage---- | Add an offset to all cursor locations and visbility requests--- in the specified rendering result. This function is critical for--- maintaining correctness in the rendering results as they are--- processed successively by box layouts and other wrapping combinators,--- since calls to this function result in converting from widget-local--- coordinates to (ultimately) terminal-global ones so they can be used--- by other combinators. You should call this any time you render--- something and then translate it or otherwise offset it from its--- original origin.-addResultOffset :: Location -> Result -> Result-addResultOffset off = addCursorOffset off . addVisibilityOffset off--addVisibilityOffset :: Location -> Result -> Result-addVisibilityOffset off r = r & visibilityRequestsL.each.vrPositionL %~ (off <>)--addCursorOffset :: Location -> Result -> Result-addCursorOffset off r =- let onlyVisible = filter isVisible- isVisible l = l^.columnL >= 0 && l^.rowL >= 0- in r & cursorsL %~ (\cs -> onlyVisible $ (`clOffset` off) <$> cs)--unrestricted :: Int-unrestricted = 100000---- | Build a widget from a 'String'. Breaks newlines up and space-pads--- short lines out to the length of the longest line.-str :: String -> Widget-str s =- Widget Fixed Fixed $ do- c <- getContext- let theLines = fixEmpty <$> (dropUnused . lines) s- fixEmpty [] = " "- fixEmpty l = l- dropUnused l = take (availWidth c) <$> take (availHeight c) l- case force theLines of- [] -> return def- [one] -> return $ def & imageL .~ (V.string (c^.attrL) one)- multiple ->- let maxLength = maximum $ length <$> multiple- lineImgs = lineImg <$> multiple- lineImg lStr = V.string (c^.attrL) (lStr ++ replicate (maxLength - length lStr) ' ')- in return $ def & imageL .~ (V.vertCat lineImgs)---- | Build a widget from a one-line 'T.Text' value. Behaves the same as--- 'str'.-txt :: T.Text -> Widget-txt = str . T.unpack---- | Pad the specified widget on the left. If max padding is used, this--- grows greedily horizontally; otherwise it defers to the padded--- widget.-padLeft :: Padding -> Widget -> Widget-padLeft padding p =- let (f, sz) = case padding of- Max -> (id, Greedy)- Pad i -> (hLimit i, hSize p)- in Widget sz (vSize p) $ do- result <- render p- render $ (f $ vLimit (result^.imageL.to V.imageHeight) $ fill ' ') <+>- (Widget Fixed Fixed $ return result)---- | Pad the specified widget on the right. If max padding is used,--- this grows greedily horizontally; otherwise it defers to the padded--- widget.-padRight :: Padding -> Widget -> Widget-padRight padding p =- let (f, sz) = case padding of- Max -> (id, Greedy)- Pad i -> (hLimit i, hSize p)- in Widget sz (vSize p) $ do- result <- render p- render $ (Widget Fixed Fixed $ return result) <+>- (f $ vLimit (result^.imageL.to V.imageHeight) $ fill ' ')---- | Pad the specified widget on the top. If max padding is used, this--- grows greedily vertically; otherwise it defers to the padded widget.-padTop :: Padding -> Widget -> Widget-padTop padding p =- let (f, sz) = case padding of- Max -> (id, Greedy)- Pad i -> (vLimit i, vSize p)- in Widget (hSize p) sz $ do- result <- render p- render $ (f $ hLimit (result^.imageL.to V.imageWidth) $ fill ' ') <=>- (Widget Fixed Fixed $ return result)---- | Pad the specified widget on the bottom. If max padding is used,--- this grows greedily vertically; otherwise it defers to the padded--- widget.-padBottom :: Padding -> Widget -> Widget-padBottom padding p =- let (f, sz) = case padding of- Max -> (id, Greedy)- Pad i -> (vLimit i, vSize p)- in Widget (hSize p) sz $ do- result <- render p- render $ (Widget Fixed Fixed $ return result) <=>- (f $ hLimit (result^.imageL.to V.imageWidth) $ fill ' ')---- | Pad a widget on the left and right. Defers to the padded widget for--- growth policy.-padLeftRight :: Int -> Widget -> Widget-padLeftRight c w = padLeft (Pad c) $ padRight (Pad c) w---- | Pad a widget on the top and bottom. Defers to the padded widget for--- growth policy.-padTopBottom :: Int -> Widget -> Widget-padTopBottom r w = padTop (Pad r) $ padBottom (Pad r) w---- | Pad a widget on all sides. Defers to the padded widget for growth--- policy.-padAll :: Int -> Widget -> Widget-padAll v w = padLeftRight v $ padTopBottom v w---- | Fill all available space with the specified character. Grows both--- horizontally and vertically.-fill :: Char -> Widget-fill ch =- Widget Greedy Greedy $ do- c <- getContext- return $ def & imageL .~ (V.charFill (c^.attrL) ch (c^.availWidthL) (c^.availHeightL))---- | Vertical box layout: put the specified widgets one above the other--- in the specified order (uppermost first). Defers growth policies to--- the growth policies of the contained widgets (if any are greedy, so--- is the box).-vBox :: [Widget] -> Widget-vBox [] = emptyWidget-vBox pairs = renderBox vBoxRenderer pairs---- | Horizontal box layout: put the specified widgets next to each other--- in the specified order (leftmost first). Defers growth policies to--- the growth policies of the contained widgets (if any are greedy, so--- is the box).-hBox :: [Widget] -> Widget-hBox [] = emptyWidget-hBox pairs = renderBox hBoxRenderer pairs---- | The process of rendering widgets in a box layout is exactly the--- same except for the dimension under consideration (width vs. height),--- in which case all of the same operations that consider one dimension--- in the layout algorithm need to be switched to consider the other.--- Because of this we fill a BoxRenderer with all of the functions--- needed to consider the "primary" dimension (e.g. vertical if the--- box layout is vertical) as well as the "secondary" dimension (e.g.--- horizontal if the box layout is vertical). Doing this permits us to--- have one implementation for box layout and parameterizing on the--- orientation of all of the operations.-data BoxRenderer =- BoxRenderer { contextPrimary :: Lens' Context Int- , contextSecondary :: Lens' Context Int- , imagePrimary :: V.Image -> Int- , imageSecondary :: V.Image -> Int- , limitPrimary :: Int -> Widget -> Widget- , limitSecondary :: Int -> Widget -> Widget- , primaryWidgetSize :: Widget -> Size- , concatenatePrimary :: [V.Image] -> V.Image- , locationFromOffset :: Int -> Location- , padImageSecondary :: Int -> V.Image -> V.Attr -> V.Image- }--vBoxRenderer :: BoxRenderer-vBoxRenderer =- BoxRenderer { contextPrimary = availHeightL- , contextSecondary = availWidthL- , imagePrimary = V.imageHeight- , imageSecondary = V.imageWidth- , limitPrimary = vLimit- , limitSecondary = hLimit- , primaryWidgetSize = vSize- , concatenatePrimary = V.vertCat- , locationFromOffset = Location . (0 ,)- , padImageSecondary = \amt img a ->- let p = V.charFill a ' ' amt (V.imageHeight img)- in V.horizCat [img, p]- }--hBoxRenderer :: BoxRenderer-hBoxRenderer =- BoxRenderer { contextPrimary = availWidthL- , contextSecondary = availHeightL- , imagePrimary = V.imageWidth- , imageSecondary = V.imageHeight- , limitPrimary = hLimit- , limitSecondary = vLimit- , primaryWidgetSize = hSize- , concatenatePrimary = V.horizCat- , locationFromOffset = Location . (, 0)- , padImageSecondary = \amt img a ->- let p = V.charFill a ' ' (V.imageWidth img) amt- in V.vertCat [img, p]- }---- | Render a series of widgets in a box layout in the order given.------ The growth policy of a box layout is the most unrestricted of the--- growth policies of the widgets it contains, so to determine the hSize--- and vSize of the box we just take the maximum (using the Ord instance--- for Size) of all of the widgets to be rendered in the box.------ Then the box layout algorithm proceeds as follows. We'll use--- the vertical case to concretely describe the algorithm, but the--- horizontal case can be envisioned just by exchanging all--- "vertical"/"horizontal" and "rows"/"columns", etc., in the--- description.------ The growth policies of the child widgets determine the order in which--- they are rendered, i.e., the order in which space in the box is--- allocated to widgets as the algorithm proceeds. This is because order--- matters: if we render greedy widgets first, there will be no space--- left for non-greedy ones.------ So we render all widgets with size 'Fixed' in the vertical dimension--- first. Each is rendered with as much room as the overall box has, but--- we assume that they will not be greedy and use it all. If they do,--- maybe it's because the terminal is small and there just isn't enough--- room to render everything.------ Then the remaining height is distributed evenly amongst all remaining--- (greedy) widgets and they are rendered in sub-boxes that are as high--- as this even slice of rows and as wide as the box is permitted to be.--- We only do this step at all if rendering the non-greedy widgets left--- us any space, i.e., if there were any rows left.------ After rendering the non-greedy and then greedy widgets, their images--- are sorted so that they are stored in the order the original widgets--- were given. All cursor locations and visibility requests in each--- sub-widget are translated according to the position of the sub-widget--- in the box.------ All images are padded to be as wide as the widest sub-widget to--- prevent attribute over-runs. Without this step the attribute used by--- a sub-widget may continue on in an undesirable fashion until it hits--- something with a different attribute. To prevent this and to behave--- in the least surprising way, we pad the image on the right with--- whitespace using the context's current attribute.------ Finally, the padded images are concatenated together vertically and--- returned along with the translated cursor positions and visibility--- requests.-renderBox :: BoxRenderer -> [Widget] -> Widget-renderBox br ws = do- Widget (maximum $ hSize <$> ws) (maximum $ vSize <$> ws) $ do- c <- getContext-- let pairsIndexed = zip [(0::Int)..] ws- (his, lows) = partition (\p -> (primaryWidgetSize br $ snd p) == Fixed) pairsIndexed-- renderedHis <- mapM (\(i, prim) -> (i,) <$> render prim) his-- renderedLows <- case lows of- [] -> return []- ls -> do- let remainingPrimary = c^.(contextPrimary br) - (sum $ (^._2.imageL.(to $ imagePrimary br)) <$> renderedHis)- primaryPerLow = remainingPrimary `div` length ls- padFirst = remainingPrimary - (primaryPerLow * length ls)- secondaryPerLow = c^.(contextSecondary br)- primaries = replicate (length ls) primaryPerLow & ix 0 %~ (+ padFirst)-- let renderLow ((i, prim), pri) =- (i,) <$> (render $ limitPrimary br pri- $ limitSecondary br secondaryPerLow- $ cropToContext prim)-- if remainingPrimary > 0 then mapM renderLow (zip ls primaries) else return []-- let rendered = sortBy (compare `DF.on` fst) $ renderedHis ++ renderedLows- allResults = snd <$> rendered- allImages = (^.imageL) <$> allResults- allPrimaries = imagePrimary br <$> allImages- allTranslatedResults = (flip map) (zip [0..] allResults) $ \(i, result) ->- let off = locationFromOffset br offPrimary- offPrimary = sum $ take i allPrimaries- in addResultOffset off result- -- Determine the secondary dimension value to pad to. In a- -- vertical box we want all images to be the same width to- -- avoid attribute over-runs or blank spaces with the wrong- -- attribute. In a horizontal box we want all images to have- -- the same height for the same reason.- maxSecondary = maximum $ imageSecondary br <$> allImages- padImage img = padImageSecondary br (maxSecondary - imageSecondary br img) img (c^.attrL)- paddedImages = padImage <$> allImages-- cropResultToContext $ Result (concatenatePrimary br paddedImages)- (concat $ cursors <$> allTranslatedResults)- (concat $ visibilityRequests <$> allTranslatedResults)---- | Limit the space available to the specified widget to the specified--- number of columns. This is important for constraining the horizontal--- growth of otherwise-greedy widgets. This is non-greedy horizontally--- and defers to the limited widget vertically.-hLimit :: Int -> Widget -> Widget-hLimit w p =- Widget Fixed (vSize p) $ do- withReaderT (& availWidthL .~ w) $ render $ cropToContext p---- | Limit the space available to the specified widget to the specified--- number of rows. This is important for constraining the vertical--- growth of otherwise-greedy widgets. This is non-greedy vertically and--- defers to the limited widget horizontally.-vLimit :: Int -> Widget -> Widget-vLimit h p =- Widget (hSize p) Fixed $ do- withReaderT (& availHeightL .~ h) $ render $ cropToContext p---- | When drawing the specified widget, set the current attribute used--- for drawing to the one with the specified name. Note that the widget--- may use further calls to 'withAttr' to override this; if you really--- want to prevent that, use 'forceAttr'. Attributes used this way still--- get merged hierarchically and still fall back to the attribute map's--- default attribute. If you want to change the default attribute, use--- 'withDefAttr'.-withAttr :: AttrName -> Widget -> Widget-withAttr an p =- Widget (hSize p) (vSize p) $ do- withReaderT (& ctxAttrNameL .~ an) (render p)---- | Update the attribute map while rendering the specified widget: set--- its new default attribute to the one that we get by looking up the--- specified attribute name in the map.-withDefAttr :: AttrName -> Widget -> Widget-withDefAttr an p =- Widget (hSize p) (vSize p) $ do- c <- getContext- withReaderT (& ctxAttrMapL %~ (setDefault (attrMapLookup an (c^.ctxAttrMapL)))) (render p)---- | When rendering the specified widget, update the attribute map with--- the specified transformation.-updateAttrMap :: (AttrMap -> AttrMap) -> Widget -> Widget-updateAttrMap f p =- Widget (hSize p) (vSize p) $ do- withReaderT (& ctxAttrMapL %~ f) (render p)---- | When rendering the specified widget, force all attribute lookups--- in the attribute map to use the value currently assigned to the--- specified attribute name.-forceAttr :: AttrName -> Widget -> Widget-forceAttr an p =- Widget (hSize p) (vSize p) $ do- c <- getContext- withReaderT (& ctxAttrMapL .~ (forceAttrMap (attrMapLookup an (c^.ctxAttrMapL)))) (render p)---- | Build a widget directly from a raw Vty image.-raw :: V.Image -> Widget-raw img = Widget Fixed Fixed $ return $ def & imageL .~ img---- | Translate the specified widget by the specified offset amount.--- Defers to the translated width for growth policy.-translateBy :: Location -> Widget -> Widget-translateBy off p =- Widget (hSize p) (vSize p) $ do- result <- render p- return $ addResultOffset off- $ result & imageL %~ (V.translate (off^.columnL) (off^.rowL))---- | Crop the specified widget on the left by the specified number of--- columns. Defers to the translated width for growth policy.-cropLeftBy :: Int -> Widget -> Widget-cropLeftBy cols p =- Widget (hSize p) (vSize p) $ do- result <- render p- let amt = V.imageWidth (result^.imageL) - cols- cropped img = if amt < 0 then V.emptyImage else V.cropLeft amt img- return $ addResultOffset (Location (-1 * cols, 0))- $ result & imageL %~ cropped---- | Crop the specified widget on the right by the specified number of--- columns. Defers to the translated width for growth policy.-cropRightBy :: Int -> Widget -> Widget-cropRightBy cols p =- Widget (hSize p) (vSize p) $ do- result <- render p- let amt = V.imageWidth (result^.imageL) - cols- cropped img = if amt < 0 then V.emptyImage else V.cropRight amt img- return $ result & imageL %~ cropped---- | Crop the specified widget on the top by the specified number of--- rows. Defers to the translated width for growth policy.-cropTopBy :: Int -> Widget -> Widget-cropTopBy rows p =- Widget (hSize p) (vSize p) $ do- result <- render p- let amt = V.imageHeight (result^.imageL) - rows- cropped img = if amt < 0 then V.emptyImage else V.cropTop amt img- return $ addResultOffset (Location (0, -1 * rows))- $ result & imageL %~ cropped---- | Crop the specified widget on the bottom by the specified number of--- rows. Defers to the translated width for growth policy.-cropBottomBy :: Int -> Widget -> Widget-cropBottomBy rows p =- Widget (hSize p) (vSize p) $ do- result <- render p- let amt = V.imageHeight (result^.imageL) - rows- cropped img = if amt < 0 then V.emptyImage else V.cropBottom amt img- return $ result & imageL %~ cropped---- | When rendering the specified widget, also register a cursor--- positioning request using the specified name and location.-showCursor :: Name -> Location -> Widget -> Widget-showCursor n cloc p =- Widget (hSize p) (vSize p) $ do- result <- render p- return $ result & cursorsL %~ (CursorLocation cloc (Just n):)--hRelease :: Widget -> Maybe Widget-hRelease p =- case hSize p of- Fixed -> Just $ Widget Greedy (vSize p) $ withReaderT (& availWidthL .~ unrestricted) (render p)- Greedy -> Nothing--vRelease :: Widget -> Maybe Widget-vRelease p =- case vSize p of- Fixed -> Just $ Widget (hSize p) Greedy $ withReaderT (& availHeightL .~ unrestricted) (render p)- Greedy -> Nothing---- | Render the specified widget in a named viewport with the--- specified type. This permits widgets to be scrolled without being--- scrolling-aware. To make the most use of viewports, the specified--- widget should use the 'visible' combinator to make a "visibility--- request". This viewport combinator will then translate the resulting--- rendering to make the requested region visible. In addition, the--- 'Brick.Main.EventM' monad provides primitives to scroll viewports--- created by this function if 'visible' is not what you want.------ If a viewport receives more than one visibility request, only the--- first is honored. If a viewport receives more than one scrolling--- request from 'Brick.Main.EventM', all are honored in the order in--- which they are received.-viewport :: Name- -- ^ The name of the viewport (must be unique and stable for- -- reliable behavior)- -> ViewportType- -- ^ The type of viewport (indicates the permitted scrolling- -- direction)- -> Widget- -- ^ The widget to be rendered in the scrollable viewport- -> Widget-viewport vpname typ p =- Widget Greedy Greedy $ do- -- First, update the viewport size.- c <- getContext- let newVp = VP 0 0 newSize- newSize = (c^.availWidthL, c^.availHeightL)- doInsert (Just vp) = Just $ vp & vpSize .~ newSize- doInsert Nothing = Just newVp-- lift $ modify (& viewportMapL %~ (M.alter doInsert vpname))-- -- Then render the sub-rendering with the rendering layout- -- constraint released (but raise an exception if we are asked to- -- render an infinitely-sized widget in the viewport's scrolling- -- dimension)- let Name vpn = vpname- release = case typ of- Vertical -> vRelease- Horizontal -> hRelease- Both -> \w -> vRelease w >>= hRelease- released = case release p of- Just w -> w- Nothing -> case typ of- Vertical -> error $ "tried to embed an infinite-height widget in vertical viewport " <> (show vpn)- Horizontal -> error $ "tried to embed an infinite-width widget in horizontal viewport " <> (show vpn)- Both -> error $ "tried to embed an infinite-width or infinite-height widget in 'Both' type viewport " <> (show vpn)-- initialResult <- render released-- -- If the rendering state includes any scrolling requests for this- -- viewport, apply those- reqs <- lift $ gets $ (^.scrollRequestsL)- let relevantRequests = snd <$> filter (\(n, _) -> n == vpname) reqs- when (not $ null relevantRequests) $ do- Just vp <- lift $ gets $ (^.viewportMapL.to (M.lookup vpname))- let updatedVp = applyRequests relevantRequests vp- applyRequests [] v = v- applyRequests (rq:rqs) v =- case typ of- Horizontal -> scrollTo typ rq (initialResult^.imageL) $ applyRequests rqs v- Vertical -> scrollTo typ rq (initialResult^.imageL) $ applyRequests rqs v- Both -> scrollTo Horizontal rq (initialResult^.imageL) $- scrollTo Vertical rq (initialResult^.imageL) $- applyRequests rqs v- lift $ modify (& viewportMapL %~ (M.insert vpname updatedVp))- return ()-- -- If the sub-rendering requested visibility, update the scroll- -- state accordingly- when (not $ null $ initialResult^.visibilityRequestsL) $ do- Just vp <- lift $ gets $ (^.viewportMapL.to (M.lookup vpname))- let rq = head $ initialResult^.visibilityRequestsL- updatedVp = case typ of- Both -> scrollToView Horizontal rq $ scrollToView Vertical rq vp- Horizontal -> scrollToView typ rq vp- Vertical -> scrollToView typ rq vp- lift $ modify (& viewportMapL %~ (M.insert vpname updatedVp))-- -- If the size of the rendering changes enough to make the- -- viewport offsets invalid, reset them- Just vp <- lift $ gets $ (^.viewportMapL.to (M.lookup vpname))- let img = initialResult^.imageL- fixTop v = if V.imageHeight img < v^.vpSize._2- then v & vpTop .~ 0- else v- fixLeft v = if V.imageWidth img < v^.vpSize._1- then v & vpLeft .~ 0- else v- updateVp = case typ of- Both -> fixLeft . fixTop- Horizontal -> fixLeft- Vertical -> fixTop- lift $ modify (& viewportMapL %~ (M.insert vpname (updateVp vp)))-- -- Get the viewport state now that it has been updated.- Just vpFinal <- lift $ gets (M.lookup vpname . (^.viewportMapL))-- -- Then perform a translation of the sub-rendering to fit into the- -- viewport- translated <- render $ translateBy (Location (-1 * vpFinal^.vpLeft, -1 * vpFinal^.vpTop))- $ Widget Fixed Fixed $ return initialResult-- -- Return the translated result with the visibility requests- -- discarded- let translatedSize = ( translated^.imageL.to V.imageWidth- , translated^.imageL.to V.imageHeight- )- case translatedSize of- (0, 0) -> return $ translated & imageL .~ (V.charFill (c^.attrL) ' ' (c^.availWidthL) (c^.availHeightL))- & visibilityRequestsL .~ mempty- _ -> render $ cropToContext- $ padBottom Max- $ padRight Max- $ Widget Fixed Fixed $ return $ translated & visibilityRequestsL .~ mempty--scrollTo :: ViewportType -> ScrollRequest -> V.Image -> Viewport -> Viewport-scrollTo Both _ _ _ = error "BUG: called scrollTo on viewport type 'Both'"-scrollTo Vertical req img vp = vp & vpTop .~ newVStart- where- newVStart = clamp 0 (V.imageHeight img - vp^.vpSize._2) adjustedAmt- adjustedAmt = case req of- VScrollBy amt -> vp^.vpTop + amt- VScrollPage Up -> vp^.vpTop - vp^.vpSize._2- VScrollPage Down -> vp^.vpTop + vp^.vpSize._2- VScrollToBeginning -> 0- VScrollToEnd -> V.imageHeight img - vp^.vpSize._2- _ -> vp^.vpTop-scrollTo Horizontal req img vp = vp & vpLeft .~ newHStart- where- newHStart = clamp 0 (V.imageWidth img - vp^.vpSize._1) adjustedAmt- adjustedAmt = case req of- HScrollBy amt -> vp^.vpLeft + amt- HScrollPage Up -> vp^.vpLeft - vp^.vpSize._1- HScrollPage Down -> vp^.vpLeft + vp^.vpSize._1- HScrollToBeginning -> 0- HScrollToEnd -> V.imageWidth img - vp^.vpSize._1- _ -> vp^.vpLeft--scrollToView :: ViewportType -> VisibilityRequest -> Viewport -> Viewport-scrollToView Both _ _ = error "BUG: called scrollToView on 'Both' type viewport"-scrollToView Vertical rq vp = vp & vpTop .~ newVStart- where- curStart = vp^.vpTop- curEnd = curStart + vp^.vpSize._2- reqStart = rq^.vrPositionL.rowL-- reqEnd = rq^.vrPositionL.rowL + rq^.vrSizeL._2- newVStart :: Int- newVStart = if reqStart < curStart- then reqStart- else if reqStart > curEnd || reqEnd > curEnd- then reqEnd - vp^.vpSize._2- else curStart-scrollToView Horizontal rq vp = vp & vpLeft .~ newHStart- where- curStart = vp^.vpLeft- curEnd = curStart + vp^.vpSize._1- reqStart = rq^.vrPositionL.columnL-- reqEnd = rq^.vrPositionL.columnL + rq^.vrSizeL._1- newHStart :: Int- newHStart = if reqStart < curStart- then reqStart- else if reqStart > curEnd || reqEnd > curEnd- then reqEnd - vp^.vpSize._1- else curStart---- | Request that the specified widget be made visible when it is--- rendered inside a viewport. This permits widgets (whose sizes and--- positions cannot be known due to being embedded in arbitrary layouts)--- to make a request for a parent viewport to locate them and scroll--- enough to put them in view. This, together with 'viewport', is what--- makes the text editor and list widgets possible without making them--- deal with the details of scrolling state management.------ This does nothing if not rendered in a viewport.-visible :: Widget -> Widget-visible p =- Widget (hSize p) (vSize p) $ do- result <- render p- let imageSize = ( result^.imageL.to V.imageWidth- , result^.imageL.to V.imageHeight- )- -- The size of the image to be made visible in a viewport must have- -- non-zero size in both dimensions.- return $ if imageSize^._1 > 0 && imageSize^._2 > 0- then result & visibilityRequestsL %~ (VR (Location (0, 0)) imageSize :)- else result---- | Similar to 'visible', request that a region (with the specified--- 'Location' as its origin and 'V.DisplayRegion' as its size) be made--- visible when it is rendered inside a viewport. The 'Location' is--- relative to the specified widget's upper-left corner of (0, 0).------ This does nothing if not rendered in a viewport.-visibleRegion :: Location -> V.DisplayRegion -> Widget -> Widget-visibleRegion vrloc sz p =- Widget (hSize p) (vSize p) $ do- result <- render p- -- The size of the image to be made visible in a viewport must have- -- non-zero size in both dimensions.- return $ if sz^._1 > 0 && sz^._2 > 0- then result & visibilityRequestsL %~ (VR vrloc sz :)- else result---- | Horizontal box layout: put the specified widgets next to each other--- in the specified order. Defers growth policies to the growth policies--- of both widgets. This operator is a binary version of 'hBox'.-(<+>) :: Widget- -- ^ Left- -> Widget- -- ^ Right- -> Widget-(<+>) a b = hBox [a, b]---- | Vertical box layout: put the specified widgets one above the other--- in the specified order. Defers growth policies to the growth policies--- of both widgets. This operator is a binary version of 'vBox'.-(<=>) :: Widget- -- ^ Top- -> Widget- -- ^ Bottom- -> Widget-(<=>) a b = vBox [a, b]+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+-- | This module provides the core widget combinators and rendering+-- routines. Everything this library does is in terms of these basic+-- primitives.+module Brick.Widgets.Core+ ( -- * Basic rendering primitives+ TextWidth(..)+ , emptyWidget+ , raw+ , txt+ , txtWrap+ , txtWrapWith+ , str+ , strWrap+ , strWrapWith+ , fill+ , hyperlink++ -- * Padding+ , Padding(..)+ , padLeft+ , padRight+ , padTop+ , padBottom+ , padLeftRight+ , padTopBottom+ , padAll++ -- * Box layout+ , (<=>)+ , (<+>)+ , hBox+ , vBox++ -- * Limits+ , hLimit+ , hLimitPercent+ , vLimit+ , vLimitPercent+ , setAvailableSize++ -- * Attribute management+ , withDefAttr+ , modifyDefAttr+ , withAttr+ , forceAttr+ , forceAttrAllowStyle+ , overrideAttr+ , updateAttrMap++ -- * Border style management+ , withBorderStyle+ , joinBorders+ , separateBorders+ , freezeBorders++ -- * Cursor placement+ , showCursor+ , putCursor++ -- * Naming+ , Named(..)++ -- * Translation and positioning+ , translateBy+ , relativeTo++ -- * Cropping+ , cropLeftBy+ , cropRightBy+ , cropTopBy+ , cropBottomBy+ , cropLeftTo+ , cropRightTo+ , cropTopTo+ , cropBottomTo++ -- * Extent reporting+ , reportExtent+ , clickable++ -- * Scrollable viewports+ , viewport+ , visible+ , visibleRegion+ , unsafeLookupViewport+ , cached++ -- ** Viewport scroll bars+ , withVScrollBars+ , withHScrollBars+ , withClickableHScrollBars+ , withClickableVScrollBars+ , withVScrollBarHandles+ , withHScrollBarHandles+ , withVScrollBarRenderer+ , withHScrollBarRenderer+ , VScrollbarRenderer(..)+ , HScrollbarRenderer(..)+ , verticalScrollbarRenderer+ , horizontalScrollbarRenderer+ , scrollbarAttr+ , scrollbarTroughAttr+ , scrollbarHandleAttr+ , verticalScrollbar+ , horizontalScrollbar++ -- ** Adding offsets to cursor positions and visibility requests+ , addResultOffset++ -- ** Cropping results+ , cropToContext+ )+where++#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif++import Lens.Micro ((^.), (.~), (&), (%~), to, _1, _2, each, to, Lens')+import Lens.Micro.Mtl (use, (%=))+import Control.Monad+import Control.Monad.State.Strict+import Control.Monad.Reader+import qualified Data.Foldable as F+import Data.Traversable (for)+import qualified Data.Text as T+import qualified Data.Map as M+import qualified Data.Set as S+import qualified Data.IMap as I+import qualified Data.Function as DF+import Data.List (sortBy, partition)+import Data.Maybe (fromMaybe)+import qualified Graphics.Vty as V+import Control.DeepSeq++import Text.Wrap (wrapTextToLines, WrapSettings, defaultWrapSettings)++import Brick.Types+import Brick.Types.Internal+import Brick.Widgets.Border.Style+import Brick.Util (clOffset, clamp)+import Brick.AttrMap+import Brick.Widgets.Internal+import qualified Brick.BorderMap as BM++-- | The class of text types that have widths measured in terminal+-- columns. NEVER use 'length' etc. to measure the length of a string if+-- you need to compute how much screen space it will occupy; always use+-- 'textWidth'.+class TextWidth a where+ textWidth :: a -> Int++instance TextWidth T.Text where+ textWidth = V.wcswidth . T.unpack++instance (F.Foldable f) => TextWidth (f Char) where+ textWidth = V.wcswidth . F.toList++-- | The class of types that store interface element names.+class Named a n where+ -- | Get the name of the specified value.+ getName :: a -> n++-- | When rendering the specified widget, use the specified border style+-- for any border rendering.+withBorderStyle :: BorderStyle -> Widget n -> Widget n+withBorderStyle bs p = Widget (hSize p) (vSize p) $+ withReaderT (ctxBorderStyleL .~ bs) (render p)++-- | When rendering the specified widget, create borders that respond+-- dynamically to their neighbors to form seamless connections.+joinBorders :: Widget n -> Widget n+joinBorders p = Widget (hSize p) (vSize p) $+ withReaderT (ctxDynBordersL .~ True) (render p)++-- | When rendering the specified widget, use static borders. This+-- may be marginally faster, but will introduce a small gap between+-- neighboring orthogonal borders.+--+-- This is the default for backwards compatibility.+separateBorders :: Widget n -> Widget n+separateBorders p = Widget (hSize p) (vSize p) $+ withReaderT (ctxDynBordersL .~ False) (render p)++-- | After the specified widget has been rendered, freeze its borders. A+-- frozen border will not be affected by neighbors, nor will it affect+-- neighbors. Compared to 'separateBorders', 'freezeBorders' will not+-- affect whether borders connect internally to a widget (whereas+-- 'separateBorders' prevents them from connecting).+--+-- Frozen borders cannot be thawed.+freezeBorders :: Widget n -> Widget n+freezeBorders p = Widget (hSize p) (vSize p) $ (bordersL %~ BM.clear) <$> render p++-- | The empty widget.+emptyWidget :: Widget n+emptyWidget = raw V.emptyImage++-- | Add an offset to all cursor locations, visibility requests, and+-- extents in the specified rendering result. This function is critical+-- for maintaining correctness in the rendering results as they are+-- processed successively by box layouts and other wrapping combinators,+-- since calls to this function result in converting from widget-local+-- coordinates to (ultimately) terminal-global ones so they can be+-- used by other combinators. You should call this any time you render+-- something and then translate it or otherwise offset it from its+-- original origin.+addResultOffset :: Location -> Result n -> Result n+addResultOffset off = addCursorOffset off .+ addVisibilityOffset off .+ addExtentOffset off .+ addDynBorderOffset off++addVisibilityOffset :: Location -> Result n -> Result n+addVisibilityOffset off r = r & visibilityRequestsL.each.vrPositionL %~ (off <>)++addExtentOffset :: Location -> Result n -> Result n+addExtentOffset off r = r & extentsL.each %~ (\(Extent n l sz) -> Extent n (off <> l) sz)++addDynBorderOffset :: Location -> Result n -> Result n+addDynBorderOffset off r = r & bordersL %~ BM.translate off++-- | Render the specified widget and record its rendering extent using+-- the specified name (see also 'lookupExtent').+--+-- This function is the counterpart to 'makeVisible'; any visibility+-- requests made with 'makeVisible' must have a corresponding+-- 'reportExtent' in order to work. The 'clickable' function will also+-- work for this purpose to tell the renderer about the clickable+-- region.+reportExtent :: (Ord n) => n -> Widget n -> Widget n+reportExtent n p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let ext = Extent n (Location (0, 0)) sz+ sz = ( result^.imageL.to V.imageWidth+ , result^.imageL.to V.imageHeight+ )+ -- If the reported extent also has a visibility request+ -- from EventM via makeVisible, add a visibility request to+ -- the render state so this gets scrolled into view by any+ -- containing viewport.+ vReqs <- use requestedVisibleNames_L+ let addVisReq = if sz^._1 > 0 && sz^._2 > 0 && n `S.member` vReqs+ then visibilityRequestsL %~ (VR (Location (0, 0)) sz :)+ else id+ return $ addVisReq $ result & extentsL %~ (ext:)++-- | Request mouse click events on the specified widget.+--+-- Regions used with 'clickable' can be scrolled into view with+-- 'makeVisible'.+clickable :: (Ord n) => n -> Widget n -> Widget n+clickable n p =+ Widget (hSize p) (vSize p) $ do+ clickableNamesL %= (n:)+ render $ reportExtent n p++addCursorOffset :: Location -> Result n -> Result n+addCursorOffset off r =+ let onlyVisible = filter isVisible+ isVisible l = l^.locationColumnL >= 0 && l^.locationRowL >= 0+ in r & cursorsL %~ (\cs -> onlyVisible $ (`clOffset` off) <$> cs)++unrestricted :: Int+unrestricted = 100000++-- | Make a widget from a string, but wrap the words in the input's+-- lines at the available width using the default wrapping settings. The+-- input string should not contain escape sequences or carriage returns.+--+-- Unlike 'str', this is greedy horizontally.+strWrap :: String -> Widget n+strWrap = strWrapWith defaultWrapSettings++-- | Make a widget from a string, but wrap the words in the input's+-- lines at the available width using the specified wrapping settings.+-- The input string should not contain escape sequences or carriage+-- returns.+--+-- Unlike 'str', this is greedy horizontally.+strWrapWith :: WrapSettings -> String -> Widget n+strWrapWith settings t = txtWrapWith settings $ T.pack t++-- | Make a widget from text, but wrap the words in the input's lines at+-- the available width using the default wrapping settings. The input+-- text should not contain escape sequences or carriage returns.+--+-- Unlike 'txt', this is greedy horizontally.+txtWrap :: T.Text -> Widget n+txtWrap = txtWrapWith defaultWrapSettings++-- | Make a widget from text, but wrap the words in the input's lines at+-- the available width using the specified wrapping settings. The input+-- text should not contain escape sequences or carriage returns.+--+-- Unlike 'txt', this is greedy horizontally.+txtWrapWith :: WrapSettings -> T.Text -> Widget n+txtWrapWith settings s =+ Widget Greedy Fixed $ do+ c <- getContext+ let theLines = fixEmpty <$> wrapTextToLines settings (c^.availWidthL) s+ fixEmpty l | T.null l = " "+ | otherwise = l+ case force theLines of+ [] -> return emptyResult+ multiple ->+ let maxLength = maximum $ textWidth <$> multiple+ padding = V.charFill (c^.attrL) ' ' (c^.availWidthL - maxLength) (length lineImgs)+ lineImgs = lineImg <$> multiple+ lineImg lStr = V.text' (c^.attrL)+ (lStr <> T.replicate (maxLength - textWidth lStr) " ")+ in return $ emptyResult & imageL .~ (V.horizCat [V.vertCat lineImgs, padding])++-- | Build a widget from a 'String'. Behaves the same as 'txt' when the+-- input contains multiple lines.+--+-- The input string must not contain tab characters. If it does,+-- interface corruption will result since the terminal will likely+-- render it as taking up more than a single column. The caller should+-- replace tabs with the appropriate number of spaces as desired. The+-- input string should not contain escape sequences or carriage returns.+str :: String -> Widget n+str = txt . T.pack++-- | Build a widget from a 'T.Text' value. Breaks newlines up and+-- space-pads short lines out to the length of the longest line.+--+-- The input string must not contain tab characters. If it does,+-- interface corruption will result since the terminal will likely+-- render it as taking up more than a single column. The caller should+-- replace tabs with the appropriate number of spaces as desired. The+-- input text should not contain escape sequences or carriage returns.+txt :: T.Text -> Widget n+txt s =+ -- Although vty Image uses lazy Text internally, using lazy text at this+ -- level may not be an improvement. Indeed it can be much worse, due+ -- the overhead of lazy Text being significant compared to the typically+ -- short string content used to compose UIs.+ Widget Fixed Fixed $ do+ c <- getContext+ let theLines = fixEmpty <$> (dropUnused . T.lines) s+ fixEmpty l = if T.null l then T.singleton ' ' else l+ dropUnused l = takeColumnsT (availWidth c) <$> take (availHeight c) l+ pure $ case theLines of+ [] -> emptyResult+ [one] -> emptyResult & imageL .~ (V.text' (c^.attrL) one)+ multiple ->+ let maxLength = maximum $ V.safeWctwidth <$> multiple+ lineImgs = lineImg <$> multiple+ lineImg lStr = V.text' (c^.attrL)+ (lStr <> T.replicate (maxLength - V.safeWctwidth lStr) (T.singleton ' '))+ in emptyResult & imageL .~ (V.vertCat lineImgs)++-- | Take up to the given width, having regard to character width.+takeColumnsT :: Int -> T.Text -> T.Text+takeColumnsT w s = T.take (fst $ T.foldl' f (0,0) s) s+ where+ -- The accumulator value is (index in Text value, width of Text so far)+ f (i,z) c+ -- Width was previously exceeded; continue with same values.+ | z < 0 = (i, z)+ -- Width exceeded. Signal this with z = -1. Index will no longer be+ -- incremented.+ --+ -- Why not short circuit (e.g. using foldlM construction)?+ -- Because in the typical case, the Either allocation costs exceed+ -- any benefits. The pathological case, string length >> width, is+ -- probably rare.+ | z + V.safeWcwidth c > w = (i, -1)+ -- Width not yet exceeded. Increment index and add character width.+ | otherwise = (i + 1, z + V.safeWcwidth c)++-- | Hyperlink the given widget to the specified URL. Not all terminal+-- emulators support this. In those that don't, this should have no+-- discernible effect.+hyperlink :: T.Text -> Widget n -> Widget n+hyperlink url p =+ Widget (hSize p) (vSize p) $ do+ c <- getContext+ let attr = (c^.attrL) `V.withURL` url+ withReaderT (ctxAttrMapL %~ setDefaultAttr attr) (render p)++-- | The type of padding.+data Padding = Pad Int+ -- ^ Pad by the specified number of rows or columns.+ | Max+ -- ^ Pad up to the number of available rows or columns.++-- | Pad the specified widget on the left. If max padding is used, this+-- grows greedily horizontally; otherwise it defers to the padded+-- widget.+padLeft :: Padding -> Widget n -> Widget n+padLeft padding p =+ let (f, sz) = case padding of+ Max -> (id, Greedy)+ Pad i -> (hLimit i, hSize p)+ in Widget sz (vSize p) $ do+ c <- getContext+ let lim = case padding of+ Max -> c^.availWidthL+ Pad i -> c^.availWidthL - i+ result <- render $ hLimit lim p+ render $ (f $ vLimit (result^.imageL.to V.imageHeight) $ fill ' ') <+>+ (Widget Fixed Fixed $ return result)++-- | Pad the specified widget on the right. If max padding is used,+-- this grows greedily horizontally; otherwise it defers to the padded+-- widget.+padRight :: Padding -> Widget n -> Widget n+padRight padding p =+ let (f, sz) = case padding of+ Max -> (id, Greedy)+ Pad i -> (hLimit i, hSize p)+ in Widget sz (vSize p) $ do+ c <- getContext+ let lim = case padding of+ Max -> c^.availWidthL+ Pad i -> c^.availWidthL - i+ result <- render $ hLimit lim p+ render $ (Widget Fixed Fixed $ return result) <+>+ (f $ vLimit (result^.imageL.to V.imageHeight) $ fill ' ')++-- | Pad the specified widget on the top. If max padding is used, this+-- grows greedily vertically; otherwise it defers to the padded widget.+padTop :: Padding -> Widget n -> Widget n+padTop padding p =+ let (f, sz) = case padding of+ Max -> (id, Greedy)+ Pad i -> (vLimit i, vSize p)+ in Widget (hSize p) sz $ do+ c <- getContext+ let lim = case padding of+ Max -> c^.availHeightL+ Pad i -> c^.availHeightL - i+ result <- render $ vLimit lim p+ render $ (f $ hLimit (result^.imageL.to V.imageWidth) $ fill ' ') <=>+ (Widget Fixed Fixed $ return result)++-- | Pad the specified widget on the bottom. If max padding is used,+-- this grows greedily vertically; otherwise it defers to the padded+-- widget.+padBottom :: Padding -> Widget n -> Widget n+padBottom padding p =+ let (f, sz) = case padding of+ Max -> (id, Greedy)+ Pad i -> (vLimit i, vSize p)+ in Widget (hSize p) sz $ do+ c <- getContext+ let lim = case padding of+ Max -> c^.availHeightL+ Pad i -> c^.availHeightL - i+ result <- render $ vLimit lim p+ render $ (Widget Fixed Fixed $ return result) <=>+ (f $ hLimit (result^.imageL.to V.imageWidth) $ fill ' ')++-- | Pad a widget on the left and right. Defers to the padded widget for+-- growth policy.+padLeftRight :: Int -> Widget n -> Widget n+padLeftRight c w = padLeft (Pad c) $ padRight (Pad c) w++-- | Pad a widget on the top and bottom. Defers to the padded widget for+-- growth policy.+padTopBottom :: Int -> Widget n -> Widget n+padTopBottom r w = padTop (Pad r) $ padBottom (Pad r) w++-- | Pad a widget on all sides. Defers to the padded widget for growth+-- policy.+padAll :: Int -> Widget n -> Widget n+padAll v w = padLeftRight v $ padTopBottom v w++-- | Fill all available space with the specified character. Grows both+-- horizontally and vertically.+fill :: Char -> Widget n+fill ch =+ Widget Greedy Greedy $ do+ c <- getContext+ return $ emptyResult & imageL .~ (V.charFill (c^.attrL) ch (c^.availWidthL) (c^.availHeightL))++-- | Vertical box layout: put the specified widgets one above the other+-- in the specified order (uppermost first). Defers growth policies to+-- the growth policies of the contained widgets (if any are greedy, so+-- is the box).+--+-- Allocates space to 'Fixed' elements first and 'Greedy' elements+-- second. For example, if a 'vBox' contains three elements @A@, @B@,+-- and @C@, and if @A@ and @B@ are 'Fixed', then 'vBox' first renders+-- @A@ and @B@. Suppose those two take up 10 rows total, and the 'vBox'+-- was given 50 rows. This means 'vBox' then allocates the remaining+-- 40 rows to @C@. If, on the other hand, @A@ and @B@ take up 50 rows+-- together, @C@ will not be rendered at all.+--+-- If all elements are 'Greedy', 'vBox' allocates the available height+-- evenly among the elements. So, for example, if a 'vBox' is rendered+-- in 90 rows and has three 'Greedy' elements, each element will be+-- allocated 30 rows.+{-# NOINLINE vBox #-}+vBox :: [Widget n] -> Widget n+vBox [] = emptyWidget+vBox [a] = a+vBox pairs = renderBox vBoxRenderer pairs++-- | Horizontal box layout: put the specified widgets next to each other+-- in the specified order (leftmost first). Defers growth policies to+-- the growth policies of the contained widgets (if any are greedy, so+-- is the box).+--+-- Allocates space to 'Fixed' elements first and 'Greedy' elements+-- second. For example, if an 'hBox' contains three elements @A@, @B@,+-- and @C@, and if @A@ and @B@ are 'Fixed', then 'hBox' first renders+-- @A@ and @B@. Suppose those two take up 10 columns total, and the+-- 'hBox' was given 50 columns. This means 'hBox' then allocates the+-- remaining 40 columns to @C@. If, on the other hand, @A@ and @B@ take+-- up 50 columns together, @C@ will not be rendered at all.+--+-- If all elements are 'Greedy', 'hBox' allocates the available width+-- evenly among the elements. So, for example, if an 'hBox' is rendered+-- in 90 columns and has three 'Greedy' elements, each element will be+-- allocated 30 columns.+{-# NOINLINE hBox #-}+hBox :: [Widget n] -> Widget n+hBox [] = emptyWidget+hBox [a] = a+hBox pairs = renderBox hBoxRenderer pairs++-- | The process of rendering widgets in a box layout is exactly the+-- same except for the dimension under consideration (width vs. height),+-- in which case all of the same operations that consider one dimension+-- in the layout algorithm need to be switched to consider the other.+-- Because of this we fill a BoxRenderer with all of the functions+-- needed to consider the "primary" dimension (e.g. vertical if the+-- box layout is vertical) as well as the "secondary" dimension (e.g.+-- horizontal if the box layout is vertical). Doing this permits us to+-- have one implementation for box layout and parameterizing on the+-- orientation of all of the operations.+data BoxRenderer n =+ BoxRenderer { contextPrimary :: Lens' (Context n) Int+ , contextSecondary :: Lens' (Context n) Int+ , imagePrimary :: V.Image -> Int+ , imageSecondary :: V.Image -> Int+ , limitPrimary :: Int -> Widget n -> Widget n+ , primaryWidgetSize :: Widget n -> Size+ , concatenatePrimary :: [V.Image] -> V.Image+ , concatenateSecondary :: [V.Image] -> V.Image+ , locationFromOffset :: Int -> Location+ , padImageSecondary :: Int -> V.Image -> V.Attr -> V.Image+ , loPrimary :: forall a. Lens' (Edges a) a -- lo: towards smaller coordinates in that dimension+ , hiPrimary :: forall a. Lens' (Edges a) a -- hi: towards larger coordinates in that dimension+ , loSecondary :: forall a. Lens' (Edges a) a+ , hiSecondary :: forall a. Lens' (Edges a) a+ , locationFromPrimarySecondary :: Int -> Int -> Location+ , splitLoPrimary :: Int -> V.Image -> V.Image+ , splitHiPrimary :: Int -> V.Image -> V.Image+ , splitLoSecondary :: Int -> V.Image -> V.Image+ , splitHiSecondary :: Int -> V.Image -> V.Image+ , lookupPrimary :: Int -> BM.BorderMap DynBorder -> I.IMap DynBorder+ , insertSecondary :: Location -> I.Run DynBorder -> BM.BorderMap DynBorder -> BM.BorderMap DynBorder+ }++vBoxRenderer :: BoxRenderer n+vBoxRenderer =+ BoxRenderer { contextPrimary = availHeightL+ , contextSecondary = availWidthL+ , imagePrimary = V.imageHeight+ , imageSecondary = V.imageWidth+ , limitPrimary = vLimit+ , primaryWidgetSize = vSize+ , concatenatePrimary = V.vertCat+ , concatenateSecondary = V.horizCat+ , locationFromOffset = Location . (0 ,)+ , padImageSecondary = \amt img a ->+ let p = V.charFill a ' ' amt (V.imageHeight img)+ in V.horizCat [img, p]+ , loPrimary = eTopL+ , hiPrimary = eBottomL+ , loSecondary = eLeftL+ , hiSecondary = eRightL+ , locationFromPrimarySecondary = \r c -> Location (c, r)+ , splitLoPrimary = V.cropBottom+ , splitHiPrimary = \n img -> V.cropTop (V.imageHeight img-n) img+ , splitLoSecondary = V.cropRight+ , splitHiSecondary = \n img -> V.cropLeft (V.imageWidth img-n) img+ , lookupPrimary = BM.lookupRow+ , insertSecondary = BM.insertH+ }++hBoxRenderer :: BoxRenderer n+hBoxRenderer =+ BoxRenderer { contextPrimary = availWidthL+ , contextSecondary = availHeightL+ , imagePrimary = V.imageWidth+ , imageSecondary = V.imageHeight+ , limitPrimary = hLimit+ , primaryWidgetSize = hSize+ , concatenatePrimary = V.horizCat+ , concatenateSecondary = V.vertCat+ , locationFromOffset = Location . (, 0)+ , padImageSecondary = \amt img a ->+ let p = V.charFill a ' ' (V.imageWidth img) amt+ in V.vertCat [img, p]+ , loPrimary = eLeftL+ , hiPrimary = eRightL+ , loSecondary = eTopL+ , hiSecondary = eBottomL+ , locationFromPrimarySecondary = \c r -> Location (c, r)+ , splitLoPrimary = V.cropRight+ , splitHiPrimary = \n img -> V.cropLeft (V.imageWidth img-n) img+ , splitLoSecondary = V.cropBottom+ , splitHiSecondary = \n img -> V.cropTop (V.imageHeight img-n) img+ , lookupPrimary = BM.lookupCol+ , insertSecondary = BM.insertV+ }++-- | Render a series of widgets in a box layout in the order given.+--+-- The growth policy of a box layout is the most unrestricted of the+-- growth policies of the widgets it contains, so to determine the hSize+-- and vSize of the box we just take the maximum (using the Ord instance+-- for Size) of all of the widgets to be rendered in the box.+--+-- Then the box layout algorithm proceeds as follows. We'll use+-- the vertical case to concretely describe the algorithm, but the+-- horizontal case can be envisioned just by exchanging all+-- "vertical"/"horizontal" and "rows"/"columns", etc., in the+-- description.+--+-- The growth policies of the child widgets determine the order in which+-- they are rendered, i.e., the order in which space in the box is+-- allocated to widgets as the algorithm proceeds. This is because order+-- matters: if we render greedy widgets first, there will be no space+-- left for non-greedy ones.+--+-- So we render all widgets with size 'Fixed' in the vertical dimension+-- first. Each is rendered with as much room as the overall box has, but+-- we assume that they will not be greedy and use it all. If they do,+-- maybe it's because the terminal is small and there just isn't enough+-- room to render everything.+--+-- Then the remaining height is distributed evenly amongst all remaining+-- (greedy) widgets and they are rendered in sub-boxes that are as high+-- as this even slice of rows and as wide as the box is permitted to be.+-- We only do this step at all if rendering the non-greedy widgets left+-- us any space, i.e., if there were any rows left.+--+-- After rendering the non-greedy and then greedy widgets, their images+-- are sorted so that they are stored in the order the original widgets+-- were given. All cursor locations and visibility requests in each+-- sub-widget are translated according to the position of the sub-widget+-- in the box.+--+-- All images are padded to be as wide as the widest sub-widget to+-- prevent attribute over-runs. Without this step the attribute used by+-- a sub-widget may continue on in an undesirable fashion until it hits+-- something with a different attribute. To prevent this and to behave+-- in the least surprising way, we pad the image on the right with+-- whitespace using the context's current attribute.+--+-- Finally, the padded images are concatenated together vertically and+-- returned along with the translated cursor positions and visibility+-- requests.+renderBox :: BoxRenderer n -> [Widget n] -> Widget n+renderBox br ws =+ Widget (maximum $ hSize <$> ws) (maximum $ vSize <$> ws) $ do+ c <- getContext++ let pairsIndexed = zip [(0::Int)..] ws+ (his, lows) = partition (\p -> (primaryWidgetSize br $ snd p) == Fixed)+ pairsIndexed++ renderHi prim = do+ remainingPrimary <- get+ result <- lift $ render $ limitPrimary br remainingPrimary prim+ result <$ (put $! remainingPrimary - (result^.imageL.(to $ imagePrimary br)))++ (renderedHis, remainingPrimary) <-+ runStateT (traverse (traverse renderHi) his) (c ^. contextPrimary br)++ renderedLows <- case lows of+ [] -> return []+ ls -> do+ let primaryPerLow = remainingPrimary `div` length ls+ rest = remainingPrimary - (primaryPerLow * length ls)+ primaries = replicate rest (primaryPerLow + 1) <>+ replicate (length ls - rest) primaryPerLow++ let renderLow ((i, prim), pri) = (i,) <$> render (limitPrimary br pri prim)++ if remainingPrimary > 0 then mapM renderLow (zip ls primaries) else return []++ let rendered = sortBy (compare `DF.on` fst) $ renderedHis ++ renderedLows+ allResults = snd <$> rendered+ allImages = (^.imageL) <$> allResults+ allTranslatedResults = flip evalState 0 $ for allResults $ \result -> do+ offPrimary <- get+ put $ offPrimary + (result ^. imageL . to (imagePrimary br))+ pure $ addResultOffset (locationFromOffset br offPrimary) result+ -- Determine the secondary dimension value to pad to. In a+ -- vertical box we want all images to be the same width to+ -- avoid attribute over-runs or blank spaces with the wrong+ -- attribute. In a horizontal box we want all images to have+ -- the same height for the same reason.+ maxSecondary = maximum $ imageSecondary br <$> allImages+ padImage img = padImageSecondary br (maxSecondary - imageSecondary br img)+ img (c^.attrL)+ (imageRewrites, newBorders) = catAllBorders br (borders <$> allTranslatedResults)+ rewrittenImages = zipWith (rewriteImage br) imageRewrites allImages+ paddedImages = padImage <$> rewrittenImages++ cropResultToContext $ Result (concatenatePrimary br paddedImages)+ (concatMap cursors allTranslatedResults)+ (concatMap visibilityRequests allTranslatedResults)+ (concatMap extents allTranslatedResults)+ newBorders++catDynBorder :: Lens' (Edges BorderSegment) BorderSegment+ -> Lens' (Edges BorderSegment) BorderSegment+ -> DynBorder+ -> DynBorder+ -> Maybe DynBorder+catDynBorder towardsA towardsB a b+ -- Currently, we check if the 'BorderStyle's are exactly the same. In the+ -- future, it might be nice to relax this restriction. For example, if a+ -- horizontal border is being rewritten to accommodate a neighboring+ -- vertical border, all we care about is that the two 'bsVertical's line up+ -- sanely. After all, if the horizontal border's 'bsVertical' is the same+ -- as the vertical one's, and the horizontal border's 'BorderStyle' is+ -- self-consistent, then it will look "right" to rewrite according to the+ -- horizontal border's 'BorderStyle'.+ | dbStyle a == dbStyle b+ && dbAttr a == dbAttr b+ && a ^. dbSegmentsL.towardsB.bsAcceptL+ && b ^. dbSegmentsL.towardsA.bsOfferL+ && not (a ^. dbSegmentsL.towardsB.bsDrawL) -- don't bother doing an update if we don't need to+ = Just (a & dbSegmentsL.towardsB.bsDrawL .~ True)+ | otherwise = Nothing++catDynBorders :: Lens' (Edges BorderSegment) BorderSegment+ -> Lens' (Edges BorderSegment) BorderSegment+ -> I.IMap DynBorder+ -> I.IMap DynBorder+ -> I.IMap DynBorder+catDynBorders towardsA towardsB am bm = I.mapMaybe id+ $ I.intersectionWith (catDynBorder towardsA towardsB) am bm++-- | Given borders that should be placed next to each other (the first argument+-- on the right or bottom, and the second argument on the left or top), compute+-- new borders and the rewrites that should be done along the edges of the two+-- images to keep the image in sync with the border information.+--+-- The input borders are assumed to be disjoint. This property is not checked.+catBorders :: (border ~ BM.BorderMap DynBorder, rewrite ~ I.IMap V.Image)+ => BoxRenderer n -> border -> border -> ((rewrite, rewrite), border)+catBorders br r l = if lCoord + 1 == rCoord+ then ((lRe, rRe), lr')+ else ((I.empty, I.empty), lr)+ where+ lr = BM.expand (BM.coordinates r) l `BM.unsafeUnion`+ BM.expand (BM.coordinates l) r+ lr' = id+ . mergeIMap lCoord lIMap'+ . mergeIMap rCoord rIMap'+ $ lr+ lCoord = BM.coordinates l ^. hiPrimary br+ rCoord = BM.coordinates r ^. loPrimary br+ lIMap = lookupPrimary br lCoord l+ rIMap = lookupPrimary br rCoord r+ lIMap' = catDynBorders (loPrimary br) (hiPrimary br) lIMap rIMap+ rIMap' = catDynBorders (hiPrimary br) (loPrimary br) rIMap lIMap+ lRe = renderDynBorder <$> lIMap'+ rRe = renderDynBorder <$> rIMap'+ mergeIMap p imap bm = F.foldl'+ (\bm' (s,v) -> insertSecondary br (locationFromPrimarySecondary br p s) v bm')+ bm+ (I.unsafeToAscList imap)++-- | Given a direction to concatenate borders in, and the border information+-- itself (which list is assumed to be already shifted so that borders do not+-- overlap and are strictly increasing in the primary direction), produce: a+-- list of rewrites for the lo and hi directions of each border, respectively,+-- and the borders describing the fully concatenated object.+catAllBorders :: BoxRenderer n+ -> [BM.BorderMap DynBorder]+ -> ([(I.IMap V.Image, I.IMap V.Image)], BM.BorderMap DynBorder)+catAllBorders _ [] = ([], BM.empty)+catAllBorders br (bm:bms) = (zip ([I.empty]++los) (his++[I.empty]), bm') where+ (rewrites, bm') = runState (traverse (state . catBorders br) bms) bm+ (his, los) = unzip rewrites++rewriteEdge :: (Int -> V.Image -> V.Image)+ -> (Int -> V.Image -> V.Image)+ -> ([V.Image] -> V.Image)+ -> I.IMap V.Image+ -> V.Image+ -> V.Image+rewriteEdge splitLo splitHi combine = (combine .) . go . offsets 0 . I.unsafeToAscList where++ -- convert absolute positions into relative ones+ offsets _ [] = []+ offsets n ((n', r):nrs) = (n'-n, r) : offsets (n'+I.len r) nrs++ go [] old = [old]+ -- TODO: might be nice to construct this image with fill rather than+ -- replicate+char+ go ((lo, I.Run len new):nrs) old+ = [splitLo lo old]+ ++ replicate len new+ ++ go nrs (splitHi (lo+len) old)++rewriteImage :: BoxRenderer n -> (I.IMap V.Image, I.IMap V.Image) -> V.Image -> V.Image+rewriteImage br (loRewrite, hiRewrite) old = rewriteHi . rewriteLo $ old where+ size = imagePrimary br old+ go = rewriteEdge (splitLoSecondary br) (splitHiSecondary br) (concatenateSecondary br)+ rewriteLo img+ | I.null loRewrite || size == 0 = img+ | otherwise = concatenatePrimary br+ [ go loRewrite (splitLoPrimary br 1 img)+ , splitHiPrimary br 1 img+ ]+ rewriteHi img+ | I.null hiRewrite || size == 0 = img+ | otherwise = concatenatePrimary br+ [ splitLoPrimary br (size-1) img+ , go hiRewrite (splitHiPrimary br (size-1) img)+ ]++-- | Limit the space available to the specified widget to the specified+-- number of columns. This is important for constraining the horizontal+-- growth of otherwise-greedy widgets. This is non-greedy horizontally+-- and defers to the limited widget vertically.+hLimit :: Int -> Widget n -> Widget n+hLimit w p+ | w <= 0 = emptyWidget+ | otherwise =+ Widget Fixed (vSize p) $+ withReaderT (availWidthL %~ (min w)) $ render $ cropToContext p++-- | Limit the space available to the specified widget to the specified+-- percentage of available width, as a value between 0 and 100+-- inclusive. Values outside the valid range will be clamped to the+-- range endpoints. This is important for constraining the horizontal+-- growth of otherwise-greedy widgets. This is non-greedy horizontally+-- and defers to the limited widget vertically.+hLimitPercent :: Int -> Widget n -> Widget n+hLimitPercent w' p+ | w' <= 0 = emptyWidget+ | otherwise =+ Widget Fixed (vSize p) $ do+ let w = clamp 0 100 w'+ ctx <- getContext+ let usableWidth = ctx^.availWidthL+ widgetWidth = round (toRational usableWidth * (toRational w / 100))+ withReaderT (availWidthL %~ (min widgetWidth)) $ render $ cropToContext p++-- | Limit the space available to the specified widget to the specified+-- number of rows. This is important for constraining the vertical+-- growth of otherwise-greedy widgets. This is non-greedy vertically and+-- defers to the limited widget horizontally.+vLimit :: Int -> Widget n -> Widget n+vLimit h p+ | h <= 0 = emptyWidget+ | otherwise =+ Widget (hSize p) Fixed $+ withReaderT (availHeightL %~ (min h)) $ render $ cropToContext p++-- | Limit the space available to the specified widget to the specified+-- percentage of available height, as a value between 0 and 100+-- inclusive. Values outside the valid range will be clamped to the+-- range endpoints. This is important for constraining the vertical+-- growth of otherwise-greedy widgets. This is non-greedy vertically and+-- defers to the limited widget horizontally.+vLimitPercent :: Int -> Widget n -> Widget n+vLimitPercent h' p+ | h' <= 0 = emptyWidget+ | otherwise =+ Widget (hSize p) Fixed $ do+ let h = clamp 0 100 h'+ ctx <- getContext+ let usableHeight = ctx^.availHeightL+ widgetHeight = round (toRational usableHeight * (toRational h / 100))+ withReaderT (availHeightL %~ (min widgetHeight)) $ render $ cropToContext p++-- | Set the rendering context height and width for this widget. This+-- is useful for relaxing the rendering size constraints on e.g. layer+-- widgets where cropping to the screen size is undesirable.+setAvailableSize :: (Int, Int) -> Widget n -> Widget n+setAvailableSize (w, h) p+ | w <= 0 || h <= 0 = emptyWidget+ | otherwise =+ Widget Fixed Fixed $+ withReaderT (\c -> c & availHeightL .~ h & availWidthL .~ w) $+ render $ cropToContext p++-- | When drawing the specified widget, set the attribute used for+-- drawing to the one with the specified name. Note that the widget may+-- make further changes to the active drawing attribute, so this only+-- takes effect if nothing in the specified widget invokes 'withAttr'+-- or otherwise changes the rendering context's attribute setup. If you+-- want to prevent that, use 'forceAttr'. Attributes used this way still+-- get merged hierarchically and still fall back to the attribute map's+-- default attribute. If you want to change the default attribute, use+-- 'withDefAttr'.+--+-- For example:+--+-- @+-- appAttrMap = attrMap (white `on` blue) [ ("highlight", fg yellow)+-- , ("warning", bg magenta)+-- ]+--+-- renderA :: (String, String) -> [Widget n]+-- renderA (a,b) = hBox [ str a+-- , str " is "+-- , withAttr "highlight" (str b)+-- ]+--+-- render1 = renderA (\"Brick\", "fun")+-- render2 = withAttr "warning" render1+-- @+--+-- In the example above, @render1@ will show @Brick is fun@ where the+-- first two words are white on a blue background and the last word+-- is yellow on a blue background. However, @render2@ will show the+-- first two words in white on magenta although the last word is still+-- rendered in yellow on blue.+withAttr :: AttrName -> Widget n -> Widget n+withAttr an p =+ Widget (hSize p) (vSize p) $+ withReaderT (ctxAttrNameL .~ an) (render p)++-- | Update the attribute map while rendering the specified widget: set+-- the map's default attribute to the one that we get by applying the+-- specified function to the current map's default attribute. This is a+-- variant of 'withDefAttr'; see the latter for more information.+modifyDefAttr :: (V.Attr -> V.Attr) -> Widget n -> Widget n+modifyDefAttr f p =+ Widget (hSize p) (vSize p) $ do+ c <- getContext+ withReaderT (ctxAttrMapL %~ (setDefaultAttr (f $ getDefaultAttr (c^.ctxAttrMapL)))) (render p)++-- | Update the attribute map used while rendering the specified+-- widget (and any sub-widgets): set its new *default* attribute+-- (i.e. the attribute components that will be applied if not+-- overridden by any more specific attributes) to the one that we get+-- by looking up the specified attribute name in the map.+--+-- For example:+--+-- @+-- ...+-- appAttrMap = attrMap (white `on` blue) [ ("highlight", fg yellow)+-- , ("warning", bg magenta)+-- , ("good", white `on` green) ]+-- ...+--+-- renderA :: (String, String) -> [Widget n]+-- renderA (a,b) = hBox [ withAttr "good" (str a)+-- , str " is "+-- , withAttr "highlight" (str b) ]+--+-- render1 = renderA (\"Brick\", "fun")+-- render2 = withDefAttr "warning" render1+-- @+--+-- In the above, render1 will show "Brick is fun" where the first word+-- is white on a green background, the middle word is white on a blue+-- background, and the last word is yellow on a blue background.+-- However, render2 will show the first word in the same colors but+-- the middle word will be shown in whatever the terminal's normal+-- foreground is on a magenta background, and the third word will be+-- yellow on a magenta background.+withDefAttr :: AttrName -> Widget n -> Widget n+withDefAttr an p =+ Widget (hSize p) (vSize p) $ do+ c <- getContext+ withReaderT (ctxAttrMapL %~ (setDefaultAttr (attrMapLookup an (c^.ctxAttrMapL)))) (render p)++-- | While rendering the specified widget, use a transformed version+-- of the current attribute map. This is a very general function with+-- broad capabilities: you probably want a more specific function such+-- as 'withDefAttr' or 'withAttr'.+updateAttrMap :: (AttrMap -> AttrMap) -> Widget n -> Widget n+updateAttrMap f p =+ Widget (hSize p) (vSize p) $+ withReaderT (ctxAttrMapL %~ f) (render p)++-- | When rendering the specified widget, force all attribute lookups+-- in the attribute map to use the value currently assigned to the+-- specified attribute name. This means that the attribute lookups will+-- behave as if they all used the name specified here. That further+-- means that the resolved attribute will still inherit from its parent+-- entry in the attribute map as would normally be the case. If you+-- want to have more control over the resulting attribute, consider+-- 'modifyDefAttr'.+--+-- For example:+--+-- @+-- ...+-- appAttrMap = attrMap (white `on` blue) [ ("highlight", fg yellow)+-- , ("notice", fg red) ]+-- ...+--+-- renderA :: (String, String) -> [Widget n]+-- renderA (a,b) = hBox [ withAttr "highlight" (str a)+-- , str " is "+-- , withAttr "highlight" (str b)+-- ]+--+-- render1 = renderA ("Brick", "fun")+-- render2 = forceAttr "notice" render1+-- @+--+-- In the above, render1 will show "Brick is fun" where the first and+-- last words are yellow on a blue background and the middle word is+-- white on a blue background. However, render2 will show all words+-- in red on a blue background. In both versions, the middle word+-- will be in white on a blue background.+forceAttr :: AttrName -> Widget n -> Widget n+forceAttr an p =+ Widget (hSize p) (vSize p) $ do+ c <- getContext+ withReaderT (ctxAttrMapL .~ (forceAttrMap (attrMapLookup an (c^.ctxAttrMapL)))) (render p)++-- | Like 'forceAttr', except that the style of attribute lookups in the+-- attribute map is preserved and merged with the forced attribute. This+-- allows for situations where 'forceAttr' would otherwise ignore style+-- information that is important to preserve.+forceAttrAllowStyle :: AttrName -> Widget n -> Widget n+forceAttrAllowStyle an p =+ Widget (hSize p) (vSize p) $ do+ c <- getContext+ let m = c^.ctxAttrMapL+ withReaderT (ctxAttrMapL .~ (forceAttrMapAllowStyle (attrMapLookup an m) m)) (render p)++-- | Override the lookup of the attribute name 'targetName' to return+-- the attribute value associated with 'fromName' when rendering the+-- specified widget.+--+-- For example:+--+-- @+-- appAttrMap = attrMap (white `on` blue) [ ("highlight", fg yellow)+-- , ("notice", fg red)+-- ]+--+-- renderA :: (String, String) -> [Widget n]+-- renderA (a, b) = str a <+> str " is " <+> withAttr "highlight" (str b)+--+-- render1 = withAttr "notice" $ renderA ("Brick", "fun")+-- render2 = overrideAttr "highlight" "notice" render1+-- @+--+-- In the example above, @render1@ will show @Brick is fun@ where the+-- first two words are red on a blue background, but @fun@ is yellow on+-- a blue background. However, @render2@ will show all three words in+-- red on a blue background.+overrideAttr :: AttrName -> AttrName -> Widget n -> Widget n+overrideAttr targetName fromName =+ updateAttrMap (mapAttrName fromName targetName)++-- | Build a widget directly from a raw Vty image.+raw :: V.Image -> Widget n+raw img = Widget Fixed Fixed $ return $ emptyResult & imageL .~ img++-- | Translate the specified widget by the specified offset amount.+-- Defers to the translated widget for growth policy.+translateBy :: Location -> Widget n -> Widget n+translateBy off p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ return $ addResultOffset off+ $ result & imageL %~ (V.translate (off^.locationColumnL) (off^.locationRowL))++-- | Given a widget, translate it to position it relative to the+-- upper-left coordinates of a reported extent with the specified+-- positioning offset. If the specified name has no reported extent,+-- this draws nothing on the basis that it only makes sense to draw what+-- was requested when the relative position can be known.+--+-- This is only useful for positioning something in a higher layer+-- relative to a reported extent in a lower layer. Any other use is+-- likely to result in the specified widget not being rendered. This+-- is because this function relies on information about lower layer+-- renderings in order to work; using it with a resource name that+-- wasn't rendered in a lower layer will result in this being equivalent+-- to @emptyWidget@.+--+-- For example, if you have two layers @topLayer@ and @bottomLayer@,+-- then a widget drawn in @bottomLayer@ with @reportExtent Foo@ can be+-- used to relatively position a widget in @topLayer@ with @topLayer =+-- relativeTo Foo ...@.+relativeTo :: (Ord n) => n -> Location -> Widget n -> Widget n+relativeTo n off w =+ Widget (hSize w) (vSize w) $ do+ mExt <- lookupReportedExtent n+ case mExt of+ Nothing -> render emptyWidget+ Just ext -> render $ translateBy (extentUpperLeft ext <> off) w++-- | Crop the specified widget on the left by the specified number of+-- columns. Defers to the cropped widget for growth policy.+cropLeftBy :: Int -> Widget n -> Widget n+cropLeftBy cols p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let amt = V.imageWidth (result^.imageL) - cols+ cropped img = if amt < 0 then V.emptyImage else V.cropLeft amt img+ render $ Widget (hSize p) (vSize p) $+ withReaderT (availWidthL .~ amt) $+ cropResultToContext $+ addResultOffset (Location (-1 * cols, 0)) $+ result & imageL %~ cropped++-- | Crop the specified widget to the specified size from the left.+-- Defers to the cropped widget for growth policy.+cropLeftTo :: Int -> Widget n -> Widget n+cropLeftTo cols p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let w = V.imageWidth $ result^.imageL+ amt = w - cols+ if w <= cols+ then return result+ else render $ cropLeftBy amt $ Widget Fixed Fixed $ return result++-- | Crop the specified widget on the right by the specified number of+-- columns. Defers to the cropped widget for growth policy.+cropRightBy :: Int -> Widget n -> Widget n+cropRightBy cols p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let amt = V.imageWidth (result^.imageL) - cols+ cropped img = if amt < 0 then V.emptyImage else V.cropRight amt img+ withReaderT (availWidthL .~ amt) $+ cropResultToContext $ result & imageL %~ cropped++-- | Crop the specified widget to the specified size from the right.+-- Defers to the cropped widget for growth policy.+cropRightTo :: Int -> Widget n -> Widget n+cropRightTo cols p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let w = V.imageWidth $ result^.imageL+ amt = w - cols+ if w <= cols+ then return result+ else render $ cropRightBy amt $ Widget Fixed Fixed $ return result++-- | Crop the specified widget on the top by the specified number of+-- rows. Defers to the cropped widget for growth policy.+cropTopBy :: Int -> Widget n -> Widget n+cropTopBy rows p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let amt = V.imageHeight (result^.imageL) - rows+ cropped img = if amt < 0 then V.emptyImage else V.cropTop amt img+ render $ Widget (hSize p) (vSize p) $+ withReaderT (availHeightL .~ amt) $+ cropResultToContext $+ addResultOffset (Location (0, -1 * rows)) $+ result & imageL %~ cropped++-- | Crop the specified widget to the specified size from the top.+-- Defers to the cropped widget for growth policy.+cropTopTo :: Int -> Widget n -> Widget n+cropTopTo rows p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let h = V.imageHeight $ result^.imageL+ amt = h - rows+ if h <= rows+ then return result+ else render $ cropTopBy amt $ Widget Fixed Fixed $ return result++-- | Crop the specified widget on the bottom by the specified number of+-- rows. Defers to the cropped widget for growth policy.+cropBottomBy :: Int -> Widget n -> Widget n+cropBottomBy rows p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let amt = V.imageHeight (result^.imageL) - rows+ cropped img = if amt < 0 then V.emptyImage else V.cropBottom amt img+ withReaderT (availHeightL .~ amt) $+ cropResultToContext $ result & imageL %~ cropped++-- | Crop the specified widget to the specified size from the bottom.+-- Defers to the cropped widget for growth policy.+cropBottomTo :: Int -> Widget n -> Widget n+cropBottomTo rows p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let h = V.imageHeight $ result^.imageL+ amt = h - rows+ if h <= rows+ then return result+ else render $ cropBottomBy amt $ Widget Fixed Fixed $ return result++-- | When rendering the specified widget, also register a cursor+-- positioning request using the specified name and location.+showCursor :: n -> Location -> Widget n -> Widget n+showCursor n cloc p = Widget (hSize p) (vSize p) $+ (cursorsL %~ (CursorLocation cloc (Just n) True:)) <$> (render p)++-- | When rendering the specified widget, also register a cursor+-- positioning request using the specified name and location.+-- The cursor will only be positioned but not made visible.+putCursor :: n -> Location -> Widget n -> Widget n+putCursor n cloc p = Widget (hSize p) (vSize p) $+ (cursorsL %~ (CursorLocation cloc (Just n) False:)) <$> (render p)++hRelease :: Widget n -> Maybe (Widget n)+hRelease p =+ case hSize p of+ Fixed -> Just $ Widget Greedy (vSize p) $+ withReaderT (availWidthL .~ unrestricted) (render p)+ Greedy -> Nothing++vRelease :: Widget n -> Maybe (Widget n)+vRelease p =+ case vSize p of+ Fixed -> Just $ Widget (hSize p) Greedy $+ withReaderT (availHeightL .~ unrestricted) (render p)+ Greedy -> Nothing++-- | If the specified resource name has an entry in the rendering cache,+-- use the rendered version from the cache. If not, render the specified+-- widget and update the cache with the result.+--+-- To ensure that mouse events are emitted correctly for cached widgets,+-- in addition to the rendered widget, we also cache (the names of) any+-- clickable extents that were rendered and restore that when utilizing+-- the cache.+--+-- See also 'invalidateCacheEntry'.+cached :: (Ord n) => n -> Widget n -> Widget n+cached n w =+ Widget (hSize w) (vSize w) $ do+ result <- cacheLookup n+ case result of+ Just (clickables, prevResult) -> do+ clickableNamesL %= (clickables ++)+ return prevResult+ Nothing -> do+ wResult <- render w+ clickables <- renderedClickables wResult+ cacheUpdate n (clickables, wResult & visibilityRequestsL .~ mempty)+ return wResult+ where+ -- Given the rendered result of a Widget, collect the list of "clickable" names+ -- from the extents that were in the result.+ renderedClickables :: (Ord n) => Result n -> RenderM n [n]+ renderedClickables renderResult = do+ allClickables <- use clickableNamesL+ return [extentName e | e <- renderResult^.extentsL, extentName e `elem` allClickables]++cacheLookup :: (Ord n) => n -> RenderM n (Maybe ([n], Result n))+cacheLookup n = do+ cache <- lift $ gets (^.renderCacheL)+ return $ M.lookup n cache++cacheUpdate :: Ord n => n -> ([n], Result n) -> RenderM n ()+cacheUpdate n r = lift $ modify (renderCacheL %~ M.insert n r)++-- | Enable vertical scroll bars on all viewports in the specified+-- widget and draw them with the specified orientation.+withVScrollBars :: VScrollBarOrientation -> Widget n -> Widget n+withVScrollBars orientation w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxVScrollBarOrientationL .~ Just orientation) (render w)++-- | Enable scroll bar handles on all vertical scroll bars in the+-- specified widget. Handles appear at the ends of the scroll bar,+-- representing the "handles" that are typically clickable in+-- graphical UIs to move the scroll bar incrementally. Vertical+-- scroll bars are also clickable if mouse mode is enabled and if+-- 'withClickableVScrollBars' is used.+--+-- This will only have an effect if 'withVScrollBars' is also called.+withVScrollBarHandles :: Widget n -> Widget n+withVScrollBarHandles w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxVScrollBarShowHandlesL .~ True) (render w)++-- | Render vertical viewport scroll bars in the specified widget with+-- the specified renderer. This is only needed if you want to override+-- the use of the default renderer, 'verticalScrollbarRenderer'.+withVScrollBarRenderer :: VScrollbarRenderer n -> Widget n -> Widget n+withVScrollBarRenderer r w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxVScrollBarRendererL .~ Just r) (render w)++-- | The default renderer for vertical viewport scroll bars. Override+-- with 'withVScrollBarRenderer'.+verticalScrollbarRenderer :: VScrollbarRenderer n+verticalScrollbarRenderer =+ VScrollbarRenderer { renderVScrollbar = fill '█'+ , renderVScrollbarTrough = fill ' '+ , renderVScrollbarHandleBefore = str "^"+ , renderVScrollbarHandleAfter = str "v"+ , scrollbarWidthAllocation = 1+ }++-- | Enable horizontal scroll bars on all viewports in the specified+-- widget and draw them with the specified orientation.+withHScrollBars :: HScrollBarOrientation -> Widget n -> Widget n+withHScrollBars orientation w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxHScrollBarOrientationL .~ Just orientation) (render w)++-- | Enable mouse click reporting on horizontal scroll bars in the+-- specified widget. This must be used with 'withHScrollBars'. The+-- provided function is used to build a resource name containing the+-- scroll bar element clicked and the viewport name associated with the+-- scroll bar. It is usually a data constructor of the @n@ type.+withClickableHScrollBars :: (ClickableScrollbarElement -> n -> n) -> Widget n -> Widget n+withClickableHScrollBars f w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxHScrollBarClickableConstrL .~ Just f) (render w)++-- | Enable mouse click reporting on vertical scroll bars in the+-- specified widget. This must be used with 'withVScrollBars'. The+-- provided function is used to build a resource name containing the+-- scroll bar element clicked and the viewport name associated with the+-- scroll bar. It is usually a data constructor of the @n@ type.+withClickableVScrollBars :: (ClickableScrollbarElement -> n -> n) -> Widget n -> Widget n+withClickableVScrollBars f w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxVScrollBarClickableConstrL .~ Just f) (render w)++-- | Enable scroll bar handles on all horizontal scroll bars in+-- the specified widget. Handles appear at the ends of the scroll+-- bar, representing the "handles" that are typically clickable in+-- graphical UIs to move the scroll bar incrementally. Horizontal+-- scroll bars are also clickable if mouse mode is enabled and if+-- 'withClickableHScrollBars' is used.+--+-- This will only have an effect if 'withHScrollBars' is also called.+withHScrollBarHandles :: Widget n -> Widget n+withHScrollBarHandles w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxHScrollBarShowHandlesL .~ True) (render w)++-- | Render horizontal viewport scroll bars in the specified widget with+-- the specified renderer. This is only needed if you want to override+-- the use of the default renderer, 'horizontalScrollbarRenderer'.+withHScrollBarRenderer :: HScrollbarRenderer n -> Widget n -> Widget n+withHScrollBarRenderer r w =+ Widget (hSize w) (vSize w) $+ withReaderT (ctxHScrollBarRendererL .~ Just r) (render w)++-- | The default renderer for horizontal viewport scroll bars. Override+-- with 'withHScrollBarRenderer'.+horizontalScrollbarRenderer :: HScrollbarRenderer n+horizontalScrollbarRenderer =+ HScrollbarRenderer { renderHScrollbar = fill '█'+ , renderHScrollbarTrough = fill ' '+ , renderHScrollbarHandleBefore = str "<"+ , renderHScrollbarHandleAfter = str ">"+ , scrollbarHeightAllocation = 1+ }++-- | Render the specified widget in a named viewport with the+-- specified type. This permits widgets to be scrolled without being+-- scrolling-aware. To make the most use of viewports, the specified+-- widget should use the 'visible' combinator to make a "visibility+-- request". This viewport combinator will then translate the resulting+-- rendering to make the requested region visible. In addition, the+-- 'Brick.Main.EventM' monad provides primitives to scroll viewports+-- created by this function if 'visible' is not what you want.+--+-- This function can automatically render vertical and horizontal scroll+-- bars if desired. To enable scroll bars, wrap your call to 'viewport'+-- with a call to 'withVScrollBars' and/or 'withHScrollBars'. If you+-- don't like the appearance of the resulting scroll bars (defaults:+-- 'verticalScrollbarRenderer' and 'horizontalScrollbarRenderer'),+-- you can customize how they are drawn by making your own+-- 'VScrollbarRenderer' or 'HScrollbarRenderer' and using+-- 'withVScrollBarRenderer' and/or 'withHScrollBarRenderer'. Note that+-- when you enable scrollbars, the content of your viewport will lose+-- one column of available space if vertical scroll bars are enabled and+-- one row of available space if horizontal scroll bars are enabled.+--+-- If a viewport receives more than one visibility request, then the+-- visibility requests are merged with the inner visibility request+-- taking preference. If a viewport receives more than one scrolling+-- request from 'Brick.Main.EventM', all are honored in the order in+-- which they are received.+--+-- Some caution should be advised when using this function. The viewport+-- renders its contents anew each time the viewport is drawn; in many+-- cases this is prohibitively expensive, and viewports should not be+-- used to display large contents for scrolling. This function is best+-- used when the contents are not too large OR when the contents are+-- large and render-cacheable.+--+-- Also, be aware that there is a rich API for accessing viewport+-- information from within the 'EventM' monad; check the docs for+-- @Brick.Main@ to learn more about ways to get information about+-- viewports after they're drawn.+viewport :: (Ord n, Show n)+ => n+ -- ^ The name of the viewport (must be unique and stable for+ -- reliable behavior)+ -> ViewportType+ -- ^ The type of viewport (indicates the permitted scrolling+ -- direction)+ -> Widget n+ -- ^ The widget to be rendered in the scrollable viewport+ -> Widget n+viewport vpname typ p =+ clickable vpname $ Widget Greedy Greedy $ do+ -- Obtain the scroll bar configuration.+ c <- getContext+ let vsOrientation = ctxVScrollBarOrientation c+ hsOrientation = ctxHScrollBarOrientation c+ vsRenderer = fromMaybe verticalScrollbarRenderer (ctxVScrollBarRenderer c)+ hsRenderer = fromMaybe horizontalScrollbarRenderer (ctxHScrollBarRenderer c)+ showVHandles = ctxVScrollBarShowHandles c+ showHHandles = ctxHScrollBarShowHandles c+ vsbClickableConstr = ctxVScrollBarClickableConstr c+ hsbClickableConstr = ctxHScrollBarClickableConstr c++ -- Observe the viewport name so we can detect multiple uses of the+ -- name.+ let observeName :: (Ord n, Show n) => n -> RenderM n ()+ observeName n = do+ observed <- use observedNamesL+ case S.member n observed of+ False -> observedNamesL %= S.insert n+ True ->+ error $ "Error: while rendering the interface, the name " <> show n <>+ " was seen more than once. You should ensure that all of the widgets " <>+ "in each interface have unique name values. This means either " <>+ "using a different name type or adding constructors to your " <>+ "existing one and using those to name your widgets. For more " <>+ "information, see the \"Resource Names\" section of the Brick User Guide."++ observeName vpname++ -- Update the viewport size.+ let newVp = VP 0 0 newSize (0, 0)+ newSize = (newWidth, newHeight)+ newWidth = c^.availWidthL - vSBWidth+ newHeight = c^.availHeightL - hSBHeight+ vSBWidth = maybe 0 (const $ scrollbarWidthAllocation vsRenderer) vsOrientation+ hSBHeight = maybe 0 (const $ scrollbarHeightAllocation hsRenderer) hsOrientation+ doInsert (Just vp) = Just $ vp & vpSize .~ newSize+ doInsert Nothing = Just newVp++ lift $ modify (viewportMapL %~ (M.alter doInsert vpname))++ -- Then render the viewport content widget with the rendering+ -- layout constraint released (but raise an exception if we are+ -- asked to render an infinitely-sized widget in the viewport's+ -- scrolling dimension). Also note that for viewports that+ -- only scroll in one direction, we apply a constraint in the+ -- non-scrolling direction in case a scroll bar is present.+ let release = case typ of+ Vertical -> vRelease . hLimit newWidth+ Horizontal -> hRelease . vLimit newHeight+ Both -> vRelease >=> hRelease+ released = case release p of+ Just w -> w+ Nothing -> case typ of+ Vertical -> error $ "tried to embed an infinite-height " <>+ "widget in vertical viewport " <> (show vpname)+ Horizontal -> error $ "tried to embed an infinite-width " <>+ "widget in horizontal viewport " <> (show vpname)+ Both -> error $ "tried to embed an infinite-width or " <>+ "infinite-height widget in 'Both' type " <>+ "viewport " <> (show vpname)++ initialResult <- render released++ -- If the rendering state includes any scrolling requests for this+ -- viewport, apply those+ reqs <- lift $ gets (^.rsScrollRequestsL)+ let relevantRequests = snd <$> filter (\(n, _) -> n == vpname) reqs+ when (not $ null relevantRequests) $ do+ mVp <- lift $ gets (^.viewportMapL.to (M.lookup vpname))+ case mVp of+ Nothing -> error $ "BUG: viewport: viewport name " <> show vpname <> " absent from viewport map"+ Just vp -> do+ let updatedVp = applyRequests relevantRequests vp+ applyRequests [] v = v+ applyRequests (rq:rqs) v =+ case typ of+ Horizontal -> scrollTo typ rq (initialResult^.imageL) $ applyRequests rqs v+ Vertical -> scrollTo typ rq (initialResult^.imageL) $ applyRequests rqs v+ Both -> scrollTo Horizontal rq (initialResult^.imageL) $+ scrollTo Vertical rq (initialResult^.imageL) $+ applyRequests rqs v+ lift $ modify (viewportMapL %~ (M.insert vpname updatedVp))++ -- If the sub-rendering requested visibility, update the scroll+ -- state accordingly+ when (not $ null $ initialResult^.visibilityRequestsL) $ do+ mVp <- lift $ gets (^.viewportMapL.to (M.lookup vpname))+ case mVp of+ Nothing -> error $ "BUG: viewport: viewport name " <> show vpname <> " absent from viewport map"+ Just vp -> do+ let rqs = initialResult^.visibilityRequestsL+ updateVp vp' rq = case typ of+ Both -> scrollToView Horizontal rq $ scrollToView Vertical rq vp'+ Horizontal -> scrollToView typ rq vp'+ Vertical -> scrollToView typ rq vp'+ lift $ modify (viewportMapL %~ (M.insert vpname $ foldl updateVp vp rqs))++ -- If the size of the rendering changes enough to make the+ -- viewport offsets invalid, reset them+ mVp <- lift $ gets (^.viewportMapL.to (M.lookup vpname))+ vp <- case mVp of+ Nothing -> error $ "BUG: viewport: viewport name " <> show vpname <> " absent from viewport map"+ Just v -> return v++ let img = initialResult^.imageL+ fixTop v = if V.imageHeight img < v^.vpSize._2+ then v & vpTop .~ 0+ else v+ fixLeft v = if V.imageWidth img < v^.vpSize._1+ then v & vpLeft .~ 0+ else v+ updateContentSize v = v & vpContentSize .~ (V.imageWidth img, V.imageHeight img)+ updateVp = updateContentSize . case typ of+ Both -> fixLeft . fixTop+ Horizontal -> fixLeft+ Vertical -> fixTop+ lift $ modify (viewportMapL %~ (M.insert vpname (updateVp vp)))++ -- Get the viewport state now that it has been updated.+ mVpFinal <- lift $ gets (M.lookup vpname . (^.viewportMapL))+ vpFinal <- case mVpFinal of+ Nothing -> error $ "BUG: viewport: viewport name " <> show vpname <> " absent from viewport map"+ Just v -> return v++ -- Then perform a translation of the sub-rendering to fit into the+ -- viewport+ translated <- render $ translateBy (Location (-1 * vpFinal^.vpLeft, -1 * vpFinal^.vpTop))+ $ Widget Fixed Fixed $ return initialResult++ -- If the vertical scroll bar is enabled, render the scroll bar+ -- area.+ let addVScrollbar = case vsOrientation of+ Nothing -> id+ Just orientation ->+ let sb = verticalScrollbar vsRenderer orientation+ vpname+ vsbClickableConstr+ showVHandles+ (vpFinal^.vpSize._2)+ (vpFinal^.vpTop)+ (vpFinal^.vpContentSize._2)+ combine = case orientation of+ OnLeft -> (<+>)+ OnRight -> flip (<+>)+ in combine sb+ addHScrollbar = case hsOrientation of+ Nothing -> id+ Just orientation ->+ let sb = horizontalScrollbar hsRenderer orientation+ vpname+ hsbClickableConstr+ showHHandles+ (vpFinal^.vpSize._1)+ (vpFinal^.vpLeft)+ (vpFinal^.vpContentSize._1)+ combine = case orientation of+ OnTop -> (<=>)+ OnBottom -> flip (<=>)+ in combine sb++ -- Return the translated result with the visibility requests+ -- discarded+ let translatedSize = ( translated^.imageL.to V.imageWidth+ , translated^.imageL.to V.imageHeight+ )+ case translatedSize of+ (0, 0) -> do+ let spaceFill = V.charFill (c^.attrL) ' ' (c^.availWidthL) (c^.availHeightL)+ return $ translated & imageL .~ spaceFill+ & visibilityRequestsL .~ mempty+ & extentsL .~ mempty+ _ -> render $ addVScrollbar+ $ addHScrollbar+ $ vLimit (vpFinal^.vpSize._2)+ $ hLimit (vpFinal^.vpSize._1)+ $ padBottom Max+ $ padRight Max+ $ Widget Fixed Fixed+ $ return $ translated & visibilityRequestsL .~ mempty++-- | The base attribute for scroll bars.+scrollbarAttr :: AttrName+scrollbarAttr = attrName "scrollbar"++-- | The attribute for scroll bar troughs. This attribute is a+-- specialization of @scrollbarAttr@.+scrollbarTroughAttr :: AttrName+scrollbarTroughAttr = scrollbarAttr <> attrName "trough"++-- | The attribute for scroll bar handles. This attribute is a+-- specialization of @scrollbarAttr@.+scrollbarHandleAttr :: AttrName+scrollbarHandleAttr = scrollbarAttr <> attrName "handle"++maybeClick :: (Ord n)+ => n+ -> Maybe (ClickableScrollbarElement -> n -> n)+ -> ClickableScrollbarElement+ -> Widget n+ -> Widget n+maybeClick _ Nothing _ w = w+maybeClick n (Just f) el w = clickable (f el n) w++-- | Build a vertical scroll bar using the specified renderer and+-- settings.+--+-- You probably don't want to use this directly; instead,+-- use @viewport@, @withVScrollBars@, and, if needed,+-- @withVScrollBarRenderer@. This is exposed so that if you want to+-- render a scroll bar of your own, you can do so outside the @viewport@+-- context.+verticalScrollbar :: (Ord n)+ => VScrollbarRenderer n+ -- ^ The renderer to use.+ -> VScrollBarOrientation+ -- ^ The scroll bar orientation. The orientation+ -- governs how additional padding is added to+ -- the scroll bar if it is smaller than it space+ -- allocation according to 'scrollbarWidthAllocation'.+ -> n+ -- ^ The viewport name associated with this scroll+ -- bar.+ -> Maybe (ClickableScrollbarElement -> n -> n)+ -- ^ Constructor for clickable scroll bar element names.+ -> Bool+ -- ^ Whether to display handles.+ -> Int+ -- ^ The total viewport height in effect.+ -> Int+ -- ^ The viewport vertical scrolling offset in effect.+ -> Int+ -- ^ The total viewport content height.+ -> Widget n+verticalScrollbar vsRenderer o n constr showHandles vpHeight vOffset contentHeight =+ hLimit (scrollbarWidthAllocation vsRenderer) $+ applyPadding $+ if showHandles+ then vBox [ vLimit 1 $+ maybeClick n constr SBHandleBefore $+ withDefAttr scrollbarHandleAttr $ renderVScrollbarHandleBefore vsRenderer+ , sbBody+ , vLimit 1 $+ maybeClick n constr SBHandleAfter $+ withDefAttr scrollbarHandleAttr $ renderVScrollbarHandleAfter vsRenderer+ ]+ else sbBody+ where+ sbBody = verticalScrollbar' vsRenderer n constr vpHeight vOffset contentHeight+ applyPadding = case o of+ OnLeft -> padRight Max+ OnRight -> padLeft Max++verticalScrollbar' :: (Ord n)+ => VScrollbarRenderer n+ -- ^ The renderer to use.+ -> n+ -- ^ The viewport name associated with this scroll+ -- bar.+ -> Maybe (ClickableScrollbarElement -> n -> n)+ -- ^ Constructor for clickable scroll bar element+ -- names. Will be given the element name and the+ -- viewport name.+ -> Int+ -- ^ The total viewport height in effect.+ -> Int+ -- ^ The viewport vertical scrolling offset in effect.+ -> Int+ -- ^ The total viewport content height.+ -> Widget n+verticalScrollbar' vsRenderer _ _ vpHeight _ 0 =+ vLimit vpHeight $ renderVScrollbarTrough vsRenderer+verticalScrollbar' vsRenderer n constr vpHeight vOffset contentHeight =+ Widget Fixed Greedy $ do+ c <- getContext++ -- Get the proportion of the total content that is visible+ let visibleContentPercent :: Double+ visibleContentPercent = fromIntegral vpHeight /+ fromIntegral contentHeight++ ctxHeight = c^.availHeightL++ -- Then get the proportion of the scroll bar that+ -- should be filled in+ sbSize = min ctxHeight $+ max 1 $+ round $ visibleContentPercent * (fromIntegral ctxHeight)++ -- Then get the vertical offset of the scroll bar+ -- itself+ sbOffset = if vOffset == 0+ then 0+ else if vOffset == contentHeight - vpHeight+ then ctxHeight - sbSize+ else min (ctxHeight - sbSize - 1) $+ max 1 $+ round $ fromIntegral ctxHeight *+ (fromIntegral vOffset /+ fromIntegral contentHeight::Double)++ sbAbove = maybeClick n constr SBTroughBefore $+ withDefAttr scrollbarTroughAttr $ vLimit sbOffset $+ renderVScrollbarTrough vsRenderer+ sbBelow = maybeClick n constr SBTroughAfter $+ withDefAttr scrollbarTroughAttr $ vLimit (ctxHeight - (sbOffset + sbSize)) $+ renderVScrollbarTrough vsRenderer+ sbMiddle = maybeClick n constr SBBar $+ withDefAttr scrollbarAttr $ vLimit sbSize $ renderVScrollbar vsRenderer++ sb = if sbSize == ctxHeight+ then vLimit sbSize $+ renderVScrollbarTrough vsRenderer+ else vBox [sbAbove, sbMiddle, sbBelow]++ render sb++-- | Build a horizontal scroll bar using the specified renderer and+-- settings.+--+-- You probably don't want to use this directly; instead, use+-- @viewport@, @withHScrollBars@, and, if needed,+-- @withHScrollBarRenderer@. This is exposed so that if you want to+-- render a scroll bar of your own, you can do so outside the @viewport@+-- context.+horizontalScrollbar :: (Ord n)+ => HScrollbarRenderer n+ -- ^ The renderer to use.+ -> HScrollBarOrientation+ -- ^ The scroll bar orientation. The orientation+ -- governs how additional padding is added+ -- to the scroll bar if it is smaller+ -- than it space allocation according to+ -- 'scrollbarHeightAllocation'.+ -> n+ -- ^ The viewport name associated with this scroll+ -- bar.+ -> Maybe (ClickableScrollbarElement -> n -> n)+ -- ^ Constructor for clickable scroll bar element+ -- names. Will be given the element name and the+ -- viewport name.+ -> Bool+ -- ^ Whether to show handles.+ -> Int+ -- ^ The total viewport width in effect.+ -> Int+ -- ^ The viewport horizontal scrolling offset in effect.+ -> Int+ -- ^ The total viewport content width.+ -> Widget n+horizontalScrollbar hsRenderer o n constr showHandles vpWidth hOffset contentWidth =+ vLimit (scrollbarHeightAllocation hsRenderer) $+ applyPadding $+ if showHandles+ then hBox [ hLimit 1 $+ maybeClick n constr SBHandleBefore $+ withDefAttr scrollbarHandleAttr $ renderHScrollbarHandleBefore hsRenderer+ , sbBody+ , hLimit 1 $+ maybeClick n constr SBHandleAfter $+ withDefAttr scrollbarHandleAttr $ renderHScrollbarHandleAfter hsRenderer+ ]+ else sbBody+ where+ sbBody = horizontalScrollbar' hsRenderer n constr vpWidth hOffset contentWidth+ applyPadding = case o of+ OnTop -> padBottom Max+ OnBottom -> padTop Max++horizontalScrollbar' :: (Ord n)+ => HScrollbarRenderer n+ -- ^ The renderer to use.+ -> n+ -- ^ The viewport name associated with this scroll+ -- bar.+ -> Maybe (ClickableScrollbarElement -> n -> n)+ -- ^ Constructor for clickable scroll bar element+ -- names.+ -> Int+ -- ^ The total viewport width in effect.+ -> Int+ -- ^ The viewport horizontal scrolling offset in effect.+ -> Int+ -- ^ The total viewport content width.+ -> Widget n+horizontalScrollbar' hsRenderer _ _ vpWidth _ 0 =+ hLimit vpWidth $ renderHScrollbarTrough hsRenderer+horizontalScrollbar' hsRenderer n constr vpWidth hOffset contentWidth =+ Widget Greedy Fixed $ do+ c <- getContext++ -- Get the proportion of the total content that is visible+ let visibleContentPercent :: Double+ visibleContentPercent = fromIntegral vpWidth /+ fromIntegral contentWidth++ ctxWidth = c^.availWidthL++ -- Then get the proportion of the scroll bar that+ -- should be filled in+ sbSize = min ctxWidth $+ max 1 $+ round $ visibleContentPercent * (fromIntegral ctxWidth)++ -- Then get the horizontal offset of the scroll bar itself+ sbOffset = if hOffset == 0+ then 0+ else if hOffset == contentWidth - vpWidth+ then ctxWidth - sbSize+ else min (ctxWidth - sbSize - 1) $+ max 1 $+ round $ fromIntegral ctxWidth *+ (fromIntegral hOffset /+ fromIntegral contentWidth::Double)++ sbLeft = maybeClick n constr SBTroughBefore $+ withDefAttr scrollbarTroughAttr $ hLimit sbOffset $+ renderHScrollbarTrough hsRenderer+ sbRight = maybeClick n constr SBTroughAfter $+ withDefAttr scrollbarTroughAttr $ hLimit (ctxWidth - (sbOffset + sbSize)) $+ renderHScrollbarTrough hsRenderer+ sbMiddle = maybeClick n constr SBBar $+ withDefAttr scrollbarAttr $ hLimit sbSize $ renderHScrollbar hsRenderer++ sb = if sbSize == ctxWidth+ then hLimit sbSize $+ renderHScrollbarTrough hsRenderer+ else hBox [sbLeft, sbMiddle, sbRight]++ render sb++-- | Given a name, obtain the viewport for that name by consulting the+-- viewport map in the rendering monad. NOTE! Some care must be taken+-- when calling this function, since it only returns useful values+-- after the viewport in question has been rendered. If you call this+-- function during rendering before a viewport has been rendered, you+-- may get nothing or you may get a stale version of the viewport. This+-- is because viewports are updated during rendering and the one you are+-- interested in may not have been rendered yet. So if you want to use+-- this, be sure you know what you are doing.+unsafeLookupViewport :: (Ord n) => n -> RenderM n (Maybe Viewport)+unsafeLookupViewport name = lift $ gets (M.lookup name . (^.viewportMapL))++scrollTo :: ViewportType -> ScrollRequest -> V.Image -> Viewport -> Viewport+scrollTo Both _ _ _ = error "BUG: called scrollTo on viewport type 'Both'"+scrollTo Vertical req img vp = vp & vpTop .~ newVStart+ where+ newVStart = clamp 0 (V.imageHeight img - vp^.vpSize._2) adjustedAmt+ adjustedAmt = case req of+ VScrollBy amt -> vp^.vpTop + amt+ VScrollPage Up -> vp^.vpTop - vp^.vpSize._2+ VScrollPage Down -> vp^.vpTop + vp^.vpSize._2+ VScrollToBeginning -> 0+ VScrollToEnd -> V.imageHeight img - vp^.vpSize._2+ SetTop i -> i+ _ -> vp^.vpTop+scrollTo Horizontal req img vp = vp & vpLeft .~ newHStart+ where+ newHStart = clamp 0 (V.imageWidth img - vp^.vpSize._1) adjustedAmt+ adjustedAmt = case req of+ HScrollBy amt -> vp^.vpLeft + amt+ HScrollPage Up -> vp^.vpLeft - vp^.vpSize._1+ HScrollPage Down -> vp^.vpLeft + vp^.vpSize._1+ HScrollToBeginning -> 0+ HScrollToEnd -> V.imageWidth img - vp^.vpSize._1+ SetLeft i -> i+ _ -> vp^.vpLeft++scrollToView :: ViewportType -> VisibilityRequest -> Viewport -> Viewport+scrollToView Both _ _ = error "BUG: called scrollToView on 'Both' type viewport"+scrollToView Vertical rq vp = vp & vpTop .~ newVStart+ where+ curStart = vp^.vpTop+ curEnd = curStart + vp^.vpSize._2+ reqStart = rq^.vrPositionL.locationRowL++ reqEnd = rq^.vrPositionL.locationRowL + rq^.vrSizeL._2+ newVStart :: Int+ newVStart = if reqStart < vStartEndVisible+ then reqStart+ else vStartEndVisible+ vStartEndVisible = if reqEnd < curEnd+ then curStart+ else curStart + (reqEnd - curEnd)+scrollToView Horizontal rq vp = vp & vpLeft .~ newHStart+ where+ curStart = vp^.vpLeft+ curEnd = curStart + vp^.vpSize._1+ reqStart = rq^.vrPositionL.locationColumnL++ reqEnd = rq^.vrPositionL.locationColumnL + rq^.vrSizeL._1+ newHStart :: Int+ newHStart = if reqStart < hStartEndVisible+ then reqStart+ else hStartEndVisible+ hStartEndVisible = if reqEnd < curEnd+ then curStart+ else curStart + (reqEnd - curEnd)++-- | Request that the specified widget be made visible when it is+-- rendered inside a viewport. This permits widgets (whose sizes and+-- positions cannot be known due to being embedded in arbitrary layouts)+-- to make a request for a parent viewport to locate them and scroll+-- enough to put them in view. This, together with 'viewport', is what+-- makes the text editor and list widgets possible without making them+-- deal with the details of scrolling state management.+--+-- This does nothing if not rendered in a viewport.+visible :: Widget n -> Widget n+visible p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ let imageSize = ( result^.imageL.to V.imageWidth+ , result^.imageL.to V.imageHeight+ )+ -- The size of the image to be made visible in a viewport must have+ -- non-zero size in both dimensions.+ return $ if imageSize^._1 > 0 && imageSize^._2 > 0+ then result & visibilityRequestsL %~ (VR (Location (0, 0)) imageSize :)+ else result++-- | Similar to 'visible', request that a region (with the specified+-- 'Location' as its origin and 'V.DisplayRegion' as its size) be made+-- visible when it is rendered inside a viewport. The 'Location' is+-- relative to the specified widget's upper-left corner of (0, 0).+--+-- This does nothing if not rendered in a viewport.+visibleRegion :: Location -> V.DisplayRegion -> Widget n -> Widget n+visibleRegion vrloc sz p =+ Widget (hSize p) (vSize p) $ do+ result <- render p+ -- The size of the image to be made visible in a viewport must have+ -- non-zero size in both dimensions.+ return $ if sz^._1 > 0 && sz^._2 > 0+ then result & visibilityRequestsL %~ (VR vrloc sz :)+ else result++-- | Horizontal box layout: put the specified widgets next to each other+-- in the specified order. Defers growth policies to the growth policies+-- of both widgets. This operator is a binary version of 'hBox'.+{-# NOINLINE (<+>) #-}+(<+>) :: Widget n+ -- ^ Left+ -> Widget n+ -- ^ Right+ -> Widget n+(<+>) a b = hBox [a, b]++-- | Vertical box layout: put the specified widgets one above the other+-- in the specified order. Defers growth policies to the growth policies+-- of both widgets. This operator is a binary version of 'vBox'.+{-# NOINLINE (<=>) #-}+(<=>) :: Widget n+ -- ^ Top+ -> Widget n+ -- ^ Bottom+ -> Widget n+(<=>) a b = vBox [a, b]++{-# RULES+"baseHbox" forall a b . a <+> b = hBox [a, b]+"hBox2" forall as bs . hBox [hBox as, hBox bs] = hBox (as ++ bs)+"hboxL" forall as b . hBox [hBox as, b] = hBox (as ++ [b])+"hboxR" forall a bs . hBox [a, hBox bs] = hBox (a : bs)+"baseVbox" forall a b . a <=> b = vBox [a, b]+"vBox2" forall as bs . vBox [vBox as, vBox bs] = vBox (as ++ bs)+"vboxL" forall as b . vBox [vBox as, b] = vBox (as ++ [b])+"vboxR" forall a bs . vBox [a, vBox bs] = vBox (a : bs)+ #-}
src/Brick/Widgets/Dialog.hs view
@@ -1,17 +1,31 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} -- | This module provides a simple dialog widget. You get to pick the -- dialog title, if any, as well as its body and buttons.+--+-- Note that this dialog is really for simple use cases where you want+-- to get the user's answer to a question, such as "Would you like to+-- save changes before quitting?" As is typical in such cases, we assume+-- that this dialog box is used modally, meaning that while it is open+-- it is has exclusive input focus until it is closed.+--+-- If you require something more sophisticated, you'll need to build it+-- yourself. You might also consider seeing the 'Brick.Forms' module for+-- help with input management and see the implementation of this module+-- to see how to reproduce a dialog-style UI. module Brick.Widgets.Dialog ( Dialog , dialogTitle- , dialogName , dialogButtons- , dialogSelectedIndex , dialogWidth -- * Construction and rendering , dialog , renderDialog+ , getDialogFocus+ , setDialogFocus+ -- * Handling events+ , handleDialogEvent -- * Getting a dialog's current value , dialogSelection -- * Attributes@@ -19,21 +33,21 @@ , buttonAttr , buttonSelectedAttr -- * Lenses- , dialogNameL , dialogButtonsL- , dialogSelectedIndexL , dialogWidthL , dialogTitleL ) where -import Control.Lens-import Control.Applicative+import Lens.Micro+import Lens.Micro.Mtl ((%=))+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid-import Data.List (intersperse)+#endif+import Data.List (intersperse, find) import Graphics.Vty.Input (Event(..), Key(..)) -import Brick.Util (clamp)+import Brick.Focus import Brick.Types import Brick.Widgets.Core import Brick.Widgets.Center@@ -41,81 +55,100 @@ import Brick.AttrMap -- | Dialogs present a window with a title (optional), a body, and--- buttons (optional). They provide a 'HandleEvent' instance that knows--- about Tab and Shift-Tab for changing which button is active. Dialog--- buttons are labeled with strings and map to values of type 'a', which--- you choose.+-- buttons (optional). Dialog buttons are labeled with strings and map+-- to values of type 'a', which you choose. ----- Dialogs handle the following events by default:+-- Dialogs handle the following events by default with+-- handleDialogEvent: ----- * Tab: selecte the next button--- * Shift-tab: select the previous button-data Dialog a =- Dialog { dialogName :: Name- -- ^ The dialog name- , dialogTitle :: Maybe String+-- * Tab or Right Arrow: select the next button+-- * Shift-tab or Left Arrow: select the previous button+data Dialog a n =+ Dialog { dialogTitle :: Maybe (Widget n) -- ^ The dialog title- , dialogButtons :: [(String, a)]- -- ^ The dialog button labels and values- , dialogSelectedIndex :: Maybe Int- -- ^ The currently selected dialog button index (if any)+ , dialogButtons :: [(String, n, a)]+ -- ^ The dialog buttons' labels, resource names, and values , dialogWidth :: Int -- ^ The maximum width of the dialog+ , dialogFocus :: FocusRing n+ -- ^ The focus ring for the dialog's buttons } suffixLenses ''Dialog -instance HandleEvent (Dialog a) where- handleEvent ev d =- case ev of- EvKey (KChar '\t') [] -> return $ nextButtonBy 1 d- EvKey KBackTab [] -> return $ nextButtonBy (-1) d- _ -> return d+handleDialogEvent :: Event -> EventM n (Dialog a n) ()+handleDialogEvent ev = do+ case ev of+ EvKey (KChar '\t') [] -> dialogFocusL %= focusNext+ EvKey KRight [] -> dialogFocusL %= focusNext+ EvKey KBackTab [] -> dialogFocusL %= focusPrev+ EvKey KLeft [] -> dialogFocusL %= focusPrev+ _ -> return () +-- | Set the focused button of a dialog.+setDialogFocus :: (Eq n) => n -> Dialog a n -> Dialog a n+setDialogFocus n d = d { dialogFocus = focusSetCurrent n $ dialogFocus d }++-- | Get the focused button of a dialog.+getDialogFocus :: Dialog a n -> Maybe n+getDialogFocus = focusGetCurrent . dialogFocus+ -- | Create a dialog.-dialog :: Name- -- ^ The dialog name, provided so that you can use this as a- -- basis for viewport names in the dialog if desired- -> Maybe String+dialog :: (Eq n)+ => Maybe (Widget n) -- ^ The dialog title- -> Maybe (Int, [(String, a)])- -- ^ The currently-selected button index (starting at zero) and- -- the button labels and values to use+ -> Maybe (n, [(String, n, a)])+ -- ^ The currently-selected button resource name and the button+ -- labels, resource names, and values to use for each button,+ -- respectively -> Int -- ^ The maximum width of the dialog- -> Dialog a-dialog name title buttonData w =- let (buttons, idx) = case buttonData of- Nothing -> ([], Nothing)- Just (_, []) -> ([], Nothing)- Just (i, bs) -> (bs, Just $ clamp 0 (length bs - 1) i)- in Dialog name title buttons idx w+ -> Dialog a n+dialog title buttonData w =+ let (r, buttons) = case buttonData of+ Nothing ->+ (focusRing [], [])+ Just (focName, entries) ->+ let ns = (\(_, n, _) -> n) <$> entries+ in (focusSetCurrent focName $ focusRing ns, entries)+ in Dialog title buttons w r -- | The default attribute of the dialog dialogAttr :: AttrName-dialogAttr = "dialog"+dialogAttr = attrName "dialog" -- | The default attribute for all dialog buttons buttonAttr :: AttrName-buttonAttr = "button"+buttonAttr = attrName "button" -- | The attribute for the selected dialog button (extends 'dialogAttr') buttonSelectedAttr :: AttrName-buttonSelectedAttr = buttonAttr <> "selected"+buttonSelectedAttr = buttonAttr <> attrName "selected" --- | Render a dialog with the specified body widget.-renderDialog :: Dialog a -> Widget -> Widget+-- | Render a dialog with the specified body widget. This renders the+-- dialog as a layer, which makes this suitable as a top-level layer in+-- your rendering function to be rendered on top of the rest of your+-- interface.+renderDialog :: (Ord n) => Dialog a n -> Widget n -> Widget n renderDialog d body = let buttonPadding = str " "- mkButton (i, (s, _)) = let att = if Just i == d^.dialogSelectedIndexL- then buttonSelectedAttr- else buttonAttr- in withAttr att $ str $ " " <> s <> " "+ foc = focusGetCurrent $ dialogFocus d+ mkButton (s, n, _) =+ let att = if Just n == foc+ then buttonSelectedAttr+ else buttonAttr+ csr = if Just n == foc+ then putCursor n (Location (1,0))+ else id+ in csr $+ clickable n $+ withAttr att $+ str $ " " <> s <> " " buttons = hBox $ intersperse buttonPadding $- mkButton <$> (zip [0..] (d^.dialogButtonsL))+ mkButton <$> (d^.dialogButtonsL) - doBorder = maybe border borderWithLabel (str <$> d^.dialogTitleL)- in center $+ doBorder = maybe border borderWithLabel (d^.dialogTitleL)+ in centerLayer $ withDefAttr dialogAttr $ hLimit (d^.dialogWidthL) $ doBorder $@@ -123,19 +156,12 @@ , hCenter buttons ] -nextButtonBy :: Int -> Dialog a -> Dialog a-nextButtonBy amt d =- let numButtons = length $ d^.dialogButtonsL- in if numButtons == 0 then d- else case d^.dialogSelectedIndexL of- Nothing -> d & dialogSelectedIndexL .~ (Just 0)- Just i -> d & dialogSelectedIndexL .~ (Just $ (i + amt) `mod` numButtons)---- | Obtain the value associated with the dialog's currently-selected--- button, if any. This function is probably what you want when someone--- presses 'Enter' in a dialog.-dialogSelection :: Dialog a -> Maybe a-dialogSelection d =- case d^.dialogSelectedIndexL of- Nothing -> Nothing- Just i -> Just $ ((d^.dialogButtonsL) !! i)^._2+-- | Obtain the resource name and value associated with the dialog's+-- currently-selected button, if any. The result of this function is+-- probably what you want when someone presses 'Enter' in a dialog.+dialogSelection :: (Eq n) => Dialog a n -> Maybe (n, a)+dialogSelection d = do+ n' <- focusGetCurrent $ dialogFocus d+ let matches (_, n, _) = n == n'+ (_, n, a) <- find matches (d^.dialogButtonsL)+ return (n, a)
src/Brick/Widgets/Edit.hs view
@@ -1,36 +1,60 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE CPP #-} -- | This module provides a basic text editor widget. You'll need to -- embed an 'Editor' in your application state and transform it with--- 'handleEvent' when relevant events arrive. To get the contents+-- 'handleEditorEvent' when relevant events arrive. To get the contents -- of the editor, just use 'getEditContents'. To modify it, use the -- 'Z.TextZipper' interface with 'applyEdit'. ----- The editor's 'HandleEvent' instance handles a set of basic input--- events that should suffice for most purposes; see the source for a--- complete list.+-- The editor's 'handleEditorEvent' function handles a set of basic+-- input events that should suffice for most purposes; see the source+-- for a complete list.+--+-- Bear in mind that the editor provided by this module is intended to+-- provide basic input support for brick applications but it is not+-- intended to be a replacement for your favorite editor such as Vim or+-- Emacs. It is also not suitable for building sophisticated editors. If+-- you want to build your own editor, I suggest starting from scratch. module Brick.Widgets.Edit- ( Editor(editContents, editorName, editDrawContents)+ ( Editor(editContents, editorName) -- * Constructing an editor , editor+ , editorText -- * Reading editor contents , getEditContents+ , getCursorPosition+ -- * Handling events+ , handleEditorEvent -- * Editing text , applyEdit -- * Lenses for working with editors , editContentsL- , editDrawContentsL -- * Rendering editors , renderEditor -- * Attributes , editAttr+ , editFocusedAttr+ -- * UTF-8 decoding of editor pastes+ , DecodeUtf8(..) ) where -import Control.Lens+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import Lens.Micro import Graphics.Vty (Event(..), Key(..), Modifier(..)) -import qualified Data.Text.Zipper as Z+import qualified Data.ByteString as BS+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Text.Zipper as Z hiding ( textZipper )+import qualified Data.Text.Zipper.Generic as Z+import qualified Data.Text.Zipper.Generic.Words as Z+import Data.Tuple (swap) import Brick.Types import Brick.Widgets.Core@@ -38,85 +62,189 @@ -- | Editor state. Editors support the following events by default: ----- * Ctrl-a: go to beginning of line--- * Ctrl-e: go to end of line+-- * Mouse clicks: change cursor position+-- * Meta-<: go to beginning of file+-- * Meta->: go to end of file+-- * Ctrl-a, Home: go to beginning of line+-- * Ctrl-e, End: go to end of line -- * Ctrl-d, Del: delete character at cursor position+-- * Meta-d: delete word at cursor position -- * Backspace: delete character prior to cursor position -- * Ctrl-k: delete all from cursor to end of line+-- * Ctrl-u: delete all from cursor to beginning of line+-- * Ctrl-t: transpose character before cursor with the one at cursor position+-- * Meta-b: move one word to the left+-- * Ctrl-b: move one character to the left+-- * Meta-f: move one word to the right+-- * Ctrl-f: move one character to the right -- * Arrow keys: move cursor -- * Enter: break the current line at the cursor position-data Editor =- Editor { editContents :: Z.TextZipper String+-- * Paste: Bracketed Pastes from the terminal will be pasted, provided+-- the incoming data is UTF-8-encoded.+data Editor t n =+ Editor { editContents :: Z.TextZipper t -- ^ The contents of the editor- , editDrawContents :: [String] -> Widget- -- ^ The function the editor uses to draw its contents- , editorName :: Name+ , editorName :: n -- ^ The name of the editor } suffixLenses ''Editor -instance HandleEvent Editor where- handleEvent e ed =- let f = case e of- EvKey (KChar 'a') [MCtrl] -> Z.gotoBOL- EvKey (KChar 'e') [MCtrl] -> Z.gotoEOL- EvKey (KChar 'd') [MCtrl] -> Z.deleteChar- EvKey (KChar 'k') [MCtrl] -> Z.killToEOL- EvKey KEnter [] -> Z.breakLine- EvKey KDel [] -> Z.deleteChar- EvKey (KChar c) [] | c /= '\t' -> Z.insertChar c- EvKey KUp [] -> Z.moveUp- EvKey KDown [] -> Z.moveDown- EvKey KLeft [] -> Z.moveLeft- EvKey KRight [] -> Z.moveRight- EvKey KBS [] -> Z.deletePrevChar- _ -> id- in return $ applyEdit f ed+instance (Show t, Show n) => Show (Editor t n) where+ show e =+ concat [ "Editor { "+ , "editContents = " <> show (editContents e)+ , ", editorName = " <> show (editorName e)+ , "}"+ ] --- | Construct an editor.-editor :: Name+instance Named (Editor t n) n where+ getName = editorName++-- | Values that can be constructed by decoding bytestrings in UTF-8+-- encoding.+class DecodeUtf8 t where+ -- | Decode a bytestring assumed to be text in UTF-8 encoding. If+ -- the decoding fails, return 'Left'. This must not raise+ -- exceptions.+ decodeUtf8 :: BS.ByteString -> Either String t++instance DecodeUtf8 T.Text where+ decodeUtf8 bs = case T.decodeUtf8' bs of+ Left e -> Left $ show e+ Right t -> Right t++instance DecodeUtf8 String where+ decodeUtf8 bs = T.unpack <$> decodeUtf8 bs++handleEditorEvent :: (Eq n, DecodeUtf8 t, Eq t, Z.GenericTextZipper t)+ => BrickEvent n e+ -> EventM n (Editor t n) ()+handleEditorEvent e = do+ ed <- get+ let f = case e of+ VtyEvent ev ->+ handleVtyEvent ev+ MouseDown n _ _ (Location pos) | n == getName ed ->+ Z.moveCursorClosest (swap pos)+ _ -> id+ handleVtyEvent ev = case ev of+ EvPaste bs -> case decodeUtf8 bs of+ Left _ -> id+ Right t -> Z.insertMany t+ EvKey (KChar 'a') [MCtrl] -> Z.gotoBOL+ EvKey (KChar 'e') [MCtrl] -> Z.gotoEOL+ EvKey (KChar 'd') [MCtrl] -> Z.deleteChar+ EvKey (KChar 'd') [MMeta] -> Z.deleteWord+ EvKey (KChar 'k') [MCtrl] -> Z.killToEOL+ EvKey (KChar 'u') [MCtrl] -> Z.killToBOL+ EvKey KEnter [] -> Z.breakLine+ EvKey KDel [] -> Z.deleteChar+ EvKey (KChar c) [] | c /= '\t' -> Z.insertChar c+ EvKey KUp [] -> Z.moveUp+ EvKey KDown [] -> Z.moveDown+ EvKey KLeft [] -> Z.moveLeft+ EvKey KRight [] -> Z.moveRight+ EvKey (KChar 'b') [MCtrl] -> Z.moveLeft+ EvKey (KChar 'f') [MCtrl] -> Z.moveRight+ EvKey (KChar 'b') [MMeta] -> Z.moveWordLeft+ EvKey (KChar 'f') [MMeta] -> Z.moveWordRight+ EvKey KBS [] -> Z.deletePrevChar+ EvKey (KChar 't') [MCtrl] -> Z.transposeChars+ EvKey KHome [] -> Z.gotoBOL+ EvKey KEnd [] -> Z.gotoEOL+ EvKey (KChar '<') [MMeta] -> Z.gotoBOF+ EvKey (KChar '>') [MMeta] -> Z.gotoEOF+ _ -> id+ put $ applyEdit f ed++-- | Construct an editor over 'Text' values+editorText :: n+ -- ^ The editor's name (must be unique)+ -> Maybe Int+ -- ^ The limit on the number of lines in the editor ('Nothing'+ -- means no limit)+ -> T.Text+ -- ^ The initial content+ -> Editor T.Text n+editorText = editor++-- | Construct an editor over generic text values+editor :: Z.GenericTextZipper a+ => n -- ^ The editor's name (must be unique)- -> ([String] -> Widget)- -- ^ The content rendering function -> Maybe Int -- ^ The limit on the number of lines in the editor ('Nothing' -- means no limit)- -> String+ -> a -- ^ The initial content- -> Editor-editor name draw limit s = Editor (Z.stringZipper [s] limit) draw name+ -> Editor a n+editor name limit s = Editor (Z.textZipper (Z.lines s) limit) name --- | Apply an editing operation to the editor's contents. Bear in mind--- that you should only apply zipper operations that operate on the--- current line; the editor will only ever render the first line of--- text.-applyEdit :: (Z.TextZipper String -> Z.TextZipper String)- -- ^ The 'Data.Text.Zipper' editing transformation to apply- -> Editor- -> Editor+-- | Apply an editing operation to the editor's contents.+--+-- This is subject to the restrictions of the underlying text zipper;+-- for example, if the underlying zipper has a line limit configured,+-- any edits applied here will be ignored if they edit text outside+-- the line limit.+applyEdit :: (Z.TextZipper t -> Z.TextZipper t)+ -- ^ The 'Z.TextZipper' editing transformation to apply+ -> Editor t n+ -> Editor t n applyEdit f e = e & editContentsL %~ f --- | The attribute assigned to the editor+-- | The attribute assigned to the editor when it does not have focus. editAttr :: AttrName-editAttr = "edit"+editAttr = attrName "edit" +-- | The attribute assigned to the editor when it has focus. Extends+-- 'editAttr'.+editFocusedAttr :: AttrName+editFocusedAttr = editAttr <> attrName "focused"+ -- | Get the contents of the editor.-getEditContents :: Editor -> [String]+getEditContents :: Monoid t => Editor t n -> [t] getEditContents e = Z.getText $ e^.editContentsL --- | Turn an editor state value into a widget-renderEditor :: Editor -> Widget-renderEditor e =- let cp = Z.cursorPosition $ e^.editContentsL- cursorLoc = Location (cp^._2, cp^._1)+-- | Get the cursor position of the editor (row, column).+getCursorPosition :: Editor t n -> (Int, Int)+getCursorPosition e = Z.cursorPosition $ e^.editContentsL++-- | Turn an editor state value into a widget. This uses the editor's+-- name for its scrollable viewport handle and the name is also used to+-- report mouse events.+renderEditor :: (Ord n, Show n, Monoid t, TextWidth t, Z.GenericTextZipper t)+ => ([t] -> Widget n)+ -- ^ The content drawing function+ -> Bool+ -- ^ Whether the editor has focus. It will report a cursor+ -- position if and only if it has focus.+ -> Editor t n+ -- ^ The editor.+ -> Widget n+renderEditor draw foc e =+ let cp = Z.cursorPosition z+ z = e^.editContentsL+ toLeft = Z.take (cp^._2) (Z.currentLine z)+ cursorLoc = Location (textWidth toLeft, cp^._1) limit = case e^.editContentsL.to Z.getLineLimit of Nothing -> id Just lim -> vLimit lim- in withAttr editAttr $+ atChar = charAtCursor $ e^.editContentsL+ atCharWidth = maybe 1 textWidth atChar+ in withAttr (if foc then editFocusedAttr else editAttr) $ limit $ viewport (e^.editorNameL) Both $- showCursor (e^.editorNameL) cursorLoc $- visibleRegion cursorLoc (1, 1) $- e^.editDrawContentsL $+ (if foc then showCursor (e^.editorNameL) cursorLoc else id) $+ visibleRegion cursorLoc (atCharWidth, 1) $+ draw $ getEditContents e++charAtCursor :: (Z.GenericTextZipper t) => Z.TextZipper t -> Maybe t+charAtCursor z =+ let col = snd $ Z.cursorPosition z+ curLine = Z.currentLine z+ toRight = Z.drop col curLine+ in if Z.length toRight > 0+ then Just $ Z.take 1 toRight+ else Nothing
+ src/Brick/Widgets/FileBrowser.hs view
@@ -0,0 +1,948 @@+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE TemplateHaskell #-}+{-# 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+-- this module, see @programs/FileBrowserDemo.hs@.+--+-- To use this module:+--+-- * Embed a 'FileBrowser' in your application state.+-- * Dispatch events to it in your event handler with+-- 'handleFileBrowserEvent'.+-- * Get the entry under the browser's cursor with 'fileBrowserCursor'+-- and get the entries selected by the user with 'Enter' or 'Space'+-- using 'fileBrowserSelection'.+-- * Inspect 'fileBrowserException' to determine whether the+-- file browser encountered an error when reading a directory in+-- 'setWorkingDirectory' or when changing directories in the event+-- handler.+--+-- File browsers have a built-in user-configurable function to limit the+-- entries displayed that defaults to showing all files. For example,+-- an application might want to limit the browser to just directories+-- and XML files. That is accomplished by setting the filter with+-- 'setFileBrowserEntryFilter' and some examples are provided in this+-- module: 'fileTypeMatch' and 'fileExtensionMatch'.+--+-- File browsers are styled using the provided collection of attribute+-- names, so add those to your attribute map to get the appearance you+-- want. File browsers also make use of a 'List' internally, so the+-- 'List' attributes will affect how the list appears.+--+-- File browsers catch 'IOException's when changing directories. If a+-- call to 'setWorkingDirectory' triggers an 'IOException' while reading+-- the working directory, the resulting 'IOException' is stored in the+-- file browser and is accessible with 'fileBrowserException'. The+-- 'setWorkingDirectory' function clears the exception field if the+-- working directory is read successfully. The caller is responsible for+-- deciding when and whether to display the exception to the user. In+-- the event that an 'IOException' is raised as described here, the file+-- browser will always present @..@ as a navigation option to allow the+-- user to continue navigating up the directory tree. It does this even+-- if the current or parent directory does not exist or cannot be read,+-- so it is always safe to present a file browser for any working+-- directory. Bear in mind that the @..@ entry is always subjected to+-- filtering and searching.+module Brick.Widgets.FileBrowser+ ( -- * Types+ FileBrowser+ , FileInfo(..)+ , FileStatus(..)+ , FileType(..)++ -- * Making a new file browser+ , newFileBrowser+ , selectNonDirectories+ , selectDirectories++ -- * Manipulating a file browser's state+ , setWorkingDirectory+ , getWorkingDirectory+ , updateFileBrowserSearch+ , setFileBrowserEntryFilter++ -- * Actions+ , actionFileBrowserBeginSearch+ , actionFileBrowserSelectEnter+ , actionFileBrowserSelectCurrent+ , actionFileBrowserToggleCurrent+ , actionFileBrowserListPageUp+ , actionFileBrowserListPageDown+ , actionFileBrowserListHalfPageUp+ , actionFileBrowserListHalfPageDown+ , actionFileBrowserListTop+ , actionFileBrowserListBottom+ , actionFileBrowserListNext+ , actionFileBrowserListPrev++ -- * Handling events+ , handleFileBrowserEvent+ , maybeSelectCurrentEntry++ -- * Rendering+ , renderFileBrowser++ -- * Getting information+ , fileBrowserCursor+ , fileBrowserIsSearching+ , fileBrowserSelection+ , fileBrowserException+ , fileBrowserSelectable+ , fileInfoFileType++ -- * Attributes+ , fileBrowserAttr+ , fileBrowserCurrentDirectoryAttr+ , fileBrowserSelectionInfoAttr+ , fileBrowserSelectedAttr+ , fileBrowserDirectoryAttr+ , fileBrowserBlockDeviceAttr+ , fileBrowserRegularFileAttr+ , fileBrowserCharacterDeviceAttr+ , fileBrowserNamedPipeAttr+ , fileBrowserSymbolicLinkAttr+ , fileBrowserUnixSocketAttr++ -- * Example browser entry filters+ , fileTypeMatch+ , fileExtensionMatch++ -- * Lenses+ , fileBrowserSelectableL+ , fileInfoFilenameL+ , fileInfoSanitizedFilenameL+ , fileInfoFilePathL+ , fileInfoFileStatusL+ , fileInfoLinkTargetTypeL+ , fileStatusSizeL+ , fileStatusFileTypeL++ -- * Getters+ , fileBrowserEntryFilterG+ , fileBrowserWorkingDirectoryG+ , fileBrowserEntriesG+ , fileBrowserLatestResultsG+ , fileBrowserSelectedFilesG+ , fileBrowserNameG+ , fileBrowserSearchStringG+ , fileBrowserExceptionG+ , fileBrowserSelectableG++ -- * Miscellaneous+ , prettyFileSize++ -- * Utilities+ , entriesForDirectory+ , getFileInfo+ )+where++import qualified Control.Exception as E+import Control.Monad (forM, when)+import Control.Monad.IO.Class (liftIO)+import Data.Char (toLower, isPrint)+import Data.Foldable (for_)+import Data.Maybe (fromMaybe, isJust, fromJust)+import qualified Data.Foldable as F+import qualified Data.Text as T+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import Data.Int (Int64)+import Data.List (sortBy, isSuffixOf, dropWhileEnd)+import qualified Data.Set as Set+import qualified Data.Vector as V+import Lens.Micro+import Lens.Micro.Mtl ((%=), use)+import Lens.Micro.TH (lensRules, generateUpdateableOptics)+import qualified Graphics.Vty as Vty+import qualified System.Directory as D+import qualified System.PosixCompat.Files as U+import qualified System.PosixCompat.Types as U+import qualified System.FilePath as FP+import Text.Printf (printf)++import Brick.Types+import Brick.AttrMap (AttrName, attrName)+import Brick.Widgets.Core+import Brick.Widgets.List++-- | A file browser's state. Embed this in your application state and+-- transform it with 'handleFileBrowserEvent' and the functions included+-- in this module.+data FileBrowser n =+ FileBrowser { fileBrowserWorkingDirectory :: FilePath+ , fileBrowserEntries :: List n FileInfo+ , fileBrowserLatestResults :: [FileInfo]+ , fileBrowserSelectedFiles :: Set.Set String+ , fileBrowserName :: n+ , fileBrowserEntryFilter :: Maybe (FileInfo -> Bool)+ , fileBrowserSearchString :: Maybe T.Text+ , fileBrowserException :: Maybe E.IOException+ -- ^ The exception status of the latest directory+ -- change. If 'Nothing', the latest directory change+ -- was successful and all entries were read. Otherwise,+ -- this contains the exception raised by the latest+ -- directory change in case the calling application+ -- needs to inspect or present the error to the user.+ , fileBrowserSelectable :: FileInfo -> Bool+ -- ^ The function that determines what kinds of entries+ -- are selectable with in the event handler. Note that+ -- if this returns 'True' for an entry, an @Enter@ or+ -- @Space@ keypress selects that entry rather than doing+ -- anything else; directory changes can only occur if+ -- this returns 'False' for directories.+ --+ -- 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 =+ FileStatus { fileStatusSize :: Int64+ -- ^ The size, in bytes, of this entry's file.+ , fileStatusFileType :: Maybe FileType+ -- ^ The type of this entry's file, if it could be+ -- determined.+ }+ deriving (Show, Eq)++-- | Information about a file entry in the browser.+data FileInfo =+ FileInfo { fileInfoFilename :: String+ -- ^ The filename of this entry, without its path.+ -- This is not for display purposes; for that, use+ -- 'fileInfoSanitizedFilename'.+ , fileInfoSanitizedFilename :: String+ -- ^ The filename of this entry with out its path,+ -- sanitized of non-printable characters (replaced with+ -- '?'). This is for display purposes only.+ , fileInfoFilePath :: FilePath+ -- ^ The full path to this entry's file.+ , fileInfoFileStatus :: Either E.IOException FileStatus+ -- ^ The file status if it could be obtained, or the+ -- exception that was caught when attempting to read the+ -- file's status.+ , fileInfoLinkTargetType :: Maybe FileType+ -- ^ If this entry is a symlink, this indicates the type of+ -- file the symlink points to, if it could be obtained.+ }+ deriving (Show, Eq)++-- | The type of file entries in the browser.+data FileType =+ RegularFile+ -- ^ A regular disk file.+ | BlockDevice+ -- ^ A block device.+ | CharacterDevice+ -- ^ A character device.+ | NamedPipe+ -- ^ A named pipe.+ | Directory+ -- ^ A directory.+ | SymbolicLink+ -- ^ A symbolic link.+ | UnixSocket+ -- ^ A Unix socket.+ deriving (Read, Show, Eq)++suffixLenses ''FileBrowser+suffixLensesWith "G" (lensRules & generateUpdateableOptics .~ False) ''FileBrowser+suffixLenses ''FileInfo+suffixLenses ''FileStatus++-- | Make a new file browser state. The provided resource name will be+-- used to render the 'List' viewport of the browser.+--+-- By default, the browser will show all files and directories+-- in its working directory. To change that behavior, see+-- 'setFileBrowserEntryFilter'.+newFileBrowser :: (FileInfo -> Bool)+ -- ^ The function used to determine what kinds of entries+ -- can be selected (see 'handleFileBrowserEvent'). A+ -- good default is 'selectNonDirectories'. This can be+ -- changed at 'any time with 'fileBrowserSelectable' or+ -- its 'corresponding lens.+ -> n+ -- ^ The resource name associated with the browser's+ -- entry listing.+ -> Maybe FilePath+ -- ^ The initial working directory that the browser+ -- displays. If not provided, this defaults to the+ -- executable's current working directory.+ -> IO (FileBrowser n)+newFileBrowser selPredicate name mCwd = do+ initialCwd <- FP.normalise <$> case mCwd of+ Just path -> return $ removeTrailingSlash path+ Nothing -> D.getCurrentDirectory++ let b = FileBrowser { fileBrowserWorkingDirectory = initialCwd+ , fileBrowserEntries = list name mempty 1+ , fileBrowserLatestResults = mempty+ , fileBrowserSelectedFiles = mempty+ , fileBrowserName = name+ , fileBrowserEntryFilter = Nothing+ , fileBrowserSearchString = Nothing+ , fileBrowserException = Nothing+ , fileBrowserSelectable = selPredicate+ }++ setWorkingDirectory initialCwd b++-- | Removes any trailing slash(es) from the supplied FilePath (which should+-- indicate a directory). This does not remove a sole slash indicating the root+-- directory.+--+-- This is done because if the FileBrowser is initialized with an initial working+-- directory that ends in a slash, then selecting the "../" entry to move to the+-- parent directory will cause the removal of the trailing slash, but it will not+-- otherwise cause any change, misleading the user into thinking no action was+-- taken (the disappearance of the trailing slash is unlikely to be noticed).+-- All subsequent parent directory selection operations are processed normally,+-- and the 'fileBrowserWorkingDirectory' never ends in a trailing slash+-- thereafter (except at the root directory).+removeTrailingSlash :: FilePath -> FilePath+removeTrailingSlash "/" = "/"+removeTrailingSlash d = dropWhileEnd (== '/') d++-- | A file entry selector that permits selection of all file entries+-- except directories. Use this if you want users to be able to navigate+-- directories in the browser. If you want users to be able to select+-- only directories, use 'selectDirectories'.+selectNonDirectories :: FileInfo -> Bool+selectNonDirectories i =+ case fileInfoFileType i of+ Just Directory -> False+ Just SymbolicLink ->+ case fileInfoLinkTargetType i of+ Just Directory -> False+ _ -> True+ _ -> True++-- | A file entry selector that permits selection of directories+-- only. This prevents directory navigation and only supports directory+-- selection.+selectDirectories :: FileInfo -> Bool+selectDirectories i =+ case fileInfoFileType i of+ Just Directory -> True+ Just SymbolicLink -> fileInfoLinkTargetType i == Just Directory+ _ -> False++-- | Set the filtering function used to determine which entries in+-- the browser's current directory appear in the browser. 'Nothing'+-- indicates no filtering, meaning all entries will be shown. 'Just'+-- indicates a function that should return 'True' for entries that+-- should be permitted to appear.+--+-- Note that this applies the filter after setting it by updating the+-- listed entries to reflect the result of the filter. That is unlike+-- setting the filter with the 'fileBrowserEntryFilterL' lens directly,+-- which just sets the filter but does not (and cannot) update the+-- listed entries.+setFileBrowserEntryFilter :: Maybe (FileInfo -> Bool) -> FileBrowser n -> FileBrowser n+setFileBrowserEntryFilter f b =+ applyFilterAndSearch $ b & fileBrowserEntryFilterL .~ f++-- | Set the working directory of the file browser. This scans the new+-- directory and repopulates the browser while maintaining any active+-- search string and/or entry filtering.+--+-- If the directory scan raises an 'IOException', the exception is+-- stored in the browser and is accessible with 'fileBrowserException'. If+-- no exception is raised, the exception field is cleared. Regardless of+-- whether an exception is raised, @..@ is always presented as a valid+-- option in the browser.+setWorkingDirectory :: FilePath -> FileBrowser n -> IO (FileBrowser n)+setWorkingDirectory path b = do+ entriesResult <- E.try $ entriesForDirectory path++ let (entries, exc) = case entriesResult of+ Left (e::E.IOException) -> ([], Just e)+ Right es -> (es, Nothing)++ allEntries <- if path == "/" then return entries else do+ parentResult <- E.try $ parentOf path+ return $ case parentResult of+ Left (_::E.IOException) -> entries+ Right parent -> parent : entries++ return $ setEntries allEntries b+ & fileBrowserWorkingDirectoryL .~ path+ & fileBrowserExceptionL .~ exc+ & fileBrowserSelectedFilesL .~ mempty++parentOf :: FilePath -> IO FileInfo+parentOf path = getFileInfo ".." $ FP.takeDirectory path++-- | Build a 'FileInfo' for the specified file and path. If an+-- 'IOException' is raised while attempting to get the file information,+-- the 'fileInfoFileStatus' field is populated with the exception.+-- Otherwise it is populated with the 'FileStatus' for the file.+getFileInfo :: String+ -- ^ The name of the file to inspect. This filename is only+ -- used to set the 'fileInfoFilename' and sanitized filename+ -- fields; the actual file to be inspected is referred+ -- to by the second argument. This is decomposed so that+ -- 'FileInfo's can be used to represent information about+ -- entries like @..@, whose display names differ from their+ -- physical paths.+ -> FilePath+ -- ^ The actual full path to the file or directory to+ -- inspect.+ -> IO FileInfo+getFileInfo name = go []+ where+ go history fullPath = do+ filePath <- D.makeAbsolute fullPath+ statusResult <- E.try $ U.getSymbolicLinkStatus filePath++ let stat = do+ status <- statusResult+ let U.COff sz = U.fileSize status+ return FileStatus { fileStatusFileType = fileTypeFromStatus status+ , fileStatusSize = sz+ }++ targetTy <- case fileStatusFileType <$> stat of+ Right (Just SymbolicLink) -> do+ targetPathResult <- E.try $ U.readSymbolicLink filePath+ case targetPathResult of+ Left (_::E.SomeException) -> return Nothing+ Right targetPath ->+ -- Watch out for recursive symlink chains:+ -- if history starts repeating, abort the+ -- symlink following process.+ --+ -- Examples:+ -- $ ln -s foo foo+ --+ -- $ ln -s foo bar+ -- $ ln -s bar foo+ if targetPath `elem` history+ then return Nothing+ else do+ targetInfo <- liftIO $ go (fullPath : history) targetPath+ case fileInfoFileStatus targetInfo of+ Right (FileStatus _ targetTy) -> return targetTy+ _ -> return Nothing+ _ -> return Nothing++ return FileInfo { fileInfoFilename = name+ , fileInfoFilePath = filePath+ , fileInfoSanitizedFilename = sanitizeFilename name+ , fileInfoFileStatus = stat+ , fileInfoLinkTargetType = targetTy+ }++-- | Get the file type for this file info entry. If the file type could+-- not be obtained due to an 'IOException', return 'Nothing'.+fileInfoFileType :: FileInfo -> Maybe FileType+fileInfoFileType i =+ case fileInfoFileStatus i of+ Left _ -> Nothing+ Right stat -> fileStatusFileType stat++-- | Get the working directory of the file browser.+getWorkingDirectory :: FileBrowser n -> FilePath+getWorkingDirectory = fileBrowserWorkingDirectory++setEntries :: [FileInfo] -> FileBrowser n -> FileBrowser n+setEntries es b =+ applyFilterAndSearch $ b & fileBrowserLatestResultsL .~ es++-- | Returns whether the file browser is in search mode, i.e., the mode+-- in which user input affects the browser's active search string and+-- displayed entries. This is used to aid in event dispatching in the+-- calling program.+fileBrowserIsSearching :: FileBrowser n -> Bool+fileBrowserIsSearching b = isJust $ b^.fileBrowserSearchStringL++-- | Get the entries chosen by the user, if any. Entries are chosen by+-- an 'Enter' or 'Space' keypress; if you want the entry under the+-- cursor, use 'fileBrowserCursor'.+fileBrowserSelection :: FileBrowser n -> [FileInfo]+fileBrowserSelection b =+ let getEntry filename = fromJust $ F.find ((== filename) . fileInfoFilename) $ b^.fileBrowserLatestResultsL+ in fmap getEntry $ F.toList $ b^.fileBrowserSelectedFilesL++-- | Modify the file browser's active search string. This causes the+-- browser's displayed entries to change to those in its current+-- directory that match the search string, if any. If a search string+-- is provided, it is matched case-insensitively anywhere in file or+-- directory names.+updateFileBrowserSearch :: (Maybe T.Text -> Maybe T.Text)+ -- ^ The search transformation. 'Nothing'+ -- indicates that search mode should be off;+ -- 'Just' indicates that it should be on and+ -- that the provided search string should be+ -- used.+ -> FileBrowser n+ -- ^ The browser to modify.+ -> FileBrowser n+updateFileBrowserSearch f b =+ let old = b^.fileBrowserSearchStringL+ new = f $ b^.fileBrowserSearchStringL+ oldLen = maybe 0 T.length old+ newLen = maybe 0 T.length new+ in if old == new+ then b+ else if oldLen == newLen+ -- This case avoids a list rebuild and cursor position reset+ -- when the search state isn't *really* changing.+ then b & fileBrowserSearchStringL .~ new+ else applyFilterAndSearch $ b & fileBrowserSearchStringL .~ new++applyFilterAndSearch :: FileBrowser n -> FileBrowser n+applyFilterAndSearch b =+ let filterMatch = fromMaybe (const True) (b^.fileBrowserEntryFilterL)+ searchMatch = maybe (const True)+ (\search i -> T.toLower search `T.isInfixOf` T.pack (toLower <$> fileInfoSanitizedFilename i))+ (b^.fileBrowserSearchStringL)+ match i = filterMatch i && searchMatch i+ matching = filter match $ b^.fileBrowserLatestResultsL+ in b { fileBrowserEntries = list (b^.fileBrowserNameL) (V.fromList matching) 1 }++-- | Generate a textual abbreviation of a file size, e.g. "10.2M" or "12+-- bytes".+prettyFileSize :: Int64+ -- ^ A file size in bytes.+ -> T.Text+prettyFileSize i+ | i >= 2 ^ (40::Int64) = T.pack $ format (i `divBy` (2 ** 40)) <> "T"+ | i >= 2 ^ (30::Int64) = T.pack $ format (i `divBy` (2 ** 30)) <> "G"+ | i >= 2 ^ (20::Int64) = T.pack $ format (i `divBy` (2 ** 20)) <> "M"+ | i >= 2 ^ (10::Int64) = T.pack $ format (i `divBy` (2 ** 10)) <> "K"+ | otherwise = T.pack $ show i <> " bytes"+ where+ format = printf "%0.1f"+ divBy :: Int64 -> Double -> Double+ divBy a b = ((fromIntegral a) :: Double) / b++-- | Build a list of file info entries for the specified directory. This+-- function does not catch any exceptions raised by calling+-- 'makeAbsolute' or 'listDirectory', but it does catch exceptions on+-- a per-file basis. Any exceptions caught when inspecting individual+-- files are stored in the 'fileInfoFileStatus' field of each+-- 'FileInfo'.+--+-- The entries returned are all entries in the specified directory+-- except for @.@ and @..@. Directories are always given first. Entries+-- are sorted in case-insensitive lexicographic order.+--+-- This function is exported for those who want to implement their own+-- file browser using the types in this module.+entriesForDirectory :: FilePath -> IO [FileInfo]+entriesForDirectory rawPath = do+ path <- D.makeAbsolute rawPath++ -- Get all entries except "." and "..", then sort them+ dirContents <- D.listDirectory path++ infos <- forM dirContents $ \f -> do+ getFileInfo f (path FP.</> f)++ let dirsFirst a b = if fileInfoFileType a == Just Directory &&+ fileInfoFileType b == Just Directory+ then compare (toLower <$> fileInfoFilename a)+ (toLower <$> fileInfoFilename b)+ else if fileInfoFileType a == Just Directory &&+ fileInfoFileType b /= Just Directory+ then LT+ else if fileInfoFileType b == Just Directory &&+ fileInfoFileType a /= Just Directory+ then GT+ else compare (toLower <$> fileInfoFilename a)+ (toLower <$> fileInfoFilename b)++ allEntries = sortBy dirsFirst infos++ return allEntries++fileTypeFromStatus :: U.FileStatus -> Maybe FileType+fileTypeFromStatus s =+ if | U.isBlockDevice s -> Just BlockDevice+ | U.isCharacterDevice s -> Just CharacterDevice+ | U.isNamedPipe s -> Just NamedPipe+ | U.isRegularFile s -> Just RegularFile+ | U.isDirectory s -> Just Directory+ | U.isSocket s -> Just UnixSocket+ | U.isSymbolicLink s -> Just SymbolicLink+ | otherwise -> Nothing++-- | Get the file information for the file under the cursor, if any.+fileBrowserCursor :: FileBrowser n -> Maybe FileInfo+fileBrowserCursor b = snd <$> listSelectedElement (b^.fileBrowserEntriesL)++-- | Handle a Vty input event. Note that event handling can+-- cause a directory change so the caller should be aware that+-- 'fileBrowserException' may need to be checked after handling an+-- event in case an exception was triggered while scanning the working+-- directory.+--+-- Events handled regardless of mode:+--+-- * @Ctrl-b@: 'actionFileBrowserListPageUp'+-- * @Ctrl-f@: 'actionFileBrowserListPageDown'+-- * @Ctrl-d@: 'actionFileBrowserListHalfPageDown'+-- * @Ctrl-u@: 'actionFileBrowserListHalfPageUp'+-- * @Ctrl-n@: 'actionFileBrowserListNext'+-- * @Ctrl-p@: 'actionFileBrowserListPrev'+--+-- Events handled only in normal mode:+--+-- * @/@: 'actionFileBrowserBeginSearch'+-- * @Enter@: 'actionFileBrowserSelectEnter'+-- * @Space@: 'actionFileBrowserToggleCurrent'+-- * @g@: 'actionFileBrowserListTop'+-- * @G@: 'actionFileBrowserListBottom'+-- * @j@: 'actionFileBrowserListNext'+-- * @k@: 'actionFileBrowserListPrev'+--+-- Events handled only in search mode:+--+-- * @Esc@, @Ctrl-C@: cancel search mode+-- * Text input: update search string++actionFileBrowserBeginSearch :: EventM n (FileBrowser n) ()+actionFileBrowserBeginSearch =+ modify $ updateFileBrowserSearch (const $ Just "")++actionFileBrowserSelectEnter :: EventM n (FileBrowser n) ()+actionFileBrowserSelectEnter =+ maybeSelectCurrentEntry++actionFileBrowserSelectCurrent :: EventM n (FileBrowser n) ()+actionFileBrowserSelectCurrent =+ selectCurrentEntry++actionFileBrowserToggleCurrent :: EventM n (FileBrowser n) ()+actionFileBrowserToggleCurrent =+ toggleCurrentEntrySelected++actionFileBrowserListPageUp :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListPageUp =+ zoom fileBrowserEntriesL listMovePageUp++actionFileBrowserListPageDown :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListPageDown =+ zoom fileBrowserEntriesL listMovePageDown++actionFileBrowserListHalfPageUp :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListHalfPageUp =+ zoom fileBrowserEntriesL (listMoveByPages (-0.5::Double))++actionFileBrowserListHalfPageDown :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListHalfPageDown =+ zoom fileBrowserEntriesL (listMoveByPages (0.5::Double))++actionFileBrowserListTop :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListTop =+ fileBrowserEntriesL %= listMoveTo 0++actionFileBrowserListBottom :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListBottom = do+ b <- get+ let sz = length (listElements $ b^.fileBrowserEntriesL)+ fileBrowserEntriesL %= listMoveTo (sz - 1)++actionFileBrowserListNext :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListNext =+ fileBrowserEntriesL %= listMoveBy 1++actionFileBrowserListPrev :: Ord n => EventM n (FileBrowser n) ()+actionFileBrowserListPrev =+ fileBrowserEntriesL %= listMoveBy (-1)++handleFileBrowserEvent :: (Ord n) => Vty.Event -> EventM n (FileBrowser n) ()+handleFileBrowserEvent e = do+ b <- get+ if fileBrowserIsSearching b+ then handleFileBrowserEventSearching e+ else handleFileBrowserEventNormal e++safeInit :: T.Text -> T.Text+safeInit t | T.length t == 0 = t+ | otherwise = T.init t++handleFileBrowserEventSearching :: (Ord n) => Vty.Event -> EventM n (FileBrowser n) ()+handleFileBrowserEventSearching e =+ case e of+ Vty.EvKey (Vty.KChar 'c') [Vty.MCtrl] ->+ modify $ updateFileBrowserSearch (const Nothing)+ Vty.EvKey Vty.KEsc [] ->+ modify $ updateFileBrowserSearch (const Nothing)+ Vty.EvKey Vty.KBS [] ->+ modify $ updateFileBrowserSearch (fmap safeInit)+ Vty.EvKey Vty.KEnter [] -> do+ maybeSelectCurrentEntry+ modify $ updateFileBrowserSearch (const Nothing)+ Vty.EvKey (Vty.KChar c) [] ->+ modify $ updateFileBrowserSearch (fmap (flip T.snoc c))+ _ ->+ handleFileBrowserEventCommon e++handleFileBrowserEventNormal :: (Ord n) => Vty.Event -> EventM n (FileBrowser n) ()+handleFileBrowserEventNormal e =+ case e of+ Vty.EvKey (Vty.KChar '/') [] ->+ -- Begin file search+ actionFileBrowserBeginSearch+ Vty.EvKey Vty.KEnter [] ->+ -- Select file or enter directory+ actionFileBrowserSelectEnter+ Vty.EvKey (Vty.KChar ' ') [] ->+ -- Toggle selected status of current entry+ actionFileBrowserToggleCurrent+ _ ->+ handleFileBrowserEventCommon e++handleFileBrowserEventCommon :: (Ord n) => Vty.Event -> EventM n (FileBrowser n) ()+handleFileBrowserEventCommon e =+ case e of+ Vty.EvKey (Vty.KChar 'b') [Vty.MCtrl] ->+ actionFileBrowserListPageUp+ Vty.EvKey (Vty.KChar 'f') [Vty.MCtrl] ->+ actionFileBrowserListPageDown+ Vty.EvKey (Vty.KChar 'd') [Vty.MCtrl] ->+ actionFileBrowserListHalfPageDown+ Vty.EvKey (Vty.KChar 'u') [Vty.MCtrl] ->+ actionFileBrowserListHalfPageUp+ Vty.EvKey (Vty.KChar 'g') [] ->+ actionFileBrowserListTop+ Vty.EvKey (Vty.KChar 'G') [] ->+ actionFileBrowserListBottom+ Vty.EvKey (Vty.KChar 'j') [] ->+ actionFileBrowserListNext+ Vty.EvKey (Vty.KChar 'k') [] ->+ actionFileBrowserListPrev+ Vty.EvKey (Vty.KChar 'n') [Vty.MCtrl] ->+ actionFileBrowserListNext+ Vty.EvKey (Vty.KChar 'p') [Vty.MCtrl] ->+ actionFileBrowserListPrev+ _ ->+ zoom fileBrowserEntriesL $ handleListEvent e++toggleSelected :: FileInfo -> EventM n (FileBrowser n) ()+toggleSelected e = do+ sel <- fileBrowserIsSelected e+ if sel+ then fileBrowserRemoveSelected e+ else fileBrowserAddSelected e++fileBrowserIsSelected :: FileInfo -> EventM n (FileBrowser n) Bool+fileBrowserIsSelected e = do+ fs <- use fileBrowserSelectedFilesL+ let fName = fileInfoFilename e+ return $ Set.member fName fs++fileBrowserAddSelected :: FileInfo -> EventM n (FileBrowser n) ()+fileBrowserAddSelected e = do+ let fName = fileInfoFilename e+ fileBrowserSelectedFilesL %= Set.insert fName++fileBrowserRemoveSelected :: FileInfo -> EventM n (FileBrowser n) ()+fileBrowserRemoveSelected e = do+ let fName = fileInfoFilename e+ fileBrowserSelectedFilesL %= Set.delete fName++-- | If the browser's current entry is selectable according to+-- @fileBrowserSelectable@, add it to the selection set and return.+-- If not, and if the entry is a directory or a symlink targeting a+-- directory, set the browser's current path to the selected directory.+--+-- Otherwise, return the browser state unchanged.+maybeSelectCurrentEntry :: EventM n (FileBrowser n) ()+maybeSelectCurrentEntry = do+ b <- get+ for_ (fileBrowserCursor b) $ \entry ->+ if fileBrowserSelectable b entry+ then fileBrowserAddSelected entry+ else when (selectDirectories entry) $+ put =<< liftIO (setWorkingDirectory (fileInfoFilePath entry) b)++selectCurrentEntry :: EventM n (FileBrowser n) ()+selectCurrentEntry = do+ b <- get+ for_ (fileBrowserCursor b) $ \entry ->+ when (fileBrowserSelectable b entry) $+ fileBrowserAddSelected entry++toggleCurrentEntrySelected :: EventM n (FileBrowser n) ()+toggleCurrentEntrySelected = do+ b <- get+ for_ (fileBrowserCursor b) $ \entry ->+ when (fileBrowserSelectable b entry) $+ toggleSelected entry++-- | Render a file browser. This renders a list of entries in the+-- working directory, a cursor to select from among the entries, a+-- header displaying the working directory, and a footer displaying+-- information about the selected entry.+--+-- Note that if the most recent file browser operation produced an+-- exception in 'fileBrowserException', that exception is not rendered+-- by this function. That exception needs to be rendered (if at all) by+-- the calling application.+--+-- The file browser is greedy in both dimensions.+renderFileBrowser :: (Show n, Ord n)+ => Bool+ -- ^ Whether the file browser has input focus.+ -> FileBrowser n+ -- ^ The browser to render.+ -> Widget n+renderFileBrowser foc b =+ let maxFilenameLength = maximum $ length . fileInfoFilename <$> (b^.fileBrowserEntriesL)+ cwdHeader = padRight Max $+ str $ sanitizeFilename $ fileBrowserWorkingDirectory b+ selInfo = case listSelectedElement (b^.fileBrowserEntriesL) of+ Nothing -> vLimit 1 $ fill ' '+ Just (_, i) -> padRight Max $ selInfoFor i+ fileTypeLabel Nothing = "unknown"+ fileTypeLabel (Just t) =+ case t of+ RegularFile -> "file"+ BlockDevice -> "block device"+ CharacterDevice -> "character device"+ NamedPipe -> "pipe"+ Directory -> "directory"+ SymbolicLink -> "symbolic link"+ UnixSocket -> "socket"+ selInfoFor i =+ let label = case fileInfoFileStatus i of+ Left _ -> "unknown"+ Right stat ->+ let maybeSize = if fileStatusFileType stat == Just RegularFile+ then ", " <> prettyFileSize (fileStatusSize stat)+ else ""+ in fileTypeLabel (fileStatusFileType stat) <> maybeSize+ in txt $ T.pack (fileInfoSanitizedFilename i) <> ": " <> label++ maybeSearchInfo = case b^.fileBrowserSearchStringL of+ Nothing -> emptyWidget+ Just s -> padRight Max $+ txt "Search: " <+>+ showCursor (b^.fileBrowserNameL) (Location (T.length s, 0)) (txt s)++ in withDefAttr fileBrowserAttr $+ vBox [ withDefAttr fileBrowserCurrentDirectoryAttr cwdHeader+ , renderList (renderFileInfo foc maxFilenameLength (b^.fileBrowserSelectedFilesL) (b^.fileBrowserNameL))+ foc (b^.fileBrowserEntriesL)+ , maybeSearchInfo+ , withDefAttr fileBrowserSelectionInfoAttr selInfo+ ]++renderFileInfo :: Bool -> Int -> Set.Set String -> n -> Bool -> FileInfo -> Widget n+renderFileInfo foc maxLen selFiles n listSel info =+ (if foc+ then (if listSel then forceAttr listSelectedFocusedAttr+ else if sel then forceAttr fileBrowserSelectedAttr else id)+ else (if listSel then forceAttr listSelectedAttr+ else if sel then forceAttr fileBrowserSelectedAttr else id)) $+ padRight Max body+ where+ sel = fileInfoFilename info `Set.member` selFiles+ addAttr = maybe id (withDefAttr . attrForFileType) (fileInfoFileType info)+ body = addAttr (hLimit (maxLen + 1) $+ padRight Max $+ (if foc && listSel then putCursor n (Location (0,0)) else id) $+ str $ fileInfoSanitizedFilename info <> suffix)+ suffix = (if fileInfoFileType info == Just Directory then "/" else "") <>+ (if sel then "*" else "")++-- | Sanitize a filename for terminal display, replacing non-printable+-- characters with '?'.+sanitizeFilename :: String -> String+sanitizeFilename = fmap toPrint+ where+ toPrint c | isPrint c = c+ | otherwise = '?'++attrForFileType :: FileType -> AttrName+attrForFileType RegularFile = fileBrowserRegularFileAttr+attrForFileType BlockDevice = fileBrowserBlockDeviceAttr+attrForFileType CharacterDevice = fileBrowserCharacterDeviceAttr+attrForFileType NamedPipe = fileBrowserNamedPipeAttr+attrForFileType Directory = fileBrowserDirectoryAttr+attrForFileType SymbolicLink = fileBrowserSymbolicLinkAttr+attrForFileType UnixSocket = fileBrowserUnixSocketAttr++-- | The base attribute for all file browser attributes.+fileBrowserAttr :: AttrName+fileBrowserAttr = attrName "fileBrowser"++-- | The attribute used for the current directory displayed at the top+-- of the browser.+fileBrowserCurrentDirectoryAttr :: AttrName+fileBrowserCurrentDirectoryAttr = fileBrowserAttr <> attrName "currentDirectory"++-- | The attribute used for the entry information displayed at the+-- bottom of the browser.+fileBrowserSelectionInfoAttr :: AttrName+fileBrowserSelectionInfoAttr = fileBrowserAttr <> attrName "selectionInfo"++-- | The attribute used to render directory entries.+fileBrowserDirectoryAttr :: AttrName+fileBrowserDirectoryAttr = fileBrowserAttr <> attrName "directory"++-- | The attribute used to render block device entries.+fileBrowserBlockDeviceAttr :: AttrName+fileBrowserBlockDeviceAttr = fileBrowserAttr <> attrName "block"++-- | The attribute used to render regular file entries.+fileBrowserRegularFileAttr :: AttrName+fileBrowserRegularFileAttr = fileBrowserAttr <> attrName "regular"++-- | The attribute used to render character device entries.+fileBrowserCharacterDeviceAttr :: AttrName+fileBrowserCharacterDeviceAttr = fileBrowserAttr <> attrName "char"++-- | The attribute used to render named pipe entries.+fileBrowserNamedPipeAttr :: AttrName+fileBrowserNamedPipeAttr = fileBrowserAttr <> attrName "pipe"++-- | The attribute used to render symbolic link entries.+fileBrowserSymbolicLinkAttr :: AttrName+fileBrowserSymbolicLinkAttr = fileBrowserAttr <> attrName "symlink"++-- | The attribute used to render Unix socket entries.+fileBrowserUnixSocketAttr :: AttrName+fileBrowserUnixSocketAttr = fileBrowserAttr <> attrName "unixSocket"++-- | The attribute used for selected entries in the file browser.+fileBrowserSelectedAttr :: AttrName+fileBrowserSelectedAttr = fileBrowserAttr <> attrName "selected"++-- | A file type filter for use with 'setFileBrowserEntryFilter'. This+-- filter permits entries whose file types are in the specified list.+fileTypeMatch :: [FileType] -> FileInfo -> Bool+fileTypeMatch tys i = maybe False (`elem` tys) $ fileInfoFileType i++-- | A filter that matches any directory regardless of name, or any+-- regular file with the specified extension. For example, an extension+-- argument of @"xml"@ would match regular files @test.xml@ and+-- @TEST.XML@ and it will match directories regardless of name.+--+-- This matcher also matches symlinks if and only if their targets are+-- directories. This is intended to make it possible to use this matcher+-- to find files with certain extensions, but also support directory+-- traversal via symlinks.+fileExtensionMatch :: String -> FileInfo -> Bool+fileExtensionMatch ext i = case fileInfoFileType i of+ Just Directory -> True+ Just RegularFile -> ('.' : (toLower <$> ext)) `isSuffixOf` (toLower <$> fileInfoFilename i)+ Just SymbolicLink -> case fileInfoLinkTargetType i of+ Just Directory -> True+ _ -> False+ _ -> False
src/Brick/Widgets/Internal.hs view
@@ -3,43 +3,197 @@ ( renderFinal , cropToContext , cropResultToContext+ , renderDynBorder+ , renderWidget ) where -import Control.Applicative-import Control.Lens ((^.), (&), (%~))-import Control.Monad.Trans.State.Lazy-import Control.Monad.Trans.Reader-import Data.Default+import Lens.Micro ((^.), (&), (%~), (.~))+import Lens.Micro.Mtl ((%=))+import Control.Monad+import Control.Monad.State.Strict+import Control.Monad.Reader+import Data.Maybe (fromMaybe, mapMaybe)+import qualified Data.Map as M+import qualified Data.Set as S import qualified Graphics.Vty as V import Brick.Types import Brick.Types.Internal import Brick.AttrMap+import Brick.Widgets.Border.Style+import Brick.BorderMap (BorderMap)+import qualified Brick.BorderMap as BM -renderFinal :: AttrMap- -> [Widget]+renderFinal :: (Ord n)+ => AttrMap+ -> [Widget n] -> V.DisplayRegion- -> ([CursorLocation] -> Maybe CursorLocation)- -> RenderState- -> (RenderState, V.Picture, Maybe CursorLocation)-renderFinal aMap layerRenders sz chooseCursor rs = (newRS, pic, theCursor)+ -> ([CursorLocation n] -> Maybe (CursorLocation n))+ -> RenderState n+ -> (RenderState n, V.Picture, Maybe (CursorLocation n), [Extent n])+renderFinal aMap layerRenders (w, h) chooseCursor rs =+ (newRS, picWithBg, theCursor, concat layerExtents) where- (layerResults, !newRS) = flip runState rs $ sequence $+ -- Reset various fields from the last rendering state so they+ -- don't accumulate or affect this rendering.+ resetRs = rs & reportedExtentsL .~ mempty+ & observedNamesL .~ mempty+ & clickableNamesL .~ mempty++ (layerResults, !newRS) = flip runState resetRs $ sequence $ (\p -> runReaderT p ctx) <$>- (render <$> cropToContext <$> layerRenders)- ctx = Context def (fst sz) (snd sz) def aMap- pic = V.picForLayers $ uncurry V.resize sz <$> (^.imageL) <$> layerResults- layerCursors = (^.cursorsL) <$> layerResults+ (\layerWidget -> do+ result <- render $ cropToContext layerWidget+ forM_ (result^.extentsL) $ \e ->+ reportedExtentsL %= M.insert (extentName e) e+ return result+ ) <$> reverse layerRenders++ ctx = Context { ctxAttrName = mempty+ , availWidth = w+ , availHeight = h+ , windowWidth = w+ , windowHeight = h+ , ctxBorderStyle = defaultBorderStyle+ , ctxAttrMap = aMap+ , ctxDynBorders = False+ , ctxVScrollBarOrientation = Nothing+ , ctxVScrollBarRenderer = Nothing+ , ctxHScrollBarOrientation = Nothing+ , ctxHScrollBarRenderer = Nothing+ , ctxHScrollBarShowHandles = False+ , ctxVScrollBarShowHandles = False+ , ctxHScrollBarClickableConstr = Nothing+ , ctxVScrollBarClickableConstr = Nothing+ }++ layersTopmostFirst = reverse layerResults+ pic = V.picForLayers $ V.resize w h <$> (^.imageL) <$> layersTopmostFirst++ -- picWithBg is a workaround for runaway attributes.+ -- See https://github.com/coreyoconnor/vty/issues/95+ picWithBg = pic { V.picBackground = V.Background ' ' V.defAttr }++ layerCursors = (^.cursorsL) <$> layersTopmostFirst+ layerExtents = reverse $ (^.extentsL) <$> layersTopmostFirst theCursor = chooseCursor $ concat layerCursors -- | After rendering the specified widget, crop its result image to the -- dimensions in the rendering context.-cropToContext :: Widget -> Widget+cropToContext :: Widget n -> Widget n cropToContext p =- Widget (hSize p) (vSize p) $ (render p >>= cropResultToContext)+ Widget (hSize p) (vSize p) (render p >>= cropResultToContext) -cropResultToContext :: Result -> RenderM Result+cropResultToContext :: Result n -> RenderM n (Result n) cropResultToContext result = do c <- getContext- return $ result & imageL %~ (V.crop (c^.availWidthL) (c^.availHeightL))+ return $ result & imageL %~ cropImage c+ & cursorsL %~ cropCursors c+ & extentsL %~ cropExtents c+ & bordersL %~ cropBorders c++cropImage :: Context n -> V.Image -> V.Image+cropImage c = V.crop (max 0 $ c^.availWidthL) (max 0 $ c^.availHeightL)++cropCursors :: Context n -> [CursorLocation n] -> [CursorLocation n]+cropCursors ctx cs = mapMaybe cropCursor cs+ where+ -- A cursor location is removed if it is not within the region+ -- described by the context.+ cropCursor c | outOfContext c = Nothing+ | otherwise = Just c+ outOfContext c =+ or [ c^.cursorLocationL.locationRowL < 0+ , c^.cursorLocationL.locationColumnL < 0+ , c^.cursorLocationL.locationRowL >= ctx^.availHeightL+ , c^.cursorLocationL.locationColumnL >= ctx^.availWidthL+ ]++cropExtents :: Context n -> [Extent n] -> [Extent n]+cropExtents ctx es = mapMaybe cropExtent es+ where+ cropExtent (Extent n (Location (c, r)) (w, h)) =+ -- Clamp the original extent's UL corner to the context.+ --+ -- Clamp the original extent's LR corner to the context.+ --+ -- Keep the modified extent (i.e. with clamped corners)+ -- only if the resulting extent has non-zero size in both+ -- dimensions.+ let nonEmpty = nonEmptyH && nonEmptyV+ nonEmptyH = newWidth > 0+ nonEmptyV = newHeight > 0+ newWidth = newEndCol - newStartCol+ newHeight = newEndRow - newStartRow+ (newStartCol, newStartRow) = clampCorner (c, r)+ (newEndCol, newEndRow) = clampCorner (c + w, r + h)+ clampCorner (cols, rows) =+ ( clampRange (ctx^.availWidthL) cols+ , clampRange (ctx^.availHeightL) rows+ )+ clampRange bound val =+ min bound $ max 0 val+ newExtent = Extent n (Location (newStartCol, newStartRow)) (newWidth, newHeight)+ in if nonEmpty+ then Just newExtent+ else Nothing++cropBorders :: Context n -> BorderMap DynBorder -> BorderMap DynBorder+cropBorders ctx = BM.crop Edges+ { eTop = 0+ , eBottom = availHeight ctx - 1+ , eLeft = 0+ , eRight = availWidth ctx - 1+ }++renderDynBorder :: DynBorder -> V.Image+renderDynBorder db = V.char (dbAttr db) $ getBorderChar $ dbStyle db+ where+ getBorderChar = case bsDraw <$> dbSegments db of+ -- top bot left right+ Edges False False False False -> const ' '+ Edges False False _ _ -> bsHorizontal+ Edges _ _ False False -> bsVertical+ Edges False True False True -> bsCornerTL+ Edges False True True False -> bsCornerTR+ Edges True False False True -> bsCornerBL+ Edges True False True False -> bsCornerBR+ Edges False True True True -> bsIntersectT+ Edges True False True True -> bsIntersectB+ Edges True True False True -> bsIntersectL+ Edges True True True False -> bsIntersectR+ Edges True True True True -> bsIntersectFull++-- | This function provides a simplified interface to rendering a list+-- of 'Widget's as a 'V.Picture' outside of the context of an 'App'.+-- This can be useful in a testing setting but isn't intended to be used+-- for normal application rendering. The API is deliberately narrower+-- than the main interactive API and is not yet stable. Use at your own+-- risk.+--+-- Consult the [Vty library documentation](https://hackage.haskell.org/package/vty)+-- for details on how to output the resulting 'V.Picture'.+renderWidget :: (Ord n)+ => Maybe AttrMap+ -- ^ Optional attribute map used to render. If omitted,+ -- an empty attribute map with the terminal's default+ -- attribute will be used.+ -> [Widget n]+ -- ^ The widget layers to render, topmost first.+ -> V.DisplayRegion+ -- ^ The size of the display region in which to render the+ -- layers.+ -> V.Picture+renderWidget mAttrMap layerRenders region = pic+ where+ initialRS = RS { viewportMap = M.empty+ , rsScrollRequests = []+ , observedNames = S.empty+ , renderCache = mempty+ , clickableNames = []+ , requestedVisibleNames_ = S.empty+ , reportedExtents = mempty+ }+ am = fromMaybe (attrMap V.defAttr []) mAttrMap+ (_, pic, _, _) = renderFinal am layerRenders region (const Nothing) initialRS
src/Brick/Widgets/List.hs view
@@ -1,46 +1,100 @@ {-# LANGUAGE TupleSections #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveGeneric #-} -- | This module provides a scrollable list type and functions for -- manipulating and rendering it.+--+-- Note that lenses are provided for direct manipulation purposes, but+-- lenses are *not* safe and should be used with care. (For example,+-- 'listElementsL' permits direct manipulation of the list container+-- without performing bounds checking on the selected index.) If you+-- need a safe API, consider one of the various functions for list+-- manipulation. For example, instead of 'listElementsL', consider+-- 'listReplace'. module Brick.Widgets.List- ( List(listElements, listSelected, listName, listItemHeight)+ ( GenericList+ , List - -- * Consructing a list+ -- * Constructing a list , list -- * Rendering a list , renderList+ , renderListWithIndex + -- * Handling events+ , handleListEvent+ , handleListEventVi+ -- * Lenses , listElementsL , listSelectedL , listNameL , listItemHeightL+ , listSelectedElementL + -- * Accessors+ , listElements+ , listName+ , listSelectedElement+ , listSelected+ , listItemHeight+ -- * Manipulating a list , listMoveBy , listMoveTo+ , listMoveToElement+ , listFindBy , listMoveUp , listMoveDown+ , listMoveByPages+ , listMovePageUp+ , listMovePageDown+ , listMoveToBeginning+ , listMoveToEnd , listInsert , listRemove , listReplace- , listSelectedElement+ , listClear+ , listReverse+ , listModify + -- * Querying a list+ , listFindFirst+ -- * Attributes , listAttr , listSelectedAttr+ , listSelectedFocusedAttr++ -- * Classes+ , Splittable(..)+ , Reversible(..) ) where -import Control.Applicative ((<$>))-import Control.Lens ((^.), (&), (.~), _2)-import Data.Monoid ((<>))+import Prelude hiding (reverse, splitAt)++import Control.Applicative ((<|>))+import Data.Foldable (find, toList)+import Control.Monad.State (evalState)++import Lens.Micro (Traversal', (^.), (^?), (&), (.~), (%~), _2, set)+import Data.Functor (($>))+import Data.List.NonEmpty (NonEmpty((:|))) import Data.Maybe (fromMaybe)-import qualified Data.Algorithm.Diff as D-import Graphics.Vty (Event(..), Key(..))+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup (Semigroup, (<>))+#endif+import Data.Semigroup (sconcat)+import qualified Data.Sequence as Seq+import Graphics.Vty (Event(..), Key(..), Modifier(..)) import qualified Data.Vector as V+import GHC.Generics (Generic) import Brick.Types import Brick.Main (lookupViewport)@@ -48,211 +102,593 @@ import Brick.Util (clamp) import Brick.AttrMap --- | List state. Lists have an element type 'e' that is the data stored--- by the list. Lists handle the following events by default:+-- | List state. Lists have a container @t@ of element type @e@ that is+-- the data stored by the list. Internally, Lists handle the following+-- events by default: -- -- * Up/down arrow keys: move cursor of selected item -- * Page up / page down keys: move cursor of selected item by one page -- at a time (based on the number of items shown) -- * Home/end keys: move cursor of selected item to beginning or end of -- list-data List e =- List { listElements :: !(V.Vector e)+--+-- The 'List' type synonym fixes @t@ to 'V.Vector' for compatibility+-- with previous versions of this library.+--+-- For a container type to be usable with 'GenericList', it must have+-- instances of 'Traversable' and 'Splittable'. The following functions+-- impose further constraints:+--+-- * 'listInsert': 'Applicative' and 'Semigroup'+-- * 'listRemove': 'Semigroup'+-- * 'listClear': 'Monoid'+-- * 'listReverse': 'Reversible'+--+data GenericList n t e =+ List { listElements :: !(t e)+ -- ^ The list's sequence of elements. , listSelected :: !(Maybe Int)- , listName :: Name+ -- ^ The list's selected element index, if any.+ , listName :: n+ -- ^ The list's name. , listItemHeight :: Int- }+ -- ^ The height of an individual item in the list.+ } deriving (Functor, Foldable, Traversable, Show, Generic) -suffixLenses ''List+suffixLenses ''GenericList -instance HandleEvent (List e) where- handleEvent e theList = f- where- f = case e of- EvKey KUp [] -> return $ listMoveUp theList- EvKey KDown [] -> return $ listMoveDown theList- EvKey KHome [] -> return $ listMoveTo 0 theList- EvKey KEnd [] -> return $ listMoveTo (V.length $ listElements theList) theList- EvKey KPageDown [] -> do- v <- lookupViewport (theList^.listNameL)- case v of- Nothing -> return theList- Just vp -> return $ listMoveBy (vp^.vpSize._2 `div` theList^.listItemHeightL) theList- EvKey KPageUp [] -> do- v <- lookupViewport (theList^.listNameL)- case v of- Nothing -> return theList- Just vp -> return $ listMoveBy (negate $ vp^.vpSize._2 `div` theList^.listItemHeightL) theList- _ -> return theList+-- | An alias for 'GenericList' specialized to use a 'Vector' as its+-- container type.+type List n e = GenericList n V.Vector e +instance Named (GenericList n t e) n where+ getName = listName++-- | Ordered container types that can be split at a given index. An+-- instance of this class is required for a container type to be usable+-- with 'GenericList'.+class Splittable t where+ {-# MINIMAL splitAt #-}++ -- | Split at the given index. Equivalent to @(take n xs, drop n xs)@+ -- and therefore total.+ splitAt :: Int -> t a -> (t a, t a)++ -- | Slice the structure. Equivalent to @(take n . drop i) xs@ and+ -- therefore total.+ --+ -- The default implementation applies 'splitAt' two times: first to+ -- drop elements leading up to the slice, and again to drop elements+ -- after the slice.+ slice :: Int {- ^ start index -} -> Int {- ^ length -} -> t a -> t a+ slice i n = fst . splitAt n . snd . splitAt i++-- | /O(1)/ 'splitAt'.+instance Splittable V.Vector where+ splitAt = V.splitAt++-- | /O(log(min(i,n-i)))/ 'splitAt'.+instance Splittable Seq.Seq where+ splitAt = Seq.splitAt++-- | Ordered container types where the order of elements can be+-- reversed. Only required if you want to use 'listReverse'.+class Reversible t where+ {-# MINIMAL reverse #-}+ reverse :: t a -> t a++-- | /O(n)/ 'reverse'+instance Reversible V.Vector where+ reverse = V.reverse++-- | /O(n)/ 'reverse'+instance Reversible Seq.Seq where+ reverse = Seq.reverse++-- | Handle events for list cursor movement. Events handled are:+--+-- * Up (up arrow key)+-- * Down (down arrow key)+-- * Page Up (PgUp)+-- * Page Down (PgDown)+-- * Go to first element (Home)+-- * Go to last element (End)+handleListEvent :: (Foldable t, Splittable t, Ord n)+ => Event+ -> EventM n (GenericList n t e) ()+handleListEvent e =+ case e of+ EvKey KUp [] -> modify listMoveUp+ EvKey KDown [] -> modify listMoveDown+ EvKey KHome [] -> modify listMoveToBeginning+ EvKey KEnd [] -> modify listMoveToEnd+ EvKey KPageDown [] -> listMovePageDown+ EvKey KPageUp [] -> listMovePageUp+ _ -> return ()++-- | Enable list movement with the vi keys with a fallback handler if+-- none match. Use 'handleListEventVi' in place of 'handleListEvent'+-- to add the vi keys bindings to the standard ones. Movements handled+-- include:+--+-- * Up (k)+-- * Down (j)+-- * Page Up (Ctrl-b)+-- * Page Down (Ctrl-f)+-- * Half Page Up (Ctrl-u)+-- * Half Page Down (Ctrl-d)+-- * Go to first element (g)+-- * Go to last element (G)+handleListEventVi :: (Foldable t, Splittable t, Ord n)+ => (Event -> EventM n (GenericList n t e) ())+ -- ^ Fallback event handler to use if none of the vi keys+ -- match.+ -> Event+ -> EventM n (GenericList n t e) ()+handleListEventVi fallback e =+ case e of+ EvKey (KChar 'k') [] -> modify listMoveUp+ EvKey (KChar 'j') [] -> modify listMoveDown+ EvKey (KChar 'g') [] -> modify listMoveToBeginning+ EvKey (KChar 'G') [] -> modify listMoveToEnd+ EvKey (KChar 'f') [MCtrl] -> listMovePageDown+ EvKey (KChar 'b') [MCtrl] -> listMovePageUp+ EvKey (KChar 'd') [MCtrl] -> listMoveByPages (0.5::Double)+ EvKey (KChar 'u') [MCtrl] -> listMoveByPages (-0.5::Double)+ _ -> fallback e++-- | Move the list selection to the first element in the list.+listMoveToBeginning :: (Foldable t, Splittable t)+ => GenericList n t e+ -> GenericList n t e+listMoveToBeginning = listMoveTo 0++-- | Move the list selection to the last element in the list.+listMoveToEnd :: (Foldable t, Splittable t)+ => GenericList n t e+ -> GenericList n t e+listMoveToEnd l = listMoveTo (max 0 $ length (listElements l) - 1) l+ -- | The top-level attribute used for the entire list. listAttr :: AttrName-listAttr = "list"+listAttr = attrName "list" --- | The attribute used only for the currently-selected list item.--- Extends 'listAttr'.+-- | The attribute used only for the currently-selected list item when+-- the list does not have focus. Extends 'listAttr'. listSelectedAttr :: AttrName-listSelectedAttr = listAttr <> "selected"+listSelectedAttr = listAttr <> attrName "selected" --- | Construct a list in terms of an element type 'e'.-list :: Name+-- | The attribute used only for the currently-selected list item when+-- the list has focus. Extends 'listSelectedAttr'.+listSelectedFocusedAttr :: AttrName+listSelectedFocusedAttr = listSelectedAttr <> attrName "focused"++-- | Construct a list in terms of container 't' with element type 'e'.+list :: (Foldable t)+ => n -- ^ The list name (must be unique)- -> V.Vector e+ -> t e -- ^ The initial list contents -> Int -- ^ The list item height in rows (all list item widgets must be- -- this high)- -> List e+ -- this high).+ -> GenericList n t e list name es h =- let selIndex = if V.null es then Nothing else Just 0- in List es selIndex name h+ let selIndex = if null es then Nothing else Just 0+ safeHeight = max 1 h+ in List es selIndex name safeHeight --- | Turn a list state value into a widget given an item drawing--- function.-renderList :: List e -> (Bool -> e -> Widget) -> Widget-renderList l drawElem =+-- | Render a list using the specified item drawing function.+--+-- Evaluates the underlying container up to, and a bit beyond, the+-- selected element. The exact amount depends on available height+-- for drawing and 'listItemHeight'. At most, it will evaluate up to+-- element @(i + h + 1)@ where @i@ is the selected index and @h@ is the+-- available height.+--+-- Note that this function renders the list with the 'listAttr' as+-- the default attribute and then uses 'listSelectedAttr' as the+-- default attribute for the selected item if the list is not focused+-- or 'listSelectedFocusedAttr' otherwise. This is provided as a+-- convenience so that the item rendering function doesn't have to be+-- concerned with attributes, but if those attributes are undesirable+-- for your purposes, 'forceAttr' can always be used by the item+-- rendering function to ensure that another attribute is used instead.+renderList :: (Traversable t, Splittable t, Ord n, Show n)+ => (Bool -> e -> Widget n)+ -- ^ Rendering function, True for the selected element+ -> Bool+ -- ^ Whether the list has focus+ -> GenericList n t e+ -- ^ The List to be rendered+ -> Widget n+ -- ^ rendered widget+renderList drawElem = renderListWithIndex $ const drawElem++-- | Like 'renderList', except the render function is also provided with+-- the index of each element.+--+-- Has the same evaluation characteristics as 'renderList'.+renderListWithIndex :: (Traversable t, Splittable t, Ord n, Show n)+ => (Int -> Bool -> e -> Widget n)+ -- ^ Rendering function, taking index, and True for+ -- the selected element+ -> Bool+ -- ^ Whether the list has focus+ -> GenericList n t e+ -- ^ The List to be rendered+ -> Widget n+ -- ^ rendered widget+renderListWithIndex drawElem foc l = withDefAttr listAttr $- drawListElements l drawElem+ drawListElements foc l drawElem -drawListElements :: List e -> (Bool -> e -> Widget) -> Widget-drawListElements l drawElem =+imap :: (Traversable t) => (Int -> a -> b) -> t a -> t b+imap f xs =+ let act = traverse (\a -> get >>= \i -> put (i + 1) $> f i a) xs+ in evalState act 0++-- | Draws the list elements.+--+-- Evaluates the underlying container up to, and a bit beyond, the+-- selected element. The exact amount depends on available height+-- for drawing and 'listItemHeight'. At most, it will evaluate up to+-- element @(i + h + 1)@ where @i@ is the selected index and @h@ is the+-- available height.+drawListElements :: (Traversable t, Splittable t, Ord n, Show n)+ => Bool+ -> GenericList n t e+ -> (Int -> Bool -> e -> Widget n)+ -> Widget n+drawListElements foc l drawElem = Widget Greedy Greedy $ do c <- getContext - let es = V.slice start num (l^.listElementsL)- idx = case l^.listSelectedL of- Nothing -> 0- Just i -> i+ -- Take (numPerHeight * 2) elements, or whatever is left+ let es = slice start (numPerHeight * 2) (l^.listElementsL) + idx = fromMaybe 0 (l^.listSelectedL)+ start = max 0 $ idx - numPerHeight + 1- num = min (numPerHeight * 2) (V.length (l^.listElementsL) - start)- numPerHeight = (c^.availHeightL) `div` (l^.listItemHeightL) + -- The number of items to show is the available height+ -- divided by the item height...+ initialNumPerHeight = (c^.availHeightL) `div` (l^.listItemHeightL)+ -- ... but if the available height leaves a remainder of+ -- an item height then we need to ensure that we render an+ -- extra item to show a partial item at the top or bottom to+ -- give the expected result when an item is more than one+ -- row high. (Example: 5 rows available with item height+ -- of 3 yields two items: one fully rendered, the other+ -- rendered with only its top 2 or bottom 2 rows visible,+ -- depending on how the viewport state changes.)+ numPerHeight = initialNumPerHeight ++ if initialNumPerHeight * (l^.listItemHeightL) == c^.availHeightL+ then 0+ else 1+ off = start * (l^.listItemHeightL) - drawnElements = (flip V.imap) es $ \i e ->- let isSelected = Just (i + start) == l^.listSelectedL- elemWidget = drawElem isSelected e+ drawnElements = flip imap es $ \i e ->+ let j = i + start+ isSelected = Just j == l^.listSelectedL+ elemWidget = drawElem j isSelected e+ selItemAttr = if foc+ then withDefAttr listSelectedFocusedAttr+ else withDefAttr listSelectedAttr makeVisible = if isSelected- then (visible . withDefAttr listSelectedAttr)+ then visible . selItemAttr else id in makeVisible elemWidget render $ viewport (l^.listNameL) Vertical $ translateBy (Location (0, off)) $- vBox $ V.toList drawnElements+ vBox $ toList drawnElements -- | Insert an item into a list at the specified position.-listInsert :: Int+--+-- Complexity: the worse of 'splitAt' and `<>` for the container type.+--+-- @+-- listInsert for 'List': O(n)+-- listInsert for 'Seq.Seq': O(log(min(i, length n - i)))+-- @+listInsert :: (Splittable t, Applicative t, Semigroup (t e))+ => Int -- ^ The position at which to insert (0 <= i <= size) -> e -- ^ The element to insert- -> List e- -> List e+ -> GenericList n t e+ -> GenericList n t e listInsert pos e l =- let safePos = clamp 0 (V.length es) pos- es = l^.listElementsL+ let es = l^.listElementsL newSel = case l^.listSelectedL of- Nothing -> 0- Just s -> if safePos < s- then s + 1- else s- (front, back) = V.splitAt safePos es+ Nothing -> 0+ Just s -> if pos <= s+ then s + 1+ else s+ (front, back) = splitAt pos es in l & listSelectedL .~ Just newSel- & listElementsL .~ (front V.++ (e `V.cons` back))+ & listElementsL .~ sconcat (front :| [pure e, back]) -- | Remove an element from a list at the specified position.-listRemove :: Int- -- ^ The position at which to remove an element (0 <= i < size)- -> List e- -> List e-listRemove pos l | V.null (l^.listElementsL) = l- | pos /= clamp 0 (V.length (l^.listElementsL) - 1) pos = l+--+-- Applies 'splitAt' two times: first to split the structure at the+-- given position, and again to remove the first element from the tail.+-- Consider the asymptotics of `splitAt` for the container type when+-- using this function.+--+-- Complexity: the worse of 'splitAt' and `<>` for the container type.+--+-- @+-- listRemove for 'List': O(n)+-- listRemove for 'Seq.Seq': O(log(min(i, n - i)))+-- @+listRemove :: (Splittable t, Foldable t, Semigroup (t e))+ => Int+ -- ^ The position at which to remove an element (0 <= i <+ -- size)+ -> GenericList n t e+ -> GenericList n t e+listRemove pos l | null l = l+ | pos /= splitClamp l pos = l | otherwise = let newSel = case l^.listSelectedL of- Nothing -> 0- Just s -> if pos == 0- then 0- else if pos == s- then pos - 1- else if pos < s- then s - 1- else s- (front, back) = V.splitAt pos es- es' = front V.++ V.tail back+ Nothing -> 0+ Just s | pos == 0 -> 0+ | pos == s -> pos - 1+ | pos < s -> s - 1+ | otherwise -> s+ (front, rest) = splitAt pos es+ (_, back) = splitAt 1 rest+ es' = front <> back es = l^.listElementsL- in l & listSelectedL .~ (if V.null es' then Nothing else Just newSel)+ in l & listSelectedL .~ (if null es' then Nothing else Just newSel) & listElementsL .~ es' --- | Replace the contents of a list with a new set of elements but--- preserve the currently selected index.-listReplace :: Eq e => V.Vector e -> List e -> List e-listReplace es' l | es' == l^.listElementsL = l- | otherwise =- let sel = fromMaybe 0 (l^.listSelectedL)- getNewSel es = case (V.null es, V.null es') of- (_, True) -> Nothing- (True, False) -> Just 0- (False, False) -> Just (maintainSel (V.toList es) (V.toList es') sel)- newSel = getNewSel (l^.listElementsL)-- in l & listSelectedL .~ newSel- & listElementsL .~ es'+-- | Replace the contents of a list with a new set of elements and+-- update the new selected index. If the list is empty, empty selection+-- is used instead. Otherwise, if the specified selected index (via+-- 'Just') is not in the list bounds, zero is used instead.+--+-- Complexity: same as 'splitAt' for the container type.+listReplace :: (Foldable t, Splittable t)+ => t e+ -> Maybe Int+ -> GenericList n t e+ -> GenericList n t e+listReplace es idx l =+ let l' = l & listElementsL .~ es+ newSel = if null es then Nothing else inBoundsOrZero <$> idx+ inBoundsOrZero i+ | i == splitClamp l' i = i+ | otherwise = 0+ in l' & listSelectedL .~ newSel -- | Move the list selected index up by one. (Moves the cursor up, -- subtracts one from the index.)-listMoveUp :: List e -> List e+listMoveUp :: (Foldable t, Splittable t)+ => GenericList n t e+ -> GenericList n t e listMoveUp = listMoveBy (-1) +-- | Move the list selected index up by one page.+listMovePageUp :: (Foldable t, Splittable t, Ord n)+ => EventM n (GenericList n t e) ()+listMovePageUp = listMoveByPages (-1::Double)+ -- | Move the list selected index down by one. (Moves the cursor down, -- adds one to the index.)-listMoveDown :: List e -> List e+listMoveDown :: (Foldable t, Splittable t)+ => GenericList n t e+ -> GenericList n t e listMoveDown = listMoveBy 1 --- | Move the list selected index by the specified amount, subject to--- validation.-listMoveBy :: Int -> List e -> List e+-- | Move the list selected index down by one page.+listMovePageDown :: (Foldable t, Splittable t, Ord n)+ => EventM n (GenericList n t e) ()+listMovePageDown = listMoveByPages (1::Double)++-- | Move the list selected index by some (fractional) number of pages.+listMoveByPages :: (Foldable t, Splittable t, Ord n, RealFrac m)+ => m+ -> EventM n (GenericList n t e) ()+listMoveByPages pages = do+ theList <- get+ v <- lookupViewport (theList^.listNameL)+ case v of+ Nothing -> return ()+ Just vp -> do+ let nElems = round $ pages * fromIntegral (vp^.vpSize._2) /+ fromIntegral (theList^.listItemHeightL)+ modify $ listMoveBy nElems++-- | Move the list selected index.+--+-- If the current selection is @Just x@, the selection is adjusted by+-- the specified amount. The value is clamped to the extents of the list+-- (i.e. the selection does not "wrap").+--+-- If the current selection is @Nothing@ (i.e. there is no selection)+-- and the direction is positive, set to @Just 0@ (first element),+-- otherwise set to @Just (length - 1)@ (last element).+--+-- Complexity: same as 'splitAt' for the container type.+--+-- @+-- listMoveBy for 'List': O(1)+-- listMoveBy for 'Seq.Seq': O(log(min(i,n-i)))+-- @+listMoveBy :: (Foldable t, Splittable t)+ => Int+ -> GenericList n t e+ -> GenericList n t e listMoveBy amt l =- let newSel = clamp 0 (V.length (l^.listElementsL) - 1) <$> (amt +) <$> (l^.listSelectedL)- in l & listSelectedL .~ newSel+ let target = case l ^. listSelectedL of+ Nothing+ | amt > 0 -> 0+ | otherwise -> length l - 1+ Just i -> max 0 (amt + i) -- don't be negative+ in listMoveTo target l -- | Set the selected index for a list to the specified index, subject -- to validation.-listMoveTo :: Int -> List e -> List e+--+-- If @pos >= 0@, indexes from the start of the list (which gets+-- evaluated up to the target index)+--+-- If @pos < 0@, indexes from the end of the list (which evaluates+-- 'length' of the list).+--+-- Complexity: same as 'splitAt' for the container type.+--+-- @+-- listMoveTo for 'List': O(1)+-- listMoveTo for 'Seq.Seq': O(log(min(i,n-i)))+-- @+listMoveTo :: (Foldable t, Splittable t)+ => Int+ -> GenericList n t e+ -> GenericList n t e listMoveTo pos l =- let len = V.length (l^.listElementsL)- newSel = clamp 0 (len - 1) $ if pos < 0 then (len - pos) else pos- in l & listSelectedL .~ if len > 0- then Just newSel- else Nothing+ let len = length l+ i = if pos < 0 then len - pos else pos+ newSel = splitClamp l i+ in l & listSelectedL .~ if null l then Nothing else Just newSel --- | Return a list's selected element, if any.-listSelectedElement :: List e -> Maybe (Int, e)-listSelectedElement l = do- sel <- l^.listSelectedL- return (sel, (l^.listElementsL) V.! sel)+-- | Split-based clamp that avoids evaluating 'length' of the structure+-- (unless the structure is already fully evaluated).+splitClamp :: (Foldable t, Splittable t) => GenericList n t e -> Int -> Int+splitClamp l i =+ let (_, t) = splitAt i (l ^. listElementsL) -- split at i+ in+ -- If the tail is empty, then the requested index is not in the+ -- list. And because we have already seen the end of the list,+ -- using 'length' will not force unwanted computation.+ --+ -- Otherwise if tail is not empty, then we already know that i+ -- is in the list, so we don't need to know the length+ clamp 0 (if null t then length l - 1 else i) i --- Assuming `xs` is an existing list that we want to update to match the--- state of `ys`. Given a selected index in `xs`, the goal is to compute--- the corresponding index in `ys`.-maintainSel :: (Eq e) => [e] -> [e] -> Int -> Int-maintainSel xs ys sel = let hunks = D.getDiff xs ys- in merge 0 sel hunks+-- | Set the selected index for a list to the index of the first+-- occurrence of the specified element if it is in the list, or leave+-- the list unmodified otherwise.+--+-- /O(n)/. Only evaluates as much of the container as needed.+listMoveToElement :: (Eq e, Foldable t, Splittable t)+ => e+ -> GenericList n t e+ -> GenericList n t e+listMoveToElement e = listFindBy (== e) . set listSelectedL Nothing -merge :: (Eq e) => Int -> Int -> [D.Diff e] -> Int-merge _ sel [] = sel-merge idx sel (h:hs) | idx > sel = sel- | otherwise = case h of- D.Both _ _ -> merge sel (idx + 1) hs+-- | Find the first element in the list that satisfies the specified+-- predicate. If such an element is found, return the resulting index+-- and element.+--+-- /O(n)/.+listFindFirst :: (Semigroup (t e), Splittable t, Traversable t)+ => (e -> Bool)+ -> GenericList n t e+ -> Maybe (Int, e)+listFindFirst f l =+ listSelectedElement $+ listFindBy f $+ set listSelectedL Nothing l - -- element removed in new list- D.First _ -> let newSel = if idx < sel- then sel - 1- else sel- in merge newSel idx hs+-- | Starting from the currently-selected position, attempt to find+-- and select the next element matching the predicate. If there are no+-- matches for the remainder of the list or if the list has no selection+-- at all, the search starts at the beginning. If no matching element is+-- found anywhere in the list, leave the list unmodified.+--+-- /O(n)/. Only evaluates as much of the container as needed.+listFindBy :: (Foldable t, Splittable t)+ => (e -> Bool)+ -> GenericList n t e+ -> GenericList n t e+listFindBy test l =+ let start = maybe 0 (+1) (l ^. listSelectedL)+ (h, t) = splitAt start (l ^. listElementsL)+ tailResult = find (test . snd) . zip [start..] . toList $ t+ headResult = find (test . snd) . zip [0..] . toList $ h+ result = tailResult <|> headResult+ in maybe id (set listSelectedL . Just . fst) result l - -- element added in new list- D.Second _ -> let newSel = if idx <= sel- then sel + 1- else sel- in merge newSel (idx + 1) hs+-- | Traversal that targets the selected element, if any.+--+-- Complexity: depends on usage as well as the list's container type.+--+-- @+-- listSelectedElementL for 'List': O(1) -- preview, fold+-- O(n) -- set, modify, traverse+-- listSelectedElementL for 'Seq.Seq': O(log(min(i, n - i))) -- all operations+-- @+--+listSelectedElementL :: (Splittable t, Traversable t, Semigroup (t e))+ => Traversal' (GenericList n t e) e+listSelectedElementL f l =+ case l ^. listSelectedL of+ Nothing -> pure l+ Just i -> listElementsL go l+ where+ go l' = let (left, rest) = splitAt i l'+ -- middle contains the target element (if any)+ (middle, right) = splitAt 1 rest+ in (\m -> left <> m <> right) <$> (traverse f middle)++-- | Return a list's selected element, if any.+--+-- Only evaluates as much of the container as needed.+--+-- Complexity: same as 'splitAt' for the container type.+--+-- @+-- listSelectedElement for 'List': O(1)+-- listSelectedElement for 'Seq.Seq': O(log(min(i, n - i)))+-- @+listSelectedElement :: (Splittable t, Traversable t, Semigroup (t e))+ => GenericList n t e+ -> Maybe (Int, e)+listSelectedElement l =+ (,) <$> l^.listSelectedL <*> l^?listSelectedElementL++-- | Remove all elements from the list and clear the selection.+--+-- /O(1)/+listClear :: (Monoid (t e)) => GenericList n t e -> GenericList n t e+listClear l = l & listElementsL .~ mempty & listSelectedL .~ Nothing++-- | Reverse the list. The element selected before the reversal will+-- again be the selected one.+--+-- Complexity: same as 'reverse' for the container type.+--+-- @+-- listReverse for 'List': O(n)+-- listReverse for 'Seq.Seq': O(n)+-- @+listReverse :: (Reversible t, Foldable t)+ => GenericList n t e+ -> GenericList n t e+listReverse l =+ l & listElementsL %~ reverse+ & listSelectedL %~ fmap (length l - 1 -)++-- | Apply a function to the selected element. If no element is selected+-- the list is not modified.+--+-- Complexity: same as 'traverse' for the container type (typically+-- /O(n)/).+--+-- Complexity: same as 'listSelectedElementL' for the list's container type.+--+-- @+-- listModify for 'List': O(n)+-- listModify for 'Seq.Seq': O(log(min(i, n - i)))+-- @+--+listModify :: (Traversable t, Splittable t, Semigroup (t e))+ => (e -> e)+ -> GenericList n t e+ -> GenericList n t e+listModify f = listSelectedElementL %~ f
src/Brick/Widgets/ProgressBar.hs view
@@ -1,15 +1,21 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} -- | This module provides a progress bar widget. module Brick.Widgets.ProgressBar ( progressBar+ , customProgressBar -- * Attributes , progressCompleteAttr , progressIncompleteAttr ) where -import Control.Lens ((^.))+import Lens.Micro ((^.))+import Data.Maybe (fromMaybe)+#if !(MIN_VERSION_base(4,11,0)) import Data.Monoid+#endif+import Graphics.Vty (safeWcswidth) import Brick.Types import Brick.AttrMap@@ -17,11 +23,11 @@ -- | The attribute of the completed portion of the progress bar. progressCompleteAttr :: AttrName-progressCompleteAttr = "progressComplete"+progressCompleteAttr = attrName "progressComplete" -- | The attribute of the incomplete portion of the progress bar. progressIncompleteAttr :: AttrName-progressIncompleteAttr = "progressIncomplete"+progressIncompleteAttr = attrName "progressIncomplete" -- | Draw a progress bar with the specified (optional) label and -- progress value. This fills available horizontal space and is one row@@ -31,19 +37,48 @@ -- the progress bar. -> Float -- ^ The progress value. Should be between 0 and 1 inclusive.- -> Widget-progressBar mLabel progress =+ -> Widget n+progressBar = customProgressBar ' ' ' '++-- | Draw a progress bar with the specified (optional) label,+-- progress value and custom characters to fill the progress.+-- This fills available horizontal space and is one row high.+-- Please be aware of using wide characters in Brick,+-- see [Wide Character Support and the TextWidth class](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst#wide-character-support-and-the-textwidth-class)+customProgressBar :: Char+ -- ^ Character to fill the completed part.+ -> Char+ -- ^ Character to fill the incomplete part.+ -> Maybe String+ -- ^ The label. If specified, this is shown in the center of+ -- the progress bar.+ -> Float+ -- ^ The progress value. Should be between 0 and 1 inclusive.+ -> Widget n+customProgressBar completeChar incompleteChar mLabel progress = Widget Greedy Fixed $ do c <- getContext let barWidth = c^.availWidthL- label = maybe "" id mLabel- labelWidth = length label+ label = fromMaybe "" mLabel+ labelWidth = safeWcswidth label spacesWidth = barWidth - labelWidth- leftPart = replicate (spacesWidth `div` 2) ' '- rightPart = replicate (barWidth - (labelWidth + length leftPart)) ' '- fullBar = leftPart <> label <> rightPart+ leftWidth = spacesWidth `div` 2+ rightWidth = barWidth - labelWidth - leftWidth completeWidth = round $ progress * toEnum barWidth- completePart = take completeWidth fullBar- incompletePart = drop completeWidth fullBar++ leftCompleteWidth = min leftWidth completeWidth+ leftIncompleteWidth = leftWidth - leftCompleteWidth+ leftPart = replicate leftCompleteWidth completeChar ++ replicate leftIncompleteWidth incompleteChar+ rightCompleteWidth = max 0 (completeWidth - labelWidth - leftWidth)+ rightIncompleteWidth = rightWidth - rightCompleteWidth+ rightPart = replicate rightCompleteWidth completeChar ++ replicate rightIncompleteWidth incompleteChar++ fullBar = leftPart <> label <> rightPart+ adjustedCompleteWidth = if completeWidth == length fullBar && progress < 1.0+ then completeWidth - 1+ else if completeWidth == 0 && progress > 0.0+ then 1+ else completeWidth+ (completePart, incompletePart) = splitAt adjustedCompleteWidth fullBar render $ (withAttr progressCompleteAttr $ str completePart) <+> (withAttr progressIncompleteAttr $ str incompletePart)
+ src/Brick/Widgets/Table.hs view
@@ -0,0 +1,389 @@+{-# LANGUAGE MultiWayIf #-}+-- | This module provides a table widget that can draw other widgets+-- in a table layout, draw borders between rows and columns, and allow+-- configuration of row and column alignment. To get started, see+-- 'table'.+module Brick.Widgets.Table+ (+ -- * Types+ Table+ , ColumnAlignment(..)+ , RowAlignment(..)+ , TableException(..)++ -- * Construction+ , table++ -- * Configuration+ , alignLeft+ , alignRight+ , alignCenter+ , alignTop+ , alignMiddle+ , alignBottom+ , setColAlignment+ , setRowAlignment+ , setDefaultColAlignment+ , setDefaultRowAlignment+ , surroundingBorder+ , rowBorders+ , columnBorders++ -- * Rendering+ , renderTable++ -- * Low-level API+ , RenderedTableCells(..)+ , BorderConfiguration(..)+ , tableCellLayout+ , addBorders+ , alignColumns+ )+where++import Control.Monad (forM)+import qualified Control.Exception as E+import Data.List (transpose, intersperse, nub)+import qualified Data.Map as M+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid ((<>))+#endif+import Graphics.Vty (imageHeight, imageWidth, charFill)+import Lens.Micro ((^.))++import Brick.Types+import Brick.Widgets.Core+import Brick.Widgets.Center+import Brick.Widgets.Border++-- | Column alignment modes. Use these modes with the alignment+-- functions in this module to configure column alignment behavior.+data ColumnAlignment =+ AlignLeft+ -- ^ Align all cells to the left.+ | AlignCenter+ -- ^ Center the content horizontally in all cells in the column.+ | AlignRight+ -- ^ Align all cells to the right.+ deriving (Eq, Show, Read)++-- | Row alignment modes. Use these modes with the alignment functions+-- in this module to configure row alignment behavior.+data RowAlignment =+ AlignTop+ -- ^ Align all cells to the top.+ | AlignMiddle+ -- ^ Center the content vertically in all cells in the row.+ | AlignBottom+ -- ^ Align all cells to the bottom.+ deriving (Eq, Show, Read)++-- | A table creation exception.+data TableException =+ TEUnequalRowSizes+ -- ^ Rows did not all have the same number of cells.+ | TEInvalidCellSizePolicy+ -- ^ Some cells in the table did not use the 'Fixed' size policy for+ -- both horizontal and vertical sizing.+ deriving (Eq, Show, Read)++instance E.Exception TableException where++-- | A table data structure for widgets of type 'Widget' @n@. Create a+-- table with 'table'.+data Table n =+ Table { columnAlignments :: M.Map Int ColumnAlignment+ , rowAlignments :: M.Map Int RowAlignment+ , tableRows :: [[Widget n]]+ , defaultColumnAlignment :: ColumnAlignment+ , defaultRowAlignment :: RowAlignment+ , tableBorderConfiguration :: BorderConfiguration+ }++-- | A border configuration for a table.+data BorderConfiguration =+ BorderConfiguration { drawSurroundingBorder :: Bool+ , drawRowBorders :: Bool+ , drawColumnBorders :: Bool+ }++-- | Construct a new table.+--+-- The argument is the list of rows with the topmost row first, with+-- each element of the argument list being the contents of the cells in+-- in each column of the respective row, with the leftmost cell first.+--+-- Each row's height is determined by the height of the tallest cell+-- in that row, and each column's width is determined by the width of+-- the widest cell in that column. This means that control over row+-- and column dimensions is a matter of controlling the size of the+-- individual cells, such as by wrapping cell contents in padding,+-- 'fill' and 'hLimit' or 'vLimit', etc. This also means that it is not+-- necessary to explicitly set the width of most table cells because+-- the table will determine the per-row and per-column dimensions by+-- looking at the largest cell contents. In particular, this means+-- that the table's alignment logic only has an effect when a given+-- cell's contents are smaller than the maximum for its row and column,+-- thus giving the table some way to pad the contents to result in the+-- desired alignment.+--+-- By default:+--+-- * All columns are left-aligned. Use the alignment functions in this+-- module to change that behavior.+-- * All rows are top-aligned. Use the alignment functions in this+-- module to change that behavior.+-- * The table will draw borders between columns, between rows, and+-- around the outside of the table. Border-drawing behavior can be+-- configured with the API in this module. Note that tables always draw+-- with 'joinBorders' enabled. If a cell's contents has smart borders+-- but you don't want those borders to connect to the surrounding table+-- borders, wrap the cell's contents with 'freezeBorders'.+--+-- All cells of all rows MUST use the 'Fixed' growth policy for both+-- horizontal and vertical growth. If the argument list contains any+-- cells that use the 'Greedy' policy, this function will raise a+-- 'TableException'.+--+-- All rows MUST have the same number of cells. If not, this function+-- will raise a 'TableException'.+table :: [[Widget n]] -> Table n+table rows =+ if | not allFixed -> E.throw TEInvalidCellSizePolicy+ | not allSameLength -> E.throw TEUnequalRowSizes+ | otherwise -> t+ where+ allSameLength = length (nub (length <$> rows)) <= 1+ allFixed = all fixedRow rows+ fixedRow = all fixedCell+ fixedCell w = hSize w == Fixed && vSize w == Fixed+ t = Table { columnAlignments = mempty+ , rowAlignments = mempty+ , tableRows = rows+ , defaultColumnAlignment = AlignLeft+ , defaultRowAlignment = AlignTop+ , tableBorderConfiguration =+ BorderConfiguration { drawSurroundingBorder = True+ , drawRowBorders = True+ , drawColumnBorders = True+ }+ }++-- | Configure whether the table draws a border on its exterior.+surroundingBorder :: Bool -> Table n -> Table n+surroundingBorder b t =+ t { tableBorderConfiguration = (tableBorderConfiguration t) { drawSurroundingBorder = b } }++-- | Configure whether the table draws borders between its rows.+rowBorders :: Bool -> Table n -> Table n+rowBorders b t =+ t { tableBorderConfiguration = (tableBorderConfiguration t) { drawRowBorders = b } }++-- | Configure whether the table draws borders between its columns.+columnBorders :: Bool -> Table n -> Table n+columnBorders b t =+ t { tableBorderConfiguration = (tableBorderConfiguration t) { drawColumnBorders = b } }++-- | Align the specified column to the right. The argument is the column+-- index, starting with zero. Silently does nothing if the index is out+-- of range.+alignRight :: Int -> Table n -> Table n+alignRight = setColAlignment AlignRight++-- | Align the specified column to the left. The argument is the column+-- index, starting with zero. Silently does nothing if the index is out+-- of range.+alignLeft :: Int -> Table n -> Table n+alignLeft = setColAlignment AlignLeft++-- | Align the specified column to center. The argument is the column+-- index, starting with zero. Silently does nothing if the index is out+-- of range.+alignCenter :: Int -> Table n -> Table n+alignCenter = setColAlignment AlignCenter++-- | Align the specified row to the top. The argument is the row index,+-- starting with zero. Silently does nothing if the index is out of+-- range.+alignTop :: Int -> Table n -> Table n+alignTop = setRowAlignment AlignTop++-- | Align the specified row to the middle. The argument is the row+-- index, starting with zero. Silently does nothing if the index is out+-- of range.+alignMiddle :: Int -> Table n -> Table n+alignMiddle = setRowAlignment AlignMiddle++-- | Align the specified row to bottom. The argument is the row index,+-- starting with zero. Silently does nothing if the index is out of+-- range.+alignBottom :: Int -> Table n -> Table n+alignBottom = setRowAlignment AlignBottom++-- | Set the alignment for the specified column index (starting at+-- zero). Silently does nothing if the index is out of range.+setColAlignment :: ColumnAlignment -> Int -> Table n -> Table n+setColAlignment a col t =+ t { columnAlignments = M.insert col a (columnAlignments t) }++-- | Set the alignment for the specified row index (starting at+-- zero). Silently does nothing if the index is out of range.+setRowAlignment :: RowAlignment -> Int -> Table n -> Table n+setRowAlignment a row t =+ t { rowAlignments = M.insert row a (rowAlignments t) }++-- | Set the default column alignment for columns with no explicitly+-- configured alignment.+setDefaultColAlignment :: ColumnAlignment -> Table n -> Table n+setDefaultColAlignment a t =+ t { defaultColumnAlignment = a }++-- | Set the default row alignment for rows with no explicitly+-- configured alignment.+setDefaultRowAlignment :: RowAlignment -> Table n -> Table n+setDefaultRowAlignment a t =+ t { defaultRowAlignment = a }++-- | Render the table.+renderTable :: Table n -> Widget n+renderTable t =+ joinBorders $+ Widget Fixed Fixed $ do+ tableCellLayout t >>= addBorders >>= render++-- | The result of performing table cell intermediate rendering and+-- layout.+data RenderedTableCells n =+ RenderedTableCells { renderedTableRows :: [[Widget n]]+ -- ^ The table's cells in row-major order.+ , renderedTableColumnWidths :: [Int]+ -- ^ The widths of the table's columns.+ , renderedTableRowHeights :: [Int]+ -- ^ The heights of the table's rows.+ , borderConfiguration :: BorderConfiguration+ -- ^ The border configuration to use.+ }++-- | Augment rendered table cells with borders according to the+-- border configuration accompanying the cells.+addBorders :: RenderedTableCells n -> RenderM n (Widget n)+addBorders r = do+ let cfg = borderConfiguration r+ rows = renderedTableRows r+ rowHeights = renderedTableRowHeights r+ colWidths = renderedTableColumnWidths r++ contentWidth = sum colWidths+ contentHeight = sum rowHeights++ hBorderLength = contentWidth + if drawColumnBorders cfg+ then max (length colWidths - 1) 0+ else 0+ vBorderHeight = contentHeight + if drawRowBorders cfg+ then max (length rowHeights - 1) 0+ else 0+ horizBorder = hLimit hBorderLength hBorder+ vertBorder = vLimit vBorderHeight vBorder++ leftBorder =+ vBox [topLeftCorner, vertBorder, bottomLeftCorner]+ rightBorder =+ vBox [topRightCorner, vertBorder, bottomRightCorner]++ maybeWrap check f =+ if check cfg then f else id+ addSurroundingBorder b =+ leftBorder <+> (horizBorder <=> b <=> horizBorder) <+> rightBorder+ addRowBorders =+ intersperse horizBorder++ rowsWithColumnBorders = (\(h, row) -> hBox $ maybeColumnBorders h row) <$> zip rowHeights rows+ maybeColumnBorders height = maybeIntersperse cfg drawColumnBorders (vLimit height vBorder)+ body = vBox $+ maybeWrap drawRowBorders addRowBorders rowsWithColumnBorders++ return $ maybeWrap drawSurroundingBorder addSurroundingBorder body++tableCellLayout :: Table n -> RenderM n (RenderedTableCells n)+tableCellLayout t = do+ ctx <- getContext+ cellResults <- forM (tableRows t) $ mapM render++ let rowHeights = rowHeight <$> cellResults+ colWidths = colWidth <$> transpose cellResults+ numRows = length rowHeights+ numCols = if length cellResults >= 1+ then length (cellResults !! 0)+ else 0+ allRowAligns = (\i -> M.findWithDefault (defaultRowAlignment t) i (rowAlignments t)) <$>+ [0..numRows - 1]+ allColAligns = (\i -> M.findWithDefault (defaultColumnAlignment t) i (columnAlignments t)) <$>+ [0..numCols - 1]+ rowHeight = maximum . fmap (imageHeight . image)+ colWidth = maximum . fmap (imageWidth . image)++ toW = Widget Fixed Fixed . return+ fillEmptyCell w h result =+ if imageWidth (image result) == 0 && imageHeight (image result) == 0+ then result { image = charFill (ctx^.attrL) ' ' w h }+ else result+ mkRow (vAlign, height, rowCells) =+ let paddedCells = flip map (zip3 allColAligns colWidths rowCells) $ \(hAlign, width, cell) ->+ applyColAlignment width hAlign $+ applyRowAlignment height vAlign $+ toW $+ fillEmptyCell width height cell+ in paddedCells++ let rows = mkRow <$> zip3 allRowAligns rowHeights cellResults++ return $ RenderedTableCells { renderedTableRows = rows+ , renderedTableColumnWidths = colWidths+ , renderedTableRowHeights = rowHeights+ , borderConfiguration = tableBorderConfiguration t+ }++maybeIntersperse :: BorderConfiguration -> (BorderConfiguration -> Bool) -> Widget n -> [Widget n] -> [Widget n]+maybeIntersperse cfg f v | f cfg = intersperse v+ | otherwise = id++topLeftCorner :: Widget n+topLeftCorner = joinableBorder $ Edges False True False True++topRightCorner :: Widget n+topRightCorner = joinableBorder $ Edges False True True False++bottomLeftCorner :: Widget n+bottomLeftCorner = joinableBorder $ Edges True False False True++bottomRightCorner :: Widget n+bottomRightCorner = joinableBorder $ Edges True False True False++-- | Given a "table row" of widgets, align each one according to the+-- list of specified column alignments in columns of the specified+-- widths.+alignColumns :: [ColumnAlignment]+ -- ^ The column alignments to use for each widget,+ -- respectively.+ -> [Int]+ -- ^ The width of each column in terminal columns,+ -- respectively.+ -> [Widget n]+ -- ^ The column cells to align.+ -> [Widget n]+alignColumns as widths cells =+ (\(w, a, c) -> applyColAlignment w a c) <$> zip3 widths as cells++applyColAlignment :: Int -> ColumnAlignment -> Widget n -> Widget n+applyColAlignment width align w =+ hLimit width $ case align of+ AlignLeft -> padRight Max w+ AlignCenter -> hCenter w+ AlignRight -> padLeft Max w++applyRowAlignment :: Int -> RowAlignment -> Widget n -> Widget n+applyRowAlignment rHeight align w =+ vLimit rHeight $ case align of+ AlignTop -> padBottom Max w+ AlignMiddle -> vCenter w+ AlignBottom -> padTop Max w
+ src/Data/IMap.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}+module Data.IMap+ ( IMap+ , Run(..)+ , empty+ , Data.IMap.null+ , singleton+ , insert+ , delete+ , restrict+ , lookup+ , splitLE+ , intersectionWith+ , mapMaybe+ , addToKeys+ , unsafeUnion+ , fromList+ , unsafeRuns+ , unsafeToAscList+ ) where++import Data.List (foldl')+import Data.Monoid+import Data.IntMap.Strict (IntMap)+import GHC.Generics+import Control.DeepSeq+import Prelude hiding (lookup)+import qualified Data.IntMap.Strict as IM++-- | Semantically, 'IMap' and 'IntMap' are identical; but 'IMap' is more+-- efficient when large sequences of contiguous keys are mapped to the same+-- value.+newtype IMap a = IMap { _runs :: IntMap (Run a) } deriving (Show, Functor, Read, Generic, NFData)++{-# INLINE unsafeRuns #-}+-- | This function is unsafe because 'IMap's that compare equal may split their+-- runs into different chunks; consumers must promise that they do not treat+-- run boundaries specially.+unsafeRuns :: IMap a -> IntMap (Run a)+unsafeRuns = _runs++instance Eq a => Eq (IMap a) where+ IMap m == IMap m' = go (IM.toAscList m) (IM.toAscList m') where+ go ((k, Run n a):kvs) ((k', Run n' a'):kvs')+ = k == k' && a == a' && case compare n n' of+ LT -> go kvs ((k'+n, Run (n'-n) a'):kvs')+ EQ -> go kvs kvs'+ GT -> go ((k+n', Run (n-n') a):kvs) kvs'+ go [] [] = True+ go _ _ = False++instance Ord a => Ord (IMap a) where+ compare (IMap m) (IMap m') = go (IM.toAscList m) (IM.toAscList m') where+ go [] [] = EQ+ go [] _ = LT+ go _ [] = GT+ go ((k, Run n a):kvs) ((k', Run n' a'):kvs')+ = compare k k' <> compare a a' <> case compare n n' of+ LT -> go kvs ((k'+n, Run (n'-n) a'):kvs')+ EQ -> go kvs kvs'+ GT -> go ((k+n', Run (n-n') a):kvs) kvs'++-- | Zippy: '(<*>)' combines values at equal keys, discarding any values whose+-- key is in only one of its two arguments.+instance Applicative IMap where+ pure a = IMap . IM.fromDistinctAscList $+ [ (minBound, Run maxBound a)+ , (-1, Run maxBound a)+ , (maxBound-1, Run 2 a)+ ]+ (<*>) = intersectionWith ($)++-- | @Run n a@ represents @n@ copies of the value @a@.+data Run a = Run+ { len :: !Int+ , val :: !a+ } deriving (Eq, Ord, Read, Show, Functor, Generic, NFData)++instance Foldable Run where foldMap f r = f (val r)+instance Traversable Run where sequenceA (Run n v) = Run n <$> v++empty :: IMap a+empty = IMap IM.empty++null :: IMap a -> Bool+null = IM.null . _runs++singleton :: Int -> Run a -> IMap a+singleton k r+ | len r >= 1 = IMap (IM.singleton k r)+ | otherwise = empty++insert :: Int -> Run a -> IMap a -> IMap a+insert k r m+ | len r < 1 = m+ | otherwise = m { _runs = IM.insert k r (_runs (delete k r m)) }++{-# INLINE delete #-}+delete :: Int -> Run ignored -> IMap a -> IMap a+delete k r m+ | len r < 1 = m+ | otherwise = m { _runs = IM.union (_runs lt) (_runs gt) }+ where+ (lt, ge) = splitLE (k-1) m+ (_ , gt) = splitLE (k+len r-1) ge++-- | Given a range of keys (as specified by a starting key and a length for+-- consistency with other functions in this module), restrict the map to keys+-- in that range. @restrict k r m@ is equivalent to @intersectionWith const m+-- (insert k r empty)@ but potentially more efficient.+restrict :: Int -> Run ignored -> IMap a -> IMap a+restrict k r = id+ . snd+ . splitLE (k-1)+ . fst+ . splitLE (k+len r-1)++lookup :: Int -> IMap a -> Maybe a+lookup k m = case IM.lookupLE k (_runs m) of+ Just (k', Run n a) | k < k'+n -> Just a+ _ -> Nothing++-- | @splitLE n m@ produces a tuple @(le, gt)@ where @le@ has all the+-- associations of @m@ where the keys are @<= n@ and @gt@ has all the+-- associations of @m@ where the keys are @> n@.+splitLE :: Int -> IMap a -> (IMap a, IMap a)+splitLE k m = case IM.lookupLE k (_runs m) of+ Nothing -> (empty, m)+ Just (k', r@(Run n _)) -> case (k' + n - 1 <= k, k' == k) of+ (True , False) -> (m { _runs = lt }, m { _runs = gt })+ (True , True ) -> (m { _runs = IM.insert k r lt }, m { _runs = gt })+ (False, _ ) -> ( m { _runs = IM.insert k' r { len = 1 + k - k' } lt' }+ , m { _runs = IM.insert (k+1) r { len = n - 1 - k + k' } gt' }+ )+ where+ (lt', gt') = IM.split k' (_runs m)+ where+ (lt, gt) = IM.split k (_runs m)++-- | Increment all keys by the given amount. This is like+-- 'IM.mapKeysMonotonic', but restricted to partially-applied addition.+addToKeys :: Int -> IMap a -> IMap a+addToKeys n m = m { _runs = IM.mapKeysMonotonic (n+) (_runs m) }++-- TODO: This is pretty inefficient. IntMap offers some splitting functions+-- that should make it possible to be more efficient here (though the+-- implementation would be significantly messier).+intersectionWith :: (a -> b -> c) -> IMap a -> IMap b -> IMap c+intersectionWith f (IMap runsa) (IMap runsb)+ = IMap . IM.fromDistinctAscList $ merge (IM.toAscList runsa) (IM.toAscList runsb)+ where+ merge as@((ka, ra):at) bs@((kb, rb):bt)+ | ka' < kb = merge at bs+ | kb' < ka = merge as bt+ | otherwise = (kc, Run (kc' - kc + 1) vc) : case compare ka' kb' of+ LT -> merge at bs+ EQ -> merge at bt+ GT -> merge as bt+ where+ ka' = ka + len ra - 1+ kb' = kb + len rb - 1+ kc = max ka kb+ kc' = min ka' kb'+ vc = f (val ra) (val rb)+ merge _ _ = []++mapMaybe :: (a -> Maybe b) -> IMap a -> IMap b+mapMaybe f (IMap runs) = IMap (IM.mapMaybe (traverse f) runs)++fromList :: [(Int, Run a)] -> IMap a+fromList = foldl' (\m (k, r) -> insert k r m) empty++-- | This function is unsafe because 'IMap's that compare equal may split their+-- runs into different chunks; consumers must promise that they do not treat+-- run boundaries specially.+unsafeToAscList :: IMap a -> [(Int, Run a)]+unsafeToAscList = IM.toAscList . _runs++-- | This function is unsafe because it assumes there is no overlap between its+-- arguments. That is, in the call @unsafeUnion a b@, the caller must guarantee+-- that if @lookup k a = Just v@ then @lookup k b = Nothing@ and vice versa.+unsafeUnion :: IMap a -> IMap a -> IMap a+unsafeUnion a b = IMap { _runs = _runs a `IM.union` _runs b }
− src/Data/Text/Markup.hs
@@ -1,74 +0,0 @@--- | This module provides an API for "marking up" text with arbitrary--- values. A piece of markup can then be converted to a list of pairs--- representing the sequences of characters assigned the same markup--- value.------ This interface is experimental. Don't use this for your full-file--- syntax highlighter just yet!-module Data.Text.Markup- ( Markup- , markupToList- , markupSet- , fromList- , fromText- , toText- , (@@)- )-where--import Control.Applicative ((<$>))-import Data.Default (Default, def)-import Data.Monoid-import Data.String (IsString(..))-import qualified Data.Text as T---- | Markup with metadata type 'a' assigned to each character.-data Markup a = Markup [(Char, a)]- deriving Show--instance Monoid (Markup a) where- mempty = Markup mempty- mappend (Markup t1) (Markup t2) =- Markup (t1 `mappend` t2)--instance (Default a) => IsString (Markup a) where- fromString = fromText . T.pack---- | Build a piece of markup; assign the specified metadata to every--- character in the specified text.-(@@) :: T.Text -> a -> Markup a-t @@ val = Markup [(c, val) | c <- T.unpack t]---- | Build markup from text with the default metadata.-fromText :: (Default a) => T.Text -> Markup a-fromText = (@@ def)---- | Extract the text from markup, discarding the markup metadata.-toText :: (Eq a) => Markup a -> T.Text-toText = T.concat . (fst <$>) . markupToList---- | Set the metadata for a range of character positions in a piece of--- markup. This is useful for, e.g., syntax highlighting.-markupSet :: (Eq a) => (Int, Int) -> a -> Markup a -> Markup a-markupSet (start, len) val m@(Markup l) = if start < 0 || start + len > length l- then m- else newM- where- newM = Markup $ theHead ++ theNewEntries ++ theTail- (theHead, theLongTail) = splitAt start l- (theOldEntries, theTail) = splitAt len theLongTail- theNewEntries = zip (fst <$> theOldEntries) (repeat val)---- | Convert markup to a list of pairs in which each pair contains the--- longest subsequence of characters having the same metadata.-markupToList :: (Eq a) => Markup a -> [(T.Text, a)]-markupToList (Markup thePairs) = toList thePairs- where- toList [] = []- toList ((ch, val):rest) = (T.pack $ ch : (fst <$> matching), val) : toList remaining- where- (matching, remaining) = break (\(_, v) -> v /= val) rest---- | Convert a list of text and metadata pairs into markup.-fromList :: [(T.Text, a)] -> Markup a-fromList pairs = Markup $ concatMap (\(t, val) -> [(c, val) | c <- T.unpack t]) pairs
+ tests/List.hs view
@@ -0,0 +1,375 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+module List+ ( main+ )+where++import Prelude hiding (reverse, splitAt)++import Data.Foldable (find)+import Data.Function (on)+import qualified Data.List+import Data.Maybe (isNothing)+import Data.Monoid (Endo(..))+import Data.Proxy+#if !(MIN_VERSION_base(4,11,0))+import Data.Semigroup (Semigroup((<>)))+#endif++import qualified Data.Sequence as Seq+import qualified Data.Vector as V+import Lens.Micro+import Test.QuickCheck++import Brick.Util (clamp)+import Brick.Widgets.List++instance (Arbitrary n, Arbitrary a) => Arbitrary (List n a) where+ arbitrary = list <$> arbitrary <*> (V.fromList <$> arbitrary) <*> pure 1++-- List move operations that never modify the underlying list+data ListMoveOp a =+ MoveUp+ | MoveDown+ | MoveBy Int+ | MoveTo Int+ | MoveToElement a+ | FindElement a+ deriving (Show)++instance Arbitrary a => Arbitrary (ListMoveOp a) where+ arbitrary =+ oneof [ pure MoveUp+ , pure MoveDown+ , MoveBy <$> arbitrary+ , MoveTo <$> arbitrary+ , MoveToElement <$> arbitrary+ , FindElement <$> arbitrary+ ]++-- List operations. We don't have "page"-based movement operations+-- because these depend on render context (i.e. effect in EventM)+data ListOp a =+ Insert Int a+ | Remove Int+ | Replace Int [a]+ | Clear+ | Reverse+ | ListMoveOp (ListMoveOp a)+ deriving (Show)++instance Arbitrary a => Arbitrary (ListOp a) where+ arbitrary =+ frequency [ (1, Insert <$> arbitrary <*> arbitrary)+ , (1, Remove <$> arbitrary)+ , (1, Replace <$> arbitrary <*> arbitrary)+ , (1, pure Clear)+ , (1, pure Reverse)+ , (6, arbitrary)+ ]++-- Turn a ListOp into a List endomorphism+op :: Eq a => ListOp a -> List n a -> List n a+op (Insert i a) = listInsert i a+op (Remove i) = listRemove i+op (Replace i xs) =+ -- avoid setting index to Nothing+ listReplace (V.fromList xs) (Just i)+op Clear = listClear+op Reverse = listReverse+op (ListMoveOp mo) = moveOp mo++-- Turn a ListMoveOp into a List endomorphism+moveOp :: (Eq a) => ListMoveOp a -> List n a -> List n a+moveOp MoveUp = listMoveUp+moveOp MoveDown = listMoveDown+moveOp (MoveBy n) = listMoveBy n+moveOp (MoveTo n) = listMoveTo n+moveOp (MoveToElement a) = listMoveToElement a+moveOp (FindElement a) = listFindBy (== a)++applyListOps :: (Foldable t)+ => (op a -> List n a -> List n a)+ -> t (op a)+ -> List n a+ -> List n a+applyListOps f = appEndo . foldMap (Endo . f)++-- | Initial selection is always 0 (or Nothing for empty list)+prop_initialSelection :: [a] -> Bool+prop_initialSelection xs =+ list () (V.fromList xs) 1 ^. listSelectedL ==+ if null xs then Nothing else Just 0++-- list operations keep the selected index in bounds+prop_listOpsMaintainSelectedValid :: (Eq a)+ => [ListOp a]+ -> List n a+ -> Bool+prop_listOpsMaintainSelectedValid ops l =+ let l' = applyListOps op ops l+ in case l' ^. listSelectedL of+ -- either there is no selection and list is empty+ Nothing -> null l'+ -- or the selected index is valid+ Just i -> i >= 0 && i < length l'++-- reversing a list keeps the selected element the same+prop_reverseMaintainsSelectedElement :: (Eq a)+ => [ListOp a]+ -> List n a+ -> Bool+prop_reverseMaintainsSelectedElement ops l =+ -- apply some random list ops to (probably) set a selected element+ let l' = applyListOps op ops l+ l'' = listReverse l'+ in fmap snd (listSelectedElement l') == fmap snd (listSelectedElement l'')++-- reversing maintains size of list+prop_reverseMaintainsSizeOfList :: List n a -> Bool+prop_reverseMaintainsSizeOfList l =+ length l == length (listReverse l)++-- an inserted element may always be found at the given index+-- (when target index is clamped to 0 <= n <= len)+prop_insert :: (Eq a) => Int -> a -> List n a -> Bool+prop_insert i a l =+ let l' = listInsert i a l+ i' = clamp 0 (length l) i+ in listSelectedElement (listMoveTo i' l') == Just (i', a)++-- inserting anywhere always increases size of list by 1+prop_insertSize :: (Eq a) => Int -> a -> List n a -> Bool+prop_insertSize i a l =+ let l' = listInsert i a l+ in length l' == length l + 1++-- inserting an element and moving to it always succeeds and+-- the selected element is the one we inserted.+--+-- The index is not necessarily the index we inserted at, because+-- the element could be present in the original list. So we don't+-- check that.+--+prop_insertMoveTo :: (Eq a) => [ListOp a] -> List n a -> Int -> a -> Bool+prop_insertMoveTo ops l i a =+ let l' = listInsert i a (applyListOps op ops l)+ sel = listSelectedElement (listMoveToElement a l')+ in fmap snd sel == Just a++-- inserting an element and repeatedly seeking it always+-- reaches the element we inserted, at the index where we+-- inserted it.+--+prop_insertFindBy :: (Eq a) => [ListOp a] -> List n a -> Int -> a -> Bool+prop_insertFindBy ops l i a =+ let l' = applyListOps op ops l+ l'' = set listSelectedL Nothing . listInsert i a $ l'+ seeks = converging ((==) `on` (^. listSelectedL)) (listFindBy (== a)) l''+ i' = clamp 0 (length l') i -- we can't have inserted past len+ in (find ((== Just i') . (^. listSelectedL)) seeks >>= listSelectedElement) == Just (i', a)++-- inserting then deleting always yields a list with the original elems+prop_insertRemove :: (Eq a) => Int -> a -> List n a -> Bool+prop_insertRemove i a l =+ let i' = clamp 0 (length l) i+ l' = listInsert i' a l -- pre-clamped+ l'' = listRemove i' l'+ in l'' ^. listElementsL == l ^. listElementsL++-- deleting in-bounds always reduces size of list by 1+-- deleting out-of-bounds never changes list size+prop_remove :: Int -> List n a -> Bool+prop_remove i l =+ let len = length l+ i' = clamp 0 (len - 1) i+ test+ | len > 0 && i == i' = (== len - 1) -- i is in bounds+ | otherwise = (== len) -- i is out of bounds+ in test (length (listRemove i l))++-- deleting an element and re-inserting it at same position+-- gives the original list elements+prop_removeInsert :: (Eq a) => Int -> List n a -> Bool+prop_removeInsert i l =+ let sel = listSelectedElement (listMoveTo i l)+ l' = maybe id (\(i', a) -> listInsert i' a . listRemove i') sel l+ in l' ^. listElementsL == l ^. listElementsL++-- Apply @f@ until @test a (f a) == True@, then return @a@.+converge :: (a -> a -> Bool) -> (a -> a) -> a -> a+converge test f = last . converging test f++-- Apply @f@ until @test a (f a) == True@, returning the start,+-- intermediate and final values as a list.+converging :: (a -> a -> Bool) -> (a -> a) -> a -> [a]+converging test f a+ | test a (f a) = [a]+ | otherwise = a : converging test f (f a)++-- listMoveUp always reaches 0 (or list is empty)+prop_moveUp :: (Eq a) => [ListOp a] -> List n a -> Bool+prop_moveUp ops l =+ let l' = applyListOps op ops l+ l'' = converge ((==) `on` (^. listSelectedL)) listMoveUp l'+ len = length l''+ in maybe (len == 0) (== 0) (l'' ^. listSelectedL)++-- listMoveDown always reaches end of list (or list is empty)+prop_moveDown :: (Eq a) => [ListOp a] -> List n a -> Bool+prop_moveDown ops l =+ let l' = applyListOps op ops l+ l'' = converge ((==) `on` (^. listSelectedL)) listMoveDown l'+ len = length l''+ in maybe (len == 0) (== len - 1) (l'' ^. listSelectedL)++-- move ops never change the list+prop_moveOpsNeverChangeList :: (Eq a) => [ListMoveOp a] -> List n a -> Bool+prop_moveOpsNeverChangeList ops l =+ let l' = applyListOps moveOp ops l+ in l' ^. listElementsL == l ^. listElementsL++-- If the list is empty, empty selection is used.+-- Otherwise, if the specified selected index is not in list bounds,+-- zero is used instead.+prop_replaceSetIndex :: (Eq a)+ => [ListOp a]+ -> List n a+ -> [a]+ -> Int+ -> Bool+prop_replaceSetIndex ops l xs i =+ let v = V.fromList xs+ l' = applyListOps op ops l+ l'' = listReplace v (Just i) l'+ i' = clamp 0 (length v - 1) i+ inBounds = i == i'+ in l'' ^. listSelectedL == case (null v, inBounds) of+ (True, _) -> Nothing+ (False, True) -> Just i+ (False, False) -> Just 0++-- Replacing with no index always clears the index+prop_replaceNoIndex :: (Eq a) => [ListOp a] -> List n a -> [a] -> Bool+prop_replaceNoIndex ops l xs =+ let v = V.fromList xs+ l' = applyListOps op ops l+ in isNothing (listReplace v Nothing l' ^. listSelectedL)++-- | Move the list selected index. If the index is `Just x`, adjust by the+-- specified amount; if it is `Nothing` (i.e. there is no selection) and the+-- direction is positive, set to `Just 0` (first element), otherwise set to+-- `Just (length - 1)` (last element). Subject to validation.+prop_moveByWhenNoSelection :: List n a -> Int -> Property+prop_moveByWhenNoSelection l amt =+ let l' = l & listSelectedL .~ Nothing+ len = length l+ expected = if amt > 0 then 0 else len - 1+ in len > 0 ==> listMoveBy amt l' ^. listSelectedL == Just expected++splitAtLength :: (Foldable t, Splittable t) => t a -> Int -> Bool+splitAtLength l i =+ let len = length l+ (h, t) = splitAt i l+ in length h + length t == len && length h == clamp 0 len i++splitAtAppend :: (Splittable t, Semigroup (t a), Eq (t a))+ => t a -> Int -> Bool+splitAtAppend l i = uncurry (<>) (splitAt i l) == l++prop_splitAtLength_Vector :: [a] -> Int -> Bool+prop_splitAtLength_Vector = splitAtLength . V.fromList++prop_splitAtAppend_Vector :: (Eq a) => [a] -> Int -> Bool+prop_splitAtAppend_Vector = splitAtAppend . V.fromList++prop_splitAtLength_Seq :: [a] -> Int -> Bool+prop_splitAtLength_Seq = splitAtLength . Seq.fromList++prop_splitAtAppend_Seq :: (Eq a) => [a] -> Int -> Bool+prop_splitAtAppend_Seq = splitAtAppend . Seq.fromList+++reverseSingleton :: forall t a. (Reversible t, Applicative t, Eq (t a))+ => Proxy t -> a -> Bool+reverseSingleton _ a =+ let l = pure a :: t a+ in reverse l == l++reverseAppend :: (Reversible t, Semigroup (t a), Eq (t a))+ => t a -> t a -> Bool+reverseAppend l1 l2 =+ reverse (l1 <> l2) == reverse l2 <> reverse l1++prop_reverseSingleton_Vector :: (Eq a) => a -> Bool+prop_reverseSingleton_Vector = reverseSingleton (Proxy :: Proxy V.Vector)++prop_reverseAppend_Vector :: (Eq a) => [a] -> [a] -> Bool+prop_reverseAppend_Vector l1 l2 =+ reverseAppend (V.fromList l1) (V.fromList l2)++prop_reverseSingleton_Seq :: (Eq a) => a -> Bool+prop_reverseSingleton_Seq = reverseSingleton (Proxy :: Proxy Seq.Seq)++prop_reverseAppend_Seq :: (Eq a) => [a] -> [a] -> Bool+prop_reverseAppend_Seq l1 l2 =+ reverseAppend (Seq.fromList l1) (Seq.fromList l2)++-- Laziness tests. Here we create a custom container type+-- that we use to ensure certain operations do not cause the+-- whole container to be evaluated.+--+newtype L a = L [a]+ deriving (Functor, Foldable, Traversable, Semigroup)++instance Splittable L where+ splitAt i (L xs) = over both L (Data.List.splitAt i xs)++-- moveBy positive amount does not evaluate 'length'+prop_moveByPosLazy :: Bool+prop_moveByPosLazy =+ let v = L (1:2:3:4:undefined) :: L Int+ l = list () v 1+ l' = listMoveBy 1 l+ in l' ^. listSelectedL == Just 1++-- listFindBy is lazy+prop_findByLazy :: Bool+prop_findByLazy =+ let v = L (1:2:3:4:undefined) :: L Int+ l = list () v 1 & listSelectedL .~ Nothing+ l' = listFindBy even l+ l'' = listFindBy even l'+ in l' ^. listSelectedL == Just 1 &&+ l'' ^. listSelectedL == Just 3++prop_listFindFirst :: Bool+prop_listFindFirst =+ let v = L [1..5] :: L Int+ l = list () v 1+ result1 = listFindFirst even l+ result2 = listFindFirst (> 10) l+ in result1 == Just (1, 2) &&+ result2 == Nothing++prop_listSelectedElement_lazy :: Bool+prop_listSelectedElement_lazy =+ let v = L (1:2:3:4:undefined) :: L Int+ l = list () v 1 & listSelectedL .~ Just 3+ in listSelectedElement l == Just (3, 4)++prop_listSelectedElementL_lazy :: Bool+prop_listSelectedElementL_lazy =+ let v = L (1:2:3:4:undefined) :: L Int+ l = list () v 1 & listSelectedL .~ Just 3+ in over listSelectedElementL (*2) l ^? listSelectedElementL == Just 8++return []++main :: IO Bool+main = $quickCheckAll
+ tests/Main.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++import Control.Applicative+import Data.Bool (bool)+import Data.Traversable (sequenceA)+import System.Exit (exitFailure, exitSuccess)++import Data.IMap (IMap, Run(Run))+import Data.IntMap (IntMap)+import Test.QuickCheck+import qualified Data.IMap as IMap+import qualified Data.IntMap as IntMap++import qualified List+import qualified Render++instance Arbitrary v => Arbitrary (Run v) where+ arbitrary = liftA2 (\(Positive n) -> Run n) arbitrary arbitrary++instance Arbitrary v => Arbitrary (IMap v) where+ arbitrary = IMap.fromList <$> arbitrary++instance (a ~ Ordering, Show b) => Show (a -> b) where+ show f = show [f x | x <- [minBound .. maxBound]]++lower :: IMap v -> IntMap v+lower m = IntMap.fromDistinctAscList+ [ (base+offset, v)+ | (base, Run n v) <- IMap.unsafeToAscList m+ , offset <- [0..n-1]+ ]++raise :: Eq v => IntMap v -> IMap v+raise = IMap.fromList . rle . map singletonRun . IntMap.toAscList where+ singletonRun (k, v) = (k, Run 1 v)++ rle ((k, Run n v):(k', Run n' v'):kvs)+ | k+n == k' && v == v' = rle ((k, Run (n+n') v):kvs)+ rle (kv:kvs) = kv:rle kvs+ rle [] = []++lowerRun :: Int -> Run v -> IntMap v+lowerRun k r = IntMap.fromAscList [(k+offset, IMap.val r) | offset <- [0..IMap.len r-1]]++type O = Ordering+type I = IMap Ordering++-- These next two probably have overflow bugs that QuickCheck can't reasonably+-- notice. Hopefully they don't come up in real use cases...+prop_raiseLowerFaithful :: IntMap O -> Bool+prop_raiseLowerFaithful m = m == lower (raise m)++prop_equalityReflexive :: I -> Bool+prop_equalityReflexive m = m == raise (lower m)++prop_equality :: I -> I -> Bool+prop_equality l r = (l == r) == (lower l == lower r)++prop_compare :: I -> I -> Bool+prop_compare l r = compare l r == compare (lower l) (lower r)++prop_applicativeIdentity :: I -> Bool+prop_applicativeIdentity v = (id <$> v) == v++prop_applicativeComposition :: IMap (O -> O) -> IMap (O -> O) -> IMap O -> Bool+prop_applicativeComposition u v w = ((.) <$> u <*> v <*> w) == (u <*> (v <*> w))++prop_applicativeHomomorphism :: (O -> O) -> O -> Bool+prop_applicativeHomomorphism f x = (f <$> pure x :: I) == pure (f x)++prop_applicativeInterchange :: IMap (O -> O) -> O -> Bool+prop_applicativeInterchange u y = (u <*> pure y) == (($ y) <$> u)++prop_empty :: Bool+prop_empty = lower (IMap.empty :: I) == IntMap.empty++prop_singleton :: Int -> Run O -> Bool+prop_singleton k r = lower (IMap.singleton k r) == lowerRun k r++prop_insert :: Int -> Run O -> I -> Bool+prop_insert k r m = lower (IMap.insert k r m) == IntMap.union (lowerRun k r) (lower m)++prop_delete :: Int -> Run () -> I -> Bool+prop_delete k r m = lower (IMap.delete k r m) == lower m IntMap.\\ lowerRun k r++prop_splitLE :: Int -> I -> Bool+prop_splitLE k m = (lower le, lower gt) == (le', gt') where+ (le, gt) = IMap.splitLE k m+ (lt, eq, gt') = IntMap.splitLookup k (lower m)+ le' = maybe id (IntMap.insert k) eq lt++prop_intersectionWith :: (O -> O -> O) -> I -> I -> Bool+prop_intersectionWith f l r = lower (IMap.intersectionWith f l r) == IntMap.intersectionWith f (lower l) (lower r)++prop_addToKeys :: Int -> I -> Bool+prop_addToKeys n m = lower (IMap.addToKeys n m) == IntMap.mapKeysMonotonic (n+) (lower m)++prop_lookup :: Int -> I -> Bool+prop_lookup k m = IMap.lookup k m == IntMap.lookup k (lower m)++prop_restrict :: Int -> Run () -> I -> Bool+prop_restrict k r m = lower (IMap.restrict k r m) == IntMap.intersection (lower m) (lowerRun k r)++prop_mapMaybe :: (O -> Maybe O) -> I -> Bool+prop_mapMaybe f m = lower (IMap.mapMaybe f m) == IntMap.mapMaybe f (lower m)++prop_null :: I -> Bool+prop_null m = IMap.null m == IntMap.null (lower m)++return []++main :: IO ()+main =+ (and <$> sequenceA [$quickCheckAll, List.main, Render.main])+ >>= bool exitFailure exitSuccess
+ tests/Render.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE CPP #-}+module Render+ ( main+ )+where++import Brick+import Control.Monad (when)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Graphics.Vty as V+import qualified Graphics.Vty.CrossPlatform.Testing as V+import Brick.Widgets.Border (hBorder)+import Control.Exception (SomeException, try)++region :: V.DisplayRegion+region = (30, 10)++renderDisplay :: Ord n => [Widget n] -> IO ()+renderDisplay ws = do+ outp <- V.mkDefaultOutput+ ctx <- V.displayContext outp region+ V.outputPicture ctx (renderWidget Nothing ws region)+ V.releaseDisplay outp++myWidget :: Widget ()+myWidget = str "Why" <=> hBorder <=> str "not?"++-- Since you can't Read a Picture, we have to compare the result with+-- the Shown one+expectedResult :: String+expectedResult = "Picture {picCursor = NoCursor, picLayers = [VertJoin {partTop = VertJoin {partTop = HorizText {attr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}, displayText = \"Why \", outputWidth = 30, charWidth = 30}, partBottom = VertJoin {partTop = HorizText {attr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}, displayText = \"\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\", outputWidth = 30, charWidth = 30}, partBottom = HorizText {attr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}, displayText = \"not? \", outputWidth = 30, charWidth = 30}, outputWidth = 30, outputHeight = 2}, outputWidth = 30, outputHeight = 3}, partBottom = BGFill {outputWidth = 30, outputHeight = 7}, outputWidth = 30, outputHeight = 10}], picBackground = Background {backgroundChar = ' ', backgroundAttr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}}}"++main :: IO Bool+main = do+ result <- try (renderDisplay [myWidget]) :: IO (Either SomeException ())+ case result of+ Left _ -> do+ putStrLn "Terminal is not available, skipping test"+ -- Even though we could not actually run the test, we return+ -- True here to prevent the absence of a terminal from+ -- causing a test suite failure in an automated context.+ -- This means that this test effectively doesn't get+ -- considered at all in the automated context.+ return True+ Right () -> do+ let matched = actualResult == expectedResult+ actualResult = show (renderWidget Nothing [myWidget] region)+ msg = if matched then "rendering match" else "rendering mismatch"++ putStrLn ""+ putStrLn $ "renderWidget test outcome: " <> msg++ when (not matched) $ do+ putStrLn "Expected result:"+ putStrLn expectedResult++ putStrLn "Actual result:"+ putStrLn actualResult++ return matched