brick

repository·master·Indexed 23 days ago

https://github.com/jtdaugherty/brick

A Haskell library for building terminal user interfaces (TUIs) based on a declarative programming model. Built on top of vty, brick allows developers to define interfaces using pure UI descriptions and state transformation functions. It includes built-in widgets for layouts, text editing, data display (lists, tables, progress bars), navigation viewports, and a type-safe validated input form API via Brick.Forms.

Tokens
11.9K
Snippets
34
Records
57
Agent score
83%

What's inside brick

  1. Overview of brick features and widgets

    master

    brick is a 'batteries included' toolkit providing several built-in UI components:

    • Layout: Vertical and horizontal box layout widgets, and general-purpose layout control combinators.
    • Text & Editing: Basic single- and multi-line text editor widgets.
    • Data Display: List and table widgets, and progress bar widgets.
    • Navigation & Viewports: Generic scrollable viewports and viewport scroll bars.
    • UI Elements: Simple dialog boxes and border-drawing widgets (which can be configured to automatically connect).
    • Advanced Features:
      • Animation support.
      • User-customizable attribute themes.
      • Type-safe, validated input form API (via the Brick.Forms module).
      • A filesystem browser for file and directory selection.
      • Extensible widget-building API for creating custom packages.
  2. What is a viewport and how to use it

    master

    A viewport is a scrollable window onto a widget. You create one using the Brick.Widgets.Core.viewport combinator, which embeds a widget in a named viewport.

    Key Requirements:

    • Unique Names: The viewport name (a resource name from your application's Name type) must be unique across your entire application.
    • Scrolling Direction: You must specify a Brick.Types.ViewportType:
      • Horizontal: Only horizontal scrolling.
      • Vertical: Only vertical scrolling.
      • Both: Both horizontal and vertical scrolling.
    • Size Constraints: A viewport can only embed a widget that has a Fixed size in the direction(s) it scrolls. Violating this results in a runtime exception.

    By default, a viewport is Greedy in both directions (it tries to take up all available space). To limit its size, use limiting combinators like hLimit and vLimit.

    Example: Creating a horizontally-scrollable viewport that is 5 columns wide and 1 row high:

    -- Assuming that App uses 'Name' for its resource names:
    data Name = Viewport1
    let w = hLimit 5 $ 
            vLimit 1 $ 
            viewport Viewport1 Horizontal $ str "Hello, world!"
    -- Assuming that App uses 'Name' for its resource names:
    data Name = Viewport1
    let w = hLimit 5 $
            vLimit 1 $
            viewport Viewport1 Horizontal $ str "Hello, world!"
  3. Handle keybinding collisions in the KeyDispatcher

    master

    A keybinding collision occurs when a user maps the same physical key to more than one abstract event (e.g., mapping both quit and close-window to Esc in an INI file).

    How collisions are handled depends on your application architecture:

    1. Single Dispatcher Context: If you attempt to build a KeyDispatcher from a KeyConfig that contains collisions for events handled in the same context, the construction of the KeyDispatcher will fail. You should handle this failure case when calling Brick.Keybindings.KeyDispatcher.keyDispatcher.
    2. Modal/Multiple Dispatcher Context: If different events are handled by different KeyDispatcher instances (e.g., one dispatcher is active only when a window is open, and another when it is closed), a collision is not a problem because the keys are not being evaluated in the same context.
    3. onKey Collisions: Using Brick.Keybindings.KeyDispatcher.onKey to bind a handler to a specific key (e.g., Tab) can collide with an abstract event also bound to that same key. If these are provided to the same dispatcher, construction will fail.

    To detect collisions at the configuration level before attempting to build a dispatcher, use Brick.Keybindings.KeyConfig.keyEventMappings.

  4. How widgets and rendering work in Brick

    master

    When Brick renders a Widget, it produces a vty Image based on a rendering context. This context provides the widget with:

    • Rendering Area: The number of rows and columns available for consumption.
    • Current Attribute: The name of the attribute currently in use.
    • Attribute Map: The map used to look up attribute names.
    • Active Border Style: The style used for drawing borders.

    Space Consumption and Growth Policies

    Widgets communicate how they use space via two fields of type Brick.Types.Size: horizontal and vertical growth policies. These can be:

    • Fixed: The widget always consumes the same number of rows or columns regardless of available space.
    • Greedy: The widget attempts to consume all available space provided to it.

    Box layout algorithms (like vBox and hBox) use these policies to allocate space. Fixed widgets are rendered first to determine how much space remains for Greedy widgets.

    Cropping

    All widgets must render to an image no larger than the rendering area specified in the context. If a widget attempts to draw outside this area, it will be forcibly cropped.

  5. Use Brick.Forms for type-safe input interfaces

    master

    The Brick.Forms module provides a high-level API to automate the creation of interactive input forms. It handles event dispatching, focus management, input validation, and rendering.

    A form is represented by the type Form s e n, where:

    • s: The type of the form state (the data record being edited).
    • e: The application's event type.
    • n: The application's resource name type.

    To use forms, you should:

    1. Define a data type for your form state (e.g., using lenses).
    2. Initialize the form using newForm and store the resulting Form value in your application state.
    3. Update the form in your event handler using handleFormEvent (typically via zoom).
    4. Render the form using renderForm.
  6. Track widget positions using Extents

    master

    If you need to know the exact screen coordinates and size of a widget, you can request its extent using reportExtent.

    1. Define a unique name for the extent (e.g., data Name = FooBox).
    2. Wrap the widget in reportExtent Name.
    3. Retrieve the extent in an event handler using Brick.Main.lookupExtent.

    The returned Extent contains the upper-left corner and the (width, height).

    -- 1. Define the name
    data Name = FooBox
    
    -- 2. Wrap the widget
    ui = center $ reportExtent FooBox $ border $ str "Foo"
    
    -- 3. Lookup in event handler
    -- Inside EventM:
    mExtent <- Brick.Main.lookupExtent FooBox
    case mExtent of
        Nothing -> ...
        Just (Extent _ upperLeft (width, height)) -> ...
  7. How attribute inheritance works

    master

    Brick uses a hierarchical attribute system similar to CSS. Attribute names are composed of segments (e.g., general and specific). If a lookup for a specific name (like general.specific) does not define a property (like background color), Brick looks up the parent segment (general) and merges the results.

    Attribute Components

    Brick uses Vty's Attr type, which consists of:

    1. Foreground color
    2. Background color
    3. Style (e.g., bold, underline; these are cumulative)

    Default Attributes

    If an attribute name is not found in the map, Brick falls back to the map's "default attribute". You can set a base style for your entire application by initializing the map with a default attribute:

    -- Sets the entire application background to blue
    let myMap = attrMap (bg blue) [ ... ]
    let w = withAttr specificAttr $ str "foobar"
        generalAttr = attrName "general"
        specificAttr = attrName "general" <> attrName "specific"
        myMap = attrMap defAttr [ (generalAttr, bg blue)
                                 , (specificAttr, fg white)
                                 ]
  8. Map attribute names for sub-widgets

    master
    If a custom widget uses its own set of attribute names but needs to render a sub-widget that expects different names, use overrideAttr or mapAttrNames to convert the custom names to the names required by the sub-widget.
  9. Manage interface styling with appAttrMap

    master

    Brick uses an attribute map to decouple widget drawing from specific visual styles. Instead of hardcoding colors, you assign an abstract attribute name to a widget. The appAttrMap then maps these names to actual vty attributes (like fg blue).

    This allows for:

    • Runtime theme changes.
    • Loading saved styles from disk.
    • Modular styling for third-party components.

    To draw a widget with a specific attribute, use Brick.Widgets.Core.withAttr.

    -- Define the map
    App { ... 
        , appAttrMap = const $ attrMap Graphics.Vty.defAttr [(someAttrName, fg blue)] 
        }
    
    -- Use the attribute in a widget
    let w = withAttr blueBg $ str "foobar"
        blueBg = attrName "blueBg"
  10. Brick API conventions: Lenses and Attributes

    master

    Brick follows several naming conventions to help navigate its API:

    • Lenses: Brick uses the microlens family. Functions or fields with an L suffix (e.g., fieldNameL) are lenses. If you prefer not to use lenses, non-lens equivalents are exported by the same module under the name without the L suffix.
    • Attributes: UI element attributes typically end with the Attr suffix (e.g., borderAttr).
    • Qualified Identifiers: When reading documentation, fully-qualified identifiers are used to distinguish between Brick's API and other libraries.
  11. How brick works: Declarative TUI programming

    master

    brick is a Haskell terminal user interface (TUI) toolkit based on a declarative programming model. Instead of manually managing widget lifecycles, you follow these two core principles:

    1. Pure UI Description: You write a pure function that describes how the user interface should be drawn based on your current application state.
    2. State Transformation: You provide a state transformation function to handle incoming events and update your application state.

    This approach uses declarative layout combinators to build interfaces, and event handling is performed by pattern-matching on incoming events. brick builds upon the vty library and depends on vty-crossplatform, allowing it to work on both Unix and Windows (for versions 2.0 and later).

  12. Handle sub-widgets and coordinate offsets

    master

    When a custom widget wraps another widget, it is responsible for translating the wrapped widget's metadata. You must offset the resulting Result's cursor locations, visibility requests, and extents so they remain correct when the parent widget is positioned elsewhere.

    Use Brick.Widgets.Core.addResultOffset to apply these translations based on the offset introduced by your wrapper logic.