packages feed

brick 0.28 → 0.29

raw patch · 8 files changed

+587/−5 lines, 8 filesdep +config-inidep ~microlensnew-component:exe:brick-theme-demoPVP ok

version bump matches the API change (PVP)

Dependencies added: config-ini

Dependency ranges changed: microlens

API changes (from Hackage documentation)

+ Brick.AttrMap: attrNameComponents :: AttrName -> [String]
+ Brick.AttrMap: instance GHC.Read.Read Brick.AttrMap.AttrName
+ Brick.Themes: CustomAttr :: Maybe (MaybeDefault Color) -> Maybe (MaybeDefault Color) -> Maybe Style -> CustomAttr
+ Brick.Themes: Theme :: Attr -> Map AttrName Attr -> Maybe CustomAttr -> Map AttrName CustomAttr -> Theme
+ Brick.Themes: ThemeDocumentation :: Map AttrName Text -> ThemeDocumentation
+ Brick.Themes: [customBg] :: CustomAttr -> Maybe (MaybeDefault Color)
+ Brick.Themes: [customFg] :: CustomAttr -> Maybe (MaybeDefault Color)
+ Brick.Themes: [customStyle] :: CustomAttr -> Maybe Style
+ Brick.Themes: [themeCustomDefaultAttr] :: Theme -> Maybe CustomAttr
+ Brick.Themes: [themeCustomMapping] :: Theme -> Map AttrName CustomAttr
+ Brick.Themes: [themeDefaultAttr] :: Theme -> Attr
+ Brick.Themes: [themeDefaultMapping] :: Theme -> Map AttrName Attr
+ Brick.Themes: [themeDescriptions] :: ThemeDocumentation -> Map AttrName Text
+ Brick.Themes: applyCustomizations :: Maybe CustomAttr -> (AttrName -> Maybe CustomAttr) -> Theme -> Theme
+ Brick.Themes: customBgL :: Lens' CustomAttr (Maybe (MaybeDefault Color))
+ Brick.Themes: customFgL :: Lens' CustomAttr (Maybe (MaybeDefault Color))
+ Brick.Themes: customStyleL :: Lens' CustomAttr (Maybe Style)
+ Brick.Themes: data CustomAttr
+ Brick.Themes: data Theme
+ Brick.Themes: data ThemeDocumentation
+ Brick.Themes: instance GHC.Base.Monoid Brick.Themes.CustomAttr
+ Brick.Themes: instance GHC.Classes.Eq Brick.Themes.CustomAttr
+ Brick.Themes: instance GHC.Classes.Eq Brick.Themes.Theme
+ Brick.Themes: instance GHC.Classes.Eq Brick.Themes.ThemeDocumentation
+ Brick.Themes: instance GHC.Generics.Generic Brick.Themes.CustomAttr
+ Brick.Themes: instance GHC.Generics.Generic Brick.Themes.Theme
+ Brick.Themes: instance GHC.Generics.Generic Brick.Themes.ThemeDocumentation
+ Brick.Themes: instance GHC.Read.Read Brick.Themes.CustomAttr
+ Brick.Themes: instance GHC.Read.Read Brick.Themes.Theme
+ Brick.Themes: instance GHC.Read.Read Brick.Themes.ThemeDocumentation
+ Brick.Themes: instance GHC.Show.Show Brick.Themes.CustomAttr
+ Brick.Themes: instance GHC.Show.Show Brick.Themes.Theme
+ Brick.Themes: instance GHC.Show.Show Brick.Themes.ThemeDocumentation
+ Brick.Themes: loadCustomizations :: FilePath -> Theme -> IO (Either String Theme)
+ Brick.Themes: newTheme :: Attr -> [(AttrName, Attr)] -> Theme
+ Brick.Themes: saveCustomizations :: FilePath -> Theme -> IO ()
+ Brick.Themes: saveTheme :: FilePath -> Theme -> IO ()
+ Brick.Themes: themeCustomDefaultAttrL :: Lens' Theme (Maybe CustomAttr)
+ Brick.Themes: themeCustomMappingL :: Lens' Theme (Map AttrName CustomAttr)
+ Brick.Themes: themeDefaultAttrL :: Lens' Theme Attr
+ Brick.Themes: themeDefaultMappingL :: Lens' Theme (Map AttrName Attr)
+ Brick.Themes: themeDescriptionsL :: Lens' ThemeDocumentation (Map AttrName Text)
+ Brick.Themes: themeToAttrMap :: Theme -> AttrMap
- Brick.Types: cursorLocationL :: forall n_aqoO. Lens' (CursorLocation n_aqoO) Location
+ Brick.Types: cursorLocationL :: forall n_aCI3. Lens' (CursorLocation n_aCI3) Location
- Brick.Types: cursorLocationNameL :: forall n_aqoO n_aqSE. Lens (CursorLocation n_aqoO) (CursorLocation n_aqSE) (Maybe n_aqoO) (Maybe n_aqSE)
+ Brick.Types: cursorLocationNameL :: forall n_aCI3 n_aD57. Lens (CursorLocation n_aCI3) (CursorLocation n_aD57) (Maybe n_aCI3) (Maybe n_aD57)
- Brick.Types: cursorsL :: forall n_aqnJ. Lens' (Result n_aqnJ) [CursorLocation n_aqnJ]
+ Brick.Types: cursorsL :: forall n_aCI2. Lens' (Result n_aCI2) [CursorLocation n_aCI2]
- Brick.Types: extentsL :: forall n_aqnJ. Lens' (Result n_aqnJ) [Extent n_aqnJ]
+ Brick.Types: extentsL :: forall n_aCI2. Lens' (Result n_aCI2) [Extent n_aCI2]
- Brick.Types: imageL :: forall n_aqnJ. Lens' (Result n_aqnJ) Image
+ Brick.Types: imageL :: forall n_aCI2. Lens' (Result n_aCI2) Image
- Brick.Types: visibilityRequestsL :: forall n_aqnJ. Lens' (Result n_aqnJ) [VisibilityRequest]
+ Brick.Types: visibilityRequestsL :: forall n_aCI2. Lens' (Result n_aCI2) [VisibilityRequest]
- Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1529 a_a152J. Lens (Dialog a_a1529) (Dialog a_a152J) [(String, a_a1529)] [(String, a_a152J)]
+ Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1i0I a_a1i1i. Lens (Dialog a_a1i0I) (Dialog a_a1i1i) [(String, a_a1i0I)] [(String, a_a1i1i)]
- Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1529. Lens' (Dialog a_a1529) (Maybe Int)
+ Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1i0I. Lens' (Dialog a_a1i0I) (Maybe Int)
- Brick.Widgets.Dialog: dialogTitleL :: forall a_a1529. Lens' (Dialog a_a1529) (Maybe String)
+ Brick.Widgets.Dialog: dialogTitleL :: forall a_a1i0I. Lens' (Dialog a_a1i0I) (Maybe String)
- Brick.Widgets.Dialog: dialogWidthL :: forall a_a1529. Lens' (Dialog a_a1529) Int
+ Brick.Widgets.Dialog: dialogWidthL :: forall a_a1i0I. Lens' (Dialog a_a1i0I) Int
- Brick.Widgets.Edit: editContentsL :: forall t_a16Nj n_a16Nk t_a16NV. Lens (Editor t_a16Nj n_a16Nk) (Editor t_a16NV n_a16Nk) (TextZipper t_a16Nj) (TextZipper t_a16NV)
+ Brick.Widgets.Edit: editContentsL :: forall t_a1jLA n_a1jLB t_a1jMc. Lens (Editor t_a1jLA n_a1jLB) (Editor t_a1jMc n_a1jLB) (TextZipper t_a1jLA) (TextZipper t_a1jMc)
- Brick.Widgets.List: listElementsL :: forall n_a18Bf e_a18Bg e_a18Lu. Lens (List n_a18Bf e_a18Bg) (List n_a18Bf e_a18Lu) (Vector e_a18Bg) (Vector e_a18Lu)
+ Brick.Widgets.List: listElementsL :: forall n_a1lE8 e_a1lE9 e_a1lO4. Lens (List n_a1lE8 e_a1lE9) (List n_a1lE8 e_a1lO4) (Vector e_a1lE9) (Vector e_a1lO4)
- Brick.Widgets.List: listItemHeightL :: forall n_a18Bf e_a18Bg. Lens' (List n_a18Bf e_a18Bg) Int
+ Brick.Widgets.List: listItemHeightL :: forall n_a1lE8 e_a1lE9. Lens' (List n_a1lE8 e_a1lE9) Int
- Brick.Widgets.List: listNameL :: forall n_a18Bf e_a18Bg n_a18Lv. Lens (List n_a18Bf e_a18Bg) (List n_a18Lv e_a18Bg) n_a18Bf n_a18Lv
+ Brick.Widgets.List: listNameL :: forall n_a1lE8 e_a1lE9 n_a1lO5. Lens (List n_a1lE8 e_a1lE9) (List n_a1lO5 e_a1lE9) n_a1lE8 n_a1lO5
- Brick.Widgets.List: listSelectedL :: forall n_a18Bf e_a18Bg. Lens' (List n_a18Bf e_a18Bg) (Maybe Int)
+ Brick.Widgets.List: listSelectedL :: forall n_a1lE8 e_a1lE9. Lens' (List n_a1lE8 e_a1lE9) (Maybe Int)

