brick-1.9/0000755000000000000000000000000007346545000010646 5ustar0000000000000000brick-1.9/CHANGELOG.md0000644000000000000000000015746007346545000012474 0ustar0000000000000000 Brick changelog --------------- 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 ) * 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 ) Behavior changes: * Honor multiple 'visible' markers in a single viewport with preference on the innermost request (thanks Thomas Strobel ) 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 ) * 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 ) * Fixed a typo in the README (thanks Markus Hauck ) * Improved the renderList documentation (thanks Profpatsch ) * 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 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 ) 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 ) 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 ) * str: only reference string content that can be shown (eliminates a space leak, fixes #14, thanks Sebastian Reuße ) 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 brick-1.9/LICENSE0000644000000000000000000000273307346545000011660 0ustar0000000000000000Copyright (c) 2015-2018, Jonathan Daugherty. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. brick-1.9/README.md0000644000000000000000000002524707346545000012137 0ustar0000000000000000![](logo/brick-final-clearbg-with-text.svg) `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 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), so some knowledge of Vty will be helpful in using this library. Example ------- Here's an example interface (see `programs/ReadmeDemo.hs`): ``` joinBorders $ withBorderStyle unicode $ borderWithLabel (str "Hello!") $ (center (str "Left") <+> vBorder <+> center (str "Right")) ``` Result: ``` ┌─────────Hello!─────────┐ │ │ │ │ │ │ │ Left │ Right │ │ │ │ │ │ │ └───────────┴────────────┘ ``` Featured Projects ----------------- 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 | | ------- | ----------- | | [`tetris`](https://github.com/SamTay/tetris) | An implementation of the Tetris game | | [`gotta-go-fast`](https://github.com/callum-oakley/gotta-go-fast) | A typing tutor | | [`haskell-player`](https://github.com/potomak/haskell-player) | An `afplay` frontend | | [`mushu`](https://github.com/elaye/mushu) | An `MPD` client | | [`matterhorn`](https://github.com/matterhorn-chat/matterhorn) | A client for [Mattermost](https://about.mattermost.com/) | | [`viewprof`](https://github.com/maoe/viewprof) | A GHC profile viewer | | [`tart`](https://github.com/jtdaugherty/tart) | A mouse-driven ASCII art drawing program | | [`silly-joy`](https://github.com/rootmos/silly-joy) | An interpreter for Joy | | [`herms`](https://github.com/jackkiefer/herms) | A command-line tool for managing kitchen recipes | | [`purebred`](https://github.com/purebred-mua/purebred) | A mail user agent | | [`2048Haskell`](https://github.com/8Gitbrix/2048Haskell) | An implementation of the 2048 game | | [`bhoogle`](https://github.com/andrevdm/bhoogle) | A [Hoogle](https://www.haskell.org/hoogle/) client | | [`clifm`](https://github.com/pasqu4le/clifm) | A file manager | | [`towerHanoi`](https://github.com/shajenM/projects/tree/master/towerHanoi) | Animated solutions to The Tower of Hanoi | | [`VOIDSPACE`](https://github.com/ChrisPenner/void-space) | A space-themed typing-tutor game | | [`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 | | [`wrapping-editor`](https://github.com/ta0kira/wrapping-editor) | An embeddable editor with support for Brick | | [`git-brunch`](https://github.com/andys8/git-brunch) | A git branch checkout utility | | [`hascard`](https://github.com/Yvee1/hascard) | A program for reviewing "flash card" notes | | [`ttyme`](https://github.com/evuez/ttyme) | A TUI for [Harvest](https://www.getharvest.com/) | | [`ghcup`](https://www.haskell.org/ghcup/) | A TUI for `ghcup`, the Haskell toolchain manager | | [`cbookview`](https://github.com/mlang/chessIO) | A TUI for exploring polyglot chess opening book files | | [`thock`](https://github.com/rmehri01/thock) | A modern TUI typing game featuring online racing against friends | | [`fifteen`](https://github.com/benjaminselfridge/fifteen) | An implementation of the [15 puzzle](https://en.wikipedia.org/wiki/15_puzzle) | | [`maze`](https://github.com/benjaminselfridge/maze) | A Brick-based maze game | | [`pboy`](https://github.com/2mol/pboy) | A tiny PDF organizer | | [`hyahtzee2`](https://github.com/DamienCassou/hyahtzee2#readme) | Famous Yahtzee dice game | | [`brewsage`](https://github.com/gerdreiss/brewsage#readme) | A TUI for Homebrew | | [`sandwich`](https://codedownio.github.io/sandwich/) | A test framework with a TUI interface | | [`youbrick`](https://github.com/florentc/youbrick) | A feed aggregator and launcher for Youtube channels | | [`swarm`](https://github.com/byorgey/swarm/) | A 2D programming and resource gathering game | | [`hledger-ui`](https://github.com/simonmichael/hledger) | A terminal UI for the hledger accounting system. | | [`hledger-iadd`](http://github.com/rootzlevel/hledger-iadd) | An interactive terminal UI for adding hledger journal entries | | [`wordle`](https://github.com/ivanjermakov/wordle) | An implementation of the Wordle game | | [`kpxhs`](https://github.com/akazukin5151/kpxhs) | An interactive [Keepass](https://github.com/keepassxreboot/keepassxc/) database viewer | | [`htyper`](https://github.com/Simon-Hostettler/htyper) | A typing speed test program | | [`ullekha`](https://github.com/ajithnn/ullekha) | An interactive terminal notes/todo app with file/redis persistence | | [`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 | | [`hic-hac-hoe`](https://github.com/blastwind/hic-hac-hoe) | Play tic tac toe in terminal! | | [`babel-cards`](https://github.com/srhoulam/babel-cards) | A TUI spaced-repetition memorization tool. Similar to Anki. | | [`codenames-haskell`](https://github.com/VigneshN1997/codenames-haskell) | An implementation of the Codenames game | | [`haradict`](https://github.com/srhoulam/haradict) | A TUI Arabic dictionary powered by [ElixirFM](https://github.com/otakar-smrz/elixir-fm) | | [`Giter`](https://gitlab.com/refaelsh/giter) | A UI wrapper around Git CLI inspired by [Magit](https://magit.vc/). | | [`Brickudoku`](https://github.com/Thecentury/brickudoku) | A hybrid of Tetris and Sudoku | These third-party packages also extend `brick`: | Project | Description | | ------- | ----------- | | [`brick-filetree`](https://github.com/ChrisPenner/brick-filetree) [[Hackage]](http://hackage.haskell.org/package/brick-filetree) | A widget for exploring a directory tree and selecting or flagging files and directories | | [`brick-panes`](https://github.com/kquick/brick-panes) [[Hackage]](https://hackage.haskell.org/package/brick-panes) | A Brick overlay library providing composition and isolation of screen areas for TUI apps. | Release Announcements / News ---------------------------- Find out about `brick` releases and other news on Twitter: https://twitter.com/brick_haskell/ Getting Started --------------- Check out the many demo programs to get a feel for different aspects of the library: ``` $ cabal new-build -f demos $ find dist-newstyle -type f -name \*-demo ``` To get started, see the [user guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst). Documentation ------------- Documentation for `brick` comes in a variety of forms: * [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) ([Screenshots](https://github.com/jtdaugherty/brick/blob/master/docs/programs-screenshots.md)) * [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) * 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 ------ 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. `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! 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. - Clearly describe the behavior you expected ... - ... and include a minimal demonstration program that exhibits the behavior you actually observed. Contributing ------------ If you decide to contribute, that's great! Here are some guidelines you should consider to make submitting patches easier for all concerned: - 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. - 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. - New commits should be `-Wall` clean. - 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-1.9/Setup.hs0000644000000000000000000000005607346545000012303 0ustar0000000000000000import Distribution.Simple main = defaultMain brick-1.9/brick.cabal0000644000000000000000000004467707346545000012746 0ustar0000000000000000name: brick version: 1.9 synopsis: A declarative terminal user interface library description: 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 = str "Hello, world!" > > main :: IO () > main = simpleMain ui . . To get started, see: . * . * The . * The demonstration programs in the 'programs' directory . . This package deprecates . license: BSD3 license-file: LICENSE author: Jonathan Daugherty maintainer: Jonathan Daugherty copyright: (c) Jonathan Daugherty 2015-2022 category: Graphics build-type: Simple cabal-version: 1.18 Homepage: https://github.com/jtdaugherty/brick/ Bug-reports: https://github.com/jtdaugherty/brick/issues tested-with: GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.5, GHC == 8.8.4, GHC == 8.10.7, GHC == 9.0.2, GHC == 9.2.4, GHC == 9.4.2 extra-doc-files: README.md, docs/guide.rst, docs/snake-demo.gif, CHANGELOG.md, programs/custom_keys.ini, docs/programs-screenshots.md, docs/programs-screenshots/brick-attr-demo.png, docs/programs-screenshots/brick-border-demo.png, docs/programs-screenshots/brick-cache-demo.png, docs/programs-screenshots/brick-custom-event-demo.png, docs/programs-screenshots/brick-dialog-demo.png, docs/programs-screenshots/brick-dynamic-border-demo.png, docs/programs-screenshots/brick-edit-demo.png, docs/programs-screenshots/brick-file-browser-demo.png, docs/programs-screenshots/brick-fill-demo.png, docs/programs-screenshots/brick-form-demo.png, docs/programs-screenshots/brick-hello-world-demo.png, docs/programs-screenshots/brick-layer-demo.png, docs/programs-screenshots/brick-list-demo.png, docs/programs-screenshots/brick-list-vi-demo.png, docs/programs-screenshots/brick-mouse-demo.png, docs/programs-screenshots/brick-padding-demo.png, docs/programs-screenshots/brick-progressbar-demo.png, docs/programs-screenshots/brick-readme-demo.png, docs/programs-screenshots/brick-suspend-resume-demo.png, docs/programs-screenshots/brick-text-wrap-demo.png, docs/programs-screenshots/brick-theme-demo.png, docs/programs-screenshots/brick-viewport-scroll-demo.png, docs/programs-screenshots/brick-visibility-demo.png Source-Repository head type: git location: git://github.com/jtdaugherty/brick.git Flag demos Description: Build demonstration programs Default: False library default-language: Haskell2010 ghc-options: -Wall -Wcompat -O2 -Wunused-packages default-extensions: CPP hs-source-dirs: src exposed-modules: Brick Brick.AttrMap Brick.BChan Brick.BorderMap Brick.Keybindings Brick.Keybindings.KeyConfig Brick.Keybindings.KeyEvents Brick.Keybindings.KeyDispatcher Brick.Keybindings.Parse Brick.Keybindings.Pretty Brick.Focus Brick.Forms Brick.Main Brick.Themes Brick.Types Brick.Util Brick.Widgets.Border Brick.Widgets.Border.Style Brick.Widgets.Center Brick.Widgets.Core Brick.Widgets.Dialog Brick.Widgets.Edit Brick.Widgets.FileBrowser Brick.Widgets.List Brick.Widgets.ProgressBar Brick.Widgets.Table Data.IMap other-modules: Brick.Types.Common Brick.Types.TH Brick.Types.EventM Brick.Types.Internal Brick.Widgets.Internal build-depends: base >= 4.9.0.0 && < 4.19.0.0, vty >= 5.36, 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, microlens-th, microlens-mtl, mtl, config-ini, vector, stm >= 2.4.3, text, text-zipper >= 0.13, template-haskell, deepseq >= 1.3 && < 1.5, unix, bytestring, word-wrap >= 0.2 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, text, vty 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, text 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 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, text, 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, text, microlens >= 0.3.0.0, microlens-th, mtl executable brick-visibility-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: VisibilityDemo.hs build-depends: base, brick, vty, text, 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, text, microlens, microlens-mtl, microlens-th executable brick-viewport-scroll-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 default-extensions: CPP main-is: ViewportScrollDemo.hs build-depends: base, brick, vty, text, microlens executable brick-dialog-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: DialogDemo.hs build-depends: base, brick, vty, text, microlens 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, text, microlens >= 0.3.0.0, microlens-th, microlens-mtl, text-zipper, mtl executable brick-layer-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: LayerDemo.hs build-depends: base, brick, vty, text, 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 -Wcompat -O2 default-language: Haskell2010 main-is: SuspendAndResumeDemo.hs build-depends: base, brick, vty, text, 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, text, microlens executable brick-padding-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: PaddingDemo.hs build-depends: base, brick, vty, text, microlens 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, text, mtl, microlens executable brick-attr-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: AttrDemo.hs build-depends: base, brick, vty, text, microlens executable brick-tabular-list-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: TabularListDemo.hs build-depends: base, brick, vty, text, microlens >= 0.3.0.0, microlens-mtl, microlens-th, mtl, vector executable brick-list-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: ListDemo.hs build-depends: base, brick, vty, text, 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, text, microlens >= 0.3.0.0, microlens-mtl, mtl, vector executable brick-custom-event-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: CustomEventDemo.hs build-depends: base, brick, vty, text, 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, vty, text, microlens executable brick-hello-world-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: HelloWorldDemo.hs build-depends: base, brick, vty, text, microlens executable brick-edit-demo if !flag(demos) Buildable: False hs-source-dirs: programs ghc-options: -threaded -Wall -Wcompat -O2 default-language: Haskell2010 main-is: EditDemo.hs build-depends: base, brick, vty, text, vector, mtl, 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, text, vector, mtl, 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 -Wcompat -O2 default-extensions: CPP default-language: Haskell2010 main-is: BorderDemo.hs build-depends: base, brick, vty, text, microlens 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, vty, text, microlens 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, text, microlens, 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, QuickCheck brick-1.9/docs/0000755000000000000000000000000007346545000011576 5ustar0000000000000000brick-1.9/docs/guide.rst0000644000000000000000000026045407346545000013440 0ustar0000000000000000Brick User Guide ~~~~~~~~~~~~~~~~ .. contents:: `Table of Contents` Introduction ============ ``brick`` is a Haskell library for programming terminal user interfaces. Its main goal is to make terminal user interface development as painless 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, 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 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 = 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.mkVty Graphics.Vty.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. 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. 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 brick-1.9/docs/programs-screenshots.md0000644000000000000000000000721707346545000016317 0ustar0000000000000000# Demo program screenshots ## [AttrDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/AttrDemo.hs) ![attr demo](./programs-screenshots/brick-attr-demo.png) ## [BorderDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/BorderDemo.hs) ![border demo](./programs-screenshots/brick-border-demo.png) ## [CacheDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/CacheDemo.hs) ![cache demo](./programs-screenshots/brick-cache-demo.png) ## [CustomEventDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/CustomEventDemo.hs) ![custom-event demo](./programs-screenshots/brick-custom-event-demo.png) ## [DialogDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/DialogDemo.hs) ![dialog demo](./programs-screenshots/brick-dialog-demo.png) ## [DynamicBorderDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/DynamicBorderDemo.hs) ![dynamic-border demo](./programs-screenshots/brick-dynamic-border-demo.png) ## [EditDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/EditDemo.hs) ![edit demo](./programs-screenshots/brick-edit-demo.png) ## [FileBrowserDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/FileBrowserDemo.hs) ![file-browser demo](./programs-screenshots/brick-file-browser-demo.png) ## [FillDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/FillDemo.hs) ![fill demo](./programs-screenshots/brick-fill-demo.png) ## [FormDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/FormDemo.hs) ![form demo](./programs-screenshots/brick-form-demo.png) ## [HelloWorldDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/HelloWorldDemo.hs) ![hello-world demo](./programs-screenshots/brick-hello-world-demo.png) ## [LayerDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/LayerDemo.hs) ![layer demo](./programs-screenshots/brick-layer-demo.png) ## [ListDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/ListDemo.hs) ![list demo](./programs-screenshots/brick-list-demo.png) ## [ListViDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/ListViDemo.hs) ![list-vi demo](./programs-screenshots/brick-list-vi-demo.png) ## [MouseDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/MouseDemo.hs) ![mouse demo](./programs-screenshots/brick-mouse-demo.png) ## [PaddingDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/PaddingDemo.hs) ![padding demo](./programs-screenshots/brick-padding-demo.png) ## [ProgressBarDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/ProgressBarDemo.hs) ![progressbar demo](./programs-screenshots/brick-progressbar-demo.png) ## [ReadmeDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/ReadmeDemo.hs) ![readme demo](./programs-screenshots/brick-readme-demo.png) ## [SuspendAndResumeDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/SuspendAndResumeDemo.hs) ![suspend-resume demo](./programs-screenshots/brick-suspend-resume-demo.png) ## [TextWrapDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/TextWrapDemo.hs) ![text-wrap demo](./programs-screenshots/brick-text-wrap-demo.png) ## [ThemeDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/ThemeDemo.hs) ![theme demo](./programs-screenshots/brick-theme-demo.png) ## [ViewportScrollDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/ViewportScrollDemo.hs) ![viewport-scroll demo](./programs-screenshots/brick-viewport-scroll-demo.png) ## [VisibilityDemo.hs](https://github.com/jtdaugherty/brick/blob/master/programs/VisibilityDemo.hs) ![visibility demo](./programs-screenshots/brick-visibility-demo.png) brick-1.9/docs/programs-screenshots/0000755000000000000000000000000007346545000015766 5ustar0000000000000000brick-1.9/docs/programs-screenshots/brick-attr-demo.png0000644000000000000000000014711007346545000021464 0ustar0000000000000000PNG  IHDR=%sBITOtEXtSoftwareShutterc IDATxw|U͖d{#!!!ދTPQ@ŎcA_"*Hkh)${lo3s?B M6x ;;9s?n ;Kj_g*5AV#sa6}@z,ۙ gkZö'M:Jq̬_^0*ƛ%̍m,ܬaQ,MeM_m؛Gx{G F&euT<;m'=e7t%R 7\17ԛ\Cy/4oa9bƍ5J#{8@^%^Q| ./ca\Z$xzv.~\s{68?[CB ;0 cn=;c9ODO1nZ\@.}/1>шm.]vHkmϏ?aԈ?y`e&6j-+9H\?g쨱cy۹Ꮾhn[~?:af4߮_ptEDmc9jo ZcymuI3v/7&B6l| -Em0TSٯ( gAi˟!n`# mp q^}zg;On4V>7Wp8%t Y,[vP64&Bͥjvd(Amo -dgГIP@0u9̠P@tܞ@_4\:qn-;а*p:߆K ^& *;sp0($Y,@}S>Q+^w#w]F,_ʶowڂ><!@r^!"`sD#S?+)2Mṷ , Y$X,$lkvCrsەvLaLפʉח4йqIKK|/^:Żtw&52q</c~8 "7,U_DM6t}UfD,ݵ5_7-zQUf>;޹؂X,"lEQ 5fE1~x9[?29N:t椀ݕX^WDMqmGiT~fyJ!SRM!67h´BcͿgO2_n}P4x{ s }cvŽ(IAȺ.NZV'Jz\+^z[v< Q{qǓǭ=%45յ/}Y.7WJxM50R.' q޾r3_oż2[ް} 8La8i9L斣˛P*2qzSם2~힡o8bs~F@GarLyx K)S2- 3מ: ;]+A:~?m8bϼ5 by-w@M6z&ŗ a*N(t2pXX28tKMCH7$?O\R5)L{>r-gJdTQ~B씴5Sw?W2W6sXV>oܺ%+;pRǖzr Y*@3ka_ZYwLٵSA/ݡ[W?dکuBM=e<"7ۏkNк&аVlvo?Y&`>CM`S5z9dӿB~ҿ,0/V\fSXj9KjoϫcE)pkw#bwL']ئC6R_+rp62\#Y#M RVW1.BLOHl}.X!n/$%%w KzNuDؕxvt}I1v&)>3hߚ6+Pgm[DO[!0呙T >Qm Y?>FG{#W6̻?)gi~O<`ǚN^[mLׇ|Я/ W4zFJH.ٜW<Х+sR<v'v$57]k{'MeTM*e$Qe|W0j{#^#0 &t*n3]_VSe8CÈ\Q%r:ʔy;vKܭ޻KIU,{ ?^$?ǒ>$RD7,׿h 'tkyГ#>.m!iUwP8r>`P<}irɖ=Ydt$TH?jZtb 9Mgt-R<]tǷHa/*k OmaE2ׅ^%KHF斖 ' X["T5# .vwm'G`j-urѽPbT&*`rpV`w+KST+[8f HYy~8\ᶚ[ K6u,T`bMW 6>!§V'Z;K$6%]u=|j4M]?=7So2eǕE/ZP靍6֖E]Oȟ\}ƁC(oYL7[$Y ys$E @p8ݑ]>:"{J,i$45U=K~=N}`q~8^, Ñ!KMM87QCY7KYr}% 4Wzѧy<ޏ=_&(,†gVzIFЗӌv;*b0 N\̽u#N"{FQչ׏@@+]I@@j$7ww~rgo2 %㢈 vk3 [6-Q^ްL<"M<ԟH3R해@p ȵ1Iz^KI?BRpmY%~;:tQ[tTq0 qJju6T|~2Ϩ'HEcKg3Ϻ\9go.;}u%TirX7\xLK(;/4+RH/75^=B:JB%Ͻzz!WE4N|x%ەW?t>i9Q+FGN^Os/IM{j6D->;SD"⦵?Q7+g\/ޚosWOc!xb_LN4KR:iw^5f( 4}5(ި҈¦#®ĒE*:X񇺷ieWĹg*TUm;߅#Qԫ( j.LXV&yK x95^8/dZTFՍ eFP*MnT'.y@ $DDϔ(W8@M,S_8FP SҞ oץШF|g7D| D WOH$\zLVu}(e~!o|BJTmH 7K~0`tB]z/Yq!Ol'_0h39W6`F QBjk?xH@U߾J? |cSw/2VO}&~I)'ݿx7w71%jS*n[dF]y]%/i+>>}3hr6`>]U>[M[=\T,hb8—y¯y_\F]ymJ}K_!LeW[qiG6Z}?N?1 OF1c5kI+jEeΏ͊2\3.)v%yOB%PnObWbI1jb 0 **+~ lba0 i#t^`0.~,`0 MII' 0p,ֵ{q쮚a= ;]2$BoϤ`w%yIMM2:./7755&{&+<}eCzz՘Z>H;X'r00L'S\T,M>,dQm2ދx'mq҄t-*IZXjp]{NokAf=`<n/gXu:CJ$,uw3e  ux˪ ]VA=۪ #\ʪ|Kˍ&, r`\H~KJrcr5w:x.c0.R-pb@0UMv@%[&a|&tx L;j3m/Г.+&`c=&&lQ-[o=En,3^Nے"ɳU|xD  +,`A懍){g ֥YqYS԰f{FOQDfQiL9_:\Nl@!(Xh#}<^ZJTIsLtJɐRwVvl>SXgfM+jo^ tGȆ6caWZDkfhf__0b{BG̖B/o ;պ+m\`0]yu=mӦj@ϛ|5`B;j'*Đ\+{4n+:$QCBaB^͜=WO=# p;4h˫Uo:x{ӵ%_g2F%埜4aqYjXuM3l< ~\PO ɲhwr5Ȩa+zs;k`*H$SQsP ^& E?II4 XA=hR]ϘݨTAnSnZhU5믩d Cfhf_ex]rά2#6 Mfle[t\4Qj/irV|zO^bJuk{YFs~PXі~ɤ \@-\=^G\-vR(+Oq!=[); :qٶйjp 4j"܋[Rck8#-D)IE? -y3 IDAT eՊ]f(7ݻ#YP"Ɯ 5fC㺳ΖvٌRN)S-t<aA}[?92@{k  YP6ʼX4EEꤜe\ pX?qM7iJv>7@ăG[Lӊɑ c<DJC:t)"bXVac\6,t6b[(+ fbKKpe UQ(RٱMaIf,@ y~h(]=IyGI}">:̞xZmVAʕX;km\v,tn׌VdCl~(.˴PNFr$7*C5QP*MT71b5R ח!G5FjiBa!iO( /sUW=5 wL\Vq`pi{e+֓JӳHD~mNEGn~7tXĨfV|gSl?Dѩ^MZkΔWʐVe%PѼ^1܆AiB`O B$˫Rdr- dž=ѠM@8P'Ooj WSs[&J34ENW\6ߒXe Oz}w  iԍ aEee~ܸM,i~0(Y m1۳G7 \`0 K > DԨ~U `04Kx /R_;|* ӉTEhK`0 & `04{V!_,e.>Bhm04|x+ :L`֢>5/n}.5x? n$Aεs@p-vXaIkrФ]?/&F9?۴>.q׼~Rg *׏jHA+Fz{sy^sLw٦N}el)%dA}1E/T7;E"Dkf:]9 gώ}%B}!30] D'*pհ{^&YѮl$fxi t,q"=y@ں6i}/ xdՊ2ՔS`0BӤx(DX(L)M܃$AHfO)3@V!kytI٠iҜM.Ƴ] pC \4G:{TC 1ʪVQ8_APos泺%|;GgsC,~Yץ {OmAF7ڣ;O')>5k1-`X<MN(U΁i=_p5Cuf}9` 5ǝj_^_p `&͉乻V-T]fKcOĿ( W*{D>W0r>cU iذ- E9?T /4=˘w+!3nб X+Ǹs84:Vp"#}7a؀W98b Χn8T|ɒ1szoeWBCkfEk'B2q/,KPԵ!l5c/gO}iIq)RCχں]<.է ௟5?Չ&|>~ZgF1m7XLlNXu|We.!ۋ쬓W楈?޽Q+guX̵<5AF6"[DkTl6zǶz#w騨պaH׏ei>0Reo;J Lz-!f_5ؚ UX&7j:PVh[l K}ugJ/y;6 5[f؈ КP$72XP}W5oѝ~㝙a_#GpAbTů$ÝO5 Fc#̅%Fő fQm>occq~榛r>x4hra#9dc޸s F¨w5i+^٦[>pi7ߛgG?]M5-\d)hJ4B/ƞUqƎ-(^@S5LRME:]knIȧ2hV&L:cEo,m坣MtHFDžMYUXu.^^0(;fo`}Ţ핅4 hMōמ_z {Jd/`6԰*--s,_YY5v@Ѡce| ꌫ>+؝f8Ԡ#rH:_ڔ^ia~2*cp@h=VN}!-F [߃pǍ X(T {F[k}گyֳ5EWL8Zf:hcθ!%Cd(KVDIF%8em:6.}=pi܋y 6 jhoRJ1PIASHR~%rn#8GBO69\d4Eaoź^J0.4Q\ !#47Ñ!Ou&LE4Y 9KM$IEZ r_ȺP-~BH SAxmQ2+3e&Sc X,+4R'(+}ɺkXU;+ڋحIX/=Ϊ.^˿Ef:c=AEp%( Ggb/]4_T4MsޞUUl4k KkQo"FB ߮iIL9֣z[OFe-S[dk_B'J7 ΀1KvPȉWЬb} ڊvԣ-4l4-.jؠ ?+ch~ݒcŖD2!3C5~l#rfwG^;jGQ꼘e6L6f7H3=8em:<.}ݥo,MSr.`Ýoć4 `t̔@1rJXn1BZ‰ `ɫ99%^Z%h14„@iGA"^]g \$PnAd!| p+lli_("(Շ!Pj-/֏iZ3VltPdFHl e|{|Gya*+m%,% <4pNSkJCCh%䶈 FSL u 5 aRZ[lm~r-HuAc$-aa#6:G(+kN6f;ЀźÕH%b~T\ t IԚYU+xjJА5gHZzmš}tQG3I/GT^=nG啂0'z>ڶy;jf눅V&Mg*o)D׆!e6&APW_,'쵣njvKk|1#=kRJdZ2w4tj}Y.0TY <<IX,UU_^\LtMէ']/S_]rcRѡ=@y_ }iXxRBBU\{G/'9 ՛s zW6H=VE:mF]SYIqds-5 ruzהٱF9{,$` o=CJO4nZfhA $! }0_XX\ΡzW'h5e0=EjYxŵ~GLUT~(\?L}U y%%7S/wid}=866]}vG?N~&}]> Qb^q r8t@%ʒxOPULrAO{)XϤ+ i)gsڼ)51]+aT|qc?v؏7޶i%-VM~  /tay'%\>Hn6Hĭ** [҄|$flDgl\ǎtF"A}bw|-5`0-ٌ`0 F:`0 u `041n]R sKsc {NvQ  ϫ+%'޴?{7Lqi7kK LE]gԊ|q|s,b+AFt\*84wpPH`Xu#yҥ, oiv}G#t c÷[ո%T #q&J~P܉ g{֪^o#HE60#3:2.秔." 3P΀ &14e,8™ BSG_{85|CY=lz`I(\)Ul|#A  69{`Xi/o0Z׏=hKcx"M)O.xrO6e[&!y? [+{ sRPa }ZE*y/siPŔ]zK~Nc=-W:]I6\z@`GfKC Y3PjxT84HߚbmlAliA[b}`W3G"w }" lDT8sko E9g\vK?ǖy(#RKoXvh{|7KBg}Id(GeS(Ga\\* 3l [B,[hDYOe[(gf6-MG` a ВWd~͸<҆gmrլ7O s+r8=C 9<+EvwGS>Zhkf:!qP ]A!0jRKJ nBV_vhOlɬQYY]j iSOd rBd#ZC+հ#}YNvJ Z)f]o'dOqhGzhxvmǼu/;MɇlU^fشЪSlum` JX%TY)Xʹ sKTl K_JqE7Hb{Z]Q[Y9]6fdk7 @VKOFk^H`D1)\[h6nx_ŒyZiHQ%A/iB4!  !J;iؑZ˶hA BR> ZA.aY| qMy倿L)6'#UymaWVbW(k,V1-➠JrvfDOlKar*4QDOyHA-RQLDW|s\II+ۼ^ruؘQ1:Qy Q=+U9 bOX m4l*C]qWULQRHAQvDWOQEw:РUX ۩$ $D ,eQ-K;] ǰs\#HnVS|݆%۫>؏&ysS r+IG׹)Iǃ/qtuq4It@jzqǼ( %VŐ@ FZP|aұyNq|~;w|rqWRSZl[ZP" ;FܭU4E^6RAe_RۯedbwhS*|E^÷w$꛼ }8306|yQUUlvk0WzpXVIopx7KP ,eܔh@]+Fn;b\I<iWșm=X 2 ̨:) W'xP"e{GFxTF5-Uvc9+oХUF果\y@Hb?ԅ}E'|ݐ fWrvxgc7]ȕ!:oѥo%]BP8!F#.,`b5HC d}9\+ٱ:Z_Js6}Y(_(=A:v`$+հ})f88d{0B*n=^sj@5XO_,&L~]I Mhث[uP΁.I2ZW' ŽnmeulG^ܮ3Dp78 Ǧ=ymD^wTmaC8Pg9}cFaF'80lB`tuӵ#>hw^6  $w>t5R7.pq6-^Ū c{mM}yC#DFN.}@KPV@Oc я}`0~%؅1RNY!z@U^5'V(ZI.h  (M$GPQGzӝm02J? n˷U{検lRH-ĐgeNZl ořən^//A*-:pr~;C3_qmt_KQi=K%}[YvHw微Ƥwzؚ$bN-$;x$FI_uWEn .M0+ McpLWz )}Ws ?Réެ{i<}qsA|2-99c@ Si{Bq`V<}TMtK=gd l<ԁ"\uglg҄go{(/#XAyc#marnx,ߜ^{kX/ 0M?ҷG$6[[ZֆX_v_a}]efJc'&ݸ7 0{|bV{~g>RN::.F_Ss2 v;oa{oE{B6x;C֗]5QF_kjnF5Rg{M/,[hDYOs'M= <(Ó@ys5Ê-ٳp,xwZULpWK?<u q'>q~#KWp.{r@\sxV8knkOlɬQ͕]rd]8$۬";K}9d!;_~CƥF6l.q0F>]j iSOd i=/3 JX%TY)X?4ϭ_X*%Wɉu76YW5yɡMAhdqU8&W`=&("#0'3Dl[9)" ¼Sn,^/'igU*̘QxQPB_|1{'[Ix^z 5r֊&W#ՙDҋ}k]\GV Ⱦu/b&cfXr'$;Cf$1=-ڨ֜."w+z7QkΪD\Jx4HC\ ϧjԉ R?{&Gu}nU4==3=9G($A A2^ڻ8}׋Xc0sM==sUu3BWuwM+G=9SR[^#.8|W`Y4I//=sdk,(MaxV3.Smm̓÷fhGg>q$7{to2i湩A(W.6$gQPN Q屩%c!cH08Qbl 7Vr@*)"Ar#[ ==w^BҟSBLeXfGYjGB9:'08ēHϜ".N73%[&wt*FITb·n?]03j؅;j<]/+2D_+58R)e3+3>=kJ*oLC@q8RIʥɅ\:hKi>N%ɂ|Q_L+G+)I4CHsP`,$F\ЀnBMgQ8Jdg})`fȕofsY,ZfL.PjUX\*(P׋PoW~=zdFjA8X2@hGy.dm.{;+j\ cۥf_}1=m_ԑtLtO>]ɒ@Y 6 M:qًڮX3sO'_>ъ6c`w !Qׯ Õ-qv/ԋmM/W}&#0``4jC]<-Lħ5n9WG z}g4n`$/ptWq*wgۛvn6\ ԋ)m wMb}yC-:Y Z-UҨĒ fԚgbօ{ZtVNpl0w~8k/~I%u͙r\eXNw5kFqK&vhto l7@?Hs5f7 4{LZA-k{3wkK7W]'_2}ZR׮wo]{I3WU} `;"]|+.R\mЃ_=~#d}{>?Re\'ɯsS8&%HiB 0w5aw;mC^s ,e}a r93P; >Yzv :^!HiB@@@@@@@J\S<. Jr)Uo8Y" ?|{ڡHHe%beMڅqyR: oxҙ}\ʺDL5]V17_z-WȮf;D#H4^Y/|w,%˵Ld)w(R 3Lա]h(K S+d9ZPW+ fu 6a|>N7Z^=5?~`P8E\Suծ+֎8C,yd?<7;Y@O=i|8x.c\q: ^5shhO< )1YR"6º>8jBBu(Wdk$*{uFG_NIMɾ^]p9CgUC*%J\; E4Us=k&P1=cjҤh刋2{~}*BU|AjJT8|dM ŊJY{}mbA +%4N|d ޫJJ 6Hr _͟i-n1?!lB +9AxiGb5__ $]P3CX!ÿoî{iER]|R) +s: m'^=Jmr?^Z9ۀwfb6XJ1Rr9*ŸJԊuyֳc{jD@{ǿ~g&ĉ/G>{Q2IF4a.:^bB=ovmsU5!ˇ,(5-3dP a k hϊ |~7l( Tis'jp"$p $Ԉq\&'dUHc`]%D*YnˇC6? WG:~PH^^bE"7l ;$VyW&W +zY|byt(!BJ!2eDZiǐIdJxJurM u(ˈKg/@Kg_9)/ةGpKa4) -<{@$1?R-˖B{s:ÀXE!iw 4aNw#MtӦ.Ҵ,:_?br]B "M' è"W ,Xf`wu_xvӞ$Oҭt8]GN6h0*Wh;.Ĕ %X#Z ^[\ Qܕ ļ%9};|f;]iBdWb#a坕V-UZCm./cgEFz|t4.W_Byc,S*MP~`u4=;C]C .G. aNYm#M= ۔ Fl(Pʨ*{J9 BN&!k9p?J*r)p*%}@!$. %)jcT;6ixeMƁp T4J^±!bV"T G5Mh\(Uo 4 vQ)3gu@iNOc`mV_I[N\bFixjDHŒ<]1i%/NCI$`Crjɕ u@ _*4F_OyP?/+yRp"S K_PN;ZduKJW.? [?2՚qi1}jɆEw9@USrꢾ᨟Et Goo;;,x>ARYmxJqᰳ!"⚋s"mS3[CIr%᝭!.^SB HweDvq?3 vt\\~ǧ /M-\# y9J.u:yD7PrBjĉ(J.q\ұ3I5ĺP ީw|gbkh=a|3OL]8"^sc _,$Zzg]z% }wO{&HSS!ׅ}(k|!q,o'e,_0v7X@4]=>g{BMGE%$С᭽;LKo/+#.ԗ67s񒢇>_o6vN+(:k= M Z_}A9w}v4Ӄ/@8:cށvެ30r7;ωJW(bBjċ(J.!q\ұ i5ĹR!" }? d;E|A+H 8p=B1 ԡx/c*PB-}o6;Ofy6 H=-yAG}h@{MnҹX\m(`C~)d!̤.!Q@p;\7nIuT ąC'7/5jyx| pѾ[S7oi9ۛռ'a?qMS$l *4! .Jij`mQ P)5@(NS %hxq%o9hجHsmYC1r,n) Ė&XںOꝌ#wm$JZp]0)0F|O MLʛ ypܔ W$.<"iJNN!c/NuL >̩FX-[sv=6'*m;h4fMwgF4$MGҢB$|sW+BnniwL(Kj,IgL[yvTHm^@a[k_1Ls4ia%H%25 IDATf~?ƺ;4RXcܙeSfDG^ck/W89/l!rM5j+ Nl,8dc#jp% :g658ŧuwwiB 0äb2@<| ,{44[u~(M Ѡmמc^qy^4y%@"fp̋T?K)B?V ^H]҄I#9!1gW/ `Tyu҅pƍM۽" 'd"X;ն}B]99]vG䅃ތC[%1(\dܻ躜#YwYC5^*[mN(Ĕ_ 'dA+׋iK8퉽Nl[Q5 k|X^eh([lN-~r_˕G$4:FZXBfe@6e|ҜU@R2k,z>ȵ*ΣDA0{f ^ &FR4in ;p#hk4E34Ԝ؉ +m򙐆P:ʔcL^H@)klǴRrdk|z߬^Zgjp!4O(5X[hAZ2LU,E$Y6ŊTX10*dq()a#0VM+ H>M?c-* )8}7#vLJHb Tew̽GN~Ü…knS ,0ٙ"8D!DKK_iiir=FPXyI;uHx Nm{Mœz$2-#LQ<.fњTir6ɕg` F9 Hf,PM-RLNn[™Y!Ŗ9AױO9胊0Ic3ȧJk`V n S>Ճ>KV<3L>1:ΕﳗgV +6WQas0FXK-(N)l*- 2WB(Abb2@ӗ@ņZ\IWK :%y RdIO$yK{O}i%qv"6qvq-gPP)-/ ~R%.)F l[,. imqH㜿dHc5ݙ<϶h5wGgn)+46apZR{u†"1 B?CIvD\1 /#PȋE͂a'U 2HT_&X-5\/mߧb7g("9j(&0!/_{C&$LO :U )SC0#cIX7U5*M"$dIX 2{99.$ii4+ W9塴`]ed5_ah%CN (C(;_uSti+poBa9W̠]cxi j& D01 r50C$m ?EZ/6W!J «oWlfi"'lc>A\lP `V=!c*+0 )/0/_)`  S& 5TO#P?8 1M6RhnTg֋*+ 14P`Ƃ#we|Z㥢U!*G;E$/J~c^BIOy+=r̝ЂTj/=em$08wh1s+Zh4H0-T9`hSuzWV{ŰPtI J=wr'9iĄipd=3e:a <7VS?P8*q[ qi Zeeֻ0̙[}zK)g W$&,qY5n&Ms ً]_r@!:1?8V=eպ&w,mUg_a]H(vxtS~xp>8 ێd$vYNɼ詓;9%^| Xb#jGv'"Ƴ=tBvy2 &l̡ڽ[3-pQw(+I\3ǡ1/R '@lr ŹU}.w}vSЍYYCCڹkr1>4׼(qUGhk^]!RmH/5m"ӵ;%$\_Cea! 2 MFzs.!y@JK5%د<LfN(X3G\ sM= rAb>@ErdȉAk>z0kvO׺gBj>w]Ƀge~d.$wz)>X& z]lٜJ۶G)&(ݮ @ J=,BÁK-cgG]]u!' d  v(Tl4`u0wWPW}Pi59)W.6wfd! ]X5%8rA\]Dw$"*McwЁPqigAvȮN?Pi:^)g$E!AʯPl4xc#D Rn2o(n]FΡ~J7ĝ8 &;di8q2G}HUK uud6\ xrQKoi}b^ɝZRrkЃ|] !;Yw^>p>y 4bb/H-/SJu&<b@ ͖0=xnK7NB|{m Ú1&H;qtgiE}@1Aְ{=-/P]~30H4 -R} ic/%/FRoź]6]F:;\t."1 %drwhך|yBu+˺(&T&*=_rO^?aa?͔=}=Ii}u~Љh3ҡݖSthLE$߻֬(BgG$1GJ'nNg+`"γ؛tiz-y_%ž3xѻ|B9spo ͭxHMh7g]Al>ʒ'gU.c}HS[(0z#[8};з/zAc+L=!\)_,ZDkW;7k^ t~Jt͔/k5Kdyp=Dʼ }snP҄@$({8~fǾ5#n/DY۰gG_kvn҄E?oo4,` t~_u%! O}4! ɵAGiq lt%}^D4$s&W (?UXGW6J7nњ. J7u35W@Sl %[˳>M_S h0n)MnBԷ{|ocu}C9y[SЛę| u$b {NJ(2ntܪ4Gz`[HHglbEaB.M8Gwm:OSْgn}&Kb.١ݛڛ>mRd ;p=P/SWoX[k O 3FM}lۓ.%'iOMxl]ea/?8XKߝ3MO(l^s^zyvn/~Uɿ#,hr.29Q&& Y +uE#&J85g߹L?R]_|bW?Wc޲n9SbeG^W鋻JX{sY{ۍ ~kYhkMsq&%,@JD( ? u:2 +U{t]yX n$Xyg_oOYQ:k]wϖWK%`O@cbrK)IčtFNt8l8W$YƂI>p7l|oV2ם3udp7"tr`w1x9я=Z]4@zj0.~mSmw~_h`yѱOUB[~ֽ.[T?g]k@ \W R^yɼv 8EɂG3:׾[T2IpPJKڕBq8g۞8S V ~ MCSĨX)3.3(&sKV>;G/]Ԟ+yM;&O Y`m[\ ~ɶNዐ"h/tM{qY18ߢ?TN 3Y^L !ɱGЈ%:nPҙ;F n҄vtsGrGB(SK\nJug̲K!/G \ O#7gxg]ژ'ٕ+N!M72dս-4f܇i2*&yA--󎯟o6R!ca{y'.T ] ;uQ礩xs&SΎBp4/AϖD:14\=8PVzCjH5Zኒ ɏ=^4x9!r)MƴqR! ?bÅo?~$va^_gZ$Ð AcL@ɖ ݧn uyAsԧ4]Sasi(tkǦy~Ⓕ_v 8, BDh$X&s,4NPkvތiC<€NO 5er|"t09o<0aCKo;skUdTk D\S R3e ~zcjc2J,b':D4Cbmjf;{gYύ@KĸaEE(mSuYX\8̥KC[Jkz =%4CHRFlBB!WM2.l/G VғKN'EAɬ.ol%C16hXCrڣv}hWL٫E"@jHqi.yQRھ?bceJ|rZpy36fqM1jMϟ~K}8}RGb [izռkYo}Ey#[{;+&dSf3l>hGy.dmn*@OnxaAZNE/]Zd׵qu^4{J nn$/(9c9 IDATc;sq#7%29Qb~ۤy͖J qd_q7WƎ^=|ī_D+Nk،%8F5cco { TD5jƖM/W}&#0`Hc :fe-DeQ&ǣʈR0RyI]kF臺tHH|Z&Tvk~Иctk do$2Olu9g߯H O+ nd8c}Pu9y"E՛:l4Wo_Qoƴgs#- +QJkWBI͹Sstת4#%A1 _Yʦ?~dK\'u &ǵKL}@o\ewߪBDSxq,}T;8֒*mF#phdWG1廿NxHUo7-5 )i;nWN(*͕^}L$2=ƂC56T=y$? v.19.TF"laW5痂̯.hu 卤365)"B8toxi& lmYy[[ j\8v}}#x}KY]!2XH#JZoh$a->)M~=`79Hir)hl5"Cw9G\9`O0Y^iy#rA \ :ї)MHiCx\֣MH>eii}JWML])5-O:*MҲu2&~( l).JgW UE qRol U95X32@>mabX+؁-.ɘK4LQҽ'؄ui\`KƕWV].+N]RRg͙Q 4 @g*$&4UAp=z_ڿx[ SW+Y+k_<P{t-4$f]Mi^<~l0]J=uN u&ܻ. @2Ŝ9;˕YJFX" <'R C#|=٦{kTRlHяUyK뜞&}k&?Z< ldkyr9[?E%52XF7u5 JZ75zu þ=7%4B!i G/]w{Ul -ÜmV3iPdQP(9 lņK 0ȋqe^hX1E_#Ip(m;ɐ8.m7biB)VJ[t?gYb@.2^\nԳq*mD nx@\?%@;Ō9c䄬Jw)#`τhP3_?`pg|avHoB"*'O]uIń{{Б#C^6 tamY1VG-{keS'z9lf#=($[8!85 /KCHC,*6L'-,zn]<7S"_gJ<$S nUJ1lϿg'fXKN  D+㚛'kLmx»UQ],QxSx|ek[YYDG i(_Bыj|i?zjPdqMPr?%.6DyޱC(W ʼiǦCV?'I)ݣNWPFm`SwWMvq=O2}qM?ps:À8֟D7$(n])|h ҆o71BSMy#9Ot?4LW=m_(b0pj{UWTJ5@$1?R-˖B-DC!iw͉W^c kװ=X茙o,ZJұϧ#WMP-sw$z wxȊo=nir G_5 $@ïPlbLd(~kr'AwԫdTEShB`DSt DPSu'DžR)$ihhm`(V"3tyHs_e.tq8JLa {jmjMXËMC>qy1{JI KlbS1JQǎq2J$P*Wd9f )}8R_Y{z/1@'8‘}z.eIZOWG$a}},R'K8( Z*dC-!a5 /Д5 Q&=I ⴬LuNnp14\Sl!wK.bb)PDP2J pϱmZ$3SCQJj(^dq䓑7!W $b#B#+7HW3p L0~ط=ׂ)EϔGã&,J{Ma)h@?)HP? ;JLWƢڂ=,#)QgC[дKˆ| .%BcsZ`#xl4b($Nc $@C¯`lf$DXa5r5$ J' </Jp 9S<\i%Dz$2Zڃ@FYP|)3B[(~ YQJb ie,P8 őOB13/P DV\C vaVtD_ڮ҄ {O}2՚qi!Eۦf J;[C#SXI ɆEw HweDvq?3 vt)KS mB^ҥK}sgnRc\8lqHHu,DUMɩ~*)<: qc mcp OlV=祉 F 7xij7hT4 `ZT"?CZps7~.Iv(Ny89Qso8fzSdݓٳ-|;cGPg ko"jv̊{MяݮOOc6P\* %l%*6D❰b]C6w9Y8Z.5BpO{|ʗ4Ab% z.84CY ݍ,o6vN+(:k= gM̜ f3롶psZ>sA[Fv4Ӄ/@8:cށvެ30r7;ρMwٹ/mn%E+e}9Z~Mski)`'t^Sk!n,Mh/:[7s-*DYtk@zӳ(\4=6ÿ<y'4Stb(2 rD}= -xlލ ~baܣbj(_f 樲I9 u(|4K)-Pd7[l q1?'HW@ƽSR"]G~ʧzi`귀.~@Vj\-_J܁澾v%MϬ^=a4! @>E@@pG׸-٦<MkB@@@@@@@J҄*^5QIߘ& "|L")e5s| V}oz\O?U95X3+*u=L᚟Sє5GG,ßhmw癏Fz012URWJ|:me0WxDݞeu<PTú׻R򖕨RZNwo%H~{ I)uw kԬ:]{:6X"lY>iHO5??2ʉ&)(%DlOޕѢ."""o->Af-h3jMDOLZ8F6IAo 7mAѢ+.Yg=?QܵNQoD$"FFdv毕owJ*u J|ee3 =mC MʊEFve0H͇ZލDt|ܾdRp ܮhDS/CӍ8^#ŘYK{l""; ,W7U8q,|vt`wbyaƿH2e߯HP11mrc;i (Ue dw7snAG5l4~gF˦n""!Hv-Ą&w'o_':Í}<{b$Ng犋"/Z?f)KWUp8BR[`[֝{Ŕ*z{]8S~W.y{=^Hﶻ'`4f 9;hW-"&_]_g7[u+I/j?ʡ@D7KL7 zEٻA/<%+RY.mޞLHyxnxG{.9 ]%)! O4r?Ĩ9-х AZlb{h&TŴP㺣3H`n~3U8~/uO5S;xT$F%Ӧ0ӵB?sF[- Ҍ]uxׯ]Zsyiw2G wH9 GR|T@-(cwy)z$uR鴅0+e =AZjVHD<_ )c wl|K5"};Cjip8bti iSs9w{g;B_1B6k9X$j#ܛ/gl DĪ&lH;w}S0T FmedqRV̟Vd.- 1Z#˟8*mZKKTS=W"!S3Ĩq!H '_㹢c'\mLYEHNVg!EcnθctR㵚wb="lJpV*0u=prL?XY$;zL( V>=%2ƿr>#QSq:i̪ű^5\dmc祜ESG:\F{2 ;4'81pƌɥZe%wjw߬ό s;yW\:;|;vF5ن9SBtQXi_4(7fbT }viʭ؏g9ȾDIܴjm||/M57̛k=i=xfwo&T$ojsV$eYy P3BDծ?9sƥSRK+M'ñ&Q8Y,jڏ mSFΜ D#ն ,yݛjMc+Oy/*,fz}. 9$'T{eM%1Fi9E,{hS~jZyܾV셷g)Į.{gG{y,jsTRck Wf?} <{@Nˋ{o{VKnR2CismscH"}0[i-`Hq۞: XmH CDSVucMJwuP/zҢRzGðl2=OT uhz^}ٗ\j@wM+0%fm[n\HMkrYM=CtZU99$s r MR۽ktJ{뾎"3߰dLRQں%hhߨ;n">k<V -DD@4@4DD@4@4DD@4@4@4DP IDATD@4@4DD)$X}9ZxZ1c0oߑe0M0{MEz$߾?o7'lqEiGJ:=rS3Vmo+0I_S *U0-7m I6,< f0wX x\#tHCAD * 37TI oDHHHV|pՕ-q]uRtJ乩䅹启6<:һ^]EaXzY_{6|&Ju:@!CF $9Eu}RZ|S p-7+H "IH٢]euGmrE"FS^)g"=ig%gV9 &eTtxV>93pJΉID: wT{wg+x%G&''waTq>Hdz3meHDď'twʧ$wWv b "FQ?üy13ɥSD$`˶1! n5#ɣa9_`#WX~S}1! m\grPa&L{I1hڝd6 |4P3{{ƥfPև?,U^W¸\L˂|7=i/X~vD2+Ev1->$^WEEF,o %H;EGIVhWnN}rYPw)z;GK3y̯BQݟYs'BsԊ`5/}D]>D ,‚V>ɧ>]V (wcW?_IG6)菖PLz8k8adFuIDLKqJk(?YzSv_ه\7eRX=ĐÙjp[oktΚK5OmڿDBoe X^ ߕҟ{у0A*TQjӎW=KxC/E Z;6ɛފ__ _z}^ Zz/"`I OdqG6̵*~;un ,85v8*.|&U KhRhۢ ;쨌}7u\ww^He :K4ݟQB9N8(#]->]z|f; \_}[E}6e|ZǗY] W=$}mO0ؤ;B tXiGF׺+"Dӹ??9'׍s;furC>[Ƙ*R_ _Y~tm7Η;i߬ O'kl)7[s:eh+LdoW|wxˎR Qs4⥬3YgϏ㨔ƛ%;/=+Sa\Ko0뫷YD}weu\;O[sxk;sSH85/F7wqvDd[Jg|wํ o THF1L&|*ާﺵ?n/~ծ~{IaB%S #q?!Vw1Ћ=ÅX viqۺG@AŨH LА&Fm<ӷ9^ab/1P*Im;܉>b\O][z0ke4j{?nRdVqnd%n?. AF`^'u8R~‚ϊ/kݗm;+:02E{zEK*ǟoHi2ʮ_3T`z&m`{gS,76bn')0C]? T7Y{=Bk/}u:8{so2LgkKٺ(G: t#$nĔLg>z*O~“&^1mz])hgUA)۴n6sihRh.>ZV r\r1UWMxD.?i_-Recrwv_A@|vO ?J'`;c/@GEWs)ޟ::M?72qmYڦ-IO\v/W vCn X}qIBO@芌k|"FHD#=DDə"9ХdקIsw}cݗcT3h25 n7|䇟(+!E]DDDjs[|Q"bY+u8:z㝪`hK/7{=D6G[?`?."F) '\CkN^'r*OZQ3k۱c%"|z*Նg>ϻ"3qA鷲x"ro˿L©$ݾcsKsqRQkfO3N5kS$FDX~w$,ynmyJY) -'yz7F| t@PBR$M={i֤;xKg$~˴2ޝQF܁#-bI~uMS->7n1{*k+Q/{x[Q"O3̯߰"ɬEZs;> 7%kE h3tpOz !2 g4I7t \ɤe\o4otF@p-b0xKdUT#K+1{ 3Y;ѲH+EOwnSr{*5yc'+r7X-N;5]`oLPh^!L\Î56y.\lg'Hv+m[ I 3.L//':e уޢߴ=)g:W'"8j''(>sE jZw6rkzi)v5~jRDaH+L蜗kك`%gk$WUYJ[|Bgo|;앩0 eJ,k׳F)Yh`+Bq,1@pE/gNY1cíg߿,{O4x׺Yƒg)9җ_0.e*C$̣mհ{9.V6uBQ a!gp=-_Ȋ M.t qJw)Ū-U.?,?FTh9W]2Vn/硢 J?̴ld-" W1+$"b$&;{Kgo\qbߋ(k9!=!wfh >k7IhBDx6wvBZo(2=K;_t(B߹Ѻg9Ȧhe0_juaK՜E|wV;F[$?|% TJAFr}QH#Oj ]ɟvKBH7#oyu]˗o Y 0V9߯2A?L⟽VWH\R`{xqybIDh VĨ9m/:xoChʸo_ Z#j.AYlb{aspꅋK:~7 ]Ɠtɱ8bJ u~Kw=|)FEbT\RK ZeYu䱪|e+>/?vRguT[~T)P}Le[۴BkڜREQ"s>|Ǽ6-{A_7Ua|;@K;@T>H.Z)rh0k9ΝmPp;$UqB$Җߚ^-}3ӗUF\&LX$lJ+\M+uBEwnj:="WV??๤ϛB^5)}jcn[}mG{^ߦE46t.8~nTd<$ =2mHuˠЎ|Oxk*2kݮHc[Gڒ !S.4ƴ4;z#K"I^ Y9$%"~cQBo2][#&!aPs~Khs> vfoDdlGf<:]՚4X+ݹ r;۽1GĮ*6KҰvႜSVeG;xڌ&m)g\HsmpX'sJP큖5{ӝL{lyGQ3\6hB%.S$Uk۫DD37|}iWLT8.߶5yiԌ|`KQR!ۦ9w̉FmKOJ!tCTmmy0{oWd*Ol] kyeӷ }Nqs~8`썯p;\/%gWM+EnſFߜlU^xyBϿu!,L5մxg0D4e[h񣕆6.ܿwCKr,"Xez6~9V-g՗}UuR̞zmK'~vۏtZU99$s rɰB42qyI4~ׇ`DoX2&)]͈~m] :0CMMMMMMMMM6+VNph |3uI2#{a!aѸ̇&^c=o#zŕ0@.+ W\a9zڜ%CTT' v_s0AC뀉WeTܪQ噏= ̰ykPwؤ[vl=3.cN cW1ajS+$!زm{4|02Գ`=o9 F=j0+%CL4Q%ե_"?Z%x|>ɤVDҕ Jӟ\z9(;`uIAkoԌbڼ»GƐL{Lۻw^y55a|a87kd'h"X:~zÐ8әkDG u}*'rKzzbDWddkd0u2r@viDDzrpG{3E"r:mKɜOe)"ƺ#ʨ3gf/(djN[۫yОw0D衱[myn/JD W21kynT'CGtS1:7mifѦ#Ѻ= q[޲uVBI=+_is3"7Io^Z5)^3hNq*v6a߇4>{wFi*qEct&!5MQXY+*uf-|;tlDDzx<DDR_xO 7%kE h3tpOz ŽPv1=:`={*4s9 ^96Dfaי_WEӍQePZRwz])r݈khoIYh!)~-(Zt"-aZ vqȪYRGW'bgPwreS7>Უg=?QܵNQoD$M}b8enPov1(s7aMPr IDAT'hXui.D"K3_SM0ÊBMkDn1YR;y7/)T rbCf؜<ݨO>Rdqͦ-KK_=iBųDB; ,W7U8/Yr̶I.( 6r=%ivkIӕ†-ҼfE|fP S6EaoD$"xO [6:ҤX^ad#JuþL(C")ڿ}WW06y氣~-;+qreP\r{T+%{/pӷ>=@yMTD3L۵9EQDD-q}j!J/GNVl6*=.]Uv(?4?50ݮhVqCwLwik‹3yS=8z-M\^n}\[`s! mD]\ ҅jZw6rܽe)v5~jRDe uG=<YC c?dXpJ1z6{"l큃j*KI J VE-aEZ񒩆-NwJPf]|\'&6r($ Xz2bޮ}PvV(un4w11J{kߐ+C1knR`"dr EeTϾYНiu*Τ-Rr/Ͽa\TIփmG?r۴aSЭ'gN)VmufYKop5BϹbtcIA' (ƲOǿ7#seK.)r-U%IDI$1 qFMr~8"5B9} )R*fh >kx>3{e㚇\=جV{mt5JA )ϕ" p D"Eb+v+Do6O/3ܳĸ;O//zfAp_llv\IQG:Ѷ=ݍSsFl Uw#Cx xR[m]M>+<}~|K>{Q|zxi-~'!$A-`ZnŒLÅ D2(vkn\RƐ6&W>J]aP1hip|fʬƻ~uX1B6;hRMlO]* evus>9+˭>¸cn}3|jwo<jcu#vQqIWKf2mneEʏ,k,QI|1AZQqU5+6a =5k6'hRK*U+]vܸd쀂+!(o p7ezfJC"q\wο&IUz/oȔ ߣz_)KDD; ڣ\RZ&bkLN_V pl3a)9#ڮ_`r5iWݡVy4\Ex[]H?:Ԃ璺?o yE֐5:=DĎ5oiz}_>ZӹWp%#֑d~6_o2.7}$K21-͎HFKC{OL[26R2(:߿ {I(SwrT7JP?}`RB@,4mp>'ǍM<ڵAyKLn.>?Jnƌ'gtܶ$o G+*It9ڜ$rL_^ "rsLZg2qɾgTi?v;ߔs ?m}FmSFΜ D#ն ܐ9H֖ 'v{uHBWf?} mpIm婫V+$ûn]V?=#miy1}yjݰsY|\=+~6[ճf: w]zrk0JɳL>f]tW}_ew3RDp:/=_}.vCL%Rbtc{ɟ3TMHz ش4|q](?@k/B_<7L/thz^}ٗ\*QwM`W0%fm[:!()*O7 NȨU3JB? j2qyI4~`q03|Ò1IjF Gm6k:㯵-џ7tX &hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&&hh&&&hh&&hh&V_?5Z- \5 V;0̦LBD QA().yʔ _b-1o5!yP?IENDB`brick-1.9/docs/programs-screenshots/brick-border-demo.png0000644000000000000000000004632607346545000021776 0ustar0000000000000000PNG  IHDR=%sBITOtEXtSoftwareShutterc IDATxg|3[IHPBhBGʥX"DA Tl"b@(HC ! =ٴM}:M~x03ggv7ސq6.!,++h I^3q`5\] R]:KGCz(2(I ʥF}whF3\tM\YFQM[~Rv̤$QZo֥* ?Ǥ+??٥>77]Nnh40StyL(фY)6 ! ҵҜ!H75yo}kkAڟܐJI_ؤ  Jma^m&~es*8Y]̵)=jdN+%ѯ]H]?,lt9i77\}Z8_޿V[NQ2x`rvިڽAuPnP?-Dk{BQƶ˛;<ׂg|;Y"ڵr׾%UEv5 ^鶭vvҞf_/?Q!½c3iwv꯿5$S.l~Ί[G"6/.D.#>)ߥno5?~Tu}SQ*[^ekLޭNm%dc&Ѹϴ#v$іvި"AuJuPjuM5:B٠}zA bM*e%[,15LɤF%];.x')~`<nƠA{?*)5' {‡<-}Vz!i?!nV?ܺ"L_kB6jk n"Қ*d<-?n|{iyVj! -+]4̷k~!#dK=X[j<*JRqv?Fxvzcg.?FQQSлkjG~p쮕lFE Sթ-8w7mțߢ&)ԝ-懇~r?[EMaw脪 T(f4A[wf2eBCb2d&TuxlHN-k.2%Cnb=K<^]߆+gqsI=F_Rh51q뿫.l>_ܦ^.oz1]&>BTv]iya}X߱箵U!#(|BU祶:`6 >"1CnB8ո%;k$Mn{vfWr7 mMjc)6Rۜvq'fwUIEW*bջnlέWT^jVu) HUDn57=)nښ]u7这fͯEjV֞ &R[Vwd֗,MskӢ7e;uV)`rzf>=%%sѤZ=ܵ&cޛi]axJЧN\ٯBHC؄}}upc^J算doh~󼯦qupmg.{4~yeyB^Ea _o˃;m zf(oRC.y:)=o\ڣҬŶG7EWǹ^Q?aCGFݬدUį,P;[Eٶ@+!ҪC{#W RNYN0ߕmKk=uWL_`{ zBȺPZyZ *mǷ_?w OPt ΢LCO#F;d/#O鳸BhvW493}PF!0Cvf>~ _[ Ms?h٥[ nsn}did!d礰R<4MXcƹ,D+fǽg k!qo[" !?]w>R9q2ߵۋ^Sn)`W` imW8>cKrU=*}'PrHA$>?OzPc5Bj7S%[|2m+5NqpÌiu"žmN !Զ/-ܖMs_Ɇ2$zc|[Rլ-lTs,^Nw[)]V.*K HkMrB W7Fmn[!,klmmT;)UtQaK[{G$+1Qe[Q/j aH' B_T'MOP;OEn̛v#k&7s}2(:0SߢoSҎa:e;yeOON'~9yWZ,h H6%gJ qS2wN6ؐ1%5ʐe>YBH5zTde\HjuU?ؠk[ "4ιfc)^VKmr'6z87+*v+|u ^' E|v#[6, \ڨҿ{eݗE's{ֿNUn3F|2WԯTi=y.i iobWhf,WI*bJ%KV>*$SD'TN"QV3;>d]M?ػU}݂G⅐T?7!9Oj/}{]=kyLJDno)皶<תmH!mObwKaHSG?_eT6tB;~IO޶&zNSw:nҶ)u”qK]EĆc4?u =3[L#KM殮vy>fY`7u{D]l[caN~i`0`ɾS}l~JTchƦ s;mr&Mk)M&Yq H9Tf|/3z[;_rnO&(t 8g=O>p=]^)~oMUXaC!|}%&:OsYޠm\6]MnrIDZdkWrvAՐ iG6G x2+ʜ384ZS xNN?!o$-f5,4sV63 {5oAi5|?FB|Xv sӂ#oP,D_8yw4Zxeq*Y-3TtEGQߜ+rc#îh+jGE%Xw=64$숐(eAԯg8˿n}թ[NoxcYԐ!.:?_.qidK۪ha^cƌY~}e|S3W1cP9{dMh*큈zPǨW^y'Ou6m^Or$jݺuϠVzЄqWDewɓ ~P0q M&GuǙ3;ʁzx4 Mh&D@4 M&zR0'T=5D LDqfpNjr^*M&D@4 M&h@4D T?#IAUgMhҶ><9T#0%-Y vh838xf'uI?Y%YTJ5ڴTZW`+eB*oJeĀ6p̧# IDAT* {׬fV*߯)k+YɺJ[g̚٣#gnάY#ǂTr}ջ*HCzDŒ3}9;_EŔ#|(EhVe&E$UNʐzdd!l~G:4rT,qk>`^X4I,Hs%SZB>?!a(>A ! B9s©ߞ1:t>KQ!L_Zs_[a%h1AۂWnU) }ʇ/;gB;\˪ٚ,K-Nӱڠ ٻvƢRxB[am]EΈ4Wujdg%g\\އř, TQ+h4#nn&OrsϽ;+8 Y;$bɄ<] oOCTw_h:mkc3^I&!C߼zFu>ivfa ۸B?eB6sΞdhݬ"<$NF1EΟ_wͲZ?:gvGS*k-/,x#_ڂ &E1[e!l'Λdw51_1wZ8Z!9f5{~"Q,hD2N/ P@!$e^F>V;{,0,>!}Ho>˻W\5\DBQ(lrlM1!PB׬v٨( Ȏr1,$ 9Fb|TFs7R IEf!tĸlllN.g:zt6L57x6L_ .Ѻ_ BQWYeD_kXgǨ}e]n]wu]mgBLM6q)ռsC:!(UĎ4H7B9чEEsdK5TO+ecWhpn2pKC'^rWoڊ(ռ}K)ʞu|Bc~U6?BDa)XF4(5lM$~/>3d/N([5fSܭK6\Ȓ0BȲL.ęƍ5Nvlhӹ"%J)iϗP!;rd,Jeni I*(EVڅ$IszijGYkv!}X1ft/]cƃ <~ tBDԵ#wv1|9h/OI95<<]Ѱc{7da`/h ClYsrT^[ B̼c z,llmn1*,P*9Dr25siZX1*,$N,Zx`ݛuo.ռnliXy5hiEU҇aiBj6J725v:(n:SK*rA]sS2M6ލumn{]3eat}YI*k{Ϻ;L=|aJͩk 셬uvӸ12Τչo纶t=emSdlq0 v@g߸4pmԶ~zДl5|Hު Og׳wZVii#)2.Ӕdn>]ddרus7MxBng^cƌY~};Dm״ovu$Mpؾs|v+B 5EMbBWG+)/!ͩ!=uzG?3:0w+'D^BcG)kv`:m\[BnjSC{6dkPKkɆ{?FVM6 ðTe#}h8q2˩qPF['yԇ m^y啂NM,aft/Uc9q>Hq/^e0a~ۯxi-kݺucǎXU,Tyݺu?a94f@T11?|l]7~vl=Jv}&ơFU4@Ѥ0g)5Ot"oxD䋉G ~g<ԫ7CJR0h@4(; IʹT Pţ h&D@4 M&h@4Փ_Gx94 M@e&3gvR3-Th@4 M&h@4D M@/&aN* z8k&D@4 }&/aZkR9P/\ffP=yxxh4Rܑ5*[nh޽O=olT+ӧO߷o߃& +[BB&}Vgb_oڵƍ{לj%33$P?üBI,BSҟ f|(P>՛BS_ ^YU/T`رLS˖ݵɓoEaBt﮹h/w:VB֥F%7 ¢"@e&7>t)KOJz^4m=+VQ_^*M gN* z&D@4 M&h@4ՌJn:&|BQ0'}wY^)cn= $Ks^.ŽWI }n0U)(,^[S$D\9>WH.Nf W !oq.Xes[3\3h&0M3Fx_TǯF!MBŒə+鑗= MZar ?&KfT/6I>G/'n-3iDdݽӛNAnof !XQ#Uǖҿ{lO'&93o=Nzc'nB)R\Tu$ZÉ&Չ\$aJ1-wYCl NFUqoL v$d2 6B-FȆRm| ļh9QBqI(\$udEr$z(f"t*ʕEʎYk/5әx9fGv|0Z Wqv)pkygd/& U9[ߣfw6^VK㦩rk{1F+zrUHB|yuD@ՔT&)6${U 0'ƒKb%gV?&)|_TE;c pqM&h@4D Mh&D Mh&D@4 M&D@4 M&h@4D Mےs%SZB>?Y9wsfz_m}/e՛jHTӳ&”#|&hr­zjbm$]ڲ?#B[am]EΈOb^}@у:5qwq3.}ÿLBHu{?#x9yYqiDyڻ/v4ݶjg/ۤdBN7;kb$c͙pOWh^K`+WUv."`[ʆcfMۊE'v5= wX0&*MT=.dNFf2׵__fkQc>F®tB)^{6y?Ȏr1,$ kŲ._' ]'U+B=r__1ƇOVթ_Kiiӿ8(0_+ۻ*ggfYYzM0B?ի3,rEj:rg_=Đp4S ;wgWQa&^w(4\ >X [D2ϘyU_yDNwc)\^å-Ntsjm|F!w1G掜9U`T~;{&rNlL"sc/7g{Aw'Beӧ5]!B!gZ83!$NʐzdtϟnexƟV/D־OսM~$Bܒh1AۂWn%RF®?{ޔ+$k9,!$6ύj{nŇB¶ ֚z9!QvM|u3ǿ4i !;9.6B%~ͪiIey4\Nq6JU:=#ͥ4%u9t%B 15i.ylcԞҾO;ƅ# ۺ3oȚĤY5bR4im,QaZR^3(U᫺Yv*|a٦ (5}|dșSJ闸sy"Cv9hi[E1&EFi;sQqNQK}m(16<Щۈ_N˗깩\;G\XAHrkк&#ȡ~?8! Q!F:KudmwI/:_=~SQFko;N%I].jrQ#,Di( L$4Q_bs u&~^]N-:4SO3=rY=ۭΏM3LM ?GrLQ1AmJz:Q{s1M~ɱԟBl^32$_=[/h0Q Hؘg^iIx0>O!D_}7e~C'qi\)D eKeaaI$蘘_EN\CT D@4Pa&h@4D M&h@4D Mh&D@4 Mh&D@4 M&h@4 M&h@4D Mф)D Mh&D@4 0K5aR88a-%{=̀%S!ţb uV4W`-Wkcfr􅿾_wQ!k2ϔʓ |Y=6/|2KrdJKBtź}Ci"W{ 2b˪X6 ZIDATɢaY=ф=)R&U0 xk_Nⰾtjqv0]ogpW3OK[Vy2_uM{'J8"gnN¹˔yx"Fv?Z)Bm_~OxR8&%yW:o{XU~ƾ/gGyb#˾2SNrLr2e ]pLkvأ{ɳ?ޓ") ʢр cZ{;'z9KBo rW!|!0je~ev^%㎢?~$wDk~ԩqq{g2_"Nܚ=W>.֦ɲivrv\_o8yC )D뀱]l})pΙq+;}g%$" ! [dI-ꅉmyY&F9fܜN t^+,l|k)wSOoCDҥHm9f x8pn2pK^= [d ]"y{!W}q:6Gb .Be*,$I*xy1weš^T XMb2!Da 0 k|wTmEz6 R^5 !D}P*h1ΧZAZZѳ"u%eɲeI5l\5{hVlֵ.EIWyM;eqPa'm]w98xsSl$eo>K/wrKmS=d0zP0pe&/45u, /֍rҦGD H}09'QeO7FǦhwdWۙ֋xq>ܓn;$$ fz О4R#EL愩Ӧ]Xzus+]ça֚#-C5G>TI?7.\>Xw4Ìuxznϻ[CӖ>S=܎uRZ;'<'s^K-RV-SNMCK}o궝ĺS3O j֦|(+)H)]^]y1Sn>WCY wOWRz7+κWPo^Q>;!GirUm'<|v8翹vC[O8dVj7bpC+g]V?AYUcOPBdoeş]r辆(7u|ꉟ;Ֆt邇2;E nϭX]Y?}Ķ~ص_X;ӝ(cۋ=rm w^sJgw~q>y%cW.y_wc熋J.7mLdϩHKť++W,[xfFWCѾ-=gq'f;RLʙhܬ֗~|7SGxQEMIm8gWm`DkxZf͛>`Б7v ?tlߏwg=R)iԬqG2g|k{b R&?U0a{m!`)wHY~Çp 1Uڴiڵkõ'ϿŮ_>=}V.ou.4XgfAŃѶK)c&?^Y{%@Xh줇a贡{7O @ u7m1H4&4&4&4&4&4&&&&&&&@@@@@@@H@H@H@H@H@H@HiHiHiHiHiHiHi Mi5m9giVAO$rsrKIMM pwyC?#k/!IENDB`brick-1.9/docs/programs-screenshots/brick-cache-demo.png0000644000000000000000000012723207346545000021560 0ustar0000000000000000PNG  IHDR=%sBITOtEXtSoftwareShutterc IDATxg\WҗWł.*؍=KK,$FI5j쉽(RufXPwga]@ڷݭ5*¬~+ΝX&dJ3)hܾK.-ͲeKڳ~ORJM 5qhb;+Fڨ8E86Hx$TI'ӏco/=;b̟6._}mv緍h293' kfQòᵙe_tv@>6oX>wd[ZzvW_z[UjjԵ>TU9FdU`i1bҾow+]? ';64kÔ@~(-\RXsIW&sҚ} o1bA9W.?Uzb㏦m >뇊 Wڴyn۩^_B&vS\? ݃x[aiAw6'+ =D[/H>wjѤG^xx͞` 9BNתYuhŜF5̥O+Cvg 9XrE!6o;(U劁.sc駣1ϯsl;=uhfRaD_N(W йs7,!4uqܠ,L9k q~^Š^HcfN io Ɋz_GjQ /.LlٞTӿUJ^ራPuJZhWN~K9 {)B^F_,[]QTKO2ǗORiW.(0w)*6 :Z[8z݃rnîC5eya̘Ovp3o_67Z^Z,3rӥJ0|欟Q@ O/_l2sɊs%F#/OkJ@Ĉ)*g[.H pͬǺyrn)b[wXua ./*:W Qs'V=}2"1-pl5hj+`rqen׬״u!B av[G76Uyܼ- g:Qc0 5Fi`H[~uFy8}+ʔ`5*nM *-R܋ ^}8 5}Yfٿ_מ_HISx37޻gon~S$uԏkF48{UԷU=0MrğJҦ6uhz~~C7<2m^<>ay_ 7l a}[B&wT{_~S*Uj::E h`wW7d=)C wv&72`r/- owJc?bS[hjjP#Q@j3uQ^?.۽hR/Vv/.@&kU&V]7&˗QuC)4*؅]:7j́RdӘn/YP hqȦ'R~PqSrjΠPumI{ߙhhL>/Uu<0t^LLH`e [43$̣fv@4[Wq :Gb@xuWc'W)Q)TkĘsrN,A>H?wN^ 9WG!4W sjjiPQ@jaiQAokD^I|{]\`h֦'FiIn笯lQ+3LhwXֵJƥ[myV뛑?|naƴ6::Ò&Y\{I_YfLqZ㟔3J &-Djmy뢍"2UVuhȤ$#axJ2Is O+,g9 uCum?D( l-xRQj̃De~6wl)ׄ[. ?i1rjFAr?(Z* 3nEyG[RJR\`jjP#Q@Ot[N-_e凚5%_}4'}pF3Cɥ!`Py[;(9cK5x_7.#|_"FPy+ί}9UKohe.]|iF5@1^p5N\^ : ; `k9mynxP9V,MA?$8h3LVE  ~Ӑ&X6XAAjX    & `i    X   =#KWMt(    R[K@UzvCp}':7't҈ЏRYm:dFsfmO[XN~pPNX; w3ppDkiSAj{ebol}[# ̧ͳio]V#sZnq0gZϤ`>yM[+=tih9gUKQI[FUݎ &HauWn%AO1ܸ>)̽m_/P}u]kE`ʼni^۲KzV"ik -/. >Z`0³+kGwEg1ܸ>%+˭v>[iq}[*<'voV͸lwWG!5a 攵iHU":tKR5$ NԹc%e#!\&d>?5 ,VmN& Nn2 8^Ώ˼׶{})eⶉW} _Eй~XQ3 2@MCє-s+ga2ct淺ס/X,?,}]jonLܒ9ޗ[V:RۜgEvRYilr']#/;8 L+l/+huS/GBl)yuhvsiZÞo:3ppv൲c%܋Of-KjwamOKvJ||ctYvmyi-pbp^p Pz9U맋~+<{`㴌^xp%_LϢ񐂻&>?تx㵗?iX qJfi~::DX%g44Xw‹Xlb8c =vI2ހoWM_A2b>E*|(۫}2# w7]n*CXgo2ǠC{ÂeTDf%2h3 ӎ=pmIaU_nunr1e1% (^x270r;ί2.sR{J5?Yn+{&Afg\ʊ cġ1GkG))|˲m,:~ռ|uM64+uڱLRbsҀX41@MC*RQq3GG Ά19-hG v,+X>Hf4<[fey˾럲5FfZo^|:v0( X[XpcwiL܌g,9NoVmʶx3W#Sv{u<# w7]_nmzҿ6G03r\V<)^|Q, m &p~5AFѝt53R )`f3Pr0iJKof hk=.QU4qkiF:nt9M 917dL>>@M_4ߟ&~cxҾp]@4xTW' oj@J$0UzJhQ:&i>a2 Ǡ<:OF3yndvKd2J{sg[R Ҳ Vכ[KBVxv0N̰4˾ L(PѾv8 nWlҽ֖([~jyVЩ~mˤS4]n׏+ ~3#sf7.gHsG-|[:lc*]쫝rg2g-yⷧo,M,*2mcµ\߼UYdcq!G 2B]AAjX  R" (Ϟ=xp8X  a B4>  䤤LHHxw,MAyH$].ai  HmKAA4AAAA,MAA4AAKAA,MAAAKAA4AAAA,MAA4AAAA,MAA4AAKAA,MAAAKAA4AAAA,MAA4AAKAA,MAA4AAKAA,MAAAKAA4AAAA,MAA4AAKAA,MAAAKAA,MAAAKAA4AAAA,MAA4AAKAA,MAAAKAA4AAAAKAA4AAAA,MAA4AAKAA,MAAAKAA4AAAA,MAA4AAAA,MAA4AAK aoD7pϰzvڨ:t1jB&'t@:Ml&5[13>zFԫ/ںi\/GSxrP6((u![zHu2 ~sk^'׵no:ٙ+BL:u&>˩w\ EN#!SzoǢ#R;ei *KPEf1VzX@kOL- UrR@~4uXˠ%"IAaciʐ3 R! 3e/Dp=l9Ƅ)ˊc![}7@y|"`ܰq;4q(YiQ֭?DC.#' ao$%qI#a~\͙G;N f67VŇS&_ pǎݺS8SZO؍W 8?X&TryiDtt`ʺp`VM, i8/֍gS)Bq|,LҜ&cb3^ChC,SޑڝElcqZ׺#2S~4a1!`0%kyEcyn{̐kfk I9v|ާ_$]_r_eNY[P4 z IDAT=Q9Z0yIB<SL}ք4;_9 KO0y Ϟ^")x#Ҽo%=a'uwibhzF@O\:-{r^~AM_Q˖U^;h3`E3 ]'6s;foJi۰6wݏ;2tuoKa#-3_do {bZohKQTX4 sqXanaEiEҿlQ4m,:©?gy!Ż L4Ï.k6e-Ry/J6]{#)>x,aاhƠFEQ$*V t::Էs}(,MF'%\,&œ{n=q6awn߼~+"K-[q'%9 *KXP8 `H 8.Mro<_Tvthx8E*=_brJLlMm=$u8'Wj z͚N8qOmr F٢)!-<X[i4^sŚwNSfӡ[#^(!UAs7p怘 m-k %Ô>.XJ !})96-oѡJ$:H&.Q:C}Ž(,MiH=D̺mӭ;u2d~?o:"$GmAyW?*2 i}UTOx7l,ڸ&ٴһ,혊bOMPf}??ɔ珲ڊx(k;MY"9(JGq5:@Y X<J(vxq.DŽ]ro|xׄA3'Du]zb(,Mu}rf(ސtn \HXhmMjLhڧFl:f1]ܬDSe_ %adbB:ujiJMʠ`1W3{ut<;LUq@A(1Dz/ҽt@X7iB,15dKwV J ;`¤/@FU\CWԗm,AaYDig&%w6-dkFl$SXHҭI>RzQX jWw?5ʂНdH3ee7}Sy4u1)G%)̌]q?+WgФ^CFZ{&.]Ҙ?/?jഅ N}hk$eŅ<.B"{mk]e8 xP\|c0a}qfqC.kƣeҢ'wJhn6KYb)S/y,ƍ9O,$Qi? FMYHJCvݸJgfv1s.-%^{\TʊmoU~TXSx=6If.S[%Aa Feb?;CYniP]uԐبӻOL=yaEAraB6꘣>it o_yd?ߕ7>_ex A"5@PX];wї& wo)fN%6GY:D> X 4;44g)g&۱RIA"H AAUCA=`i  &Hq18еǨ }pзYgg|N>5O[1<{l,Ms܆;W5ԭۀ^6Խ>^owH8Ry P5(C[6+mcY7ȿQKk 9:4kno0m{f݇,MjSnhdgjl)c V!)Eq9B';JT1RЇ>5Btw S|}t{UfGZ"JxDQLT2?&| ![}Jea彟x!aܰq;4q(YiQ֭?DS-{jdaHygnp* /6Ԋ{>̧ s/̦(NϷ~߆k G]T*(KLzĀ^|2/vG:w9ahW{#()KB~c&kaM U[M8nͱjQgn]gkgR=,^~Nİ+%:PuluWlb7c2p Xr\>҄)x#Ҽo܇͟!=sd* pNV#/pf'p0Uk'v~O'˜?A ȓG)̚<{z.C˖U^;h3`E3 ]g 8䋻OPX7-`K]^5tuoKa#-3_dw2tj*usx)X4=d̒WˡY=˔E%X1/ojuݙt$\TƝc',ug/̕}eK)hfGh/vx˞\_P*iC3zRX5ZȖ,ejQ έߠ.5^z_z,cCm!aͳ)%G{^/#I%`n,Ogk_(/.B*V83(ˊK, !*j8$z/HTQjNݜ&4ݚHe[QF(b|x/]-ۈ]sY@R:u̮^(`4UaHBwέidN]*`X6X)?jr/a!׿ˤ"c“JE[>^_O\M|؝7ߊȒj=vsNW HGC{h`cJHKdDVr󤄋[c#SO}y^%rmH(4 |N(|%u,Ҥ!7FLqBX,diEC7#@!qe^FG Ԝ(VbciJysR~w6rVoO@At-fZ^,bm26X{W>d׻L-̇ulX ?m8BeR{WTMN_$4ŽtV7 QѯbéXz2luZUHj 9(,L>D!W0`jfJ P%ia.qxݘ=n~{:S\Dvio}gT\wWӇN͠) X6ST%rcU;U&eJbب7zMv5SU=<82)8zyڪҒtkwfE)JȤuV^ i*uy,F Ez2o.\fime$bq c( ai n:mj'fd .nnVD&,=5;WaXr?>VDET<+wW+2T"Tg<8mꝻ#+9ƖvPHQ骎]S/?-&V,V>~߲ 82p.{t鱈H7iB,15dKwW.SXHҭI>R,c=x-Z5ѽLѝB.2wL~!.!k֊)^Ur5@奦K>A(1Dz/ҽtJ'G(vA CD &6ީCXUl4_PVe*sove?7I>LIdXrBs1Yk(Ke,uX]3j&_ϻuQO<5&Ʈ zc &| 9t0<|`kE(=k#2q@nA 87;a2:fL]<֌G$EIOr+.jn6?+WgФ^CFZ{&.]z\䐝ÄFX/2 m'eZaq4fƏ8m9ӟ~1Zn4{^ HqN0oʢ@Rt tCyIcZ'3orT̸ٲw营X:Mʪ-X0vgw>.ccQ_=(3nywѡZʵ_,ƍ9O,TM(ƍ.X6w"(6ѿm/s>+:MU_#^ ގE/XyV?]]- U74V͂cI#.W?2E?D$!)3zQlkiBќ)JytasO+~N:N^ WUpԾdeϵnأU' CZ&΋?uԊr9fi6&LYVv 6lNϷ~߆k G%[}7@yQF lnoȋ7iiLyiX,T!aܰq;4q(YiQ֭?Dc 0\%!enwldcanbȋZ{>Jni,:8}M諧jyiD{>̧ sH32FZ ( LK,f|<9*ꇲu⟾h`,ύ}bϓ;pޭX0ũC8Y@G]:"Q Y%(;R(Ej8v_FL,L{tшRRK4OtC(euMMPܫY:yF,`4 _ڡGF/)]J>>fXFu5~~߲ 82p.{t鱈*39]ձK姅r¤5bTg<8mꝻ#+9ƖvP.{T^jT4,s,.K!#KO!UVbjGq^ڝf,JРϼQѩ2Պ.jם.X`>q<$N0nbC%09ۯ˭Mab΍q:.VJ/=pA(uާɺIbg!\/[/d!s*$w k,QVu|b֠Ufm0y.= 1|ޗyf8 {?,]C̦|ce~%BүnEO %qiVIA+3hR/!#-̌=@}qfqC.kƣeҢ'W_u O`HŹ7 -C('q?]"CXI/,HyAByUFz{fYX3ee7}S4u1)G%)̌-z@c(IJR^\r` ,^a)Acd15C)ώc\Z"J ?؊$N[h'__DYG:!WkPJT6[37>p^#lL83ގ2ACٱt|AAA(MH'azGG o=xMLw|G L&:q olEch֦ucf=mؽsUP* t7!0(Snzyp>t*~KNQ9g5(Ҳйkw~iW}\>b`ON3wxw},a3e$_[!VD]3QEUxahdgjtB A_PILH ͡|׻~%۽!2ؘӀ`i o%Yۘ$Y;\el6p ~L- Urt- %@Ḣa9|Td_ ~SQZ:3E).wi)&:/!]_r_ ~ta)6ӷ$dׇOvXԐ{9fi6&LYVv >/6ak4'e:`59XM\Dҡ\F7ͳҤO\:-{r^~AM+}h˿=se߾CRkb9ɔ>fXFu5~WjaG&gMM1Z߂,f{Ya}?ňT&"Jua'uwibhʮ8MOx7_ܺL: P} /xp(һ/7";Ov[.4m,:©?gy<1ŨsToV7X˥MF.^&Xl#k[Pt3͇2JȓG)̚<{z." 4&e2/`MJ);U)MP^\b4&6]IL;t$\B.M{xۊw29MN3h;5W[֖$!4eW^dD/^N'=RD42|+Kߠiǿ\&#92Tlhn |FҹgvmguB$^lf.GS( }:5(?X°b4;)y mSgҶYs[i\dJQ!:e\H xf(*yqfPXB@B+ĹYe2*eefecn_fI<#e@HoeMGD^GŨgn<.zϙ3ð  ("* J(&[rJV.ie{[_b[ 0,0Y~40 =y9s˨ů>ۣl;;Zn8ꋮ\7?5%mؾ'5ic'Z ֖LuNūfoabEDT[s  qާ RuwjF"LI_zaQº3y5Sѽ+y\E;to~NVBB+zl! a;KJ ,11TsaUQw/<|k%Y9%Om0Y=Zm_c 5(ܺiãȓ94G<1 3GאR5ar9Ϻv(N٥; OQؚ;,DkF;||R/IۗDjO`Ā^E{vGV0 4HS{Wܵtu4@So SN8uQY/{yCGz0׿ѷn e9a$Ru 69a{.q.aXs0-}啛+?6{tcXAiY'H;AskKdf㞩bG`Gmk+ޚ6XU[-˿W/?{>jS8ǕV%ѐRޑl}؀#yb͕ ks.>sK%Q8 O-p2Qe>:yUeOf(?loѕiɥGo6n2e‰aҨ3^"<*cGߜEsDdXpUķXr--.}듟Y0e}zΗWmM#GMqWg q3؆SO_+j&ײnXI}~Fぼ3JFm{].<g-U(oH_cntmiA63_!}ʎdv4#u)0µ/B7889'Qy\V#k,/L7clC#@PrBMe }eg /S,s_ga~y2{k~.[ nyos:0a(m7܉^żԴ<[[:YMsvJR H7mJg]U3x k.J>3h[" [Nw̙4oc$o?WmaOe_˜t}@BL[Biηs۰/qϯo/>\Zq]`mœ_s9T; wm%.="oxy TItf޹ ry__, ybr FGA[ۄ.5 W(  =7t(  lMA֤08 j 4g~r4ȋD:y:t¹"ؚ<ؐaivBdگ7mo_LJupF{D<6֏pwӺJt^ᴋPv3 jz+!)>Dk"sT'ɔ 5AK)ɰg=}cC+?$+o| x30ACue' ?iD=LY((j]Dqjj=Ge7`.rc٤VHܝc@Xϻ3n EXOWR& t%0m74dd[c(G/>>֗,CҚҡϿtBj)wSe6É7cޑ!~^.IbL~rfR`/VUrMn~9'>"&դ, ΎrX(?,Zsʼn˾~V/,.u{Ƕm{k6O]Q%oSC(<#uto1ceGKira/sguU'~ܰ3Ώr;$/<~U:u/ĉ,|(\oԹ' Fu;6<ߣ5?N`׼U p3gޣC$"بJW~{55/{i8qβu[a#sZ43!_֤٧ [ڡlTD Z~)Q4\۳Bٳ93^Y^a%밼\ѐm<G=w0s^r뎯ik/'vqqxkIl7~]cR^cw AnÒ,t~CzE${%Ϟbk >Ǖ|rȕغӁ[^[թX=tzV:֤"c.֒ Qm8CS/?9?.jϓˎlMwzM?\z'.CF(\u=)W~۳Zʅ{P쑔G?! p5m+G# 4^^Y}G*u i$1m\PO}q'|0mjUYItKUvgD +}Nα-/(:'U3@\g>#'&ya> & xsC2DLبSz*?7P@An(سnqP=ȳd^ d]1䖻}$k7~l$q%t%o.;.P.;zwm-1}Lp? 5DpLm2RHZwHLO?_q׷,X i2jO`Ā^E{vG}`Yr9[^;4n"\[wkKϖ_7`Ӗ*v^w/ߒyRѻ^N;TVmtӺt-oNcY鈆msWv6n|:1S"ʪ<^1U[zDTۺ|B¶5 KQ.̢F%Ԩ>q]e3,drV3eLHo8*Upaa)5*&6UI'֟*+;al}qJ0"aɖ6<:P^;44,xxz`f3\tKcc*&-:\ux{7ޟ3{ٷv|=EsjLmZ2cB@j4P|Xs%ʎYMyuQeӗXDs)OsH~Yչg^'$J+|jX1'+5Hrjң7L{7M1ͪtp[SW{r{t\V*cɫҖ/#~:4E@BF^\.,5ϓؚpn^)/1S+ C6qY3OY0V*f ?@oy}͜IUWr-tRQfCI4kׯTjZ=z UsmL{?>yBً7$OjxuՀ:ĕ ry__,|&dD݇'[*jFgHKk6B2lѿ m)ӹXW"򇂭 ' =lZ~BƠ,_~m7&_EZ w:ؕ%@Ac*  lMA֤c ܢO&(the Itj` ;RNelMLb`r =nƤ#C]X:ۻ:X(J. mii3Ww|*~LHSOeLjR PgۏI!jS];.)TusMzgS^kkk`i|cuv}uNc$$F=K6PѳZ5rjؔyW.6xDo%!|-\rsoUM"i[! iIi=#OkX`;pcȹ;Z7r9Dؚ#a:tUZ rȬQT$:tʪFL䞯i̥w⒞=dDEH_ojY(*F[z +Ⱥc-wSHh{h`7>[FH2Im<T_Acu 6k&ovSY'_@Vە,64䥕w@'I#)H ~xC j.31vmXց()\!P!'+` ",zmI"lM75Z-!@+֝ɫtunrֈ]kkA~n1B0>!@Qظ|7_iZ#+WQW4f ZXLԹdxtwW7x'aH!OMȢeL:Bv[jF"LIFMT#[gMˑqY2rGSI VnLw|Ba8]ّo8؎t.ᡂSul/:z%+DB$p {.MCߚ0 "fF Ug79aH|dAQ]m+,U?[~y߀aO[:hyV2sswlJ^ 7kٓ-}k=+R:Qΰ,˶N(;,̈́$ 4m^mj萿:)lxoBa'XpUjSSv=eY ``;v@cp4LIK;4wvI Q wnphsrak@CRbb[4}bb9 V.z )<aGnU4;,pY!xgWs{l!\jaXH}[`@(QZ1F%ԨefW_\H,;sW6-*CK|@ö<Z]mMf<<=08U8B=0ʊ6>S 8OöYHY(\\2mC+S(/,F*,(k{CC"+XݠjSBIUe>Ӗhy1cZؚ  ȋ˅QgP|oi,.omK/ZZ2h1ކ?KkېPAt9űi4lR H7mJg([cUN^|ӱ|-ʏ{ϔ!1Na^RxBԡB:]y\ak6'] 4WviwƕUQ]]ne6ćmmIgR=E`)~6dotv +Sc_Pgsߘ)bFU(xիf?eX54T^%0}K2cȴ'JńI(>]F\ǻiBBJ}ձ"cmXxrl.).m/>> =-oUzrF %1cac3cjt žcey3;XRp}˻kL17T柫6ڷpk!t8_yn BԡBV=cEI'w2I g/~|ހj EaB.o[|܂ ҮOTԨϐ)i=>5.`Cl  ^AMKxCԕ_kۯ: X;D A칡?u  H'[AA5ApJ3?9D 08 ' A?:~@w/+u-vއֿJHVm̖\0Gcc:qo ۩hk+2 KLKŵ!j1y؄;?ly{y9G!&ȃ:ag>HWp{!6f~rٲE;ovZV8ߌ6|j ) EQc aaէ?]QHU3^)Ww o': 6xec&}KSMF5fpBlMG6Ks( ]e-〒%$qLI=DF{j4]I~ǩJK\Bw?5=W0v·?nؙ}w I'g& bU%mt!&H?*[57~YR}݅TcM6*S9G9BW^-b̜y hc*}{_18z@p^zȹx䞯i̥w⒞=dD\ %/2PkmzeAaMٻ.2.2=w:`21cm:;ln0^Q>2G/>2Vcxu$MXMQ a5Z#&$@vZ D [c=Q{.dgobmm 5 .l;ZױnSo|;Y_2aDlt?[|?p8 AAB.o[|܂AD`k  &  ؚ    & `k    ؚ  `k  &  ؚ    &  ؚ    & `k    ؚ   !Jt$lFZ?E@pAAlMAA5A:y4v8K Npd~CcX"ؚ ddldW54poqŮ!}ڜD"Qd7vҾZy}D,KID{*਎_U:kzĬ0!v' >tlEwZ4g36ƊBHFȹ2RiB/Sl{*بvRQ}~TvRלN挃g&TP~F`k ewSEJQm`z%&O:hj;;r?ژxJg%_*A_qHlt:}Ah5ܛfsG*gO._@5AX#UxD~L %pY  `)X|ovq1"(9-ʹFSM󯞊TwHҋA)yxw[0 pjZ;(𱾞akTƋ{h2*.0g7\VlncqRfNƜGuNV80ߩBduI6^OO-t#Yڐuf{o(.W+KkeZ,S ^9/u9'vȚ{2}}4TXxlM 0: &NEXfæ&WLHH95rXSQ_N_S3a7rYX'ZR 5eqz%׳sŸU!|&mLz\:_[A`O >,by~jw-UOr-,ӴoGѩ٫<,;T@,7hx*E”5<^[g.nKk4^Pu6;񡦆,(.W'/[үDg[ ^97g]N?'6)tKFIWαVYX,&xke7 ̥XQL@βwr}MBoh{;NS"o6pEH{ Kbfrٞq6%@js+91Jв;qD %s[ef,͂۝@Np_ED`jb,%&~;)f Wc UQ@ A IDATPQ԰Gdn>5Yf:TiFרJD$MMsu2;&B,6!&,$jZ^7?J385tR=$FK|[nQDeV$YG(nx *O X|_G s/_lu{Ʊ:R]$`@ 5f] /pT&Ҏ(rڗn"L ==MXօ"c*#ugjuD"]qCSQn&Iattt+\\W51XӅ&1ֱb[:ʄvi`٤]+6Msmxߎ 5+ϳdQl݅xZQJ'[D~.#% , I=DŽ,o@@d~Hz1׊6_nA՘it)WrnNVbn-)0oТ!f LRG]GMz$tQ*pfs\Sɞ+tauP*G̺]GFqP1Ymu:C}QUn5EU^}M4'԰<e?j#*+ aDZrO$tK%(Jo# "cC`CW^h\W7;Hao*YN"^c)Z? T|~}[wMęg^V.}_T`_a5W* iBsl] B| {z_i5@ 8:5tJו`LTufϵ KGB,Q֥z`ݻW TAA:2AA:ؚ    & `k    ؚ  `k  &  ؚ    & `k    & `k    !J8ƠQs~.t(AÂ&A\IG=D)`kY!Ȯ"skhE=]s C9Dnn`a6ֺRwLIJDITHP6tΚ1+L 򇃏"NtME:y6ccK*`ΎK }.+{[a:>_J/S+'zU6;[sm k8X*HxfrHg4XlMLwJC; SOغVBMogGgOۘxJg%_*AeٍdiR|HeåWk&kTx0j0U/vAנ?^>K!,њ=UFfZl≀_=wVR{ SzBA1"Na (vPc}=$Lթ3*,dT\`Z_n>$7^ؚݤǜ$9eK(띬}255P"t'Ym>=zO>t@qf}|, Yc%l#@{===hЍdujCŚy^xw%/!v] Źd~yy\ĞD KΗ\`fJ@v2.'khLHMRaC5At0: &NEXfæ&WLHHy.rXSQ_N_S3a7rYX'ZR 5eqz%׳sŸU!|&mLz\:_[A`O >,by~jw-UOr-,ӴoGѩ٫<,;T@,7h., _oԁ0:.p䮍ߗU׈U]{<.Uc=J0e VҼ.ToM@xr|p{5?+ls%V.Jy&s|x-"MM;^Ja {Xc !:XlM?[ke7 ̥XQL@βwr}MBoh{;NS"o6pEH{ KbfrٞqPE E }dK^efMaTOpDSTrQA )d 3ܽ:WAc~ ]XFݠ?TYhgZuF4u|gg>5Mew5أZo|u?d'\rB58(?~B,W:!G} (^㹕v-s%nmKyB.[^[eV:*u@Fe,,Vwݒy J qA 6?%[luxOޠao*Y;%EmDzSw6Ӳ(AK\׷ pAW^V.}_T`_򧄭RoK"cK|bc*O10?lMc@Wh+jƟk/P?+Tɲe] noܽo `F A칡AA  ؚ  `k  &  ؚ    & `k    ؚ  `k  &  ؚ  `k  &  ؚ    & `k   j Itj<&nQIs'GVo!`k QSa}Z4b}ai֌C?μ:O iҪ-ۛٲtcfai!Bq#z䱱~-D6!D ={& KS$Y_,pH_X!f8[(AlMRQ]6 =&Ϟ*Bvwۤobiآibէ?]QHUZ1C-y0z\^mbH3AqWJ eoy9G rwaMm`g3G{K6o:p "̙萨 667lBkՍJܳ Mzs q"KyxϮ$?L]TV{|͋_^ť}|܀poV]zض-{7p|Yf&ɤ"J#=mKJ}{%?^fG @H> ABMO^6w3GEB1Pܱ9_rOag @(<#uto1ceGKisT;X ؚ ȝS! ;vl`ĦM[Z 5J@=U,QlM7{bÊOԱ@l7~]cRշy>ò{ o YTGQ띥ǺOy}rعV$[?*'F||[^k.P;.xx'ٍ+yWk_{@ cȽkJ'o 8(tmOՕ:tS/5V.\@9h!<K5}Q! \s )4\۳Bٳ93^Y^a%\@ [ڡlTD 摗o-DlM`OMZsrsEW_ojY(*F[Q0'm|6[dHBΒKL \|@3cyQ,&\DcB]Cu _;0ol8I(-!@+ph.B-c ;8=3. b"Kff=]KZjgnuDQeq`f 8bb Ù9ssMGs1X[!@4;+NdG+I5gϒE3~N+1"}lOHehW OFt'ՔfjlkccwԒ^#jd*ޠ!mtKC>my!Ԣψ~ +wBKZ̮(]?FХ={G>Lri\)iB 4t[cq9Ti7<7j ictܶ$MFX7꼝롍維;sʄwf7߱?Qk07)1ǵlc;б݈y{D,翟??wȰ{:*%Wug%E a-(ňFndԧ'E[%u6~演e(7-9cB_*ɽcEJIyzȔY~zo7'_a_l=`[b7fzeҏ?1y87kiQa7_cI޺|s& 3D/;ލJUrv cGZ.K:tFӪQUu޾fk>g1ѨI<2CvojfB+ E%$u_lH;2sޘ{ߞנՖzٲ[8q"GMEpO^$ή"&]X;)!Df_u*Z&D.ۆx=F>ί4A4*v"n[a>cΝx-k5{ʻoBegʭRzEwiDl!]Kb3 i̪^a_xWg̘9#&~{/.*V,rqrjH-Rѵ2@h?KB|9ty˗K_|cU-K~J1lÄB)Y08Y7z@^ʵSkWo9Sk Msۖk{j\6bL7xCɳyrȅYǶ|joZ"<=Zx{:m޵+֟WnPѡZy;)+T{G>_>ow>QuLKɹccRZyp+OTSF]Ww_|\ZY(3W ^oGU1sg~YպK£A͜ 0j܁kO)?f%/W6kZBn䌉ݳ~XdVʥoJYeT([cSaI[ϧX*.][1fv6ڱFI&}a%+5CڳyjHs23FYLrS5'm_Y8uhh^[z}^. gc<pLB$j;.߹=’p$Ru5};o)ⱯS&!]:,ySG «gT9nI![ý*1z2Qׯ@">]8 IDAT9O6vNK&I I B䄚Qe^vU9_jn~R.[Y'7x0uG8}k#x&W?g+B%Xz%7j!$IbBk<_ڶO̪xc˕D>mcn,yx[7ͭb)%Q ԓ=زTRP(sP~?V& /Cxg]2WM^Z#e7lyѨW9-+>ab:`\fI v5lT-tC)+R?4jU|[U~KV y$ɚgɢF$E6Zj7o̙>yjk޽z9Ҧm8{QF)׍⸽Ǯ AקiϚm..;EZ_^®FUF I6& daB84.Z 6l2KN..$[/}|t!ڷcP#9-Ekso䥤Onͯ󯗱u͏G2BެT\f*H-/}Zdn*Ne)Hݙs]s[[eTۨ*ݽG/44D[M1)./,+IF)0(.>[NQkqJxY1&9Mfk p/䤦x77wHu4ݒxs7OlpH'3b_v)維;sʄwf7߱?Qk07)1mF.%];*(Ɵ6k] ,^N;a ;UgM zРJaI5eTר*ݰ:;/%s./H 0nZRp&I# w$^=tl7bC'x/io?yQ3gH& %AF9uϊJzm]_Rrr͗;<DŽ4;RU{1ҋU,2>)=W0(˨Q/Jz0۷//K[0iV)/%z١Xm?[˜x̘K:rf==\4]B]e&QƎ ? W\tUtBn#p?|P=O]t#%tO|[9oLýoO_ɝf疡Ֆzٲ[8q"GM̡M挬k*GD+:qq4>7tEr @4h!{n誱\%õ?!ŹIz` :S:<!D@4 u(VV_rlйS{|is._~Tku]@:'eEo)ytBMQ%,kZP3=UrlԴYl JqQbKQsܷ󾍻#,ZxaӪ]oɬ&.U , JX0sƎҽ0?VOnZj9m鿶ZVe_ UBa &JÂ_Bwp:R^z˙Ó~1tnEUVK9uqo…a>82sڊ^ }󳗂|t>ba&&7lfiv#gLÊE'JU.}W*$HC̟z-YV㜣=-MMn3=tfmV!KQyp7wVh%#GϞoysyor#rIX>Nr&Cv(+QǗ/v,BI:4]4|X-gcl-5wך3}:N!{>#rԥM?pPª(k"j*?B5O؝5u6ODBKIWͻv?fYrrq]C,S冁A>QB1yFŢMͰ6| q%*׳ʻLpusjTug`بၞ?iP'I.mF/Ŧ{Vpט%a Zד4^MZu٩IޑR˄5OWԪЈ>m]qlش5fAǺg]ZZ4Gϡ{41][t1aDPq+),z$o󱁃~|`^M ;bgj"iǏ]47 /l`X>݂Z/O2I-r, өK~ұl 6 ѿ5yǗKWLWOg48`_k԰~Q*> bpxG:7YvŐUZv,|]ڸgmUXqDs'OdrK>&ptgZm-eЋ'ZPM@;棋 nQ@4 M&h@4D M&h@4D Mh&D@4 Mh&D@4 M&h@4 M&h@4D Mh@4D Mh&D@4 h&D@4 M&h@4D M&h@4D 8:9 3ڵڷZCwk׮BKd]o cdh't@B4unJW O>cxͤ.ZmV*qP] x(Y}ɐ?tNo6 [=lڎ_sߏl_Զ;ot 8UH7`*?oadn.*sƞ'5# mJdFaTVT<ʷw,7IeN۵V<}mo;r1I 9yoצ \-K$ͤŦ[ {\EaNzY(f!V\M?kIFrAeNٵ-zwuVE~?%q6.g6 gWmP{?rz춹}/ק[uz8ʏ_!t}FBɵ'yyS m9$+GvΉ?tڄ-z8[){s)5su?R[ZL̿ :2’avVg,ՑvT )W.ؾ[9xxB5Bm՘i_tIǜHX&٫]"nWs/ۘrjY eHX  B-'Gr8j\Mӳjn*";|2l摢2&ig-{q̺Kn?uz%"il"ťNA6' Tfߝ1]+3] c~Q)8jRĤ 𰕉s׾sY+zZ>u$E`[keF#LBXI^Ɛ9dVm4K.,ٿޜzCCYqpCaw()2'ֈVСVͿ>rx$C ɮ[)yg36f_)+?9L޹{ cFyzٕuqZIHVc !L+5J҄`L\oA%vY!gT􀿿jK?&fWfȵI*mw߰k嵃tW,iIN%:xsBaֺhr*@k49l8QE_.}°waߺkiTK~S& ^75U8}34޳G-HӮɪ YYY/%7t}4ȶ9}S6$0jٽըE*v pZH2H k4C{R%>kԜSJuO*yz.=5ǸLzvrBh_hB/za~qiξk=987vǵ6ի%mo@o-k Q/o؟3ڥj|=!Py:}lb;CnLj<B4},R6y&u:3IBw[[\Hjwe:gSӓ,l#:*WvՖ=yזye)жrR'[O#܃kem?ܵ_Ǽ_sjj\Zw7N!ͼf^1B"{\'-޼%vhlwJf@'\s/SSjv|n̐T!l`u};}_zYI*ˬܯ^z|L o|2u`mĜ;?1_9ڗ1=Ќ3- o3"-Bo_f*{n%fέM~ag0BN*֏>fe!P[thouIjh瑗dVyS}8z,Vk5~~[]c4 !b0^}"t0G_E |<ʀ꧹2ҙݑ]ޜkH9{ Zdߤ;D"wg%Y}b>7wg"%uhd.>!w:=cl줩[Z?^c+[tCMoj,t3{GjByvόn3.جygXr0EqnA7rZض8wmI lx xOqk7k#|0PmLlksM'jhc@&l-\65PFoFj9ԥqbΚJ~ ,"#1) A4,PdY>v ZOPPZ}X<$eImY6tFvi0 ϟ;r&ڴnPWJ_tv=?r̔OF?@xDG6**%oH}?\޼1޳ߪy M}/&~=>a3_Yfz3T{=֛4ᔜO?ms)tKK)lEM5Һ0bpV ܭS.l]jwtwxD͟z{+oO<{g27 sƮyOZ!VϷ&t}P$s%7u{PV<5ϵw3Z:e驫1"\QgްlÅj_{ot SޅrSO=˩BT&c)wtјӢnXlO^Bɺ^gءVtB_i?7/B(y;|S5W+?/%'_˖n9w~xoϴw~=_S wTWO >`z:jMI{V.Y}"CUv̧KlqHs S;l֬>f^ٜ |w[Nߞ%CP{0)pE]yM_hdc"tb;m<;JVN-te|sX(in^qe!9 $BG&ikDІ3;s*vgXMĐ݇+-$Zխ|*.D=utyzKK)B+ C%?xi6Bܿ]Sm^Hn.*%?YnkbcoPOetM=s/%_{;IT %축gP!V+җͤC{ *P9N&<ۣ[V󦤰! }ջO_T46uٕ Õ~fs9?mXO/2S#"3FtiޙSL8 UMEEƚc&BHh%[0Ɣ߿u!^75g?KE$fo.sb*ȔԞ?&IݽK *Tfny\$(~"I&x !:QHQyiSǪK?,SZ#4k-k_X"r>` 3nDS/svFl]M MVJi"ƷEN4 !4Fũ![WѬgWWFQ䄋nYd.ԋlߝܽWNI팑S+جwߖm;{/ڵ.RAxIa(yyUHn !,YeRu:ɛRت>|u+oMɉemRE*+ N8oӫ%EM8Sf=e!ګEswS|L,M.a[[$OyK7okoe 3SurrL)0xp߸糋%kO?`8Q`Һ4sSE(_K9v6剉m7l Mȓ-\ Nl sۙ/#C~*\W0CuA_zCtvIk]y^⁐lܴvi 䅇30hܰ-f2Bszɤ?yɩeEVE$%_‘vԹUm9BngcRdqІy9\ْQE]ܨMt ag/];]d!EU'Ñ8CI/^:fTa[iFÆ_uj K>KLJ*bE7]ر%_CIN6v]r#?]~'t5?'H)lf w"2fH@Ԡ4toԦukU%FE3Wx]?BGj[5p2^NuхϣoLpl{1y5J'EY^9pE%25.M$gF1j3:m׀AGn]Ic YB;l֬>fI |w[Nߞ%CFޱwVIDAT֔g'2_?B%o痯t$5W+?/%'_˖n9_rJɶQaj99X(ŗϮ1ϊV6ߞi>G3tWSQMyƔute{H7ɗı ^ !Ȝθ*Y̳;4|[صؑڎ`-'* W뽚o1PDkչMܺ<|hIo#'Mq_#ukY?Ouܓ©iߧFӷ|qv4x7[ϟ),"fBahĔɏX:FмOOzC7q֮LEɩyϾ]KH56N"-U*| !Rwp3s}n>y²(ӝ.Ɋ&_n-sV*?y ՍO!%OU6^>Jثy+o;[S{[X6}rtw'yir]+Tؒsޞk?qK,Dxd!Om83͙Q! BD${u ?raMO)/0)R.9 ~j;ٟ!4~ e_W?qՐpXUA) ?rZFbbeӱSݢB@#EN+fLлMJQw /se I[{9jG;Qr6<"*?RR +\ rql14fuڷa=Mȴ++?95"2cD9UGAM|$Q꺍Xڿp٫WסjYwd:AA>G@챧x[ZJZI;[[ C%?xi6Bܿrr=mW۳osSwP3Ĝ%[vBngŬVtMPI7*\/$|iٛ3SA]?Rtju [ޥvgJjy K.Tٷ1a仚Rb7Ѝ;0hD9߬9s[:;w?K+Zؐ&iܸqo6irk KTv !bMϯmv.X:z$&%Uz5ij^%jpIENDB`brick-1.9/docs/programs-screenshots/brick-dialog-demo.png0000644000000000000000000003363007346545000021752 0ustar0000000000000000PNG  IHDR=%sBITOtEXtSoftwareShutterc IDATxw|e̖IBRTEbYN쇞 H" JIl!&>2<(^vNWee}s*+KnJ&9,4,sbA&`PevkO;WUZ\Nu(Q׻hslظa1nmlBDקj(t 8bZ[MIIM2pf COMMθCpb,gT@%Mp02D(&".!M@3m/-緶y%|RPM qMp.-2)25 aa9BsJTP#z_vŔ BH4Ԁv}7oo1WRC/7v*NɯGh"U3R4jlxP"E.KCSr8fƍG(3mJJ:dnpן/QTG?tcռ#0j8ik"bRdj?&䜦u._ $"J掉ny]Dq]Svkk}mɗ͸iw~?D rw\5g@*'g{1⭏o;pױ RK]QEC>ϻ0!2(ocD/B{_{S%Fsq1buw2qpVb۽8~TZ$1~St3Tfo[.JE &?1>[ Ww1 )0/G6zF o2BuW~ue{>qݔUc{h3rfJq(e1%'=ڮuyqw|-wx<fOw _/%!2s=EwX/eO.,aSf满f{h3rfͨtoEgDDw9ny 3D?K{MZv 5GqXdiys>rȩ/ c'993f!O"󨝓6kկ\qŗ:/2+/^X/(OScGM0@ugλgܨQǿkk/uNac\7?5|ܭSۚ%7aïxe]j㢍z'_v ;յM׽Hji39w F_j%xaGDϝ;c̨=s&Ə>V] ;0Xij40l-|{`FN96U>yyYnQ]&CFRiKuJN&[r#xmmG_Mw:]b>j##K~}UqT#EƬ"/{c7 c[o\٢8κ^g#WRͱS],%O[8/kÚfE+ueh";v(J{殿jQ1=@DŽ^V;_70 Խw?# ʄ;l-v{`FNӌYSu4iGT\{vcN1LiZC[j0~ jH{x' UEh2aκ|ԇCԳ4tI5Dt{/Wg~Mo)Ў=\N]DQ 8NEUEψݺb6وFn4< Ny:GwrB; QC9.b?P!K;ЩuFJSvek߬k_jkLGE'-qP}kٲ矵ϛ:|]iw!};sxZ/9̥4IK*NSom ,+MJ^,wBh*[>&G>ÜW4&b3 uU*u,ȇtT6 Ѳ6]Z]mUR$sM;ͩnPow{v9 ^d{N\G3/ln̜mF}yܚ]m  4Yܸq{p[+s~}zWd#i_9WlɮtW:DsrmNZ]}."ZђO{+6$%tMtvZ 5Iօ?q3#llΜy%~ehÇ%IKe8rDQ_ku܎V]k\־( WE?VGmwziyj=_z&8ً|yUmbBja_We.ج,oh%[z |;my c%Mei>n<;{AEKf~q;xe|]n3]]Z-Z}q;>7uԙ?ˀQqF/ߋ:[]RX6w(^Cwk^4#_>6~I`sC ֔)hkƖo)S)wm5rYD ~}=J6Tu)FTT.K:xýzYoH'|}Lw95qx`sYKOsZsרO6wcbsN{ڟa:;cTrWy:&]IJl1+ED4_˗o:t8}KߢRB7R$tLɩw]".q4zIE[n>BKs畊DsCQ]tEUX&M[e[%M=Spja q^x۾rE]|;fON 7D\""`ݙW=ZY!0i_t~Mٵc,>,i̕aWG\Iyi{W'ݰ7Ğڗލ_v1xL]dKE&\ = n ?n7k;f]z[yCL4JYWrUUvN\CK][8M;[6J_{j~)c{ڶ$K;w`\wDߛl{w0RR:,݆YMV(R̝9 u-Sߢ)8%Spxcz:uMuG=c}U=^0xi_Ն<8W\rQ~{] Cf_Yӹ'wܳwhw=.9+Ñ":پ9c1UoT .ҼTTۭ7nxAvzxz`u߼̸}²ݴ-潻;ᱹ乂2uA'O)6垰's|8ULZTxj@KIx˶U/ީxL ySz4EwQ*jG=KR G;=fj>E]ؽcY ֤)p/mzo+]ǿ(8f@^#eM=Sp* z"`[g->~'K:v. 4+]o KJ;_L[~3{qOaZLqfwpLMqZl#ϖ>.VttVɑ:K0E9W&w+x˭:'< Uj߰0s$qM-y9w;<>QD/eܿzURB̝ot|*H:.mzBF=PgE"^QPMrhZ ~.y-Vdt&Hw8sKDD|5)+S3ϵOt+""f.r}I|j-_%T1 H W56ckLY47}exc|wtbn&v-q*݂wq& b[nת-?7nG}JM 2Ue&W0Hgn[5.yjR!A7O U}֥Jko94K.r9'mH׈wW+'vѬk?% @8Vh['iZU;ӷ役-pBz?@&4 Mi@4 iH4 iH& MH@&yPq]O/$GcZ2mïqrV MJ1' 1]\~b2ŷmմ:}G=CԿ^wЃ3?o>ų!1ؓV@0281;-゚5p]0O3d9oP 6?V rSKV|:S1z4(@g>+RztpnnCDJ" ~.ٛiaM^/]RE+w ppMDJ9c!Uի}ϩ(A3jqfݳqU!{{)nRfm~h[[_꧈k3V^/оGĤnm zm떂jx 9!;̤86fo'eЫʭfg#E5uqY7XoUZW D\7O2rp7xH ƨ@nY!nk=eSkcT{#˞o@'Mq]Eоgq_^!56cˌ7w,^\Sƈ@W~,2<1~:st׻^"jmꡭY}rv8}v7\05ks(qAZ5fD6lmC\GKӾɫ?$CZ]߸6w~t lRkDNCZ.-^~ NQh$jT&CjH8ݭw_uz[='|5_Q=;2TdV%i4OUOr#θbK׵2(%Al HZVc4 ^QntDpY/˴cY]ی',-;kա;hz؝z"sLJ6Q|u7gmH8ɮ̲ )?97"b0 Ղ7V8D$f i?>uW=,i5K[Dɴx, мGSb }]'01[}Wl]ބ7XzamLߑ?y 'M !1sM z{0lHgj4QJ"RB uYpܤd;/Kje( nmpϴ;ug+U*빯zWْf5Ux$oO, p*hX]?3|U?)oC&p1ϋ}M_/Ew)sIQ vĪ^O^Z:;ԭx^6>Pl۟k-v&pKnSXjcrr.(I}jkrrW޾R8$ַgN[2J+jC{R24{+np4bOB𢲽UR{'-q*݂wq& b[nת,?l?WцRNX5R."ic׊*VA R#%~5FSnaCWY,"R}5AGF*JgǞTnj(LWոy ЌGM27-qq`skD5 G7Dޘn~FnQ5i'*yRޘٻvhֵsʒ{I"Zyi26bd+<]UeM.׎'=}SϡQ]w˹?9oCFi;>o(qA]dzsaPVTzO-?:s˂VRPwK.|*6V(;r:9R8xʎl@xLsL͝ ?ћ+~~ 5(v}]tfvf_ǘٳkh5p.Hn{ax moihb6V5lhht pH-L1;x{*ݕgӽ&Ό=ob^yJwn5cgx)\Ol͂4 iH& MH@& MH@4! k 9{L<'g7w2PggGMi@M{n4Ө3pZ^^ .sU1};h~}vcѡY6_pr^^Y_/w@x!&M TE^>ΪU?و0o-9Fl7 o00i̓f,4Fse;;yK̗ =A~pK&?rCO<7yG)zF{s`?"%&v JUM& MfO0"!n4 Lt*"3c~ܨzWw5G+=Tbnwt*6SbJ?l_[ h9~qY=oߜ`g?og^WLϯT.1Q~-)\m |HDŽ~G~XSTT8USxxdZh#zߔ]PZY4Gw6x4 &(Cs;\2;bvšq۪Ng8O"@)_w (ټ,0۝̓#mۯ,3z6}{za-:Nꝿo8 Cu2luS ="Dm`4=oϮ awO,{~>{a>8׹^7F1TNjTDz$\N.EDk\=?TPX"vkIWq C?һWn%n~A~ve{Mn}YݭiҲOH(=$^~eSWF#:60/ɪօ4ivR?pF8uE}%"5&gpTlHLN?u}?ȡhnDC+:λ}Z%PՖ]8tZEsjN ѨGG[Ma 8mrE0o]i>"Cb5}Bb {2\5C4]QtݦH(~~JMAEOU06me:C>Eܫ+\rEWc^39:Pi^=⍋c$V\)>%cG{5~j)i['Ow6w v>e"'&tkYA~$c0&9w@}o t.{G߮>ժ}VNR=rxW_NԪCHxh^AQvCZ3G>bW"zi-99;PzLsL<[^rs?xNro@1lYj*7;Np>gϮiӧ gEx Zf?'Ң(Q@&4 Mi@k-y@ R|% @&4 Mi@skr4:p MH@&4 Mi@4 i@4 iH& MH@& MH@&4 Mi@4 i@4 iH& MH@& MH@&4 Mi@4 i@4 iH& MH@& MH@&4 Mi@4 i@4 iH& MH@& MH@&4 Mi@4 i@4 iH& MH@& MH@&4 Mi@4 i@4 iH& MH@& 8<<=7iΘ;Ν:Y>}D$#=vݧϑ)8= 'tY4grm IENDB`brick-1.9/docs/programs-screenshots/brick-dynamic-border-demo.png0000644000000000000000000006222207346545000023411 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxgXl {b`&ֿ1V%oh%F vQ4鰴-lb0gΜ<{,<RiѢ%%,"O(ԲtmεP3TąFD]*-U*e9C{0 ji8GW7-mR } moq,˲2Y\ӣORw3H'PIE&u???߬sJR$Balb76/++Fs@!-+㛙m B!)r44VF&WFj@ͤ*2,̱VX UfJE1mڠA%M0W϶V !* |ZƷD ` ӻi~-c`ɭW.f")XD_ J)dUw7ߵ=u^m_o-_|a^/^I΂UIa?ͪYlPL0W2akIf Qz{4+7U Ϻ{kS}=3RIxlkߙʢo2?:VO-ϥ(&|p=wⶨ\6W"ǿbye._MsՓz t91W|n=](|*^o'NpUY_@' =R~tRn  @#'yK*ri#?[Qo=$#1Ma`k%/HЬϸA]IUVP(5dKg "bt, e< G (񿧾oѳ{}NYZ()30s>nV.s†[eTROKDC&:FSb1r+1@O+,̚tE1~M|%;@=p֘>}Fu88RwWߖGdU1 ;OLqg䋁dPxVMtNc΢9,O 5V qB!_a̞,|&6pv]ݻqzn\EEYyrtE>\ܺz*i~ Ơˢ߶0SyV &ȂEZN嘏e~7Saq]z!Fg֗D$哧{1P0I'f}weؤT;Mq=MI+:7s4y< R(U4DWb6oѤKٜ L,LUV1iZevx9OSm8""Mnttxf&|wżӫ(U!]uX"Nò܇y?y65V>B!Ci4ާT*%UQ&n5lײz*b RUhމl:s5\S<Z>76/&kR 6iނ&ԏNvxסy\<=&TSİ݄Q)ԽʓP LG@xm<1WE\&=}ՉK&mf;Q?GS*uW*sԤF?3304 "/\It's{HexDz7j;f =xrI|AK~.^RYYw+{ n'$UK8!u&nvf =9^T.K$'wlVU8AMAZ44 yރ}*Q]J2MHhd̈4B3=AazACQ8M揟W)xGGskOƗSG_kΦqs4 T&ԧ 6nGX~ީ|5E).5 T&ԫcZ o/&NK-rp[W4 T&Ի]9|53Z%b򫊣=9Z)xU5FSd(J5^6l>i1\BWITNr$YhpPPPucn݇w= /#(waKi_կ>%9%Ú~ ,ۀHj~#BQNV,8s6%B:S-~,57/S ^3wI+tLOg~xGYB Q#t~I>OsTVΗl?jVfhS#L{\ C*ty2;xztο]@ʹp7..x|ă Xnnf'Bۛ8-9qJU{&1L^v(g~Bnh&C{3W˪{)\R&% {bPt)4VyB8"h^v e\9{*ѕbͩ&]g fʣ3zZ/K9?d0 VQfyoߵ o<0b:Y Iʭ,KZ2݂eKMC7eoY4z~3NKM=H4\Wg bʥOsclb7?K'ݙMlYiֹY՗&vTD\hƫ@ …ĪjYʬ+߾6?: %3K[@D QErJ[yNkL_zzC4/19+RIDdrceJd &ۂqk5hb XbgyETΈm-k:ŝ6jSOi6Z}5=T? ԍU?*!{)|/_37 ԧh"R*qZZZnnx~$%&>k||(P?R铿*rhPyv9;߽W?t$YhpPPPPPPPP]'SBi<==f߹sg߾}* ;}2i$;;;܆FVVV&M닣}S<2&cf\T4CL^8G跢π8dʩMc~Hy8->Oh;5ڥ>Eُ*!\ Gfo|4@oK[_u{JS?N_vw>D6%uLH]q! 4L/m|˜Y6Vi=Z SH|`i_fedr..fE 4r}Aˑݘ;{. o S,Րk\kubj^cպ؁yWn%kY*Ʈu*EK[W˽:K?֜/oz 8Hi,KB`f3C[s%k{KTyrQ&[^7m)^u_<@(Mip@c?i$KN|)g*_Vq/Z`޿_Lѯ3_}|Nx/JDX%Ee 4.%;DآGUzvg`iؖpkZw}K cdӶ8IHkpYw}K sq4ΗPPs@)..j[bee%HG]h;(W\9sf˖-`5iO?z*ex4n7iҤ+V7y ___.i J4dPPPPPPP <' η&9/%h !CV|NYZ'IRVh[Y (}ZVo?x}4VJn^3ft ,,^CSf|6]{]'Z-JQ}-ְ+3H ,xJy\\Qbl\pD2yW''[#09q%۬#{8 YyIa}cz܎B""խm3+4|O|꿼ap5Uuw{[qQwnO|󎣧Mф+J wTE :Yiq~l7sZc{D7Gsc}ZuoP1KD30 Y""yc9k?p1Ko:{%U^YskiuYc+T7FTm i^WP 9FGX*mks4If:| e>/qo9q|7[3Vg){_z-|̳g7-.dHi=W=`kZWMc 'o* Z :E9"]ik|؛Gv`<'NZh2"bL\ ]:xHVPXCDu|5ҶkI%dzȔql؄_-bgtؾ4 J\iʍ$rưsE|uFƪ;koK۴җDEgT(au  #4l{D;UB1?ſsD&pڱO79fޏP!Gy,?$"ƨ˜ t#o8-娆uP"߹$0l}zvc@+~=+b"3vǍ8saOwu|5*/5/>$,RI-H),5,UsRqHtX`&jXҪ9S18XvV.r ?ƊlrF2ex}dn|f٘U^X+cL *987]~j6+*ZݧESB˴gef]s53 Io-qB˵<-!CTޟ+s=rVgm砊7Z^V࿮'>jmE(N(~ 2 ɽ34%"Re^yy={{lwmy((;SzЎJ>ـ!"b.ʹĢ2s$獲ũ!~|JMgDǫT(97&- 7e_;㞜P^hjïH\-թIJwm1yʿĺdw:k~U 01l}>8g2?ž!gl^ɾ͑>ҮZGJkȽL<8lGO`h32ݳEϞ]x7Sus;]FiL>"PPr&?!%)(P1,4<4 4dԏN&8ؙ= 格)ږ.MلnѪwI^ɕ3s; T\?j$p``/\/&Ƌx[W=M Pm3H;.@@ysnk̍ Vp`JG߰"GO4 WvЩ|H}/:]r Ff2"ưՐ:9qɡ|٘ %O**y!FlgKrcپL K 0̽(͆35UWfNPK 3[GEU7bۙoƬs W|q]wݞz\ifԥ?uk!F:FW{āAS (/Uq)7Nʓ枓gY*-W1=Mqo~"F7W)[M\ ]:xHVPv*+Oo9q|7[3Vg=쫫YT56WK/'"o'MCıGgz4'o* Z :E:7Tڐ˓fǽ`ggH +%$i%ydŠ^v:@c@+~=+b"3vǍ8sPq+R[wdxMz:=&1?ſsD&pڱO7IZm=jipVxEta*ȧ_L~j,s|#c*ͼu?XCDQC^T $'԰TͩKř"ѳsP4YSEg>K;T熪C/$4A/45T1b8#0|fk[6+*ZݧESBuIX]nǒ^r.-t W&ŒGN-Y\[2Ddb?q+O]mXњ]bCnz^f7W$92Đb^]mpI^C5^a@h4պ3.wvѪe S~*0?m>8PpҼ{;jТϦYG=-\$v^6٤b+OM*NwۣeOlXL WZeh41Lm++ ({fxlZBڳ %"m;WKujRFyץ9Y˫gVo'.&JV{4J|fZ:i>r28]CDC+\B^.~[o7bƩS}~:qQuGbtװ. UTjttuRr5sKv]-)lmQٺ70r%Tn>9\XO3ٳHӵ\U7;t5ҊYm[0q+JJS;$|\kn.ձ苕M^ΐVtN[kAGedTT-4kd&Pe_S͢GnJmH ߴ05VZ5SC=|TQ8oax%_Vӆ&ΝKZVƍr 6# XP_Hns#$-~[sAm?̩ǽ<ϯ8hڜ@ Dvj?L?|ުiB\ZRS>_qdQ}j($9I (TyF/4ppK!)KiG~QͭuȮS͓%\{vԩ*P}>8g2?ž!P#oTxo1Uua(^γg.`h32ݳEϞ]_   (Sv棩M>(-$I9Po1^w($"R6sk- .Wrkx5 i )1"n) 'F:FW{āAa!'zur5 C<W7;6i`gG(%ߡS, :ߵ:^cޙ[ﺧƮhI]Z;"|LLw-:SجhwMcrm,t ]:\Ye(Pq0?m>xSu&+#kgݬ]ēod Juv]-),i jOfZ[UDRsÐlj boRWQljx+ Z|9Z۪Az@@ycܹDe:`(o8vSaiŬyih`"PpRQFk฾eV|N[x #W[2Ou솈3aC΅7 9"MFR`h߇HO_ԃw+@b/ IɐrhuKgL9Lj$io>w<$'f޿VF\U>!Y<{Q7lSzFF{<ٳیA@xWs~.@@gSv)  Z-$I9P;1^w($"R6sk /#(c>Ç _[W.[p` k3H"%@@bDO.Ɏ}TyE9\u"'Ɖ_fj^lsƕ  \iʍ$rưsE|s_T6k>ue>;CJ8v^fʑ=GdFçOYPR" {d5FlvTpväTKZ4gsʣ/ XIDi߿?_h`.=#4ӎ}]>* ߝPyU8ϸohȊL!z6ipI]Z;B/`r,x*8|W6:htA>fuжpg{d*LGD hr/LqQ@@Wq8tJ?}dpbWy@۵wT$Ħ%$=]io%VqJ͑CJLKES{{uoi^f%{r>@570O1w^"ղr0nCVEQ];gbVּ440L\F8(5p\2+Jx'+vGqe69T[5@KK sd"+6FԻU#8߆gcdDD$'5{6'+sѣƟwÁs|).!P TL-WɋJ\gUE<m}oU0h{xA= ՛&((((0C8|hM((((N ZzMiy2(Pglo\ٵͳU]Ք0PPPP[1v7=$;WR"@@zĕ8;(O"g {N1g\7^?lvTpväTKZ4gs0(Poxm?{G }FV,f  @=a-luCERaeM,Z:i>r28]CDCC@W()M{쐔q FA +MV >o4F.-)L ϑa @}RC!;   ?Fש; ~D@hp'ٷ6Z:\u"'Ɖh@@INe_ZW3_(P ~J"H4a=BP9(o?NǷwlz# 4|tmAy[1ΝKZVƍr 6C(G#oTxo12 @J\'Y((((РNnߞo6+HҥKFk.k!rԪݢ״]o Dn7ce甥w$}2ږvVF&fJ~nϷpYXT̾Jn^3ft@@XX;2^kl2+OZxZnF*ΐ#-Rp/¸aj^lsƕqeyi)%Ʀi;mE WmnSlc(d%#\Fs; T|3|I׏?1VpyܝU^}/:]r! Ff۲j+2p0Nv&FzZ7,xD4fcV]D$0o?d~L5rIn?۷Iac9k?p1Ko:{%U^YskiuYc+T7SvoqI;;pE)a v`Օ#{&Ɍ\Org #T e{dl9OZ(.ZgKg3nZv]%{*^{ֹ;Q ]qI,lvWDP c:`HwGd@τ"bN|BQ}!b4D+gk.ƴi"ΝOT54ƉAy9cs9Ke:H#Bc՝۵%eimZK3Uo5>^#;d0'\k~h| +=1?ſsD&pڱO7kPan#کbr5726Q5DUQ,? "ƨ˜ t#o8-\yNq+R[wdxMz:=&jS2=rp3AA),5,UsRqHD_v Zg"QRz,iҜϩ W_,;kSgr9e붖 ^Vv1cڤ_YK!ɾT幒%=%'=˵ƋGCssM[yʶָohȊL!kݹ9c[wgr ۶sPE-aLw-:aYQb>-/_( I^i$.ͫ?f~E {gs(Pya;{ܲމ'},o;xއ%g9x{ʗݻrhUN2QdkU*. GUiQGm IyM6y̕\ wOs[jbD+ ilZBӥVr28]CDC{:UjttuijqһAcD=^xشgWKD|vԤ ̒PM8ѣhMmM8cbЪs{sIRvY.Zzʄ 0)|Д#j j;jZ@[ĺkwo e[pCuCLg?;=Y]R$g:?dW ߎQU]Dewn',۸4xЀ>] J܉VrQif= 1lgE\M4~ek9?tp.-$o=*qҌb끃ڲ ή`GۥG$2-z 5|@7M]#k2o&ʳh;g;M}"̯oՋ7xU32ݳEϞ]sP"F׺.4`{?MTh\P"~fO>@h .w?|-PPFkGC\] ~?格]s; TDIږ.Mل!wI-$I9x[W=x.Kwc/x4Rqiמb/x?7|ֺؐhE VK{ +∈oqI;;pE)agӕ/+-ُ߰id ~-#b [ <ѫWzٸY@]A+2atNv&$7[ΤD :Yiq~l7SSu剈m=m IDAT0ujXTu#Yh?qulS3B+͌cwVn U6$U[g9*rbo8p8(]z/g֬M{_Uw2=ϛ9v5>^#;d0'\k~h,N­cG~\KD<ۮ'Dv`Օ#{&Ɍ\Org㥼<4ƉAy9cs9Kej"ƴi"ΝOT˿D1q0;w#YE2VgB勪<e{dl9OZfQ؄_-bgtؾ4 JD4h5tO-_ PujC""/O w*vպ2!%k \(!!L+ 3>6bfաZcڤ_YK!_}fGg?LJj뱤UKs&>=C/jϦS E>Jez}1؀J3GO(Q|UC),5,UsRqHMV;DO0r1չІ /+("M ,0z @@WS!81 wpn&Ȼ]elVTػO꒰%=%'=]Z;"+L<3n3;Z3b6%d\V+Ï￱5ۻĆܼtLθE%92Đb^]mpI^C5PFQ;{WnX_12 ɽ3.ͫc-luCERaeM* 2=!?]*^uC׎Y*0,65/ W۹4{CR(悊^X kjEIoWj;ꃎȨqZhL*)PE5ܔ4؉yiSaj୴jkXf/Wӆ&ΝKޝpn[.q$RgJAHַժݢo/Җ:UGFQG(E&D&ٝp||f=ygvf\vay.ƽۣ4cwZ7/N]uLJq|Q?YZ!oϱ;uV>6]{w)5˛. ?tR_{sE'&T!l&xSCIqan(G F9\*Hm݉3)EJ^2WxTej~j|`qofŅ픦?ԩPF]N{M ř[?!8Z[Ÿ{ܭ; >'))VZuǤ7&M*GPpOQ(= YzRYxԤO*umȈE.>gsܡSِs5R7m\E)]4XۅPph÷|l?Tڬտ+ڨ,-ze쐷؉|K4~ C f:I*DM{n9YvBgRhjwvPPP`` uy\౓R`UwLzcҤX xP(@@  uX`PPuyO~@C)@@  P(@@  ^\mQYo~4Uc Kk PF_Sc_S%Y)~pjRONA05࿿:[!խA 4O|JzVvrғ wP(@@  (@@  P(@@  P(@@  P(@@  P(@@ w󽞱9ңx;խkJcvvg(5K c_ճeiƥ;p4GW;6~:(yIBnu1<H^FL)͋ٺ}W BF=Ƽ6gLT"֠m+hh&ݿq}B.|${y+jo*_N"p^k|V/<\!'{AoP&[ evm`e/L=kWJc%)nڜ !Y)+f{Λ% ܋-γ/٣1e)wת.I8fW\1SYjoo]6äa[n+p[V0nT%f)FpKa7.Y잟?e3'uIiUiu\\5X fPJQ/lʛ3JRgnY/Km۹ BneB:(=RqȶH2ޛF z LqQc_cuz6$gDW&8UbTґ}vḘDegOqzy>n"ąHC끭*F5|F\XG !DcԞϮ:2\N.$m~%Cmgo#DwUy^ʫ-^ʄ+fm|j$f(FK)8%ut޵ry6b@@JKY^MEg3ߢ+b9-2*3gꃹUGuMTYc+Q7jaamʍoܘHΎ*@o_{|wtG]1(uՅ*[ JezQ}5u%R$bl"^ykgl۴uE*a(CBwsHTu$ZtoS7(>]=s;tca;׶Bfk6AՅ\O{o|7}y>Xeḻ5X/̑?Ľ}KCb pye!kέTDDݰJ/.ȚDsM*,3esL6M.reL1ŋzׯѽMte:Jc*]԰B0K>D͕ˉ:Fvǻ}'o)2,|0:'غ9[9ZJبqeyi5;ɳCb3W#GX"DC捜>YxԤOؔ;<ĬИ2KOgI+&߾os[ 9): 9yߺJDotߜ3֮Sɲ0k3eIEzSǖ&BmMړ%i]GĨ"^ 95PrjF߭_.JV͝LnjKo B*"R/f/g] Pi`p=7j_cFmTiڥ?P{5mO2vsDΥwFUۑk7-Zr/Z91ʿPry…YNkohsRĄ qI{$z:{r0WxTej~ju-ۏ}k06+.to4ŊBխUʐv`W6FO~07|VBHխ!ם'-~l+9x2y{k#E$.>;(((00@UT -gGmO$Cf-|c< ))VZuǤ7&MbjsovUs :H(xBzjk(v8}W5GfyyH@Y6GS_Tzӆ̔b!*}^Ojruob5(xʇ lzgmN¬=Cޒ( vj?lK:4uP΅nݴb,P^mU^amiԍS;zPҞܛ$ !w{s]8X cOZEɶ}M8[S"o]̚`*!WXR /j~zymT-J3㯙DV72/u?B _yB(OnJګAx9;Y)y1[}AɲY' T.)̽zl'k+FBZ GC;%7/WsϓVѢCBasKm۹ Bne" !,ϟ+زrK+ॹ [ܤz^8]ݚWjjZf Rt|wGK$ƾ挒ԙcBiQr`Wq#ߝZ0Z.I8fW\1SYjoo]6äa[jPy#v|<#y%{4!8[~RrYu-iyZAo/uˀvixzʪ^Q soYc¸Sse,jTʥyY6::{MQVk|KE*uQaު[qmȊ=s.LHUethk/ܓcC+B\Ԙ4טAݷ 6iׅͯyc" vߌͫ,?jm^e^MM,K>s(c7j;{Qt5&2& 923w?Z!5&+xh VU(OpB;},7vv {ͶM[w]Ծ!ݳIoQ}سEc:}sQrr28Uٿ[3O֒\23SS3H6ȯᵒyRWFj}vnԴ*pC?pR/9qcj!P7jaamʍocHΎ*M(@-3GN֣- "j"dĠ(5.?n|jcAdDB);.RۨImOKyt7]=s;tca;׶BEHdvhҽɷ/"7jÓPCfݾ^~ YwnݠR7l" (V צWVrU-vv[ĽY-?E'j_N[;o}R|rL6֕@+I&.Qܶ}:T=/]NMyc 9inLnj8y53LjF~`h86$$ߨtaUxH;C9}nTI#3BU\ҽ6хQ9O'IT'l_hnߢRujz"YNMRɪ=$'\9 IDATvay5YxDotߜ3֮S7m\E)r ?bLi{hLf$5UKwWklz%d,{@RjȤ"cKG]ZV߾os[ 9): 9wJkțO JJA~֯ Mό</x~P .$Y~>M];^eׯ>siݵwĦӑeJQbFݬ 2/9Z!Yyl?BUNCzԋ`JjZvۿn#w}Q5[x%JҴK/<6?dDƤMG>б]~ gF/չw.XdzM.Rz^tK.}|{vhns*b>\{ AAAOUa ?{[9eSxp ))VZuǤ7&M*)<$,%+_up]c'wr5dDž[l!#lJGo82c@@  P(@@  P(@@ P(@@ P(@@ P(@@ P(@@3??D]#P) x8(h2OK++ xZlyϴju EN zYxS$䔔j_)!GuHwHIENDB`brick-1.9/docs/programs-screenshots/brick-edit-demo.png0000644000000000000000000004435407346545000021445 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw|]6ݛB -e2"SdLADQ~ {Rt6]HҌm&MP~zy'w+,˫T\IbBhr,FE/Hd`gkħȐSݹ\ATRi4Q/x aYVd``ieշW@6 JQD͛2j/F#+EĴoہe޾u^yR;++KРA( EEEbK+T&C9(֖:H* V@AP x'GDj&:gCPr1 zW'ؗ%LS27x{PClԓT"-!DNHvv)nmtkc(ƿuhΈmnGIk'bv~]Z>NuWPdƧ׀~mRCbףP|eh?E"`)gҝB^ 5""2ũ[AǗ8ocä͌g/4kƃfϜ0ܿe4lΨ6ϭ5rt4VSuV[3?kkb^"C=wROU&(Pz= uzLE?NwT+?ᆛdȴC_?KK;w:hKqq|i/g/3k7X,rmU]Qd m5i]{Nd6cʋAaN? #"Cl?P2E],X.ܩ*`"blDro7+"bz2kX˼N{?Np[X_O)k?~ִ] M#e<c3tΜqo8'1B"J׾>x8'h&*w=ښұ!K nTHpBWVssk~] kf̩ú8??̈ן\օ 4qlQ2}xUv ]\EQƞ"R\9P<5LcUq:)']zw6g5N3cf#gmwr0{̚[2aCz1%,;Gal)zf2n6-45&(?WahȢt($^6*xq EԌ"G,*d: Xq~Kd lCoY|QNJjiy\ Ք 8|CSS^#N-NkIF&-X79}*nR1m3Ǎc< H-+(a?K*EW +t1\"CTߐzz~"2xxrŎUa  x:ƅF`hhϞ /qK;ÿO>V].C:>ٿ׷>ؕPfcOmg_zDzw+u^=CuYKf{/0 CXbEjзo.뚧4Q>6tȐ>]M؀ҽN;߷/j`o!knyVܮ0i~jQ`#Ǜ4Y\|RIuQ"_2wO\uYtRPpĺkvƌ&Q8"u a:{(Y* ʌ&> si9OjX퉓YX%A/Ϯ (;4{M i1s rN,6r2\wMC9v䶉M ގ*{xt c{sOtO&o )dZO[}̩ .9#IQ36=?uE*[z;DƳV}V>vUȪ/I8Ƥ<~v5 q j_{aWc]UB׵.FW]֭+"P?5l2aE~aer%"iěó 3] Nz}HyUvȔ\&VLdgtέϧ'瑩.)U_k~y '? t~v9VMHp|RlX8,Mi/f'$ v"$.֭&jL ?O)z~hJ/9O=y xڑQ71??ls(Wdt1 2 Qpi}YAyUޞB翱E(2K/K'SZO I'dd> yS31EF/O,53A%rUOtpjK}Cng%x$%s@/KX7wPE1dQę ȀSUlr䊛dA@@@@@AZi^@ ґ4A^S$} SV qgjgZ;ælN_e ՚?'z1_R"A|jm4h1v6z-%X~Zuݵc,{ X͞mB̔[/cn9@cQlI/,kg8T=BWQK PՀo.Ƿmʥ{UT$/ykˮWi^~vIIk^Xt%uDdkofA{YJSEvvjn;2Gm e{onjY#|64߂_%Q`pWm9MXn/{_z_BIkJ;~Ikt0[~n=e*}扌kT/{>snߌ^U<=ez/{Δ8#껮'o4jȌvE{aGrަmҘF:߯ږbv is. YKG<v9,a='fw#w_hRP^۷4߼]qyOi:ZжRKn~{k1kYY 0jdU@V?5C.팪\*&WՕ(%%"M"7Ȁf,xf<:9 .>㡹|@nQu`~>Öf"]LOJ\t 9D{31Stv@^j8xYє'ດ_x"P;yF {5DDsA%'L>FmVYtjݬ^HtR ր-]8 ).{&$/8Vx%c7]1_QK{,{]ܲe$\ռA<]S6b9?,Jkd>_s?ro5iF٪6"xˏ͂H_SV+_mW?\`~f! o#%$c';Ξ՝?jrJ-wRhFy"H _w`ʅsoo֓ܠwmGV*zU#HPe\ISĜkVγrҊCQ'X)E7657%ݶg 8ɂlnls-*;lxVr;JJ%3; HJ'1[YuTd׀6Fꪔ'Pt] %ճ;.P˱"@޴{+!Rʌj̩iQ{S˝29>P7O,I͖K@7pue]eXx=2ڮ+ uܼXwi~.O,zp""".CyԬ)w|41ǧڳ-'hma*^)9+S]5AWʮDI+i)@m Bc^1i* DDF"k^*ቈ2Q35Oϝ|ɷ r?R\Qb&2G$bwtp泳>?CHg]Qh¢&$Xl||_^Tϼ{&'{5ה\y;?0wbޅy•ȋM<yɟHT? ,z7d圜V =rTϪez=JZ62sW*%$J cФa3:˔W[MЕ+QRJZ\s$%gcBY6cn[ղxY(f*# X>ZܳGLyr'Jw$N}A6[yf$AaC:yΠ \+>:Fe=g-cbN!)NT>9eL۶7dVgH-rNƌh} ¿˕[?+δuC½?I+/%f*Տ +G/p/κ]TN!{K3Cw;#VRu (O2 a"{uԜOn<`/^U~yŠQg.-#_EIʾ]3:ZвR}+(+X)ނI}xJ[T-[;6fm\NjgZՖnIжGWݕ(i%}J~91}܎|$4.|WMDoGFnrT~t㏶q"khBx"؜=KMʑ53mm"Z۴Lұu5al򍞡I`{"W钢+QR Nyzh偏 0KDI^/xPi1+1gm8_/)%xރ(LII Π@u$ FuǏE"kz;w(*R|a``СCɯ(%x5bodd䓿#QXt/R(PB@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@7%+Q^KgB}^{~PΠ  T1o2pt/76؝; /ל)Cc<gV/@Ɉ[dL_b>d׭+ ҬjX([}۸}Kצ].b\s d7Vpu^OKyǜ/q/h}z}fmk'1y*-h7iTU~{IS?^n;| 6ϿH#VyhhlojrRޛkjb\+YKΚho΃&K'Nj%#[r銫fOk K=( | B MNZ:ٷ|m27S(0w! */+#gZf鄈+$S[TaK| qEU)~.u-W/5ckHmЍ66 !&?#11P[옄4}, Z "">i?S'}?< V>j_=c]{o?|柵o] 7On|wןN>`ʌ"-U1o\Ҿ.c޸-:Y9wNqQ~q0Q$:CRf@$ m98Z&2m&.ЏÊmA5.Axpá2!O=eUmɦi%U"f*X"1\Y YfC} ̈8OgK".hqKwt_9La,FO灰 =6(}_Ğ.X0 IDAT-T\v64C DjUDɱ=>'$"u?Ƿ׾^E0tnPqr|lw̐3>]~,#2tn-Bۦ|x Q(埜G-o\s$#5Y]_>4=*:j* }]~$"X.3Tn4q8?櫔ӧ??"/P#/꣺{O1 f93&-ž ӅN7hc\Ӷ͑TӺN>-]LǑ 쒒 ZC6cZ3).4. !"^f~0€xqKDjߩc{M~?׼vNo Msz&ԋ}%c79*A,%2:Ɉ" u,P{fv9'a3>"ۦK@q>{y󶬠$jYE@'47ZԂ"7֧B\>+g]fXvK.i]ԎC7V3Dd/SgUw91jGBuQ15l$Qa5+qQc, &_.$#E#<yٜ-@@yxizq37c蹣:)aFn+"#c!e Er""2 SOU]==Lͧo9(2,I]c3/DfV./+1[n_O k̒4E]% /ע#LS5@R jaΕnb)Oec@xIE'"MS5 6vX6jMaTo=-}d8b-5x'@P%P*63aHΗۑʶP]~<8=׵T/1 e߰h$ȫ 25?T_\N 1E{Gy4#X X\U{sm[8t!eRdO#"Y4;7 !]&d IxG'1UJT1F&&/~"$'oU-ERddlĐ"%OfD{?M~!} IUwl` , Q|]n:6psw.1b^}[N "MA=mT(g3L2: *YU`4*,wǓ}Ů̿R2J5`eOvuOwuOzóR0ܘKyPLs{gKCkca]\ɔzLƽGcB4 mwmlE&N,43kdPc&#.A.0xx8@*rşP}"u2sz 9EHI#6ϗ (P09)5[N̞bN*YW$ĥz gjƞG,Q[AmVxgI 7o383OkgC e#1 Yyfԙmn&^UZ>\ۍ9eɗu&s 2&!;e6-yԋ._&\޸M;Kl[nP-om s0F j+_at3O﨓~jӽM6]ƲKvl_t͞${e}D/E~>g[H^{~P "JLJ*˶[˽%P7&ƍmg,ΧM_>3F@K P>E5߾%OB@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@EԜ6         @#D Nމ"T*h8zpPPy{ֹ}tezi s԰le5q,ݡψخO:A>3綿<[hc~훸X sy[N"|DSV:C;g{S b5zQRUYZ)op6dg-:kFʢgk4#1C["**"ܒ&*\V 丢ZKmի )ou.nzѨ1_$BG[f dmoV;E ۛn0b#g[)%7M~Fb>c!y6;9)te}GޡT&=g;MK>̀eD9GCs9"b݆YWn}&"Q9?5k1 /h%mY"ADD|~uK=S^hS%\+= UX$FĈn+""R?YP>`ʌ"-U1o\ҾeƼq[6tsbعأ0Q$:CRf'OtK`8ndV-ܸg:t?կ|˫k,cୱn\Q\?Be,l߷> xpjXC?+]dRyL R;qLK銛 A#_ܭ|"n`Yc'e}Q(U 6Tܸ>8,3d̼OK 4s?~kpжi>_,^xgBgg'g<~QIxMaVc$1ƚTXV]e}:Ͽ#֢ˌ/k̺Anjm?فoJ<}o#RR:wsE}Tvo# ?fL52yƼݤE^سa{Щ$M2h-G.XlzRW`Z(x ;}ڿ)X+'ujssbNۖ6GR%O̔|} iUͽg?"g=:5]r p6YdF_,xL""Lt(5eF}$z`tûO+gE<S>U_ pyƔ5.$'WP't>e\Υ1 \R8UV*#F.=u JoqdD:Һˌ{fv9'a3>"ۦK@q>{y|뵯˪^?X~hyA_@W^-*Xͨ %axTGD+,L|t" ӓ?ȃ-j?Yg7 KEF WD:zM6 ܢ#ON0LȒ!@@yG #H/>jOC|0ƍWv;K*\Ef& ɫF]ht-UK p7=4LM3LuZjU&}~,*ywo|n_Xk`L(O^ᙱR*#"bx (k4& @@y&Ml5"ʓ&Nb&$JbLLpe:N;T|$'oU%J͓Cݯ,RdjfZKw+Lڗh5 Q $7$DŪ7uFW~T-Swq&Ib*_VЍ UyA'N<5cg6psw)2b^}[N {X[6N~*O|<^cЎ͊+1Jm;[X3 cZOMM(h{d?&4MaFy](~PjBaB9F6 =0h2+WQ :BGw`no='1sgVT")*~78id@;I٬!rBdf{vW$ĥz gjƞg 圫[NJD10J ~Znjƞ'7TG\ܼϠKl^(:yzȩ&8۴lU T,Q}5{l;QNJm"3a4\yһ*+^JNa]_wՁ#z@@yCKlm^a7NQ?bP/%@@@@@@@@@@@@@@@@:F@5m"3(((((((((((za=O#JADq#z PêMk` SnfWgVd3_.-46 aM :tk5|XBOfw#)_W6Paot2xvWM-Ջ9*3sn[K?ӼŽ1s7PX )VStm.8ɧ íԄ74D"07ٷJcc-j`xɁ:Uټ} IIK3o-#d׻_܋'80H)BuoJ8 e8QlT45q.8bL={ ߱iCg[K (7|ʯpDĘ7;zˆN|N;w{1 ;EE~Nʕ_#D^KZMEĘzyNNJ,xzzw&Bu~ZC;v^W6|&6r4.J^Nd4}, Z "">i?S7p^[qwu~VS]da/vqn+""R+B4;,GFΖBqdG7R3mMu_p>'56nahs=%wČ;ڈMEji {#|toԄ]$B2KF5;E;E̤DCd1^ڛX+cFjaoe${ymؚ5Ur*Hq-wZvLB/JRqİ CDXێ?c\FhbIisE}Tvo# ?fL52yx ;}ڿ)X+'uj1MKՍVqWrTwOswF3@sCiCFXh,%(_+M??91Z߸HGk 'Pcqk޲;q>|m؜!ޚKaZtŠ*l!ci;pOeOrnHhQ_DD\P57vج<)JjZP.Q^rnPqr|lw̐3>]~,ӹOdMZa;= n\:Ҫ)WS>D+Q/Md=v~.T6i9bM]^R'1" זͪL :=䙏{fv9'a3>"Xͨ %axTGDN_vJ{x=1M;ieDx5g$ BuwsTRR`^U`:%k&P] je.+=lXFiSMⴕ/I䈓eʈK$l8_}\+)B̛mߺy& kmchBF.QU_Aw(&4nftCk]6]#\tCλeI)z _/BDNTwM\wH*Q/MXu9SȬ+݊꽉P\]ܾ{&0~tu{TP|Ӟ|2,ei!_tGpIDAT#]|ʽy\jbF }y}[SF04d2 D QVyɣLA.ʪi\[Ki%&adDWW] je^ghklj[ [Z^ۋsҫA"UvI!g,ʫBPkh]^. imP׺K1+R uxmQcΒbBu?^h5nK!(o"ÿn#WreL p7=t""UMnɯߐ#~ݾ|^K:&q>$5o1bhK?Lēk0ie.5)oРs}ڷniϤ(_-x"g9xU+xi um[ku4Uktn.NQ/D9+/sPPd%%&%N4 QjRҥOoCܵዏLxgLF8ݼ$}zXlB|XJ˜#pncNlJbLLrq, N|1I4X.yIIy6juĩ+Q 6NH=e'S3SF-bbrXl^0/S@[}u4Xu6uQjREpdD^54Iqmo ϓ&NbvI{1OIAs?oL1t)sꮄ'2h:=_僇qګP_|7&$; wQvcn#|fBalj.-49d[UsФɈK; > \En$hr8!s]Mfu6-k94}anWIsܞqp^5gvP={fIx Gq \] MVsvRMW\C]@e 򕭫G/sϛ?.Q7e^NaSkO߶ ߾ Vֵ^'fdyj׎^4o蜩n1>PFi@8eXlά CWIr 1iu5#ܢ(S.Z"s'#g4 Df>?3@?.Ky"]/Uoɍ 83(ωI/^c\$:xW / ɃwtL<@Vufۥ :.F/C$,inN]+(@E46;cpT{m eol/_&V@B|{W9::3 @ @ @ @(@(@(@(@((((@@@@@@@@ P P P P P P P P @ @ @ @(@(@(@(@((((@@@@@@@@ P P P PXk6q%PKӴn϶fXBE+wd?ЊieP:(@X*IENDB`brick-1.9/docs/programs-screenshots/brick-file-browser-demo.png0000644000000000000000000012403607346545000023114 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw|e3۳-!H*XP3gʒa(ˆ,cm8B!=H7h4ZD$aw6@ѽ7y0rG)B!y@aF$Gk4ӮY<|X"Xhe_}>#BgR8^!)1q{JtB T~_lC)!:Z#P+> !BhUƾ@!P? P(BPB! B!tv%i{42!Bg-ܕ w|6`Mh 7E B_h^LxI+&kzٚ >.!t2mU؎xbEQW]5Lkjr _)9ذ!+伔^AoēdT[siyMҠUƒ gRKUv8Cyv緲πP?MQϵvӣ1 t擫Q2\+XHa>cڦ")^Pf;eX)9;1Dp }ShŒNߊ.H*0r<OM9שa |]eijlP#%쎽Ob0JJ>MG_o]^x S-~h8$[L j!$i䫎1 t i儫0(},%4P?5sPaqǠտN$6y1J6=5 qk{Z,-svݦ |gmy{q(n>:*E?:]]n.~yb=sKwG,&6Z ALU (]jIӆKIzGX,>Jlٝw>rK?fȬ_X%<5 E"@y>Ų!"`i]3mO JPam1 t哤IS |hF}~='7$g?DǞxWͻΗ[QRF!Eݍo~3bRe* ?A䌋$)X>S?{*Ky "Sy){kQW mą<=ŀХOR'O,lqDh ]u[9wg'nӡxڨ9m{sd%BZmWI4)) D "mcy ڄc(|g+ umNAk[2~Y BT>ɺn祹6s&BB7-7z(xbBT><%WHe?ls&BZk~b6Ao !.%SԿǭV_6xD]p4la$us+KaYtx!BgK5h`M;PB!tHH  5B!Φ _z+$B~ B!0 Ba@A!B! (!€B!t] 8+ c1۷a"BB;$;s]BgYPlyZ[Wr*{y.*~PeyװErŜ>:<B͟_M.$y0vdLN=h$?v-BPBkck6q5 4_ ?Y=>sW]~BsqǏ("ED2OۏzG=S//x(P6ha@A&,=_޶}Z獯z=Ge_gLn5-_>.kڌ^VH'ey˂#[DMN9^"7̯{yhy/Np"Z[?hB B̶d+ֺ<I-ޓ3Po̘$ZςZH7p &1/k Gκt??ud9Зs /'y^AI#buΕ,LygEkNj=,Z9M I|4'?\$icI[mW|@7 w~g{n+O#K=V+BPBg"uvm͖P2frHU_HYҚΜrd?j0V]ذ7%&*Nk=Z6~k17=mKjeџ=<`aa Lkk7T=4ST6uT qHm:LF+\G?˰1:W,^Y[jݻ̠!Raj 1"-MD0/ُ$:~em<^ 5_"`P`%B ݜ/y 5d%x5qB@ͩ-JP (ɾA;ڶf| 8 "ѩ`yV9R~|3i0 О.TML=55~rqm}}ڻS? Y>u9o)jb!^C4QBh1n)1:SiݶhFfܤ:2ٖ/Upp\oe4p,_۶,D< (ֿ8c7[_SH&$r}-ʧK?nx+QmEQjGXٿRI2bh.Y4c'+DF"bBKS߹ę潞v묆o Nj5=TPthˇjzߘ}Mx1B瓰7 ?Iv'WgX!sHƬ* 4`vqϴ[ ԓd,SuI ky{R/!X Kecg\qvKx >ףUVYi-濗yyF͟mW2[n5))Cpsڒg;[kS|xd*Y><)(m3Gh?-;o )r6~!a{pWMӞ0 q=1a@AkIA_MM+(PX @.WħJ;?磧 Ѷo=ڔniv34{xۂ0 Hj[6̊ p`֎E'6z l炘o+ңi[CنO>`ϓ,cҢ[-ITawؘF'NRQGW}QwOaGG%FG nS>X1y.AoߌXߵɯ8}*!!͎U1+>)o?ݭ!EEpoBdnyq3I0V= I-)6}L&J6.S-XSW yKM{yiٿY̜ <*igܐuc 9oxlRpÕRFuսO>^c! (WV^pYwxh!feq 4cn&G] pY{y1_$ qPU~b d̍3{\j?nv;yTk]`bH9L!Qz4ZZ:}6<}|ԥi Pnu{1:ͭ~$br2֟*L휱²`\V`Iphx' ,yǶ,\y;>9Vˀ-Dz_80=VNDBb-7(,0 [l,9%zک< Eβo?\W3vC/vS z[ɡX#_L- k>|cQY+MߊGx6#9h8{򌂨3H\SMC8vcp$ nk"M:>zYsn@pLnrUaZ^X/woj=ZoaI( WȻ0 IIKGP Ŷ?uOɿ/.P֬`P_l]77ͼ $\w{QPyiBut?~ W?58ItRgekk {ôGA#<F@Suͼ.y·KwW8eҝK BB~AX ;!t- *AA!B! (*LeLЯ"W&H\׌ J[:NqMA7շ /E_nRFPйXCY d"\2N&)DiàރuJ|+< Lb"[0grNL&NjsZaOh'eg*2dx: *z׍3 nDu'^rթz.]IpԔQ QIBV+Bp &F{}4M]ϴ~vPwBщu2V(& ՌH颂~иӻ֧t!\\^ePfm,v1Ί9[ xA+0#*Ril]#01O͋fXLg*8OWׇ)ANQ Lzyz,C(=6Qq"j5 O&*KT U³᪟( WVGD ei18uٹBR85>wwi2"A.s>OˋI}h&mZ{$\4J`[7~G4i# $j! ^a7D,%NˑOMB|( ̼`&5>J#MP ene{lxU܇ 9w&'?iy%:okcմoQw^5Fo{^>zX7" `(T]ըnR.9%[~FR!rimLc'=ViMzm.]yD-Fy]N34a60.!d3Ϲ!]Yr4,] LaF2Osn&]YQ)xE PP$HP W˪|ݽQ_Bj@ ؟?fv@N:9@(PB@!(k)BDN^ F°ۍ)¶BLTƥIRxBBhwnH+( )A >´GsHw3_^@VP 1/QʲPeg¾-Jzhi줇PT׺viɦ$o|#CE PvꏚiW!kRaU 5t#)TλmD֍}7K iӞ5Y^ozB+fle4IϏt_D$H\(i SՔ}a8$\#!#gZ=A:~[Ƹ|򔣠 )Pa@F B@υ.ax9;~"#׸zQ8Q^"gRDaKap;;VY(;ك?&g8fH(\!aX KrHKywUFkෆ{( 949&i"5]Gu6|YZ ҅L#.H[-Z4"Du*\ {((xkkl%!v`ɧx(Izo[oťAVKTz_F=xųLyAʡBUePۃ䑩]&/ȔiY3X\W(VOʖ0T70vrVTF4S'` Ȕ9ry\?7K9:c㿪ճRg&0Y^DU&=纴kcB8{7l7[B$I;PY=9Wg7kœI9bQvUT:YzBԠ ^h !/R_';QUK*d MfEĻ.y*ڷ(>\ bo"/ȔizP"Uy)wS!;&z{vm&P S@w\1&iRT IM>';hzoqQ%]tF} ]9삋S]=+5LqxQ಺M7\w4쎟=$Bmv-CytWt[ -ffyRIIfw}4i]||ܔ c9=th q1ޭ <?rdJZ_Qdm\pmbӇCˠ5^2G+sͽ|.ڴe\ҔIclCqgu`;/ѳhU"BM%5Ao*yQvY# Iz]#%G:CU4!Ɵm!*a) i״Pn8pVѾmD !ryB e9ݻˆK Iq ƴ6ĸzSu8zk˺U>wr_{]n7aǯ$6o{~\aW^`X rB+ĎF5E}[)V[{DwW8eҝKEL* m XvQa<'k&>ڲ⺁9緁JQjXvQa u6uGMonq{p8#w[.V'p`H]2n}%FBdL7Emn/n*QxB]_Ee!€B!uw$4 B /V,i#€ *+RdD!0i9!?YwBa@AVδg /Ba@Aת-sBPP?Aj6cX"~ B! (p_;Ib_Xe;!0WlؘݞSp6ixE9^&BPйXtܴ@$009vS[q*B~ ?H_…:L&{EqiKBPP 04LJK!_uUM'1, g6P!PT]B! (!BP.ARyԂlr>Kp~q!$i/JLQ BPйyt} ‚ۖyD0X:E+O%%g6问ڢ)󱈢xyfV€zo2 D5vDhɼ9If%,!P?b\N([=50%k2SMa €rcgλ]*{٪cCwTMRT-hBM&.S-߰sF$ O )o|-S aniPJj.r}4hj|jv oX@ÕvFu#oKB'_RQ0__$W+Y* @bgdOTʨa 3KˎNF0y`LmcSXqTb D! (&;$'<°O44zKM,3b58-%V1DXiiGdeiڀ/Dd t"L^;y"=Z; ߳{QFOw?|RK쬜Iܛ_3yJv iR2.k&PpbX>link+}` ?>Q}Q żC_|i%cf˔Ksp-"˛<1ầF'2}zcWR€r١ZEO4/&eh:~r;`>Xċ+VP!h`>*xhdɘ-FbGEڶW€r#]?q˙%nxt[Sx_ŲQ 6d;pP#B5E3M@)Kmnz$8Oc<|=`LX.8poh3v9B}>11fK)MT-&LJ;|Sʕ|küX 7npz">oȌܡepۈ,gNղn' 5r0!0\~h0h˜>𾐵j|7,NX=~H`pl)BUpaoJ{`{ajjݲL0ꆄJ4 {띮-~Oʘz-VߑmzIg,.ynCevg7O G7LW1:+MMéwɋ5E)wH)k?h#KxDʬa"NϏE_9[z }/L_=8Ub)\=Bg4*Zfv7a:A] Qf]!l: Ίsb7 Y#(!€B!R$O->/`)#/.$B(1Ey>:|>B! (1"͊a/-,mH$ (r*j--=/{.}3Ӥ#+0 D5x~1J3GEBU<V-?lm]1/=&k2SM=!"ޝ̝w\-]UU52$',"Z$Ѱӽr#M]xZ/ۿaw"'OI2N$R9Nmyc{AӠ\+d! d[K?6i@ӎU/V !p򻇈RQ0__$W+Y*c63~.;#{LRFUcP1X5XvݬwO4-hm%BŌ 48I 0  CRX N @IU Qi'link+}` ?>Q}\Q żC_|i%cfGz dR>16Ba@M-wТ'24m M?9P_~K,-0a;?B,ͻ;\G|"e3xxSщe7(¨K*e2n9>JU *os2AIѺXGj={3+wj}ވP| Xj:._0CejvDa/If&+=xZ€rDd#R} "[.=e{I^!|Coj2r+dYQf0k7V]NvlDˈjb6'J(Nxk +7.ovx)BK&Umm۫Ba@boLW](pߥbt*Uշ/!ߛ IeT7BMozwW/{.Sw#dLX.8poh3v9@}>11fK)< ˏSH؛⩽ǏS&'>I#ןCNDwK/dĄJ8~BP.Ay:+NԚ,v/ෙjG_.! f5-L=Zp&h1[ZNVO/OY{h BNӑC>YΜ$me27ldl19>e,WP[^"uLqCسgzz$aFnR\BEJθ Y ߯6{yw/w}C+ޣ9귏 &"^hh w@hf꩔ɋ| >UpX\RxW?2u\.ynCevg\O G7LW1:+MMMsE~"e0{Â"POի,Xp~F(L_=8U^&b)\=B4*Zfv7a:ANo]8e¶=;AN.sb7 ip!BPB!0\,zŅr/| rU$I3|Qb-€7fbܶ|ЌK$rU($J3=Vs`RESD_7p4)!92 D5x I3GEBEW\V([=5ⷾ\L 7v"KsO;3wrwUV54+jhXDNjʍT61wj_lݝ3P<%i8u|HH9;卝F ssOWVtXQG pUvC}Uߋ2_ܲD"ºC#+uEr˫zBv䭺|0ZM$vFęJƠ1/Ba@4M-wТ'24m ۙM?9/O[`G ++~tΏKκ2_|=)HL&A3곉)`n ꪰ7W7u֣nƺ?TIܛYUBlƊVpx|Qt :u\<`>j$<щe7(¨K*e2n990äk3ƕm,Ba@ ct)P߾E-=$/v>Yw!EKybPk5$sbS"@{*$%"29QBp[ǟ(WXѱB.#6kk,@P!mVcjM.U%cjk^wB側2{fz'r&G0.W} $qqNQUJ9|DZfwetQ BZ7avtsB8iFn}nRpzwwY#$RϏE! (&IKm^}0aJrɇ{}^F9\\ 0l7ጩ1^ҷOL5ni3DhIljך)0GE+d1"}}}Wus)ԯB}yqJ]i5P`O](L&#&P€r)ȳΰ ^9`vdY| W8r i44SiofӒՇ0Grr͵zʚ@xFK0ғDh7&8&7Hĩp IDAT"link+}` ?>Q}#Uii\O$So\HsH (X4ϛ]|2.k&Ps{>tdR>1B mrx7-zy1y(Cб=[Cy BNy&/xM[(?e-ͻ;\G|"e'xx(B]atsFD 2Exֺqg#U/ɯ}(lO2 $2u[ƒe/#ō6EgpDE196Ba@Q^cazؠQg86)b: ż! =SW;cKhetzvTl2I,Gء%sT%`m6X ބekY^5kE! (KTq,NRV $34$%b1h92vuE!'t}oHj_*E:l;@2 D9/"ȽyE6/ l8pI#)6(3d~kbiBYqqf≲4+3M_6o1 e)BH 㧳ɮ˔y<Ba@HىL3cҳ?ǜdO֨jո~e4<>†6v1';b ,,xrԴ a掕4!^n@Z>_;e4;7zzN,VK. &_wr -Xrt 6I W4r %ըi 5F [/%]hC$Y97*~囯:ciUxzKY \&֛J+SZӚ/vZvM H@.*h=p3s88>ҧIzlZI营cY5%=Ay%|rΐɘeB@0J<朐xP@]|w i蠥cY ;\"*]V>š ~ 61!sev>yo U >Jҝ[#kFK 9ZLlӼW2tMSêk`g! 4zGdjz".a[gߘ=QܔC;x)ӈiCO~M%33;]ePӍ{U>/D(y}INl*̮+Թ5`LBߴ7zJգBw0ev~ fwna6'u8 )R 4f CqoŲ&8u xY>WX`65K$#"o?'p e o'_=6Q:i7͛}\fAhǒ,]0R8njD ̫'$ S2AĻ/Xp xDQ2GIz5ݺPQcIEز[nHZ2nltQ}%빻ӛé> |ue-&G^_DQٞ.g^ԇb])MɧqDE19! (5f' uc}T,<[?zHbpvȗz<6I_-H>/kYr%Kr`Po~KV#.t旨\.~ޮ-/7MnyD|f##^]]g1zzb%ECh}$Yݩ>P_W#uv4'dasYn _9V;W=]谚z>NPLK"Y|(%1.oy,BcoW+3d^kbi|c)(K3bjn=1p͛S%ź7/IcIv]ߍwBP&%:ѝ1gr7ٓ5G5_́SPnxɎX"!]69|NhJlE\5=)6SF'DU;V};K){6yJduOyP9d 6qWJ:y'dfAecBP&!F-SgS.ַ/}{P 2ʢ~Ͼ1PC,\?3%{{Tj:CzCގfm< L}1~_,} $jݗok^&])ZlT"6&/%EC Md:?\b]dxEaV)'! (hD\"z\i=yJ}סxZEN+Tݮt{b(e9ڢP=N~&t~ٞ,is}O/\M!KDzkJ{vJd7kͱP}N &5\IE]9ν` yL5Mt,+xarCD+J} > :dn]\^LӮ'X>O \Ә/&d;\S]BP&Z;åEK6s>n5yd䛦Usd']-~ChRMU k=?;}O%:Ko0 xcs&=؈#(͐(^olNS.Sdu1 .k6HK +ط` ^qHoz>q0"0k5׻wb3 .>!BPB!0\K%X'+jufuؔ+.BP&"ߙɾѬM3 ӝIJ*0E´*f˵#ʼkÛ qoE! (h~hP `DqGQJeD[\s!tq*Iƻ7rg ”UMI nU!e!˾X.d}ݮm^p.(͹iB],d{z a37 IDATcsM/:"dBXCh}$Y1U,@ |&V:7aKIAdox Hc!]WOy} q ip3& 3:ӳ3LH{"U .' ovF:Ӿp$/"@kI ;[, lxf86emq]*Ѳe)BQg0N:gvhJȨөdeJs&w=YyT1 m Jc34js=Nbj!X0qHTSU,6x~,Ba@LMpZclpA]((j=D6'=Yx=%fJ~ש[/usr RX{X8K!!Uc+EgN RLs[)Qf8~BP&頉D&8{C拄V]So7PrE1%ZkO{$ L>ƳzlK.^&e=j%Ck;z}Y=!KDzkJ{vJ䜁*pLϴ%B4e+Exұl.P+m]ϴ&/P|"\5Ka[oNN lhEўT`*L[Uw]q"L_˥4m}Gڝ* Ui"ĝX. \K:ӳ)y?0$W]D(x bpDŧ(#/F9AMi:@|"Jn[.m o$^70&nIeB4uBPP o{beϨη͖xer)DfL9}y8rljm9H(FD*~N:jFO{g}w.l _Qy DuIwf.Y=%H0RHw8p$5?*( B KD?~)H3h4 5 Ti4TFDfz+TXRe;=~;# !0qQ^cazؠQg86ܳƒh(  'v`jpL[>{z acsM/:"dkX HIAdoxih*۷^?]Or^-.=T8e\TCeɛ ?NP?2>ø{F:I!e).#EJJdl3Oy} q xف@Yl rBPPzQ8IY@Nސlx3uXF#}|F\;hJ/,_LM\e792k(,`Nu-gvhJȨ~-ܩ ʧȝnM@{!W.9I)>das^PCG4@Y b(ɮ˔y<B Jtcҳ?ǜdO֨jո~e4FLaCmuC1gڝ js=b@2k(2fktAV0Og[xS5-z?׽{ܚ?GYY!0L6${yٷ˅ղk97-VLG^{[t$\vzyǥɰ-p7ZFQWt hOb0S&W*D;O. 5jJ+ha'>H%Wj*r) {[ѷv'HۖK"v~ z/ݸJ;Z$uNh"Zi'!ɇڷZ|IA=O1g@DӲg@[fK<2|9x"Mv3D>T+n\0w[%㷟Nچѓ]li:{ f?yb(ve\vo+*97 B͂LJYʦǝ[L0W$=]N(]oK*–ݿ7roXv]ieݼ&Ksw=?jczs wR€2P^cazؠQg86)bh(  '?ᘶ}ZG/,xnY][ 3t-Kt 9)PLO22.eD5G!vt(!eҡѨ3\Z_t)aC1GVsKNizX5wMv7$z^,TS[6l ls<_5ʞpuhoRz18ժ|0sP׀1Ic ӆJff6wwCWD/vBpi Nm6FWOzTH޶'-tXW۷744`;\iagmGt8D) ol|ΤG^<u] b:AGA -9v .wb3 e#(!€B!z$̟^rE.g?YQ[u=\EԊyS!Utbq9 ͪT90y@A,˸ew_qsugWV/^(io**Ľ!.$; #R*WʯM]Bl1x- q"4I2-&S9€&Dݛ]3p勸N񿻬[UMI nBdC}!\]- !(͹iB],d{z ?V<,Cώ-B:՚{Ϝ(M&gDɘ7MnyD|f##^]]g1zzlcz#S^n_Bv3D-B 6~9Y6LtFohi9l{v=yl{w<ź(T,Ba@Q8IY@N|lx3uXF#}|F\;h[& 4YZ|fX|O @(KBxeq:lα(_Fry uS<ɮ˔y<Ba@PoV>(=s̙MdQWFs`6Զ[74*9t#vXJ_xeP y4!@mG,-kfϏE! (SN+%|1ۃPQ{9BCmO{pzfKz̔`b/K5*b3Q`.x}Aa2Tjz&Ixz'gJY I:h".k N<žP<"anT=a mQb '?:Gl>ҧIzl^dݡee3/LpytZi:*(tұ|⠼>9=:dn]\^LӮ'X>O K)(!0L:4vK닖.%l(h1}jN`7MYN][$Xyy<†mm~cbgFsS^\/7m荮Rh]'mue7m ߯|dfaqw;HŮ4ӖT{;-0Igv!0+O+p>s*=87-,x4m/QMUܩ@OHH8uK 7:SJ[-yc'0s f0gceS71v8,>\v}l3Y/)ԧYkIw |HFIG7Ҧ:aHG=HK#q80r 74b:A! (k%ZCH*o=:C>PX; Ixs.gZ?}#A7N*\.&cYL4h~=cxE<р0̮$7OXhGfwݞH %o(sX2k0=!0aL͜,&\ϗd8]|B5c2I;.J/S#?З|Rwܕ_(b ~!tˌa2[m@7P0~!' /$qY PZ&Ul s|+i#X_z]9.BW AveƲ%꥙p /'\ܤ1GлD֓@8 IDATqHI`\#T%8L(roQ>9{'e ^u }{M!#eCx_Yo"Ap{#lDuSN@;-ؾz=i4akUܵ8־ŝ !0K:bBh52it}ުe8I r5-GhAD|'J/s1*.W$Ppe}s杋2B1]7:|l,.Y_&nkѼoh>$4Yo=j oy ؾ}{CCÙq(O};l8еywSfza73ơ8&e0 Eo2tBW (h2*WC}]W?EB D#4E[B]2cBa@A!€B! (!BPB!!B B!0 B}Fm׆۷c# €"+B A!B! (!€B!!BPB!0 B B!B! (!€B!!BPB!0 B B!Ba@A!€B!!BPB!0 B B!Ba@A!€B!!BPB!0 B B!&/uDٷy뉹˵\AN٬e"-\lR^-1KW_O^/lnnAkUJ:vxo@^ E~[ogフ퟿kk{ϗĽ! (ˇR7'ѡZfjmd g-;3ş#tg6h7=Q9B B_"W-RgI4d;utwyXL3g[dh!` vl̆ydU}w㦂Lo3 ܛO*>}_U7ʺ,Z?ӛ>j?;@d5摃7"*\o֖IyɈmVW,YruG??ӉjYUK}m3K56u;?e3p >~2BPBh*oǫXOODOuD'oؘHZYcf|/L~o1W޿'sܴ@.%rkd2pYfzuIÍCf[CS?tRb3fNzFMH |Zˎ7_>f0 *aW<8;X~񆽎%\D1?Q{ݟ(O'YHgO)k7ظA&N<҃Ĩ1w;έ43>bf(;}1 pӼ|=o^ ,Cs݊,[)ut7fL7 e9]Eo~;ޝ#K `hzzD~˷|ƚ-(laO>pPRũ[2Zvۇ7䙖ZMi AhdEH:W:!8sT#䂩\gm8inm7O|ny"fΪΕ{,ykETD#-O}ٍ:8_ fcKJRl˕UVv! (H(n9)K+bsr\MS~& _8SЬ`7 oT{ŵJsy(C.ܸiOnwڻNׯO>y+#FX @iDx-B" B_q@T*;|fPw~Ir*+"eskMz?ɝw{5OBr?=mFpo3$ _Vޒ[u ӻȧ-j4o÷^mx6HYe|,;D!UqI +zwUvk0 /g9`$߾ou _)k.5u+^糎T028wRA֮4fO~{ S(q? }]KrϏG- S8QѤbq߭!I.>>bH+sMgmo#RBG Cu&mayݽ+;^b oCx6 BP„7/gԏ!4 a@A*'3HM| 䄇xB!tE!B]0 B B{ F DQq#REG[Vuu=Պ루?g{bZP6 +!qZ I@W4ܛss>w*LNFU`O$(åݸE~Ǥow/5sݫw}Q۷vݔ/|OxAs_5w*Q|'lܐLU>t9~N0vfQ̠r_g?^zxaMꈙ'O"S4o^T!S' ݍ>DrCmiAB`NY2h0 lcBHvmA}/+geNO==l̒Gݤ*?rlpx\G}mʓE)7LR&wֻeY]6KTȭr K ypcF&\EahRC0>p0ߖt@KHrq؁'/U2-/!-Y}btjv^Έ2令IyoޫlizzEk AO::Ty'iƅIۮ3WD3IH;w61qG1CZH^#ߛ0{[3J!SSeF\$enCg+7\3ҒRjɧHK]_hݪ6,B-9eh[\ kki (5M6 RԪN޳fomLD' hQTI)"%MPwk A֍.~v2Ͼ.umK?␰죛7IfEQtO?&LKe?|gSRۜXP[<Ҁ!a Hi&,o~+$Zv.~U;tZt(B*ϥ / T#@3Zjr͈(б¦{ @`0$?qoS~;A#Ӕkxؤ /As,,7,ߛ!!ڼF K*B(r?\ SSgÜ$׸5Qj2۳k<SS =------Aq-ch1m,K0۩N#~~wB6WM [;tE|Ag-j$?a{̛9Ɓ 2-}߀{>TDZ{YsE BSّ+.[avxͤ%kN].U۹yHD[*S|M`HV. )W\XknO)b4Pcѣ%oV> K ]SVX` `ӱV(۲l;{ xiIW~V7bĂVlЍ1k.Q9䒼߷^Ȩk&3+@`(* OeD%t-&|9VTWf'( +c=R`LIKr/PPKw7 Q=<~Mل~&"7CBjV9AxCXn0y6Γ l<':2&^{)=h"mﺍG]fAy6FKS.j7p24zp+yI֓/W͚3(`co>ŗDoq=*88M~e,d'/FDׇ_mȩHPUSwYofk'>K LNn3;C|x$^'^c;w6+1U|w9{ηٮY;s2?L.4=oS˵&[*ZGl< AJ2*QJ"xceխ4:T CH5{gXb?v4R@k-竟> 7ѷSu8A%?X=ʖE~XBad9ICA'ts`*&u~<$Qݜm--xFL]U>[09egOrl ҘVJK8)Fε1?hϭX?GK!Fm_%jo)28tʴ8Zeue፝$*b%dU<^WGB&sϧN_i& AW|Ė;IJkPeK_le !0 GTԁ*ó$Œ I܌YRPV~s.ϝ`AUWhZxtECJU#{^Ւu% !brLޞ<}"e⽙]LLW,A>ˡl;iAjݛc7s'WnUq8ruOK,ޚj?kuՔ©+jor}{+-;M5 4ugф;xyISE3xVwmSd4E 'Z^_Mkl236ZTx$(*-Y}b;2令Iyoޫl!*iI/0qK NKڱe !$;}֗ϕ7˱@j3oY׃J>i5&gH@^e\|4^<=]r$!ib~i/\1mqjlqyÆlxqL,$>ZB@Yɍ'6o%_r!$)ePZB)M^iunJ93ZB\<~RGS2igm{t)òmHR4[[: 9ffdhBHf!X [zujz)#C$(JS'YXFYcFZR\u{<֚EwΌ4 ]ww:h\w% ՒOCK5 Cw521Ufĥad{'nFNl:g]lCXŨ0B.9eҊ6B[& م}RWTy#el~gN򽻹Φ)ot抈o\z#u-l֮J$(2HrE"Q < Eѥntp.mݓ_}t#L 땩֎ $(wQ)oc6/mFO@ `a=LyNN%ko?kqqp3?yxl>M%jRB"q!nMeI*Y6~n>.&T 1371tCoE5є"^BG-!Mh՘;2TXb%R忥g9)C:Yl@{iyeDxlcԱ4A-CqeC?'  xm lg%*$/-{קzKyrI^6\U/-g:5tl4 UBw9Q^ն9cO8fPʚ>(z?숌|꫏ftPu~qe3xGM–T%ԡ)s U3"d'o4$({p;֌C+ 2~mN#|=]{w E2 $( jLӾոNil޻Wg S<me:bM)5<= ^(Gnj@NZ(%KxT]ݺ,AS C;Km8?L;2mt.|VY]Yxc= rl ҘVJKGQ}fğaxA ٞǪ~(an0 'u>ݵr߿sFB_l؇K!~e;l#VeBƮ?B٥s~NҠ@[xVwmSd4E )(+kNz+p4-<~uI܌YRǘzMYHQߥI4a G{3!I:acZrτeּUcNuEѝ|Nф0ښr\@mo>[SGa[̉0#5#KFB2S=CRw.⣬$A;zP2 (+q# HChZB)\QIK |%L@N޳fomLJ*]1Kg}/f7 ߶LٸЙGʚ|3#͸p0gw]K@DE]xuے'{8$,MGҫBhi7Mf^PW9 5Ɵ߷uՂ{;>ā")ɩdyir^&y D+Ӷ]Y#SkRթbfn?PԔ yPt8Fl]ڷ ]SVX`ǼJII5G]T ͽS̝f˙kibmjeOr.)n׃2o2Dp}707Od§kSa]H%9١g*h.6 eΰЀx=a?Жq3X)R 7B\F@mg76ږ1fsͅ? 3\=f$W_) 3xcNZsmx ]XOӊiC T'?~0au%fI͂f6Wd)XX+:?\2D]s)w|F,+zǵ[y5yThd'H'@|Îȧh֬^9HP{P56OC5@k@ A$(HP $( A@HP$(HP @ A@HP$(s/)NL Of0V7od6W(W_ \>uܜ,Ng0dYf IBP98:|Om-TZrhЉ5xdY6 &׍? !nF?𯗊F}iii:uGEgRàQioktD^~`3!QXXj p 6*T-w3CmB !P@ux7]K;C4UV2}SL(GCtR"D'RTS&nMYՕ9EeH-1r|=m[g 9nMT>;'-iW*C@+Q2GZXG~#ģ\(%B!N)MeNQAy28诅L^lfOUkųKyVQ;`vs(ok^fu۵aZ+O/ٲϺ)l$o:+Odfҧ,xNL8eFJ+uΪq,*ƺM1i3M1jӇKr cw=-V5ҹJ3ve IZdoAgu7AA%e/T:DRX^?]{ f_:VF!ZC_' Tɮ}'Ƅ; !$nonaUpo>\wT S'i׸~Meaz/sǟfK}{>9}g&N|[NwQ{ tvv_MzeRako+ Cz,Ƥ Ԣ__a;+8[:yNlX5kQ&̉;gg+B(&jVsV$_ܿeO>mEFO|O(\Z2aH'7s~.zߴc^\h1{Ϻf>ө )Kב,&"Ul=#uP=? !ԝ[^Pn:*'U ]p&Yyddh.SUKNtԽgXrn{l4/6œ w[cNm/uo/g%!d]vr|F0TNwY$c~FͱU+?YOLVP| #MVz4Ҧ2{47 Yj~]'-E 'K[ }~n]6hk?vPmEfRSCcud&˝I4!XGfģL_OL~UKwLRՒh,UT !.hKwu2gW e!JJC0lOn>d5fN6ޞ^JB !PQ>`̤o#3i\W#E\|JRzt;[ԦkOn{43{uYm4_#0^3Yհ}&zjeލ{{p/jmʼ5 vnn֒ȒoM)jx4Իf0rwyҎ 3 3׌N>%m֎Owz>%97RA̤!==(k4nK3q҆V:V:2e$oy2RAX)o;;~>JFK=O><}O|o-PxtD-w퉽|?ݶ}ώգ֟, u Up}v<עEyg<ɦ#+ˆΆ,Hݶvy1n zɵӜ ޹]'>Q}1^khcі2TpysDfRwndM;@WɄ14YGf)(; b;$m\?Eժ{B9S{ JՖ.Yu7Rc۱#T[?~ ?_9;q͋K~Y8!vٹ7>WZ Z¬7zލ8 ww66۲@:+ ՅQӹMoyJwY*jF{۾2ooavmRi1pkYF5,nf[8Irv;9{L`V)vki/NhRSZsK&Shhܖ g=*ڀ[5;zWf֥N|ޭG^jt1sLu׋ycބfݝz3yGp sӸQ W뾚tuŖ>j-g>9`#Ҟ0L"gWT'.2iNݜnN%qut=!5ȭ2!5mVEciYK'pۆ-mBr">/ |om߸`ń#Yص_ _@{ ׌8߸-\c;{ ߗ0/u]{M/v}-[ߗ9 !9Xv P]vWN᧹Eo3,m3O{xa[jUj#bz1ow̛[pUL4kpM n?JX$ 'D q7h;soЍ#pi5_"[g_@bz".ɒLqʘ+ͅW-ﶺF}Bq07iYtLBR|;1H Y0b7B#~ + yL_PYCWrʍ1geۙq#&vivʀn6꫹'T}x O9\Z_/:V]}3Gږ  6=߬Ņf-.r0NGP eQ'Wl}.dih99nKX-&u&᥿˿Kݜz֓O(f\y taK6}c.]22;<3Wo]&cKK l&m[Hvil?HѺSDMؚEh?y徝"Y)7CX5f/:cz6]x5R)Gz=bފ(b"duyy?:ٌYdt0N'qe:}0\SzaTϞ)1ڣ*J(<]ޚ>05UjB!BuYYE`z,_I?zK"I~&5(1c[TĹ#&{oRg1=ќ}?o>aWNwwF Zʣɯ4FIXq v!M}BVgn[ʹ% !Dv?m;w]ܾoVef?󗍈\d3{]۽vOO6:Gz=bn[o~()Y=<k|4fCOi>|6wu^ШU,2fEo9:JBȺO5/W_)%>}cb׌wJO?oFxbހG-OJ#~?˭#1okUG*W'!^{aAm?n@Ic*.\OT}[3|>+MUX:. H֝f}3^j’7ߦHrj'\܄ {608 dm MB;+֋e.#F=ݺPp~8)qz×ۥNr>BH=֬qm# ɩESxWd!ٷx Wf%dv۸;X9Ia'[?@.oȕK>_s#}rfݿ\A!6}yy8ۘ"N?C2UqnA)1_O\1d̙Hc_ P=9wݭ 1uk'(=0oZɹK y\ͽ͟Cê;>=E3kT9/|}c7.h;zlܭ 7eo'U>.!ܨ#9U zu gWn˽VUV{Y7/7I>;{߇h03gT. __K)L 9"$ζQܗ-*Zigo#r.]mB]0hrms>7%!>PD}\ϮݡlYrJNN ?~Xoickl֦r6Xh|\B.KadY8Kark^9Ԣ8{o= Ĺ8uerqO,K8sVb/TU:|M#y:J";y}̼C9t=3)k{׵7*)I! B]?6\:gĆYbtm*9.}^ Ҹ.07RԒκz)ʙÊ/g:ԃ!I7mH2Cݚ=wlPwBꠀ}:P0B!?sE.^Vp`[h>`Ќ"6}QK$Ik>_gd}n;B_OU~Vۀ7\),o&EK}꜑n6əq<}۰ū1r\*C9sXEһ4w+JFȸGrs1F]ÞhTYD%)>5kzPu5'b ¬aj_Jի|9t9Sgo3p%,cMu1#kY<m0wp=t,߰WZ>eIDATgW\XmӥSǏ<=5J!>2F6``Ϩ] %+/gT+7Qe 9ܢcs:Qx$^a ݋f7^l/)$fG?:^>'&:XDM2o@?噝ui{V>'̕jcڨ67%&O8d _oۄݫɓ .;Æv7In! fόeu䓵'd!Du?|8g -,cp\?57Fڨy9gR=,Ze]9hBֿ,OOe'bl!rM[zӫyAr聕+~ *L1\H=܍?V.|>2k"0}PLx(ࡣb PԿ~/PYA ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(BL V@7 ( P<"Y6bLOo%SH,uR@vw)߮zwW݁z7$ V|%_8k.7rUV|_ۼ_˺i?6w^ΣvHYǔ/Ve=UjU?*rOԃÿJ?n@Ic*.\OT}W3|>+MUXM:. ŏK֝f}3^j’sj'\܄ {608 dm MB;ߖe.#F=ݺPp~8)w×ۥj9{_ !$[ߞÇvkVNθz_Αn)}cM _]ߜ59|ɶ!zoX1Rcghf-ٛ*_oLmGmuᆐ| ~%ud7R $[N_ZAaa*qʚr3VY-I;{߇hòv *_sW[Oz[~ė8Ctx (sV+DHm壚7ݹ/[6% IU=F&\p)<(`f%'e}nJB|]=Cٲ""FUo-&y~_m7q.$N]}٠\ܓ!cBș$_xrH@V5`+2Դg~[{17 :zЁg4p,^Yۻ/O,zL]" Em^cåczyFl-FwߦR7.kXKss)L- q(^r-\MUv]C)P0B!?sE.Ap`[h>`Ќ"6}QKJ$Ik>_g7Y~MvjLt):!WnPzzcW}TDAb£. oEң>:2`lPٯǽJtl  &*I!]Ӄ)>kf T{BTJ^SIKј=õ}C/)g)kw1d^̚gelsSd!,29Īo.:~98Q 1{F^(Yy9\*c@eayM?8+ϬA<w,:) qOD]xV!!r,y>'&:XDM2'>P!5yyfg]ZUO sƿ>6jM et`^w0B{1yry'uu75]-|d,<'́uel˘c6H /'#LrsW*%^+:e~ЋWeK_d-}aӓfى3,[GF2T˜gs!IF~p@>(8x(@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ ( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ PP-,jieEOO|}>V1;_ѲeK!DdD`_^-$1;A+2<: xN++PIENDB`brick-1.9/docs/programs-screenshots/brick-form-demo.png0000644000000000000000000005036407346545000021461 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw|N#{Ib*URbTIUE_(mAڳAD;;:?bWHx=>|>h>B6iq.fLn~5qvqT[*Ji½zgjdez@$BprvxGm,,TzRxGQ@yPXXt-(ȡ/(P89LFjʉBBQP U半i!{BLtAzC(rhC}k_ӌ U^*GI~SPGyń,~OS-OSӔݩSJil-3.%=(yK;{ޝ BH>nYYZ[?2Dz]Z CAfޔqȺl`-̅:+O{sA8\cy_/w%!dCNJBN2t¶i!yQ]Ǒ {6K4YW^c T39TylÕSQs;I悬 UeGMMwVKNJ-t6:΅>ޑ!+dZR_kEhxnqj^55֯MBFku˾( +78|ﻎ'LWx%sҚ7tYݣo;A8!x\n:\{˦S̃s^so>ܢ9TeY;-l(_߾%ܵkX);v4Hl`+튟 9qͻ^=,£_w1plLnEՀ:*Lk }A͍jjg-0ۀYG%}oҽ翔5/m>)~h'ũy%ݑc8N4X6];I^B@yK%S]ovș /d]_7`kU&~֒95|R.Eo⹙9u*ԩS'N`N [HÛ$>v3Ya_ă{\IH kɜ~1,vNMԺz*ɜybI,Ab.UF,%ɲ5BN5Y[IRyU}(~ZyI_4Zjqj5BzTg8QTJ2^ڵ;wI*ZB6)J!|7SR˙;SJ0[ݖF,P*A!$B, !Tjte #vq09t^]\6LEn(~hԼjV._םq=ʭ2zn؞W\xY|צx~瞘 K j #eع}S[Bmg̿֟gqߏ"\"]llg=7UKr^ū]nUxDz4ul߷{M=R{ceE曅se¤ߕy/MMfݨ""hp𰕄^ܢnoqj^65 5AQ*0 mNG5d>RI~bu~ԵZwUEvpsR˖ aq5|/{tY=?\ہYrƨG r>\lBufo}!z_iM>|w۱/~.IQğ͛,>_X5g`3*Gw'6'.kr_z-f- S/_H֗,K_ʚv'r${wK}f\xȡ;ͱkt|)?;SE}S29J!?vmt`["X{!(B,޿ |J(jo33*YkNOW㛞yAzm =(O˖ӿJ."B-?ԴvS~锚2ۅwDEib֦ ɡMZ4ٷR3Qq4r6\#eϏj"?U?u*M bwQ~cOE =>役t!s);j׾LUib֦Hrqzۿc>]yTz9j }eNTҜukº/.49Ujj/JyOWx9toy(*=((W<ֵ9y6~DjBMsTڡiƚZ괭5ѵ_, O57(=&.IoӴ8>[5$VwlbJZ6oR]I tM/٫yPTq #ZjuMxcF#-Z&NVu\8%uAroyӴU~[5=GKG!D)Rܜ'Iqďuog_G%,{ܰhl4뾎~TjV6ԧB{h*H"wו.MZ֌mU}1wT_:FzW.m5k8\PXfhēU߻e!.~j[(e}ΝInI -[-PST$ TkyU6.@ԿޥMlRw?F"]gQuQ܃ UzR˿)`!>Iعm aL6S :{򗫭~6Ps΍)ui;P|D4q9mbJ^eպl-(6ty?p^HBxc' j1k踲5uK:B!U'8A'!jqi=`q'W%B%8*w6P6Z`炩re!ys\M-TAiz1 B f,;3BME9)'oMIBx`*RZ6 ⷲwX.vhĔJ})a(Ģ߿hllԕBS"d!l\Enhϩ(;+}]=kh[>ѽtϢQ/϶h2˛\\_M<퉥&ms!݇[6vi:sJg.YmaUQaK"_1wH+B6 g//*VXGj۷61wNQTǡK=.KqN[XW ^ה+}9/Mi4ɢ^o}hrĔJ}=Uih㘥v7̡rNMUSOBR]cpZBζp+a}^ۺ{^ْ(X5ok\aM[S] NcBy#pƧsS̓οǤ2a۶~GNfc>q&qoT7&<'YeꊘYaW?jUI!g!dGK/Ow1y_e|6%p5cevVvͥeRW6~v8l[%*c%=T6Edo=iV3顙FCտ̙LɲSG/M\>R?Lo9q}93y.vOsO;^&xmS3|+}5XBIp| 4~191 ӫҒ+B鱕lVBn47vF,h؍47gRUs9-_䵥C=? NZV0nK tB[v˯GxU~SgI|0FBGYo}?sg./&9/esF~_eoJsVR})υJ9/3jf█&.MLԷL`\W}.Hu^zVAC6* _dĶN]]٭㮧oJJ5Gi W41:H*V6tWO<41*.VaIԉZ|URĔ%S\|}C (w(Ia2N?~eZ]jv)J /?^/[XXis'9QQ7""Ͽ u: ((p?S^SxD(?  x(@@  (@@  P(@@  P(QQl]ܠNP\i?[$JJqsQ uPP*ÿVWJ]ޙ<#l͊$+tg1ͲI̩W滌y'gw6QIWͧN-ͫ63 !Tib i˷5)62ѷEڱ(=Eo\$Їogt:SBtJ>-լQV_H"g^-)|yFMZv̝+P=(O9_'^׶ cGf]~nVԙ]Z9cFzЀ.Fuv \-xEpppPPu(l]ܠNP\i? l_K1hQEp;Pp PRgC=skg2у(@@  P(@@  P(@@  om^* (x>* `7_|ާ%G)+Q'|{z sVj@5B(_rJRH+:xynyU=xfl*X'־OuzU3SR(m~aÙ?^ l[cuTI!ٷz}[4W4EпOݛb !9uh%]lUR֤qif! :a%k~=4N;qtY( ?usO%6^Ƥk) GGChuc> :XK 9?L$<]F9 V gw'uϱ,+7hRKcS©^pdLU:fqpTph9?o5@@pħm[KȲ8{:n2 :F1}Y0sn"HDž;zx!c~=C~sd!DTւqZ۞!+D^Ű9f!DD,~PWґ~/wn?Gdgo/YSrwF_hu\\T $T~c#BN k5ߖNmtj'*]PPE nvy[h, =;S+Q1&w:Ǡ{MoM+[ Y*)E!2L3)󛇇9|DO 6(ӕ[l,>by/Ϣt^&ZBcnujTV,-t $ģ2ʚTco' Ƿ:йx@@^͛{[U{_fcbn`ǠbIdNܿn W6楛x 77{}}E7k/!F&cSIW&w*uyg05+;o4~}#K2l6 EP*dJ'Nkμ=EIEo5k>.562رa*2?nv,Wr#rϏfO6'Y@F 11B6$ʧN-ͫ63 !TiӚzYJBWh0.1VD T Sf .'y&l&[vfٰ>Kv_L[vDQtu{M;O,\N<& aѠ+M®.uj IE7cP”7d2TB&D۶peץBɦ.DY{%Y3Crl+)bv!'g*_\֙J"f?cDv |+胑?O*+i4@@pdMk1lu:{a5C%[1?3$BmAwiARf&\rq&!Y*v~]FpS }^Z~? LYY[ twK6LExI;ϳޯǘ٩ͺܬ3)^ e/k {:0b}F0zb}F%Co XͶr:uiBYEC )A9t*ȆսyQZ [%w 9j=(PV5T3%gvS'0um٪섈c\}; jܲ5Exڸ-wmȕD5E8(MWAU3) P{NÃ/<׾ mUIψM |imZ4]I@6r;ҡcM_WӧC D Q46пYuW+YIK9f\Y! k}_C2Rr;ڶcgrsɥUk2oԝΜZMBXM2'qٟ\+I:qzl6gq'[YjH@@yO۶nBrn-[WW !ԶOæχ\J3_Erlׯ{ҹuRT? Q4҅=hwIʓ \YrʩZjeS<7TmbLJCMoM+[ Y*)E!2L#rASEռU\Z*jV98&N(Ja25pHdNܿ7^y餓2&v0BHR%k!̔(OI>Ԭw_sI-UQ-p[͚KMOHiw\j1vnv,JO>*ZM,CkE#Ykt7Vp98,>p3g6q޲3Γo}0Tez}"M!uxݶnS{Obikɝ؅S鷣i1JSG") P|bװims0x5]tDb`FvdZ4jˁ֥F !d43҃t5Ln!8S&+_D=Z~mJ<=!%(' ݻO7(1RG*Oe#_K1hQEЃ$RlPSUnڣOtPaP@ggf2k"-r  $gYInQYzQ7(n^+t*s. I'sP7]F|\|B-DS"nߪG/^f”s5dNJ?yg/ yC 9'>>ǵnNu#ny(s*eyVSY4ҫjt=o%2F6B< O SBtm>Wv],ljn$^i?v%-ϨvSEeHg9>:VխϡoB4ʿ/؎"Ȇ/~1I=H=OYG69τҺֵ$a̜c] dXf1:!g{yc':M sm8,j/|fd4g&\<%eEpppPPu(lM'ِl`˭?~^A#G*kP@C@נ%uk1P(@@ P(@@ P@@ *ɺ|^̀WK=%xX 19ݦY6Ę,}"D%5ѿHMӮOӊCsSj ;_UBy缱Y?MTL|2_biIbM(@@ -GLҜz]7}ȝd^E%y@QV:n|}c`c4- fYJ2F$E^LnVr t9ߏl̫XWa&+G7lso5<|a6+Og GD-c߽Q-GMЦڨۿz隓iE;7ɱ詯tQs#oZ|U|ϱ ^iw7B3kO~#~dwkOeu+7hꔮ:>d8{U0;]a((>91C yu݇&8{>^]yn6]ؓ'"#O[D%F^~/H*VSszDsz!bm|rے5”v4=W-{nW)"EXzMJb}Q6%&$b'hN:wP޼}96:F1}Y0sn*@c~=C~sd!DTւqZ۞!ߣ(@@)}jWW}>Ɣv>-C*=7$ !$! !˲PHTRIȚTco' ,±~A:5f+istRwXO֤j%'{I ,Aoll" f6sΤqS~8I mz>`'p6V|>[3t_U3.N=I_u|s3r=w' K+gHХרΎ63–0=FGzN ʉࠠ P)k ;я&5~jAsf=ɣ~=6Z\||?/YrǠFA ),Mvv$@I(-ER@ ezեxzR<( % !d]\ 5(@(k ;_UwHVݢ"?(؟n^$~Jfl7|tfMSϲ}ggM;.~ULgsx.  2mŖ,Xq+?-e7o<+%IQ',,>/31k싾L5ҽr%[JĔ3UUgDIdfgg6ɮ+o6س=:)9"۵yB.(RVca w&v_Z92j;X)?3=(ҷ~g9Ï_L_QL( L,0d$Iɩm.^?aUBmt[:wP޼}9kR1/-1!dT3wlj^]Ȥ:Dx#*”w$ >57bȚTd/ n@@*09B\v폇cfU3uN\IBKP(d*Ql 3'(?xPP~#Oavrb+lmʐfv }^vhJO}}]Aoll$Q~? B-z.MUrꡝ' }02Ufm\ǚRjоiF-:to3gP܋6lp.:Ua/k,ھ:a›A}]$!Cfr\x{ǏzB%ƩU?mƤ) j[يA#9oC^X7uwkg !}ڼAY!olȶ34jN,yb@@LJ+{''', 4w>+$),yu q:]=?9/LG jgcŬgF|{I fʬN8'r;L1^]ʑ>pc;9)GLCAJ(Bɺ|y 'נ1p~UB"fcy)~?g\/OxIDATWy&>׳&1@2mŖ,Kt0[蕒J2dYڹ[*͆씘s֯r.$\SV:n|}C@@&]nvv#VcaI#&*xꯧ>- еp3 $djElLd[9=jVVTB(]BjNrӻWt6$P6xuB޳VB9gϬ1?Vx_-f#%]S x Kj-8*(NX 2tdUO'Muov5gkB6L!̳v7'ܑF  Ҭ-ߪ$I!¸Cd!ܮ W}hkIՕnӅ=Y^ggyi w'T/x7ohs_ @ϯSz ;j{ |s?Yp,KS*҇))|ZPU{%hY|%A@@@P8W#LUhvMIb6B(2OIB60F^15WC?ws zdecC(l\ -j}ipc_NgB:a}aj}}Rv:uĔz%Vئπss՝b[C3efr٨Mtpunў_>sn˃_;AhbllyqVq?}<6 xHro_X`SFB;8IJ'bφlB5: ,Mvv$%PT lQ`i~,шooX  PL1^]ʑ>p! R@@  P(@@  P(@@ P(*Ʃr6t%Zѵ&S*JUU ! N5NEZS)6=E>ۻ=YFEQ@!DS4RY͔GV7Eii=6J+>Z-^! hȠH2 rRZ X)9~:_Ι9,9i?wt8׸1ˬG?ij<5$C! G޼MZ+O>(,3'R,6:1?$ 2o1}mEgb&O1?пJ8G_kp.1Mmg2Slܑy҂@Lo`l뛄P:tvIS9{xV|Y 4>9'Xkگ.չK [8+AH -]7Uq8|_؜~…O[ /m;Y|yS>,Pj͹;2[-Lݶ5Tz=Σ{V"'\iyٔGΜ:E`#Ԟҫ˖\uZ*U{3+bo?' cϖg뭭ݒsW2֗Mڼ@Qs'V|Ϯy}KyΡ7}mq.*ͰqGymokW;Jv< YwO*9=wo;Ip Mש BWbbipk¬tZ|Svi8c :M,;0Q.1 !eVPuPxh͛.6TjQViBԧTiMŢ;ضrU+ 6lor {ǛJx>*}kЄ{tɖ\W61:c훖O-zȋƚOI϶ږ}+*V&Vujӣ}S}:ZvZ rsQRq#}ɥ&!Nex}ƴqIXX亓rL钴BV4ĵ̩<Rw뎹8}w?scF!5qAҌ )Ԛ'qwźf~t*uٿZRvڴl{2_#[]M۾LkBҩ1{-2R;>x wvYV'oi+KOCrc9o)}u&*62>gr_SrY;-yWE!C ٻ|ޞQzs- ~<á](+[ ɳ4QjklzHBtaɖ+T)'xW+i֘s??Re-nNCza?^aE3(~G6}=4pXo3.=)^ͧ7e5X]e, r,$۞Hdؿ]W~xnܢ=3]}ߕ6(z5$|uӴ{fY/-mX\v4ὲ*dܨ #K=Sj" clӖpLJ93٪i.e^"Siz½g/H4v oq 1—ޘ31#_6 AiM;֍N^XJ %3n(wdm[̒]@ws.f͚}veQ/5Jpa^T~ѱo-?}{;o.`Ԧ|OeM~ĬνUCn(sb:yc/:BicǏeyiʬnڙ^eY_T"{}(^3G X=cĘɑF/L;-kX=S5i\Fqy KZr)O0Ǿߝ$){514PӠMzϳ~Gs/36:ȳkT/l|L}H'#riH '߱#k2ˮ6lwxN852c&CoAwgT JVfnXkO/*ĦO?cխ%4t> 0 LAm>wl͒z{X&F8CV2v~U>;; (1ipLRZuReiQ'=՟z .u8vtKn; |`i{."2o׶희:םHoM6MQ΃"Ibi>P$.JέÅgwV [ס$~rrLչe^N옟gpoS쐤I\[m5t N=rk|vg^çoQ_>CF]QyQA`2 Lí-bZ͹ܓ;b0g; nr| {:+Fr6\;Npj'WsYsҿ -iԜbïͰi đs %>3䦤54=,9O4RG2u ]0_L9c׶uEa.UsݖSBl%% VͰ! ^nRUtPՒAS7w 'Oq6tbPyf:A}&}`K?U,Y8ɫ -YPKڿUٵ}`mRiP>fLBӠ>:U5%efgV:)Se(=P$Kmۢo2Ɣ=ϙd߸_Vƛ1ܽU!E%Nۺfr MaTeFn]e:88YH"f+LgtN9tPP/KT^RTiP+n ܘd:''Mӂ9;)}_ 1\{gvo?|^ڸAt&H6JS!TdVpqDz󗦨 C:w7bKvtZx}["jמħn>dPcݹ1%x|C{ɒ~ώy*{Q{-cmĠ-g%_ KV9֩rj2 EpkKʲ|f :I4Ӡ*k9&,47INʮG)(9&ڷeOhGNpa_/ nttnێ.Y7ӍvG{G%Rܵm޽wcRt_(N9U7.ݔl{c۷>omtBaoResVn_K%}_%ܽ]\+[PYQrXh!Ĵ#7}<$ }ek^=_NR[-iTfߧc}* x{X}T=رfYM#S=ZVsgb@zkL0&6h@E{vY3HJ!*$1WekcqkK,Pzג:m\_Z/Mg望v[/O?$G km,m]Zz@-?X%gh-; jFHcjcqk :ۣ }k !/Nx*>y/;3Rz{hvç޿"?7_[\ \^+gP'?ѝ͛V^ZymO[ՠ}jq>]C^\ӧ MﷻeڲKKcvuV\GMekcoAv{NnWYmʐaiZ͎h1Jv#{K۹;éYej;qjadsjy=@7\UZ~[g[ա*eg$ӗtЧ޲9M^/*l|ٴZɺ@nTtSZ4mSp[L0&6h!.̭GWY|d]꽻87 n>/5~5ykMYzNNm'tsB]p=YmVBl/۔pRº]v^}+5仞^LV#rWmVނK>VgBr:.Ϗ/}qIdVZ.Kn=N~P O,(3bSuj<5]g18е@ ! %T!46f)6IM:9,;a޷ةq+]}䨳__]mTB5\;c(@6ugeފOT";rv(ߒ_zuPqV7yPUf!Pgz̸=\--pd\_o1TN|t"Ø~mA}1K-ٯ~li{7ϝTVJ"6v62*OY+>;褢· _޹u=w_ RcLWpl9]uHAᎎ {b[Ocԧ䤖H[/bdlOߪcKO,5omݕ%EKGl㬫k)C 0K҄iom.IKE{r H{4#,h&`-߹iej&K:us!;bv#[Ŗ@rם> ̓٢qVg18е@_= 3ޟ[Bgy8_Hݛ(JҾ85sIY)|c&گgL<-&8:,^Nퟝ_!Bƨ Ƽ&_'֦bk_S%(T6)9R5W瀱՛ͶlZigIV O۾ٶ-͖Lu-:i?㫺a6me~?nͶn \:˳FB'Lh!\;,Ëӗn^aC+Y96k^U^uJB?d*7F 95O.jm{OSϻ%X.[@?mnlAȮ4m8 ƁM܍K\Fހ<~礴G'7 a/|4x~ME?MsX U’.|]PǶA8~eG|[<g׌+}(Xϣ!zA?w4 rhixB=F4 h/UEj5ңyB?h&8_RBbbpwpda69wEzΠ^ k̅<܆g .DDD\A\XbkBLJ V|W42|ۏ6,`PSq忌رՓBMӟllo x ]QԂ_O7欉6[ߝm\?NwΪP_cwp RX$Rߺxj+Erau]ԜKǶ,_bۇ<+\(593]֦d ]%u!\\DΕےB l\leJެupJu_j-M ;<-RPT\?=RJq!e`N|U XrvdVH]ͯ6%8u6QWtYnGPJŅ/:2gXq[]NxϮF4UgnCARgR@Q2vhSB/'!a&vΓ\>ڃF+yy4hHlmN9s6cL&5;rL>8=59o.~5o&$k[mpfOD^{7h,_K2[w[oK? oҲMS [(Ƃ9s܍U*?͙9ftڪsoYsԆlhl7dm@DbZŷg4EşXᲵ7Grk:A9qOZau5]M6?xPEmc.KU7܏}7vtߧ^Tt@JIU^_Uܾ|ɾo|kh@U>o*|k ewXv w/C@Eo, }|i_q\x  P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@ ( ( P@( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( P( P@( P@(+͛2(0ZFڵBƚ]RBbb(nʴʥIENDB`brick-1.9/docs/programs-screenshots/brick-layer-demo.png0000644000000000000000000005417707346545000021640 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxgXWw{GņEEQX0ƒDcI55jlI4#b b.;첻3bo}ݙs3<{,P(KJӢ3^>Xbdh`ध"V[ ٝQJeYQ/xbDWOϽG]$*"Bq;.ƍ#Ҫ*^1Ye\w"s.Ҳ[7O kO*R#[$E(&hiTUj7DuL Px oJ/7sDTdRe6^X.jhƙ5_ rL8nsebB<y4j~a,:vѽ^̊NIFz2ȸPZ{ t5K.{_ iѩ{m5dKaqGmuJaS$Bkp 6jAMj :Y|Ǯ&}3wxjӘu =nv.M0曯d79I~\>ox8aߪqG᭝;6c usz\3\`baܡkk 5|aCq_Kf ⧘(yR Qi8I}Ž 8V=ѯRNaaRξʿ!/ྠ',vy'@}v嵓?AkGպψH(b b]ݻi Hp"b ܾ?L]vϟ6N&Xr樮mZ65V%[25>P䯟5\IӧRWPbZk[ymk~Wmi3>SPw|!6"yqZ N&p*f[sSNV=*Y :OjvFs~t,R?xZ#D<=qֱX͈o okXWC(ˍpBA]l8Ɓ_sf)fMDRq͛F=8߸3:,&AEǶ1* )mGaqG_|!gRKuתupNMZA"^":iltRc. }UKE+ȕQ30,. -]~ATڛU=FAb[QNq歟٫W]^"S5}o:]ff[[je%֮~kR2vˑ;Y o:X xEż[6Kux}J%g aPeBe݋^r:fNoG<[UZjV2bٹR0Z۲ȯ|68T^lZ_0o/YpzQ\Iv)04m\k5(pW>zMۓtqukƴ^5󹆚 qX_&Lɿޒ¹1\]-sg{1)/_%F#69p~nFLDfشog^n^뮖?L|Rw0oӸ/h>ŃN\6i古 !IaΞݘX#Rf_U&BJ2^PֶbpŗNDlU%Kw 0voL\%ˉ$=W٣,?k:k1wRMM_زGR]۩2lLtNԭ 0mg>28ʣG,*oe%bKy"dU˗D& j u1% Wt%QSasj\6re`EwTW^ !};2g fDb1CsF& )n'*y0#nuv,dՂ& v:G jt$=bI|d|pgnnA=3|E\l*K4g*9kҦ w~VK quu֎q]ʿʿ^5@bI]P:y伓Qjgb%mq;OlGΞ47πbRƜ=#7?yI*TfCjOxc+?wҩ:+\5tl\GOfRҔ@mϒ*qq;}?6e7)_xT6/`A'8"28dz-9{7~\g);{Ҟ_TFzq;utv~⿗ tiF v\QD׾o&o-, p"l@wK6'"08>U30Tg ]9ewb7 VNY_Dfii%7R$]WLiťXHhF9[YR$yOG3v;wR/K\vwk=+xȿfF/zMCRKXU];19K2wɰJX}\݅WeƜ-#"uQUFџg~gt5SV/^hȋ.G=25[2 <tPT>ZY훠7A!"O>/ | ZLvu#fro|s#eI)nw;*oQʙF>yK@a砼ĭ:ӈ_?/lǨSQrTqG-pPd 6<3xDؘtZK],x -FJι]kVyFV㎿&#W\d(uS-WtB8/ 7@ \!gnVr^9M!I>[!x\rTQj^&ˌ(((/s/dA\ˁ((SusfXL!0]>6Lڹ4Ds:6 p'MrH.$+U塵tkT||UoSI!!9tݫʄjwJݠ-&[.qө*]v:On#T[tmnY'z>3=yM;㘐Q*,s:dʖW:TVt YMtBs%{R: 6kf4yɣnd0oYg"_7gBnZUT|}F򦣥J_v[t$"6XK, *?ҮE[N,iFrKW{W26SZիlҴ,{ xM w)-JaiC'e759j[1:X5asG_^z#Ʒlm>sPƅ,Fݝ}!f_Ws܃К',\ca}/NnZ˫(X~=[Яhea{@ٶiٮ_Լ)lNDHvᏵ2l‘f"^K_S%"?Ykv)cZ9ihi13~i.aP$jШl4ɉ9\jc묮^ʄ̦TE+OS秝@iSW0oS{6UFu'[_91Du7￴E)7=a#VjSvdU<~`vG$l̷$;ߡdy`]GYFe:&+/geYw%&Sʸeq75iAMp%e""E#l+%N ulOCk>^{uz!U/1"GQ!UYZZ0oD$jս "S>mΚ ̽>G7r|QgQ]v٘˕eUW~ӧsO$)N.MĈU}EJQ:6 q2fѲ}Q eڷ}O?Ttcs'5kk킽-?_I4'@Sfomx }{)tUdt`Dz7ǵ^Uwь9NDTufK4Y*L EU;u/u //1aU++5'/׊˹AՙC mi0DZ 󂕤ӯoc?otX~;]|yEAz;c""Z(Mj(<,ͪثxjr ۄajK#dH/^Q]`sn:̀4!ՍXhy""Um9{fl=?o<\mf>K) ,Sωϭ\w i}2n,h"]xͪlB%bgck3aҊ<^Y2cKg_o=V_Ns,ϡ7O_geqIs4={f]_\1uF/utOKXꁦ]3sǎ U)?I5M}{g¶jN>{H%'㶤m[==ĎAC 5 oNvx42ی;tBݼr^UW+E1;z U#?N~7]U]ԜQ3HVog~ZyI[EF_{WgPx32|nuoxx=XyƝ;wbqf\]]B! NAAAD$oI$.]~E1둔XF@ף/wPͅ( y*___^///(g+4 u!@@@@@@@@@@xJl#lBXR8ے-ӜDD~59(Fiki9EUInr,hauܤ2@|?ū t\-P3DꉾQYthoAJb*Ky4;PŒ4)%'[\HyJ""1[%)(dHho hؚÈsk硺(<3t <1z\L(":Cl~0ף1yyinVVWVgefu>T+/LH$Tif]T)YD]˩1)Ub:Ƕe7I FvF؈:(|x"JLYoգӅwgrA篅)a쀢޻DcU7zТesQ蜻͎Ѣ\3Y`܃y"b!y tZpd6LH O|ɭˑvd|D"T6-T5fl~/T EL YlK4( IDAT#=]J62սύӥx!LջDxzs^_.oϯa^_Iy _vj~©34onxeE (wqDlZBWkSLfFԤ{k\3OX̓jxD?L|v+JTTO6"V֒c>GsD$WBGe_`7890QBjn¦%$+{4_O-U^s@Q733hZ|02⫇,1gLYQ#,r\-ؼ4NcBTil=ս@t(ѳ8JӘ`̤4K)goU3VҒ"/ RsWl¥GBX]3`|\ *H}>╲ہnlff|IeR/xU봥{i̬r( u0ƶw;N'B>WNu(vXrTTw2MVk~KІUxEдI}C2njn~bo hؚÈsk$b4lbomn.Kb}égL]qS]u<']Re5_?\ks~-uP}{F$j?\sVuO+~GհAtrx"F}hvf|qri]'ohy&DD[_'%Fyqtni%feY^;oZ ?DM]ݢK[cER߼CK:L.7e}a읷kFnHo3!U1|ouܣc٘DP&흛VW>l׭zʡ}4fb/Fl\{<#š]׽kOvRN+ ,(1ZVN:W9;^Y~Ha҅nU>Ec-^dgbTmϔʵu8gtڌ]8Myr5Tl-mTcGfI (+# 9"ek[Ҩ Fϛ9Crdu=3evuCè4Ěɝgu:Y3:Fx.KQ89[ o'D$0бYuđ(Y589w* (¦M)b*!'Ks**"?+3sPA篅)X)dR9Qrj~LJػαme 1z\L(":Cl~0ף1g'`JNi_Q$3!!TP妙wb&v37S;|2zlVNI, tjYr(kA,)_?8xaE&"iяǽOfS+'n>m, 5iLE)gkТesQ蜻Kgzh*<SajաH rvҽ(5)ɭnD?0yfkBKZ3zOc"&P_@,_rrĩ]:-qeギ^ x9ҮS#>V޳ؼ4N᣻slӡ  a>Wff lپ]:56ҔMw`@\7㋯ϵ1g`v9gE<&wTZrRSkAjRw+xM}μ*D_9x*t¥wsnƩc_d6}3~% ~蔱.ml:Yҗe""6_j):A'/^@kO4vkV3dΠp2|؅튭'^]H/㻎REU*kqR#i<҂nU6b}~S&oQ߫37hN1bNVY^+}ݡ4{cXMia:- OɉYʦUS"f ɲX6WxsO{MVqB+d\Y5)}uTxiQfX{ 8|uGXӡgFbWEaw`K,x6}N uVC'"I!\!_54lziB3Wbi+f+n䷭gs߱d']U􌌚oCMrw׸E~7vLWC|ʫعnnmLy҉A)߸$jVP, 'B_,d&\۹vgp[Wt0=lʱK (DėA^ e9~{Ptjw ((((=Fm A_        D(+"P1]C<(((((]<FmɖiNb""_?Ex˜v0]yo7/x]|?ū t\-DꢌصxHF3VlUEU(_ʸJb*KyTPN0#h!yAQfFzE 4p?_{K]$%ߞ,1ZN~1K Dt -at\.HY. 274?t$HEc-^dgHѢm!"C~?zeޱgJ)WXP*cz8u,swSwlc,H戄m5J3X"RZt϶IRm -%]6'btwv."u]G::id"3G s=s&R&Eȉ#SiCw=wް9Qrj~LJػαme|Y8vRgã'x46"AA踎0 >2D[ftfӃ_ Sb4!h\Tp): zh*TeFXT--#5 tZpd6LH O|ɭˑvd|D"T6-T5fl~/T EL Y C31&QY)j5oS+.fgdei& ʊ@@xWi )^Lw2Y"9:)S23FvvlzjfRIUM!A"V֒c>GsD$WBGe_`7890QB-LVi.ɿ[ {/zȒs+\d5"bjJ dsX.#F,ƜWff YViLnelfRҥpu+C1x?~#O &|[He:_9xja j M+cUt *Bq3 ޫWL;dBm*M ;~h郤T4y,7M7.>fI ] ʨlܠ9ak9YeyqbXW,M?Zv{ʕ^WGe'1@@x׍x( ?k'Kv[;k3m89yb4{<{X""- Z/Yּϸ C:4&oBgyڛ₢[tikH wh _gIe毬/,vMIFnHo3!U1|ouܣc6bw/wnZ^MЄ\\bUnݞb/1fw:u]"eE꺔5 ث>[}2^,ZfhyqQ1oPHX)-kjSqjq#wN )O&ײ|Oj;xLh,µ|eDpccAR6G$lmQyS:c}HLgf̮n~LNٙy\?:cCoDgTu+vKDUGZ<80T6c;vDweSJx G#x"B& '!A篅=q 1z\L(":Cl~0ף1g'`JNi_q5K2B% UnYq(fb7s3U#+#Gfudᛔ̒IU!B+_Ʃβ،P5bU5IdO?Ω C&d/1 GhѲRt׳Q^=Z4)05TZ$9;^V7o; w.l -mZjh=^\C}|ɭˑvd|D"TfvpǕnt z%,ig'x"b!y beyh`H],$'k҆_{uUUSPE TA =/9;7Q{R"7MN>ͱXǔe"꘳3{u͐t׹s\I 5>USʸwxI0= Zq!j=C۶e՝̻CVVa\svi׮ЙsFXZqIi"{gA%.Z8&5z׫ȽǪTnBD|񕃧2M-\:yp76m:A~~In'83WBϐHNڮHQ^.}e""6_j):A'/ 7f4vkV3d0n&ყ.lWݽ1خ%%&:R&Ӿًϴ"iPJu8ZZ3lZE<_/rn,{N^Ug +Wx?tJ_^Zs<֟^r_7\qoϨŚ?0"?- K#,Rx6}N xODCFB~k^fzB3Wbi+!u="[/m1w\w,Y{6k(5?l߶푇>2~7vLWwCfºuɳ?лs2K'|3f\LD8KCYCgwՀ/ 2 \3-_Qq8}(e1Gc^*cvx܃S|8((((Fm/w4                         (xgC<(((((n&Q h-ͼ=(c?Rm9h7k5 ?Vn-[cܮ7"//s+]u{& nQIDAT h$|6ewܮߥyٕ'vS@P127V4Q2|˯(^U:m[ 5&҆ ʈ]`;Nn4s`[UD\UPQr L)2?ʒj 2 @\iaFz!B̌{!"9q Cu"+*`B Y"FmɖiNb""_?嚼& TKʋ.jw9&.t\.HY. 274?t$H2D[ftݝ<=0e ,U=ەEޛ5m) Eob{GS96/x[ Lp O?)޽FUMĀZ,奀Z]ĂRQZZBQ^E۫+늏TEAQR!$ĄdfW5(| 0;3g2 ֬3z>lx?X-X:޽ھ0]3VT( Knmw(r2 !oA am0ʧ(bGGGn-  }$I,Ttg(۔7gSޜm/ic.eC8kQhԸсh׹CͳےBFNIMѫ QŲ w[ݶ5[O$ o+ K^P}\r3?wZg7uAOkkbbG?+\1~~'gT4۞ޭe`D,:MnlP+ 'Bmwkߔ_?[8ꢪ‚ioW^YVi罱9vzy+ J NČ-{=wweoJͺsj\=}ZD=/<3s*,Kd6mӼ|ܷJekg&qV!7G0ܞGI[Q^X Xh_pIcO[SRݸcX8R5x [u7bh-s~dOB({'om&;{ծdv汐sRM~.ň믪ybQq}FhmoO>)|Rߚ/M+Bfzn8evP]^Rw>f?&LX3EyE-sݙ=n䐫n,Ab ˷pGrO/hrK?|tzn1ffFW?f^fvsQC>apnfl_Wg,+*`wv?aXZ{Šzn0OU~WSK/iUfі&oŽ^{ӅɊ ݟg`5sGpȆ}廋,9p_bÿ{\tBioUul_1K2s1c|v*Ͱ~gŭ7>m.*}ӧ8z߸ucY]fK]N@ r;/1uҽ.(pW?tY%@}I۞/#'w镃:@(#3{gZ;wo|mÒٲәg|©I.׭E=qqz_ o~c@-o8qmO{tR}+z:Z̖m[59͗NǂwpHVUXŞ}ޤk !D{5u`@){΄/yRӬޭx_TXvI~MLz4?B ϼ枟w?1B,ˠKFz6'D7.}gĚt0svhۢ Y }{<=^2X|qSǓr$~z0X)fXH9=KzoǒU_ ?*xTҵ3*-K2SNҤt媢D!Ә|Ǧ%nCƍS_kڭߠ|-{*YM֒d!w=q?m/ZլM}aH5V.}e-ݛn cz]}u=̟/޳AI5XWN٥<~2t?̶]{t3e{CSk~[!X;ʵrabSټ'_{gg !D*$띿|c{uvkLԟwNZ(o^s}@g(u咭԰wlx?<:!)gXOsvbu}4y&(:F!;wF(ҿLֱ0V Q>H_uN, >Kx5n gܡYmd!#&U,p-}m[n.4Q~cN_iXÝElSޜMysf𦩗Y[SRϰAd[zjM<xkߝ֧_Г()f+\1~~'gtfFeeQImrcw 6QXGg quXIUGIvecrO0ki+^T Pਈ*v0n6fQ"Q_?A/k_,}/׬;7ȫƅӗ}\G{ѓS<3²Df6˗}$]vîC/QbeAIy;Qeo]ka!wVmM9bdDzX+>;s9W_y '~uUQiMzfBKo%ja{/^8e')nܱE,]*Q;ykk6a٫v%;6D UXG}P:ʉ=^4 6} ]8`T]yeMZmDcwv͕C[> ~:] PHuvW 0y`yU$̙vgCɪ7,v!ɭ/?}.-}v=p~QϮRc ᐖ BuyIᪿº9OV?sw2]p;%]èd\:dOW-ʣ3\ȯ^:$=^x͢-UuNd=w` 7lWx͒*X_>(ue;;wl0,ryFaőw(xOO2~'ٙy +n_G]Nƀc_asQ/>opxϠ8Y7nݾmXV~}ْgx>@?Vɝ̘:c^W8h}ޱ|& P P P P5ci̙qǕP/@@@@@@@@ P P P P P P P P @ @ @ @ @(@(@(@(((((@@@@@@@@ P P P P P P P P @ @ @ @ @(@(@(@(((((@@@@@@@@@@@@ P P P P P P P P @ @ @>25: pu?ڥˇ_gX8묳B6:|㬳>VEEu/wUa9IENDB`brick-1.9/docs/programs-screenshots/brick-list-demo.png0000644000000000000000000004313307346545000021465 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxg|gB B JD@ T"x,X" ""M" Bzu72s_4"yϔU>ONZm RdV~"ZFq4:z[Ǐp}aAj횦Q/p(^ ܡb;s_&5ivn6xҹc]^~?;~%+TfϿ m}ڠ[j2SPKLz/F-@-QRRX)]*@ Uɶ)q%2B@,<hmaS6dR-JD4oNAEkh5VB|aXa5K;гGcIccH/*_7AW~A٨S=ZO1kW[IO5MCyh?!"\U'*hãsVؽOXƥm;WrtٻWK3DzxH_]+s$<ȩs2w?7O-/i=>5j{WI_/{MymOnnB@ANzԌ %6]ytUtRbvق鮱^ҡGjIqU5hcgldmZg<8.k~,dQ .{xk \jwvlm%~^Sɒ^ҡGjI/ٹ{wwlb{E2y,~CnLoM"#|m_ (G7xX=]廳WŖ}Z5/ɎaʣnA.Iڶugų?>w^¤f<bl8ꃯ&v0|ܓsW˕#|l!| ܴLxoZ]G_su;?}zN#x{ot,HHU "eOw__o?^iD_~nX밺^olwPpoɉ޾oҥ:3"'t܀Toxbu&N3p|F^QR"~M3`^Dpzxz7JI^\l>oww=nCV."81t wd9oWsYw-1-E-)-v rW q5<<;ٮ'}}ƿGzOė8jR)INɷ-53fOKQM9Y:=ue;_OaFKnJzWhQ/7.=,M.-\5KANۊf7=2-ki4ŵsϾL% VM=r&5e7->dsҏ1 >vOsg-{ۣܧ[}jC]oϼ!k3C=z!NVGomy=jS#>iH]#n㗌ܿ-ohɽ 0r"ŧ(Zgu,U߸oom3k:NݚDtAK[=u-zz{kz]PI7T4:Vt(%ʸUtEuO_Ow3~V:)Ui(TO|zأ׬9^%n"֔Dqn(SZ˝mh&YQsZ,ڬME=h׾=%{ӗXDn|}?wܽ{]t|[6[ĹW|ڣ:UL& ]yRvnIfzm;wu b{لZ1vQt..ΊyH*:7/OieTX}Uӹ7o֔ѡC)i%AqU|'vlڨsѨhVuz*zZn:|֯x1cbh?^s>4'QDDtUi"b0WU+\ݦS_+=ij0lVMDu"bZEDNbԼjk"yXy+ڡ%U'ZHNJ>LY68fإ۞oQlV #nki5jƜm1'l[{?d$E'sֽjlo뇣X״5@WE_`V-_b-{u7w~aU砰FnJLb0r2u!z>_˭jގ|ST*uJRk%ggҪ.2آVִ{u:w(%QI_5AM1E_.uI(gfRm (('Mx[Cu||sFX.iu`e_I'E{/Q,PrՄ_́}gy/-~lOgc۴V_]4(RkTWmԬ!!^\Bӭgg[zWѡXQ.TWƐa]1z/ggsM;WC/yRҚ:vق_sUV3v\ӗ[7b6Ȳl.?de۸;MݯQ֬O:ASokwsgc',8Q}Mˍ˱y)ko}u,.0NmR_ۿI,f 1FFMÃ|:?r55י/}%:Po1.5v§M,2x{v8'"Z_o}㕻rN^%Z(Vq}@b0%^OyDT㒬 #(xۻoA+y]ՈZR:%Y2ktȚ\}M҉p *oүoA3o[1}5PR:/ҌY6elX?&sҌ5L'!p4pTGں{K-ʎB,CWPQ'AFM/b՝W.9C<R*Q%bOr$YPP(@@)DIۼ k]CGBsՐ KyOjio}&%x7] VqlW={Eh׿ﹱ ١ Gs-S=ɂ,Q+Yu޹ǰ⃘7Cf|v|kO=]1G7̚\OHnߖaJa'肼^]HBbs}ː=mx I#b2;uZݱܫxPogN:9T&g.PqwE?zۻǺh.ZaНuZ~gSj=fl5}QNvQIŝmVثPT1yW 9eaD\v⬶Թ%X:TT3-q7;]lG++z)?v<fM #|6{eƃGV%K`SCBʩ)Vӻw&Ao>cOWݝq 颜j㓟#6sG ~UXn=d97BړҔi/񿐿[m8S5偄CXg?WnZEq=8F7ϊ-zΝ)]:aߴv[ 'lJvWlcf4#f*"YNM蠞6áe^28a!P*HgVt=8kF]ک;k7,ks<9zvn ~--}ϗ3뇭8Эt=yKҒL)zi>=CD45rKƝl45pK,"[{4R6ռo ~1D7׷?ϻ÷q(,''/{ށY[/'OFɰz9Ś8+'x ?&J9kP!w'M3ɇGۧj__x6ru=nJ6>#;VT`hD]DkFf\] 68a̜Fͩ=Z'r$cNfǜ̎_{;V@m뼂Vm>gO ϯ+ WnM8{}שSqQ;Ϻ nML7cع-՚&z/Qwq9iMINEt\뷀Q(J,ގQD*9_$ɫze)9;]݆9ߕr""rw?6[Y_#(@]X07UvhWDq_4%E- tZ^W r5KÎ|cQV +96E^'< }s*g-""jJÃwSE}=XEqTt&Uj+mri bh|}VJ[D]ΎUR)bJ<7Zl/GGZ%8}4kJ&"ˍmtON,lnh=V8NOf^2Jc4*o/tkpBܨzw>%cE⊧;d߻V݇z٥i9Q} `QD4ѭ6oؘב^5 xt9hf,zoE^nGfJv\/>z|_%۹vl\ϽWK64v+PͰd(ªS[qq@u[x89wsQc0涥{v3nOQ jxWF-7%\GO &7ͽopdt[؁mpmˈ))/'\G#(D:p7ю9n+Fzf]gdk//K+qFFF3[R\L@P(@@ P(@@ P@@ P@@ P(W%GȻuSD]G{~F~&N xkI[~^ZO_;xeBt\wȤ"7G/n=7"HeɌ,_{îo>SG%-SPc?/ziG;M4RmK{f?&'(̚NJL(<5^oDdhf>>(M5w۰`-'fת;VㇴtyQs?\'q}cjzG>9F-gO9Pe80Zu$ɋ߻leQ '׀7XUgO9V4N?rz/!1s5iرp:Pp YZ5oG0(@@pYX' bk@RvtlrI@-+fFStw>p{ٳo[Q(1 +7"dz|"mڐu;*UW]9@@pB(]'y88-*vmoMDD[5xAD4{Ծe crlsvwwqg^=]/;ػI=ZfAӈӿ>hp0z9(嗢Ysd-/g[{&]Ϲsi%R |;:X@@6:F6)-U-4/"w4EDcݧqC}~b:=ҨG8T?*ACG&}gfAkp*fH_P jpKЩap[_ -)s .b;^ x=%ӓJ;";9 k COǸf_ڲw{ rF9R!"!8ȹ=K'q]tԒfƎUX9%y)>AnX.(FۃΙMqŗ3ľ|Q6gs<Ƶm}wQʔ޿N P\X`u䣫O=j5WPeDQDӴRؙx-Gx;8*ւbSmQ_l U|=nhٛNtԽIvnЉ  88~?3>zC:?d8xmzQ A-j0Vӑ=mEZ??UKI9-/A6w((UC3j*)s} շC\Zç=RU@1dh^SUü@ 򠿳NDT[N(PF̵y6_/;E:(j{%P3"ZNvIv\c]ۮM5W՚lsv~Mz-/ȧEԵNuBRNN&ѱUEqj檬MOͿ^ҴjZvo7=Gu (PS7j[(jir,Ss06lܽ.IϚV}\xNuA%oZuSF5 ˏNm^otpT=OS:y2bjPU k49(6VoiP烧/dŔ׸^w:_( TC!فn[)"b/EDtNlnޒ,]{gc-'>8hչ95^8Vߖǥ4 yf]qQ5;$g*:5n_9 -'>aת (G-)tD+̷d(.CZxĦHj!އEDVd?R4CQJj&ƃ[D$xx;ϥ9Z5s]Bz~Mw!/&"&Z~fz<ɪoTG'9vͲՙg$=AM_GQDDeWsq,X9ˊ4[* b/t-mI 0CQ}CJ\Kᩏ58ѢR31,VVة?c T0:@Kvج_yy p ~FmG7^Fmb)\&ۣC5fH1z|K쬂}hepUI9#:\k>MJ+Չ\\wŌUU@ȌJ}7*'/嬇mI?*H\] l;p-@@  P(Γ-2os r}PO@oQH|} CTrݦMs %MSÂ'wU/N]\1c.ؒT,d$s}twA""Rp nšB(mrk׊I[T( }z_/5ϴ'A wZc�vH?{,D3W`GOVlߕ8{wZ\@@=i|vU""]DDר[6-Uw?r⃃VScoꥩChemy\JoUa[CR?ӫIAj\STsUn. $-?n0ߒ] !E-_n /oj6Q׹F/y=[Pw;5q⯲=n v}kjQFb(E%Fc-wGmI%t Po|5eDϞsxU>#vmVA+=ko޴]#{ rbvZ0cz=ڹq^\׿kfR ucUwZ`‹ַxht"2tCEDu3bMDm{gv =kX|V1Nylۗ_!31<Щ$DA'gbRM7}QDĺ̩mQUweUwA+a6٥AіxpʰW^ [(:EQ"nI[?\%ciSY7,81辻'7Ozk}&&#9yv\m)'?Bt@vÖk}6ŧZxUD{NmߺEJ^#*Te/t>͂MLqzfrq?tF+m?Ki@,."`q炿?xIq/ =nQUue.l/UcmŠjώaoj_WbvȰNk$- ;{w 7mDD!=;*K{xIaGUV7rruقO%G'Yvޟmyᑩݹu˦%OܫTkC"(L^KD_gj2:zr2FlTW.c:Fqs~9Qt0*"t"+j|Rs*nyW~H(h&:EQlrMܥ߽{ 6.{![ԅnؗUV$奥+K\ +vW2[L9Y&[Mi*G~M(ەjL5yh׭Gß2pVJ ~&]־{u и["Vch8d{u%/xpM*ҹ]^e(Je+P*|!uj5oi߆#lIu6}vXSM^BhZ[u六zMUEW~j\mk~=>*+[k Mġm8Qh34 1XS J$b҅36'c۶(%j==6e#yzƸ9[u.ْ9bx3VwJRtkac.Q\S5ܺl&>HŹ"&]X*檺u~pRҵyӑ2)1[׻s$f?Vg-;]t1k'ثۢ*ʋ)TkxBUҮ_&Cx@{+km9jd騙n7C.5Yucͨ؛VzbʻgdUo+N:%tIBTd-kӊZbuB"_'o䊯WϣZٲ3T% zhM֬us6H(궨b U^X/Pj)&CJUهrǸ^9y74v QBbbs9kҘcKxprlѭGmv i>=yx;MZC<*Ppo߮>9wڰں*čڀKAqkc;fM9kݒWMW|<3ZΚןs>^wʄ+xݐ_'c{m\C_w p^7{,Vwj㴗ղzOT$_ZƏ&ڒbKH p}Lq"1Q'$o<>^5ψ][pqJrڛ'"7mÂ=]X*Ρ7s߭9ɛ~۽j&Uǔp/W@o#Զq/G8/Z-@? P-ǿgo!٧]/-7v.ߟU}NPOx>ߕigw9u!G)>("b߇R5@@QvU+^來?QJnp UD1|ԴYmtݓǛ'>S}ϏYSu.UL0U,P6էW-ꇤgszF5?bZ:cFTZhqI[}(Qk䝓y̜M5?NTykDKw}xL32i">'^zʅIDATk\GݶCutJ]'ϒϿ~xvcoS&"' aUYM <Hޖ*hh:.S@ ^ƒ\^hlIIeUP d(Xgݗ^W;WY(͸1u :I[ҨtRLJc PkqEEKꁋ፜\}Sz磓,eOܺe'?«To_YpX(Rp_5rK#C J=)e٨~] %"_>^G?hx+"@@PvW2[L9Y٦ˠ(G~M(lvkƏ'YѮ[=??dԉ}oI)ʈ=]H* %":fL&{=x!^cj+\:Y Ii5K'"oxCFR䴼s9q;XKOO^o Tj2T@-+::GפK' UX-VE9ɱ6A'JnoN_D}fq_~ƵkZJ,*5ZәȳyQwo]R+CO KߩQ;o?7(zkeGJSҝ;rK+/ͪ8yoڮo^tIDZp+04SvY[R太VѥqAm'vƚ=Or7X Ͷ*f%t۠]E38{vp]mb/*ml c WPz(ݮEEb1ځvc A=H=Xuh 6]Y"ոj)UDR'$Oryy鮿{AdP2<\H<jӊ7<`y5Z'gڨP:~wcNk7<˼iW[pF~fe FzG~m{$ɫ!;b/Mgnd}ށm[WEהfR:Z ̪X__9wVA0큋Lom׻O[n[χK;v^穦{fr0[5}Wz׬GNaR`"$AsSKShv`(@(@(@(@(((((@@@@@@@@L¢/g `x*/awID }i:EHdl*'5nS@gX%OIENDB`brick-1.9/docs/programs-screenshots/brick-list-vi-demo.png0000644000000000000000000004402007346545000022075 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxg|lI=&(ElAEQ< T,Gc}QQ(HGNd"@B"씻̵U|NY܌#I)' v\}FDm}|:^ k{PXgZviEtF/o}uj`ZO߽sf:טivn6{Sκ̿wF:=_~9++K86jO/oɔOudxy6%%:5+P 1P rЂd۔K !+K6wVeO|Ͱ)[ R"7Bekh5VB|aXaimpsN$^/9!=}ʫxi:߈>7eL~Mg{tor2ŬhPiVQ4@jX+"+rꤓ=Eju괖rʚ]{wߟ>nhkO_yէw7ܟЉٻWK3DytH_]+s$< Zr@+;e; M(K~=}tjƦ?T>r]`.ͼ]wm\mro\ltW|7\+\$ &| @9=BY} Uqlڹw/aa՞K]}"˻#U/VuZ Wh>γ{ ޡ]YN˔~l^| =G2;F4UD}f$(S79d&"oW v)N޿g-S#j\M_ˏsÃ\{&g;|mx -1sbl8꣯_a>Ÿ&G-V t 1sb|4-jigtx|Ty┹o\FՄ{]=7WDcABb)[޿G3}A?tEIчaxӎnZ)IAý%'zg.EOP|pߒe' 琏~ȧk?ew̠7[o"Fpu[UeRKuw?䎦Zb0Z("ƞw9=:x[/ʿ4ܷatQu~ Ge2޷XM9¬r8O1-E-)-v rW qf'ΚӞboVǰ;^Xw[^BM\JuINrJUlrwyGW;h~;~mZh+>c[ՔeSSW[tig3ZrSKBzw=/U䩞m?V4)qXcO[)b}f*,hj9[.̗ƕ>pZ᪶w5KD",Ώ(ViX9 AZws J-'޷֯9v{ֶ|M2hx7׳{лG1ǟ[|ZwGէ->t{ߝyw#C'ܘ^ā5^ogO\ڣf]Ç=,R޺EڭNܭb;J֭͞:{[,e;AcF62Zo!CH9hwLYV[صADpc#:hikgQn\Z~%擊FQpUZWۅȱn^Rt:?:祓RU^B@}ħGΊ=zړe_V*bMټM熍BSZnTЬMhYY.}yJ/8斿ݽw{6LP٣ms)o uLCƦ5{5CX:l,cS+9ICfڸ'OZʮ24iQQ[_cݿMUQ uϻ4P+8z(.YQ3KUE14mAQs-_jK[5{h2T\4 h+ u:S<.WNuQK1jU]J׋v7Ei6 ^Ͼw5i}f# '[sG$j?Wz-Mn㮌b*1j¥J"POݗfD^'"VUDDD*귂횈pQ'V\4h R8{I)ף+˦Pj9}-cy;3U]:Ț1g(-G ni5Ѫ(SweEnn#9|d'7GZ7욈G`"/7xz'|-{v7w~aU砰FnJLb0r2E7ybŴep,QAm=XwJ}m`kc.*RWIJzmYn}AWg4Q \L+سTiOU^=JmQ+*ŭ>PpZFrrl..rNJG97ؔ*{O@Iӻ&>zGo{kmۡ]3Y箏V<? Dt rpCv?.=JVX[;}&Z* ˿g<%3 ?5Egr{7flݼDueOgoZP98Aѩ ;7.Wcwq(ȷ/u-@٢~a{QEI?e4gٵdh95v+ڝ<KOM\Ypׇl`JܻHEB= %;w^V\4 h ` \,^6k5܇eA&ӫk]Ǿf>4 }>\#|mOYR(Ԫbdu!jQ+8Sf.YN:AMM 3hk6gWw}>\/_l@StVMNS<Ji਎uPeWO'Z]Y*!Q'AFM/b]T.9S<J*Q%r/r"YPP(@@ڔ" RmB ثZ(nLs +w|k[T4]}w~hhz:Ͼ<0J%oVj!sLYn|=Nv 7ɯo2vw0=+dSoZץ6d{C-ݱ ꖫ҇zOSɵ[u)\[.' ÷9oцR-ʼnڕڌ*hhQm cm_~he>SEģgkdv2YMg">sV)y Z.|ݿx¶h_T1X\{aԼ6%8nX`ϠCG>MqKܐXۗ["ZTmm{.KAn=C,_)yOi& D`9y~ÊbގS2&\w튱M?Ugq7 <2cI^Cֻ37]ZKPl:oNA1y~DLfQB9[v[C[rw_8j|Cgf}vNLqxrMǬ]oṜuZ~'Sj5=y'S;2nSDDW?ZF%u0w2Yn7~}Qa=o6]Gew,jsAۂKm^IEeNha p~WDrlejԶz)╴?v71cY%GOAـFn2MDr 7ԧK^Mg%}"r{.\^V7uNQ;5ﱑcn>ۙSL޿P6[u[e>`j^uJ{ڶ5-ac |ǒ6\g-?w ~!x9x`}wx9jMo_Hң+C'>rEC::]tm_7JJD+VHm=8Fs7AD::f,7]¶)X>A]۲'Wf _ M8{}ױcqQǓ;ϻnML7cp-՚&z/Qsq9lQ[m۟+T KD+ߠLmWԉ^SG/(kKz;DDz_⧬v37v3wΩ*:h8Tt*Pm]\Ϧ W;طF5luq|%noI=ʠoQܽMX2/Fx/4-'U;']{saEtE+)YJsjO ,bQ)"hVժteʕmM(FcYDrmejkhӐsG[^nui߽bu]jc^&"dѬ)(.KK?e/;}Cs4(Ɓw{7ڔTZQy`6}p9> GdHZ|鎭=z-IMQkf,zo}X9|z!wV>7{5Lv~AKq ܤh^y{FʎwQ-]lmgX2fabSά%kp+SۗYg"Pݖ.9'2NZ?)W[0^mU;5}nƘ̝)"a; H5B`cy]DDM>l;Oʔ.zV3W.Ga>A:1џ(oX]sVsdz__*(/U{zrVzi:#3>mVbG> _/qWD:2e~։8Mru^iej:\K UPP@Vvr䈈X,Ǐ;88t|'9Soddٿ ((**:%PsP(@@  (@@  P(@@  P(@@  P(PQ?:_y{ohH8jgk* P)M>=ʊTz?ikq!cքo_<7"cG% -SPc_,~iG;M4RQ'&D?(-{AzBȬiĄ3~uNDq~_޴#{ rb^0!--w)iԹu5z܏˭tB_{[%g:]Dm3d}nj[fo|?!"bWVUZ>ְn/o;^5$D-1i]/=wVƫ"(.">j~M ,6{D z)ِ͓ moc:۴콷{;~Y(=nɴgk>DS'o_D~ϑwO1o/%>YsoַOK})~ZSQFUߣ"&EEs2vK<}:NYTh^xw'(nɋ_d9x$Xgݖ]UU/>"G~x{I뿪 .o} v(Wn6@@PX-VE95{|TgxCƎSo7v˱^=>*ֳyDd]D-#mqщWhMSE95w(OHz&KnJZ{zlٳtuqkvs-Y5`S]ow -ܳf_e =<@wWRSR瞴.ޖ1bxu3\V(^mȎN+QoƆ'>(1g={jޅ_y_' љ3sg|V|_5౷y:j줣+ }Aߺ^DDuͧ4,xsV1n_6k#˨ '׀ƽ 窳+%@m R5~gK;gyƌ['q@C@5נ:tlU" P\V(]Xls"YJ3uyh:n8YsW䷻-:?ȨSQʵ u2f>m:Ox!Ng&E@D.""| "=@ڲC196չMwsqg^=]n=YwwQ ẑf.g}`rP|/Eu0~Z)^nի7!]?J8xwp3ht΃G65-Յ-4/"w4EDb?~s}~bO[:Mli r !! #٘پAPj\-{3'gn*Bw :" >c+3bp_?tEl'Ғ>}+ |lRatk.Vp+U(UjG>Vb\@@jմbѩM]:SD^bu }u[tգ돵U۲fij|㐧Z_$^~{R YywI?ީԸ)r$]8[N}sʮU5PG-)tD+̷d(.CMbCPݽd狈fKI54ͭ)ɦfF**M͇/\ٟobo7y.Ѫ (%Ԩw~g^)JKi"hUG'Nutc)PC,[Vů<'9Oh&WEM P/jc2qv0]VR^@@j1CQ}CJ\KÿWhѷB)J6DEy+б$̍1X}(jmҪd06k?ײas^-b!GsD@@-NAnNDq )r-SռD}GJQPP!X-[煎'9fn).PuCW#!^бaNk%TƠJ(|#ByP{/6.]a~s0e{D=-vVA־28hjnvꤜCrU8&e>F,1GSwEx(@m'fJkQGe)H9e[tOʿ]]hɿjSբ"QrK.͟L%Ŗ-@@jʈ-:%?{s(}F "V{7^޴#{ rb^08h/.I͛7W1JO:.wR`wַxdt"2tCED3MDmM }~~*ciur1 R#w.`S\q11=գySIƩtN2Vɤ| ox ㇴt}Q s?^/W.]&oBئ=QZzw?Nse}(_6F>_sVԣ*owIi[Ѻ6ٹAіxxOɰWE/PP٭VU(^m?aJnp UD1|ԴYm}Sƛ'!S}Ϗ˼Y{Su.ӇJ&]8P.]JtƤuL냳Yh1z ?_9)1oOK3=iѮIK,(t7i{o'w#w.^a͇>\ҹMvPே+ N:ze?*UYSV6Ҫu9i&-f>q|3~OU+-|\Wѣn!:8ݗ\#$~[/`ےȩxCg{tٸ:K{xIaǢU<\v8xeЮrwsZeQZYҒ m0#9)n-yߓ"$>s*?SZgtCn5mD̓naW4{IUb+_Dܤ}Qnɑ\'IN:T\䢃;б1kaL{+*Ը)+$9JzTMYhͥl%Vu;3rC.->yRu>_׭u?oxEQĒ}b,:i<4o>OG JNKm{񱩳߽}ۖ[$O+T{!zQE,)"jo3ęQ΁99Q~G_f<.}ơ?^=Q=կϩγ{ժb/vtT2.A=] |5W<=.EW~H(h&:EQ*-r-{Թ߭[ W^JԎ}uOZ^ZzRM/^µPpڼ=f)'+d7MEQ7~2뷄]f+̴X7¾7uޭ燌:,>ZU>f bhqkh?MV^v04?{׎j_須fɊS_!Wl}с}IE:֣׫l-TNȗ^gVlڙ'oQ'm5Ug]BU٣.)/m/z+TUt;ĥ@@8+1!bl=C2vE:u++R*']ka^JBTUHHH8/B»vڵJ+J9y4kj*N..yEl?d}p[\tb.C&a +W*" uNlSgP^pRUE2 `IE.9vcҭ{wMtyrh{5FQjI}Rb*QjKKJUKlާkݡkZ@@Aml[j._/>[g; MġX(})WV2v蓱cG Ξooor IZq|l>#nޝY3^UpS^NE]p /z/*(WU/}!O=68Pz8Ş 3G;xm9;cD8n} w30#/}%OlRu\n7OجkGr[ޅ_y_' љ3sg|f>Y{Ǹ٬˪GԔ?f]h7Z\TsjZ߿N#{?ܝz[Qvұm)XL"W^VDEֲ:mߖ..s]+YQ>OkɊ^7gĢ3y(7;nfEuWBTգ.ܔSQK+eTUem)G']IUهZrh'So~O)2\C ̝3IcƎ-c=RwSdy(擈ݓwSd8)NP(Gv759G<7sP_S6JP:?ƘnNsSNY'jc~,ϼͧ]|]Mο )u\=!>-@@"_k׾g1詡^]Vn?m3^^ۏZO&ڒbKwLq"1Q$go<>^5ψ]ZpqJr۫UQIDATwdaZN̞ ~Gd^?pGAF{qANonIU1e&&$㗌+]UCnm۸Z~|io~uNDq~_޷͐woO/?UuNϾ5}'Oų㸙w=uG?{QDĺ㿏|[RvU+^m?aJnp UD1|ԴYm}Sƛ'!S}Ϗ˼Y{Su.ӃL4,P:g;W/gszF?bZ:cҺF_z﬌WE4{Qi q=uro/LTyi3_'w_73Wf+蘮+f,Lv8;.t6 0uHDza96"DxƆzڿK,ТB2*;>-U')Pjm\jyTSҠVآnkCG7[~gީ(7"s\mùv*4r髒,Jy(inoc&oOM2УGWg:S"y􂰢}  H#CӴ\. y"ѩݜI+X  +PSEEEVڦ&GSAi bѪ;Ai"**:r }  HA,  H*P  45AAS6mƢXF2  V2iP&;Ȉ >L3WB\`gEAxfHY ) G X5062B6ce}\* H_k<9|ǵvΫ!BC8 1 '"O4L8104HddxA@g-JPFɀ>mh,,o.&5f׹Gn߿{L[F.7ǭgl9;Y8~M1b:YiNdMC4ArKF`<>>IܻZe⤉SzZ]{iæ&޳0&ҷ&+-9R1$,rvw%;"^5>J= y8tڤBP1{AVwҥ>b';!HͥdW%ϼ7GŖ@S"@g BŽIe߹%zaP33MQvĵ}?ﺞ* -͟ݫC qq~f2 _Ϟ=֭.Yub\w_{_1~Ma3ꇅ^[j1Dy.+l%OlTa `dGFi ;nM|(1FX,ionܱ%40z}?{tFRBEum6qp&jez!VQf>4LO/"yMIhL[ L?;Z}ё*Jby'pHyڝ1Rp 4L, KJUqw0o~VlgԠZ (.ipEf;^(qYHݔCIA5 .ɕg^Zp0o]3:wע%e%4~hcFFz,hL[ L1mOL0eWW}Sݾzsz`?= Q.Zo٬Sqw;h74-tɯm8 o[@U}×^/4bۨO4ˎ @ݚM\9|Z~dWmqK u5w[C@xiÒD>q~QnF_wo.)O?3ok$oAUBkQ-CSDiUb`h/4H]݊k'ԘVtpI%*`>k[^TK4XX|2Tެif{' *}p|WD.\e͹ ABBB~WnYlTRSS r) u f;G;6A| X/O34U]"hTe `])A^T%"VS*ɤ4A2HJI(Qr`1#eԱ8S -NwPC4<Ȧb՛\ :YT(3_Y$C49jF:;|f>ʻ,P7 ffznϡiop'5ܼQ\=‘ Tq|l&\,%!I6HwY`ik3UC%79b{OU_DsYBTdXޞn!׋=vbty|\D ,:t0$OIolG.-x$ڑ{Dl=s]qN>E罯ia(u5EȅIV$Pf&4r'4atgYFm٘Q7lQJ%~`尴R.WM*6WR hE{Fd \#] 5ٳW<[T(4য়i˅2X4L%104/n]Tb1+o~+xw?fp$_~,A~o˟o5T`b`b H@6״v{rYgT(8Ҝ:A6LZ~C*BK &&|j1OhȭRFM9(ALZBl04Ҥ3U$#fHY ^Ii؟ǪauAAJ6Pz3%ijrƙ"ʕ;X  P#Ei3M9RVu  4q}'$YAAX    ( `    X  |l'hAka:AA#( 4'k .-Y,F&&ZrDNjs̆6Xzn=͖E]`,DQOAD}bgr0ՙȰAbl ]nZ-4s,%r,Hs$VM-h9@Z(HtA! gQ|IB(F~X?"x?"բdSay<{;.,kuu/Ks.H؃v90-˝6uT'CJawk 2YBY݄|[LYq|WAyP7 siEE3`VuݿUcGih>[[UeëtVu[/wuFGgjO"?H -h9hX }nU<B#-\M"cN?'7oqWC/,]Xvf:eeR;u~toE?ɸPKWff}SMa -G䕞'Պx[I1-;ɉCO}ރ4~nUoܽV'Klo|섉| EPt5rejȠ[*}j +GJ,\+[P ulnrԜX{K-+{qzm* -K6ӵK0-߭:zzIنI/GLϺ#޴Z No(+jZ?*syƢb Q 1x:ՊmzjnUxȃnZk;J]Rˢg-yRm¦%1f;3nbwQO5t:TM]z%~ś;RrBh.|{<:NH([7ξ⧮ɿĿRFKI֖h 4uH6YyR@S Chx,i;J7 x91p&qmCz#涯kxb?,gY D p@x4ev@֙B|k}Ju>[X&~? Mٕ DkPmh+j`"́fJv)6LjEܱ~%< :@iafDaBPa[:ǯ(A]]²:+S-;FAw?zjc7kMvlYEuGIeOrH&jieOT=c\Kr+oT5J v=\Fu\i0tuhESIJ,FJ˾kz3nRU[' -_iE"AW8l];h3WcfWW+(ըm"́20 ghNSU'1P0_$j>]~:烑-ܺdҸ( w %5 ! R IkZPv=<#`gDzR@R+0 JHP#HZJIi +%+!OS G 3Y}*V{o i١o(yNJ"X&PO+j E&+t"́fcQ&JVtH+?Ņk R0f3d,}ZS@YƇu[O;qϭJ& M\YcFuVb": t~jښ Qޚ?Q<"L$S[rZm'Uj uв{Ik6'~V!lۦJ>-Ղz&P޺Ɔ.ڛ6i~%k0Dª? ]$qaBKAj̬2+j %&xG[E507K˂јZ+:Iysnڬ9csqYeDhw֛ڮoXlMx({N];uW"lV'l 0%1jl1ߖ3l}:J/,+{=$v}5(:WȞr]_x!vOY dӺȔ3%ѯ”}f4$nCx]橗\=n߭,!4&5w˝wE K7P+|Q d0~dEdnɱ׃B9Px!VQ:RS?AŜqvN鴶6jO߃*j6[9LscQcIXai`߂r}k92A{=WoG[*7', lմk/iilõƚh4!^Z2 M˱x*kN>{(}ǜEO޶t#zXbʼ0q z$mnntA|;oDmJ^}"𺵢τm-ig ײ=<bd-V6cP%9w+IG%- i8rڴ_2 M(b6?/̊c7 ,#s~]G%x4(>V2՛~Z~ޙ3/(s#❋exN--8+\Ÿ$jjOxe)Fp4I=L/pj?dYlv.2{M q#{p~ "GPpiQt{ʱ5bc99 _ Q[8Z6sssA ]  Hc78  H AA,  \W^X-[0 ,PAD"yf>QxA4$'%τ7AAO@ xb AA.X    *>\s)t  o(  z+AAX    ( `    X  `  (  X  `e?Npba  (u,'n+^@oZ3j*Rʹ}G[MB 25#.|i0f9/ߢ^GZ=6tٓ}KI7+=SԌ̍tLH7D'wɽݲi@OUm/lٙt0~T:p\$[r<~\A,PEyd3f?gx^@UWMJP)BP*SJ H7e"jh͚& Υj廊hɆ 4efZm4Mgefm -C{[hiqie}>$]Y6pŒܽ=ujkFxw|%UImŞo*Hlk v>˗ 7y)Q,8q25jd{'-9q[V괈W|ze*z91;[ҥ'_,4{.{{k~YCM[g6B =glɔ&""J IL=tӿ{Y|Kىn ;:b*(Kxyr-+/ʒ75"4èI_ϱw\3 ll sO3VH9[9uHE%GiiӚU0*Vrm[1 V%ω)Ӧv\hJAa=-ϤKKc/4Hݐw2"|QfujLyeD=hć<~tlaO`XٴQ{lla0#3;}멘D$#xZJxqi^g2퍭6BlI#$8zW40կJf߽8R^.=}mL4MQ@+`Mս%ʿs때2hYygE}J"TGn "4/>>( fPeN0Ɠ.JN.!m(TJ&Ѡ^M7-Kh"j!.:qVM^T<'36ѯՆTiU0st0%g֧޺eu+խU˜쌻'.Ǧ%L)ּ?ʔ<>ijU9?-/=LU%[עM}3vؔ@Ak˳e=]So, 5FP<#CSSSb ͏x)lҳ!iQ^mEV-bpu45зhѲBRW&SGg Y52`ś|Ay$ʳv-%OMr|(C+tJ2l=Na}S,B5T;h']pU<_yU#ļd>[p%)g %ő<_gfzL]OZ|{gZ,zvL6L\09uU'Pi/~<.'M|!9akq/=#cZ. ЫÀ+C:ʹkh'j($)"Su=۹l4d5Ka #mH cq4G2ޭ~gҤ+ Îm=zra[^o6v/ckQ? 1sWg^ 7uTɦ]KI*w7K=IaabOʇTNlDWɥCy3S|}}J:inK¶c%1t7 򅢨̬ˁ?84c̏8Bh6 4LZ/x/:aquf䖊HmN^"NA@aҡTQb͇C}Qc 4+ɾM' Ai:,G,GE|3 AA]  (  @!42-3s mtwQw9-qBW}`3Ӊk%ܬo"j1ѨjF6]:` YвjCF lt5jیsV?UE߶{2j;ښh6ΏaB&ܨ5R?&7]J :^qׁ#ǏodhZ2 ͠@ate? s1r<)O+ը-tٓ}K;ᄚ>K/FrK] 8.;m݆m M+{i$T`v穫(vrɖ}',\ݶl_x9| jJn"J)i&7啩A[T+ c܈A(Q_:Q>`IZ|/ W7o<󲂮 'ωcvҥKSO)( ,\ND]\$PwW%eF;ythArC&[,.59b?d G}>T~e%mW,9l'ƾi ξ $(mISk] Qs&=GBC7+"5Ld9UQj6V;tPoy€w iBa{wΑ#'rJr.Pbg.Vr ͺvk)-R!P!sk컎6_NhM3HG^84_n.p<ѹ}A<iwܤYDYd*6u2^sw"_dV5/P>N8^=wȺ$,t]ݺMuuDĕn'$,YJg1BfW~۝wCyL3G7v51YZtc׎2m>~Ԃy%kRu %Tr)^MVbۥL uv`L&P +vZnԛA&Sp_EERBK@xF"ә-~;ЛE4C qq| HtaiHJgZקEUd xo܁D>TrH ^VatMV])Ѳ쬬@*'L..w4ss+ƶwYրHȣU l@5n&3ywь>""JTEQNB Ey ڦ_tկ $\],9@dL&|먑.co*T|/ֵ+4iH(m:7 M/: ^2ZY0H&8{ђ+wuy(ٕeXٴQ{l+0D> F0ΖgRRť%º 14//_HXr$n;XjBHM(djҠY' *ܓJL?tg^ۭy2,۶b>ɭ#ω)ӦvC%߮1,ZLE%G^rCi*A)3ӼèI_ϱw\5ASм9S@ BET:tUs,;{qν\]\G{&˙2z%mG U5 /=-]L&_󼠽nVZy&( + 5xeMO% 9}Z`ը @uT@y;n>sӷhXQ[?M}sCi(DubA(6Yu:tYP̯ߴtbG t!x(_GWI6]$BKwkHo0`$KMxsi!7\=鱌6_gBSd6YoITj*u7խ`W[+C H%RB]u0-mwO\MIOKzR %Ngd(9un02u j%4hikHDܸzs&vTTiՆ 3G#YZr\!U>4ttjB ա&(v҇@ nÝM&B錠|OyCpPtգaжsm0`˜?1IZP.cZ39eBK6dȀZojf7 TC7cM9 !;俞[w OMr|(C+te<+9]=b1ڀIK`Ϊɲ˧B{ΙWd =cHuWN 2I*6wSsaɔr@ Rz6$%_1jlGYD>ޣ,7>.Ro [؟8W hkH3sCY(D5S^&+S&䁣gbXMW4M H)P. k2@YVv l59Y-ƤIa'7}5V@1Xfxi@R^AD/YgЈj8+r\ U(C*\ yg a؉?rǻ}ԓ&x4]l8Ʈ~=f/͢Dw&T+nh5h2]pU<_yU#ļdvVrHc̛k-LP(ՙ8n"uuRbrCDF@*?CkR#D4:u ȧ4\OԏN` EEEFff_3glAq4LZk&~j*V'E MF-jL%|87uE,PA>=أ 'E? R ~AA,PAADB;Ltot 4aB*96j8m;Wk'utԨP3ҹNscrǫyAieO-_ڮ<$Ԍ̍t,?|FUr<)wTs ȗK3YC 2@JŵX&T'-͠EG! 5(PHK͛(z.<4_kwW%eF;ythA)ؙ $}HsL.I ~՗{Z667dҸ7]˦ %20;ss7~?ٗWD E>dOPCFng$Ϳq/d2gNmu(/?򮟯a9qVtijx#_^!eT`2cnY&;j2ee O/9v/MJPU ~{5䢲7bs :/'"84k(X yCI2l}gְ\gf"ASP !QtM:;rGonk]ϣ^DSvCtv]F{8A|ҙ{zQ؃$#npgi]^OF퍄qR*r*ĵQ^ w_L3rU冢"3# ^o~F3У )%jJX >92ZV^U);v]4lz]x 4M;䏫YrȘLQ#]..UxDq(\x ύέMrea;["!PCWʳ$DcC  u//_DXr V-{ }'°iř]IPT/oХFM|4Z[ڴf=ʬ:tE~._PCՎI28sKs>y38J CƀJ8m[1 V DiӂqLY]L[hk7d !_FYK($GK%>_d quA&V FM|$Qv]'ATݝ[d iYya= FMK'[*p{tZ)O2 R*'3v4iټ}҅=X'9a9 5:J5a-SF\xƃ/h},?N+·_uoҹo 4"^|45f yzbѩC6YoIj$X&Ѡ2 fPeN0{]D,AK[f&s2i#ZA33kJF[CdU6dȀZojf7 j,,&$BRn? 7o&=ԥE驹䰁CZdJ %&oz~huY>øLNgb^)AzV %RJSӅ\g/82kJ$]YdO3ǥ->IzƐ|y+F--=(h{e^%4Cuʍ Y鲞^^ kj̇l$Qѩ2>SS#Lz/؊ |=۹l4d稜!=~3o¯2AqVܣD/YgЈj8+r\ @Myy¥, 0HRQHf*?C?!W:c5c~Ң>(BYoXiJe񔒔-k^4>)21EB Zʇ°^wf'),L FC[{OFQAAj"#3/Ќ3k8+, _レZJ[*";yx BwDݲ& |}h=CÛ6J BԾ(1ZLJXdߦGQAA@أ 'mHt}+Ԇ55ASxEA AA,P͝}&{:qp/A  mjh2,]ܻZ+u[yT%b7YtFS[MD A ǿ/jVjFZ:f%0tٓ}K;Jޔh9x^Ɵm"j  ѡӥwȲRşMd(AAAF" Xs☁t/FU>Z6R IDAT67dҸ7]˦sL.I ~՗e40Z[D&agU:W0;}ȑ9B9SSr (5U+K:Y7~P7 t{'wc9yEP^y⹵یX]]/S\ST}MH,AԽ ӆ Yz.^M.j 2&euH_Vr78Bӳh/O@R:zϼ>^]NpNI!A⨊ <]+J6u).fp<;.:.O@Bt̮Ao܄Gԇ yBo(s])Ѳ쬬ʿ&]5 SauUA, eV1U<':O%{FMu-ΜfnL"P" h /̈Qm-wd-rWǏ?|-Ej!ª%Yx/n(, ;+C-BlԽJJA@iz%kZV^n7CSEײ&ks@}K{M:3e4M`(]\K i V7Tv"_6$YJ m3E XSe}L0st0%gU"kӊ]$I7 e/9I( io_i!7Bn2|V_ʥi>Okq"jaɎN- iZP&Ѡ]u| *+H uM_LK[kvƥ3(`r **I: |a-0XX|ΖyE=2/pOtɣ׆Rt\M̠]?]a+g $m 9܇ +t8꫒ mcH4i/o{[g cmZ>i9:K8yO@u }}9F b/?_gfzL]OZ|{gViCDy;Bv4Hw tAiZ6MKsDM{|f7_k1d⬸GUUPp%)g %)y(bV*w7K=IaabOJ*Ʈ~=f/͢D<3^T25]'kMf^vZʱBAi|z)__8ákvNzo roxEG_ZABQQYph̙AAf ӴPw{/%bu \ZotyCk`:evA 5^q5 ՘k ]dRV2 HYF]cG}˲zްMDeM΀tYV՘k%(!(MTCIf9/oioaIiyۅ i Awo~ښQ"^~]?_Is☁t/F8q25jd{'-|H$4Z7G[SK.*+~xpшʍ N;q>nt蒔ǎ\}z j0a! #73}GQ}?}7e{!RHBo U).(׫>׆^`A4!B ޓݔfGG Lg>aSN(bj/nqwv=w-Q&|3ON.a*Ey魟olI$\Z!sČ6A:Pr핷y򫯌"{_Zk&ʉ{:Gjzr?/5B8v"A/6Ni*<  (kodP#Q0"lI2}jʨA_ykgELJ_Fe /.P*~EszUn*=Oml<GLYlȦ[ t1gN_@CJt7yqUZA83tD"{hM :Z s!U -Mk+Uz)dд'.ԗ/ڐk&S3˞OTټj]76y|4rU Ϲs=_ؠ=xDLr79⨼ZQFM_t8un-jFK^n`=w98v +ھ;-?Yyuڒ{V~h-pmV׎/۝Uw S{M[EghӜ|c8:^ xP}eY._cd)>?r2 rBMi$$DɄXU*DSf!Y7}'WA t;oU'5k\8q4ZҴçrNWej~YP,RHw*OkU\bFF4T[D(}6?=󊑐b{ca\+1V(;iBi(3=<Wf [ДWTʹi6R-xqvd7?;PbMjsS7~H(w>/},}ΣMK[C/ ({D;OW~Px'XvU>OWfe+  0kT#ET,1,[jکǏ\SP/6̻yq&=xr78p4_DQ#N5Áҩb1 )Bx<\]v6g)'JM+OU;- ֞,kS;x3a}%}D~|rM-|׮*i{roB+0G|oTqŗ~Kӿo N]id EQLU+wqxc%mS7Zՠ˰ooR^^xew֍kyNqSMsJJϰ[b字זn2<>t[S e_ޏ0uZZA]j{OZ]/+E9d9zsJVnɶ87Ѷ !lsDln;wڂz^dR.+2{D1iuqW MMB#:V9qk4QOhc.ªUjV+oC (2LTzxӯ_+T[z`9' F88:ptŃ.-厸Zҍ[RYVzFDNjMfH-=luB&5sXh}hzY/>s?=3lxM-w9xKiv"A/!̍e5/OJNRU=YJ1xTB'l8uW\yFܤ0wH̊|*1 br|zKqjA|bz>GF 7|^B̵Yg. 1lH߄0w^E<ٍ#|ua4א#$sfU8<i*.:JȈ˧OdV (0ߩWя6W4 :B̊sM~=2eP?[w􉛫E*.c{ 1|`P'UΙjE /ђ)8g|*oaĠ/͸r8r.צ9㬔"l]I(אvuVɺyo\|=mZ/_'A'jT- 10y!}CdҫWtePX]yz!n´/VD9 'Sdarf7Q؉ }͍ٞ{2B iw_er=-[L25(Q^5CO6|:Ӻ7iȣO8;R(-+k~ڵwLzvΜx, #MU z_#>w^ |=zDȧ!tC4Jr3!qk(=;ܻ;@t}[(hGP.ϫʽ7}oa5N腤 ~%'? ("&?56މ 溿G[_^ PA}Ǐ/+j i]~63{B}ylk\{y98ݏ_hV/*oo`{lUKJEx}^yp?Ji3$'}'RNGL-י/o=Z(!?uQ"F#J[1=~S]؆K7̨l- =ɯ}x!!|t#YyA_i&{kG/-5\M6&y }ɆK ,w(?z7Fy!V4BXl\=sZ_wGT!uˏk"R&,oW*O9OynrRAhVeټnōQw1in~.bFkv1 Z./e\\ۆ@;~EszUn*=OmlGLYlȦ[ t1gN_@CJrJ|f#W&lC_X$>MeTРɏ/]ﳹn\|UsP.uS{/Ln{}ϛ=lW(:o SCǼϸ*aaimsh\=d~]b|V2y.ߐ<3tD"{hM :Z s!U (⬼/:>L/6.#Oxq7T1P}Bo7 BFp7hŽY<܃,w$<5Cͺy)wh_q5K/}`@O'5YB.g1Q#\sQ}ǧ{iB2˄~+'zbbk&BTmo6]5%;V^=MGPE@!.[B}OX4vJJ#K(bjZk(`+'>u| j2,m-s3 KؿbY{Vٶ2lswÆ Cx-a@sqm;mfۗTiuqW MMB#:V[&y;Bs%yEf376\\PF[m޼#b0VYjC5p9O~x\gS+ԍd4Qj*?.]ڄ晶,LTT=ݠ }IQIC^Գ4Hj5v=uc;Ehkհ^:oWZΣ5E%:y9L5@XL mu-wH*N01\}꿧X;+oChok˅{eL|)8xFRjAm\\1Þ($F$8Oo2yؘ9CbVWW~eWN*!4!+~_/=7ksNVuc.]wVj%DUcVÖ|o={`Sm'ΕjYVG򧧧}e1hsN7w\ey֋#cy:e^ΖhYl93%W_=?5qe_V*;OFO3pܑr1ePUTZJm\ hWwު޲e˔)SPl+U3T|]*Jʚ_|v3~:p+i'їk:HOA"eysk~s=s"t|(=&?56  (bψ  ~P#zK'/۸ƅ}> g`Ѭ>^N xfEu<w|*ʴ6Ӱ(ʩi:mXZ#K?z7Fy!V4BXldΚ8 [bPxDys=~S]؆K7̨~\CDʤQ<\e"АV0űG/-5\M6&y }ɆK ,!O%ƪܳ;o8RGE1e#n-9nj9}ݒ)Y:o SCǼϸl IDAT*aaiDՑͫU |cw9M3cْdԦ/6QA&?t׾/\sQ.1CG$֯T٠2R`xn^}a v|^Ef9C.MXMk+Uz)dд'.ԗ/ڐkF8/~w\BKa/Z֨8i̬Y(/!{?f_5K,&jdK$׾S|~=4!eBy o04tŹlCy^%Q^ScEʓ|%$WK]u E}_NVx DlP۱wEI;eǓڱƠeT@GJaލS;_@U{H]WGr[Q $PˮjiVL?Xo6szP,KxEQ#N5Áҩb1 ;΅pEjls!tԈsaGBQSsxݥggFI~n P֋sao[sk ںr-)nʢi.ؾE@K ̓":g[ DId2[Gsb&Eӄol`6Y"J(bdmˮd՜'o/ (2LTۦ_O2"ܓ֟y,sعRk.쿤h tMQN4~r/Sw ?SBs5xjǁG_95Ga{P|sR꣯Mxiir*p Aթ+NV[^VKN0>ы<_F[ˮj3ܻE(YO7zK9e1sŬʮTBnM_OOұ<2/cgK\gyݕozbr̗su9'+͓7RG/q&++}/u|tHNN[ȳ!_fH`L(*tN^wD 3tV =qа=K bq^Sw W@@y._KmSQԷw"zP9c&s6Bx s/Ȝ[n`(:OS|BH}e\c! |W!"JM/EeiB2S(8غߊwoZ:#9CAh|"?/=C R/0M -Ǣ_WTG?#Bv;5BmK\?CsL"P^/ #dҊ>=PB<"b7 ϬmJXCuUVng&e-4FtMr70o4+h3;RDJ2/Ԛ !|Esx'2zzhB! h!fCKUXIA@ Pt3kSV>wq>wZBfI#4#_vagM$^lkfhBx.h"lThk?Y+&, #hUP7p$ZcgL=ν-:5Bs =8Cgs@Ky{?!*-!Ry?֜MG!])9,kPb-Wi%$Fھ45-ާ$`'^/ıNVl%^v^S)C(cnF D{y$ v^0$΁_ېՀʖ-[Pv4eʔs%|urO[SӫfaUz}Qk$J6Wr$r=MPL ez:4-G.M:q6.ߎ("tkVΠj55M-(ѰA>U3~)/bS ? L9NN&B !=)w9qGƇQU}qmdoFREpxwMjYuA#̈́ը I8Qo/BK װ1͡Q7j( 6X(?uRDݶ]ߑ>P3` (l M+D󢢤rT pp4Zf>0!R(o?t1FC9M"|TX4oWQ(qVtmJe˫Pֲd(/iGLaҔ^]ƍK YD#! 5(ݥA|өR#scZfz,JǯoJ# )bA'>'qxqɶ=5`AMkkiBU,~犚jBB)e%<ﯶ'eLZx![UeX£]lKa e(Sb'grPM1Bez\/,k#g;o^WGX>^Pe%LRW喚$S7h֫ {BBk5?e; ldj1.▓!%"6iqG(2,!טX4(LL8T2!.- ګws%LGvT.. e(BX°H]DΖ͘O&u]+Q扺 }E{}4eY(\<2G$HJO\ZB1՘}^ 6oR!h. H)aiBGsL2}WiPLMT3_2wuJ"J#$֛́ %Fg?ܥ:7aSʯM7:R0gz_6>[DP^ܳ|cqѪiWL`hyy=3ء<-+ ǐcVUda]44s(&q{2"0$cb0sQE!+ZzJ͌Dz(eF~DkV r[>$jw¤͊u?kvj]\=4& ã=9]k@@y#3*$z{(kUZkTMKEA ?1 hjL$jc}x1xB5P|@NZ]]]]s|% ћ(+|/ehTM?%M}_$fUcӥLz"&ǐq̤ʲʕzIM`~q3+w{'o8`Bˆ\ٿ5adoyEkCF߲[>ť̬gRW{SӞ%59p=ރ2 3*O&Iū >{ #je_wMb|3]Sٵfo)"(fQ57__yԤCcP5pv$CcPv5H9nnaevj*-+k~ڵwLzvΜ02@G(((((((((((((7Jq\"<pEx}0E@@y@8zEx-;"u%[lfvqSa)#Col RXI'D1e#n-9nj9}ݒ)Y"㋍ĥˈ^\~=U 󶿹05t+X68$оe՝o:W{?loonnb8ܾRBM{rB} fQ()e'l^Z< lL]Ư[;|bҿkX Ρ\$&co67ribO %:mK=+?L'֖e*Uyn#'/ӫu+/Vy2W_os X$>MeTРɏ/]u@c|xi*Gh@@y͕oUY[O7FȠMKvrqn2/iK ξ5E%:y9L5@XL } $]ы2I_RT{$eк40l)mABW_ximVEy*ޘUИnanSeevS;R}mKLNS&MN]qޖ9gxCP/~ΪƙX<ʉ4g-䯿[o)L6fPՕ_ٕJ GM_OOұ<2/cgK]4~EUZڲ}c!ش 1_3ўBb(JVߑJ[TuYyrڣ8UIwdl*Sy%|h] W*-+k~ڵwLzvΜ t8(((((((((((pS\^&},;(((((((((%>_pE                  W(Aj/A@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@y $IDAT@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@B!B㗡pO}]wPA@GqGx᣼jBĮI}pHh/3ԙ)>|; ˻rǐ=Bgٴd7`ӱ604H ;y{j;ny)gBD>t] k%sϠonװMy{">؍/+w;?9QX(9{IyMyU[ 7ƨ1ʤ3>]woUlM)C(9y:.C"1⣗/\P!.#cܜxS6@߳]\(BHsc !՟?m{;RȡnrD,YB%-C66H;Yk ^ q.r15j*sSr -o~qu$2t,;oBFt$2kʛxnr14׍حnm(<|@e0[؞6+\(ayuLInLB]vn@)^[N^d !^gsgx?r>U%Re !2`XbzYsdCiM,%wr^?KQ 0k̄}F ҋ-ڟvr/0q-Aā1?wdKq)YמgU5:4D)C&AJ3dZ{ჵ>b) uȃC '~4WخS{Wͩe*om`Yȫw׸=uVr2W7# MJR/^.3GnS|u#P{*S+HJhYBIDWUTjYḄk4817m: 9EŹ*v~.@!e*~~ ~ yږZ}y&*<38 {]9 cN6T)]d8yhFŋ9e gxH+=fd0Ү^XcjRe֙h/8p{c'ϩn{ږn l*-VCRU%^&Ω[6ښ+4!bVNIOS18 beXTxAF4fhT+HHyٷJiazqFӽ2I<1Fx7[Zohq8`z÷'2nDN[줄hw"juw]-bn2 ^ڹ_J$&ɃG#H zJe2xwC/3kn9#eBRk2uͧ4O IJr mhPU>{!*.ΔA[{RA75PE%E&%JIPd"Z7+\,vbNrU{'9ԁUUgY`iw54DHegrXų{5qc YQtE}kإ+"i՞XxE}l݉K Qcuu/flnUW%!ݧ}v]9ZSu].* #1*o*s` #1{ L&bjXdddfFvke߀E]{mKq (VeL< cxEiYY 4֧o_AB]MF:쩫8^t܆'%%%%%y @CB8;HA*)?.Bj'@ t-% zyzԔIENDB`brick-1.9/docs/programs-screenshots/brick-padding-demo.png0000644000000000000000000005344407346545000022126 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw\q] (F$XF-VXbƟM$j%bWlPEzp;>`WD?ԹݝgYWEE)KJ5y"{sSSH ̯(UBF)-U*F<  ð,+161׮XWTܼvm25F.V(NĴkʊn\?to=;'??_P~p4 RcTbP^^:\`jlѨPGTT(XRP PzY6" Lrˬ k%cTz7檙 ł},%S]1hPxEcT*BrrE5ܛmZ06:ulm/sjq`fi7do Xi;ulfy?S״߬X3nzzeN,kwj6Aǽ̱S6W ^"0Ҁe)LPEzߜv؂/'iOj;'l=}A['lwOپS6^TmX5Vǽ3sflO }6踷qPIwcMM9\`Z/7A@3lrN߬Us/] Y*:5 8x̄+3/Oxk%/I0hrYgNݮV\?`<@? 6xn۴j*i~՟I}G<@ޯsD$bhlc2Μ WI1fo/}Ec~Zsy€Mٛ * vWWO|GMqp4fKw-剣W>ίD ͟doi*CChlOD\5#gkS0NUkɉ:uxw+qIj#$PUEfU8©VR._qnW2 fkH YnzbwD*oAv#׿$[{('8vj@EvԹsyy:Paµ}?/FA^X&~1NYݳ;~eԬ;k?ٜĶuh M[tEdD0lasNx>TZj䉛;}2[̽|zR1/m(ثO.<2Zb_pn]|: dPaşZr6{qE}Szu9fstD;]"RoWߏG騟oIЪ%AjRM۵ Z^xн{rYo;!y~9g_2WR +w߿%|,'U/ߏf爯Ow~ˮyVڶ=j}r>?\.݆:fİy? :t>:_|ݑ>_c3RoCA@ ĴSz&ԩ4*G<.JMSgFR=FhKsw=+g9O)/S<~ҫ{+)Y1D%_v3,fXtqqp5R7{X+bȉ;c#aNB+~6T!J숈lXcs^9q!lTqS1ÈM{f#A FhcoK5İz kh, x0\YjR=v[ųWW踚1lޯa{Ua=0yX.uS!qHD q\uQ_wt2=j'bYe :O?Ө5{7b"x" UIR1cMk0>+v\H{xENZVJ""bXH{׼xNZ}A[3bؼW:zJǣTy~':G`񽭒^ZZ}أԑ'O'>H<Wi߭:16^ݫIǾvaӕ$24=X-<Š?u1 6 ʪ ,Цsg7!IW=C1}4OXfҰb""u\Lʯc^7=UjHx?Q{xf~Fnep +9kPe5ū{5NlWƑqEfWjĆG+{3c}x_"/Nզ4e5mjW˩5<C2͈a t\7 }_Pf5/x*<@m|ƖT.=H~tSopw\R٭ۮ+tK=Ԑ/8izaP֒_}e7oeJVゆ]bs aY_Nu-YHŌ:K&&&kj?vO)40z0crbBTDԣ;u=?RK5o>߮]&rRCőz.Twq96==w]!HJt=OלO.yIIJQM|+bbؼWw:ׂȫf =5yV1njU3)~A%8{P}\굳eyw·esl="R:4Yw/fi5y/\8fڶx?pNM/&C(P#v.=\NM?oB'_U7ʉ*n2;jS6:FK,&e2X$M~մrO~7zڿ^ r|xNzjx̔Ò4RqE,emWxx\/,O ^1a Zv\bFba*Vܼp)pVZ@0lޏa+hӪ n 5W˗~KPHy~ߣ/A!"O[s_]lQEs~dso":, O?}~?Qׁa$ֶl~LE8ۦ{&Qt :f ֪>x]h_~xOwg!_qD|O1ݿ?WO`TdE$:Y[ U/ˮ :uo٢|53J%⾚٢-x]3g"x4%sT/_X'wPMd- *VשW0>f u   iUq;/Aav@"MM F=n\2v]?'銃tc85M/ G=:,ycg <* CD4SMYdZWCF㧙5V"zw\Rc^V^PdfRF{Q}G;=|}%N5=#w{V@#Nr3[⊕"?qھޞzs=5Xwy^igY4z n)k[k$eems]770~B+ݧ7\Wsuvi|_9Vm_oOj `์zS5{2샒z&*fvnX0{DJfc4&赡b?l&<*qi2:nwƅ<|8#]d?簂~K[sklN Y>n EQ%ݭKV~`?ޒBwkeJjd"Ǜ[8t4$"ڜo+PJjެG4>`ȫe4s_epP5=(q|_<#jWÇDDA޺quU?\|{IPS/b}'XtܶJ 3&];D/߽*{88dײS]ޘ:/O 37;1RC G[|?^Qges_KS5(縏z4PaR |werӤb'/_C? %GB[nlpw=fMfܸ>vKiױ^pƳ]?~rgQnֺj}%W]ac#={4D^GO]"k+οVz_42*.9V⡹pTdi:03yj\C5vNLݧ∈xbMut8e[/DW[φ|Hg'MDqâ#=oWd~'d]l'J;WCDi"B|V#In['yX% w]P"㉈7bu'WJD&lѩ<qzoֈ` DDe oXx5a_Fؕ~+qM?z03k)zŜӹtGaƭD~6?z/eC7 >i_v/#/L;N.:AD<{M.+3Uo2IcB ofK։kcx 7m_S랪j #!詷Ss?Y?zI H5xDV"U<|_|jJDw#;eӥ0L-0a+D87I-jT]_LYƥAZ Ty $ߓnzp=ӦL2)YlӤm+nZWL]FyFzk"D~?oaO=Jb衜K5_1'|fKo&?9$"Zo4Zo|}LyE/TuaYkpm w%E<-""".G={51yD~ӴnVwh3/6\dBMLz쥜[oFo* <V.w0k1 a{g_2WX; 11bZ^Xs{6tT6~mzU2:. o0Bz=0{K rܾXVl$o'\-6tiQ(Z&#[4$7뇩w[?;a,jIшI3Ggp-ZGhzDnbE}fA:y+(WI(k`FC#:46LupvSid4yw0kӬ&i,ɻgF<|$HlzdžnF빋8tKSIo7sg11 fEpN 3FLI u kPT9r|V bxq@}!?-;&_l^{)3o՚%u" lݣ&QzJ[t0n;!|Z+P0.qE>#wrbO=PS/lގRSQM%1YQEz/̃Տ tq{nFN7HsfX8_lz}mL"q R,[X,U!{4<((oTfw_|2L=2 ]}"K*0ꂊk)*jӒY~XHύ)qыqח&q_N鷕^)ͤs6:j25\)VO?kERFYκW_HDDbۏ_FQ ZWST I?Zai"W\OyEq-(.WlRV:gD}s EK-ެM,I}X|󶘙̊kg˳gcf^0Bzk=/8$\u>{Ȍ[j"4(`ME d#OX,V(f]O\dߨrVBnj\J䓙ㅷojJk/wGq^|ywܬkkO4Mz/{ ##=XA훷]&*E H/MfNKܒt[FFz P-\R4o8p gF>.Piq*ӞAO;oJMK1{xU{A#녀uރoF~E"}zަ7oFFFR'qގ'(v=  ](((((((((((((((((s'%L^nog(_g^ eҴ%k6mןW.*pnղѿ;|[֮vXz#~czwu7h^wMպkF竪Zn)iα1"_+57(*L=zLw5~t\'KCWd8wل2a;-l- TvBJ|-~5/ kPOw\^OB?ߐ;:&Țټʈ_=3JTA(c42 F IDATEC#01GD|˜%"E;?""R5CE}KD^2;jj>xҐN6f:|yN|hȞG",0RAxZ %Yׂn;vڿnEn|GRTyiJŜD+YIHqڤMf:Hcw,p<'t5ϫjf"} y"3ˉ˞{tI;8gmpD2/޸$LxCrl1^7/  HƯ6*2,{NW~ӌ%".؂Fh_y:6r W+u9X!3y:"s杋?KVVվ4~hV(`5wˉ,ppv_ O-yYvq00T̿ 9zs-.ͫY:b⒎\xM!"^\,CDeɳ[E2V,pGa TQP+ͽ 6\1ybԴg`hjԘt޳@@GSnJ\s}ܫnR]}",~Tf\!:4nzh+ %*z4&_Ty-E/ %O )&3-lh^4׫h2 䅗wy5Az2Y+~_^q'Ҥ':&WP; L|z 1kdΐ*KV5|#NGl}8A󊔤,w] nX!R59O8m4KWg0; ~5ÊǗ,6Ts¼*!D5-!QP/f򼸐M\K).j=^zZ1׭q'".ĚU#3P)J ogy""yEd@]V})SAi9/-[Ք<`pLa\t")kqg3>T'xzj_xe#FUI%"jvyU P7.[7I{ | `kEk|E7/'5-/6n|h1XA:EԠ*5Hڶ h\* )#k<9yQz Kw_/'pח47qa5âA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@x{((((nў={FKo:xPPPj _(us S! 'pnղSFNvE/Cx{h%c-$b*/N ?;r58%kj^վl #Lg萯*f|"G E|y~ZjR [V@9j""Ysჺt4拒n|7_CD|˜%Kv%"O/w(p=c2'fdEf3VH4_gǑȢF56Okc}$+ZmuiNV9BSK#i;dO6F|abm[+v8i}W\}=fh.Z+qŜD+YIHqڤMf:Hcw,p<'t5"w?;y'au;1={^:r?Ô:掭MY7  bh2#"s;5.Ta0q ""e'_MO3c &uu툼~9ΆܿKSGsF^<C9c=Hѹdm-67eIVV/O^*'JدqY 6_H9ۘK:r5xe~r՟1xCeɳ[E2V,pucjWQ0 sv#ΫK4P1,Ks&%.IZp?]CDw urBڣ Rhf~QQ;$Rm,~Tfupq1פ&kuJ*GDBI7~XDuggV-r{5^a PT7xH3u﷊x /hnT.3: r MNR\00 LE'hUC+T^ФLdʳ_FŗW\)4 )jIT0Nfi/|Ty33CszRW_tiA}g3>y+X#61+ =y+ײ KQ(;Yj(Vݻas<|Hma~;*,Vk)evRxsQ=-ŊYyb*n(e%}2ٰ>#~l PG_TP5E{;. ؽϘR1//H:BѺjϞ=hck5DkR*igFS9(P=(o}7j+(((((((((((((((((((((((((((((d9VE/C@@wok;]'DM"ϩNkb<o31DmF}7ٷ`Ӎr"c@lAzZj)X`n-[!#i>xҐN6f:|yN|hȞG"8Ӻ#􉧵P]{-xsɊʺ:'W+݊!Ky42ЧEC#01GTC@@M}>˻Jc:4+h-r"CUʄn$]-ZuڶK.BLH:9v"}X51}VvnB&#HE$v Cunƽ r#wCgM\v&׶ @ƘxtNG5Dw#D:]X;EpNmL/z+@@NШ0,M5 Nj~:vD^gD$pf,qL3Z<+u9X!3y:"s杋㟯"G뷍{B&ܔ%Y[ [8za .ʅt"\[1'nwϷXICj0U|KIE p9gW/?輺4OS4MJ\~6䄴WC sMjr",~Tfuxr+C:vPV#Q٣06Y\ 9ɽ|Uo(P|4q}ҙzsYrʥAz2Y+~_^qwM^&=!E17Խ FL<ƩfLgD*3c=Μg|VJFllcVzV.e+@@O#|4~RnX@qQ9RȎ9n펛%5yWNj2OYe8߸9 E0v'"Gm_L6lψeї2+?.Hqo…yU*QC4Z ncik5|[kO{4`'0z|uF +tkWr;|A.}{Xc#}Or04?uɚM[ 'uwxGRޣi)b$M| iƈ/L =m{%P9+?odWs-;.V1f2;4DD:&Qܯwi2/|R݉,im&dbYsჺt4拒n|7_CDyfRZqn潡jCgkeݢc͡lKv%"O/۝'OLe_q>a H_(޽h ^3wEDD?0m?|OfBxeﺽw}؏Ǜgv/,_COU^K  (OШT1,Ip!O`$N;M>}[IQl+;{lO@gE$v Cunƽ r#wCgM\v&׶Uu@p,WgY— Qhj&drDG&ȴ gjʬ>N7Gsy]iC灃f-%RN$ukPqbͪ2vo^nCM 43{RkN<^T)^^[ ؽo[֭21erDTr1Q }5\ΛՃwÞb2j3h;%B`݃{( 6 E3?׬^Fdd?n֩[3&+JVeiB&͘l2Ac<ѝεqNIY4 <ŧةUoU} }⊒W ~w|[Y 7 y8UԺ-->]};u=!&]ޯwp)9/p%QD-=$Dz^򦑖6畲RIWf?9k)zO.G̳mLy2rb0^mY!"+k*UwDi5#ZyHN-kRCxͻ&2 Cʂ{]~$J]{ߥ#/VL!bybeZHƯ6⇐67eIVVދtvP/q1|X϶GH<T"5ܓwaQ.#FyDf=T5 N>= 1d%6|AV76SX88;/WVE,;G8K*$@@y:bwCGʌeɳ[E2V,pG<1La.GRեyjvX"*g;;=~ҡyc.jKxYMAFQߙ?\]qľyjm_jӢZ;dz|L+@R@@y"?-5C:vP+&%.Q٣060V[\_'jijsnBZ9;ea'/alw&bkXMKš͖$ugWwkt  l<- in*5Oz )۰z*Q<:ogڵBɓ5L;:;2qk!ozBګcoҩ{^>pә!ѹJF /XϠ3Rۘk٪Ha֨eSY{~}2O,{KT'O&t?n1w6`6h'sc\CzVdiyS+_x@GsMw)i@?% yWj֧[ oZRi۰IJK=F2(&5i4ո^6kbK61Z&P/#t,k(q'֬2oܜ"NQVR;[ɣ/Z&6gԏ 겂KK0Zv.\8{1]b^^u8:Hu+-*2c;~8qNo!W{鎣QOi:sG3?3hQ#s_Z?nYF#|4~RnX@;@{Cv;2?!d㥛ie6T'xzj_xe#FUI%"٧!'aIJ:7{w".ū|Y翙3ckEk|EoϞ=g+5-/IDAT6n|h1or?l]lre$*ڷ6:{uAG_GUjV5mи,tU B@yǤY,+[m -m{֒fڭ}?ABN^{uz*&fP眭OKZ5pȧ= / m8xӭ])anNb V-7ձvKo 'І_q]wھןDO+:6O\b1q-_ kޱ,+]K X1cZ2sK}mZ7:|^}qзӯHz}ϯ)x7=ꕄ@@$Qb\<_?C;ؠO<꒬k[KVT~خOwrҭȍhwZI!}Z41 Coz^I;>6hYUd\{ˎ DaL=>o N n2]ZEY`;zHkV ԮƬAZ:EIBvo!"Fsٙ<^V`gi 0WO>snhGSK6!#861EDVښA5o4=fh.Z+q/#2oF7ԭYAq%4!Y{jfLl6u ݱ`lNոr'Z,lbS M񂪷>Rņ>qEIDwS+j;d>_|z֭̃𬅋 TW I PȘe8}2m鵆G5^I;6x@pCim#ȇ}$|U{_U%&+7x;g34 4lHN6k7xx&9l͞!LZ_P|C!TyG~L :#'ͯ ub)rqyŭnO cNf^+tY}B;tXPp ))6,{&uFѽWwgys[ CAoFj$=Myׯtx'cnu-tkΜ06q-6mx@Tƒk?r9yܷf8٘ueBS6)#f.4J .*{-˗O5~pW;Y_ZK1B,neAMajJzRei2㷽^}>=.b8Y\UDylĊI^{QлE%Z%}wWU͚6Q>{W[|@}GxB_?u}FO8Ӯ8'}!lzhwdlsh~_ 4j*:LX!ܖ{Qbko fqo/ڒv!/?/~5ٜA WW~//vCr):z&:!h]'<ڌ +7UO>|9挚>1e6,_6}j͞? ^c@ P@@ @@ ( ( PTLD۷og ?-%@(@ P@@ P@@ @ ( 4Mxx8 ( P*۷og"]u[~|]&p( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@LynBt[CͻIC=x?r(}MPGj cx@\GDn^gCm6A3e;*h]T-XB Og{0wxirIɱЧسc;UV} &]&Cn85jG%h#V)-mem}UVgrOᰩ.qG;W).ƥ*4 ñ<)9Zu"ڷw0Z #Lۚ/>6K][!Y/O`k?m{7l0;O8eEY*ZѦ]/caBȕ.JWT;yo6iPG pU6>':A]ak*.:A#[zR[ 6޷؆-1GGgcR؏P'_Sv}O4k3b7D P=cmdL P@(@ P@(@ P@@ @@ @ (wthӆ@X``٣G*fBdefMo[ϷpwpӈIJIENDB`brick-1.9/docs/programs-screenshots/brick-progressbar-demo.png0000644000000000000000000004000107346545000023032 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw\U﹋=e@P;[s6(-+2-GmQ2S4˙#q8@LGss>g]ow*hJt*!lNNlZqws\7JTJ}`ԟ=s9Zh6eY^? IBPk4..];h\ce2gZϧF?&˲l6tzo !5k\NF xIEteb/DPP89() ΎfZ @0TB@Ej"$],!prɋ@_SMltUA*@ ҀJўFu1!(_RUKSe-֡!.gr ϐݶR2库:l:}D͝Rp=x,&[.߬ъ"@3<1 q3)DD#;BSiwsl ί'l#UzmjBLo-?e(r:f_ӿuweNK>:_B(ZM\U䘭sbO XTmWʐyKtNWo^ xV9qIJZ3f[߯R|z׺]i{4wjOfNEQ| S9[=85osc!;]ǫ=f0Z F+ҊoEh,R\j)Pvき6# ڰW!mKfBݜ"9>ѹ|qSFJ!$O_L­Z32}8_Uu!v˜q0bSwoGKANcW_éOǷp2ǯ6ӷ|һafAvrAvF!A/삩 y㻹OS~nc߭Wiԋ?D$V,*J69[juz&2>myOmJL)pb.C&oInOP[6{o>WQ"% f{o>+9Ϻ_!{ZqHnBs܉%K~fkEYVz7lW>r~֧Cקȯ o(PT: 'q=v}^UDcVUw!իs~dSӖ*!VNYiŻjEQnG}Qgwqٌq-Pagnu+ 7 OwVl{5+bdz -hS 97!!]Yԍ)\eTTG\RCMDzni\ӆ!_<߷k\BKsY'^|< Nܓڻgwltv+z̗b/zg7k_VWiE1igAٹEEl֪]Pqڵӌjðcy唾>whgdF^EՎ]ꪅ1bێ+0'{$r٦9_g~sү`'1﷞Hv.rLӍI2^u:a[oě+UCYr:9BƂ:*?>|/!%uޚfvHb4=F!bɪ $FB̎V׬%DlY0-Yn/W'}xɵNfMkfǝ7<&,[Rg~scy*'wg.5g%X^9!/'|Ǒlʐdʭ\Z=듵GsyYfViG*&.':]o9Q7^EaؽEPFY,I)}#e#YJ[ Df_MX6WMw1!"=AkBSmK'4!N*r3ЊP^^^z@Q=2끎UrAؖbϰ{HF<^-t@EoZ4?UT`,eYɣP]:d NfrT3c+.@+ft4Ө.fQ eL'jVL-.\ùR!J!{HrQQ\r:{".i * P(tP(Ʉ,RPp؂*v@i!eVCR3F ~5{S*_JUVUv|C^W^^cDT9:Q"8(7Bn<]|bm^ctkZLRz"m%9thWT(ZIKPPîsDu,s飼d:Fd6DGd .c?/ -Y~r <9^J>NK6͹Xw%G&+;4%2pX;[ Zܠ ?xU,zV萷쎜=|:y@dx.c?$W뫷siX.V_2ykw_J dzRXCʟ SE)p9}ᒺi=|-(*2y+}Ҵ@8bmէ$RUAqMf ,u}6ɕ('. Ik׆WqzjXf /Zkn{/6Սo7˻\wDq(PVv,bFS}zCK+wot.A4B=+g6ܻȳBHt>1㵞^%TI(_ZU?)Q$hner#Jo,?i.7|(r*_o{.NL`'/ͨOTH^nG'>6*Vi_=!.Vo8Gt]lBйkwSZ{yO:V*aJcwccח[ڽV?oBw7Z1H"g[lFg&}qSc[zz?mGvн-媪}\[h>u-u*ҵyzr]_ܕ+$7^=8>ED' zTl__ U2$o7 x,]ḭz-QŶC[i_9b8=- Tm%-(mgxUO~.d촪)ڵW/e9Մɢ ߅C}shK4j|Z~s{?𬭊KBHu* q|iȒ4&TXsg5/7~OgN}i}KWGѣsi>>pٵ痳>2u)S@݋{ %V0SkK̐FBȆ ]XΟ3ΆO+ۦyߖo (EiaUY/ο;_\!LB"0w2 !|aWx'_ ʟrየ[ u#_ ;LBQQ6%W!#wfm n9.fפxE^/ɟ3/J"{Ye0 =nvjg~FЫy"}_Ѿ!au"jNgyL]G8znEXRds!;i3_ɷ "{{}{ B'%6kBD& 9O<Að?BkJ{*V_#c?KpV&2랾_CҐZ!5TTc钍$gLxs3;<="s}'$ !6/_G~q"J%()\Y3C/J>|^#١!y[Nowfv!5$-kI ˰O*ٴou^0S]?ɒ0~7gBXRtF-OXTܑѭT a2 %I’>#w3DAjr mo^7d7_%y0xKуzBHB!cGY F!YI IYFQ E)+җ-,$-KQ.v:.[z.z0^.YA IFP aBk/lLL w̵i;Xr|}ɨeu>9 ERw^yWW'B vrl}/Ԧ76u}>}[wle>7~tC[zS\xӋYo/eBȦw޵;{OжWQrכ|>Joo[[]R/ڠ5 !MI7¾غ"o,JRiKNgX]PCdxױs\-KN(b[TCWG!]}ݢ爮XҿԵuԟI5ؾlG s7,|2:&PCS:;Ѭ/^;VffpY6^j0dÐ!Sc s>q*xQWu{·d3$e-~:x.k!n\W\ċGݦ,~nJzgCJ9bN|=E1vFufaP?Foz%ۆB!8[ )1˹ (2#utekϵnbJ9K{]$SFu^PdO>KgʩQoBX|aZyohpp'C%w\|fW7=m$?U0rBN|wLg^Ӗ{dRv췻G}\/?pڸՓ3=뻼_%ɘYpd_晄*G8?rI_$rMIgrgzzDϪ2.>pvoFV.~eޚWlʹMP*\EiL,83?QXbg~h7Gt]< /7 gnxUONVQBsvs\fmyՅb/B=.Gf׵klr4)N_n81s6cUgBtٳjJժZR* :uJa0DEi4ͯ|E1xqJ>u`]Y>!ޝ(dWSOk6d\:soOg_9{VQ)]ȷo}I^^*!IDATލ7KK6kPskKaj%mx+@O od(<;5UƤ+Pdmkm9 #(fʮc_v~g`a[ 8wۇbn2WBg, ʔ %egSf!8Q>I!$;[Kz|L\<}m:>3jԳm%GݴRQx7bo ؋V!dbcNMLz`R89۪$;p,Y{h_<hֱ.tbs4}ܺl9rHpƶov\=^cI7>1R]kV>"^!!UM Ma(Pާt>BĴ|Pj3Xnӭc݇cL @ydߤoi;,^AڥȎ’xXBPqTBHN~~*g4vW؟#m+7o^eM8@1cGl('_â oy̨ѣ &! 羜=Ԉ>_տ" _Uv[P@C@ x:𹹿4!5Q$-@ɔ՚vߣ[}vѿS{unۨ3_xfsF]z]YHn]XEoroGP,ʥjPNUWZiQMxb{')6n|EXٛSlBЭG`ۯ_^r ?}1 zfH=Sʷi;y:>-++ҩ:JY{(_@"ؚ5'm ⣔ h|'o wp*v> 8hB9+MIʻ+d]nE- ;7W̌<(+]k?!ĭ7u9z K'6GUJx{ː*%$VOb1rA|i㏛x)4%ܒCdm֕+Dj }T؋Y.Ujxڲ! 7l:i6GmrO:#I}#ϟ;}x߻cDFA% M&IN 1J#i͹9W:1fjj[[5eV4ʀ/)Z[ǎ BRRh6M>-5[Y;(t6U'l3zvŒxXBPqTB!9<>ȱp]ξG-rV-:5RF6܁Nng(9^raŐeMDjNH8I).k(/}jPzj<,@XYUT8@@ J!}f~9_y?%++($姴e4~ePzf'(sNia!b(g@fb3o4՟?iʿ'#Mi/dUWCT !9WN>>%UJ*\Err o=V#/؜p;w$ǐ*?cQBȹ}2LtYYɥHT]Ѡ2/\ "6Cpw}7wp*vA"L˗V$cHXkUnblK m[YNa@l}9eݼvI'BQc ݏ^Cedhx4C\||.1F.! \`-U%%@@ Ql>׻:Dž3P~F5N!|.V=6Q5ǚ?`<.=_Pa9=W {e;GoUV2n-=ʽ%d4d2Ϫø dc T(e9GK؂{r~^l6?{Q/e (S ^jB^A/}B_!1;^O#)+K-_5։/|wD+ ɩ؏^?{KN>.1 oY!4_^v'"|Ԡ3;5,g{u' _G4{ nΎ +ȑlBYcz}㗾=f*Y%(~J/Nyʔ}hݒ.^y |ցST &¶vKUɓ~C'(94ռ*9٩MV.^u$Uuл]Z,zmJԆo5 !$]QΌ9yMgJpɡW]^#秜?o7R撖r 5TWԄ:#</To?ljy9~cR=/%Ӄ:5a(NIpx;Bٳ;&!_s^M⾭)9{+ZSܺ>8fΫO̝Eٜ^Ư4zqp3򹩟o1bTW\O'E>n;q$Ԩn=ŅDj'Oś0lyobG?+]/8xǏ&|J';tm*XdEbάuI;/!>O- !1e=gͥ=(J-7viCC}M}„&!94+MV~8Y]cW;ɂ WS`B(CT-8 A [ u>o췧oJ!;2!@eRXy+ASbܵb :ß4N7q#U:u}s6Yr 4pȻSfMY/q)PCߛE:g &_cxN,aʀF7KcIz)7{WyaS&w:vɗRM 2Zϩ0=_2P!jܾ’rT!,BiX%ɗsrM)7rBBdmޖwr7}T& W\AϺOP>z8<"ۺ)½N.9}Ҫo'ݦ'Nū+گOug^y3ąoR8vҿ}jnv.[oe%iԒ jUiP!F澫cboVUו)ñE= FMSLBɱN.7%ݓ%Ndri9/xW8_%L̽Q'Ӳ-޵)ݡOS ;Z[Nlu6$Qg.}LaKo(u =oB(<rnlْW]k YeVZ>}bM;soF*i/$\x[vUwҘ* X\ˆr 5O5Uoף}}ѷ~OzR'X5AFtTVvΞՃh~`؂=OhdwC١j]B_k %АkԩSj6G{T n_>q2p_曅?rW㐐+1Q$v[/w8N]mrzKz+ti^Х;jT7_->Y,g}jIT;z$g~vsђNߑrLНZ᧹}rDGtOyJw_Ar,Vnǭ~ΊXʇ۲XE7͗%q93j>/'t͜67S2"'ow:o\v%M7_OЫM9YI {~',>W֟uƌA;Je$M6Gv'G^0oyXHß=d|>BBK}G/~RmyTw{0^:ǜ_je!JsYo\$=y=_Կ|4N/_c)LD;z|ʢJ>+?ܯU%.@C@ P( P(@@ P(@@ P(@@  (@@  (@@  P(@@  P(@@  P(@@  P(@@  P(@@ P(@@ P(@@ P(@@ P(@@ P@@ P@@ P( P(@@@@ P@@ P( P( P(@@ P(@@ P(@@  (@@  (@@  P(@@  P(@@  P(@@  P(@@  P(@@ P(@@ P(@@ P(@@ P(@@ P@@ P( P( P(@@ P(@@ P(@@  (@@  (@@  P(@@  P(@@  P(@@  P(@@  P(@@0+k%, _Ԯ}gD&MΟ7͵B4vDu).>ǰT8P^#gIENDB`brick-1.9/docs/programs-screenshots/brick-readme-demo.png0000644000000000000000000002560507346545000021753 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw|e2.eC)Pl!2*.b .P {N޻MqQ(6U ]rϓ/wҗT`Lf2Zyzx6 lZkR/ .8{~wnN`0L} IJZYѯmvV6p9#ul#p)b2L:]RA5? !mNz xॢ~_:--M]v+(2(,,tvqU;;g9@%vsq2l PI6\VM.+W+ !ҍr4@wK^647>5(VM$%%6jwhqAk&B !E*Ek^lJMrΝZ]˽ PyzNGe+k̖laPiaT?DCJS[4lʶ;ۏafч(]^]Trm',#ʻy^:4؆~1[xtFmoviQ?'5E}ޝ\Tr YS>o#Yu£'o[>eIe7m}k`uCc" !n_@:~I%~IJ|mê մ˞}R6h -Kk̖*:[2^Uέ-bZ,ɱE6YBY:f̂sH'2]ݽAtBֻGFNQ?Ӥ?9hv=[ C~zF)7_\M/Y6 s2 l|pvSLil8s7K}K6ٵQ̄BmZ8|4EM^ڂ̤<wzeĒwY>"ij5U7va-kls2dgo!S~Vɫa Wؕ/[TUB,}W\d]+a-tn[MǼu"gl$rղ]o>_cطNa;zB1mG*X9(V9kRy Je.K_J"9a͋Az:`7}qƭطOPnc~. Sy7~pn=1VF̟D]Mځ'ݓ"Xrg1jg3`@asIn]yS  M͖-T>2Eؿw}g/WLi[d=籞A)*6iPyқuroWֱR={נƣƫ}RfdTx=Bnþu̴ymѳ YN2T>qܫd ߱#$ĵ7 Ƅ}{/N޶N]?r`0 C۰y$gH")?$]Aվv.[^.ZدwJ)}^U %Ij6q/}D8q'ڪŰVh7ԣ{Nd)B's.2uWJrRȥDCgٕZ5} ~luiʺ݌&$9]F!7o9J.V>o|v%頩]fdTxU{u>@tT^zw !iZIEJB(lq=On/NZ-Rןədau̧9]hozZI(BVS[Ѡ!*!`BIB6R[bv]r2nrwKM&EʪBo(k32[la*x ۜ)~I)ףfNēW~[ݑTYHxa[JM>D#rً5FF6s4MqzupP];6مxa\ q_3:Q2^ZcfZWm߯ ϓk5„dYUF 0lͩ֓F>d-pCΝ~|}?2W$|U~5ͭB,.u}x@mܐKWMBTdLypkCf~.MN*s32[laxW5AQ7 ;MuRu'EG)(F܏'P| 2j]Ǿ֫Vs+! lSOF%N\u@<{+9U;F|׻_W;؞kN5cYnec>N^hnOi8laW$A}Qv2ŭTrt0< tco=bbTɉݷ¿:PP)*57ư3w^0GT:#(UmL\u0m^{ߚ>s&)jyn}] u5/Ŵ.е͊t5nw*]A4ue`TjV:ZߴkhRkeBm-%Oiwk،[,1A:TX!̦Ǜ%eOl}nokZWV||q i:ԇ5OBnƛ[|j ަ őUպJ`Վ˻ƺY& L A B¥[ޗ$m;d}ouhu'ZxCֺ#'$?W+ҝA!|=KV%DΎ+->G۾(uRe~(UKn2-quZq?Q 5qB6K"4lr_ڿ1`FӂB{,;PdCUx@_ƭwy̷\>:9# \Q.v4nw5!zcs>xcRY_KH.]:i,3kiizc<[NE;]sk,y;ͯ܏[v[P3>pqTx*ܮ{ĨBzP$0D)X !VmX 3Ju;xg:YtôWC;Su^6^k"ۢ_YkB_/x&߾^L̎T~ݡҧxn&L3 u3~_~CJ~a!v]P5:Yž{טB6ߙÈ%?QI紊\r c45ݾ[ަr!d礲R=[6ǭMBT9l[37m8'y2lyQx?nߨEBW}KVЃ+␾`ӿ-K/!8wNu!5fq;bol0N!M?a:S"nL41?@Ec4?>jroA󣎉ßR oH1$_.&zJ^u=]y5ȜTZB"V6/ky2:"N^#>]#ݻݭOre}RmXk99s8AգfM^'7c2Er-՘u:NH,zY;/ xM! ֆPzQ ;Z}͊kKdIlkN*LW56BɿULơ Sgփڥl9Pp˭9u1>Y`evVWczᐛ|~yY* 1PvHMhrˢ ' ~PrQ~lU\UNArdnSφcEVjv-fܲi\>صt"w'9Y`S$$RvPIm7&FU  TsFkGcN˞bS]z87n--G!ԝQG4mpOe/d]V`Bu|9ǕM 7"r͸s쯿9gE|~:rX:<=a w ܆533禽nչl[i;uN?7O\1-ڪ?~ hG)E}=-?L9-װsHr@9cu'-elHX|ћkIE+m*!Lfh*(/c-~µglLXhOurK >=Zĉ Sh~9mz&]inH u 3Uib0@ "b_vt?{\7g9$+ 㒖򘱴vF|({T ® 'ω s܎Ϊ5zWǻfF{ٵBbȊ)*jo+Jr.>_tOQP'漎T vkjV2uU2,4'{Zдy-NI I}wnN!0rfP_nwZ_梲a>Id䯝ˆjO[egIkL!B^9',lT۟~)"!̬E3?nsQi+̖'U| FFQ]rFB=I7 J.솬#&F՚ +397kn.Kz)Bj}&-/vSy.rZB,^Lu]|F~v^_b|1'BK6mg(ی{G=[bb01TEpoې?&岏cL ?j,fCd=˩ &_! rP(da29jרѱcGZM.\B!!VVVڵ#PCrt\sB'-,Q򟡡ŁkPuӋ~|d:R{?PWjk>{],6p?ӕ7iyy7}{a/ 伔XG'LuYѧX kOIίQNV7^;g.*bbcbR;/| t@r5 P( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ \$ IDATP@(@ P@@ P@@ @ ( ( P@( P@(@ P& ( P( P@(@ P@(@ P@(@ P@@ @@ @ B4j6MKBֵώx$);%KwD?)tdf$f֥`ܙD, {.gE-g%Ը~w_{REhocܕB6]c@ÂZuR2Nl]rEuC%{G+lLRJ\4pB5wٞ}%*[!=ձ]Y9hq͵Ba8s/CjfF=Pmw +%.5Kל)MȏOSr'L] (({|M)SON:owb m֫;>fg- f*B]dK V+P$F!6*_|g`6zikZ-Z?r@?5gr.Ϯ_}$Yg/fTs!<ϬlEtbn{)\\bM+)"ʝֱzM !?Pr~ZBtYݑ\ZroW8vF/ąX&jqlXN]˿e_5RJ.h6v٪1*ZeJ;%r5h-w'd%%뤚.M; PPgM$'b17vI17ղ:a.Ց^6;&TI(yja2YeY$*j0EGk[ zdcg'3031>ni̓9a`_?M 񊧿?y߀8_cQFy麌1p6Y~*'9+:v]RRV^~8B>b:Ԡ]W^D_IT1{l{/U#1O x)1oG^ EOo@AiO_xIS_ .>a(٩z>6KN!,cNO6@K ߹#yto 92E(e`>¶z` ˱߀wg3o[bbcbR;/Aںz2$f7Aމ]p(|w {ˌ =Շ< C_!p6c@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ ( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P?+88@ @ ͭzj D'BS<@ P@dmcSvv x4hP auBȈT^V[_$u8*T:V*y5IENDB`brick-1.9/docs/programs-screenshots/brick-suspend-resume-demo.png0000644000000000000000000003074307346545000023474 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw\Ue C4S4mJ3+-GZjVJ+3-WVr" ^ET)9ssϕY}Xu:}zzRTdPLܵA'=ڬKF5n\mP*oVi]x(;+C YY^HP(ff}zo۪*_w̟\(g, FnٺQmvNc/F[RRRիBA^^:77*\gY "/Oײ @ hT,pj"U/E=<.k`itS߾k^:ܖϗ>ԨA(|t"D'TeMZ}lZwܩc՘r[ ɦjv;tJ M9bf\ʒ(%h9ʲ IʲGΞkv{;༷&-]^E 2]܄p eզwW'1^my pk]ڵUc$r笒ǹn.EsYUONLtrP Tma%$Ν+[l_}UIa5}Ԟ o`GվclB%6AMOɮCv c}:6"hWׅ+ZM߹J)Pw~ rRin[>eɱ&Oiߤ2/_tsb߿P7yV1k ;uPjnNrVL ˾=y#E&cY9ꢿȗxr m>6y lXtkP6epoT-eɶȓ;QwjyB?BuI I^lCtZ7sync:Y3C#l<\ʹٲPUI G^.$}I;]'8=c즉/,TiۦԱ[cɳ:odm ~pd!0&Az3Q=JOk>,$,dffvσy߱ctƹo.q JRՒh4BR*JSQyY< g-t:]a8]1|h~c1zY&E_Kxxĭ[B BhSSs±J&.;wƫ;:cf:];T]{>15r':d{DO3'#z}M9HKaN:|R/{OOXxtT-*]<|,i 7R:T ^_vuBLBڣR11)Rʀ7Ӥzv. s~['$%^oN!t6]ۻBCOmi"5;w33T &)=|U6矬ҟ%P'OX-t9'NB~*Wr2FhB̌٤ZzK{X|o*m@C֬REh?v|Bsn'¨vR:==oD}E"c S'%f'5[ ~1x–c qqOt/ Vū(Xf]o<=)vIkcɬ^i7>Bigbn, Ƈ:p0ťs1Q{WtIW#ndVN[@9Κ 613?$D. 5j_춹 rmawUmo&+l+~2sggj6W`pUf5m4@?ar(F3\N\ӻErrYGb]O>W}b;!ulTky0NζK/_VX~cаE/GK>KD@*Cl¬{ۏkPQl}04.uگVB'[^jմJGB Begj|62 RANӷ7:! BRロ Lxz^3[)2ڽ7P(\z,bַ#6 1orvdn,&W#|V$넫փ( wKUfL;Xq,IEJ=$>QfVJls@E oa(TJ>VI;ScR~dVV1rv~.O4Ƭ ż]WwC9A'llZl C'I%ViګI7&L;r; S3qBH k Oy[ŶUW/<Ve~ݥG=+֤d7.5lvy[J3b4Y`Ir0O*j4V~SϼHWB!BuQڌ}>_ohb (B`S㸴)<.z9y笟ƴ3s [S&{rtiy"F/ыMN=A%}jޙ}f%{)>kea#dNhFz\Q܊95u^Vit& ?^Yukߪ7XghvϿ<݅ĺKd!Dȕ61i[Z5W/z׳/W9}Q»yjF ?5+(tA iH?ίx=<}xGj:ĩnTڶ7ڣӸc]m8*T8*x, 3g]V=VСRy:Aen_]qʓ_PPP~~~NN`??/@1Tevd[ɾǬ[ l~ײѨ/X3Pc~ Jr7wW+ ^_aN6uz5|<]BC~RqSGZvr 7s5KBɺNO~ kUvq2үlXdlġ;[сG6TIٯswRCXHA%zZ!t+^d쥷lc&&tBpak nvR{nwI&Bk5:Id_F~'}jJrRRѨHLJI/cL*<;w ޱAkKoTznѵ-OlQMܤnkWv07j3w.nxipR#k7愉?Zt@=zl6Bo7@3nzwH!' u͞[wdӚaFG>I3mdYɡ3}ȇ֯1D|Q!+7lVKw,PψN`ޞxB?ZɶFᯌ!SN}~®ď&a^~c=לQ49?3 QsOkѬ/lwB[8ߤ]ٙ]csڰ?7y~i⇖id!9vƋ~sLz21_}o~;%'rNjXϬ}G ٶzYc(r|Ð.}㙲"4RUkդέr@~S19|!#xEcۿ3d!!>T|m"vx^]g)AϢ0 IQ³;%NC9%4|!.E ~g S?u^>U?5.R{x]z|BMrlߢvb,Fm[}c(B\Ty.y_R`ô/ߪKqO.A} 1azNᅳ'YpVyO/D*z%- U8otkdmdxٞ>ϤeZ*n8Uw)zopk7xH̓KNB̋~j,CnXw1DgwhN4{mK`ṱd7sܵ/M!$-d䘹6zfJ=a̜{Eg蔖"L(f|}wEV^ؾV';! !O7^yh'z46}~D7ThX{Ld'$kgeMj]WwjƸmF ̑6j6'+-4 U7 !;Zz姄Xs/:2~[q_.%FZ5z:u5&]VZf?zD3b+r@3ŠGuBiܼqFa![4,/.rLnf>2$ۼ= y~5;π.72Žt/:} &ow\7܏֮YsחƎWo*T8*}3/GPp߁"0EӨo"5)49S2T%J]tUT3T2jpF^f3˿ncU>`|luz7[)Ƅcy㝉L&P$N^cκ:Buض9?Wٸ73b?#bu^V GWLtDŽU)k ͫImmhͥg*WUSqiGWFx-?<|3FPQ}m숌4 ɺNO~ kUvq2үlXBgkyi7ް~ՂkKջ<깶mmVZ??|s/&79:*ꞷb69ʹIݚ׮`nf$];^uߏ(B!go/JfGسE59=w\L)CzjL ;eՖ Yg^<~odݓ${|ꍂtqY[348ySkćuW/\lB\rf/=} g3971_v|/{j;w+>+>̎DhWJ/ۆ-Y:sF?gۮe|>'hgWdbcl 8]5wg{kF sIDAT>#:u[ r\Wνm˿8OC(Z=B)o`cBRHRPg_).]cPY9$B6{VoݑMki5b$ʹe;t긶[t.^rp|\fK8%>xoISۻ[&&3\>oʁf]F!dCN߲YzNnZ)Z:> ye5AIz}a q!٬S߼l8cab%eSSNSعVy,_s.GҰЗfζl+Z!~bwǓ3m_?E3uC241lؒ}߳KuR ~߇*Mml¼rf{}7^Ry{;K ҳ:3XJm|gG>s_aHJ־LYjRNNv6";JF!DH(KQ͏ugDU`ô/C_*ÏHͶؘKAr0{o|' ŠhuEwcA+9sɖpBmA8: cnJ\4y1CSK|1k e:^^TtrJhB]-0S 0kzuerc+U)Dɓ/uÖ̪tu#6zdޣsU؄˜wRV WҢ_7{)~-?u"BEjU"I.xEma\m{`UI6M$I^=͒t\TSv oo*URB~[w1|[K9 ۻc;t{RXʽw<|I/ 4{j.֒&Skn.%%!DVK˥mتƯ~Fy_3TUY5lm$q!Essr%q@i4:dc%b@uܴܲ$I2&^hwN6!.lѾSN}ْmd_*MNRDJ( !xX60 ( :M;b5zȁ~曃.NpPk3Fnݸ, &GatoT2ߪ45R6lCCN\%(B>mQ?h75K߰K[)p d #֪nV쭴)Q1 e! oT7K-6!uW|4y/&K唰4EvmQ._'YXy CpKF7Jƍ*#¢MMã.3 93#Sr/˗T^_0X/E]<܋ٺJ"b B2:SoDF^yWeb}˪41R7l9?Zsfl.Ve.u.u5)#b 쿁1wni{~'6zǾsl}d!xYlکn-'..C_*gժdIM%޵{˜9= )-]EKߓ#5 tTG;iH;ǁ>ޛm'b}{%ԓy!。~:^rmԳH(u '(hSYwMF+L_Ɓ3Q34d&۽S51 8*5h޸saƠžRP!3O.9* a {w'|<ɪֽejc+Uijeٰu 'T'4ٟ~{hރ=UYfBkYaJM; (}hυg}~+:ꯟ|]&L,ZGir}ڐX%nt9eG?c1/;-ʩ/0~[q_.%F| V34+=M`hot7K.;!,`ˢ_~9oL_WsmBU+˒ƸmF ̑6j6'+-4 AS*yc*ML1ſ}:Wg9yثVjKݰMof5V1tE*=5 +<Ǡ5!-Υ#3|︢Xe֡9S}p?WEEG|v͚4vܸ>o_z;n*R8ѻ]|>@OdA< S~Ǯ,NSE>P!(@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@  P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( PenaQSK++EAAt,-]==MLMuy$Cu\%"\NQ0/AA HkߦW& ~茤 }  H0 EQD-s0ѾmR${V' 畊D"?&M%%DSA M8BaY݁  H=cj,(AAzByɤAAW^@AE  ]u  "\,PAZ]2JO[޾5WAsY2MPAAj3d+9OE"g\\^[XZXO kENmL_GH6_ ,IcMuܩQL 󱅘4sϯYfhi(0c - i=r ^yqHߖU{ 8x]Ûu0g7C}\մF4tr֫R;N7;i=Xs݇\O3ʦP`=ğI/c*nhZ`= jJҹ7/?ۤgGC6^D :':}>5k?(0qVJ"PJs܌~ o|;t-:Ԗ {Ǟ+ᐄV;bm#5$8$DoۺuMgx{ A,ާx(:]p~ <)d@θ~%B.}ml ߲RR9ik֌!^͛547_6%3m S!tjڴ>-%VR/oeg%ҸqǑkmo&jڀ|$UHc/{v0Hz|!0gӜB*E+i0=)9))%KL{w~ͩ2&}f㏬ZƵxgfu5kTɃrtu #Bϵ.AB2IsJH38 iٵLT(Y:wzQM޹h`|kkgZI,شLO.mZ6g|w\B[q~w!aߺ!UN w\tb`3  !TiK{F~dzu?ab B.X0XE}bŷ!A%_: ޾'f8 yǧsIS*%ҢѮLӧ 8i[foK+w! B. ߤMy_3tR´  @tݞgefcT5 Gi;JbtЍTp -'9(}t'옘|xf?gϳ+Mz/W/^rbY'H=p(:3YO^z.obis.8,'z?{㋽űoCB,q;& :.=u-OA*%)E5ǖug1 !m[V,8$使6XN%Q$cT܏m׫P,(@>]Pv+$&a-丰a )}SSb` o(~c4;+4q+n FtoyCJB c$7)"`XJoݗ"2%\nYZ٭3fUP}*&Ԙ໑9C):,*-*Ca?j Qo6M x3p0GB6o]'? O6>:]m3H? 1S̰Wûx Aineʟ]V!F!b& {g`[@Af%׫DeҴ"&]ڜ8ѰgǚGyXxZWҾ,nV6NS3E튓=-[L:}Ѧ]N}3gy|cX /zqG=%DZD`8'St2KqQ#U;ܧy ;7*X26%ND}/Uby4  VowR wnAJP/*0@\꿹}6+8+W53im[tj[(8[x4xVcWX tOk'[^_4 .l}~s-/:0Lp^cU_f+mygU[!@+I]'#ͭVdžH- _ePQ*qѠbjqǟm':nfQћzEVa;m3}t85#6Tھ4ŵ͕߬SleYWV L*s( lhei{I祟iv?MiIیic+\v=4C=l ӽIz3n&uޏf-B|n88q/Vf_){t&cٟ ot;Y}Z]gE'$>BweنҥWZzׂ [)u{4-8]eиbRk4`QTt曺45 f@K_衳owʤo9ͥ_8A^i1+j{G /eDs/J 9N4*Z:y 9>d,`%~]fdӛA)s*x@7kodFedcб5 k(.(Z~o^> h @~/F,|A+]sdUn܋oH7C]~NM/S&{QGtL*rњsNK\$~w|s7sgӫv}6bƱTi"߻ptڷTRA4hd%.RAݹV7`RF:|'p͟s~:j.J.(yHgSt!FE2nQ֌?ѵ(5vFߴ'( *jGiέl}1o*>3}E R?-^Eƿ) DHo _04(0`<S~NEgߴ@8Ư} sN:w-..sŌProq0Y'Qt5]'wf=KW6o5A}w&.4]co3p_lNM;1I.e$aw^rzѯS}RR 6_X r2өv[RmO/-+1hF꠸XZ")s^6tTTP{4 )|'B<`w &REHd&kS a!REu+%t\QMHu-B|88|CaEݘл9zM̘KKW $.>b8|xy2UeL:<15{ݭexr`?yT<ΛlxѹQQ'ӡ-gkWaw}Z^0m}h7߸w&kb8f0MĀ׊N<0={g+^KG.z[ј{.?;/|/[ %Տl}YFrSэw#v[$njP?~04:?>WzgL l-hq ־VRaknMnϞ6 oPV9N57?fݿPE5K_s،[ y QZN<ͥ4M]ŦCv8- iLr}"L݄P 5}!i7d^/voxe|x\F*Knw~ל`6L`mv\EqV죳L26 %.*)ޝu7SХKB /fQ̰| @ -[x̗QNjr/KU 9⻑^6fF:LYNӠ\*ki.k^f& {TFU_}3ݬe91QM;'\gEiWJ-@ubyoc{O$H~cG-)Yzcw|uدC/^!ذ(SXP4 q([$ 0f0vێ~" 50\Z2noۉnq k314Ck'?b>׽މbM'[_5#T(S5;7OҥA$ީ䉤߾qSIUmڨBTSA]ΔJeL(7mN4)Oc<[ 4(2*݉l@l칗-ںxnR5w9t+EKka.k^ /MxFU_<&.96~<ӛ>m|GjYBy];9z7;bSgvnkzRF a=uɅSQij[V.c@V.N IgF}&J{ڃ冘3ˀS@h%ёqbb#k8g2Zy/f@3ԡ]l`˸^6ig:L߃5Vn,}#woܐD5ڤg qZL*ڠXoxr7bܔ,/>2Gi6Xrovpq1R/j}MaqfRSf a{&|GR<_P̔Sὓh+RĔY+Xǔ>=u) -3kO^ C\6h4ytQ!첃6ܯ|SiȊ_DXkYtZB Ok[EEcg'^N }T[>p|dPtL9wO|"xw[=#MI"ң XlhI&=J@ǭמȤ+,TqNҾj .mKL+O6o}qҥ#r76ÃcORU@53#կ9I)aCǤ!/O'(:vtEA9a`o%jԠ:VbȳY'gVΚ;d݆k`W$(`\V_(sfRS& VY(fV 鋣k ګB]FR|t<{n9ozNYK^p0$Qm*7 J =wn&=6^K"1>C0:Y0uĊ˱jb??vhPs2(J ңeV[S֨<5WV{(T҅?O)v'!(6'CB艾GQ軏ݼځJ_m| ;PBU:s{/:r,6N[?2Gz)P[;X)ғ>)JҮnރO)MU6?2ee (2u(ܴR JʙjDu?dA`vCey \6݋T@!>vOn{-~;kN@GP=w9!:/e &P:g^,hc3EIς>Oi7d^/voxe|x\0t9oG7[scS^}|3Tp[M߳Ȍ ޾Z{%[Us،[ y QZN<}U64]ss$R؆'L BIcl̚ϣŅw;Tn@Ew*Ey XWξ#t0O/:l#H/{3#,'i?G.Df}wkXrbr!fw_w66*b=xfƵakWbcsTs$?-50l*a =-ҫUM=Fo(KX2[r<9pL[Ӫ#'>8D4YDTtzlMUٸ7nndD^?? $p~}ˍGZ@a F|1cSqV jw-wnP;ֶZ؛V54ċ%>ɬ09{OOmuow"XTY"':T9:uFQȹ!g-Xw9h~} 4\w.,B"R$egK$7#=je#~8Ц 9 if!&{Z'\[`:Ƴ4h շ+K &f&$!ek0y4t^l@dێq<D`#EҮ}\+fJ m,#FP' 6R( -Dz7;bSgvnkzR>fL~|hs@TD2@s17 rGyD_eiD@eE>ȪgB\g'3"6[Å\CEllDli uU/ 䐆R`q`/3KwMAWŵ&W7DNݫ&љw;Ƕ_!MGܨvےMR۞܍7C; 9|7;y()p5ݎz*32*wX 9 |-S%667q֞Ʒ$¢\dD9RŽoDlиw3VIbsm =1 ޕPqkׂgZyu4hS۶Fؖڧ4%)sH`%(&c^s@ yu?As:d~!7SFF贄nvw8jO>\Z5Mf oAѹ2f(ڥvlW:B_{""rK oG}KH4Q(*=!Eѱ@ߤ/ {3{(MO+ }zRZfd TyHC6x¿N3&zIswTlTO5M9w <*ߨX]onOW5]YYmq#Isra1EIlv/Kf2ۧ(_XZvwI5?uaezř]chZ@a7ܢ#vM@@"V}{whhO=VC1_<i $" ^NYERȶ@ҧ#8kнJ±⾖t~܃6A?#(lzM`5psC.-)J}cC"5 ;AA*xHt  ,PAB艾OO! Y(`إ_wsM<] ֭ Smoh֠껏ݼځ.A -i{o,UxC ] [KCMS'e RpHڱ(JT@AɫmBIcl̚ϣŅw;}wP60C_z吊z= Lpku_ep㾫v|Ӱ 0%׏yׄ_w66*b=xfญ}_'چi{~0פ/9⻑^6fF:LYNӠ\*C;46țxK =Vop< ٪1S,jޮҠjBezXۥ½׬3ooOE v7v|QeFAAA$q:rl ɒ&6VWV,-ylo3%6^c<.,(d KSB- P\j) (Gfg3StwDRou)*;z^PKksJVj.vŁrztpCRC,>ut^޹=]|G 3Spu`zއ IDATհyC}E>cK#x`òUA{)~ۂ=RŌ>5mj<ڥgĭ$8aWɹǢlA:U"AIFtdt'B~ayG@| amMo\*v-xvW'N?mkbsnt!Z(J(`;ĶixX5ie4wDfHͦiH ,{Z2*m$Ű#[ kU{ *~/!6ʿKPR.h[(L|KJe4sia-+u4u '_NmGvg,*%.QձNx{Cl 8-Ml6WM7w׵,xZư.A'ܱ3Ris;E\&' B }u9@CFb 5U7$EWWwkt 86䄴\нrFȘZͽ:fwI>O5g^?]еo 2ҕh1 MkOYDdRng`ʕgK)wb-7<{Y51+yzY.r4;-!ۧݭ"豳ړ/JOHQt47ꋂrތ*"v84qLvkkѵplݪz  x9K: yS.$RQ?a^X+iAAA@Q\ 0h;D}O<گ3Ev,JR'/AbHRæ! RKW {0%׏ɧϲS=yR~~پ5GI 3_wu,;kN G0k Sw9ԧ)L|z ^l=1@M0F pH{@{%[ S=`kn,0aʋb @;NYukc8+كn&K9⻑^6fF:LYNӠ\*5v |Z3<ʑ9=d.2L葂/fA4tY%% LljXS5;7O4ubyoc{O$H~cG-)YzSp/kNrSI^%V*cTSXP4 q([$ 0ؽo;2$r+J $ީ䉤߾qSIUںxnR5w9t+EK5t `tf'a24α=Wn,? Hɂ$#:2:NL@ldU3J% FQ ރ,ۚ޸0w8;{R⶞ߟ+W `bfB"[檉 GgjPY*=Ɍͮ؈aC#"az!b4s `Ry{A#!R#{ '.:&;eHGiTϐ?C^,.mnJB>3;oL6~$WRB Td"Vv\DQ\@PgQ7QAdZd+ m)Ԯi閴MrEٚ6-Ir$M53?uu- ܥ=4*MhGY^~c'6T§Pntp$!o..h\X7xS+O"b*?|>9z} 7?bRl9WѤ$I}+ʲ\gʢω{審6wujA=}u3u6C}SūI#:]㲫TnwM?6d!E$[[S+ Wkg\g7-ZiC5Hj$Gg+Ynl_}INr<2艹kx^cr`A<*ʽ{yGʡbRӷDU Y6n?z.]z!={2/dDFmFĖңg}O'dV켺zir+) c@$U^ݦ]f1w?/ydbQfޕ'NB8>'WN/|yL?ꧧ.hNҴPbeI3WXiZQ?_(xk uåѧÝ/^B[!g@5 7\U9Iѧ,bvJI762lKU+\g3vcUEij\ϓ&3}|zCl,Z0Qܾx3̱7WNׯbgk\uӳff n9 hN.o~dh2Π6 ɩf Rh_L|""č\n w=@q k٥phڷUe.A=4,اE9;4 js^_&\t5rMYҦcEZ[Ȥp3u 7}dYy۩xnvfcEiNo}gx^a(lEfU/!뮈铇(G7LqnR_,xQIAϾƻ\_dj! m,۶(V%W=o|߽ߦOm=]%7\*]漄yi{K={KMұ4B@ ]8K҄x0z;> {IB)PwtRO.ꀠ.ns>Z79FR 3Jײ\FU6?}Wf.^z>Cbڲmo}TA]cu YSt01̹ t[jjtq Eºśl;6Z{QӇNUt?xp'ug 7`XJwq̫. IBȭ~l[^ r>:뙺}ɾRK@Y&lb*BRWr=Hd)طrٮvTYdZW[v;wPq#&n]srSoz5ez//Z-fs3ߔwBj\ےEiP^E:/Gj[pڶ wV?%ߑ^.aqU*&ٚ͟hSw PrܻǘR%eU_z0$jz[>o֦QiaشRkZ4Q={{w6;¶Ѽ5r䰠 /)glD){WmB; nkL5\ԶgOsVlOmbM͹e>|>㷭{-?ؚzkgܸhEq+=[pα aѥi51f:粇q٨S=4b°r_UzZǀȉIxMG̨cZ#~~hqwIN|$q;Vm%,\#괶diŏv-aDDHm|BFaΫ{.Дa(lKi͜^ &d_'eoaAjQm#ۢ~}KΖ: 0??#MxU99\J30n|#-m>6l_=u'g]y@n쥴~kcޜ5 ɹFj=R868WEJO1uL>]-d}$SJ]o 5C9UUe8ӆ?(Be(Jqm0[wM>G]ԦùMF/|tiѤ# J-:\D)}fAm<1m]Dme6_Qa(lKi M3y:x}gӉ ~+p#5-~r@:ɩf R(7C۠о]ݯ܌K} Q Zo![o˃oV56 }]<<7Ӄrfݠ\qE/Xo7[Ž^֍ fZ a*U F!XPΔQÒ`ɩxnvfcEiNo}WjG 2NGoZlcԪB<7!\ٟMcﭙ,W$[N]Ac3KYcK>cFB&͝8 IKؿ' /뮈铇(G7!y gC+Ty[QS';\~ukw_RZ9d׆o]Ts>9c\FL=w\ScQX/-dSuj/Wvî2u hZJB&D Ũ/8êwegD8VWrx7,h:WEt~!(NUm:3^|Q<}#?ڨ8y_J.)[rtױ>U!.eoԺl sPBȕ~zHoܺ?g^6f_ljOazZqBx5m܋ϕy!zM[AOM-&|368*}㑫/TCФ%ۿq4{=gX/ERZEiI& Kk=EifE!yuڵs f+ҵ^g+֜1qkKW>ޝmloٸG8Wdk9n&TY}[rc~=S+DέϪ}ܢBs^Ѽi{K={Krc7S U?¢B$h{|os`ޑiBع l8,ŹB24D<~L3C"U !T,[]JA6mT~г?ᅥ?n#Zh5BũqjH|gl䠝)`M蹫-MiX(̼D6֦W.?} ٱ@Vٻ!lJn@ï\*|ÇĜɩuQΚyX8iH׎꽥Vذ/0Hn(Py 2;|\$CA*h(%7WIU!) IDATA]co++mGf[jfKpy.}\QiBC<|=hFSˤK(_/ʳ!$ oKcEלP+ͼ-<֧W.5&g{W[>u?&+Hn@u9V/8aGLJݺ-Z*Mb*IBwWKؼ~y\vIz6[ .PV D5 I,V.ەu)T6Um\JK.WӮs_۷+m3#eې_jhlضJ۲B ^8ASy+b1/lOQ!-羊WLό噱{֯|cޢuY]6$YaE; nkLk9:ٹ"Y6n?z.]z!Nn $ G'ˮg~su9r۠ /~aqV..M]6%پ0|}R*=ot=<#OQ1)E[ͪ[^cl[s͛i`[rqZZ{U͟+O\u*PCK GO_1G&wsA=JW@/M]nA!^]piȄqo)m«5sA1 rb*^n.3Vʜ5 ɹFj P+Q 1ҸjW_WYa?1Q'#:-[O}{ R¦>54yFLV[J>_r~ _.;Nk+OF.R.M  V_>4c^ձ=Dc1rrgn7`nG>8ZҔ͜jh[?|{r[qDNWUgVyNXƦ.9**m3tfͩ+OG̝9W,?mHj;Oij'j+ ߯Puo-||s 6n۲ MYé6E*CQJgksZ:cٯMw*JS/E7S9y;‘ikdK.]GLyq P!˖&jh/YcZ08mYJ%Yhđcg p %~H¬J񔞚c}Z"6XIh>z&3}|zCl2llaے^3a}τ{8H5_UgB>,yl;¡ONVW-͹aalbGM7'loh;SV^~l\95>\9]AkRuX!{uz5kY4]װQuYyeFc"W\_OS.~Ns鏶9zwu#Pn3{ރ 㢱ʲ}Φn(u_4Rrw:xd ԔxTL(@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ ( P@(@ P@(@ P@@ P@@ @@ ( ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ @ ( n:Gٙ@7Y/ߞ=zfBnEn,[<C[;Ƃ&IENDB`brick-1.9/docs/programs-screenshots/brick-theme-demo.png0000644000000000000000000003216507346545000021617 0ustar0000000000000000PNG  IHDRͮsBITOtEXtSoftwareShutterc IDATxw|enz'PCE " p˝ޡy+v=?˝zy(EP8ҤH#&wg~ )HD?۞NoRUUW>XYSS{3-b{ݧwsiox]صz~al/p(bs <^7.p:l#pNg܏EsU6}E^*NUTTh:F펈"BvC }l J.q!@ hmLlв,FYJ9{Cb]rCgo>_|Z6TQUJ=٠͈͊!.3)37ڴXiu|*=/(P]r)e=kSBS =ѕԈ.CF/(iQcz_4ic'wXԠUz0se;.MsB@Uصf-i4en^ĭ];<S<薏VY |g>x٘M\>[B`2Wzp hQYꋏ}V>5'#S+ySǨ;k,_`jv7WMn/7!Pڦ!.Vli t^n\ Ҫ}9ыX?-35-6G, L θ1+[ohϛ1ql2tHU/^|{Z/e|?):U4MU#Yؼu rw~gͼ#g\0tD׋Y1+1(8J燫ze;DD Ky鵯s"jw2㼾ۅjʬ>tKƆ'JDKn1:Fw~]ֻs_}?}Iz6m"ޭ/^r?3B%{Vy啹>dbÝ7M#V)qG}QѺ]umJ^oƑ9{2wSs-P4߈zf>!&gD|>}N8@ecܕv{eߡ]USmqzT69LF^:vPxU0m1fA}nz{v&5^ ?|CzaE='k]UXdvtm*@aD'3得OwWjT#; Ѵ(}SahFbxjzD7 z\xsޜK^a5FOz~莪 SR)C tۯE|Zwxǡ=O87vXԜGyw"4 1|O>}(V0IhL?@Y>c^WGϢ~z˸LqPcM?˺=;޸)S.v%KY}F;'_Wb=yK4Ԉ!45ܯ3ΏPyn4~_94xÈt݌d_\x˟_o3{]}П55'' 5/ja05gnjBO95x5 럏W'4y {P\D7$Hg-_[~WYÂ:wI$瘷N^O,&GA^/; 3qƉnˏ?zzgu!"](i}{(ﶹn;>AٳB%{:FF4z O3ɫ:|Ӫ-VC9zܖԇѽOOW['dֳO'm.fEϙyS 㗱03=5h t}`F1j߾K1͊zS+F41NtF^RM}_nxO_M Q9(*&b36z[~.<"Am":81Mʉ) h1tQ,I]ca"Ss-{bwr##ǣWSMKu͓sr]͸۽lŁU̘$zeM5b䭋zvVxImcq='9>%,d ` ۇ!ǝdnG?wen|Y`K_g9U;psaJtk: 6Q5)Qij9vsh+J{9]q \Oب2-zJ+x5͗M;|v<4AA㳗uy%lu6]Lό!n R.ZWjBs{rkݟzMh}vp>}|:(kecn%Uh9;З 5|CnW=__a2۹0,;F>i!kߋ!"7}vuL]>^;pK7yr‹'0-Jl:/{<&"!a,v]h\8jL/^ Q"F0Bc䡬ɗ1_#=7pc?ED5O_ouWYՀ#`xAO.J{kMSў^I& pZgADFи-"YP6ӧtoYΡC_ޗ;/"BĆM󼺈9hKE]K=qmr^|Ό?90:2`ڌ 8=oD3l6d'T3Y/aW}66݈>ŭ<%ɵN8~oB8 O֣&'1{Ё L?O*sIɈi1mr4 6|fɭ}b*i +UVQě[z"o[+|OԖ:_;ĴPQtNKu%zp>CDAw 1(W\8~ZO:G_6طxvDk6 4͵7icPHH~՚쟜K\n༢wVzD""yqlcVA!ay'-(XsGW--2\Dx7L K~Sm_Vڃ' 4}n,n}G~ۛ}fmtvWe'1e>"/"+OʲO+hG_\uE;=""ɿO7lNn@A[t= }Tic><{|:jCSEDR7!>CTQEE yã*E]xE PTEë{ QU.^;:"':>o]C(fsϗm~{췹gN, 5{W{{BK=꿷_tFWqOD$0mx QGWklrf;/ujBCᢘ'\#ruy¡c6+w[Yg9P܇}*pE߹N|z@ ۷|t;[.lH?fͺcMUuw]^×ޖ~ts-*W\#s}{p7cټuŭ=|lFmatQ63DĔ6"YqwP)S,=*3#f >pfpQeWVfۿ c|䑈 mĘPE"isWvp U)o`/"z*W_h)νqw!hÐϧe}urehG:9kMEWs_Kw5<7*Z "FAoz$?~ ߻#uU)PhDvR̫uqy\b7}z1o}m\bS7T߳eoha ]sC¢|`n7y<#tx`2^}ҹJ5O$[|W~9YgT/ؓ|3X)~YV*gDt˪l1mn4 G "{-v>m'K/@K7;e=\Q-ߩ-Ⓢ m)7ώDǙK#jvdq""ۛ滬G^6C|(=,uʑFN2E9Wz^ݧznT;׬NK_{¼-nƕQ\_TQ^y#O"ozvcz?v)?xi-ʆf˄gn519~zjǼyq>61bv_N{$JqUz2V-pGd8^Lqqi{69O~|0&JӫW9JSߢޚaB}G~}fB_?OƋtX|YS{n+B^KyBίʼ]nX`+ I3k{>hԼ}B^5H)60ןW9N=;͌ ҂=\akf1LZ=<_^3p}QMN˯g g1Zhlp&4y 5N^3huM6n4SRiΦ͛7޽[D<Ͼt2thW8;gggFFƑ(ۏwE֋@ ( P( P@(@ P@(@ P@(@ P@@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( ( P@( P@(@ P@(@ P@@ P@@ @@ ( ( P@ @@ @ ( P( P@( P@(@ P@(@ P@@ P@@ @ ( =t  Pl9SYE`Jt@Gh]{ƌjoZ8&6mtZUw}a3Z_i(KʆfN~Jf8  4y&q[Zm%*I=rr\pJX%5M=}d73D, 6 w[.7LmPsb'`*6@-2ܞ2[$)i7tKU`tИ!)A aE2m[EK?oh(Ͱo+tPOXGL=~ku;=MB\]I:\u]NjaIDAT^EDfg=k}" ohJXj׬/Ym2w\CK_e;rVsĭ7%f|XyϬ>7#6O3G(JT詃#&D Û൭uhJM ŷFox ,5.掋ۥE^c[ީ[h튂F[#ӕ?izA(h(f*t1LT{tyR=}#:dkkR=WU)>(ֽf4|Rc4Wݱhn긘&Yu=Fvi] i+iO~/ Z5bM4SlX} S #G iIP"Ӓ:1Lϭ\ȮB,AVQZ2: ~rVht eFy/KMܢ䙣m-Yl 4ck@A[LIb&vUKw}y2fWsk>ey׊ )vf ]YiHX&.OF}SD$+9KV Ĩy*_ ϒ1[ݺ|)!KoTZbTŖE1GFЫܢmޓhF=eUyۏɃ ٵ ̄Lv35|#MD _Qs:~=,f[lM,XS߼;YQzNf-t\;(HE* cS|]hfm P6rҙuм=u { JҪ;;ޚ@sE JNs]#T‚JF[E/OWR5E|z#T^6Gw!"!4x ~yϮqy/KY4oYy"RUUQםÇui%o.,j⤳׺WJ$ѩa,JUM~Ɍiet],l?F1b4w=(h3 `u/w]cՊJw?V#@u_~v%Sr0݌>9'|y^Q gߜb{;2Waso8ԁQ?Z$DN`*w;o`(ZpFh:s-,Zi[DWxJFėSxk{NR?OxrԴ݃y{P_LحE;=jt5"\rMDxky~%*)(J5J6rR UEs١6@ <ڠ!M5%+3\z1 m?WR{MonjQݹE6,j7vS6ow׻czAG=oCS2N=o xe{] 0,5hѭe—1"4j D ${\ޒZn2O)E3!zJ#Xl5K,o򥿑s=]DUu_Z -E bǎN*>P*vL9iltY<ρ=śS g-1w.6PF$?K {U'=32Pvx6 5 N^}/޻wgͪwhm@(@ P'.}1B V];v :ˣLkϘQZk'S!3_Ԇ6A45-E~kǝe_lwZװ|՝U^ET)($>ܬǗ}qhFMmې) JDhFUyzA@A[[Kʞ[]ѴGv`>0{zMOBmy65(.H{*ϋwz8aȮE1DDBTOt-o~ru/ SDd{M,j3$)b-]dI k`Jqo-w~/1GzS}aƇeJ꣉x3o[l4sDu:8ob@p9[ ^ZW'oJ J<[|k41GQb]Z:띺߮(ht5=+gԍC,"5F>@AlODQE%8dԀ0cWo)\|vpZ#Pц4$F(iV.]QZdM! FkKZ?Yi;[7<Y68/5% sҒgn%8Kod5uCAF dJ3Z˓q0cٕ)ͻVd}|]Y=âvWV˓g?zFNjvwPJQG`jwm'FSTytJhĤ~EvMQ]'X_W60xS*.R+gu9R5"^UlE 02<(/o ԫj7dzD+^3w0-5|#MD _Qs:~=,f[lz.vTho<mk&yw(f)|{kZ踴w/UQ͑T &lƽr݇l䤦i^K`)Y-&];luOzAIZUucVoM9ڢv T'z9.*aA]B%kc8rK@Ҷаsiӯu ^v!qI%?(S:Ybݓ!(YW,chbk\PB/ (x~UM8l{]T:;s殱jEb/?r)9nFҜ|u[lu_3oNK=DUKf_9D7NSb"'t0W7O@-y#4j9vIݲ[DWxJFėSxk{NR'nk'Fdu5mxt`4vmpO@]2i>ڽe^_J RׇFNj"e$4#lu/>&56_ <ڠ!M5%+3\z1 m?WR{Monrl)1̣ ׷աũUux~7kOZ=BۉsMTh iK(]Rj> 9܉6 /f׃k~ P_=s] 5qS\d1Jv!j֣_p,ڻ)C76vR#|a>{z1^ݒ>"c<lV˩UA!3:fiN6}~n~Jcڕflo=~FhZ&(/:ics/ m#]|ʖO"%"!C#տe P^򰀧wW#q!(Re<s+2})D<_Lѥu}>`>\5w;0eH/w^Iz3|j)&~kb$yg[?6O5Ǿ롛w!?T{bW,}[v7c--iweñ22t2vMk?~W>]b8`zvN6ɰE_umTK;ksɸҊóh?f=]uJ3CQE\ <F*eߡӉT+?tO[|LL /NZ\ ٚߣZI'1ϻU ]Ϛ5=kQqj# z+};khdžV&b"ţp2,v>bݳYR :ñ'&.&Py~ {םHNJtR  ']z4/Eȓѳ((,:~ [e݇m/&UY~BPS"r`sIzĖ*tm b=Z3;kҎ- v C.-Ŭf4gѾ#4._?wظvPYSnҸ߷MG;Sp}UvVZY;Vm Osrm)bXķuY̕U^cw60mWy:.+)7rMƓ>@Y*sE*Rg={x륟tW%V:0\g=p^& [V *)y89eq5u4ŝ8MDcdղea 8%5 bMQ]:DDzVM\cEXyX@6:qbibF|P̣Ӽy"gqH?}@M%y*,LJ˿ܯ{}zxMޕhK?l*^]5cipn.j=%oų 1ta񗯤U>| ~K2f=yY 4oJWǵCk]xnv-xlk\>zd ?IxV?D?wO/׋8}u3}5 >1|ذ6DO?͢34՘>ۻEWuWIR?5lYwJ@AMw.Rl[;~Ŋm u^\Ĭ{z&'%Re^x_M^ uYNrպ^>p8IiJ#v팩E'+tzpvxxhxxhx؅E[7{Hs*G~A-E #j3@Hhx] #mh#VrZwukժ WR\kްu[G>WvrXYU"TϸsCk+-v|8a+u#WtDIzslniSwch^M +8LTJkG&k1lރ,x&|m$V|Զzϻ.={.!X|"jC4;Gmic_ (?गOߚեG(&eęil ~2FdDl+5l?h(ې޶=ogQ˷Jm,V8"cދ8#"iRV~ҤFf6{Oخ>U6,@(`jR5WWFjG<"RTDD GZxejx|I)ףӤvMqu -yj5jo Ǫ5!Wjm5OPw6a葴zW^5WtkPf4ϯN]wRq=@{moW><T:ԧOrFƺ<*رHG|;ώiuG.(E,^zٶݔcB&oOPU{}OHG:,!M;ߊ z~%ČK)Hs.8uǍ/)EJnV%l~=ۻ@cdi,bԒ[zjߣڎlǣݾ.x5:2ﮦk'Ub/UZMزW]"uW.Ukϓ̩18>r z}{(hzBLHG%C[#^{TSi`g1,P?x5UҞe[#z◃{-8u?0?p}YǕ- -ޢ"";=cb&yz6>8.BQq~7o6o ͝m&HH:rjCӆJm*ȸR8POJYgyeu )Ԩϳ#/g$m'ox=֥###0Y_Sڼ0Lټ?b-RsJSl8<*jƞ^3l gc]nb"R [_k# S7n8qRjJБ2>mɧ3XS@@x5ڪs!c+lON4\X#cDDe$l;#rNՃ޽o [arЈeI'3Qˌ{?%IY#ey^тӵ*C9ȥLJ~"nOVt2Rho7oQZ.-us;]""г=N)1Se}m ^TzәcV#KʏbL8sEuhWSEFv׺c)=t~@-'wz{Olma^c>>d Zp_hc}o^~ e^λ4ԞhMXGqk$DSd5d]Dˉ&arMh|w0jچNѬC;R((u(1"tZm_? ;+ }&(9>eAhꨭaj""M`|`KQ/~P ϝۅ&>ecRwettx )O\Wkl-ɧ 8's6S;׭Rݾhz-Zҽ>gw\\H=Z _Zv3sX"bΆg}Koq2M7ւ~l!VCdx%akG]jVDxyTKTocRxS@#ʈ"b(EٖކZ6u+qgGz^;h!"I/tv]dp9;aݚjoN7,Uw0-Ȁ[2[[R.TӊR)C|.~gE'':vfAO6 9"F8[u@/3W&F{e5u!MbQH&iR]-۷ҥDpҖ#h܌Y$ѽ_;u(kSI, gP|A=*ǮnĆUAQQQ!e%ODۅV 6E-xMztHS >(^̢ (Et)oxٕtroXukUqΛR]:JQʢۺvIgO~VWj4i%W,=K͐S߆Z6GSJTܚ>j( ";LrN{&Ԅ7IdU~GݎT9F4gC}ލ&siy9GW;j[Ütܸ;iИ? y甛G4-ʢ\U[8:?x pJVbvu7dɖ1v\oodi󕿖)㸬iHkc`Qf.K1ͬ: ogf<{*1CW&~S-[rB!miA0~ڰr՚U"76t',I]njlŕ7~(wiɗv~DHDk/]$#eVwϧ?r_nEN<̓q2 5̛9l@SQ4tӰtH3hŖ#S 6lZ1d8wk@wk`8FNdjģgO Wbvv34}B-6<>xLEbo+Em'qV ݹ[dDBⳍ=+Vyu&LXCGeS^L>NC}nK +?s`~SN77dwv4e4Fh&a#ZGpW#6tM=C F[߅~yj98t['9#056I/EAk-x6ZޱF>x^Z7A7 q$Yh4nތ 5(MDJ2ʯ()HJL|׸?#Giiӿ+(B@.ڿ?=y xPPP(]nkxջ1l;Ƹ Q x3;k'N|J@@7swUM6ͭ^Ɂkv}E96M[tcK>k-tj׶zsr[C;pz bQ_^>e< ufƇ?tCiޒN>7Ng9ye¬Nqjt-/l IDATąleThoN2Hf"K;+_J7u(Jt?#ޣo,Za :[҉ܴRbJ ˟;4nsz^Z Vc7>LIQ%'$RM_tD#v3b@ggG; #}0f?`1l=ֈ+H;sbkޘqwjfc(( 2n;:&i?bٲ?}&"aYۿ05c\1t.Ml tİsw~N˧'Ο.D_.Ίyt΋c7X,TҢ.~ ,YԠȏwoe[#Q^}U|sc?+L8o{R NmiK?.eBɪXeҶ/z~tr҃e\;c}ίNϟ'ICDnƯd lok'kf'v|}""u_枚4d!DD\SƯVykVBLJ4k3x^mr Ynzܭ,$p2p>Wua϶R U)˩WʫQXbx CDĘ]رcof\#7\H`~?ŽF.>Fϝ.Xv!#~sپ"cu]ܷn{khz*=4nڛpVpƆ FVf,[421*kZ&X>hzT,/++lۺ%eD|V-IJ4ͫNTk34 /\A_e%,W^QM4:zZ*_|c<"V:"2. t-ʿwdAcF̘V83+W(*0Bv:o)SCoQW[=X2+E%Jw`|Z $#&*&%'Mj/>JDWEG>4˱udEȪ1顎͙yqJ(m] OeȼOW݋?ݍb[oeRgIkc9C_m>!&[1lR2571.WM=cYSsS.7;ݤukvxis ZYIy;[QĭY=,[:[c%^u"Pʲ3J KԜ$7#=IoDRybˤeefCڦfч"f^y]%Qtq\ WSlı֥{3 CKWWi{SS]M;<ڇ&ŒGRΡ׾dvB_r y.eˠN7[3{~paKq#Bȋ"#de({CdxyW5]Ϊ|3dFEuoڐ~NVBOlؾ'hfrIne\-ްzr}e4o;zl\?huD*TuloK'ڌ_^NMҫiu~Ȳs䌽!CE+W∊R|=L,+$0lN'RpN]!"Re\0'X.< =/5/OVrf@#)cZs'go(RH=G ۽̴Lպq[Gujgdgue<dZui#$+G?-l\q襰q_tbw:ޭ9{׽vlF=vZkzu|h]U?++JP34-5-=~oDIBܩNnٲ'ךiٵnsw6`t/k>j~)q6uC1-[ZhR1*oȍɒɹ*~lBWi$ )jV.6ɋob~!(-ȼ{-cpk|Ij [_h׫h43goLtcfEWȯ$s)gc&|فΙ:kf_+~͚:x-i}0sKpDlZDпաBq_t⅜I'/u+j*o?_̚]˵lDD>ç#n`㑄N(̥ QpNU^mUNDn17)6> zgkzBgD"/TMՠ}9Vߪeo'>ֳjWz% ?\b,Ywoc'O<_hw%uEi۸x>~=\-/)Hxj\Qi'D6nZ`7yfZq}|3@*J dy_~dc|@W\TЋJ%G?5d;W8-8$'fƜ(#Bυkxm^aY-X0vG)FԾU}օE~ +yN/wl1|W)M:x-4𽿝45y`$lAUع m^6Jk7V4Ɵ S,_g^#.<> Xyaz\? Pqy} uxW5룿Pxyo)Ͽ; PPUayۉRk"tj׶6lkv}E~^b{XP\ƭ&!eS{ʛߨwՇ+}sҫ#60{Wbe[{ yiĔs1ՄK$jYܻ׺! ԃ&_k:[츄%4!=4nڛpg~sپ"ISײ4dSc'2eJ"(-ȼ{-cpk|Ijٳ!lB0]vm5d[л1! 'E"7ËM H4{ްYST3f}nq22*%D-4kh&Peˉ<6BRJYו[K,cEL,;CJl&ضc4Rt.ZhZ)WF=)'t0Jt>RRʱC[.;q4Դ,Ngɒ狀7@g.| Oײe>w3NaˤeeaB#"DQ}uۺʒp˻[fq#Dq̂OKJ(ǜM:*x d9 Ѐ!pꦝSɶ0㑴lmtp7w7GRj 85ñ,a6'x݊ONp꒼ĕISI1ٺkjqbK[Pr%Gb1CzD$!YөNnHߤ"x<<Pi 5zw3UhWpЩ  iD&%aAZ ce!+g,4|o|" @ur%Gb1CzD$!YөNnHːnݏn+5prk+>'C:wEFԏ~cK2ـSHRxZ$%e4+vTC拏EH4"[󒰠\mQK37)pLi)M(,嫟ą+7*~Ӥzgœ^*iRKKgɒ @͸<upH";g7GřkËɤa3}dK$r[0Ir#qN,WGD12U&y25P&l4 6/.4"RI%w^7,{eW8"J7N,TkOT5 ?qcY1 0lN'R'%y+_޾nx\ɑ@̐QE#IHVtj|eVwEFVy8)$)Y}F-[L2\;t!G"$EyIXPDt#tR0ӵjkh! >wmOBF*Pդzgœ^*iRKKgɒj05"K;+m;F׶ 5NdWxi]ZRʗ%lԯ8f_p~狀7@g.i(}]f2L&:kwpc(Ue2bZF4ؼЈH%QDydd9eN IDAT\)(Q"p8{R-i[wȴ"M'I_pPEpcohTWl8u)Sɶ0㑴3ft #e?lۭ[k[5ďzap,KB%YԺU #߮-,> 'x]oѰQ#Ҽʡ N!I 3w~h؊|fڡS />!)҈LlK‚"rf p򐢃wJlzgqvS[P]BW? eWoSUf[:LK%M:xVjy)b,Ywoc'O<s^aJlj`&dͫkyoXTIlΉf6+.@bNO)a\xa FNfq^M(knB""UȚ ?xyoXE]ܷ@Xޣ|m;wk8ynr|nˍ:~Bv"eU3CsN[8Dd{ӋZ|2%9-Ouqo$ϸs]ȫg`'/ȱaˤeeaB#"DQ}uۺʒp˻[fq#Dq̂OKPz%8x''Q0 xNMuņS7(rLܕgEKp>?1,a6'x݊O".ɫ8&]6,N)OP+9R#5h$ NurCϻ(~JS EFԏ~cK2qSHRxZ$%e4+vTC拏EH4"[󒰠ܺ cҢ-x"^ (@BW? eWo|n&փ>RI^Zm^=KX9GT9MC[^ w ߿u@'@Oy=R*~Xm$]<A@@@@@sr[CQ Wj{cE>^aJlj`&d+mcĸ=]J8sϥr< P\ukuz+zoft;m鯙]?0y. l@@'cy' T!k&|W0n~|=l̍B,-r5hk$޹dl~CwwK镍ojqm6)l @@Ǹ<upH";g7GřkËɤa3}dK$r[0Ir#qN,-ٹj9MsGĄl@@jLYF\V6/.4"RI%w^7,{eW8"J7N,TKB .; h~NB "-ڱպT *k*ieJiǶb;.m-KѪƽZu:(-\P vH%!˽TTGߧGro;|͉ZsF~غt @ -%Dk!ߺo^s+r;QΎk.5g,{󹩕Ch+~t4,B)e*Rb fCt>xb#9՟Esw, t::U\aWoʴ<]An=m%1mȿ}O7-i (ݕ'^QRռ q&_8qdɗ M̆՗&LLp<)S6m\22+M Vufw<1L>[[5H'}BYGmh^XGXcҋW M=6ʕ IIab>*yV2!&[;kϾg3(J+HPJy: 6K4 kx(((`;kN)}F?yCV@igFdO}|B{{JoZ,$ (0\L$9K*qꟕ> YaeXKPෙAt\?!6^Mp xUQCgT[Zg tղ}{s: 3TwxEljKHuY8> @@_޶F/`#S{rƫf@`E+B^egJkDABGh"לQ(oieZn(-dGr y @@VLP;NzflOL„2x}],!$On%bDe U)&\;zȷsvgF PUWhi/5%*W_ p}[)׌;qc~mSX]ѥ4  ?:X!J !RTD~5):,Vvrɖ"3]%rdP-FKJt*Fy0ګyeZeTWhx. &K[Z @@ά`%UtjecWNYB!l &G.Oʔ|ѥa->nO:Anͺ֐p>:lsFmƂG<‚f/W柖^Rh5ڜ+W*$&[Ʉuš{9V$];]P 66V"xt5mlJhִ{3PPPwڝUSpQ ~On;: D.="w.#o~{CTFv߿/?| +>*\L +3썷ʧU$1KBs43tɇJ,(,2׏G! ק $C=AU}ƖV<]Dl,#qu 3M13?z7M1,kt1ѿׅ$@@nm o[pD#—O~=iZb3 0de !|/IԲ5"ߠY!#4k(LF㽣QaY猙$Qقk%㫾{5CniP6QsuQI- NbUj-sH Pڍ -%Dk!ߺo^s+r;Q~tB 0C)!PJBPj:ajkX+[[>eJBւUըt t_FKJt*Fy0ګyeZ#;etܠXBђ; < -?1*MS`2ı',rф Q# 'eF}]FRfeG'vv8=Df+^(ndO\{pA3"Oy f]kh89C n ֛z,m΁+q|VSU|ĭdB:H\/vkZLJѯէV(2Vq5?)DUp5{*0,@Jbcc% ^)OGaQQv9aaMX9(Z   s7 |Pll,1D@`x~      PK/~K@yPO|GΞ6Q!ؿgMMx6סwPKYkwJWMb澞ֈ꠼z|g\C?X5gŒ45w3NMQ3π>>W|iؙ֦;x6:ablaӥjP@igFdOOs_#1pC@_}+:8 |'SgfB`eQ(7>Ϙ,'ECg6l͹HyCqAsiv (ePy@K,7WzB^c1Fde6:Bށ3yjYF⾽9,!Pޯi֛sꒋ;WMA鎳!p[cs3ZUW`BL&V}yn &k=}Pg+3Cmٝ;{Oߗ5󝱃=lؚC4Nc}v?󺡓+ے7H4pe  !-%/]tPfRvrGOa36R'MV3?wbJyH,2D ק:[Y [_=}gq7o|sSjgؘXьW[ yɱ?ݨi~ݵ_v:qUޮkS}މj;7S)_z`j_T' l/ 쳇vΨ4v4lLrZ3yP^ʡB왽{hfV6!$jxكoЬ5g,,앲ZXٺ06a +?x|ǡFٲ!s侓|/֜3MWWp_wARa3B?T[پ;jwO,F8J=7tUլXHLdGU*FDUw6UKm/8qKBo_:Ai0] L QBS˱:k 𚅫OVlm궅-8'?B5ڥ9qKܜ|#!!ľ6&(u^W|(QniMWꨆ*oT"BS>jwľ>~ړ6^%6C.P/Z_Ƙ~&ND]9:t|(5eE٩̒ѩ+LOIf p 캠f !yr3-إ$*YHlMJneU3;٭Lt[!íwF~s%_dzl:a5U?j;1q(!eZT`,N-k̿g3N9ڪ)c]-H/7xS'?~\Ilm9T̵+O۲NjstĂ_)ycμr?p<8q%WjSʧMa](Su:Bn n@aRe jTR oBiYڈ!/͗ W=+%mw_QVw-릎Yȗ_Onm:|/_Klt:k+Jj >>2koԳ GUI- A)%skztr=Zr.r;Qu׎/6t;K]^iyٷ=s_i OBʄ|v0i ϧ s%k/Hn~o5?4FdT_7ej@?q^9qהw⺷>+۳SN[ŬW!K#ᆶյ\m*C{]]='|UU^lMָKf^Z+C{HU]`ƨaw~e+׋rWߖyl>f|КxOD8<v|;CkVV5t%J)e*Rb [XCH!sfg#[mXwf-k_A,!,k<֗S~esJau{-O:xهoV\(l͓أ`y~%;Nji'ކEig CçˣcoTd2f}Z5Li~r^y;fk7I[j<eXB }5|v(; yVҡa[OD&c>fPߟ՗&LLp<)S6m\22+YB}&.3{ڙKk^&L'b3(R鹖N$ͯqcE\#:%'KŵuKJw*NةU_kv"_Zq0lS9UԪ}57Ndȥb>j^_T&tU YI;kK\gguw^""wLtlJxw86u!q&_8qdɗ ]I7'gҘsx_&MGC= FЩZZv򏽆])ݽUVMv)wx"m ·8\r\/泚'n%b$Yh#Jo%TSBoTA'6 b>mTSzQ]ojg8$`8Enxztd"SŔܴzִyicyG247 HQɯǦ/ۭ’䴪seGxlFye?X=s{fֵ_̏?9gF]sbdxw<6qؘF'ޔ v p^ZzJa=z 8է}(kz/l!=KyvW|Q_j+!KS5c4z<y>Gbcc% ฼QY_,9$3;βo=fjӃK@;WEEMۥ愅aw}\-̷mS7 (7en5{2j1E 3 n~{ߣذ/BPk(?KYkwJWME- |Ha>M7q@wtu=] <=m_Ns,(x[s.&RP\Dq~O}j.3\˖ tTt\?!6ܷ.$Iƒz8ۋ2;Q(IDAT<(݇3*-ށ3yjYF⾽9ݱ4񫏓F#!\Q2WݹTt @+6uшlyjOnx  BhE|CKlA|7hVM3 mN'jiggi,+Ě` jT\aw3td˭|bB&x 캠f !yr3-إ$*;wy3+OI}(UBK{ )Qq*yV2!&P S' 5(J+HPQXTԴ]*}iNXX>@@@@@@KYkwJWME-Pj?k${jܙ.k+>*\Lץ== <n#}چS[oG~bϠ*>{hJcKwA."Zoo|Nm'ÌOޱNZOakS-m_>F䦍Wk̀ "ԋVė1$Q׈|f,D9 Pas"ZclPMLP;NzflOL„2x}],!$On%bDBF~׷.\r\Q|@@߉UWhi/5%*W_ p}Z׌;qS!^7D^ÇO:?:X!J !RTD2͆:[ C?j4\*Y;|}PAFKJt Fy0ګyeZФ\-_+|nKkןB:S(4eV+/(L;ݿ*]-pȒ/ a/M59rxR\m۸e$eVFYZO\z#WW*t t[wZMs^Р֜04>daemjXBa5J-:P XeBifOmIƒz8ۋ<(݇3*-ށ3yjYF⾽9 VR~G^؈ MݶG4"|>~ړ6^%6C.P/Z_D-?{Pz8_# 8B挢b.[h5\[+ iP6Q3(gfɖ[Ą(L(SiA׷/uABf["F KIT !Q|m⬔û ]ê+Кë@8yͭkF8DiCL NBN?Zʯܑ ?:qe¡B(LEJLt\!lSt+@lݻ{ade5X"PDT1se^+t]gs@@֬`%U8jecWNYB!l &G.Oʔ|J0u!p0fXٴ\LJ7 f]kh89ƂG<‚f/W柖^RXo건9VTHM YMU 1yZX6rtGkRu(=H$(,*j.>4',ig   וּ;(O  wtu\z=E?[aۮ]bbkD{ x=_O?9RE ܇n#}چS[oG~bϠ*>{hJcKwA."Zoo|NmGa+x¤(> @m o[pD#—O~=iZb3 0de !|/IԲ5"ߠY!#4k(LE*8WC|;͎oP]CL9@@0 B5vf4ŝ,!r+ e*">-.YBHsKĨ!v)Ji9ǢĬ"5ɜ%Vu 7_ED VU^֔8^}-[kn\3Z`!gL-쬘ҫg~R+ڷG`q'Pщ9 aRB2)1qLkNa4FpB%*(U,O$WykԱD`-D׉` zWa'2EJjXB.e(ݙ'^QRbĬƩE>n+XD:n¢RMsš6=[s.&RP\DN]~݈R  ~n#}چS[WB?IxPg{gPe=pF;pFp O[-Hܷ7>J_a*qY 6uшlyjOnx  BhE|CKlA|7hVM3N g}t eiU;Wwŝ,!r+ e*">-.YBHsKĨ!v)$j?j]#?cPw`UZKhMB u߼5v]9} ߦ'&G'f2X!J !RTDF Pԗq}FpZ=OL5X"PDj0se^+t+e VnzEI)ZyAgUjG|l[}h„ɑǓ2j#ž.#)tCmyqwu@@ f]kh89y6cA܎#aA+OK/^)7Xڜ+W*$&[Ʉt|©N-^(J+HPQXTԴ]*}iNXX@@@@@@KYkwJWME- ,0jPum]"}O;3"3>9>kn"N}makD{ q>!Hű_}Wq칋՟Jk8`閹~9T' l/ 쳇vΨ4z"beԚ 䨼Hj{<}3z=(+6uшlyjOnx  BhE|CKlA|7hVM3 S*4ؽ0 kS\[t(@hhP6QsuQI- M7h;: DvfDdow-佷Gx;Yd߮BckD{ lLtwYi3%>Yd~KB>m ?IxPg{gPe=pF;pFp O[-Hܷ7>K1CyWĦY^| ψ[mm hD6]}<'7mZKlL]^"!%Z6^pF4+dq&rB\&?Bvd/7 Wh4(Kըڹ(gfɖ[Ą(L(SiA׷/uABf["F KITPXݜR:1`ϵ7|;gKaxf|P XUyZSxxo7rh(M\1nG.6eky]jM @!e¡B(LEJLt\a[YCϒhe'7~lik/2ՙ\.Gf@ڢkԱD`-D׉b zWy_FMu`RU@ ^QRiJV^Pƙ8vUZđ%_.41V_:0!jr2LȷqHʬ lmܜ] Gc@@x~߬k  6g4f,q#,h1zei+&Y9rB{hJcKwA."Zoo|NGΙBqҦۙr_G~Z\5n{Ft t7lm궅-8'?wԞܴj-2uAzъ2$jxكoЬ5g&#Ѩ,ssglAU^ex{ ̡J4(Kըڹ(gfɖ[Ą(L(SiA׷/uABf["F KI ˶α 9:+'Ѫzzڊ9S(F UyZSxxo7rh(W0.8ũQ^x%n?:q!e¡B(LEJLt\!lSt5055-2J%!ThkkjT P/]%k%ND*yV2!$ {Po5-S?f8ޟA*=g? %u<EEMۥ愅5m`{x     7 $X[7?MR{LM{4P3̤̥۬45Ss ETvaaf"yc3{9 Z ;E~#-#(W'vl߱S.V2T"D"P$>6,zPzA_08XZY {ŧWw/X\xqZBS&^׫ՙ%%;wFz 9/D:gTj_vZjs+) AiiDR;(VD2Ks^FhJи z1/QQ.}xlm{m#^RkQvhXu h.^+%hښ-ޖ-糉lmѧ3kѣo~ݭrcӔ|ξkF NP(4s_3uƵdœjgҳ_~jy`]􀷆 > k&DOu5 9 +իgʨ!^9쿁_~ɂGE߽ަ#tvw{ˇ$]w|͞65wKpR+nڛ^^yEzk?MrP1w2R_Gn#&(ϔqϡe}ۛһ]:'Ш>=P*3ʼnc{-5 >Rb4 Bcc-쵕j*e;g9hIϒװ~'1-֞/Vb}׳u<ʏ D"5 >Rb4 CrjQv/ ֝?9 Z=$[6dJP+fgX_sO Z!"f%ź#JC}gK8٘J?4"t,_9sgKKNcٜ͕^Edqo޻($z0?!-;Wߴ/81֨2 39kO{T< jj侇UG_*\ $^]Gz~u`IiwF]Ґ߷h;rYqE)w3m׿(5o;N%.y$'srZr^ٳp61~`G{+ׯb|җ~u_m ⼶.a4 6 >"gVk4D$8p'%[>+K'e[>8`J擾CqzyhȍScȾ )՜eW|~/c"?vH6Ƽ"3+s|LuEqԊRC3bYx|No }Or% #JNMDt*"}w*m~Jw'1)Πjlyinߺ%S'UDxvo ^\nnEEf-dqwzOg}NʨUo%<Ԛe/ֆZEw朩/7ӪFvZ5e FD̤?37o5?~;AC+쵿54~PZA(R[U=^=SxO g4mYɖ=br0Pi>0 >]~ÆB5֛3pp/OM^iَF#!jW^~{ߒn:qu.x/D6_mH rzOkYxdtϤV~gܐ %W~xCG~E[dn-'}oǏ;y>ZAԢK-8fڭtqˬ8aZpzÆ MŎgDGt7]#< ?=vY|BfȢQz>O?Ӈ`CIo )TU29SEFӖItR恧 <|"?'=_17jy"mƩ7uČZ4VyAr}F/8koZ ҫK{CY"t'{5D>=s+W^IqɐAگ_ K݌HϱU5d{_ kPlywN6;wiRނQŌϻt"P j*⶝Kzpꕃ0&vtvPt3*YO366b|NL8SK ]?i_ YNDP0~AM{RpʳٽJe:(L5֞~szJIw#x2&nio'1y17SVaoWiHbj**;˙奁_}z3mﹸʯwjNߜҫŠͼ?MNn8׶{ D&/(Z\]LYŤC:'Ոi9_-p[$h}<-te_jIMԍXIτb Yfd-..*Z3UQW5)a2&!dռsg..t.%?Ac' _Tz2UI';1*g|{(O)7gګD:/oNq|ytQGTZXq_"XrTeۥ:Sn;:q!_|=7m7.5s{ @gDn?de*%"ii5o:~vnka}-Wc:ot$(>&c5WLXZAK +1Iv|_>w]-LK7|YM}'gb ~߇3 zkH)Qc{o?ӋD65oպZZ^y?`#ԂȣgR[{q#K~׌d/hBE| <|y9fNF M[+3aqz#kc}NnK|b{Bn~ LSPp;_WyQ$mU?Gɸ󴹣_nMD .Dd֥ˊk)gyV O-i}lRH&3(Rh$wm鵝ojO9StPxvU]K-[ZJٷn\_HDB|w4+MCK_z#QjXzuwF)Q+X|y KQ03[TsY**ѭ[iwU2+/BDS-BDZd=x~ |8˫ڙɣCj#ydԠ?? 4Ru8hHv>83;yq W{p.d62c;ӶTkwZ,nYg@vWr*{Zඖ:+ i\l+ B!ScZI-Ҵm̔3Rʏ1D j%hwA@'Eʑ72\r=8S)OKk14:((( $z,O{ 3(иJozqci}j3g&^c[=I-sJY'tի<"}^{cxYλy瞫b!# &7HP,@@*#"Zp?x{kKɱp\i]$/݊Z;֝UWɨ@x3W~+5Zj33efa{Vy<,X#.VRwA O_o*ZԠ Q\,>>s=_U_'陷kp[BKAYQ{*G:W׻5OY9Ы׎xO37SqWVW/Xz}FLna U67x3Z|ztm{r-oz7A-z"&_۽Oo Rw>E:θD0D%刍ZR@5oxeJyx͖}㢋7yY/;X-33( kn}]Y^+zzEzEza)w9Ex 8sBxa!uV%^ ǽT~b~W T8 FffeY3]bvw+=?-9"K+q=z;^.}|W^*Ϡ1l{-2^;}CKE'iɎw{0gDԶ_(bqm?#s=zJ\ W; yA ED"8hD$*ճ]qfWK˺qw[E&|feіTހ҈֠jӒ'h.OO _ 1} )u=zڷa:"7vux~,34LGDTy"KgƖb=@"Uo" "p iyfdL8 ǡgVٰ|:u"/\E[󻨣s'^}='R=~Cm/Y%F=m %mVHFNnFK]^ 7eY?Ƹ݌;\Hos{!" 6^Zp~ړ!s{s-vY+$ʛFZLSؐb4p(k}G$/ AAʎOśwŽ_x]ΊۿjB\*BO{[ʦRuFn.#G[bܣalF޸\=ľ_+4<$Q0*XQ*g#͠ =Qǵϵ6ATx7Qq!:z~ЙBw0IS;=xM~❲H}b9r<*cwѿOǭ($k.]mxR\I<fG&W"RvfB޶K3YޣgIO Ѽjz}jojKDD"u&#KsF֮[;:XO ϤgwW,2j}G$MsXOXbs{guptx;1y2kO&Nz;զ~VS&^Qn7:7j3(4q}sl䌺GYAq_.пǞÖ^+4nDo3"g594?ocx˓%*VIiT2j2zb6Y~wqۻō])%ۜڑ8>\7K3r8gLj&hx@\18D oT; yQ ̋UFi92fusO?[";;422y"Yр."ע{m3/ZZ1ʁG>˶p2'-ʨ]&\w)>:R#,dRǎyj]:е$1>2ԙߝ])KZBDY٢'Oєɭ^}z6_gSTZ6߷bZ&24Hol5?A_14A _jGn>&F}_\A>,e;ޠD|Ccn•&D䕶d};$*~ZA"%N徵tY NgMτ3L=9/3L_l1]2Yإݧk]2KDĄi\l^aJIŦ @YyJ-fPǠ|QalNB_[B<89uM0W:-wu=9feKRO"%67E%Oa8/'s ivT+unA\\g[Hן?0tZwʤmgߢIom~XsBH{W^Ke_?Ԑ%-UK||/_ݑC;ޔry|>ioE]e׋'(Cj}G$/ AeOKGMP8?p۹>/M'vRVu}"'%7EkDc]-]ӊ%ͻш5:ute;>1ӲrM=C f;^~ELGc73b].%4ba`Xt"Ӌ;[kG{ Ǿ c93 z;ծF/{V A)Bm-q=ieͪۥt4MX$$(4e`$q񭷝v|CRASϛ{ME,{VD8WOD$u_h䎍}&OZ^*4ju0hsrYsU{׬L3kTߋ[C[W w&˸*aJ<'[J \1>ķL8!9%< ԀO=ff_3n˹D*7%)7%):*Gx;|]o9%hzw㎈9+# WO=W_價k3ή@\! J/fsjH *7J&K+pf*G8vyI酥ȶ׼C?0V6 ܌Y"+#WF./5|v_u)2(58Sye[^mȈx1:-^iEf\w7{R%(,:Ŭ@[J}:w|fл˗ b턪˅ ,&yX8S$ >}Q^,n_fN5cDtBDH&3ƅ'f+͔:ANOK ̢8ۈ9("J]Cv∂piz\y9o3GC+_fßf @ZZmfydY DĈ @c쮺I[{wN7lG (Pbc:S*Ntʜdj$3j3z]X|\VeUAuZd_HT}.bCԃY~{8_j>J'2kf)51d͝U̢on^cBv>쟇/`pB/tVg?t}u14ٔb zwuBmVPu6R_ݱ$ Gq꜄c/ʋ˗ l!yi7D#z3̪Ma~ӆDfSEaΝl)Բ7V􀀲4:(((uںub/o<, wW U]M<4p^KK&)qD^0 r0J%v5q˂/49O}y^o QEiOعs]V@$鋲RZe}%#l8"}484sCYa=]ͅ#۶-HzQF+7J]'- T/XZw:9׬ުkV5sȭq:FڶJ'QɈ @@4J3N)*36"xΦ3 b8 " -!! AmsnNѐzd2*ʽZPD-\ Mf6<[,FƑރ"bĈA 1VvWՕ_}tq~G+>ݗ(Pcn.uTtʜqjpUnJRnJRtTnw0N2Cc* ɺn /$j>\R~C!`CCx8,i(M̮Xf5wrVi 22'.Yj 1~\TIr;\^dH\Z:Z+B³vfm^Xʌl|:;\fN5cD̪)>ޛl*zPftR'iiYc쟇/`pB/tVg?dԹi-, ZEVFn1A& zwuBmVPu6R_ݱ$ Gq꜄c/ʋ˗ l!yi7D#ңCjEaΝl)Բ7V􀀲4:(((uںub/o<, wW U]M<4p^KK&)"l󸕟ZTuלQB 9']xMj㭥~l*MAγXa {~<[  P(}QVJ3qQ ,}%#l8"}484sCYa=]ͅ#۶-HzQF+7J]ķL8V)s*xjӿ~lҡi5Zfhl\eU=.vJ0k?h+69'/$`P&f3 MXvBՅH}udžCg,ũs(/Jb/_77`TP<}ascF2TSf'G}8^O\tAv~1JIM-qcM(k@(((P[.6#Bm }W|UPQՅDCtnBc=|es  4ABo^xk۽,JS?H<7\3υL% 4U"f./-5^,O]d GD&"ǃfn3k7lvdۖCEZ8HsF6)4mu 79B-8R Ԗ\,{VD8WOD$u_h䎍}&OZ^*4ju0hsrYsU{׬L3kTߋ[8FLz:.'r/j̡ T)LW)uNV/Eq>s6QD(ؿ i  *otsJfW\K\;]ķL8V)s*x~ ^'̄Q@D|nN 10f<jظ:(zyBn[+ ElRzhwsv "Q3gGNx5ˌcN*MAFf}Q^,n_]P;w`sxRRSnoXGnZ<    Ej֭9 HP[0{_/FDTu6y.-?Vܧ[2¦J.}os5&G9kRo-c_Vi r >-/32!pMdd:J/J)b&.jARSө41:hh""Aqs6QD(ؿ i  *otsJfW\d%}' !jbhb6kB$f62$#F$qƵ<7'@@:+c|։o??R#2Ay6 "jݷSѥ߮cPvZWYE/OH tke}!QS SXsnv[KҴYJeF1Ys'g #HWEn^cBv>쟇/`pB/tVg?lQH<AUic6]]Pۄk'T]Wwl8d1{Q:'Xb$v9~C|[Hu^LjiU5#>wEEaΝl)Բ7V􀀲4:(((uںub/o<, wW U]M<4p^KK&)O! d߉9SX P$! I{ԏ^|Y)yO{r}3n(A)e1 pyi)Y uKFpD4:hh""Aq|l#l:(Q.v!ى# "  Tަq1放 )Omw9,ӺgڷvK5/QK7CY,,"1˴qDĈ @cZ0C+[kGċݬyV}^8p>s(5!V0~~0tZA̩'p$+:}iz:zS>]p, }-346^ BZ(0`fN-wJjcc̨4&HdfRj,3bɚ;94E,uKE$C.g<<|Lz2,$.T޲OΣ>| sNlQJjjٍhz@@ \@@@@@@Hmݺuu1Ǘ jkfڈ.&r8ץsO}RZvvڼ#m9SdirN~Ը[KeU+31Y{rL}݇skJ/J)b&.jARS"떌htDDx :"bfMtu4Îlr(H QGib(u&-uekw$/>=Q  TJ. {=luy"A_'"/^4T{r]Ij>'- T/XZw:9׬ުkV5sȭqg &52 /MNHp2b^8Iqfg+uN~T b}F᱉1 שY٩BAZBB)bΟ5s4E|lk=Jˆ39q$qn.(0ĉZZm{H2md)*B1bD +f[l#D 9_Y5!$0~~0$VA̩|Gy^z'{\Q dzPZW9֢'$2ȾpI ]Ć) ^8zm;b} sNlQJjjٍhz@@ |@@&PPPPP6Ν; P *D@hpPPPP:( zx$A N w7֑G9#bf}'B$_C2l)CGQ(@@i\[rS{=ɍ3]JQ< H2dkW{[.Ok,N]ȯGk{'{7fQ(tTS( NmpgMrѯX62Sn`YQzJ[m3X8:E'DW5++-/y}]K×O<4p^KK&)ODCߝ->ంj7pւ}z4 7WV\yg:9'@K1Ȯ"TTTP\]Ez ^uz+[uQEIX $!,|@VYb_ sf̙3}zv1 ރ`w假a,klx =U$*HzAy{jK;~)#@YCKZ&iZڰ@ŦG^j٨}\ !f?75S'CZ RINE7~?'ʰzve_dHR'>(Lq{un")L3yn]>3bՊ^L[>6ʉjkeSZ,{}?_f!˖;<9[9\E!B,ʺ񟫕J?&B |o !jDƠWCN>l9)4 Q$(tE@k6!QCHSKlDkk?Z'7`˟%"RG:%TŎ|U3?]2AQep:p9POi:D"VW=]Ka*.j1x\[3K7Tr0Us1Ie\ak{! BT !1G;{y8X5Oo_pQ̾`>]REzҢc3j*8{YѸ"I$yקe ǵV+Vٚ B(E͞ɣ))ufOs%6q4ԛ07sSv48&c/UKbmzK%jTeN>}HP0xEYUֳI* YdW_J6rQX%_6+8"/^c]`3p"L-8i6~3y8)oBj8GsrX}˪廼8ڠ2>|&?'ѵ)6v}_qlQzLfc&D^sXx|7 BXt늪.߀lxѴF;ڎKӵ=Jirf>DgpX=Z+B!u Z׉d) $(Ѥ$ l2";[Ϩcf<-Bz"Y?j!Lf8N&(4آ_ҪȜKkۿߟ.$B/49j~z# B)V7rƔ%.ΦԓBſ?cݻ]"k 2T DNjyPΗS i(!/ qᅭvӚӦ(L_rI#)),E6&#(J6EavFlbO^}h0㚯,3#אQΤ|ɸ5OzeY.B:`D Ce0"qdJ$ߤ$pRCD̷;|xѨbT,EH1227E\%Q}{ӆGit%$5w8oݵs@L^7lF1E 5#(R)v^˭^Eö%"Q-'-Z'*QJ FPN%C$*n,FM?T"h FTG)(M2UJ-tk3yjݗRqe%eh 4Kz~GkzjwUFQiZ|ԓqDuA7~?z-`0쇸.O@b =Zوz9hVmqPY)KI*YpU›+8^WKU5cǙofd7Һ|E:%)AʑZ!)ӚToI^ePkT-sY/$#%PzFTakX$5_WƤExVE\ʑ<׹A Uo$dpTMCmӄ^FzRm鏺LW00PWՕ0 SظQ"in%(ok >:Di|IX*5ZKT]^ S?u"U3JMQ^uQy]3F5I*鷗oH5头BH.lZVX4=fS B A/s-BZj7:/ԆTj o!"1j%#!Wm܌5fU"ip,oliXdž) !?y! OgԷ_QŬfMRHzwaZ젥Bm:3ue/ǜiî_p'cN#G|OٮfjPIquYN\r(Mz7gr;ޯpc$[M#krmӴ`PcTZ`Z:J,MZkoU;'+a,g:817;OQ B8]WMY&*κ~QHbcc===soW5*,6fVWFJwP巜k`1R9kݰ:~5DQ ~+_oH0AuRbڽ;L E@Qq--myyt֮[z}] /UiM5ϓ9L}mhvl1uv`ɔW$'6=V*_o xƒq߽|7S|CUT@TCں-[8BO2c.6ޢy{jK;~)#@YCKvڐ K Q$(O>a^ʾ4qǴW-ͿL[>6ʉjkeS abόXo^ߏYHOVGWQ}QY\EYu?W+% c{Wϕ~87㟇F!VAs1/ǹZ[0L *Cos] -ij}Xןhd6v M#0:E{iyA>WHbᵲ.iJFv;kpRt\YPJ3 8Zo VCILdD2rW.d eLXU^aoLcmgSIP!Gޟ۱ݮGe*"'G(˩F4ǐ qᅭ5~Dx IKRdc2dV"@ԛ+i;#6Q1k/Pr\efD7%aR dܚ'Ʋ{]zT!0@2XT".nl%Q}{~֒(Git%$5KT筻VsNיF1G)FÓ3q&6sxwX :'L;V/g1(^bضD0EkPQ]64x0O7шaQyxn2!c& 9T,"C at˅ߺ1SMϰh jZ@Tko! EY|v3온mwkHo|p?,ge @2@J輜SP,neRZ(rByjݗ *He~GkwQeɇ(Jz@8.V/L)Q]1l LJ3Ҥ2Xt X#"Im5W5&-ų(Zl咄TϿ Zz#!S/jj &d,.D2K|nLiҨ^IUjlcΡ-᧎rfV#w~5 ԕq}u%76n++j;^ĩ:*B7[K(4m\1nNUI<-D!R,(k?EF9)4 K)[9-E$MzƢY:ԂPHP Lg,eAS4;mص D>xi$޿1uL *).ˉ˽dDiҭ/{Y?+pmbi2~%&Ӵ`PcTZ|S][@rhYݖOU;'++R EMUosuTqI}eq/cnfw.Qa4:k,w]5e8K׳F?Ell@1t:}w.坒,c/iڮ;ߨqo@c`--m-ނx/D=(c|o(cڟ۽mN;<D(@[<.PzNjI>[W⇤P}vA^"Q D ت7bcc===G?--myyt֮[n0HPcZzW~y@Q ePBյhӘzO>/H\=vꊠ+?Q D( -K?Ǿh\Ms2 +IMQD퐭 r!2_4<ژR'QK1kR(v?+/U6F ̥>;~0C D[6ZyΑ&ǜ\$Vj.o$>269_lIt-,|ʦ]?1aE\-[Sd4]ٳ>&g򒟃|$ke]NӔv6q57 BXt*9=Qoz;pVhBgT5hMb&_'L( d$ur!S(#eª}cSfFn;RO" 9J܎uv=B-+(P)9=B9_N52>wQt@ `bbeNSenS0 GBCXKcQѕXDv?L`@͜%G]/ÙF1E5q޺k;䁘<1걾Uj؋˜v^˭^}scQİmaTmI֬ lhD `*-$(n0C7eB~ MA"spX,E,5`H IDATdw/ EYe?8Ӵqqҫ.*k躣9iW%Eu{7rIn>a`+aqDXYQЪNd!*Ji|IX*5ZKT]^H(BXPQ~rRZ_Y!h$@TSrZl-+,HEtɩCFΌl]gY:1ni,wڰk.܉|H6?c뺙TR\]{Ҥ[_p~v!WaSUosuTqI}eq/cnfwMiZMPg(1yw).-k o94Inç* =1DEaZ:J,MZkoAB@]Ba4:k,w]5e8K׳F?Ell@1t:}w.坒,c/iڮ;ߨqo@c`--m-ނx/D=(c|o(cڟ۽mN;<D(@[<.PzNjI>[W⇤P}vA^"Q D ت7bcc===G?--myyt֮[n0HPcZzW~y@Q ePBյhӘzO>/H\=vꊠ+?Q D( 3bo-{}?_f!˖;<9[9\EVMr7wZCY6# 5(njalFԓP DƠWCN>m'ih A L\LxuY)mj{! BT !1G;{y8X5Oo_pQ̾`>]REzҢc3j/=e,h\{Va1^ۗ%EY)wQt@ `bbeNSenS0 GBCXKcQѕX0r3AF1E5q޺k;䁘<1걾<9kb3gGQw׋ ϔK ۖFնh?ˆOB2&0 #*?p^&w$A$R<`bw,D1I&x(g1QBּwʂ4\/&(n[7fjiݷJ pܢ,yN#e O{4 ^>8w糲\ AH%R`tzt^)(M2UJ-2Ym"Yu_C^VR&X A c'g\{ðc@Eeɇ(La%G؜z@8.(Z jTp `@04 0HR[kh啯8bIHsr b~9d==fA%e9qwz,(Mz7grr V,J7y}^m`7vH[M#kr|w;UNIuwTUVwh1loTl7Q DA1mo^<yEBxxnSt1Bv g a7 L16Mv'@Q - Yqv`qy$]c+RxCR(>|Z\ޠ]/D(lᖖ<}[k׭kyxp@$(@1]fT+?jJ Q D(@2(ZN4WPaiL\'L~$;uE߉(@zyhw >ҺBu^ۇb<{{?=9[}R~}:n$RikRZ'Ik =~Ax0wCwzoTg{̵,<}zv1ATCں-[8BO2c.6ޢy{jK;~)#@YHϥ(~'ʰzve_dHR8cZ{c#0N\ 9u5<.Qߖ/eMM3(` IWNTn^v( }fDfreQ֍\0]=W<4Ԩz&p̢HPyC%S5$^]Vm[2vuy:8BhaHB"l<)Ca^N6dM/D/vz/OT=yl>i()Ōھ gt K=W$y"_A8n:l^%Zcm4rĦ9nqfzS(v\B/ mLmLI.:5:Ŕ#Mt4yc/UKbmqRW @\ c8BQ G!gPirEbq.L,SkDB˧lڸ{V'ڲE1oEILdD2G^sXxixѴF;=ܢ&;Aat.]Q%Qk={'̮}z3ہ{vBz=w) RHP2:^)2aUyY1)K\t3#M'B%{nǺw!D֖d/P~cC&lĽJv|ɸ5OzeY.BwK`Rdc2dVT[L_rI<Dp;mgf;*^zjT7Xǩx\CFz$|v3온mwkHΥ(T"h FG圂bt+S.ږ.eZ=e%eh 6 )~flTV&Ƣ0-;h<>^C(Ez@8.V/L)Q xHP>>&c6Ij+x 9G1iG.UGb+$r5unB@ z9UP[4!=fq!^s[fN3ȕ[ K"g=c,~rjANj (ok >>}BY@]WW(L}cFE)tuR5kv:Jm6UW5 (LI뎚]Η bAEY)j6Ii}eQ]BRU J?ֵ}>Hsr b~9d==fA%e9qwz,(Mz7grF8鬑uՔeª/]tӴ`PcTZ|S][@rhYݖOU;'++ ӲQbl ulʈx^{ BMUosuTqI}eq/cnfw.QO;Gs DKy$n;**+ŋa7*``D(XpKK[}`/"!<pg0Ce*X`\^:ct}KHIkqyvD@Ucozzz:~[Z".o]` ƴv5R r+5 D(@ ʠke;\C 1q@;3}_&{AW~'@Q xݢM5H>!ym+P lqRJa_s1ܙ[kr IQhn=;{>ރ`w假*Ppko=BKYy~7cҵu[q*Ue$\mQE65vRF%-"M _"HP||3*ؕ}i"I-㟇F!VAs1/ǹZ[0L *Cos] -ij}Xןhd6v7U?HƛU&(=?+?򒟃|$ke]NӔv6q57 BXt*94]ٳ>&gv3@ԛqܳu Z׉d) $(I\HvߘƔ%.ΦԓBſ?cݻ]"k 2T DNjyPΗS i(!6%sOj߾UZ{]-C_d&)),E6&#(J6lEE+' ~=#ATWvFlbO^JFu#+̈5$nKä|ɸ5OzeY.B:`D Ce0B1227E\Z)JO!!%Q1 kQ(JH,jKbIeՑpچ9~J7 )j!{QT筻VsN˓3q&6sxwX :by-zq?IP"xa¨ږY@GuTZHP>݄,aDn˄$DRXXj u%295#B_M*kt?FcPs֢0BoݘϦu*B-prx9̖mfLfiR|"&(g1QBv|jT4 ^>8w糲\ AH%R`tzt^)(M2UJ-2Ym"Yu_C^VR&X A c2kkI(FƆå2ҌJX~G{|.DԓqDuA7~?z-`0쇸.O@a,HP>>&c6Ij+x 9G1iG.UGb+$r5unB@ z9UP[4!=fq!^s[f-/yp'˥_y&4Ĉc:=Z炏$OP00PWՕ0 SظQ"ix:~Dƫg lFNRcMU( Ӵqqҫ.*k躣9iW%H5头BH.lZVX4=fS B A'0[ٺϲu^cNXa\/1l#z~lu35,'.BI2XfBn۰DTs{**^$=ze4&X3omP][@rhYݖOU;'++ ӲQbl ulʈx^{ BMUosuTqI}eq/cnfw.Qa4:k,w]5e8K׳F?Ell@1t:}w.坒,c/iڮ;ߨqo@c`--m-ނx/D=(c|o(cڟ۽mN;<D(@[<.PzNjI>[W4R(>|Z\ޠ]/D(lᖖ<}[k׭kyxp@$(@1]fT+?jJ Q D(@2(ZN4WPaiL\'L~$;uE߉(@zyhw >ҺBu^ۇb<{{?=9[}R~uw{}7R1tøg|:{໹<]0UsGh)5 8FzLPn8SEĘ 3vڦ΀_vb$RQikRCB J?@zFe}/2M$1-UKGo=dt IDAT!8.lZs2:ϟ)L3yn]>3bo-{}?_f!˖;<9[9\EVMr7wZCY6# \EYu?W+% c{Wϕ~87㟇F!VAs1/ǹZ[0L *Cos] -ijcS-[cdPVNS'ۗs$)^+{rd4mюx(DNBŢ_WTAT/ǥZϞ 49kތ38v}h5^ϨkК-MN$#(HQ AHxBPFʄUe4,q͌v6Dr,zY[VPR rR+{rjdHC} y+ig ._IS,'ɾMRRXlLFPPl؊jWNz GxlGPËBBYǩx\CFz$TT ?(LeM4bFT;~(LXI Hx.BS YcACbbX uc?}Z{ H-ю4:>2[:@1I󦷣`agDm [Cn~sLm`y>+k(T"h FG圂bt+SBIkS| M0UAn&Ql3m׳SiFiea, rXc#Q5$R' 8əo~j[.޽aq]BuX||(L=#M*E05m,VZsxy+Xc"\>}BY@]WW(L}cFE)tuR5kv:Jm6UW5 (LI뎚]@"łSl A#bkYa _Dg,ON- 5 tflg>˂_ym$N9uKcӆ] p1NǜF]Ԡ겜;OO&b ]AP ؆xͣnN1M  7e $-5m9aT|~20-KK%P ﵷ !T,6w[GWgr8fv磂(EG.AB8]WMY&*κ~QO;Gs DKy$n;**+ŋa7*``D(XpKK[}`/"!<p-.oЮ@Q`jXOOO@яpKK[^D>孵ֵ[< 8 A֮^F*A5w^b(@ APul'04&ag ?d"hO@Q DO[i]!:C1~E`ᜭ>NJ)?|Z>w]`%?a㍯ݸ}DvCǯ7{v|^  U)* S51zRn٭Qklx =U$*Hz>cAmj. Qog!KB-5k[E6,E<$D$?gT{=+"DZyr\t4s#Q.#8{vQjNg6ѐ0-v̈U.[oupL[>6ʉjkeSڌ֬V,ʺ񟫕J?&SO>7݄z=ƠWCN>m'ih A L\LxuY)mj{! BT !1G;{y8X5Oo_pQ̾`>]REzҢc3j/=e,h\Q,(l]Aʿ-K?Ǿh\Ms2 +IMQD퐭 r!2_4<ژR'Q]Kujtԛ9R);GiDu2xm_g*p97HPrRpuso}??!"9BQ<HcN_.sY|`gZTƇ/$Z>eծ{m٢.{Yռ*S;KzbR`W)/9 IR,V4DgpX=ZjQQנ5)Z ~HF0Q@2ʅL :liLY⢛lJ=*(Ys;ֽ! C@V|9Ȑ{f+i.j7kt ⾿>I`Rdc2dVT[L_rI<Dp;mgf;*^zjT7Xǩx\CFz$TT ?(LeMbFT;~(LXI Hx.Pcnv29J}ޗ3-Ӧ?7Ơz11Eat˅ߺ1SMUQ[$fhGws-Q̘ҤDLyQ0ϰcޭ!AT_Ys6pt<5j E*b4ӣrNAlJUj5F Yu_C^VR&X As 2M)MReɇ(La%Gא JѦ$ 'g nxӇa?wy jTp `@04 0HR[kh啯8bI5&X3o;cn5F$~tSx b´,-uXB/2"6(FSճylU\R_Yᘛٝ (EG.AB8]WMY&*κ~QO;Gs DKy$n;**+ŋa7*``D(Xp_{g$CAQ<ċ(~ժ ޵RbmUjբxK*** *-B @!!f?EJ6Т>Ov>33lv["؋/ -enJA3q3wuWsL1 & >őP BP{|?wqX/&/RzC[ײ>A(  ت3gtHhHH 6G<t9 As}F[k{iP BP$($tC[=W`{Pc;$E󹛷P BP)Oۢx6BoCoȳkXnt_o #b؜HMRlW5u6`cҎBT@y#廭[V)R_}Ba=G?ľ.[rl蕴sPNe)Yi Vs'gւPdg1noXB; +hVKH/-( A%}zqM=>o]eֿG>`%mt՜*S`$e<ѱKM{#>U(&p{`Nd>TFVjjqU?.-L黆.y=x|wgX$ө3d9v~\ӴO skˋj1BT^WMu BhγSBٹH!1x{6"o:@Oq0f˳z:f$a=Tb'w֞U~#sSF *$,#l.(j>nO+žsIU%V8ܹ2qJN55'ZKUc~eH- (ϛ-hBy{5zr/5e¢(\ڶ&+>K }P 8Bmgn4Vr\,9 v߭u7)34o5*q[d)'Ƀb<ɼ>\  ^!/p4WCR .n^q >rDAv`k[(DNBCOV y njCeOngP\,w)s\zQV :ꕼz)ń]/vbNJH*JKj0m鞆y;Eng(}k|)ILʩG쬘'zfj gͭQ+_"@/_btU0DQ31`PMM/P`\6fՍ) :7wٻ3ę߶ږ\<|<ܿ&O =ҾIU'T  ʶsSK$* QM"05&  \T4$#;}0ܑ9M+s>|Z( D/V3y(m:!*θPG|nDE_'lj8fMiSn<Ȫn1uR tj Wˌ4!ͦOU{NBI8|T Lr3J4`}2fΜ :qt"Elv㏣!!-,\HP AHP>9`%Y JVx$( $(@ @ s̜ҏ-|oOx'2l<~;Ζ R~xRkGD/+ƋZQ-ݼ霎\ B ^Ni{l}qcF&u/1b%IbfWvQ<[/μi^ L8kzt_Qq wa_.Z#*nI[ IDAT;-Ea: &8qFuPYEx:>=A{DyXs!Z[. }gIkDaZ॥q!eP&KKmIt]t ?vAEۄQnxΥmkzN^Ⳕ eM2$J\i7~oUAkTY j[T55 j* %ja~NV,!D18Ȣ!J 1(Z/2UY \i:J\w/&T+E149Q6| No;xU60?B(fgY7 ¯C{CafJ!Zښ$3-3/Ce)5 !(ݔ9ni~*+ANu?8%u:)!().nJS=O#f ԇymϵȪ(AZ}AJlha<]K![Z0!Ay o)V(Vc=UHP޾lOA*nl[gf煥V9ƒz/)tu6~BڂnV٭ F6i^لWӜz'ab`MМ<iהqoK dB7nC*og\ܐA}'Z:b'P--RZ67P J_fidY#4uR*߭:QSʪ^Tff:D,eq ^'ާ9-/ɗhB}Y#} ,6>׷isX$SFU/ &fZ+1E2Y!!:xWZ~Ifս;N& mV[ -#F:rt o,_w V jbCZ=bUS:xOI^%R10$niII%`jk)QeyAD+!lUw헗ޮAL M %-T>̛ y; JWhIORDX2b\9f2x(HrmSA3k<^;On7%RsW#}}rM^~Ũ 1CEQ6N[Drq1f1t4KN'B2668^ a漕C(:/fJ]ܽ~QՀЧuzK8l[V۾7O ؤX:uM3%ّӗCXrHU_WIUzY$ _M4ZXo3xI ]#ĺk S.fWQ;YX'zq60r Gi aUqƵ(ݽdRtH|]S0:UEqOCM$(LghEET<ؿ9c0EXz$]GK^#_^0FeE_PjEt~_QgW+A“4e4fFZ:-%,:Dm/ĕ/R5W[JKw$҂^'/1Ţ6lF2M<8Ph,-o݁l5 D/ AZ[v|=[jy7OxQK Du Bۏg'!4-W9D;NE!g#ͷZƟ~`gLpq63RW%j2F ō/VHbm$U)<tW#D⼘; 4ǹ{Bu%iw[ƓX؊.6lN֖e? ?q~ 3^FZ=JO::<#ڀG,y2t7~^7jyoX?EyE=!VbC.3l>pN~_kq<a#Λ4ƄjK~2I"G1eD3L+Ϻ|=BQhݣH"גWeo D}yn|Tؙ52ѥKe*hb1}OJ/L֕' MI A vMc%υj9zΛn0`JϹmMTW|Z !@V Zt'TK3^\h1QwO8WZ#i:B9쟥F|ٖ--diX֠Y+9vS;(*Jr3[VV _#'˜=|hTx"ɹCؘpY6k.&Ѵ0'BJ)i X!ӘJAJ|t@FV !Uo; ^Z:GQ{y-R_A e"T(LuiJgYckTMJ.7Z(yy-*XF I F{ްY{S v=OkpdE}BӁ,R@b(ei1e?YnzcxEhkGÛMu ϸp..yoJ%n'mU{vva[O|3 z9A1w0fHBJ22s2PvRrM㦞v<ۯTwѪG!2ys-*7ܤ 1BhǮ^W5N]7AJlha< K!Br0ZՐZ 6)$(޾lOA*nl[gfCjeSMЩ%Mq (CjOWg!/$mT\Y01owF=~} 40!_Dc*BD;N|^x4-bО;e| s`oOHwZL ]v)ԭ$j?Jr`L>qqſvSSVܓ%(ܣ0}[[}YJYUM7(JgG Y[PNJncI*}Q X=B^ts==,loEffvIg A8%*"YNaށEoCH*kS4WgêgmOVNHR+eL&C SV0rqߧM*PyGw]J1>>e!NFXiBgޕ^60(,-[I=fe^Ԧ %%~O:}ߔ~{!C@*&zW{G[:Of6uP3N " +߶ zKw6$(.eFi /ɓZ %b 4[!g/6"7 !qS3Kw[[=,D [8^֪EW])!h 1y{;P_!iGCi#f]BlYگnrER=}<%$!QuU?qjZ~ETEMRZTGIŻ,v:l5B;2:bvcaIF( I:7N*c!D3k<^;On7<B/1ߠ ((ZPurHPoA ^ =fQtBXUq-3 !%NpHP9QG>-!{brxcz!c7jEc\VT8vӭ$)jo鑢ʄa;Ft'￝Å:ōȫqrV- Bg*%0aoсul?c pf!(V(Su<u`|y-yWh(b iQ`BA;*I}JOqVEӬW/A4܆aÌ[B7淈|N/|@fIT=ŴL\92РɄ5ٱ:&U&ɸ91CA< :: et9 AcYun㦪P ߎv G|=ֆ  ӀZʾi{l:5&2u" :/5r>6:;8ozP?HP:^a_ȉ'iQMbr]cŗGN[}KY5fFZhpc>3xES/kzLI% * jsDP<8Ph,-o1!\_'񽟶$5..a~ (;NE!g#R0 3&8Xh 5u(c.6lN֖e? ?q~@ZdfPM5zQutx GKYΛ5n~GHavO@w^xQO 1x{6"o:@Pnn?۸ӲDm\{.Gȹ& 1aGǷL"]c) qyV\zCOό$GJ!V:Vh֢XLӤ?52@ YjNԕ={B|Z;h |cchd=z&U.d/ T=6&I-W5m](RMaǵ*Q_vzz\( Rt]'?zʿwCs.m[sV(CMq'N+4uTV!%D]KDWDp/ʖ_K~wSܡslruvn MWWUVL hZFzkhj%ǴFiNvMc%υj9zΛn0`JB%ЧU*CjNB^~ WPBkj,-U4u7)34o5*q[d)'qyU Ӟ58s`!A )WDƓA!YUʕbMsg| j*b:^Ll#Cp({d/5Dy!Ee[N1կLh)Ɏdd’C^ϐ:L85-NZ&)*#^p'2c A qMG)0즵7:r-Lf@_@qԴɖv)2?aVn]&QDegkQ$B)^ݘY2yt1UB̻ t5?ꐠt>;VN9u!*HЫ7 DhXn EXuBWW-(`ˊ y&;JVz+xK6jD$sSۃ4@ ..lLrKQ@qQUN9y6[}v}W}!$Nl``L1ҦªkQ55㲲L Fh:Yiu;{'U~F_}wTfl)2삭VCiVF&d܄Dd2^Hɿ~B#$\֝Gϊ|y/!eNv]zogԫ-|+5%? @b:%𧡩[ןσ'n &prI:"6XRvus2=(/tc~ȇcJ:;;;;;CcW`$Ąq~h >ySdՊIENDB`brick-1.9/docs/snake-demo.gif0000644000000000000000000143176507346545000014331 0ustar0000000000000000GIF89a_ҟfǒZJDLx׉.͑AlئV͔a\Y.||.u՚nȬfs]S${vӘ+ԸIʑO.pOWhAA......ַ\Aԭ\ԒNAvA..͟\ɏڥkҘ\?z...YIxTl.\JW˔tѰG.ʴ.u`a.\JKKAO\tYP}}}IJױ00600Nn'0G0Y0qGOSrxz_ZhUY0BRmaӕʏǑɫٸ˖ukzڧ! NETSCAPE2.0! ICCRGBG1012tapplmntrRGB XYZ  :acspAPPL-applfIp;r8A[.=>R; L @*I  "0-(pi x&%@Y sB9@@ H@j8r}`0z Ǟ{9 +! w\DPp' ,9l]"2@ }~ $]oA.2h1o"("p$&#cAIKFaGN Rrwb+ &*aq a`#:RJb)gt:sYg{YY?qhpġGA*[-d"A/(.FĦ&0z&"Z*ꨍjj+Ϊ+() Nl!okirm kC:āk\Ƅ!࡜Iy@9xj (^ [[˜k $qoiW z!Y5Ap  awM#'³&| "#Ľm_.T4PG;]lԴ`K5OS+T5|]]Sg\kt俟oq?P_He a@)$ `2~n_F2H4H88(¶#N^0-/:̫},Zrhu2˄?҄wO BD&"킫bhET"H0o(["E|Qc\cXA1N tM>NĆzF 1]d ;D$#CFR+\H& M^'gIP^e A%CiJRn])[IWR1T#-1 vr&e-{9Bbz{C0bƝюu\&3 _mt-HM_F3fE`Sc:sB_(Nwnњ8iOp}'INg?Չi\ʅ.fc\(:jj4G6t-IOI}6Li&z4.%uM)%OHШDgP3ԤgijRΥ:5V Uըtզ_}JXY:֦ug)TJϬ^lldN:ih^~(ug`2Xpn+&/fdXQ>/ffYYgµ,P+Z^6k-m uL~L4caSmq Y7; Gg9[=nį_n"^Gy䋪9^~@B(!,x"Oa?Ĺ̕?ٲߘƃ> ( ``*$oÇBP"ŋ-bHO#Ǐ<IN$ɓL\IM%˗\œIL&͛lI˟A@DhN[*ťsSMMJuԫX%uSԮ`#V K6زh'}Mu-۫nJ+שϭuś-ݾE*x†o"N@h$H$L餐P)JVd$j!@ !,xZ"HP0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱ%Ǐ CIɓ(S\ɲ˗0cʜI͛8ٔPY"!,x"8$0I8 ȍdi",p̅'x^!,"}pHR0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~2/ & ^B8P%VP$-JOtF#qȒNlr墖}Z!s͛8sɳϟ@ Jѣj S "jөwZjGkUu>KG,Tr^EG-Sm…67uɫ ߾jH!,xZ"*8$0I8ͻ`Hdiˆlp,it|ϷoH, l:D JZ) zZwL.$z|x=C~C2/"ýȸ͹ҿ ׵ !,xZ"tHP0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~v + !,xZ"8&0I8ͻ dY lp,fMx|6pH,sql:IsJZAuZ7xLGz:<:~Kz:>}xrkc[S ] !,]Z*-ZG3VuB#Ybg6/3Rom+!,OZ*-ZG3VuB#Ybg6/3Rom+!,xZ"HR0I8ͻ@(di~(Bp,kXm/pHt䎧"R:hII7uzU%+.`YB^4h{9}L8>7=It~p%Mn$ !,xZ"8$0I8;`(dDp,4U޸XXpH䎦"R:r@v˵Q]6L.C`3jnghÞz9~n8]7OIC$R:> !,%Z*-ZG3VuB#Ybg6/3Rom+!,Z*-ZG3VuB#Ybg6/3Rom+!, Z*-ZG3VuB#Ybg6/3Rom+!,Z*-ZG3VuB#Ybg6/3Rom+!,Z*-ZG3VuB#Ybg6/3Rom+!,Z*-ZG3VuB#Ybg6/3Rom+!,Z*-ZG3VuB#Ybg6/3Rom+!,Z*-ZG3VuB#Ybg6/3Rom+!,Z= ,FJ-S'x XYbk ,o[㷺9uC Ix$& dSdrS(!,Z* ګ޼H扦ʶ l!,xh"*8$0I!뽯`(di lptmxc1pH|ŤrlNtJvbeݰxLfj^pc;N~LxBv5-&¼"Ƿ̸ѾִV !,xZ"FHR0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.϶zͶ|N~ ģǞQzyxwvP9DpƁ(Ŋl.bDq#!,x"8$0I!뽯`(di lptmxc1pH|ŤrlNtJvbeݰxLfj^pc;N~LxBI&zqkc[St !,x"HV0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.϶h `N^ vsxO~~vK}tGpCqw>:m73/+"˛H !,x"HR0Iͻ`&diBp,tmv|pH,ȤR9?˨tJZs zx:z.y|Nǹߠ~O}K5]uhxVo5 !,x"8$0I8S`diDp,tm|pH,Ȥ29A˨tJZ:szx>z.}|Nǹ߼~OP}J?4uhVx !,x"HR0I8 `(ndi(Bp,sbm{sY 0x+dR:ZLu N`e6LznnFŞ5}y7~6{5sG{m!Lw v> !,x"8$0I8`(\iDp,cmsmOps+R:3 ?v{N6L{fn;F\žu^iy6r}i5dGZ"L!U= !, *-<61'2Rr'nѺo|mxÔ!,= ,FJ-S'x XYbk,o[㷺9uC Ix$& dSdrS(!,x"ڋ޼g!,x"(0I8k&dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~r,|upj_WO !,x"8$0I8`(Aihlp* gxoH,Ȥ$ШtJ:؏zlv .KNَxY{@}x?f>SN/R"! !,*-ZG3VuB#Ybg6/3Rom+!,*-ZG3VuB#Ybg6/3Rom+!,*-ZG3VuB#Ybg6/3Rom+!,0}A@(Ȥrl: BZجvzYznp-~l|a  n  o M  tɄE  H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹JM4[S>mKjֵ\MKviڮlJjޫ|\8qU'|CAi:سkνËOӫ_Ͼ˟OϿ(h& 6F(VxKr(.u(8b&vb$NȢ./Ȣ)h'(#"!,82clZ܅ՇBJ/l}^ۼBx!,82clZ܅ՇBJ/l}^ۼBx!,82clZ܅ՇBJ/l}^ۼBx!,0}HP0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱǏ Cb$&S\ɲ˗0cʜI͛8t3ϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^' !,x"8$0I!뽯`(di lptmxc1pH|ŤrlNtJvbeݰxLfj^pc;N~LxB&zqkc[S !,*U ,FJ-S'x XYbk ,o[㷺9uC Ix$& dSdrShUf[lvcq|6?ku !,8%ګ޼H扦ʶ Ls!,x"ڋ޼g!,x"*8&09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购P;N^R}Bj ag51&˹"кڶq !,x"8$09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购P;N^R}B`, ~vphɋ[Γ !,x"8$0I8ͻV(dihN@k,~tmxa,bq c! &!,x"G8&ЀIk8ͻ`(diŒVKp,tm߸y ȤrY*ZШts^جvx靌购 P;N^R}B"*!#hI < C8I!; B0H/Bʨǎ>TC`HJ% $!,x"G8&ЀIk8ͻ`(diŒVKp,tm߸y ȤrY*ZШts^جvx靌购 P;N^R}B"*!#hI < C8I!; B0H/Bʨǎ>TC`HJ% $!,x"G8&ЀIk8ͻ`(diŒVKp,tm߸y ȤrY*ZШts^جvx靌购 P;N^R}B"*!#hI < C8I!; B0H/Bʨǎ>TC`HJ% $!,xv"G( 0J骽8ͻ`(dMhdlp,i:x<]OpH,~@rltJb*v-b!ݰx 贚c^6|NT!,xZ"G8&ЀIk8ͻ`(diŒVKp,tm߸y ȤrY*ZШts^جvx靌购 P;N^R}B 8*!#>@z;pB~ 5=QĊ&I#K?V )ɒn$$)!,L}G8&0I5]`(^ihlj4=a| ȤrlΊ7tJZ'z`6.npxe{Lbka  H*\S#JȰV3FHN6F zm(Qa!,>}G8&0I5]`(^ihlj4=a| ȤrlΊ7tJZ'z`6.npxe{Lbk H 2<XȰaÃTrH!n+^䖑3m~Hl%-rJ-_Zk@f56)m'Oh>"@h!,>}9HP0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|Nxbk[FZ !,>*8Zڋ޼Hʶ v[zvg< "By̩Jjjܮ N ]Y!,>}MV޼HT!,>8;ڋ޼H扦 L ĢL*̦ J!,>F6 C^gI&ʥ O.:qB[;"Y!,>F6 C^gI&ʥ O.:qB[;"Y!,>F6 C^gI&ʥ O.:qB[;"Y!,>F6 C^gI&ʥ O.:qB[;"Y!,>}HR0I]dYlۦp,x铼k ȤrL~MZجYíxL.ES:2_~G_w+}gkK1wv|MfLq !,>}MV޼HT!,0}(0I8)`vdihl뾘(pmx9ϽpH,?ql:̤JZؘz.hn5N۩~&Ca{gXL=/&ŜM !,"**a ZG3VuB#Ybg6/3Rom+L*̦ JԪj$qiӁum`:|V!<,>}D*\ȰC >Dqŋ3jȱǏ CtXqɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkJ޹;Ȑ!N ( %C1yC,dR( w_~(xv)"LRIz؇{\y!$drB '! 80bE}hEwIz 5!EADa}8D !7"yPX#(zQzTŊ@~z(#)xE"H')I{ rRHZε$!`Bo!aȟLz$m&&U9& y>2'x_• ](j 0@(\i"eC߬QI`u &PϢHy7P߳&(k"Y{ezRbiH*BAbYаjkyNu(/`!,"} 8'0I ]`(dihZp,tmx|@'H,Ȥrl:!tJZج(zxz=6|.>=}{dOQv4+# Ů̧Ȃ(ρў%ʝ!~h8 Ç)$+ŋsXȱ㋍C^)>&SvCe..c[!*6sR©g#>R!(+F*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠC Ok;¾qvԶ%}7H Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿsuрh-lJZxjHz8x"*"-blH6ވF:@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠 `j \ƊjJkފp[lx%!,x"8&0I8 &di l({pmx!  γN8B H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sӒ@HQD=RM<uЦVbj*WYZ;SK]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνg@yQϫ/~7>}珿}7}X߀!,*-<61'2Rr'nѺo|mxÔ!,*-<61'2Rr'nѺo|mxÔ!,"} HR0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~-E H*\ȰÇ#'Ŋ3jȱǏ CIɓ(S>2˖0cʜI͛8sɳϟ_ ѣH*]ʴӧPJ(LTjʵׯ`ÊKٳ!T۷pʝKݻxe߽ LÈ+^_KL˘3k̹gILӨS^ͺF ۸sͻc9<ȓ+_μC سkνAM8>ӫ_Ͼo @}ϟW'`}g~ 8`1~BH:a!rHV!h! ! ʸ`4j#( !,x"8$0I8 `(ndi(Dp,sbm{sY0x+dR:ZLu N`e6LznnFŞ5}y7~6{5sG{m!Lw v> !,x"(0I8{`(dih:^˭p,tmc{|\ H,ȅГl:Z zowL.zf-ۣ~O4~FEX|upj_WO+ !, * ڋ޼/H扦ʶ l!,x"*8$0I8 ȍdi ld(pmx73+&# (p \…A<(q"aHn#o?j )ɒN$r%.qIk&!,x"ڋ޼g!,x"*(0I8{`(dih:^˭p,tmc{|\ H,ȅГl:Z zowL.zf-ۣ~O4~F2f+'#ýȸ͹ҿ ׵ !,*AZG3VuB#Ybg6/3Rom+L*̦ JԪj!,x"ڋ޼g!,x"HR0Iͻ`&diBptxc a+Ȥs-)ZMu{Np)+.;Rz h{vӭ}_y6H~C5:;4/2G"Q%> !,x"8$0IͻX(di(D04xPF $R:+IZ'L*n`d6Fk>žD^_5~j}(4_2FJ;8(> !,*-ZG3VuB#Ybg6/3Rom+!,x"HR0I8ͻ`(dihB`'lxT <m $RЌsJR@(z6lBz|ʞC~d4|%us3=~z2rEq|ms> !,x"8$098M`(di(D[,comxmO졙,dRFЋsJZ?5zc˅ti- Zg{H}T27S1$NDJI= !,*-ZG3VuB#Ybg6/3Rom+!,*-ZG3VuB#Ybg6/3Rom+!!,0}AH*Ȥrl:CBZجvzYznp-~l!|a  n  !o !M  tɄE  H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳ@ JѣH*]ʴӧPJJիXjʵׯ`ê*cٳhъC\ڷg W@o ěVo@jW?f Fb=o2|/ˬY!|.ӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6R[ R(Ub6^!Z1b%Nq):"-618cLhKȣ>DBAPA!,xh"*8$Ik8ͻ`(diŒVKp,tm߸y ȤrY*ZШts^جvx靌购 P;N^R}B51&¼"Ƿ̸Ѿִ !,xZ"88$Ik8ͻ`(diŒVKp,tm߸y ȤrY*ZШts^جvx靌购 P;N^R}B`*!ϝ#ز߱R xj RBJ!,0}HR0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~Ķ- H*\ȰÇ#JHŋ3j|2B CH(A$2Q-QSP5GSNO=?thrBtӦB$u V}`ʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ N ΐ!,Z*1ڋ޼H_ʶ L Ģ,!,Z82Kȋs{Q߈ Bj-l3mt{O?ah!,xZ"HP0I8ͻ`(NJ!,xZ"8$098ͻ`(dih)Q+tmxpHŤrl tJvްxĒ4άn!6|N||B]2#}uohaZͲ !,Z*U ,FJ-S'x XYbk,o[㷺9uC Ix$& dSdrShUf[lvcq|6?ku !,Z8%ګ޼H扦ʶ Ls!,xh"88$0I 8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~?вױܰL Hj A& !,xv"88$0I 8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~?вױܰL Hj A& !,x"88$0I 8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~?вױܰL Hj A& !,x"88$0I 8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~?вױܰL Hj A& !,x"88$0I 8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~?вױܰL Hj A& !,x"88$0I 8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~?вױܰL Hj A& !,x"ڋ޼g!,0}A@(Ȥrl: BZجvzYzn|N|d\[ 31#$m6+ 52,n!7."/N 0)'#4* z(F-%kKxof  :21NÇ =0ZƎ } I2Ȓ(Lɲ˗0cʜI͛8sɳ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(&N 6JZ j' zx"*""-bFH#6ҘZA!,x"*8&0I 8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~? d3($̺ ѻ۷C !,0}HR0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ĕ-y6\)"p*\ȰÇ#JHŋpǏ CIɓ(S\ɲ˗0l`&͛8sɳϟ@ JQ6o&=ʴӧPJJիXj54f׭`ÊKٳhӪ]˶[__}Kݻx˷߿ @aÂc^\81 ƌX ʇ-Xs ΍=MXhҦO%^!{vڶͻw!| ?r7<t+\ߝ]?AI< CXI; B0I/Jʨ ǎ>2!,]vT1ڋ޼H扦 L Ģ+!,xv"9ڋ޼g H扦ʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZjz +;K[k{+R!,xh"G8&0I8ͻ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz:G ;c*STyO`' rBhO&f(OD1Q Ǝ>$r䣒$)!,xZ"G(0I87`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~A5.{@z)\P†(I"H/>ʨQ GB:Zb!,L}F8$0I%]`(dilۦctmx볼pH,Hl:ШMجvKxLVyz|NW~P~9  H ?KXȰaC$rHn+^䖑+m~Hl%-rJ-_Zk@f56)m'Oh>$!,}; l "P Jx)@Y  Ypcȓ"I0Ys$\0RP͗8M8fΟے8rOH]!BA JŶͩXFʵׯ`Ê ٳhђ]+۳lzV]Vt݋*oZIUp +ބǘLʘ!]9Π }zt+ft:\˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&ƒL6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮘Fԛ+f.>KmF[r̂؊l4!,0}UHP0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰCf-Ȑ!,>}F8$0I%]`(di,TtmxmpH, l:ШPٓZجImmx\z]4|tnV}A$ H2CH*\bÉ,ZĸM,cE<>M$DL^DiM%C-]¬2Lj5oN˩3Ϟ~"I!,x>"9h0I8= ȍdih_+tm8 |,H0l:RZi zoZwL.zv-۳~_4~7uAvثݪ (j ",!,x>"+(0I8k &dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~m)%²dz̹ ѯ@ !,%>FBڋ޼H扦ʶkL ĢL*̦ JԪj!,L}MV޼HT!,>}(0I8ͻ dYhlp, fMx|6oH,Vl:ШniZجA=mXֽzRx#=_{y=ak `[OC3)ǝ !,>}HR0I]`(di^,TtmxmpH, l:ШPٓZجImmx\z]4|tnV}A$cсzslh`YVOH pA*pÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& ~e!,>}88$0I%]`(diA+tmxnpH,_ql:PfZجuzm贺YN^|E.o l@| =q",èQ ǎ>D'S!,x>"FH0I8k&dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~NɵH< CP!F "TH0I8k&dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~' o APځ8Pb-P#=~) H%ٜLf%K3._Y)!,L}T8$0I%]`(dhfQtmx>|pH,_ql:PfZجuze贺YF^|E. H`5!LȰÇ#JHŋ3jȱFǏ C4ɓ'GCҤb-[$a5Q3N`=]PE* @i/NyAk*U\V?%!,xZ"T8$0I8y`vdi sl뾭(pmx}~HV0I5]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|Nxbk[M<  l Ae:Hf!Ç#JHŋ3jȱǏ CId&Lɲ˗0cʜI͛8sɳϟW!tѣH*]ʴӧPJJիX1,jbV`ÊKٳhӪ]˶۷p @]r[7|W0w AWq}LWdʖP]>"ztv!,xh"pHR0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.+nxMoz}]oYpTvPlMIE@;61,'оӺֶٲ7% H*\ȰÇ#JHŋ3jȱ{Ǐ CIɓ\ɒ%(-c| ELOlDgKM|DLJDT)$J c(QF%!,%T-ګ޼H扦ʶ L !,%b4ڋ޼/H扦ʶ L ĢL*!,x"9ڋ޼g H扦ʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZjz +;K[k{+R!,x"bHR0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.+nxMoz}]oYpTvPlMIE@;6U% H*\ȰÇ#Jŋ'*4$;~D#%142J"-=L5o @g> c(QF%!,x"T8$0I8y`vdi sl뾭(pmx!,b|q cl! ,} B l "P JHEB&4XGxCL%Ǔ) 0 QE1srC@% &u ҥ'*6G+k$,  *Pj`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(㌒D8樣ώ@H#!A9 Ey I$MdBNIO^Tfy]r%c>ihlp)tix|矀HA Q&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸b듾+Kl.. @BR+*2!,0} HV0I5]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhǴ`6۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCM:^͚40mڶoέ6ޮWxu'gNسkνËOӫ_OOx|W ~֗`~ނ5 !,x"*8&0I8y`vdi sl뾭(pmxzI!,x"T8$0I8ͻ dY lp,fMx|6pH,sql:IsJZAuZ7xLGz:<:~Kz:  AP!8P"-P=~)R%L g%5._y$!,x"b8&0I8ͻ dY lp,fMx|6pH,sql:IsJZAuZ7xLGz:<:~Kz:2 ;D H*\ȰÇ#JHŋ3j,=Ǐ7zIң.%K䒒--A3*5MޤN=N@F Md)#Ne$!,x"HP0I8ͻ`(NJ!,x"U8&0I8ͻ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz:*EH\80a P-hP& =)PH& 2ʖp^\#sfؼC`A!,x"Gh0I8`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~d,e c@zDP†H"EE/"ʨ v:>"@.!,x"9h0I8= ȍdih_+tm8 |,H0l:RZi zoZwL.zv-۳~_4~7ɉ,Ίثݪ (j ",!,x"+(0I8k &dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~)%²dz̹ ѯ+ !,%TIڋ޼H扦ʶ L ĢL*̦ JԪjܮ !,b?Uֈzʅ(}yLU >Ǜ ̅ =<),!,x"HR0I8 ȍh0m,ϴ_kjpHtpŢϗl:-{լv; L."3IREtcM_w~|}~n`UY !,x"8$0I8 &d`(J°p,h{t.pH݈HBl:І-yլv;k L.!sISH4aǛ ̅ =<),!,x"HR0Iͻ`(hlBt]xuP,ri:: J]1X\uˍHp&K.wh7ܷ}ϪyI~NZ^R"=3V({B !,x"ڋ޼g!,x"HR0I 뽯`dYh0m,C\͸jdd l:b Tج%lgVwL.7.SH bǛ ̅ =<),!,x"8$09ͻD $ h0mLd%lx V!ql:;D3M,.%xLp5ؔngmE} {|H!neZ !,b?Uֈzʅ(}yLU >Ǜ ̅ =<),!,b?Uֈzʅ(}yLU >Ǜ ̅ =<),!,}b?Uֈzʅ(}yLU >Ǜ ̅ =<),!,x"8&f0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B0 y7~#ztjbZӝ !,x"ڋ޼g!,7c; Hh@"Cq! HËPq ƏPtKY S&) *c>ðq2s&3't 5 -,*-`ǥPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfz0@ $ț bS#N".Xb2(a&ވ4c?C¨E x1PF)TViXf\v`)dihlp)tix|矀jh.h"*aBJRi:颟>:饄Z騤6!,x"G8$0wͻ`(di Zl,tm8yP Ȥri*Шtsfجv¸x魌购 P;N^R}B`, ~vphɋ[ H*\ȰÇ#JHŋ3jȱ Ǐ IE!,x"G(0*@2ͻ`(diWZlp,ϝx|)pH,k@ql:aIsJZuxLfz-9;N[ވ9},# ytle]UM H*\ȰÇ#JHŋ3jܨ +y,!,*1ڋ޼H_ʶ L Ģ,!,FBڋ޼H扦ʶkL ĢL*̦ JԪj!,}bPڋ޼H扦ʶ L- ĢL*̦ JԪjܮ N!,}pBZiHf'fʶiƲ*c e*"%%B'a!,pBZiHf'fʶiƲ*c e*"%%B'a!,bq(ݼ{eHWʡ) ǎ6LknbgLR&.R'ܮ׫U~pL6cuu`9^\wh3X(s2X!,T*|({޼c܅]Ne*,o !Q<J!{BԪjܮ NՀ^CtQy7(Phwv X9(IX!,F8|("C^gI&ʥ O.:qB[;"Yʥ JԪjܮ N UǓ76'hh8Xh7yXiY!,x"FH0ͻ`(di_^hp,ϴu|K7oH,l:'JZRuxLFz 9N_:}ȃɵH< CP!F ̹.ͻ 8 0? >> ^̓QF<~rdLAr˖o^l#Z!,x"b8$0I8S»`(dih"p,]Fx|pH,H0l:P2JZ)vzCگxLfv^p0&N~M}?d.H*\ȰÇ#J ŋ1R$5ōc2 *T p !,x"bHR0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.h`n{Ύt}?_~n[oVuRkOKGB=8 H*\ȰdHH;f*jp2ƍ|2Ȋ%J-S̚Df9D'O!> *w !,x"F8$0I8Z(di l_ܽtmx㲼pHbŤrln tJQvbgݰxLre,~M~@*w'{JpB Nj@$#ah#F?RH!,x"88$0I8Z(di l_ܽtmx㲼pHbŤrln tJQvbgݰxLre,~M~@Μϳֲ۱߰,%p`!,x"*8&0I8Z(di l_ܽtmx㲼pHbŤrln tJQvbgݰxLre,~M~@| JM5-&˹"кڶ !,x"8$0I8Z(di l_ܽtmx㲼pHbŤrln tJQvbgݰxLre,~M~@.,|vph]U !,x"FHR0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.϶zͶ|N~yyسwמϿ5W .HAJaLH^h_FRb#:X$!,x"+ڋ޼g H扦ʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZZbR!,x"F(oI8ͻ`(d@clp,i:x<]CpH,~@rltJb*v-bݰx 贚c^6|N,!,x"T8$D0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B*J 8L*h… TU@+^? a, fRH!,x"8&f0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B H*\ȰÇ#JHŋ3jȱdǏ CIɓ(S\ɲ˗0cʤA8s朙b@qD4E5TMw>UTUo^Śuk+ٲ΢ݠv->݊[ !,x"8&f0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B H*\ȰÇ#JHŋ3jȱdǏ CIɓ(S\ɲ˗0cʤA8s朙b@qD4E5TMw>UTUo^Śuk+ٲ΢ݠv->݊[ !,x"8&f0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B H*\ȰÇ#JHŋ3jȱdǏ CIɓ(S\ɲ˗0cʤA8s朙b@qD4E5TMw>UTUo^Śuk+ٲ΢ݠv->݊[ !,xv"8&f0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B H*\ȰÇ#JHŋ3jȱdǏ CIɓ(S\ɲ˗0cʤA8s朙b@qD4E5TMw>UTUo^Śuk+ٲ΢ݠv->݊[ !,xh"8&f0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B H*\ȰÇ#JHŋ3jȱdǏ CIɓ(S\ɲ˗0cʤA8s朙b@qD4E5TMw>UTUo^Śuk+ٲ΢ݠv->݊[ !,xZ"8&f0F8ͻ`(diTR p,tm߸y ȤrY*JШtsNجvx}购 P;N^R}B H*\ȰÇ#JHŋ3jȱdǏ CIɓ(S\ɲ˗0cʤA8s朙b@qD4E5TMw>UTUo^Śuk+ٲ΢ݠv->݊[ !,L}8&0I5]`ndihlk4)`| ȤrlΊ7tJZ'z`v.8nxe{Lbs H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXju\`ÆGYd=6ZmܱuE7ޯ} oo #vxqƎ@܅ $!,>}8&0I5]`ndihlk4)`| ȤrlΊ7tJZ'z`v.8nxe{Lbs H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXju\`ÆGYd=6ZmܱuE7ޯ} oo #vxqƎ@܅ $!,0}8&0I5]`ndihlk4)`| ȤrlΊ7tJZ'z`v.8nxe{Lbs H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXju\`ÆGYd=6ZmܱuE7ޯ} oo #vxqƎ@܅ $!,"}8&0I5]`ndihlk4)`| ȤrlΊ7tJZ'z`v.8nxe{Lbs H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXju\`ÆGYd=6ZmܱuE7ޯ} oo #vxqƎ@܅ $!,x""q( 0J骽8ͻ`(dMhdlp,i:x<]OpH,~@rltJb*v-b!ݰx 贚c^6|Nz9;N[ވ9} H*ÇnHѡ++^4*!~2'%-tJ-_6i@&6id'O"> 'D $!,x""U(0*@2ͻ`(diWZlp,ϝx|)pH,k@ql:aIsJZuxLfz-9;N[ވ9}zG@v DXGa?tG@o,ØQ#G6|\r&ˠL9f%K+x 'f!,x""G(0Jwͻ`(diVhp,tbu|K7oH, l:'eJZRu,xLx^e0Nc(<}c5HN@ p†BD$qP ! ,"} АʘػӪ۔ H`"@6#ATHĊ3j4Ə "G!S,V *$ISW$BF8QV09ѣ.QbCPJТLM*H'TJl4 h)Z-׎nʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(c4$8樣(;㌹)$$.(#8#H9$X#Yj+Z+aJffK !,"}HR0I]`hdiblWtmөxpH,6v;rl:PRZجuzzE贺Y|&^|E.ǫ2ib2("`@=aJqċ* 8Q==1H%O6L!˖_\o6sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞] ' !,L}MV޼HT!,0}8&0I5]`hdiblWtmөxpH,6v;rl:PRZجuzzE贺Y|&^|E.whYMA3"ƞ  k" !,}0bdֈzʅ(}yLU >Ǜ ̅ =<)i!s1l5~r5^CE=T!,"}88$0I ]#`(dihlp,tx{zpH,url:Pg2JZoz+.49n |N_~WZy@+t.*&Eswu >!,bq #с,!,"}THR0I] `(dhlpurmewpH,vHql:%I IجvTݰxL.{`zn7I|N;~ϟ}Y:B9+J$#b4n' H*\ȰÇ#JHEr2jܸh< 5z2Hx%A|Jw-;dSu53ęs|*Ѣ"s)ӚNƌ:N)oVv˪u׮پ&vlfMm-gn6+w]} !,"}9MV޼Hʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZjz +;K[k{ ,}88$0I%]`hdiblWtmөxpH,6v;rl:PRZجuzzE贺Y|&^|E.P@ԗ| =q",è ǎ>D'S!,L}*8&0I5]`hdiblWtmөxpH,6v;rl:PRZجuzzE贺Y|&^|E.c Fыxqjb^XQD E "\D!Ç7*J!,xZ"8$0I39`(di lvtmxGqpH`ŤrlRtJvbeݰxLjj^pc;N~LxB]#zqkc[Su !,xh"8$0I39`($`(J°p w&x^V^l:DsjTج,zcwLp=%Rg{`xg\M}F{n9e,^TMGy !,hHڋ_'"q㉦V& U L2s|Jyѩ5VjS٭gA!,h~z( >٦扚d Lת^9 .|G $rB7 b- c,N3Znq\\?yH6HXxUx95IX!,"}pHR0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|Nxbk[O, ³LtW H*\ȰÇ7Hbň,jƍI}rH%EJ-?S̚fI'ON>j *ѢH*]ʴӧPJJիXjʵ `ÊسaAV-gށ;VdV`,X`N g1c7#!, hb8(ݼ{eHWʡ) ǎ6LknbgLR&.R'jܮ N (8Phxxhȸ'Y pY) IY(jIZZ!,xh"FHR0I8]`vdi l0$`lxFӽpH, 3rl:=sJZKQuV5xL?z}:<:~ϷF9*{p@z%\P†B|D/2G& !,0}P*\Ȱ >Dqŋ3jȱǏ CtXqɓ(S\ɲ˗09&I͛8s|I ; JѐpQ`ѧPJC0PʵׯV3 tٳhT@B̙RʨK]̙RH̻ >7q ^̸qQk R츲/ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)di晝hp)tix|hS j(Ju衉FB>5頕u)韝ziD} CPS\kj뭸뮼+,kI6I2;H"{H&-H:G;.QroK/F!,0}HV0I5]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~] vKcPUDp(Ŋ.b$qǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjuN_KٳhӪ]˶۷pʝKݻx˷߿ n$È'QxŐ;9 !_YZhFz\-;7n㮡G!,xh"UHP0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~1%M% !,3h*b<61'2Rr'nѺo|mxÔBL*̦ JԪjܮ N (8H2u(4TtyR!,xh"b8$0I8ͻA(g lpܑ(x|4pH,sql:IsJZMuZ6xLCz}:<:~P;z H*\ȰÇŋL5ň^ * ѢKI!,3h*b\ڋ޼H扦ʶ L ĢL*̦ JԪhܮ N !,%hFbڋ޼H扦ʶk L ĢL*̦ JԪjܮ N (8HXhx)Piy *:JZjz +;K[k{[[!,xh"bHV0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.϶x\:{~az]rYsT|PoMIE@n9 H*\ȰÇ#JHq3fD6G|H%Ԙʍ-s̋5m)&A|"tFy M!,xv"THV0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.϶x\:{~az]rYsT|PoMIE@n9H`^ *$… 73($̼ ѽ۹ !,x"88&09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}Bj >~*ֿԺ(0"lpY!,x"T8$09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}B[  AP8P-Pc=~)%L g%K7._Y,!,xv"b8$09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}B H*\ȰÇqHŋM\5ňayr,Sy N>f":`!,L}HR0I] $ hlRtm[e>|pHtŤrl:c JZجC:z5贺Yޭ|^|E.2˸ H*\ȰÁ(ŋ3jȱǏ CIɓ(SGq%.cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶̯oʝKݻx˷߿$È$,Â<~dǕ\NYfŝ|håMNtZ\"{ڶ!,L}8$0I%]dYhlTtmfN|pHtŤrl:cJZجCjz=贺Yޭ|^|E. H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶[g2KnݎqK-|~Wv kA|W1}_~"zt!,xL"h0I'8ʻ`(di袭Xp,ϴ̲u|KoH,l:(JZuxLx^e0Nc(<} H*\ȰÇ#JHŋ3jȱ]Ǐ CIɓ(S\ɲIf͉/cɓfK={,PEmM}>uTU~@^; [a, fq<+!,xL"qh0I+8km`(diYp,ϴ̲u|KoH,l:(%JZuxLx^e0Nc(<} H*\ȰÇ#JHŋ3jKIdɉC1ɒd-[5Mԑ=]tPE(@ Nc@b*VQ!,xL"ch0I 뽯`(dihpp,tm|+H,ȠЕl:PѲZ)zmwL.zv-۳~_4~ H*l5""J8q!P(9=ZQdMFAYQ]:QL6 3ΞH~Ոsh9 84 Ei!,xL"Uh0I9`(dih v즾p,tm|  H,ȠГl:PђZϩ zuZwL.zv-ۡ~3~DK 0B; >1D9^Q%6}r$LAR ˖e^#slY !,xL"Gh0I8{`(dih:^˭p,tmc{|\ H,ȅГl:Z zowL.zf-ۣ~O4~PDIF c@zDP†H"EE/"ʨ ǎ6Hd!,xL"9h0I8;Z(dih_ۭp,tmc{|\ H,ȅl:Z zoZwL.zބ-ۣ~O4~|DuFԬܫB i A/<@!,xL"+(0I8k&dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~'+#Ʋ˸ Ю !,Ljڋ޼H扦ʶ L #L*̦ JԪjܮ N (8HXhxU!,L}aHP0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N2\ @ "A?!<=>ɋԖݹײ# :8; H*\P#FlHb7!ZZƌCȓ(UM+'I3RL5sDtΟ;{JТH[MTҦP->JuԪXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx%JV LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&eL6PF)TViXf\v`)dihlp)'iwˆv~狂hZ2|B ()VX !,x"ڋ޼g!,>}8$0I%]`(di@+tmxnpH,_ql:PfZجuzm贺YN^|E."whYMA3ɝ !,> ֶⲕوڙ& DzPqbJx2*$ :zqbL*[:1,v =EX81fXиIy PYyI*ZHZ X!,> ֶⲕوڙ& DzPqbJx2*$ :zqbL*[:1,v =EX81fXиIy PYyI*ZHZ X!,>}HP0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺk-XfH!,L}8$0I%]`hdiblp,ˮxwpțcĤrlN@t:جvۀ.* gF7 ;A~xzH:$r0!haZSrLeDT !,L(ڋ_'"q㉦V& U L2s|Jyѩ5VjS٭gdNln!pΨx('GfxȦX9IVi)Y!,L~*( >٦扚d Lת^9 .|G $rB7 b- N \gX7XchHX")Hby 0zRzjQZ!,L}8HR0I]`(``(JlRtm߸gpH,xD'rI!,L}F8$0I%]`(dhl[tm8hpH,x^|E.2 HC =07-v8Q*#7=n Qd6MbCPJ.̚n✦sgK!,xL"TH0I8Z(dih_ۭp,tmc{|\ H,ȅl:Z zoZwL.zބ-ۣ~O4~<M 80B; >D9 ^Q6~r$LAR ˖e^#H!,xL"bH0I8k&dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~ H*\ȰÇ#@E\X13=ZQdMFAQ]:IQL63ΞH~shE!,xL"pH0I8k&dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~ H*2f>Hŋ3jȱGǏ C ɓ(Q쑲ɕ<\CfK7liCJ4|:CIE"aM*Uժ(I!,>}8$0I%]`(diRA^tmxKlpH, l:ШPٓZجIemx\z]4|tnV}A$ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʕ_KٳhӪ]˶۷p@]r[7/|~Ww kAW1}_LWdʖPYQ>~"ztH!,x>"H0I8= ȍdih,_+tm8 |,H0l:RZi zoZwL.zv-۳~_4~ H*\ȰÇ#JHŋ3jȱkǏ CIɓ(S\ɲ˗0cʜI͛8szbTѣH*]ʴӧPJիWbjU+׭^f]*kزΖU+W\%\tI!,x>"H0I8[`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~ H*\ȰÇ#JHŋ3jȱ{Ǐ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]aL 8իXjʵׯ`+Kl٢fӒE6-۶g5v.sۖZn G!,x>"H0I8w`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXF@@@S^KٳhӪ]˶-pʕ[tݸuͫ.Ѿs pᾇ&xp~ H!,xL"H0I8w`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXF@@@S^KٳhӪ]˶-pʕ[tݸuͫ.Ѿs pᾇ&xp~ H!,xZ"8$0I8ͻ[ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz: H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯTL ٳhӪ]˶۷pKnݥvū7/߾ww0ƒVܗ^~ M!,xh"8$0I8ͻ[ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz: H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯTL ٳhӪ]˶۷pKnݥvū7/߾ww0ƒVܗ^~ M!,xv"H0I8w`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXF@@@S^KٳhӪ]˶-pʕ[tݸuͫ.Ѿs pᾇ&xp~ H!,x"ڋ޼g!,Owڋ޼/ H扦ʶ L ĢL*̦ JԪjܮ N '(8HXhx)9IyX!,A*ڋ޼Hʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZjzZ!,38ڋ޼H扦 L ĢL*̦ JԪjܮ N (8HXhx)9IYiyI *:JZjz +;K[k{ ,< Z!,%F~ڋ޼H扦ʶk L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:Jp +;K[k{ ,Ǜ ̅ =<)ijܮ N (8HXhx)9IYiy *:JZjz**;+[k ;l[<{|˚ܻ-=M][!,pT Z+ޜ_uHj Xz~ o63c~!(j0*GȥS6|J#ͩjխ N (8HXhx)9IYiy *:JZj *;hZ k <+ KL˛j -=Z!,>}HR0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|Nxbk[M@udX HYPɼ#JHŋ3jȱǏ C!&O\ɲ˗0cʜI͛8s&N< JѣH*]ʴӧPaԫXjʵׯ`ÊKlX8њ]˶۷pʝKݻxPk޿ LÈ+^_KL˘3k̹gMti^M֬]Z6ڦm}Zޭ}_>Zp-C }:RnËOArw>˟OwiϒE(h&XdB Dh]xaFa"#h"!,>2- *\ȰÇ#JHŋ3jaÁ*lIɓ(S\ٰ) H|`I͛8sL00: J(&6<0ʴӧP@PիXSׯ` bӪ]Ǐ Kݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@gDYd$HBM>Q:4U6tY.%]*a9f iAnYPr)x|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪦ hI+jjk lo[ګ2{B;+Vkfv+k覫t+okޫo/oKp /lp'cL1_w _!,x"+ڋ޼g H扦ʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZZbR!,x"8HR0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.9nxMoz}]oYpTvPlMIE@;6ʷ%V@XԶ۵ߴ%p!,x"8$0I8ͻ[ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz:(,|vpe]Uγ !,x"8$09ͻ`(h0mLd%x꣼pV $S:IEsJB>Vl%vv5Z4&g{NOugܒx~G{`BT;G79,%"V !,x"(0*@2ͻ`(diWZlp,ϝx|)pH,k@ql:aIsJZuxLfz-9;N[ވ9}bEC ~uld\TLDN !,x"*8$09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}B%3($ý ȸ͹ҿ ׵v !,x"8(0*@2ͻ`(diWZlp,ϝx|)pH,k@ql:aIsJZuxLfz-9;N[ވ9}b2C<ӢԶ۵ (.J!,x"F8$09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}B*aN% VBNB)1 qĊ.bq!,x"T8&09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}Bj > lBРB@ >D?^ܓ1"<~H%O)@%.f&M56_%!,x"b8$09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}B H*\ȰÇHbňa,j2ƍ|rH%J-S̚Sf)E'(>2 *T [ !,x"p8$09ͻ`(di h^l,tm8yP Ȥri*Шtsjجv¸x鵌购 P;N^R}B H*\ȰÇ#JHŋ3jȱFǏ CLɓ'GAҤJ!-[ L 5Q3=]PE(@ Nm@Jc*UV%!,x"pHR0I8km`(di Y+tmx°p(⽈Ȥr9ͨtJ=լv}\cݰxLbZfx-~L~?Q*8I@ H*\ȰÇ#JHŋ3jȱQǏ CIɓ(S&ە*˗0anc*͗*ܼsN=4PEřRM>i9bšu^iݖ!,x"p8$0I 뽯`(di lptmxc1pH|ŤrlNtJvbeݰxLfj^pc;N~LxB H*\ȰÇ#JHŋ3jȱIǏ CbS(S Wʗ(G S/mR'+}S'E4MqǛ ̅ =<)ijܮ N (8HXhx)9IYiy *:JZjz +;`{{ 8+ll< \l;}\m=\!,x"cHR0I8 ȍdi ld(pmx2 *T њ*ZQ!,~T\{޼{G&)ʶJ}oYDpTLʎ|JKѩSj#٭N (8HXhx)9IYiy *:JZjzz++9k[{kK;  \{|;  ]m}]!,x"F8$0I89`vdi sl뾭(pmx!,x"9h0I8k &dihzl,tm|,Hl:2Z) zowL.zv-۳~_4~NAuFԬܫB i A/<@!,x"+(0I8= ȍdih_+tm8 |,H0l:RZi zoZwL.zv-۳~_4~-)%űʷϭ  !,tڋ޼H扦ʶ L ĢL* JԪjܮ N (8HXhxX!,x"RHP0I8ͻ`(N 3hlp,tmx|pH,Ȥrl:ШtJZجvzxL.϶n[Ԃ! ,0}4Р*\Ȱ >Dqŋ3jȱǏ CtXqɓ(S\ɲ˗0cʜI͛8sf4apΟ@ JtT1F)ʴӧP[&Y իXBPCZÊKe<.D<(˶۷ȅܻxb' L [j+BL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_hϗK Zս PGGϾ}\} o3?2- H(!)F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔bWRd馛:uiQ**P*O)NA)L:*Eٚ)+QP+Tl:-F+Vkfvk' n!,0}HR0I]`(dihZp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI]rܹ&@u4P-E͒R,M{>U*Us^Śu+N+V ٲP΢uT oʝKݻx˷߿Lp? +&ō{9fƝq|Z7F@mC5_ψ-;mz !,x"88$0I8ͻ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz:(*^ccز߱R xj R6I!,x"F8&0I8ͻ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz:^ ;<y7ėPw >a,bq c' I!!,xv"T8$0I8ͻ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz:  AP!8P"-P=~)R%L g%5._.!,xh"b(0I8`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~X5|> H*\ȰÇ#JH3jȱ9Ǐ CI[HVr啖. e&M'6o&ɩϞE~"tFy Mzci!,xZ"p8$0I8ͻ $  lp,e-x|6pH,sql:sJZAQuZ#xLz}::~Kz: H*\ȰÇ#JHŋ3jȱIǏ C"U(SVrʗ(G S/mRg+}S'E4Mq<:c*V:-!,xL"~(0I8`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~X5 H*\ȰÇ#JHŋ3jȱ\Ǐ CIɓ(S\ɲ˗0cʜ)8si̦@s44E%tMy>UT Uq^Śukb+ٲ΢X!,x"ڋ޼g!,L}~8$0I ]`(dihl$ 4 x|pHdՎrl:ШZجvPݰxL.5zn{N۳q}ay2jQ<# H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳh)nVڶ%$wvͻg/_;~ , m#^xƎ@!,L*~ ZG3VuB#Ybg6/3Rom+L*̦ JԪjܮ N (8HXhxT#X1IbQi" *Y:JjZZ!,L}~HR0I]`(dhfQtmx>|pH,_ql:PfZجuze贺YF^|E. H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵqŠ;WdӊGZzܦG.Yx;GZrX08xƎ@f!,L}~8$0I%]`(dhfdtmx|pH,_ql:PfZجuze贺YF^|E. H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXj5xŠ;vWdӊGZzܦG.Yx;GZrX08xƎ@f!, LF~ڋ޼H扦ʶkL ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZjzʪX!,L}~HR0I]`( `(JlRtm߸gpH,x *QR!0!,"}Ɗϳɨߟڐ H *\ȰÇ#z @ŋ(ЁcƏ {Qpcȓ(e) dǔ0c 2s4c; e%q] JȴPCZ"ׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L~ (M1KLreč"c>d3͠5yѝMf׈Z_M{ϵkߞ[޴w[8ƏN4УKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8< ي!,L}T8$0I%]dYhlTtmfN|pHtŤrl:cJZجCjz=贺Yޭ|^|E. H*\ȰÇ#JHŋ3jȱFG2Id/!?1ɒb-[$a5MN`=]tPE*@i/NyAk*U\V$!,xZ"98&0I 뽯`(di lptmxc1pH|ŤrlNtJvbeݰxLfj^pc;N~LxB΋*cad۱ (f[ !,xh"8&0I 뽯`(di lptmxc1pH|ŤrlNtJvbeݰxLfj^pc;N~LxB#|zqkc[S] !,hvڋsKHvꊙ &Oy"Hh0*$ .Ҫj  lNOn=?(FXXvYU!,Z8sڈ扦I  `L:i`T]\۶ 1ԷN (8HXhx)9IYiy 9BJj: *{B[jk˩  2\qܡ\1,-=M]m}]!,xL"TH0I8[`vdihl'`+tm8 |,HPl:P2Z)zmwL.zv-۳~_4~GA *{ !,x"ڋ޼g!,Lb̔*؋1ݼ?eV ,L׮ >|$h Z̠2Ќ*j͢nO wN (8HXhx)9IYiy *:JZjz +;K[k{ , Pl||l̼*] p]- M],nM^Z!,Lb̔*؋1ݼ?eV ,L׮ >|$h Z̠2Ќ*j͢nO wN (8HXhx)9IYiy *:JZjz +;K[k{ , Pl||l̼*] p]- M],nM^Z!,Lb̔*؋1ݼ?eV ,L׮ >|$h Z̠2Ќ*j͢nO wN (8HXhx)9IYiy *:JZjz +;K[k{ , Pl||l̼*] p]- M],nM^Z!,Lb̔*؋1ݼ?eV ,L׮ >|$h Z̠2Ќ*j͢nO wN (8HXhx)9IYiy *:JZjz +;K[k{ , Pl||l̼*] p]- M],nM^Z!,}Lb̔*؋1ݼ?eV ,L׮ >|$h Z̠2Ќ*j͢nO wN (8HXhx)9IYiy *:JZjz +;K[k{ , Pl||l̼*] p]- M],nM^Z!,xL"U(0I!뽯`(dihpp,tm|+H,ȠЕl:PѲZ)zmwL.zv-۳~_4~u2@}F(0Ik8km`(dihfl,tmxNpH \Ťrl:%sJZ( zKL.'1n 5N۩~&p H*\ȰÇ#JtV*Dv/˨1&ǎ>,'r丒&áLm%n._n)3͚n⬦`!,0}b8$0I%]`ndihlk4)`| ȤrlΊ7tJZ'z`v.8nxe{Lbs H*\ȰÇ#JHŋ3jȱ_{2>b )ɓ(S\ɲ˗0cʜI͛89ɳgϜ| СE]RMY=UTUS]ݙU֮{J,YSfϒJVۀ ! ,"}c  ūϟȚجқ۱ mNC JG`@aQƎ CI$A$hjEL˛8, @@jE'B*HSJ:ʧ)NP FXѵYC(TXPۮ=Kݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸s;T؀ X㿋cF\eǝ[\ze­Oƞ\d/>2x!Ox=//Ͽ(h& 6F(Vhfv q"2H !<,"vx H*\!JHŋ3jȱDž#~Iɓ(S\ң'\iI͛8s4@!CI %CaOC Q(dFUx γhӪ]Q)H*lQC8d葤I`t:Х]D8deHI0˘3k9apQz;-ihJ-%hZtX,Y̻1 t*X+QAG]} Q5N6 ˩eӫ|PM-QUM{^Aj%r}$=eT58 ZڍE 4(gZ6v GІA,tSuJ}m~p"XAzgr Y">ꘈV-6 % XPrqU%"Y"_!bAPrpi;brick-1.9/programs/0000755000000000000000000000000007346545000012500 5ustar0000000000000000brick-1.9/programs/AttrDemo.hs0000644000000000000000000000531707346545000014561 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE CPP #-} module Main where #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid #endif import Graphics.Vty ( Attr, white, blue, cyan, green, red, yellow , black, withURL ) import Brick.Main import Brick.Types ( Widget ) import Brick.Widgets.Core ( (<=>) , withAttr , vBox , str , hyperlink , modifyDefAttr ) import Brick.Util (on, fg) import Brick.AttrMap (attrMap, AttrMap, attrName) ui :: Widget n ui = vBox [ str "This text uses the global default attribute." , withAttr (attrName "foundFull") $ str "Specifying an attribute name means we look it up in the attribute tree." , 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 (attrName "general" <> attrName "specific") $ str "In this way we build complete attribute values by using an inheritance scheme." , withAttr (attrName "foundFull") $ str "You can override everything ..." , 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. 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 globalDefault = white `on` blue theMap :: AttrMap theMap = attrMap globalDefault [ (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 () e () app = App { appDraw = const [ui] , appHandleEvent = resizeOrQuit , appStartEvent = return () , appAttrMap = const theMap , appChooseCursor = neverShowCursor } main :: IO () main = defaultMain app () brick-1.9/programs/BorderDemo.hs0000644000000000000000000000472707346545000015070 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Main where #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, on) import qualified Brick.AttrMap as A import Brick.Types ( Widget ) import Brick.Widgets.Core ( (<=>) , (<+>) , withAttr , vLimit , hLimit , hBox , updateAttrMap , withBorderStyle , txt , str ) import qualified Brick.Widgets.Center as C import qualified Brick.Widgets.Border as B import qualified Brick.Widgets.Border.Style as BS styles :: [(T.Text, BS.BorderStyle)] styles = [ ("ascii", BS.ascii) , ("unicode", BS.unicode) , ("unicode bold", BS.unicodeBold) , ("unicode rounded", BS.unicodeRounded) , ("custom", custom) , ("from 'x'", BS.borderStyleFromChar 'x') ] custom :: BS.BorderStyle custom = BS.BorderStyle { BS.bsCornerTL = '/' , BS.bsCornerTR = '\\' , BS.bsCornerBR = '/' , BS.bsCornerBL = '\\' , BS.bsIntersectFull = '.' , BS.bsIntersectL = '.' , BS.bsIntersectR = '.' , BS.bsIntersectT = '.' , BS.bsIntersectB = '.' , BS.bsHorizontal = '*' , BS.bsVertical = '!' } borderDemos :: [Widget ()] borderDemos = mkBorderDemo <$> styles mkBorderDemo :: (T.Text, BS.BorderStyle) -> Widget () mkBorderDemo (styleName, sty) = withBorderStyle sty $ B.borderWithLabel (str "label") $ vLimit 5 $ C.vCenter $ txt $ " " <> styleName <> " style " titleAttr :: A.AttrName titleAttr = A.attrName "title" borderMappings :: [(A.AttrName, V.Attr)] borderMappings = [ (B.borderAttr, V.yellow `on` V.black) , (B.vBorderAttr, fg V.cyan) , (B.hBorderAttr, fg V.magenta) , (titleAttr, fg V.cyan) ] colorDemo :: Widget () colorDemo = updateAttrMap (A.applyAttrMappings borderMappings) $ B.borderWithLabel (withAttr titleAttr $ str "title") $ hLimit 20 $ vLimit 5 $ C.center $ str "colors!" 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")) main :: IO () main = M.simpleMain ui brick-1.9/programs/CacheDemo.hs0000644000000000000000000000473307346545000014653 0ustar0000000000000000{-# 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 brick-1.9/programs/CroppingDemo.hs0000644000000000000000000000313107346545000015420 0ustar0000000000000000{-# 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 () brick-1.9/programs/CustomEventDemo.hs0000644000000000000000000000362007346545000016116 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE CPP #-} module Main where import Lens.Micro ((^.)) import Lens.Micro.TH (makeLenses) import Lens.Micro.Mtl import Control.Monad (void, forever) 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 , halt ) import Brick.AttrMap ( attrMap ) import Brick.Types ( Widget , EventM , BrickEvent(..) ) import Brick.Widgets.Core ( (<=>) , str ) data CustomEvent = Counter deriving Show data St = St { _stLastBrickEvent :: Maybe (BrickEvent () CustomEvent) , _stCounter :: Int } makeLenses ''St drawUI :: St -> [Widget ()] drawUI st = [a] where a = (str $ "Last event: " <> (show $ st^.stLastBrickEvent)) <=> (str $ "Counter value is: " <> (show $ st^.stCounter)) appEvent :: BrickEvent () CustomEvent -> EventM () St () appEvent e = case e of VtyEvent (V.EvKey V.KEsc []) -> halt VtyEvent _ -> stLastBrickEvent .= (Just e) AppEvent Counter -> do stCounter %= (+1) stLastBrickEvent .= (Just e) _ -> return () initialState :: St initialState = St { _stLastBrickEvent = Nothing , _stCounter = 0 } theApp :: App St CustomEvent () theApp = App { appDraw = drawUI , appChooseCursor = showFirstCursor , appHandleEvent = appEvent , appStartEvent = return () , appAttrMap = const $ attrMap V.defAttr [] } main :: IO () main = do chan <- newBChan 10 void $ forkIO $ forever $ do writeBChan chan Counter threadDelay 1000000 let buildVty = V.mkVty V.defaultConfig initialVty <- buildVty void $ customMain initialVty buildVty (Just chan) theApp initialState brick-1.9/programs/CustomKeybindingDemo.hs0000644000000000000000000002401707346545000017123 0ustar0000000000000000{-# 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 brick-1.9/programs/DialogDemo.hs0000644000000000000000000000357407346545000015051 0ustar0000000000000000{-# 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 , str ) import qualified Brick.Widgets.Dialog as D import qualified Brick.Widgets.Center as C import qualified Brick.AttrMap as A import Brick.Util (on, bg) import qualified Brick.Types as T data Choice = Red | Blue | Green deriving Show 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 :: BrickEvent Name e -> T.EventM Name (D.Dialog Choice Name) () appEvent (VtyEvent ev) = case ev of V.EvKey V.KEsc [] -> M.halt V.EvKey V.KEnter [] -> M.halt _ -> D.handleDialogEvent ev appEvent _ = return () initialState :: D.Dialog Choice Name initialState = D.dialog (Just $ str "Title") (Just (RedButton, choices)) 50 where choices = [ ("Red", RedButton, Red) , ("Blue", BlueButton, Blue) , ("Green", GreenButton, Green) ] theMap :: A.AttrMap theMap = A.attrMap V.defAttr [ (D.dialogAttr, V.white `on` V.blue) , (D.buttonAttr, V.black `on` V.white) , (D.buttonSelectedAttr, bg V.yellow) ] 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.appAttrMap = const theMap } main :: IO () main = do d <- M.defaultMain theApp initialState putStrLn $ "You chose: " <> show (D.dialogSelection d) brick-1.9/programs/DynamicBorderDemo.hs0000644000000000000000000000377507346545000016377 0ustar0000000000000000{-# 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) brick-1.9/programs/EditDemo.hs0000644000000000000000000000523307346545000014531 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE RankNTypes #-} module Main where import Lens.Micro import Lens.Micro.TH import Lens.Micro.Mtl import qualified Graphics.Vty as V import qualified Brick.Main as M import qualified Brick.Types as T import Brick.Widgets.Core ( (<+>) , (<=>) , hLimit , vLimit , str ) 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 { _focusRing :: F.FocusRing Name , _edit1 :: E.Editor String Name , _edit2 :: E.Editor String Name } makeLenses ''St drawUI :: St -> [T.Widget Name] drawUI st = [ui] where e1 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit1) e2 = F.withFocusRing (st^.focusRing) (E.renderEditor (str . unlines)) (st^.edit2) 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 (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.editFocusedAttr, V.black `on` V.yellow) ] appCursor :: St -> [T.CursorLocation Name] -> Maybe (T.CursorLocation Name) appCursor = F.focusRingCursor (^.focusRing) theApp :: M.App St e Name theApp = M.App { M.appDraw = drawUI , M.appChooseCursor = appCursor , M.appHandleEvent = appEvent , M.appStartEvent = return () , M.appAttrMap = const theMap } main :: IO () main = do st <- M.defaultMain theApp initialState putStrLn "In input 1 you entered:\n" putStrLn $ unlines $ E.getEditContents $ st^.edit1 putStrLn "In input 2 you entered:\n" putStrLn $ unlines $ E.getEditContents $ st^.edit2 brick-1.9/programs/EditorLineNumbersDemo.hs0000644000000000000000000001120107346545000017226 0ustar0000000000000000{-# 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 brick-1.9/programs/FileBrowserDemo.hs0000644000000000000000000000670407346545000016073 0ustar0000000000000000{-# 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) brick-1.9/programs/FillDemo.hs0000644000000000000000000000072707346545000014535 0ustar0000000000000000module 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 brick-1.9/programs/FormDemo.hs0000644000000000000000000001226707346545000014554 0ustar0000000000000000{-# 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 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 <- V.mkVty =<< V.standardIOConfig 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') brick-1.9/programs/HelloWorldDemo.hs0000644000000000000000000000015607346545000015716 0ustar0000000000000000module Main where import Brick ui :: Widget () ui = str "Hello, world!" main :: IO () main = simpleMain ui brick-1.9/programs/LayerDemo.hs0000644000000000000000000000574407346545000014727 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} module Main where #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 qualified Graphics.Vty as V import qualified Brick.Types as T import Brick.Types (locationRowL, locationColumnL, Location(..), Widget) import qualified Brick.Main as M import qualified Brick.Widgets.Border as B 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 { _middleLayerLocation :: T.Location , _bottomLayerLocation :: T.Location } makeLenses ''St data Name = MiddleLayerElement deriving (Ord, Eq, Show) drawUi :: St -> [Widget Name] drawUi st = [ C.centerLayer $ B.border $ str "This layer is centered but other\nlayers are placed underneath it." , arrowLayer , middleLayer st , bottomLayer st ] arrowLayer :: Widget Name arrowLayer = let msg = "Relatively\n" <> "positioned\n" <> "arrow---->" in relativeTo MiddleLayerElement (Location (-10, -2)) $ withDefAttr arrowAttr $ str msg middleLayer :: St -> Widget Name middleLayer st = translateBy (st^.middleLayerLocation) $ reportExtent MiddleLayerElement $ B.border $ str "Middle layer\n(Arrow keys move)" bottomLayer :: St -> Widget Name bottomLayer st = translateBy (st^.bottomLayerLocation) $ B.border $ str "Bottom layer\n(Ctrl-arrow keys move)" 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 (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 (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt appEvent _ = return () arrowAttr :: AttrName arrowAttr = attrName "attr" app :: M.App St e Name app = M.App { M.appDraw = drawUi , M.appStartEvent = return () , M.appHandleEvent = appEvent , M.appAttrMap = const $ attrMap V.defAttr [(arrowAttr, fg V.cyan)] , M.appChooseCursor = M.neverShowCursor } main :: IO () main = void $ M.defaultMain app $ St (T.Location (20, 5)) (T.Location (0, 0)) brick-1.9/programs/ListDemo.hs0000644000000000000000000000605707346545000014564 0ustar0000000000000000{-# LANGUAGE CPP #-} module Main where 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 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.AttrMap as A import qualified Data.Vector as Vec import Brick.Types ( Widget ) import Brick.Widgets.Core ( (<+>) , str , vLimit , hLimit , vBox , withAttr ) import Brick.Util (fg, on) 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.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 brick-1.9/programs/ListViDemo.hs0000644000000000000000000000605207346545000015056 0ustar0000000000000000{-# 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 brick-1.9/programs/MouseDemo.hs0000644000000000000000000001203607346545000014733 0ustar0000000000000000{-# 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 "") brick-1.9/programs/PaddingDemo.hs0000644000000000000000000000273307346545000015214 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main where import Brick.Main (App(..), neverShowCursor, resizeOrQuit, defaultMain) import Brick.Types ( Widget ) import Brick.Widgets.Core ( vBox , hBox , str , padAll , padLeft , padRight , padTop , padBottom , padTopBottom , padLeftRight , Padding(..) ) 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 = vBox [ hBox [ padLeft Max $ C.vCenter $ str "Left-padded" , B.vBorder , padRight Max $ C.vCenter $ str "Right-padded" ] , B.hBorder , hBox [ padTop Max $ C.hCenter $ str "Top-padded" , B.vBorder , padBottom Max $ C.hCenter $ str "Bottom-padded" ] , B.hBorder , hBox [ padLeftRight 2 $ str "Padded by 2 on left/right" , B.vBorder , vBox [ padTopBottom 1 $ str "Padded by 1 on top/bottom" , B.hBorder ] ] , B.hBorder , padAll 2 $ str "Padded by 2 on all sides" ] app :: App () e () app = App { appDraw = const [ui] , appHandleEvent = resizeOrQuit , appStartEvent = return () , appAttrMap = const $ attrMap V.defAttr [] , appChooseCursor = neverShowCursor } main :: IO () main = defaultMain app () brick-1.9/programs/ProgressBarDemo.hs0000644000000000000000000000654707346545000016106 0ustar0000000000000000{-# 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 , updateAttrMap , overrideAttr ) import Brick.Util (fg, bg, on, clamp) data MyAppState n = MyAppState { _x, _y, _z :: Float } 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 lbl c = Just $ show $ fromEnum $ c * 100 bar v = P.progressBar (lbl v) v ui = (str "X: " <+> xBar) <=> (str "Y: " <+> yBar) <=> (str "Z: " <+> zBar) <=> str "" <=> str "Hit 'x', 'y', or 'z' to advance progress, 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 'q') [] -> M.halt _ -> return () appEvent _ = return () initialState :: MyAppState () initialState = MyAppState 0.25 0.18 0.63 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" 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) , (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 brick-1.9/programs/ReadmeDemo.hs0000644000000000000000000000066507346545000015045 0ustar0000000000000000module 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 brick-1.9/programs/SuspendAndResumeDemo.hs0000644000000000000000000000306207346545000017067 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE CPP #-} module Main where import Lens.Micro ((^.)) import Lens.Micro.TH (makeLenses) import Control.Monad (void) #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid #endif import qualified Graphics.Vty as V import Brick.Main ( App(..), neverShowCursor, defaultMain , suspendAndResume, halt ) import Brick.AttrMap ( attrMap ) import Brick.Types ( Widget , EventM , BrickEvent(..) ) import Brick.Widgets.Core ( vBox , str ) data St = St { _stExternalInput :: String } makeLenses ''St 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 :: BrickEvent () e -> EventM () St () appEvent (VtyEvent e) = case e of 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 } _ -> return () appEvent _ = return () initialState :: St initialState = St { _stExternalInput = "" } theApp :: App St e () theApp = App { appDraw = drawUI , appChooseCursor = neverShowCursor , appHandleEvent = appEvent , appStartEvent = return () , appAttrMap = const $ attrMap V.defAttr [] } main :: IO () main = void $ defaultMain theApp initialState brick-1.9/programs/TableDemo.hs0000644000000000000000000000321707346545000014673 0ustar0000000000000000{-# 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 brick-1.9/programs/TabularListDemo.hs0000644000000000000000000001012507346545000016066 0ustar0000000000000000{-# 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 brick-1.9/programs/TailDemo.hs0000644000000000000000000001141607346545000014535 0ustar0000000000000000{-# 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 cfg <- V.standardIOConfig vty <- V.mkVty cfg chan <- newBChan 10 -- Run thread to simulate incoming data void $ forkIO $ generateLines chan void $ customMain vty (V.mkVty cfg) (Just chan) app initialState brick-1.9/programs/TextWrapDemo.hs0000644000000000000000000000147407346545000015425 0ustar0000000000000000{-# 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 brick-1.9/programs/ThemeDemo.hs0000644000000000000000000000421007346545000014700 0ustar0000000000000000module 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 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 brick-1.9/programs/ViewportScrollDemo.hs0000644000000000000000000000525607346545000016647 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} 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 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, Vertical, Both) ) import Brick.AttrMap ( attrMap ) import Brick.Widgets.Core ( hLimit , vLimit , hBox , vBox , viewport , str ) data Name = VP1 | VP2 | VP3 deriving (Ord, Show, Eq) drawUi :: () -> [Widget Name] drawUi = const [ui] where ui = C.center $ B.border $ hLimit 60 $ vLimit 21 $ vBox [ pair, B.hBorder, singleton ] 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 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 VP2 Horizontal $ str "Press left and right arrow keys to scroll this viewport." ] vp1Scroll :: M.ViewportScroll Name vp1Scroll = M.viewportScroll VP1 vp2Scroll :: M.ViewportScroll Name vp2Scroll = M.viewportScroll VP2 vp3Scroll :: M.ViewportScroll Name vp3Scroll = M.viewportScroll VP3 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 () e Name app = M.App { M.appDraw = drawUi , M.appStartEvent = return () , M.appHandleEvent = appEvent , M.appAttrMap = const $ attrMap V.defAttr [] , M.appChooseCursor = M.neverShowCursor } main :: IO () main = void $ M.defaultMain app () brick-1.9/programs/ViewportScrollbarsDemo.hs0000644000000000000000000001134007346545000017506 0ustar0000000000000000{-# 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 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 , withVScrollBarHandles , withHScrollBarHandles , withClickableHScrollBars , withClickableVScrollBars , ScrollbarRenderer(..) , scrollbarAttr , scrollbarHandleAttr ) customScrollbars :: ScrollbarRenderer n customScrollbars = ScrollbarRenderer { renderScrollbar = fill '^' , renderScrollbarTrough = fill ' ' , renderScrollbarHandleBefore = str "<<" , renderScrollbarHandleAfter = str ">>" } 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 70 $ 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 customScrollbars $ 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 $ withVScrollBarHandles $ viewport VP2 Both $ vBox $ str "Press ctrl-arrow keys to scroll this viewport horizontally and vertically." : (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 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 () lastClickedElement .= Just (el, n) 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 <- V.mkVty =<< V.standardIOConfig V.setMode (V.outputIface v) V.Mouse True return v initialVty <- buildVty void $ M.customMain initialVty buildVty Nothing app (St Nothing) brick-1.9/programs/VisibilityDemo.hs0000644000000000000000000001023107346545000015765 0ustar0000000000000000{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE CPP #-} module Main where import Control.Monad (void) 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, attrName) import Brick.Util (on) import Brick.Types ( Widget , ViewportType(Horizontal, Vertical, Both) ) import Brick.Widgets.Core ( withAttr , hLimit , vLimit , hBox , vBox , viewport , str , visible ) data St = St { _vp1Index :: Int , _vp2Index :: Int , _vp3Index :: (Int, Int) } makeLenses ''St data Name = VP1 | VP2 | VP3 deriving (Show, Ord, Eq) vp1Size :: Int vp1Size = 15 vp2Size :: Int vp2Size = 15 vp3Size :: (Int, Int) vp3Size = (25, 25) selectedAttr :: AttrName selectedAttr = attrName "selected" drawUi :: St -> [Widget Name] drawUi st = [ui] where ui = C.center $ hLimit 60 $ vLimit 30 $ vBox [ B.border $ vBox [ pair, B.hBorder, singleton ] , str $ "- Up/down arrow keys scroll the top-left viewport\n" <> "- Left/right arrow keys scroll the top-right viewport\n" <> "- Ctrl-arrow keys move the bottom viewport" ] singleton = viewport VP3 Both $ vBox $ do i <- [1..vp3Size^._1] let row = do j <- [1..vp3Size^._2] let mkItem = if (i, j) == st^.vp3Index then withAttr selectedAttr . visible else id return $ mkItem $ str $ "Item " <> show (i, j) <> " " return $ hBox row pair = hBox [ vp1, B.vBorder, vp2 ] 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 VP2 Horizontal $ hBox $ do i <- [1..vp2Size] let mkItem = if i == st^.vp2Index then withAttr selectedAttr . visible else id return $ mkItem $ str $ "Item " <> show i <> " " vp1Scroll :: M.ViewportScroll Name vp1Scroll = M.viewportScroll VP1 vp2Scroll :: M.ViewportScroll Name vp2Scroll = M.viewportScroll VP2 vp3Scroll :: M.ViewportScroll Name vp3Scroll = M.viewportScroll VP3 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 e Name app = M.App { M.appDraw = drawUi , M.appStartEvent = return () , M.appHandleEvent = appEvent , M.appAttrMap = const theMap , M.appChooseCursor = M.neverShowCursor } initialState :: St initialState = St 1 1 (1, 1) main :: IO () main = void $ M.defaultMain app initialState brick-1.9/programs/custom_keys.ini0000644000000000000000000000006307346545000015545 0ustar0000000000000000[keybindings] quit = x increment = i decrement = d brick-1.9/src/0000755000000000000000000000000007346545000011435 5ustar0000000000000000brick-1.9/src/Brick.hs0000644000000000000000000000141707346545000013026 0ustar0000000000000000-- | This module is provided as a convenience to import the most -- 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 , module Brick.Widgets.Core , module Brick.AttrMap , module Brick.Util ) where import Brick.Main import Brick.Types import Brick.Widgets.Core import Brick.AttrMap import Brick.Util brick-1.9/src/Brick/0000755000000000000000000000000007346545000012467 5ustar0000000000000000brick-1.9/src/Brick/AttrMap.hs0000644000000000000000000002137507346545000014403 0ustar0000000000000000{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} -- | This module provides types and functions for managing an attribute -- map which maps attribute names ('AttrName') to attributes ('Attr'). -- -- Attribute maps work by mapping hierarchical attribute names to -- attributes and inheriting parent names' attributes when child names -- specify partial attributes. Hierarchical names are created with 'mappend': -- -- @ -- let n = attrName "parent" <> attrName "child" -- @ -- -- 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 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 -- attributes in the same hierarchy. This allows more modularity and -- less repetition in specifying how elements of your user interface -- take on different attributes. module Brick.AttrMap ( AttrMap , AttrName -- * Construction , attrMap , forceAttrMap , forceAttrMapAllowStyle , attrName -- * Inspection , attrNameComponents -- * Finding attributes from names , attrMapLookup -- * Manipulating attribute maps , setDefaultAttr , getDefaultAttr , applyAttrMappings , mergeWithDefault , mapAttrName , mapAttrNames ) where import qualified Data.Semigroup as Sem import Control.DeepSeq import Data.Bits ((.|.)) import qualified Data.Map as M import Data.Maybe (mapMaybe) import Data.List (inits) import GHC.Generics (Generic) import Graphics.Vty (Attr(..), MaybeDefault(..), Style) -- | An attribute name. Attribute names are hierarchical; use 'mappend' -- ('<>') 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: -- -- @ -- attrName "window" <> attrName "border" -- attrName "window" <> attrName "title" -- attrName "header" <> attrName "clock" <> attrName "seconds" -- @ data AttrName = AttrName [String] deriving (Show, Read, Eq, Ord, Generic, NFData) instance Sem.Semigroup AttrName where (AttrName as) <> (AttrName bs) = AttrName $ as `mappend` bs instance Monoid AttrName where mempty = AttrName [] mappend = (Sem.<>) -- | An attribute map which maps 'AttrName' values to 'Attr' values. data AttrMap = AttrMap Attr (M.Map AttrName Attr) | ForceAttr Attr | 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 -- lookup fails, and the attribute that will be merged with -- successful lookups. -> [(AttrName, Attr)] -- ^ The map's initial contents. -> AttrMap attrMap theDefault pairs = AttrMap theDefault (M.fromList pairs) -- | Create an attribute map in which all lookups map to the same -- 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 -- the one specified here. Otherwise the attribute given here is merged -- with the attribute map's default attribute in that any aspect of the -- specified attribute that is not provided falls back to the map -- default. For example, -- -- @ -- mergeWithDefault (fg blue) $ attrMap (bg red) [] -- @ -- -- returns -- -- @ -- blue \`on\` red -- @ 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 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 (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 = mapMaybe (\n -> M.lookup (AttrName n) m) (inits ns) in foldl combineAttrs theDefault results -- | Set the default attribute value in an attribute map. 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 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 brick-1.9/src/Brick/BChan.hs0000644000000000000000000000271207346545000014000 0ustar0000000000000000module 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) brick-1.9/src/Brick/BorderMap.hs0000644000000000000000000002175607346545000014711 0ustar0000000000000000{-# 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) import Control.Applicative (liftA2) 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') } brick-1.9/src/Brick/Focus.hs0000644000000000000000000001004107346545000014076 0ustar0000000000000000-- | This module provides a type and functions for handling focus rings -- of values. module Brick.Focus ( FocusRing , focusRing , focusNext , focusPrev , focusGetCurrent , focusSetCurrent , focusRingLength , focusRingToList , focusRingCursor , withFocusRing , focusRingModify ) where 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 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 resource names. focusRing :: [n] -> FocusRing n focusRing = FocusRing . C.fromList -- | 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 value in the ring. focusPrev :: FocusRing n -> FocusRing n focusPrev r@(FocusRing l) | C.isEmpty l = r | otherwise = FocusRing $ C.rotL l -- | 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 :: (Eq n) => (a -> FocusRing n) -- ^ The function used to get the focus ring out of your -- application state. -> a -- ^ Your application state. -> [CursorLocation n] -- ^ The list of available cursor positions. -> 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) brick-1.9/src/Brick/Forms.hs0000644000000000000000000011150307346545000014112 0ustar0000000000000000{-# 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(..) -- * Creating and using forms , newForm , formFocus , formState , handleFormEvent , renderForm , renderFormFieldState , (@@=) , allFieldsValid , invalidFields , setFieldValid , setFormConcat , setFieldConcat , setFormFocus , updateFormState -- * 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. } -- | 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. } -> 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 -> FormFieldState (upd (newState^.l) st) l upd s rh concatAll 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 } 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 } -- | 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 } 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 } -- | 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) -- | 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 -> 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 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) = let renderFields [] = [] renderFields ((FormField n validate extValid renderField _):fs) = let maybeInvalid = if (isJust $ validate st) && extValid then id else forceAttr invalidFormInputAttr foc = Just n == focusGetCurrent fr maybeVisible = if foc then visible else id in (maybeVisible $ maybeInvalid $ renderField foc st) : renderFields fs in helper $ concatFields $ renderFields fields -- | 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 -> 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 formFieldStatesL .= prev <> [newFieldState] <> es case maybeSt of Nothing -> return () Just s -> formStateL.stLens .= s states <- gets formFieldStates findFieldState [] states brick-1.9/src/Brick/Keybindings.hs0000644000000000000000000000116307346545000015272 0ustar0000000000000000-- | 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 brick-1.9/src/Brick/Keybindings/0000755000000000000000000000000007346545000014735 5ustar0000000000000000brick-1.9/src/Brick/Keybindings/KeyConfig.hs0000644000000000000000000002232107346545000017147 0ustar0000000000000000-- | 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 -- | 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. binding :: Vty.Key -> [Vty.Modifier] -> Binding binding k mods = Binding { kbKey = 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 in b { kbMods = S.insert m (kbMods b) } -- | Add Meta to a binding. meta :: (ToBinding a) => a -> Binding meta = addModifier Vty.MMeta -- | Add Ctrl to a binding. ctrl :: (ToBinding a) => a -> Binding ctrl = addModifier Vty.MCtrl -- | Add Shift to a binding. shift :: (ToBinding a) => a -> Binding shift = addModifier Vty.MShift -- | Function key binding. fn :: Int -> Binding fn = bind . Vty.KFun brick-1.9/src/Brick/Keybindings/KeyDispatcher.hs0000644000000000000000000002311607346545000020033 0ustar0000000000000000-- | 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. } brick-1.9/src/Brick/Keybindings/KeyEvents.hs0000644000000000000000000000377707346545000017224 0ustar0000000000000000-- | 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 brick-1.9/src/Brick/Keybindings/Parse.hs0000644000000000000000000001515207346545000016347 0ustar0000000000000000{-# 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 , keybindingsFromIni , keybindingsFromFile , keybindingIniParser ) where import Control.Monad (forM) import Data.Maybe (catMaybes) import qualified Data.Set as S 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 -- | 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 { kbMods = S.fromList mods, kbKey = k' } 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 -- -- > = <"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 brick-1.9/src/Brick/Keybindings/Pretty.hs0000644000000000000000000002243107346545000016562 0ustar0000000000000000{-# 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) import Data.Maybe (fromJust) #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 = fromJust $ keyEventName (keyConfigEvents kc) ev in case lookupKeyConfigBindings kc ev of Nothing -> if not (null (allDefaultBindings kc ev)) then (Verbatim name, Verbatim <$> ppBinding <$> allDefaultBindings kc ev) else (Verbatim name, unbound) Just Unbound -> (Verbatim name, unbound) Just (BindingList bs) -> let result = if not (null bs) then Verbatim <$> ppBinding <$> bs else unbound in (Verbatim 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 -- TODO: was "; " <> s Verbatim s -> txt s -- TODO: was: emph $ 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" brick-1.9/src/Brick/Main.hs0000644000000000000000000006552307346545000013722 0ustar0000000000000000{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE RankNTypes #-} module Brick.Main ( App(..) , defaultMain , customMain , customMainWithVty , simpleMain , resizeOrQuit , simpleApp -- * Event handler functions , continueWithoutRedraw , halt , suspendAndResume , suspendAndResume' , makeVisible , lookupViewport , lookupExtent , findClickedExtents , clickedExtent , getVtyHandle -- ** Viewport scrolling , viewportScroll , ViewportScroll , vScrollBy , vScrollPage , vScrollToBeginning , vScrollToEnd , hScrollBy , 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 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(..) , Cursor(..) , Event(..) , update , outputIface , displayBounds , shutdown , nextEvent , mkVty , defaultConfig , restoreInputState , inputIface ) import Graphics.Vty.Attributes (defAttr) 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 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 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 -- returns 'Nothing', no cursor is placed. The rationale here -- 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 :: 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. } -- | The default main entry point which takes an application and an -- initial state and returns the final state returned by a 'halt' -- operation. defaultMain :: (Ord n) => App s e n -- ^ The application. -> s -- ^ The initial application state. -> IO s defaultMain app st = do let builder = mkVty defaultConfig initialVty <- builder customMain initialVty builder Nothing app st -- | 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 :: (Ord n) => Widget n -- ^ The widget to draw. -> IO () 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 :: BrickEvent n e -> EventM n s () resizeOrQuit (VtyEvent (EvResize _ _)) = return () resizeOrQuit _ = halt readBrickEvent :: BChan (BrickEvent n e) -> BChan e -> IO (BrickEvent n e) readBrickEvent brickChan userChan = either id AppEvent <$> readBChan2 brickChan userChan 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. 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). 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 initialVty buildVty mUserChan app initialAppState = do let restoreInitialState = restoreInputState $ inputIface initialVty (s, vty) <- customMainWithVty initialVty buildVty mUserChan app initialAppState `E.catch` (\(e::E.SomeException) -> restoreInitialState >> E.throw e) shutdown vty restoreInitialState return s -- | 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). 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) -- | 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) -- | 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 cloc -> pic { picCursor = (if cursorLocationVisible cloc then AbsoluteCursor else PositionOnly True) (cloc^.locationColumnL) (cloc^.locationRowL) } update (vtyContextHandle vtyCtx) picWithCursor 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 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 n] -> Maybe (CursorLocation n) showFirstCursor = const listToMaybe -- | 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 l = l^.cursorLocationNameL == Just name in find matches locs -- | A viewport scrolling handle for managing the scroll state of -- viewports. data ViewportScroll n = ViewportScroll { viewportName :: n -- ^ The name of the viewport to be controlled by -- this scrolling handle. , hScrollPage :: forall s. Direction -> EventM n s () -- ^ Scroll the viewport horizontally by one page in -- the specified direction. , 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 :: forall s. EventM n s () -- ^ Scroll horizontally to the beginning of the -- viewport. , hScrollToEnd :: forall s. EventM n s () -- ^ Scroll horizontally to the end of the viewport. , vScrollPage :: forall s. Direction -> EventM n s () -- ^ Scroll the viewport vertically by one page in -- the specified direction. , 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 :: forall s. EventM n s () -- ^ Scroll vertically to the beginning of the viewport. , 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 :: n -> ViewportScroll n viewportScroll n = 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 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 :: 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, 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 }) brick-1.9/src/Brick/Themes.hs0000644000000000000000000003745307346545000014264 0ustar0000000000000000{-# 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 } brick-1.9/src/Brick/Types.hs0000644000000000000000000001074107346545000014132 0ustar0000000000000000-- | Basic types used by this library. {-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Brick.Types ( -- * The Widget type Widget(..) -- * Location types and lenses , Location(..) , locL , TerminalLocation(..) , CursorLocation(..) , cursorLocationL , cursorLocationNameL -- * Viewports , Viewport(..) , ViewportType(..) , vpSize , vpTop , vpLeft , vpContentSize , VScrollBarOrientation(..) , HScrollBarOrientation(..) , ScrollbarRenderer(..) , ClickableScrollbarElement(..) -- * Event-handling types and functions , EventM , BrickEvent(..) , nestEventM , nestEventM' -- * Rendering infrastructure , RenderM , getContext -- ** The rendering context , 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(..) , vrPositionL , vrSizeL -- * Making lenses , suffixLenses , suffixLensesWith -- * Dynamic borders , bordersL , DynBorder(..) , dbStyleL, dbAttrL, dbSegmentsL , BorderSegment(..) , bsAcceptL, bsOfferL, bsDrawL , Edges(..) , eTopL, eBottomL, eRightL, eLeftL -- * Miscellaneous , Size(..) , Direction(..) -- * Renderer internals (for benchmarking) , RenderState -- * Re-exports for convenience , get , gets , put , modify , zoom ) where 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) -- | 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 -- | 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 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 :: forall r n. Getting r (Context n) Attr attrL = to (\c -> attrMapLookup (c^.ctxAttrNameL) (c^.ctxAttrMapL)) 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 n Attr lookupAttrName n = do c <- getContext return $ attrMapLookup n (c^.ctxAttrMapL) brick-1.9/src/Brick/Types/0000755000000000000000000000000007346545000013573 5ustar0000000000000000brick-1.9/src/Brick/Types/Common.hs0000644000000000000000000000305507346545000015362 0ustar0000000000000000{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} 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) import Lens.Micro.Internal (Field1, Field2) -- | 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)) brick-1.9/src/Brick/Types/EventM.hs0000644000000000000000000000242607346545000015331 0ustar0000000000000000{-# 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) brick-1.9/src/Brick/Types/Internal.hs0000644000000000000000000003647707346545000015724 0ustar0000000000000000{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} module Brick.Types.Internal ( ScrollRequest(..) , VisibilityRequest(..) , vrPositionL , vrSizeL , Location(..) , locL , origin , TerminalLocation(..) , Viewport(..) , ViewportType(..) , RenderState(..) , Direction(..) , CursorLocation(..) , cursorLocationL , cursorLocationNameL , cursorLocationVisibleL , VScrollBarOrientation(..) , HScrollBarOrientation(..) , ScrollbarRenderer(..) , ClickableScrollbarElement(..) , Context(..) , ctxAttrMapL , ctxAttrNameL , ctxBorderStyleL , ctxDynBordersL , ctxVScrollBarOrientationL , ctxVScrollBarRendererL , ctxHScrollBarOrientationL , ctxHScrollBarRendererL , ctxVScrollBarShowHandlesL , ctxHScrollBarShowHandlesL , ctxVScrollBarClickableConstrL , ctxHScrollBarClickableConstrL , availWidthL , availHeightL , windowWidthL , windowHeightL , 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.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 (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) data ScrollRequest = HScrollBy Int | HScrollPage Direction | HScrollToBeginning | HScrollToEnd | 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 scroll bar renderer. data ScrollbarRenderer n = ScrollbarRenderer { renderScrollbar :: 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, if this was used to -- render vertical scroll bars, this widget would -- need to be one that expands vertically such as -- @fill@. The same goes for the trough widget. , renderScrollbarTrough :: 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. , renderScrollbarHandleBefore :: Widget n -- ^ How to render the handle that appears at the -- top or left of the scrollbar. The result should -- be at most one row high for horizontal handles -- and one column wide for vertical handles. , renderScrollbarHandleAfter :: Widget n -- ^ How to render the handle that appears at -- the bottom or right of the scrollbar. The -- result should be at most one row high for -- horizontal handles and one column wide for -- vertical handles. } data VisibilityRequest = VR { vrPosition :: Location , vrSize :: DisplayRegion } 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 -- ^ The column offset of left side of the viewport. , _vpTop :: Int -- ^ The row offset of the top of the viewport. , _vpSize :: DisplayRegion -- ^ The size of the viewport. , _vpContentSize :: DisplayRegion -- ^ The size of the contents of the viewport. } 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, Eq) 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 NextAction = Continue | ContinueWithoutRedraw | Halt -- | Scrolling direction. data Direction = Up -- ^ Up/left | Down -- ^ Down/right 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 locationColumnL :: Lens' a Int locationColumn :: a -> Int -- | Get the row out of the value locationRowL :: Lens' a Int locationRow :: a -> Int instance TerminalLocation Location where 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 n = CursorLocation { cursorLocation :: !Location -- ^ The location , cursorLocationName :: !(Maybe n) -- ^ The name of the widget associated with the location , cursorLocationVisible :: !Bool -- ^ Whether the cursor should actually be visible } 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 bordering style should be used, and the attribute map -- available for rendering. 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 (ScrollbarRenderer n) , ctxHScrollBarOrientation :: Maybe HScrollBarOrientation , ctxHScrollBarRenderer :: Maybe (ScrollbarRenderer 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 brick-1.9/src/Brick/Types/TH.hs0000644000000000000000000000176207346545000014450 0ustar0000000000000000module Brick.Types.TH ( suffixLenses , suffixLensesWith ) where import qualified Language.Haskell.TH.Syntax as TH import qualified Language.Haskell.TH.Lib as TH 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 '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 = 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]) brick-1.9/src/Brick/Util.hs0000644000000000000000000000316707346545000013747 0ustar0000000000000000-- | Utility functions. module Brick.Util ( clamp , on , fg , bg , style , clOffset ) where import Lens.Micro ((&), (%~)) #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid ((<>)) #endif import Graphics.Vty import Brick.Types.Internal (Location(..), CursorLocation(..), cursorLocationL) -- | Given a minimum value and a maximum value, clamp a value to that -- range (values less than the minimum map to the minimum and values -- greater than the maximum map to the maximum). -- -- >>> clamp 1 10 11 -- 10 -- >>> clamp 1 10 2 -- 2 -- >>> clamp 5 10 1 -- 5 clamp :: (Ord a) => a -- ^ The minimum value -> a -- ^ The maximum value -> a -- ^ The value to clamp -> a clamp mn mx val = max mn (min val mx) -- | Build an attribute from a foreground color and a background color. -- Intended to be used infix. on :: Color -- ^ The foreground color -> Color -- ^ The background color -> Attr on f b = defAttr `withForeColor` f `withBackColor` b -- | Create an attribute from the specified foreground color (the -- background color is the "default"). fg :: Color -> Attr fg = (defAttr `withForeColor`) -- | Create an attribute from the specified background color (the -- 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 n -> Location -> CursorLocation n clOffset cl off = cl & cursorLocationL %~ (<> off) brick-1.9/src/Brick/Widgets/0000755000000000000000000000000007346545000014075 5ustar0000000000000000brick-1.9/src/Brick/Widgets/Border.hs0000644000000000000000000001467207346545000015660 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} -- | This module provides border widgets: vertical borders, horizontal -- borders, and a box border wrapper widget. All functions in this -- module use the rendering context's active 'BorderStyle'; to change -- the 'BorderStyle', use 'withBorderStyle'. module Brick.Widgets.Border ( -- * Border wrapper border , borderWithLabel -- * Horizontal border , hBorder , hBorderWithLabel -- * Vertical border , vBorder -- * Drawing single border elements , borderElem -- * Attribute names , borderAttr , hBorderAttr , vBorderAttr -- * Utility , joinableBorder ) where #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.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 = attrName "border" -- | The horizontal border attribute name. Inherits from 'borderAttr'. hBorderAttr :: AttrName hBorderAttr = borderAttr <> attrName "horizontal" -- | 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'. -- -- 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 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. -- -- 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 n -- ^ The widget to put a border around -> Widget n borderWithLabel label = border_ (Just label) border_ :: Maybe (Widget n) -> Widget n -> Widget n border_ label wrapped = Widget (hSize wrapped) (vSize wrapped) $ do c <- getContext middleResult <- render $ hLimit (c^.availWidthL - 2) $ vLimit (c^.availHeightL - 2) $ wrapped 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 render $ hLimit (middleResult^.imageL.to imageWidth + 2) $ vLimit (middleResult^.imageL.to imageHeight + 2) $ total -- | 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 n -- ^ The label widget -> Widget n hBorderWithLabel label = Widget Greedy Fixed $ do res <- render $ vLimit 1 label render $ hBox [hBorder, Widget Fixed Fixed (return res), hBorder] -- | A vertical border. Fills all vertical space. Draws using -- 'vBorderAttr'. vBorder :: Widget n vBorder = 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))) brick-1.9/src/Brick/Widgets/Border/0000755000000000000000000000000007346545000015312 5ustar0000000000000000brick-1.9/src/Brick/Widgets/Border/Style.hs0000644000000000000000000001051507346545000016750 0ustar0000000000000000{-# 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 -- users may be using. Because of this, we provide the 'ascii' style in -- addition to the Unicode styles. The 'unicode' style is also a safe -- bet. -- -- To use these in your widgets, see -- 'Brick.Widgets.Core.withBorderStyle'. By default, widgets rendered -- without a specified border style use 'unicode' style. module Brick.Widgets.Border.Style ( BorderStyle(..) , borderStyleFromChar , ascii , unicode , unicodeBold , unicodeRounded , defaultBorderStyle ) where import GHC.Generics import Control.DeepSeq -- | A border style for use in any widget that needs to render borders -- in a consistent style. data BorderStyle = BorderStyle { bsCornerTL :: Char -- ^ Top-left corner character , bsCornerTR :: Char -- ^ Top-right corner character , bsCornerBR :: Char -- ^ Bottom-right corner character , bsCornerBL :: Char -- ^ Bottom-left corner character , bsIntersectFull :: Char -- ^ Full intersection (cross) , bsIntersectL :: Char -- ^ Left side of a horizontal border intersecting a vertical one , bsIntersectR :: Char -- ^ Right side of a horizontal border intersecting a vertical one , bsIntersectT :: Char -- ^ Top of a vertical border intersecting a horizontal one , bsIntersectB :: Char -- ^ Bottom of a vertical border intersecting a horizontal one , bsHorizontal :: Char -- ^ Horizontal border character , bsVertical :: Char -- ^ Vertical border character } deriving (Show, Read, Eq, Generic, NFData) defaultBorderStyle :: BorderStyle defaultBorderStyle = unicode -- | Make a border style using the specified character everywhere. borderStyleFromChar :: Char -> BorderStyle borderStyleFromChar c = BorderStyle c c c c c c c c c c c -- |An ASCII border style which will work in any terminal. ascii :: BorderStyle ascii = BorderStyle { bsCornerTL = '+' , bsCornerTR = '+' , bsCornerBR = '+' , bsCornerBL = '+' , bsIntersectFull = '+' , bsIntersectL = '+' , bsIntersectR = '+' , bsIntersectT = '+' , bsIntersectB = '+' , bsHorizontal = '-' , bsVertical = '|' } -- |A unicode border style with real corner and intersection characters. unicode :: BorderStyle unicode = BorderStyle { bsCornerTL = '┌' , bsCornerTR = '┐' , bsCornerBR = '┘' , bsCornerBL = '└' , bsIntersectFull = '┼' , bsIntersectL = '├' , bsIntersectR = '┤' , bsIntersectT = '┬' , bsIntersectB = '┴' , bsHorizontal = '─' , bsVertical = '│' } -- |A unicode border style in a bold typeface. unicodeBold :: BorderStyle unicodeBold = BorderStyle { bsCornerTL = '┏' , bsCornerTR = '┓' , bsCornerBR = '┛' , bsCornerBL = '┗' , bsIntersectFull = '╋' , bsIntersectL = '┣' , bsIntersectR = '┫' , bsIntersectT = '┳' , bsIntersectB = '┻' , bsHorizontal = '━' , bsVertical = '┃' } -- |A unicode border style with rounded corners. unicodeRounded :: BorderStyle unicodeRounded = BorderStyle { bsCornerTL = '╭' , bsCornerTR = '╮' , bsCornerBR = '╯' , bsCornerBL = '╰' , bsIntersectFull = '┼' , bsIntersectL = '├' , bsIntersectR = '┤' , bsIntersectT = '┬' , bsIntersectB = '┴' , bsHorizontal = '─' , bsVertical = '│' } brick-1.9/src/Brick/Widgets/Center.hs0000644000000000000000000001623707346545000015662 0ustar0000000000000000-- | This module provides combinators for centering other widgets. module Brick.Widgets.Center ( -- * 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 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 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 n -> Widget n hCenterWith mChar p = 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 = max 0 $ c^.availWidthL - (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 , result^.imageL , rightPadding ] off = Location (leftPaddingAmount, 0) if leftPaddingAmount == 0 && rightPaddingAmount == 0 then return result else return $ addResultOffset off $ result & imageL .~ paddedImage -- | Center a widget vertically. Consumes all vertical space. 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 n -> Widget n vCenterWith mChar p = 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 = 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 , result^.imageL , bottomPadding ] off = Location (0, topPaddingAmount) if topPaddingAmount == 0 && bottomPaddingAmount == 0 then return result else return $ addResultOffset off $ result & imageL .~ paddedImage -- | Center a widget both vertically and horizontally. Consumes all -- available vertical and horizontal space. 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 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 n -> Widget n centerAbout l p = Widget Greedy Greedy $ do -- Compute translation offset so that loc is in the middle of the -- rendering area c <- getContext let centerW = c^.availWidthL `div` 2 centerH = c^.availHeightL `div` 2 off = Location ( centerW - l^.locationColumnL , centerH - l^.locationRowL ) 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 brick-1.9/src/Brick/Widgets/Core.hs0000644000000000000000000024652207346545000015334 0ustar0000000000000000{-# LANGUAGE RankNTypes #-} {-# LANGUAGE TupleSections #-} {-# 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 , ScrollbarRenderer(..) , 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 = -- Althoguh 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 just draws the specified widget with no special positioning. -- -- 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 being rendered as-is with -- no translation. 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 @id@. -- -- 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 w 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 return $ 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 return $ 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 return $ 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 return $ 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 :: ScrollbarRenderer 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 :: ScrollbarRenderer n verticalScrollbarRenderer = ScrollbarRenderer { renderScrollbar = fill '█' , renderScrollbarTrough = fill ' ' , renderScrollbarHandleBefore = str "^" , renderScrollbarHandleAfter = str "v" } -- | 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 :: ScrollbarRenderer 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 :: ScrollbarRenderer n horizontalScrollbarRenderer = ScrollbarRenderer { renderScrollbar = fill '█' , renderScrollbarTrough = fill ' ' , renderScrollbarHandleBefore = str "<" , renderScrollbarHandleAfter = str ">" } -- | 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 -- '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. -- -- 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 1) vsOrientation hSBHeight = maybe 0 (const 1) 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 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 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 render 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) => ScrollbarRenderer 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. -> 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 n constr False vpHeight vOffset contentHeight = verticalScrollbar' vsRenderer n constr vpHeight vOffset contentHeight verticalScrollbar vsRenderer n constr True vpHeight vOffset contentHeight = vBox [ maybeClick n constr SBHandleBefore $ hLimit 1 $ withDefAttr scrollbarHandleAttr $ renderScrollbarHandleBefore vsRenderer , verticalScrollbar' vsRenderer n constr vpHeight vOffset contentHeight , maybeClick n constr SBHandleAfter $ hLimit 1 $ withDefAttr scrollbarHandleAttr $ renderScrollbarHandleAfter vsRenderer ] verticalScrollbar' :: (Ord n) => ScrollbarRenderer 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 height in effect. -> Int -- ^ The viewport vertical scrolling offset in effect. -> Int -- ^ The total viewport content height. -> Widget n verticalScrollbar' vsRenderer _ _ vpHeight _ 0 = hLimit 1 $ vLimit vpHeight $ renderScrollbarTrough 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 $ renderScrollbarTrough vsRenderer sbBelow = maybeClick n constr SBTroughAfter $ withDefAttr scrollbarTroughAttr $ vLimit (ctxHeight - (sbOffset + sbSize)) $ renderScrollbarTrough vsRenderer sbMiddle = maybeClick n constr SBBar $ withDefAttr scrollbarAttr $ vLimit sbSize $ renderScrollbar vsRenderer sb = hLimit 1 $ if sbSize == ctxHeight then vLimit sbSize $ renderScrollbarTrough vsRenderer else vBox [sbAbove, sbMiddle, sbBelow] render sb -- | Build a horizontal scroll bar using the specified render 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) => ScrollbarRenderer 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. -> 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 n constr False vpWidth hOffset contentWidth = horizontalScrollbar' hsRenderer n constr vpWidth hOffset contentWidth horizontalScrollbar hsRenderer n constr True vpWidth hOffset contentWidth = hBox [ maybeClick n constr SBHandleBefore $ vLimit 1 $ withDefAttr scrollbarHandleAttr $ renderScrollbarHandleBefore hsRenderer , horizontalScrollbar' hsRenderer n constr vpWidth hOffset contentWidth , maybeClick n constr SBHandleAfter $ vLimit 1 $ withDefAttr scrollbarHandleAttr $ renderScrollbarHandleAfter hsRenderer ] horizontalScrollbar' :: (Ord n) => ScrollbarRenderer 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 = vLimit 1 $ hLimit vpWidth $ renderScrollbarTrough 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 $ renderScrollbarTrough hsRenderer sbRight = maybeClick n constr SBTroughAfter $ withDefAttr scrollbarTroughAttr $ hLimit (ctxWidth - (sbOffset + sbSize)) $ renderScrollbarTrough hsRenderer sbMiddle = maybeClick n constr SBBar $ withDefAttr scrollbarAttr $ hLimit sbSize $ renderScrollbar hsRenderer sb = vLimit 1 $ if sbSize == ctxWidth then hLimit sbSize $ renderScrollbarTrough 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) #-} brick-1.9/src/Brick/Widgets/Dialog.hs0000644000000000000000000001306107346545000015631 0ustar0000000000000000{-# 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 , dialogButtons , dialogWidth -- * Construction and rendering , dialog , renderDialog , getDialogFocus , setDialogFocus -- * Handling events , handleDialogEvent -- * Getting a dialog's current value , dialogSelection -- * Attributes , dialogAttr , buttonAttr , buttonSelectedAttr -- * Lenses , dialogButtonsL , dialogWidthL , dialogTitleL ) where import Lens.Micro import Lens.Micro.Mtl ((%=)) #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid #endif import Data.List (intersperse, find) import Graphics.Vty.Input (Event(..), Key(..)) import Brick.Focus import Brick.Types import Brick.Widgets.Core import Brick.Widgets.Center import Brick.Widgets.Border import Brick.AttrMap -- | Dialogs present a window with a title (optional), a body, and -- 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 with -- handleDialogEvent: -- -- * 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, 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 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 :: (Eq n) => Maybe (Widget n) -- ^ The dialog title -> 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 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 = attrName "dialog" -- | The default attribute for all dialog buttons buttonAttr :: AttrName buttonAttr = attrName "button" -- | The attribute for the selected dialog button (extends 'dialogAttr') buttonSelectedAttr :: AttrName buttonSelectedAttr = buttonAttr <> attrName "selected" -- | 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 " " 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 <$> (d^.dialogButtonsL) doBorder = maybe border borderWithLabel (d^.dialogTitleL) in centerLayer $ withDefAttr dialogAttr $ hLimit (d^.dialogWidthL) $ doBorder $ vBox [ body , hCenter buttons ] -- | 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) brick-1.9/src/Brick/Widgets/Edit.hs0000644000000000000000000002161007346545000015316 0ustar0000000000000000{-# 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 -- '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 '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) -- * Constructing an editor , editor , editorText -- * Reading editor contents , getEditContents , getCursorPosition -- * Handling events , handleEditorEvent -- * Editing text , applyEdit -- * Lenses for working with editors , editContentsL -- * Rendering editors , renderEditor -- * Attributes , editAttr , editFocusedAttr -- * UTF-8 decoding of editor pastes , DecodeUtf8(..) ) where #if !(MIN_VERSION_base(4,11,0)) import Data.Monoid #endif import Lens.Micro import Graphics.Vty (Event(..), Key(..), Modifier(..)) 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 import Brick.AttrMap -- | Editor state. Editors support the following events by default: -- -- * 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 -- * 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 , editorName :: n -- ^ The name of the editor } suffixLenses ''Editor instance (Show t, Show n) => Show (Editor t n) where show e = concat [ "Editor { " , "editContents = " <> show (editContents e) , ", editorName = " <> show (editorName e) , "}" ] 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 'String' values editor :: Z.GenericTextZipper a => n -- ^ The editor's name (must be unique) -> Maybe Int -- ^ The limit on the number of lines in the editor ('Nothing' -- means no limit) -> a -- ^ The initial content -> Editor a n editor name limit s = Editor (Z.textZipper (Z.lines s) limit) name -- | 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 when it does not have focus. editAttr :: AttrName 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 :: Monoid t => Editor t n -> [t] getEditContents e = Z.getText $ e^.editContentsL -- | 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 atChar = charAtCursor $ e^.editContentsL atCharWidth = maybe 1 textWidth atChar in withAttr (if foc then editFocusedAttr else editAttr) $ limit $ viewport (e^.editorNameL) Both $ (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 brick-1.9/src/Brick/Widgets/FileBrowser.hs0000644000000000000000000011073307346545000016661 0ustar0000000000000000{-# 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 , 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) import qualified Data.Set as Set import qualified Data.Vector as V import Lens.Micro import Lens.Micro.Mtl ((%=)) import Lens.Micro.TH (lensRules, generateUpdateableOptics) import qualified Graphics.Vty as Vty import qualified System.Directory as D import qualified System.Posix.Files as U import qualified System.Posix.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 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 -- | 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@: 'actionFileBrowserSelectCurrent' -- * @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 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 ' ') [] -> -- Select entry actionFileBrowserSelectCurrent _ -> 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 markSelected :: FileInfo -> EventM n (FileBrowser n) () markSelected e = fileBrowserSelectedFilesL %= Set.insert (fileInfoFilename e) -- | 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 markSelected entry else when (selectDirectories entry) $ put =<< liftIO (setWorkingDirectory (fileInfoFilePath entry) b) selectCurrentEntry :: EventM n (FileBrowser n) () selectCurrentEntry = do b <- get for_ (fileBrowserCursor b) markSelected -- | 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 brick-1.9/src/Brick/Widgets/Internal.hs0000644000000000000000000001723707346545000016217 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} module Brick.Widgets.Internal ( renderFinal , cropToContext , cropResultToContext , renderDynBorder , renderWidget ) where 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 :: (Ord n) => AttrMap -> [Widget n] -> V.DisplayRegion -> ([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 $ (\p -> runReaderT p ctx) <$> (\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 n -> Widget n cropToContext p = Widget (hSize p) (vSize p) (render p >>= cropResultToContext) cropResultToContext :: Result n -> RenderM n (Result n) cropResultToContext result = do c <- getContext 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 -- An extent is cropped in places where it is not within the -- region described by the context. -- -- If its entirety is outside the context region, it is dropped. -- -- Otherwise its size is adjusted so that it is contained within -- the context region. cropExtent (Extent n (Location (c, r)) (w, h)) = -- Determine the new lower-right corner let endCol = c + w endRow = r + h -- Then clamp the lower-right corner based on the -- context endCol' = min (ctx^.availWidthL) endCol endRow' = min (ctx^.availHeightL) endRow -- Then compute the new width and height from the -- clamped lower-right corner. w' = endCol' - c h' = endRow' - r e = Extent n (Location (c, r)) (w', h') in if w' < 0 || h' < 0 then Nothing else Just e 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 brick-1.9/src/Brick/Widgets/List.hs0000644000000000000000000005704207346545000015354 0ustar0000000000000000{-# 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 ( GenericList , 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 , listClear , listReverse , listModify -- * Attributes , listAttr , listSelectedAttr , listSelectedFocusedAttr -- * Classes , Splittable(..) , Reversible(..) ) where 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) #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) import Brick.Widgets.Core import Brick.Util (clamp) import Brick.AttrMap -- | 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 -- -- 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) -- ^ 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 ''GenericList -- | 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 = attrName "list" -- | The attribute used only for the currently-selected list item when -- the list does not have focus. Extends 'listAttr'. listSelectedAttr :: AttrName listSelectedAttr = listAttr <> attrName "selected" -- | 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) -> t e -- ^ The initial list contents -> Int -- ^ The list item height in rows (all list item widgets must be -- this high). -> GenericList n t e list name es h = let selIndex = if null es then Nothing else Just 0 safeHeight = max 1 h in List es selIndex name safeHeight -- | 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 foc 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 -- 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 -- 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 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 . selItemAttr else id in makeVisible elemWidget render $ viewport (l^.listNameL) Vertical $ translateBy (Location (0, off)) $ vBox $ toList drawnElements -- | Insert an item into a list at the specified position. -- -- 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 -> GenericList n t e -> GenericList n t e listInsert pos e l = let es = l^.listElementsL newSel = case l^.listSelectedL of Nothing -> 0 Just s -> if pos <= s then s + 1 else s (front, back) = splitAt pos es in l & listSelectedL .~ Just newSel & listElementsL .~ sconcat (front :| [pure e, back]) -- | Remove an element from a list at the specified position. -- -- 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 | 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 null es' then Nothing else Just 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 :: (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 :: (Foldable t, Splittable t) => GenericList n t e -> GenericList n t e listMoveDown = listMoveBy 1 -- | 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 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. -- -- 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 = 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 -- | 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 -- | 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 -- | 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 -- | 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 brick-1.9/src/Brick/Widgets/ProgressBar.hs0000644000000000000000000000427707346545000016674 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE CPP #-} -- | This module provides a progress bar widget. module Brick.Widgets.ProgressBar ( progressBar -- * Attributes , progressCompleteAttr , progressIncompleteAttr ) where 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 import Brick.Widgets.Core -- | The attribute of the completed portion of the progress bar. progressCompleteAttr :: AttrName progressCompleteAttr = attrName "progressComplete" -- | The attribute of the incomplete portion of the progress bar. progressIncompleteAttr :: AttrName progressIncompleteAttr = attrName "progressIncomplete" -- | Draw a progress bar with the specified (optional) label and -- progress value. This fills available horizontal space and is one row -- high. progressBar :: 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 progressBar mLabel progress = Widget Greedy Fixed $ do c <- getContext let barWidth = c^.availWidthL label = fromMaybe "" mLabel labelWidth = safeWcswidth label spacesWidth = barWidth - labelWidth leftPart = replicate (spacesWidth `div` 2) ' ' rightPart = replicate (barWidth - (labelWidth + length leftPart)) ' ' fullBar = leftPart <> label <> rightPart completeWidth = round $ progress * toEnum (length fullBar) 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) brick-1.9/src/Brick/Widgets/Table.hs0000644000000000000000000003533107346545000015465 0ustar0000000000000000{-# 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 brick-1.9/src/Data/0000755000000000000000000000000007346545000012306 5ustar0000000000000000brick-1.9/src/Data/IMap.hs0000644000000000000000000001454107346545000013475 0ustar0000000000000000{-# 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 } brick-1.9/tests/0000755000000000000000000000000007346545000012010 5ustar0000000000000000brick-1.9/tests/List.hs0000644000000000000000000003074407346545000013267 0ustar0000000000000000{-# 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_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 brick-1.9/tests/Main.hs0000644000000000000000000000743707346545000013243 0ustar0000000000000000{-# 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 brick-1.9/tests/Render.hs0000644000000000000000000000602607346545000013567 0ustar0000000000000000{-# 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 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.outputForConfig V.defaultConfig 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