Files

CHANGELOG.md view
@@ -2,6 +2,22 @@ Brick changelog --------------- +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 ---- 
README.md view
@@ -60,6 +60,7 @@  * `tart`, a mouse-driven ASCII art drawing program: https://github.com/jtdaugherty/tart  * `silly-joy`, an interpreter for Joy in Haskell: https://github.com/rootmos/silly-joy  * `herms`, a command-line tool for managing kitchen recipes: https://github.com/jackkiefer/herms+ * `purebred`, a mail user agent: https://github.com/purebred-mua/purebred  Getting Started ---------------@@ -98,6 +99,7 @@  * Border-drawing widgets (put borders around or in between things)  * Generic scrollable viewports  * Extensible widget-building API+ * User-customizable attribute themes  * (And many more general-purpose layout control combinators)  In addition, some of `brick`'s more powerful features may not be obvious
brick.cabal view
@@ -1,5 +1,5 @@ name:                brick-version:             0.28+version:             0.29 synopsis:            A declarative terminal user interface library description:   Write terminal applications painlessly with 'brick'! You write an@@ -64,6 +64,7 @@     Brick.Focus     Brick.Main     Brick.Markup+    Brick.Themes     Brick.Types     Brick.Util     Brick.Widgets.Border@@ -89,6 +90,7 @@                        microlens >= 0.3.0.0,                        microlens-th,                        microlens-mtl,+                       config-ini,                        vector,                        contravariant,                        stm >= 2.4,@@ -229,6 +231,19 @@   ghc-options:         -threaded -Wall -fno-warn-unused-do-bind -O3   default-language:    Haskell2010   main-is:             PaddingDemo.hs+  build-depends:       base <= 5,+                       brick,+                       vty >= 5.18.1,+                       text,+                       microlens++executable brick-theme-demo+  if !flag(demos)+    Buildable: False+  hs-source-dirs:      programs+  ghc-options:         -threaded -Wall -fno-warn-unused-do-bind -O3+  default-language:    Haskell2010+  main-is:             ThemeDemo.hs   build-depends:       base <= 5,                        brick,                        vty >= 5.18.1,
docs/guide.rst view
@@ -304,7 +304,7 @@  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+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``. @@ -751,6 +751,93 @@ * ``Brick.Widgets.Core.forceAttr`` * ``Brick.Widgets.Core.withDefAttr`` * ``Brick.Widgets.Core.overrideAttr``++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 them 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::++   [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 simutaneously+(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)++   defaultTheme :: Theme+   defaultTheme =+       newTheme (white `on` blue)+                [ ("someAttribute",  fg yellow)+                , ("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 (see `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 ==============================================
+ programs/ThemeDemo.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Control.Monad (void)+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+  , Next+  )+import Brick.Widgets.Center+  ( hCenter+  , center+  )+import Brick.Widgets.Core+  ( (<+>)+  , vBox+  , str+  , hLimit+  , withDefAttr+  )+import Brick.Util (on, fg)+import Brick.AttrMap (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 = "keybinding"++theme1 :: Theme+theme1 =+    newTheme (white `on` blue)+             [ (keybindingAttr, fg magenta)+             ]++theme2 :: Theme+theme2 =+    newTheme (green `on` black)+             [ (keybindingAttr, fg yellow)+             ]++appEvent :: Int -> BrickEvent () e -> EventM () (Next Int)+appEvent _ (VtyEvent (EvKey (KChar '1') [])) = continue 1+appEvent _ (VtyEvent (EvKey (KChar '2') [])) = continue 2+appEvent s (VtyEvent (EvKey (KChar 'q') [])) = halt s+appEvent s (VtyEvent (EvKey KEsc [])) = halt s+appEvent s _ = continue s++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.+            if s == 1+            then themeToAttrMap theme1+            else themeToAttrMap theme2+        , appChooseCursor = neverShowCursor+        }++main :: IO ()+main = void $ defaultMain app 1
src/Brick/AttrMap.hs view
@@ -29,6 +29,8 @@   , attrMap   , forceAttrMap   , attrName+  -- * Inspection+  , attrNameComponents   -- * Finding attributes from names   , attrMapLookup   -- * Manipulating attribute maps@@ -66,7 +68,7 @@ -- "header" <> "clock" <> "seconds" -- @ data AttrName = AttrName [String]-              deriving (Show, Eq, Ord)+              deriving (Show, Read, Eq, Ord)  instance Monoid AttrName where     mempty = AttrName []@@ -83,6 +85,10 @@ -- | 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
+ src/Brick/Themes.hs view
@@ -0,0 +1,369 @@+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TemplateHaskell #-}+-- | Support for representing attribute themes and loading and saving+-- theme customizations in INI-style files.+--+-- The file format is as follows:+-- * 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 values (no quotes)+--   "black", "red", "green", "yellow", "blue", "magenta", "cyan",+--   "white", "brightBlack", "brightRed", "brightGreen", "brightYellow",+--   "brightBlue", "brightMagenta", "brightCyan", or "brightWhite".+-- * 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.+--   * "standout"+--   * "underline"+--   * "reverseVideo"+--   * "blink"+--   * "dim"+--   * "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 ("foo" <>+--   "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 "list" <> "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.Monad (forM, join)+import Control.Applicative ((<|>))+import qualified Data.Text as T+import qualified Data.Text.IO as T+import qualified Data.Map as M+import Data.Tuple (swap)+import Data.List (intercalate)+import Data.Bits ((.|.), (.&.))+import Data.Maybe (fromMaybe, isNothing, catMaybes)+import Data.Monoid ((<>))+import qualified Data.Foldable as F++import Data.Ini.Config++import Brick.AttrMap (AttrMap, AttrName, attrMap, attrNameComponents)+import Brick.Types.TH (suffixLenses)++-- | 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)++instance Monoid CustomAttr where+    mempty = CustomAttr Nothing Nothing Nothing+    mappend a b =+        CustomAttr { customFg    = customFg a    <|> customFg b+                   , customBg    = customBg a    <|> customBg b+                   , customStyle = customStyle a <|> customStyle b+                   }++-- | 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)++-- | 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)++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)++parseColor :: T.Text -> Either String (MaybeDefault Color)+parseColor s =+    let stripped = T.strip $ T.toLower s+    in if stripped == "default"+       then Right Default+       else maybe (Left $ "Invalid color: " <> show s) (Right . SetTo) $+                  lookup stripped (swap <$> allColors)++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)+    , ("reverseVideo", reverseVideo)+    , ("blink", blink)+    , ("dim", dim)+    , ("bold", bold)+    ]++parseStyle :: T.Text -> Either String Style+parseStyle s =+    let lookupStyle n = case lookup n allStyles of+            Just sty -> Right sty+            Nothing  -> Left $ T.unpack $ "Invalid style: " <> n+        stripped = T.strip $ T.toLower s+        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 ss++    in if bracketed+       then parseStyleList+       else lookupStyle stripped++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 (Color240 _) = error "Color240 space not supported yet"+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 $ attrForeColor a+               , customStyle = case attrStyle a of+                   SetTo s -> Just s+                   _       -> Nothing+               }
src/Brick/Types/Internal.hs view
@@ -133,7 +133,7 @@ data Location = Location { loc :: (Int, Int)                          -- ^ (Column, Row)                          }-                deriving (Show, Eq)+                deriving (Show, Eq, Ord)  suffixLenses ''Location @@ -210,7 +210,7 @@                     -- ^ 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)+                    deriving (Show, Eq, Ord)  data RenderState n =     RS { viewportMap :: M.Map n Viewport