packages feed

rainbox (empty) → 0.2.0.0

raw patch · 23 files changed

+3177/−0 lines, 23 filesdep +QuickCheckdep +arraydep +basesetup-changed

Dependencies added: QuickCheck, array, base, rainbow, random, tasty, tasty-quickcheck, text, transformers

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Omari Norman++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.++    * Neither the name of Omari Norman nor the names of other+      contributors may 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+OWNER 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.
+ README.md view
@@ -0,0 +1,35 @@+Rainbox+=======++Provides pretty printing of boxes in two dimensions.  Rainbox is+useful for console programs that need to format tabular data.++Current development status+==========================++Currently the library is done and passes all tests; I'm working on+adding more tests and some documentation.++Documentation+=============++In addition to the Haddock documentation, a tutorial is available in+[the Rainbox.Tutorial module](lib/Rainbox/Tutorial.lhs).  This+module is best read in your text editor or through the Github web+interface, as it is written in literate Haskell, which HsColour does+not fare so well with.++Portability+===========++There's nothing unportable in Rainbox; however, it does use+[Rainbow](http://hackage.haskell.org/package/rainbow) which works+only on UNIX-like systems because it uses the UNIX terminfo library.+I only develop for UNIX-like systems because they are the only ones+I use.++License+=======++Rainbox is licensed under the BSD license; please see the LICENSE+file.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lib/Rainbox.hs view
@@ -0,0 +1,162 @@+-- | Create grids of (possibly) colorful boxes.+--+-- For an introduction, see "Rainbox.Tutorial".  That file is+-- written in literate Haskell, so you will want to look at the+-- source itself.  HsColour does not do very well with literate+-- Haskell, so you will want to view the file in your text editor or+-- on Github:+--+-- <https://github.com/massysett/rainbox/blob/master/lib/Rainbox/Tutorial.lhs>+--+-- This module only helps you create simple grids of cells, rather+-- like a spreadsheet that does not allow you to merge or split+-- cells.  If your needs are more complicated, use "Rainbox.Box",+-- which allows you to build 'Box'es of arbitrary complexity by+-- pasting simpler 'Box'es together.  (You can of course use this+-- module together with "Rainbox.Box" to create very complex+-- layouts.)+module Rainbox+  ( -- * Backgrounds+    Background(..)+  , defaultBackground+  , backgroundFromChunk+  , same++  -- * Alignment+  , Align+  , Horiz+  , Vert+  , top+  , bottom+  , left+  , right+  , center++  -- * Bar+  , Bar(..)++  -- * Cell+  , Cell(..)++  -- * Creating Box and gluing them together++  -- | For simple needs you will only need 'gridByRows' or+  -- 'gridByCols'; 'boxCells' and 'glueBoxes' are provided for more+  -- complex needs.+  , gridByRows+  , gridByCols+  , checkGrid+  , boxCells+  , glueBoxes++  -- * Rendering+  , render+  , printBox+  ) where++import Rainbox.Box+import Rainbox.Array2d+import Data.Array+import Data.String++-- | A 'Cell' consists of multiple screen lines; each screen line is+-- a 'Bar'.+data Cell = Cell+  { bars :: [Bar]+  -- ^ Each Bar is one line on the screen.++  , horiz :: Align Horiz+  -- ^ How this Cell aligns compared to the other Cell in its+  -- column; use 'left', 'center', or 'right'.++  , vert :: Align Vert+  -- ^ How this Cell aligns compared to other Cell in its row; use+  -- 'top', 'center', or 'bottom'.++  , background :: Background+  -- ^ Background color for necessary padding that is added to the+  -- Cell to make it the correct width and height.  Does not affect+  -- the 'Chunk' contained in the 'bars'; these will use the colors+  -- that are designated in the 'Chunk' itself.+  } deriving (Eq, Show)++-- | Creates a Cell with a 'left' horizontal alignment, a 'top'+-- vertical alignment, and a 'defaultBackground'.  The cell will be+-- one 'Bar' tall and contain the text given in the string.+instance IsString Cell where+  fromString s = Cell [(fromString s)] left top defaultBackground++-- | Returns the width of each 'Bar' in the 'Cell'.+cellWidths :: Cell -> [Int]+cellWidths = map width . bars++-- | Transforms a grid of 'Cell' to a grid of 'Box' by adding+-- necessary padding to each 'Cell'.  In every row of the array, all+-- the 'Box' will have equal height; in every column of the array,+-- all the 'Box' will have equal width.+boxCells+  :: (Ix col, Ix row)+  => Array (col, row) Cell+  -> Array (col, row) Box+boxCells ay = cells $ mapTable conv tbl+  where+    tbl = table getWidth getHeight ay+      where+        getWidth _ = maximum . (0:) . concat . map cellWidths . map snd+        getHeight _ = maximum . (0:) . map (length . bars . snd)+    conv lCol lRow _ _ c = grow bk (Height lRow) (Width lCol) av ah bx+      where+        Cell bs ah av bk = c+        bx = barsToBox bk ah bs++-- | Use 'catH' and 'catV' to fuse an array of 'Box' into a single+-- 'Box'.  For example, if the 'bounds' of the array are+-- @((0,0),(3,5))@, then the array has the number of cells given by+-- @rangeSize ((0,0), (3,5))@ (that is, 24).  The upper left corner+-- is @(0,0)@ and the lower right corner is @(3,5)@; the upper right+-- and lower left corners are @(3,0)@ and @(0,5)@, respectively.+glueBoxes+  :: (Ix col, Ix row)+  => Array (col, row) Box+  -> Box+glueBoxes+  = catH defaultBackground top+  . map (catV defaultBackground left)+  . cols++-- | Creates a single 'Box' from a list of rows of 'Cell'.  Each+-- list is a row of 'Cell'.  The list of rows is from top to bottom;+-- within each row, the cells are given from left to right.+--+-- /This function is partial./  Each list of 'Cell' must be+-- the same length; otherwise, your program will crash.  Since you+-- will typically generate the list of rows using 'map' or list+-- comprehensions or the like, this isn't typically a problem; if it+-- is a problem, then check the inputs to this function with+-- 'checkGrid' before you apply it.++gridByRows :: [[Cell]] -> Box+gridByRows = glueBoxes . boxCells . arrayByRows++-- | Creates a single 'Box' from a list of columns of 'Cell'.  Each+-- list is a column of 'Cell'.  The list of columns is from left to+-- right; within each column, the cells are given from top to+-- bottom.+--+-- /This function is partial./  Each list of 'Cell' must be+-- the same length; otherwise, your program will crash.  Since you+-- will typically generate the list of columns using 'map' or list+-- comprehensions or the like, this isn't typically a problem; if it+-- is a problem, then check the inputs to this function with+-- 'checkGrid' before you apply it.++gridByCols :: [[Cell]] -> Box+gridByCols = glueBoxes . boxCells . arrayByCols++-- | Checks the input to 'gridByRows' or 'gridByCols' to ensure that+-- it is safe.  True if the list of list of 'Cell' is safe for+-- either of these functions; False if not.+checkGrid :: [[Cell]] -> Bool+checkGrid ls = case ls of+  [] -> True+  x:xs -> let len = length x in all ((== len) . length) xs
+ lib/Rainbox/Array2d.hs view
@@ -0,0 +1,216 @@+-- | Helpers for two-dimensional arrays.+module Rainbox.Array2d+  (+  -- * Tables+    Table+  , lCols+  , lRows+  , cells+  , table+  , labelCols+  , labelRows+  , mapTable+  , mapColLabels+  , mapRowLabels++  -- * Two-dimensional arrays+  , cols+  , rows+  , arrayByRows+  , arrayByCols+  ) where++import Data.Array++-- * Tables++-- | A Table is a two-dimensional array with two associated+-- one-dimensional arrays: an array of labels for each column, and+-- an array of labels for each row.+data Table lCol lRow col row a = Table+  { lCols :: Array col lCol+  -- ^ One label for each column+  , lRows :: Array row lRow+  -- ^ One label for each row+  , cells :: Array (col, row) a+  -- ^ Two-dimensional array of cells+  } deriving (Eq, Show)++instance (Ix col, Ix row) => Functor (Table lCol lRow col row) where+  fmap f t =  t { cells = fmap f . cells $ t }++-- | Make a new Table.+table+  :: (Ix col, Ix row)+  => (col -> [(row, a)] -> lCol)+  -- ^ Function to generate the column labels.  It is applied to the+  -- column index and the full contents of the column.+ +  -> (row -> [(col, a)] -> lRow)+  -- ^ Function to generate the row labels.  It is applied to the+  -- row index and the full contents of the row.++  -> Array (col, row) a+  -- ^ Cells of the table++  -> Table lCol lRow col row a+table fCol fRow ay = Table ayc ayr ay+  where+    ayc = labelCols fCol ay+    ayr = labelRows fRow ay++-- | Given a two-dimensional array and a function that generates+-- labels, return an array of column labels.+labelCols+  :: (Ix col, Ix row)+  => (col -> [(row, a)] -> lCol)+  -- ^ Function to generate the column labels.  It is applied to the+  -- column index and the full contents of the column.+  -> Array (col, row) a+  -> Array col lCol+labelCols f a = listArray (minCol, maxCol) es+  where+    ((minCol, minRow), (maxCol, maxRow)) = bounds a+    es = zipWith f ixsCols . map mkRow $ ixsCols+      where+        ixsCols = range (minCol, maxCol)+        mkRow col = zip ixsRows (map (\rw -> a ! (col, rw)) ixsRows)+          where+            ixsRows = range (minRow, maxRow)++-- | Given a two-dimensional array and a function that generates+-- labels, return an array of row labels.+labelRows+  :: (Ix col, Ix row)+  => (row -> [(col, a)] -> lRow)+  -- ^ Function to generate the row labels.  It is applied to the+  -- row index and the full contents of the row.+  -> Array (col, row) a+  -> Array row lRow+labelRows f a = listArray (minRow, maxRow) es+  where+    ((minCol, minRow), (maxCol, maxRow)) = bounds a+    es = zipWith f ixsRows . map mkCol $ ixsRows+      where+        ixsRows = range (minRow, maxRow)+        mkCol row = zip ixsCols (map (\cl -> a ! (cl, row)) ixsCols)+          where+            ixsCols = range (minCol, maxCol)++-- | Transform the cells of the table.  Similar to the Functor+-- instance, but the mapping function has access to the label and+-- index of each cell in the 'Table'.+mapTable+  :: (Ix col, Ix row)+  => (lCol -> lRow -> col -> row -> a -> b)+  -- ^ Function is passed the label for the column, the label for+  -- the row, the column index, the row index, and the contents of+  -- the cell.  It returns a new cell.+  -> Table lCol lRow col row a+  -> Table lCol lRow col row b+mapTable f (Table cs rs ls) = Table cs rs ls'+  where+    ls' = listArray (bounds ls) . map g . assocs $ ls+      where+        g ((col, row), e) = f (cs ! col) (rs ! row) col row e++-- | Transform the column labels.+mapColLabels+  :: (Ix col, Ix row)+  => (lCol -> col -> [(lRow, row, a)] -> lCol')+  -- ^ The function is passed the column label, column index, and+  -- the full contents of the column.+  -> Table lCol lRow col row a+  -> Table lCol' lRow col row a+mapColLabels f (Table cs rs ls) = Table cs' rs ls+  where+    ((colMin, rowMin), (colMax, rowMax)) = bounds ls+    cs' = listArray (colMin, colMax) es+      where+        es = zipWith3 f (elems cs) (indices cs) rws+          where+            rws = map mkRow . indices $ cs+              where+                mkRow idx = zipWith3 (,,) (elems rs)+                  (indices rs)+                  (map (ls !) (range ((idx, rowMin), (idx, rowMax))))++-- | Transform the row labels.+mapRowLabels+  :: (Ix col, Ix row)+  => (lRow -> row -> [(lCol, col, a)] -> lRow')+  -- ^ The function is passed the row label, the row index, and the+  -- full contents of the row.+  -> Table lCol lRow col row a+  -> Table lCol lRow' col row a+mapRowLabels f (Table cs rs ls) = Table cs rs' ls+  where+    ((colMin, rowMin), (colMax, rowMax)) = bounds ls+    rs' = listArray (rowMin, rowMax) es+      where+        es = zipWith3 f (elems rs) (indices rs) cls+          where+            cls = map mkCol . indices $ rs+              where+                mkCol idx = zipWith3 (,,) (elems cs)+                  (indices cs)+                  (map (ls !) (range ((colMin, idx), (colMax, idx))))++-- * Two-dimensional arrays++-- | Given a two-dimensional array, return a list of columns in+-- order.+cols+  :: (Ix col, Ix row)+  => Array (col, row) a+  -> [[a]]+cols ay = map getCol $ range (minCol, maxCol)+  where+    ((minCol, minRow), (maxCol, maxRow)) = bounds ay+    ixsRows = range (minRow, maxRow)+    getCol ixCol = map (\rw -> ay ! (ixCol, rw)) ixsRows++-- | Given a two-dimensional array, return a list of rows in order.+rows+  :: (Ix col, Ix row)+  => Array (col, row) a+  -> [[a]]+rows ay = map getRow $ range (minRow, maxRow)+  where+    ((minCol, minRow), (maxCol, maxRow)) = bounds ay+    ixsCols = range (minCol, maxCol)+    getRow ixRow = map (\cl -> ay ! (cl, ixRow)) ixsCols++-- | Generate a two-dimensional array from a list of rows.  Each row+-- must be of equal length; otherwise, the generated array will have+-- undefined elements.+arrayByRows+  :: [[a]]+  -> Array (Int, Int) a+arrayByRows ls = array ((0,0), (colMax, rowMax)) $ indexRows ls+  where+    rowMax = length ls - 1+    colMax = case ls of+      [] -> -1+      x:_ -> length x - 1++indexRows :: [[a]] -> [((Int, Int),a)]+indexRows = concat . map f . zip [0 ..]+  where+    f (rw, ls) = map g $ zip [0 ..] ls+      where+        g (cl, a) = ((cl, rw), a)++-- | Generate a two-dimensional array from a list of columns.  Each+-- column must be of equal length; otherwise, the generated array+-- will have undefined elements.+arrayByCols+  :: [[a]]+  -> Array (Int, Int) a+arrayByCols ls = listArray ((0,0), (colMax, rowMax)) . concat $ ls+  where+    colMax = length ls - 1+    rowMax = case ls of+      [] -> -1+      x:_ -> length x - 1+
+ lib/Rainbox/Box.hs view
@@ -0,0 +1,341 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Working with 'Box'.+--+-- A 'Box' is a rectangular block of text.  You can paste 'Box'+-- together to create new rectangles, and you can grow or reduce+-- existing 'Box' to create new 'Box'es.+--+-- There are only six primitive functions that make a 'Box':+--+-- * 'B.blank' - formats a blank box with nothing but a (possibly)+-- colorful background.  Useful to paste to other 'Box' to provide+-- white space.+--+-- * 'B.chunks' - Makes a box out of Rainbow 'Chunk'.+--+-- * 'B.catH' - paste 'Box' together horizontally+--+-- * 'B.catV' - paste 'Box' together vertically+--+-- * 'B.viewH' - view a 'Box', keeping the same height but possibly+-- trimming the width+--+-- * 'B.viewV' - view a 'Box', keeping the same width but possibly+-- trimming the height+--+-- The other functions use these building blocks to do other useful+-- things.+--+-- There are many crude diagrams in the Haddock documentation.  A+-- dash means a character with data; a period means a blank+-- character.  When you print your 'Box', the blank characters will+-- have the appropriate background color.+module Rainbox.Box+  ( -- * Backgrounds+    Background(..)+  , defaultBackground+  , backgroundFromChunk+  , backgroundToTextSpec+  , same+  +  -- * Height and columns+  , Height(..)+  , B.height+  , Width(..)+  , B.HasWidth(..)++  -- * Alignment+  , Align+  , Vert+  , Horiz+  , B.center+  , B.top+  , B.bottom+  , B.left+  , B.right++  -- * Box properties+  , B.Bar(..)+  , B.barToBox+  , B.barsToBox+  , B.Box+  , B.unBox++  -- * Making Boxes+  , B.blank+  , blankH+  , blankV+  , B.chunks+  , chunk++  -- * Pasting Boxes together+  , B.catH+  , B.catV+  , sepH+  , sepV+  , punctuateH+  , punctuateV++  -- * Viewing Boxes+  , view+  , B.viewH+  , B.viewV++  -- * Growing Boxes+  , grow+  , growH+  , growV+  , column++  -- * Resizing+  , resize+  , resizeH+  , resizeV++  -- * Printing Boxes+  , render+  , printBox+  ) where++import Data.Monoid+import Data.List (intersperse)+import qualified Data.Text as X+import System.Console.Rainbow+import System.Console.Rainbow.Types+import System.Console.Rainbow.Colors+import qualified Rainbox.Box.Primitives as B+import Rainbox.Box.Primitives+  ( Box+  , Align+  , Horiz+  , Vert+  , Height(..)+  , Width(..)+  , Background+  , unBox+  )+import qualified System.IO as IO++backgroundFromChunk :: Chunk -> B.Background+backgroundFromChunk (Chunk ts _) = B.Background bk8 bk256+  where+    bk8 = case getLast . background8 . style8 $ ts of+      Nothing -> c8_default+      Just c -> c+    bk256 = case getLast . background256 . style256 $ ts of+      Nothing -> c256_default+      Just c -> c++backgroundToTextSpec :: B.Background -> TextSpec+backgroundToTextSpec (B.Background bk8 bk256) = TextSpec+  { style8 = mempty { background8 = Last . Just $ bk8 }+  , style256 = mempty { background256 = Last . Just $ bk256 } }++-- | Use the default background colors of the current terminal.+defaultBackground :: B.Background+defaultBackground = B.Background c8_default c256_default++-- | Use the same color for 8 and 256-color backgrounds.+same :: Color8 -> B.Background+same c = B.Background c (to256 c)++--+-- # Box making+--++-- | A blank horizontal box with a given width and no height.+blankH :: Background -> Int -> Box+blankH bk i = B.blank bk (Height 0) (Width i)++-- | A blank vertical box with a given length.+blankV :: Background -> Int -> Box+blankV bk i = B.blank bk (Height i) (Width 0)++-- | A Box made of a single 'Chunk'.+chunk :: Chunk -> Box+chunk = B.chunks . (:[])++-- | Grow a box.  Each dimension of the result 'Box' is never smaller+-- than the corresponding dimension of the input 'Box'.  Analogous to+-- 'view', so you give the resulting dimensions that you want.  The+-- alignment is analogous to 'view'; for instance, if you specify+-- that the alignment is 'top' and 'left', the extra padding is+-- added to the right and bottom sides of the resulting 'Box'.++grow+  :: Background+  -> Height+  -> Width+  -> Align Vert+  -> Align Horiz+  -> Box+  -> Box+grow bk (B.Height h) (B.Width w) av ah+  = growH bk w ah+  . growV bk h av++-- | Grow a 'Box' horizontally.++growH+  :: Background+  -> Int+  -- ^ Resulting width+  -> Align Horiz+  -> Box+  -> Box+growH bk tgtW a b+  | tgtW < w = b+  | otherwise = B.catH bk B.top [lft, b, rt]+  where+    w = B.width b+    diff = tgtW - w+    (lft, rt) = (blankH bk wl, blankH bk wr)+    (wl, wr)+      | a == B.center = B.split diff+      | a == B.left = (0, diff)+      | otherwise = (diff, 0)++-- | Grow a 'Box' vertically.+growV+  :: Background+  -> Int+  -- ^ Resulting height+  -> Align Vert+  -> Box+  -> Box+growV bk tgtH a b+  | tgtH < h = b+  | otherwise = B.catV bk B.left [tp, b, bt]+  where+    h = B.height b+    diff = tgtH - h+    (tp, bt) = (blankV bk ht, blankV bk hb)+    (ht, hb)+      | a == B.center = B.split diff+      | a == B.top = (0, diff)+      | otherwise = (diff, 0)++-- | Returns a list of 'Box', each being exactly as wide as the+-- widest 'Box' in the input list.+column+  :: Background+  -> Align Horiz+  -> [Box]+  -> [Box]+column bk ah bs = map (growH bk w ah) bs+  where+    w = maximum . (0:) . map B.width $ bs++view+  :: Height+  -> Width+  -> Align Vert+  -> Align Horiz+  -> Box+  -> Box+view h w av ah+  = B.viewH (B.unWidth w) ah+  . B.viewV (B.unHeight h) av++--+-- # Resizing+--++-- | Resize a 'Box'.  Will grow or trim it as necessary in order to+-- reach the resulting size.  Returns an empty 'Box' if either+-- 'Height' or 'Width' is less than 1.++resize+  :: Background+  -> Height+  -> Width+  -> Align Vert+  -> Align Horiz+  -> Box+  -> Box+resize bk h w av ah+  = resizeH bk (unWidth w) ah+  . resizeV bk (unHeight h) av++-- | Resize horizontally.+resizeH+  :: Background+  -> Int+  -- ^ Resulting width+  -> Align Horiz+  -> Box+  -> Box+resizeH bk w a b+  | bw < w = growH bk w a b+  | bw > w = B.viewH w a b+  | otherwise = b+  where+    bw = B.width b++-- | Resize vertically.+resizeV+  :: Background+  -> Int+  -- ^ Resulting height+  -> Align Vert+  -> Box+  -> Box+resizeV bk h a b+  | bh < h = growV bk h a b+  | bh > h = B.viewV h a b+  | otherwise = b+  where+    bh = B.height b++--+-- # Glueing+--++-- | @sepH sep a bs@ lays out @bs@ horizontally with alignment @a@,+--   with @sep@ amount of space in between each.+sepH :: Background -> Int -> Align Vert -> [Box] -> Box+sepH bk sep a = punctuateH bk a bl+  where+    bl = blankH bk sep++-- | @sepV sep a bs@ lays out @bs@ vertically with alignment @a@,+--   with @sep@ amount of space in between each.+sepV :: Background -> Int -> Align Horiz -> [Box] -> Box+sepV bk sep a = punctuateV bk a bl+  where+    bl = blankV bk sep++-- | @punctuateH a p bs@ horizontally lays out the boxes @bs@ with a+--   copy of @p@ interspersed between each.+punctuateH :: Background -> Align Vert -> Box -> [Box] -> Box+punctuateH bk a sep = B.catH bk a . intersperse sep++-- | A vertical version of 'punctuateH'.+punctuateV :: Background -> Align Horiz -> Box -> [Box] -> Box+punctuateV bk a sep = B.catV bk a . intersperse sep++-- | Convert a 'Box' to Rainbow 'Chunk's.  You can then print it+-- using 'putChunks' or the like.+render :: Box -> [Chunk]+render bx = case unBox bx of+  B.NoHeight _ -> []+  B.WithHeight rw ->+    concat . concat . map (: [["\n"]])+    . map renderRod $ rw++renderRod :: B.Rod -> [Chunk]+renderRod = map toChunk . B.unRod+  where+    toChunk = either spcToChunk id . B.unNibble+    spcToChunk ss =+      Chunk (backgroundToTextSpec (B.spcBackground ss))+            (X.replicate (B.numSpaces ss) (X.singleton ' '))++-- | Prints a Box to standard output.  If standard output is not a+-- terminal, no colors are used.  Otherwise, colors are used if your+-- TERM environment variable suggests they are available.+printBox :: Box -> IO ()+printBox b = do+  t <- smartTermFromEnv IO.stdout+  hPutChunks IO.stdout t . render $ b+
+ lib/Rainbox/Box/Primitives.hs view
@@ -0,0 +1,536 @@+-- | Box primitives.+--+-- This module provides all functions that have access to the+-- internals of a 'Box'.  There are only six functions that make a+-- 'Box':+--+-- * 'blank' - formats a blank box with nothing but a (possibly)+-- colorful background.  Useful to paste to other 'Box' to provide+-- white space.+--+-- * 'chunks' - Makes a box out of Rainbow 'Chunk'.+--+-- * 'catH' - paste 'Box' together horizontally+--+-- * 'catV' - paste 'Box' together vertically+--+-- * 'viewH' - view a 'Box', keeping the same height but possibly+-- trimming the width+--+-- * 'viewV' - view a 'Box', keeping the same width but possibly+-- trimming the height+--+-- There are many crude diagrams in the Haddock documentation.  A+-- dash means a character with data; a period means a blank+-- character.  When you print your 'Box', the blank characters will+-- have the appropriate background color.+module Rainbox.Box.Primitives+  ( -- * Background+    Background(..)++  -- * Alignment+  , Align+  , Vert+  , Horiz+  , center+  , top+  , bottom+  , left+  , right++  -- * Box+  , Bar(..)+  , Rod(..)+  , barToBox+  , barsToBox+  , Nibble+  , unNibble+  , Spaces+  , numSpaces+  , spcBackground+  , BoxP(..)+  , Box+  , unBox++  -- * Height and Width+  , Height(..)+  , height+  , Width(..)+  , HasWidth(..)++  -- * Making Boxes+  , blank+  , chunks+  , catH+  , catV+  , viewH+  , viewV++  -- * Helpers+  , split++  ) where++import qualified Data.Foldable as F+import System.Console.Rainbow.Types+import Data.Monoid+import qualified Data.Text as X+import Data.String+import System.Console.Rainbow.Colors++-- # Background++-- | Background colors to use when inserting necessary padding.+data Background = Background+  { boxBackground8 :: Color8+  , boxBackground256 :: Color256+  } deriving (Eq, Show)++-- # Box++data Spaces = Spaces+  { numSpaces :: Int+  , spcBackground :: Background+  } deriving (Eq, Show)++instance HasWidth Spaces where+  width = numSpaces++newtype Nibble = Nibble { unNibble :: Either Spaces Chunk }+  deriving (Eq, Show)++instance IsString Nibble where+  fromString = Nibble . Right . fromString++instance HasWidth Nibble where+  width = either width width . unNibble++-- | Occupies a single row on screen.  The 'Chunk' you place in a+-- 'Bar' should not have any control characters such as newlines or+-- tabs, as rainbox assumes that each character in a 'Bar' takes up+-- one screen column and that each character does not create+-- newlines.  Leave newline handling up to rainbox.  However,+-- rainbox will /not/ check to make sure that your inputs do not+-- contain newlines, tabs, or other spurious characters.  Similarly, use of+-- combining characters will create unexpected results, as Rainbox+-- will see something that takes up (for instance) two characters+-- and think it takes up two screen columns, when in reality it will+-- take up only one screen column.  So, if you need accented+-- characters, use a single Unicode code point, not two code points.+-- For example, for é, use U+00E9, not U+0065 and U+0301.+newtype Bar = Bar { unBar :: [Chunk] }+  deriving (Eq, Show)++barToBox :: Bar -> Box+barToBox = chunks . unBar++barsToBox :: Background -> Align Horiz -> [Bar] -> Box+barsToBox bk ah = catV bk ah . map barToBox++instance IsString Bar where+  fromString = Bar . (:[]) . fromString++instance Monoid Bar where+  mempty = Bar []+  mappend (Bar l) (Bar r) = Bar $ l ++ r++-- | A 'Box' has a width in columns and a height in rows.  Its+-- height and width both are always at least zero.  It can have+-- positive height even if its width is zero, and it can have+-- positive width even if its height is zero.+--+-- Each row in a 'Box' always has the same number of characters; a+-- 'Box' with zero height has no characters but still has a certain+-- width.++newtype Box = Box { unBox :: BoxP }+  deriving (Eq, Show)++newtype Rod = Rod { unRod :: [Nibble] }+  deriving (Eq, Show)++instance IsString Rod where+  fromString = Rod . (:[]) . fromString++instance HasWidth Rod where+  width = sum . map width . unRod++-- | Box payload.  Has the data of the box.+data BoxP+  = NoHeight Int+  -- ^ A Box with width but no height.  The Int must be at least+  -- zero.  If it is zero, the Box has no height and no width.+  | WithHeight [Rod]+  -- ^ A Box that has height of at least one.  It must have at least+  -- one component Bar.+  deriving (Eq, Show)++instance HasWidth BoxP where+  width b = case b of+    NoHeight w -> w+    WithHeight ns -> sum . map width $ ns++instance IsString Box where+  fromString = Box . WithHeight . (:[]) . fromString++-- # Height and Width++-- | A count of rows+newtype Height = Height { unHeight :: Int }+  deriving (Eq, Ord, Show)++-- | How many 'Rod' are in this 'Box'?+height :: Box -> Int+height b = case unBox b of+  NoHeight _ -> 0+  WithHeight rs -> length rs++-- | A count of columns+newtype Width = Width { unWidth :: Int }+  deriving (Eq, Ord, Show)++-- | How many columns are in this thing? A column is one character+-- wide.  Every 'Bar' in a 'Box' always has the same number of+-- columns.+--+-- This is for things that have a single, solitary width, not things+-- like columns that might have different widths at different+-- points.+class HasWidth a where+  width :: a -> Int++instance HasWidth Bar where+  width = sum . map (X.length . text) . unBar++instance HasWidth Box where+  width b = case unBox b of+    NoHeight i -> i+    WithHeight rs -> case rs of+      [] -> error "cols: error"+      x:_ -> width x++instance HasWidth Chunk where+  width = X.length . text++-- # Making Boxes++-- | A blank 'Box'.  Useful for aligning other 'Box'.+blank+  :: Background+  -> Height+  -> Width+  -> Box+blank bk r c+  | unHeight r < 1 = Box $ NoHeight (max 0 (unWidth c))+  | otherwise = Box . WithHeight $ replicate (unHeight r) row+  where+    row | unWidth c < 1 = Rod []+        | otherwise = Rod [ blanks bk (unWidth c) ]++-- | A 'Box' made of 'Chunk'.  Always one Bar tall, and has as many+-- columns as there are characters in the 'Chunk'.+chunks :: [Chunk] -> Box+chunks = Box . WithHeight . (:[]) . Rod . map (Nibble . Right)++-- | Alignment.+data Align a = Center | NonCenter a+  deriving (Eq, Show)++-- | Vertical alignment.+data Vert = ATop | ABottom+  deriving (Eq, Show)++-- | Horizontal alignment.+data Horiz = ALeft | ARight+  deriving (Eq, Show)++center :: Align a+center = Center++top :: Align Vert+top = NonCenter ATop++bottom :: Align Vert+bottom = NonCenter ABottom++left :: Align Horiz+left = NonCenter ALeft++right :: Align Horiz+right = NonCenter ARight++-- | Merge several Box horizontally into one Box.  That is, with+-- alignment set to ATop:+--+-- > --- ------- ----+-- > --- -------+-- > ---+--+-- becomes+--+-- > --------------+-- > ----------....+-- > ---...........+--+-- With alignment set to ABottom, becomes+--+-- > ---...........+-- > ----------....+-- > --------------++catH :: Background -> Align Vert -> [Box] -> Box+catH bk al bs+  | null bs = Box $ NoHeight 0+  | hght == 0 = Box . NoHeight . sum . map width $ bs+  | otherwise = Box . WithHeight . mergeHoriz . map (pad . unBox) $ bs+  where+    pad = padHoriz bk al hght+    hght = F.maximum . (0:) . map height $ bs++-- | Merge several Box vertically into one Box.  That is, with+-- alignment set to 'left':+--+-- > -------+-- > -------+-- >+-- > ---+-- > ---+-- >+-- > ----+-- > ----+--+-- becomes+--+-- > -------+-- > -------+-- > ---....+-- > ---....+-- > ---....+-- > ----...+-- > ----...+--+-- With alignment set to 'right', becomes+--+-- > -------+-- > -------+-- > ....---+-- > ....---+-- > ...----+-- > ...----++catV :: Background -> Align Horiz -> [Box] -> Box+catV bk al bs+  | null bs = Box $ NoHeight 0+  | otherwise = Box . foldr f (NoHeight w)+      . concat . map (flatten . unBox) $ bs+  where+    w = F.maximum . (0:) . map width $ bs+    f mayR bp = case mayR of+      Nothing -> bp+      Just rw -> case bp of+        WithHeight wh -> WithHeight $ padded : wh+        _ -> WithHeight [padded]+        where+          padded = padVert bk al w rw+    flatten bp = case bp of+      NoHeight _ -> [Nothing]+      WithHeight rs -> map Just rs+++-- | Given the resulting height, pad a list of Height.  So, when given+-- a height of 3 and an alignment of 'top':+--+-- > --------+-- > --------+--+-- becomes+--+-- > --------+-- > --------+-- > ........+--+-- where dashes is a 'Bar' with data, and dots is a blank 'Bar'.++padHoriz :: Background -> Align Vert -> Int -> BoxP -> [Rod]+padHoriz bk a hght bp = case bp of+  NoHeight w -> map (Rod . (:[])) . replicate h $ blanks bk w+  WithHeight rs -> concat [tp, rs, bot]+    where+      nPad = max 0 $ h - length rs+      (nATop, nBot) = case a of+        Center -> split nPad+        NonCenter ATop -> (0, nPad)+        NonCenter ABottom -> (nPad, 0)+      pad = Rod [blanks bk len]+        where+          len = case rs of+            [] -> 0+            x:_ -> width x+      (tp, bot) = (replicate nATop pad, replicate nBot pad)+  where+    h = max 0 hght++-- | Given the resulting width, pad a 'Bar'.  So, when given+-- a width of 10 and an alignment of 'right',+--+-- > -------+--+-- becomes+--+-- > ...-------++padVert+  :: Background+  -> Align Horiz+  -> Int+  -> Rod+  -> Rod+padVert bk a wdth rw@(Rod cs) = Rod . concat $ [lft, cs, rght]+  where+    nPad = max 0 $ w - width rw+    (nLeft, nRight) = case a of+      Center -> split nPad+      NonCenter ALeft -> (0, nPad)+      NonCenter ARight -> (nPad, 0)+    (lft, rght) = (mkPad nLeft, mkPad nRight)+    mkPad n+      | n == 0 = []+      | otherwise = [blanks bk n]+    w = max 0 wdth+++-- | Merge several horizontal Height into one set of horizontal 'Bar'.+-- That is:+--+-- > ----- ----- -----+-- > ----- ----- -----+-- > ----- ----- -----+--+-- into+--+-- > ---------------+-- > ---------------+-- > ---------------+--+-- Strange behavior will result if each input list is not exactly+-- the same length.++mergeHoriz :: [[Rod]] -> [Rod]+mergeHoriz = foldr (zipWith merge) (repeat (Rod []))+  where+    merge (Rod r1) (Rod r2) = Rod $ r1 ++ r2++-- # Viewing++-- | View a 'Box', possibly shrinking it.  You set the size of your+-- viewport and how it is oriented relative to the 'Box' as a whole.+-- The 'Box' returned may be smaller than the argument 'Box', but it+-- will never be bigger.+--+-- Examples:+--+-- >>> :set -XOverloadedStrings+-- >>> let box = catV defaultBackground top [ "ab", "cd" ]+-- >>> printBox . view (Height 1) (Width 1) left top $ box+-- a+--+-- >>> printBox . view (Height 1) (Width 1) right bottom $ box+-- d++viewV :: Int -> Align Vert -> Box -> Box+viewV hght a (Box b) = Box $ case b of+  WithHeight rs+    | h == 0 -> NoHeight . width . head $ rs+    | otherwise -> WithHeight $ case a of+        NonCenter ATop -> take h rs+        NonCenter ABottom -> drop extra rs+        Center -> drop nDrop . take nTake $ rs+          where+            (trimL, trimR) = split extra+            nTake = length rs - trimR+            nDrop = trimL+        where+          extra = max 0 $ length rs - h+  x -> x+  where+    h = max 0 hght++viewH :: Int -> Align Horiz -> Box -> Box+viewH wdth a (Box b) = Box $ case b of+  NoHeight nh -> NoHeight (min w nh)+  WithHeight rs -> WithHeight $ map f rs+    where+      f rw = case a of+        NonCenter ALeft -> takeChars w rw+        NonCenter ARight -> dropChars extra rw+        Center -> dropChars nDrop . takeChars nTake $ rw+          where+            (trimL, trimR) = split extra+            nTake = max 0 $ width rw - trimR+            nDrop = trimL+        where+          extra = max 0 $ width rw - w+  where+    w = max 0 wdth+++dropChars :: Int -> Rod -> Rod+dropChars colsIn = Rod . go colsIn . unRod+  where+    go n cs+      | n <= 0 = cs+      | otherwise = case cs of+         [] -> []+         x:xs+           | lenX <= n -> go (n - lenX) xs+           | otherwise -> x' : xs+           where+             lenX = case unNibble x of+              Left blnk -> numSpaces blnk+              Right chk -> X.length . text $ chk+             x' = case unNibble x of+              Left blnk -> Nibble . Left $+                blnk { numSpaces = numSpaces blnk - n }+              Right chk -> Nibble . Right $ +                chk { text = X.drop n . text $ chk }++takeChars :: Int -> Rod -> Rod+takeChars colsIn = Rod . go colsIn . unRod+  where+    go n cs+      | n <= 0 = []+      | otherwise = case cs of+          [] -> []+          x:xs+            | lenX <= n -> x : go (n - lenX) xs+            | otherwise -> [x']+            where+              (lenX, x') = case unNibble x of+                Left blnk ->+                  ( numSpaces blnk,+                    Nibble . Left $ blnk { numSpaces = n } )+                Right chk ->+                  ( X.length . text $ chk,+                    Nibble . Right $+                    chk { text = X.take n . text $ chk } )++--+-- # Helpers+--++-- | Generate spaces.+blanks+  :: Background+  -- ^ Background colors+  -> Int+  -- ^ Number of blanks+  -> Nibble+blanks bk c = Nibble (Left (Spaces c bk))++-- | Split a number into two parts, so that the sum of the two parts+-- is equal to the original number.+split :: Int -> (Int, Int)+split i = (r, r + rm)+  where+    (r, rm) = i `quotRem` 2+
+ lib/Rainbox/Reader.hs view
@@ -0,0 +1,293 @@+-- | 'Box' with many functions in a 'Reader' monad.+--+-- The advantage of this module over "Rainbox" is that many of the+-- functions have fewer arguments because they are instead carried+-- in the 'Reader' monad.  This also allows you to use four infix+-- operators to easily join up 'Box'.  The disadvantage is that+-- using the 'Reader' monad adds a layer of indirection.+module Rainbox.Reader+  ( -- * Backgrounds+    B.Background(..)+  , R.defaultBackground+  , R.same++  -- * Box properties+  , B.Bar(..)+  , B.Box+  , B.unBox++  -- * Height and columns+  , B.Height(..)+  , B.height+  , B.Width(..)+  , B.HasWidth(..)++  -- * Alignment+  , B.Align+  , B.Vert+  , B.Horiz+  , B.center+  , B.top+  , B.bottom+  , B.left+  , B.right++  -- * Reader monad+  , Specs(..)+  , Env+  , runEnv++  -- * Making Boxes+  , B.blank+  , blankH+  , blankV+  , B.chunks+  , R.chunk++  -- * Pasting Boxes together+  , catH+  , catV+  , sepH+  , sepV+  , punctuateH+  , punctuateV+  , (<->)+  , (<+>)+  , (/-/)+  , (/+/)++  -- * Viewing Boxes+  , view+  , viewH+  , viewV++  -- * Growing Boxes+  , grow+  , growH+  , growV+  , column++  -- * Resizing+  , resize+  , resizeH+  , resizeV++  -- * Printing Boxes+  , R.render+  , R.printBox+  ) where+++import Control.Monad.Trans.Reader+import Data.Functor.Identity+import qualified Rainbox.Box.Primitives as B+import qualified Rainbox.Box as R+import Rainbox.Box.Primitives+  ( Box+  , Height+  , Width+  , Align+  , Horiz+  , Vert+  )++data Specs = Specs+  { background :: B.Background+  , alignH :: Align Horiz+  , alignV :: Align Vert+  , spaceH :: Int+  -- ^ Amount of intervening space for horizontal joins+  , spaceV :: Int+  --  ^ Amount of intervening space for vertical joins+  } deriving (Eq, Show)++type Env = ReaderT Specs++runEnv :: Specs -> Env Identity a -> a+runEnv s = runIdentity . ($ s) . runReaderT++blankH :: Monad m => Int -> Env m Box+blankH i = do+  b <- asks background+  return $ R.blankH b i++blankV :: Monad m => Int -> Env m Box+blankV i = do+  b <- asks background+  return $ R.blankV b i++catH :: Monad m => [Box] -> Env m Box+catH bxs = do+  bk <- asks background+  al <- asks alignV+  return $ B.catH bk al bxs++catV :: Monad m => [Box] -> Env m Box+catV bxs = do+  bk <- asks background+  al <- asks alignH+  return $ B.catV bk al bxs++grow :: Monad m => Height -> Width -> Box -> Env m Box+grow r c bx = do+  b <- asks background+  h <- asks alignH+  v <- asks alignV+  return $ R.grow b r c v h bx++growH :: Monad m => Int -> Box -> Env m Box+growH i bx = do+  b <- asks background+  h <- asks alignH+  return $ R.growH b i h bx++growV :: Monad m => Int -> Box -> Env m Box+growV i bx = do+  b <- asks background+  v <- asks alignV+  return $ R.growV b i v bx++column :: Monad m => [Box] -> Env m [Box]+column bs = do+  bk <- asks background+  ah <- asks alignH+  return $ R.column bk ah bs++resize+  :: Monad m => Height+  -> Width+  -> Box+  -> Env m Box+resize r c bx = do+  b <- asks background+  h <- asks alignH+  v <- asks alignV+  return $ R.resize b r c v h bx++resizeH+  :: Monad m => Int+  -> Box+  -> Env m Box+resizeH i bx = do+  b <- asks background+  h <- asks alignH+  return $ R.resizeH b i h bx++resizeV+  :: Monad m => Int+  -> Box+  -> Env m Box+resizeV i bx = do+  b <- asks background+  v <- asks alignV+  return $ R.resizeV b i v bx++sepH+  :: Monad m => Int+  -> [Box]+  -> Env m Box+sepH i bx = do+  b <- asks background+  v <- asks alignV+  return $ R.sepH b i v bx++sepV+  :: Monad m => Int+  -> [Box]+  -> Env m Box+sepV i bx = do+  b <- asks background+  h <- asks alignH+  return $ R.sepV b i h bx++punctuateH+  :: Monad m => Box+  -> [Box]+  -> Env m Box+punctuateH bx bxs = do+  b <- asks background+  v <- asks alignV+  return $ R.punctuateH b v bx bxs++punctuateV+  :: Monad m => Box+  -> [Box]+  -> Env m Box+punctuateV bx bxs = do+  b <- asks background+  h <- asks alignH+  return $ R.punctuateV b h bx bxs++view+  :: Monad m+  => Height+  -> Width+  -> Box+  -> Env m Box+view h w b = do+  av <- asks alignV+  ah <- asks alignH+  return $ R.view h w av ah b++viewH+  :: Monad m+  => Int+  -> Box+  -> Env m Box+viewH h b = do+  ah <- asks alignH+  return $ B.viewH h ah b++viewV+  :: Monad m+  => Int+  -> Box+  -> Env m Box+viewV h b = do+  av <- asks alignV+  return $ B.viewV h av b++-- | Paste two 'Box' together horizontally with no intervening+-- space.  Left fixity, precedence 5.+(<->) :: Monad m => Box -> Box -> Env m Box+(<->) l r = do+  b <- asks background+  a <- asks alignV+  return $ B.catH b a [l, r]++infixl 5 <->++-- | Paste two 'Box' together horizontally.  Intervening space is+-- determined by 'spaceH'.+-- Left fixity, precedence 5.+(<+>) :: Monad m => Box -> Box -> Env m Box+(<+>) l r = do+  bk <- asks background+  a <- asks alignV+  sp <- asks spaceH+  bx <- blankH sp+  return $ B.catH bk a [ l, bx, r ]++infixl 5 <+>++-- | Paste two 'Box' together vertically with no intervening space.+-- Left fixity, precedence 6.+(/-/) :: Monad m => Box -> Box -> Env m Box+(/-/) h l = do+  b <- asks background+  a <- asks alignH+  return $ B.catV b a [ h, l ]++infixl 6 /-/++-- | Paste two 'Box' together vertically.  Intervening space is+-- determined by 'spaceV'.  Left fixity, precedence 6.+(/+/) :: Monad m => Box -> Box -> Env m Box+(/+/) h l = do+  bk <- asks background+  a <- asks alignH+  sp <- asks spaceV+  bx <- blankV sp+  return $ B.catV bk a [ h, bx, l ]++infixl 6 /+/
+ lib/Rainbox/Tutorial.lhs view
@@ -0,0 +1,174 @@+Rainbox tutorial - introduction+===============================++Rainbox (that is, *Rain*bow and *box*) helps you create colorful,+nicely formatted text boxes.  It is based on the `rainbow` package,+which provides all the color support, so read the [documentation for+that](http://hackage.haskell.org/package/rainbow) before continuing.++[boxes](http://hackage.haskell.org/package/boxes) is a similar+package but without color support.++This file is written in literate Haskell, so it compile and run for+you.  It also means that the compiler checks the examples, which+keeps them accurate.  However, HsColour does not fare so well with+literate Haskell, so this file will not look good from the+hyperlinked source in Haddock.  You're better off viewing it from a+text editor or through [the Gihub+website](http://www.github.com/massysett/rainbox).++A grid of boxes+===============++`Rainbox` is built on `Rainbox.Box`, which contains the building+blocks to create rectangular boxes of text.  Each box is justified+as appropriate and is filled in with a background color to make it+rectangular.++If your needs are complex, use `Rainbox.Box`.  Using it you can+patch boxes together into sort of a crazy quilt.  For simpler needs+you can use `Rainbox`, which only allows you to create grids of+boxes, like a spreadsheet.  This tutorial will show you how to use+the `Rainbox` module.  Everything you need from this package will be+available from the `Rainbox` module.  You will also need to import+packages from `rainbow`:++> {-# LANGUAGE OverloadedStrings #-}+> -- | If you are viewing this module in Haddock, note+> -- that the tutorial is contained in the source code of the+> -- module, which is written in literate Haskell.+> -- It is best viewed in your text editor or through+> -- Github at+> --+> -- <https://github.com/massysett/rainbox/blob/master/lib/Rainbox/Tutorial.lhs>++> module Rainbox.Tutorial where+>+> import Data.List (intersperse)+> import Data.Monoid+> import Data.String+> import System.Console.Rainbow+> import Rainbox+++Making a table of name data+===========================++For this example, we'll print a table of names, addresses, and+account balances.  This type holds the data we're interested in:++> data Record = Record+>   { firstName :: String+>   , lastName :: String+>   , address :: [String]+>   , phone :: String+>   , email :: String+>   , balance :: String+>   } deriving Show++And let's make a list of some sample data:++> records :: [Record]+> records =+>   [ Record+>       { firstName = "Nell"+>       , lastName = "Langston"+>       , address = [ "Owings Mills, MD 21117" ]+>       , phone = "800-588-2300"+>       , email = "NellJLangston@dayrep.com"+>       , balance = "0"+>       }+> +>   , Record+>       { firstName = "Sharon"+>       , lastName = "Sutton"+>       , address = [ "37 Church Street", "Flushing, NY 11354" ]+>       , phone = "312-555-8100"+>       , email = "SharonJSutton@teleworm.us"+>       , balance = "1033.54"+>       }+> +>   , Record+>       { firstName = "Barack"+>       , lastName = "Obama"+>       , address = [ "1600 Pennsylvania Ave NW", "Washington, DC" ]+>       , phone = "877-CASH-NOW"+>       , email = "president@whitehouse.gov"+>       , balance = "23562.00"+>       }+> +>   , Record+>       { firstName = "Bert and Ernie"+>       , lastName = "Sesame"+>       , address = [ "123 Sesame Street", "Lower Level",+>                     "Sesame, WN V6B432" ]+>       , phone = "+45-123-4567"+>       , email = "lower@rhyta.com"+>       , balance = "100,451.05"+>       }+>+>   , Record+>       { firstName = "Vip"+>       , lastName = "Vipperman"+>       , address = [ "10000 Smiley Lane", "Denver, CO 80266" ]+>       , phone = "303-555-1212"+>       , email = "vipperman@rhyta.com"+>       , balance = "301.00"+>       }+>   ]++Building each row of cells+==========================++`Rainbox` works with rows of `Cell`s.  You build one list of `Cell`+for each row in your grid.  Here we will make the last name bold,+and the rest of the text will be plain.  We will also alternate each+row--every even row (starting with the first row) will be the+default color, and the odd rows will be yellow.  We'll make a+function that takes a `Record` and returns another function that,+when applied to a `Chunk` that contains the color for the row,+returns a list of `Cell`.  First let's make a small function that+will make a function that returns a `Cell` with our desired+defaults:++> cell :: [Chunk] -> Chunk -> Cell+> cell cks back = Cell brs left top (backgroundFromChunk back)+>   where+>     brs = map Bar . map ((:[]) . (<> back)) $ cks+++> recordToCells :: Record -> Chunk -> [Cell]+> recordToCells r ck = map ($ ck) $+>   [ cell . (:[]) . fromString . firstName $ r+>   , cell . (:[]) $ (fromString (lastName r) <> bold)+>   , cell . map fromString . address $ r+>   , cell . (:[]) . fromString . phone $ r+>   , cell . (:[]) . fromString . email $ r+>   , cell . (:[]) . fromString . balance $ r+>   ]++Zipping to get rows of cells+============================++> cellRows :: [[Cell]]+> cellRows = zipWith recordToCells records (cycle [mempty, f_yellow])++Adding white space between columns+==================================++If we print the table like it is now, there will be no whitespace,+as `Rainbox` does not add whitespace for you.  Fortunately this is+easy to add.  The string literal, " ", becomes a Cell due to the use+of the Overloaded Strings extension; the cell will have the default+background color.++> spacedOutCells :: [[Cell]]+> spacedOutCells = map (intersperse " ") cellRows++Printing the cells+==================++To see the result, run this function in ghci:++> printSampleBox :: IO ()+> printSampleBox = printBox . gridByRows $ spacedOutCells
+ rainbox.cabal view
@@ -0,0 +1,119 @@+-- Initial rainbox.cabal generated by cabal init.  For further +-- documentation, see http://haskell.org/cabal/users-guide/++name:                rainbox+version:             0.2.0.0+synopsis:            Two-dimensional box pretty printing, with colors+description:         +  Prints boxes in two dimensions, with colors.  Boxes are+  automatically padded with necessary whitespace.+  .+  For more information, please see the Haddock documentation and+  .+  <http://www.github.com/massysett/rainbox>++homepage:            http://www.github.com/massysett/rainbox+license:             BSD3+license-file:        LICENSE+author:              Omari Norman+maintainer:          omari@smileystation.com+copyright:           2014 Omari Norman+category:            Text+build-type:          Simple+extra-source-files:  README.md, sunlight-test.hs+cabal-version:       >=1.10+tested-with: GHC ==7.4.1, GHC ==7.6.3++source-repository head+  type: git+  location: https://github.com/massysett/rainbox.git++library+  ghc-options: -Wall+  exposed-modules:     +      Rainbox+    , Rainbox.Reader+    , Rainbox.Box+    , Rainbox.Box.Primitives+    , Rainbox.Array2d+    , Rainbox.Tutorial++  build-depends:+      base >=4.5.0.0 && < 5+    , rainbow >= 0.12.0.0+    , text >= 0.11.3.1+    , transformers >= 0.3.0.0+    , array >= 0.4.0.1++  hs-source-dirs: lib+  default-language:    Haskell2010++Test-Suite rainbox-visual+  type: exitcode-stdio-1.0+  ghc-options: -Wall+  main-is: rainbox-visual.hs+  hs-source-dirs: test lib+  default-language:    Haskell2010+  ghc-prof-options: -fprof-auto -caf-all+  build-depends:+      tasty >= 0.8+    , tasty-quickcheck >= 0.8+    , array >= 0.4.0.1+    , base >= 4.5.0.0 && < 5+    , rainbow >= 0.12.0.0+    , text >= 0.11.3.1+    , transformers >= 0.3.0.0+    , random >= 1.0.0.0+    , QuickCheck >= 2.6 && < 2.7++  other-modules:+      Visual+    , Test.Rainbow.Generators++Executable rainbox-mosaic+  ghc-options: -Wall+  main-is: rainbox-mosaic.hs+  hs-source-dirs: test lib+  default-language: Haskell2010+  build-depends:+      QuickCheck >= 2.6 && < 2.7+    , base >= 4.5.0.0 && < 5+    , random >= 1.0.0.0+    , rainbow >= 0.12.0.0+    , text >= 0.11.3.1+    , tasty >= 0.8+    , tasty-quickcheck >= 0.8++  if ! flag(mosaic)+    buildable: False++Flag mosaic+  Description: Build the rainbox-mosaic executable.+  Default: False++Test-Suite rainbox-test+  type: exitcode-stdio-1.0+  ghc-options: -Wall+  Main-is: rainbox-test.hs+  hs-source-dirs: test lib+  default-language:    Haskell2010+  ghc-prof-options: -fprof-auto -caf-all+  build-depends:+      tasty >= 0.8+    , tasty-quickcheck >= 0.8+    , array >= 0.4.0.1+    , base >= 4.5.0.0 && < 5+    , rainbow >= 0.12.0.0+    , text >= 0.11.3.1+    , transformers >= 0.3.0.0+    , QuickCheck >= 2.6 && < 2.7++  other-modules:+      RainboxDir+    , RainboxTests+    , Rainbox.Array2dTests+    , Rainbox.BoxTests+    , Rainbox.ReaderTests+    , Rainbox.BoxDir+    , Rainbox.Box.PrimitivesTests+    , Test.Rainbow.Generators
+ sunlight-test.hs view
@@ -0,0 +1,14 @@+module Main where++import Test.Sunlight++inputs = TestInputs+  { tiDescription = Nothing+  , tiCabal = "cabal"+  , tiLowest = ("7.4.1", "ghc-7.4.1", "ghc-pkg-7.4.1")+  , tiDefault = [ ("7.4.1", "ghc-7.4.1", "ghc-pkg-7.4.1")+                , ("7.6.3", "ghc-7.6.3", "ghc-pkg-7.6.3") ]+  , tiTest = []+  }++main = runTests inputs
+ test/Rainbox/Array2dTests.hs view
@@ -0,0 +1,389 @@+module Rainbox.Array2dTests where++import Test.Tasty+import Test.QuickCheck+import Test.Tasty.QuickCheck (testProperty)+import Data.Array+import Rainbox.Array2d++-- | Generates a two-dimensional array of Int.  The size of the+-- array depends on the size parameter.+genArray :: Gen (Array (Int, Int) Int)+genArray = do+  bnds <- genBounds+  let nElems = rangeSize bnds+  es <- vectorOf nElems arbitraryBoundedIntegral+  return $ listArray bnds es++-- | Generates array bounds.  The size of the bounds depends on the+-- size parameter.+genBounds :: Gen ((Int, Int), (Int, Int))+genBounds = do+  w:x:y:z:[] <- vectorOf 4 arbitrarySizedIntegral+  let (minC, maxC) | w < x = (w, x)+                   | otherwise = (x, w)+      (minR, maxR) | y < z = (y, z)+                   | otherwise = (z, y)+  return ((minC, minR), (maxC, maxR))++genTable :: Gen (Table (Int, [(Int, Int)]) (Int, [(Int, Int)]) Int Int Int)+genTable = do+  ay <- genArray+  return $ table (,) (,) ay++type LabelF+  = (Int, [(Int, Int)])+  -> (Int, [(Int, Int)])+  -> Int -> Int -> Int -> Int++type ChangeLabelF+  = (Int, [(Int, Int)])+  -> Int+  -> [((Int, [(Int, Int)]), Int, Int)]+  -> Int++genLabelF :: Gen LabelF+genLabelF = arbitrary++genChangeLabelF :: Gen ChangeLabelF+genChangeLabelF = arbitrary++-- # Properties++-- | Bounds of columns in a Table matches those of the cells+propTableColsBounds+  :: (Ix col, Ix row)+  => Table lCol lRow col row a+  -> Bool+propTableColsBounds tbl = bounds cls == tgtBounds+  where+    cls = lCols tbl+    ((minC, _), (maxC, _)) = bounds . cells $ tbl+    tgtBounds = (minC, maxC)++-- | Bounds of rows in a Table matches those of the cells+propTableRowsBounds+  :: (Ix col, Ix row)+  => Table lCol lRow col row a+  -> Bool+propTableRowsBounds tbl = bounds rws == tgtBounds+  where+    rws = lRows tbl+    ((_, minR), (_, maxR)) = bounds . cells $ tbl+    tgtBounds = (minR, maxR)++-- | Generating a table using the contents of the rows as labels+-- allows reconstruction of the original array++propGenRebuildByRow+  :: (Ix col, Ix row, Eq a)+  => Array (col, row) a+  -> Bool+propGenRebuildByRow ay = ay == ay'+  where+    ay' = array (bounds ay) . concat . elems . lRows+      . table (\_ _ -> ()) fRow $ ay+    fRow rw ls = map g ls+      where+        g (col, a) = ((col, rw), a)++-- | Generating a table using the contents of the columns as labels+-- allows reconstruction of the original array++propGenRebuildByCol+  :: (Ix col, Ix row, Eq a)+  => Array (col, row) a+  -> Bool+propGenRebuildByCol ay = ay == ay'+  where+    ay' = array (bounds ay) . concat . elems . lCols+      . table fCol (\_ _ -> ()) $ ay+    fCol cl ls = map g ls+      where+        g (rw, a) = ((cl, rw), a)++-- | Round-tripping through rows and arrayByRows+propRoundTripRows+  :: Eq a+  => Array (Int, Int) a+  -> Bool+propRoundTripRows ay = sameShape ay ay'+  where+    ay' = arrayByRows . rows $ ay++-- | Round-tripping through columns and arrayByCols+propRoundTripCols+  :: (Eq a, Show a)+  => Array (Int, Int) a+  -> Bool+propRoundTripCols ay = sameShape ay ay'+  where+    ay' = arrayByCols . cols $ ay++-- | True if both arrays have the same shape; that is, the same+-- number of rows and the same number of columns and the same+-- elements.++sameShape+  :: (Ix col, Ix row, Eq a)+  => Array (col, row) a+  -> Array (col, row) a+  -> Bool+sameShape x y = rx == ry && cx == cy && ex == ey+  where+    ((minCx, minRx), (maxCx, maxRx)) = bounds x+    ((minCy, minRy), (maxCy, maxRy)) = bounds y+    rx = rangeSize (minRx, maxRx)+    ry = rangeSize (minRy, maxRy)+    cx = rangeSize (minCx, maxCx)+    cy = rangeSize (minCy, maxCy)+    ex = elems x+    ey = elems y++-- # mapTable properties++-- | mapTable does not change lCols+mapTableNoChangeCols+  :: (Ix col, Ix row, Eq lCol)+  => (lCol -> lRow -> col -> row -> a -> b)+  -> Table lCol lRow col row a+  -> Bool+mapTableNoChangeCols f t = lCols t == lCols t'+  where+    t' = mapTable f t++-- | mapTable does not change lRows+mapTableNoChangeRows+  :: (Ix col, Ix row, Eq lRow)+  => (lCol -> lRow -> col -> row -> a -> b)+  -> Table lCol lRow col row a+  -> Bool+mapTableNoChangeRows f t = lRows t == lRows t'+  where+    t' = mapTable f t++-- | mapTable allows rebuild of original array+mapTableRebuildNoIndices+  :: (Ix col, Ix row, Eq a)+  => Table lCol lRow col row a+  -> Bool+mapTableRebuildNoIndices tbl = cells tbl == ay'+  where+    ay' = listArray (bounds . cells $ tbl) . elems . cells+        . mapTable f $ tbl+    f _ _ _ _ a = a++mapTableRebuildWithIndices+  :: (Ix col, Ix row, Eq a)+  => Table lCol lRow col row a+  -> Bool+mapTableRebuildWithIndices tbl = cells tbl == ay'+  where+    ay' = array (bounds . cells $ tbl) . elems . cells+      . mapTable f $ tbl+    f _ _ cl rw a = ((cl, rw), a)++-- # labelRows and labelCols properties++-- | labelCols allows rebuild of original array+propLabelColsRebuild+  :: (Ix col, Ix row, Eq a)+  => Array (col, row) a+  -> Bool+propLabelColsRebuild ay = ay == ay'+  where+    ay' = array (bounds ay) . concat . elems+      . labelCols f $ ay+    f cl ls = map g ls+      where+        g (rw, a) = ((cl, rw), a)++-- | labelRows allows rebuild of original array+propLabelRowsRebuild+  :: (Ix col, Ix row, Eq a)+  => Array (col, row) a+  -> Bool+propLabelRowsRebuild ay = ay == ay'+  where+    ay' = array (bounds ay) . concat . elems+      . labelRows f $ ay+    f rw ls = map g ls+      where+        g (cl, a) = ((cl, rw), a)++-- # mapRowLabels properties++-- | mapRowLabels does not change column labels+propMapRowLabelsCols+  :: (Ix col, Ix row, Eq lCol)+  => (lRow -> row -> [(lCol, col, a)] -> lRow')+  -> Table lCol lRow col row a+  -> Bool+propMapRowLabelsCols f tb = lbls == lbls'+  where+    lbls = lCols tb+    tb' = mapRowLabels f tb+    lbls' = lCols tb'++-- | mapRowLabels does not change cells+propMapRowLabelsCells+  :: (Ix col, Ix row, Eq a, Eq lCol)+  => (lRow -> row -> [(lCol, col, a)] -> lRow')+  -> Table lCol lRow col row a+  -> Bool+propMapRowLabelsCells f tb = ay == ay'+  where+    ay = cells tb+    tb' = mapRowLabels f tb+    ay' = cells tb'++-- | mapRowLabels permits reconstruction of original array+propMapRowLabelsRebuild+  :: (Ix col, Ix row, Eq a)+  => Table lCol lRow col row a+  -> Bool+propMapRowLabelsRebuild t = ay == ay'+  where+    ay = cells t+    ay' = array (bounds ay) . concat . elems+      . lRows . mapRowLabels f $ t+    f _ rw ls = map g ls+      where+        g (_, cl, a) = ((cl, rw), a)++-- | mapRowLabels gives the original row labels+propMapRowLabelsRelabel+  :: (Ix col, Ix row, Eq a, Eq lRow, Eq lCol)+  => Table lCol lRow col row a+  -> Bool+propMapRowLabelsRelabel t = t == t'+  where+    t' = mapRowLabels (\r _ _ -> r) t++-- # mapColLabels properties++-- | mapColLabels does not change row labels+propMapColLabelsCols+  :: (Ix col, Ix row, Eq lRow)+  => (lCol -> col -> [(lRow, row, a)] -> lCol')+  -> Table lCol lRow col row a+  -> Bool+propMapColLabelsCols f tb = lbls == lbls'+  where+    lbls = lRows tb+    tb' = mapColLabels f tb+    lbls' = lRows tb'++-- | mapColLabels does not change cells+propMapColLabelsCells+  :: (Ix col, Ix row, Eq a, Eq lCol)+  => (lCol -> col -> [(lRow, row, a)] -> lCol')+  -> Table lCol lRow col row a+  -> Bool+propMapColLabelsCells f tb = ay == ay'+  where+    ay = cells tb+    tb' = mapColLabels f tb+    ay' = cells tb'++-- | mapColLabels permits reconstruction of original array+propMapColLabelsRebuild+  :: (Ix col, Ix row, Eq a)+  => Table lCol lRow col row a+  -> Bool+propMapColLabelsRebuild t = ay == ay'+  where+    ay = cells t+    ay' = array (bounds ay) . concat . elems+      . lCols . mapColLabels f $ t+    f _ cl ls = map g ls+      where+        g (_, rw, a) = ((cl, rw), a)++-- | mapColLabels gives the original row labels+propMapColLabelsRelabel+  :: (Ix col, Ix row, Eq a, Eq lRow, Eq lCol)+  => Table lCol lRow col row a+  -> Bool+propMapColLabelsRelabel t = t == t'+  where+    t' = mapColLabels (\r _ _ -> r) t++tests :: TestTree+tests = testGroup "Array2d"+  [ testProperty "bounds of columns in Table matches those of cells" $+    forAll genTable $+    propTableColsBounds++  , testProperty "bounds of rows in Table matches those of cells" $+    forAll genTable $+    propTableRowsBounds++  , testProperty "propGenRebuildByRow" $+    forAll genArray propGenRebuildByRow++  , testProperty "propGenRebuildByCol" $+    forAll genArray propGenRebuildByCol++  , testProperty "propRoundTripRows" $+    forAll genArray propRoundTripRows++  , testProperty "propRoundTripCols" $+    forAll genArray propRoundTripCols++  , testProperty "mapTableNoChangeCols" $+    forAll genLabelF $ \f ->+    forAll genTable $ \t ->+    mapTableNoChangeCols f t++  , testProperty "mapTableNoChangeRows" $+    forAll genLabelF $ \f ->+    forAll genTable $ \t ->+    mapTableNoChangeRows f t++  , testProperty "mapTableRebuildNoIndices" $+    forAll genTable mapTableRebuildNoIndices++  , testProperty "mapTableRebuildWithIndices" $+    forAll genTable mapTableRebuildWithIndices++  , testProperty "propLabelColsRebuild" $+    forAll genArray propLabelColsRebuild++  , testProperty "propLabelRowsRebuild" $+    forAll genArray propLabelRowsRebuild++  , testProperty "propMapRowLabelsCols" $+    forAll genChangeLabelF $ \f ->+    forAll genTable $ \t ->+    propMapRowLabelsCols f t++  , testProperty "propMapRowLabelsCells" $+    forAll genChangeLabelF $ \f ->+    forAll genTable $ \t ->+    propMapRowLabelsCells f t++  , testProperty "propMapRowLabelsRebuild" $+    forAll genTable propMapRowLabelsRebuild++  , testProperty "propMapRowLabelsRelabel" $+    forAll genTable propMapRowLabelsRelabel++  , testProperty "propMapColLabelsCols" $+    forAll genChangeLabelF $ \f ->+    forAll genTable $ \t ->+    propMapColLabelsCols f t++  , testProperty "propMapColLabelsCells" $+    forAll genChangeLabelF $ \f ->+    forAll genTable $ \t ->+    propMapColLabelsCells f t++  , testProperty "propMapColLabelsRebuild" $+    forAll genTable propMapColLabelsRebuild++  , testProperty "propMapColLabelsRelabel" $+    forAll genTable propMapColLabelsRelabel++  ]+
+ test/Rainbox/Box/PrimitivesTests.hs view
@@ -0,0 +1,222 @@+module Rainbox.Box.PrimitivesTests where++import Control.Monad+import Control.Applicative+import Test.Tasty+import Test.Tasty.QuickCheck (testProperty)+import Test.QuickCheck+import System.Console.Rainbow+import qualified Data.Text as X+import qualified Test.Rainbow.Generators as G+import Rainbox.Box.Primitives+import Rainbox.Box (backgroundToTextSpec)++genText :: Gen X.Text+genText = fmap X.pack $ listOf c+  where+    c = elements ['0'..'Z']++genChunk :: Gen Chunk+genChunk = genText >>= G.chunk++genHeight :: Gen Height+genHeight = fmap Height $ frequency [(3, nonNeg), (1, neg)]+  where+    nonNeg = fmap getNonNegative arbitrarySizedIntegral+    neg = fmap (negate . abs) arbitrarySizedIntegral++genWidth :: Gen Width+genWidth = fmap Width $ frequency [(3, nonNeg), (1, neg)]+  where+    nonNeg = fmap abs arbitrarySizedIntegral+    neg = fmap (negate . abs) arbitrarySizedIntegral++genBackground :: Gen Background+genBackground = liftM2 Background G.colors8 G.colors256++-- | Generates blank Box.+genBlankBox :: Gen Box+genBlankBox = liftM3 blank genBackground rw cl+  where+    rw = fmap (Height . abs) arbitrarySizedIntegral+    cl = fmap (Width . abs) arbitrarySizedIntegral++-- | Generates a box using chunks.+genChunkBox :: Gen Box+genChunkBox = fmap chunks $ listOf genChunk++-- | Generates a box using catH.+genCatHBox :: Gen Box+genCatHBox = sized $ \s -> do+  bk <- genBackground+  av <- genAlignVert+  bs <- listOf (resize (s `div` 2) genBox)+  return $ catH bk av bs++-- | Generates a box using catV.+genCatVBox :: Gen Box+genCatVBox = sized $ \s -> do+  bk <- genBackground+  ah <- genAlignHoriz+  bs <- listOf (resize (s `div` 2) genBox)+  return $ catV bk ah bs++-- | Generates a random box.+genBox :: Gen Box+genBox = oneof [ genBlankBox, genCatHBox, genCatVBox, genChunkBox ]++genChunkLen :: Background -> Int -> Gen Chunk+genChunkLen bk l = do+  let ts = backgroundToTextSpec bk+  txt <- fmap X.pack $ vectorOf l (elements ['0'..'Z'])+  return $ Chunk ts txt++-- | Generates a box of text; its horizontal and vertical size+-- depends on the size parameter.+genTextBox :: Gen Box+genTextBox = do+  w <- fmap abs arbitrarySizedIntegral+  h <- fmap abs arbitrarySizedIntegral+  bk <- genBackground+  cks <- vectorOf h (genChunkLen bk w)+  let bxs = map (chunks . (:[])) cks+  bk' <- genBackground+  return $ catV bk' left bxs++++-- # Alignment++genAlignVert :: Gen (Align Vert)+genAlignVert = elements+  [ center, top, bottom ]++genAlignHoriz :: Gen (Align Horiz)+genAlignHoriz = elements [ center, left, right ]++validBox :: Box -> Bool+validBox box = case unBox box of+  NoHeight i -> i > -1+  WithHeight rw -> case rw of+    [] -> False+    x:xs -> all (== width x) . map width $ xs++biggest :: Int -> Gen a -> Gen a+biggest m g = sized $ \s -> resize (min s m) g++data Inputs = Inputs+  { iChunks :: [Chunk]+  , iBackground :: Background+  , iHeight :: Height+  , iWidth :: Width+  , iVert :: Align Vert+  , iHoriz :: Align Horiz+  , iBoxes :: [Box]+  , iBox :: Box+  , iChunk :: Chunk+  } deriving Show++instance Arbitrary Inputs where+  arbitrary = Inputs+    <$> listOf genChunk+    <*> genBackground+    <*> genHeight+    <*> genWidth+    <*> genAlignVert+    <*> genAlignHoriz+    <*> listOf genBlankBox+    <*> genBlankBox+    <*> genChunk++tests :: TestTree+tests = testGroup "BoxTests"+  [ testGroup "blank"+    [ testProperty "makes valid Box" $ \i ->+      validBox $ blank (iBackground i) (iHeight i)+        (iWidth i)++    , testProperty "has right number of rows" $ \i ->+      let ht = unHeight . iHeight $ i+      in (== max 0 ht) . height $ blank (iBackground i)+            (iHeight i) (iWidth i)++    , testProperty "has right number of columns" $ \i ->+      let wt = unWidth . iWidth $ i+      in (== max 0 wt) . width $ blank (iBackground i)+            (iHeight i) (iWidth i)+    ]++  , testGroup "chunks"+    [ testProperty "makes valid Box" $+      validBox . chunks . iChunks++    , testProperty "makes Box whose height is 1" $+      (== 1) . height . chunks . iChunks++    , testProperty "makes Box with cols == number of characters" $ \i ->+      let cks = iChunks i+          nChars = sum . map X.length . map text $ cks+      in (== nChars) . width $ chunks cks+    ]++  , testGroup "catH"+    [ testProperty "makes valid Box" $ \i ->+      validBox $ catH (iBackground i) (iVert i) (iBoxes i)++    , testProperty "is as tall as tallest box" $ \i ->+      let h = maximum . (0 :) . map height $ bs+          bs = iBoxes i+      in (== h) . height $ catH (iBackground i) (iVert i) bs++    , testProperty "is as wide as sum of all widths" $ \i ->+      let s = sum . map width $ bs+          bs = iBoxes i+      in (== s) . width $ catH (iBackground i) (iVert i) bs+    ]++  , testGroup "catV"+    [ testProperty "makes a valid Box" $ \i ->+      validBox $ catV (iBackground i) (iHoriz i) (iBoxes i)++    , testProperty "is as tall as the sum of all heights" $ \i ->+      let h = sum . map height $ bs+          bs = iBoxes i+      in (== h) . height $ catV (iBackground i) (iHoriz i) bs++    , testProperty "is as wide as the widest box" $ \i ->+      let w = maximum . (0:) . map width $ bs+          bs = iBoxes i+      in (== w) . width $ catV (iBackground i) (iHoriz i) bs+    ]++  , testGroup "viewH"+    [ testProperty "makes a valid Box" $ \i ->+      validBox $ viewH (unWidth . iWidth $ i) (iHoriz i) (iBox i)++    , testProperty "number of rows does not change" $ \i ->+      let b = iBox i+      in (== height b) . height $ viewH (unWidth . iWidth $ i)+                                    (iHoriz i) b++    , testProperty "number of columns <= number requested" $ \i ->+      let c = unWidth . iWidth $ i+          tgt = max 0 c+      in (<= tgt) . width $ viewH c (iHoriz i) (iBox i)+    ]++  , testGroup "viewV"+    [ testProperty "makes a valid Box" $ \i ->+      validBox $ viewV (unHeight . iHeight $ i) (iVert i) (iBox i)++    , testProperty "width does not change" $ \i ->+      let b = iBox i+      in (== width b) . width $ viewV (unHeight . iHeight $ i)+                                    (iVert i) b++    , testProperty "number of rows <= number requested" $ \i ->+      let r = unHeight . iHeight $ i+          tgt = max 0 r+      in (<= tgt) . height $ viewV r (iVert i) (iBox i)+    ]+  ]+
+ test/Rainbox/BoxDir.hs view
@@ -0,0 +1,7 @@+module Rainbox.BoxDir where++import qualified Rainbox.Box.PrimitivesTests+import Test.Tasty++tests :: TestTree+tests = testGroup "Box" [ Rainbox.Box.PrimitivesTests.tests ]
+ test/Rainbox/BoxTests.hs view
@@ -0,0 +1,207 @@+module Rainbox.BoxTests where++import Rainbox.Box+import Rainbox.Box.PrimitivesTests+import qualified Data.Text as X+import Test.Tasty.QuickCheck (testProperty)+import Test.Tasty+import System.Console.Rainbow+import Test.QuickCheck hiding (resize)++tests :: TestTree+tests = testGroup "RainboxTests"+  [ testGroup "blankH"+    [ testProperty "makes Box with no height" $ \i ->+      (== 0) . height $ blankH (iBackground i) (unWidth . iWidth $ i)++    , testProperty "makes Box with correct width" $ \i ->+      let w = unWidth . iWidth $ i+          tgt = max 0 w+      in (== tgt) . width $ blankH (iBackground i) w+    ]++  , testGroup "blankV"+    [ testProperty "makes Box with no width" $ \i ->+      (== 0) . width $ blankV (iBackground i) (unHeight . iHeight $ i)++    , testProperty "makes Box with correct height" $ \i ->+      let h = unHeight . iHeight $ i+          tgt = max 0 h+      in (== tgt) . height $ blankV (iBackground i) h+    ]++  , testGroup "chunk"+    [ testProperty "makes Box one high" $+      (== 1) . height . chunk . iChunk++    , testProperty "makes Box as wide as characters in chunk" $ \i ->+      let cs = X.length . text . iChunk $ i+      in (== cs) . width . chunk . iChunk $ i+    ]++  , testGroup "growH"+    [ testProperty "does not change height" $ \i ->+      let bx = iBox i+      in (== height bx) . height $ growH (iBackground i)+            (unWidth . iWidth $ i) (iHoriz i) bx++    , testProperty "new Box is of correct width" $ \i ->+      let bx = iBox i+          tgt = max wdth (width bx)+          wdth = unWidth . iWidth $ i+      in (== tgt) . width $+            growH (iBackground i) wdth (iHoriz i) bx++    , testProperty "new Box is at least as wide as old Box" $ \i ->+      let bx = iBox i+      in (>= width bx) . width $ growH (iBackground i)+            (unWidth . iWidth $ i) (iHoriz i) bx+    ]++  , testGroup "growV"+    [ testProperty "does not change width" $ \i ->+      let bx = iBox i+      in (== width bx) . width $ growV (iBackground i)+            (unWidth . iWidth $ i) (iVert i) bx++    , testProperty "new Box is of correct height" $ \i ->+      let bx = iBox i+          tgt = max (height bx) hght+          hght = unHeight . iHeight $ i+      in (== tgt) . height $+            growV (iBackground i) hght (iVert i) bx++    , testProperty "new Box is at least as tall as old Box" $ \i ->+      let bx = iBox i+      in (>= height bx) . height $ growV (iBackground i)+            (unWidth . iWidth $ i) (iVert i) bx+    ]++  , testGroup "grow"+    [  testProperty "new Box is of correct width" $ \i ->+      let bx = iBox i+          tgt = unWidth . iWidth $ i+      in (\w -> w == width bx || w == tgt) . width $+            growH (iBackground i) tgt (iHoriz i) bx++    , testProperty "new Box is at least as wide as old Box" $ \i ->+      let bx = iBox i+      in (>= width bx) . width $ grow (iBackground i) (iHeight i)+            (iWidth i) (iVert i) (iHoriz i) (iBox i)++    , testProperty "new Box is of correct height" $ \i ->+      let bx = iBox i+          tgt = unHeight . iHeight $ i+      in (\h -> h == height bx || h == tgt) . height $+            grow (iBackground i) (iHeight i) (iWidth i)+                 (iVert i) (iHoriz i) (iBox i)++    , testProperty "new Box is at least as tall as old Box" $ \i ->+      let bx = iBox i+      in (>= height bx) . height $ grow (iBackground i)+            (iHeight i) (iWidth i) (iVert i) (iHoriz i) (iBox i)+    ]++  , testGroup "column"+    [ testProperty "number of inputs == number of outputs" $ \i ->+      let bs = iBoxes i+      in (== length bs) . length $ column (iBackground i) (iHoriz i) bs++    , testProperty "width of outputs is identical" $ \i ->+      case column (iBackground i) (iHoriz i) (iBoxes i) of+        [] -> True+        x:xs -> all (== width x) . map width $ xs++    , testProperty "width of output is as wide as widest input" $ \i ->+      let r = column (iBackground i) (iHoriz i) (iBoxes i)+      in case iBoxes i of+        [] -> null r+        xs -> width (head r) == (maximum . map width $ xs)+    ]++  , testGroup "resizeH"+    [ testProperty "height of resulting Box unchanged" $ \i ->+      let bx = iBox i+      in (== height bx) . height $ resizeH (iBackground i)+            (unWidth . iWidth $ i) (iHoriz i) bx++    , testProperty "result has desired width" $ \i ->+      let tgt = max 0 . unWidth . iWidth $ i+      in (== tgt) . width $ resizeH (iBackground i)+            (unWidth . iWidth $ i) (iHoriz i) (iBox i)+    ]++  , testGroup "resizeV"+    [ testProperty "width of resulting Box unchanged" $ \i ->+      let bx = iBox i+      in (== width bx) . width $ resizeV (iBackground i)+            (unHeight . iHeight $ i) (iVert i) bx++    , testProperty "result has desired height" $ \i ->+      let tgt = max 0 . unHeight . iHeight $ i+      in (== tgt) . height $ resizeV (iBackground i)+            (unHeight . iHeight $ i) (iVert i) (iBox i)+    ]++  , testGroup "resize"+    [ testProperty "result has desired height" $ \i ->+      let tgt = max 0 . unHeight . iHeight $ i+      in (== tgt) . height $ resize (iBackground i)+            (iHeight i) (iWidth i)+            (iVert i) (iHoriz i) (iBox i)++    , testProperty "result has desired width" $ \i ->+      let tgt = max 0 . unWidth . iWidth $ i+      in (== tgt) . width $ resize (iBackground i)+            (iHeight i) (iWidth i) (iVert i) (iHoriz i)+            (iBox i)+    ]++  , testGroup "punctuateH"+    [ testProperty "result has desired width" $ \i ->+      let tgt = (sum . map width $ bs)+            + width bx * (max 0 $ len - 1)+          len = length bs+          bs = iBoxes i+          bx = iBox i+      in (== tgt) . width $ punctuateH (iBackground i)+          (iVert i) bx bs+    ]++  , testGroup "punctuateV"+    [ testProperty "result has desired height" $ \i ->+      let tgt = (sum . map height $ bs)+            + height bx * (max 0 $ len - 1)+          len = length bs+          bs = iBoxes i+          bx = iBox i+      in (== tgt) . height $ punctuateV (iBackground i)+          (iHoriz i) bx bs+    ]++  -- Have to cap size on this one, which is not satisfying.  There+  -- are no apparent bugs.  Apparently what is taking so long is the+  -- Text.replicate in Box.blanks, which is applied from+  -- Box.padHoriz.+  , testGroup "sepH"+    [ testProperty "result has correct width" $+      forAll arbitrarySizedIntegral $ \len ->+      forAll arbitrary $ \i ->+      let tgt = (sum . map width $ bs)+            + max 0 len * max 0 (length bs - 1)+          bs = iBoxes i+      in (== tgt) . width $ sepH (iBackground i) len+          (iVert i) (iBoxes i)+    ]++  , testGroup "sepV"+    [ testProperty "result has correct height" $+      forAll arbitrarySizedIntegral $ \len ->+      forAll arbitrary $ \i ->+      let tgt = (sum . map height $ bs)+            + max 0 len * max 0 (length bs - 1)+          bs = iBoxes i+      in (== tgt) . height $ sepV (iBackground i) len+          (iHoriz i) (iBoxes i)+    ]+  ]
+ test/Rainbox/ReaderTests.hs view
@@ -0,0 +1,133 @@+module Rainbox.ReaderTests where++import qualified Rainbox.Box as R+import Rainbox.Reader+import Rainbox.Box.PrimitivesTests+import Test.QuickCheck hiding (resize)+import Test.Tasty.QuickCheck (testProperty)+import Test.Tasty+import Data.Functor.Identity++tests :: TestTree+tests = testGroup "ReaderTests"+  [ testProperty "blankH" $ \(SpecPair i s) ->+    let p = R.blankH (iBackground i) (unWidth . iWidth $ i)+    in testEq s (blankH (unWidth . iWidth $ i)) p++  , testProperty "blankV" $ \(SpecPair i s) ->+    let p = R.blankV (iBackground i) (unHeight . iHeight $ i)+    in testEq s (blankV (unHeight . iHeight $ i)) p++  , testProperty "catH" $ \(SpecPair i s) ->+    let p = R.catH (iBackground i) (iVert i) (iBoxes i)+    in testEq s (catH (iBoxes i)) p++  , testProperty "catV" $ \(SpecPair i s) ->+    let p = R.catV (iBackground i) (iHoriz i) (iBoxes i)+    in testEq s (catV (iBoxes i)) p++  , testProperty "grow" $ \(SpecPair i s) ->+    let p = R.grow (iBackground i) (iHeight i) (iWidth i)+          (iVert i) (iHoriz i) (iBox i)+    in testEq s (grow (iHeight i) (iWidth i) (iBox i)) p++  , testProperty "growH" $ \(SpecPair i s) ->+    let p = R.growH (iBackground i) (unWidth . iWidth $ i)+          (iHoriz i) (iBox i)+    in testEq s (growH (unWidth . iWidth $ i)+        (iBox i)) p++  , testProperty "growV" $ \(SpecPair i s) ->+    let p = R.growV (iBackground i) (unHeight . iHeight $ i)+          (iVert i) (iBox i)+    in testEq s (growV (unHeight . iHeight $ i)+        (iBox i)) p++  , testProperty "column" $ \(SpecPair i s) ->+    let p = R.column (iBackground i) (iHoriz i) (iBoxes i)+    in testEq s (column (iBoxes i)) p++  , testProperty "resize" $ \(SpecPair i s) ->+    let p = R.resize (iBackground i) (iHeight i) (iWidth i)+          (iVert i) (iHoriz i) (iBox i)+    in testEq s (resize (iHeight i) (iWidth i) (iBox i)) p++  , testProperty "resizeH" $ \(SpecPair i s) ->+    let p = R.resizeH (iBackground i) (unWidth . iWidth $ i)+          (iHoriz i) (iBox i)+    in testEq s (resizeH (unWidth . iWidth $ i) (iBox i)) p++  , testProperty "resizeV" $ \(SpecPair i s) ->+    let p = R.resizeV (iBackground i) (unHeight . iHeight $ i)+          (iVert i) (iBox i)+    in testEq s (resizeV (unHeight . iHeight $ i) (iBox i)) p++  , testProperty "sepH" $ \(SpecPair i s) ->+    let p = R.sepH (iBackground i) (spaceH s) (iVert i) (iBoxes i)+    in testEq s (sepH (spaceH s) (iBoxes i)) p++  , testProperty "sepV" $ \(SpecPair i s) ->+    let p = R.sepV (iBackground i) (spaceV s) (iHoriz i) (iBoxes i)+    in testEq s (sepV (spaceV s) (iBoxes i)) p++  , testProperty "punctuateH" $ \(SpecPair i s) ->+    let p = R.punctuateH (iBackground i) (iVert i) (iBox i) (iBoxes i)+    in testEq s (punctuateH (iBox i) (iBoxes i)) p++  , testProperty "punctuateV" $ \(SpecPair i s) ->+    let p = R.punctuateV (iBackground i) (iHoriz i) (iBox i) (iBoxes i)+    in testEq s (punctuateV (iBox i) (iBoxes i)) p++  , testProperty "viewH" $ \(SpecPair i s) ->+    let p = R.viewH (unWidth . iWidth $ i) (iHoriz i) (iBox i)+    in testEq s (viewH (unWidth . iWidth $ i) (iBox i)) p++  , testProperty "viewV" $ \(SpecPair i s) ->+    let p = R.viewV (unHeight . iHeight $ i) (iVert i) (iBox i)+    in testEq s (viewV (unHeight . iHeight $ i) (iBox i)) p++  , testProperty "view" $ \(SpecPair i s) ->+    let p = R.view (iHeight i) (iWidth i) (iVert i) (iHoriz i)+          (iBox i)+    in testEq s (view (iHeight i) (iWidth i) (iBox i)) p+  ]++testEq :: Eq a => Specs -> Env Identity a -> a -> Bool+testEq s e a = r == a+  where+    r = runEnv s e++specs+  :: Int+  -- ^ Space for horizontal joins+  -> Int+  -- ^ Space for vertical joins+  -> Inputs+  -> Specs+specs h v i = Specs+  { background = iBackground i+  , alignH = iHoriz i+  , alignV = iVert i+  , spaceH = h+  , spaceV = v+  }++genSpecs :: Gen (Inputs, Specs)+genSpecs = do+  h <- frequency [(3, fmap getPositive arbitrarySizedIntegral),+                  (1, arbitrarySizedIntegral)]+  v <- frequency [(3, fmap getPositive arbitrarySizedIntegral),+                  (1, arbitrarySizedIntegral)]+  i <- arbitrary+  let ss = specs h v i+  return (i, ss)++data SpecPair = SpecPair+  { spInputs :: Inputs+  , spSpecs :: Specs+  } deriving Show++instance Arbitrary SpecPair where+  arbitrary = do+    (i, s) <- genSpecs+    return $ SpecPair i s
+ test/RainboxDir.hs view
@@ -0,0 +1,13 @@+module RainboxDir where++import qualified Rainbox.BoxTests+import qualified Rainbox.BoxDir+import qualified Rainbox.ReaderTests+import qualified Rainbox.Array2dTests+import Test.Tasty++tests :: TestTree+tests = testGroup "RainboxDir" [ Rainbox.BoxTests.tests+                               , Rainbox.BoxDir.tests+                               , Rainbox.ReaderTests.tests+                               , Rainbox.Array2dTests.tests ]
+ test/RainboxTests.hs view
@@ -0,0 +1,7 @@+module RainboxTests where++import Test.Tasty+-- import Rainbox++tests :: TestTree+tests = testGroup "Rainbox" []
+ test/Test/Rainbow/Generators.hs view
@@ -0,0 +1,82 @@+module Test.Rainbow.Generators where++import qualified Data.Text as X+import Prelude hiding (last)+import Control.Monad+import Data.Monoid+import Test.QuickCheck+import System.Console.Rainbow.Colors+import System.Console.Rainbow hiding+  (textSpec)+import System.Console.Rainbow.Types hiding+  (style8, style256, textSpec)++-- | Generates one of the valid colors for an 8-color terminal,+-- including the default color.+colors8 :: Gen Color8+colors8 = elements $ c8_default : map snd c8_all++-- | Generates one of the valid colors for an 256-color terminal,+-- including the default color.+colors256 :: Gen Color256+colors256 = elements $ c256_default : map snd c256_all++-- | Generates a foreground color chunk for 8-color terminals.+fgColorChunk8 :: Gen Chunk+fgColorChunk8 = fmap fc8 colors8++-- | Generates a foreground color chunk for 256-color terminals.+fgColorChunk256 :: Gen Chunk+fgColorChunk256 = fmap fc256 colors256++-- | Generates a background color chunk for 8-color terminals.+bgColorChunk8 :: Gen Chunk+bgColorChunk8 = fmap bc8 colors8++-- | Generates a background color chunk for 256-color terminals.+bgColorChunk256 :: Gen Chunk+bgColorChunk256 = fmap bc256 colors256++-- | Generates a color chunk (half foreground, half background) for+-- 8-color terminals.+colorChunk8 :: Gen Chunk+colorChunk8 = oneof [ fgColorChunk8, bgColorChunk8 ]++-- | Generates a color chunk (half foreground, half background) for+-- 256-color terminals.+colorChunk256 :: Gen Chunk+colorChunk256 = oneof [ fgColorChunk256, bgColorChunk256 ]++-- | Generates a color chunk (half for 8-color, half for 256-color).+colorChunk :: Gen Chunk+colorChunk = oneof [ colorChunk8, colorChunk256 ]++last :: a -> Gen (Last a)+last a = frequency [ (3, return $ Last (Just a)),+                     (1, return $ Last Nothing)]++styleCommon :: Gen StyleCommon+styleCommon = liftM4 StyleCommon g g g g+  where+    g = arbitrary >>= last++style8 :: Gen Style8+style8 = liftM3 Style8+  (colors8 >>= last) (colors8 >>= last) styleCommon++style256 :: Gen Style256+style256 = liftM3 Style256+  (colors256 >>= last) (colors256 >>= last) styleCommon++textSpec :: Gen TextSpec+textSpec = liftM2 TextSpec style8 style256++chunk :: X.Text -> Gen Chunk+chunk x = liftM2 Chunk textSpec (return x)++-- | Generates one Chunk for each Text in the list and combines them+-- into one Chunk.+combinedChunks :: [X.Text] -> Gen Chunk+combinedChunks ls = do+  cs <- mapM chunk ls+  return $ foldl (<>) mempty cs
+ test/Visual.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}+module Visual where++import Control.Monad+import Rainbox.Box+import System.Console.Rainbow+import System.Console.Rainbow.Colors+import Data.Monoid+import System.Random+import Test.QuickCheck.Gen hiding (resize)+import Rainbox.Box.PrimitivesTests+import Data.Maybe (fromJust)+import Data.String++colors = f_yellow <> b_blue++narrow = "narrow box" <> colors++midwidth = "medium width box" <> colors++wide = "a wide box, see how wide I am?" <> colors++greenBack = same c8_green++yellowBack = same c8_yellow++all3 = [narrow, midwidth, wide]++short = chunk narrow++midheight = catV greenBack left . map chunk $ [narrow, midwidth]++tall = catV greenBack left . map chunk $ [narrow, midwidth, wide]++sizeParam = 7++putBox b = do+  term <- termFromEnv+  putChunks term . render $ b++describe s b = do+  putStrLn (s ++ ":")+  putBox b+  putStrLn ""++testCompound :: String -> (Background -> [Box] -> Box) -> IO ()+testCompound d f = do+  g <- newStdGen+  let bxs = unGen (replicateM 5 genTextBox) g sizeParam +      bk = unGen genBackground g sizeParam+  describe d $ f bk bxs++testVert+  :: String+  -> (Background -> Align Vert -> [Box] -> Box)+  -> IO ()+testVert d f = do+  testCompound (d ++ ", top align") (\bk bxs -> f bk top bxs)+  testCompound (d ++ ", center align") (\bk bxs -> f bk center bxs)+  testCompound (d ++ ", bottom align") (\bk bxs -> f bk bottom bxs)++testHoriz+  :: String+  -> (Background -> Align Horiz -> [Box] -> Box)+  -> IO ()+testHoriz d f = do+  testCompound (d ++ ", left align") (\bk bxs -> f bk left bxs)+  testCompound (d ++ ", center align") (\bk bxs -> f bk center bxs)+  testCompound (d ++ ", right align") (\bk bxs -> f bk right bxs)++-- | Makes a 10x10 test box.+testBox :: Box+testBox = catV defaultBackground left . map mkLine $ clrs+  where+    mkLine clr = chunk $ txt <> clr+    txt = fromString ['0'..'9']+    lkp k = bc256 . fromJust . lookup k $ c256_all+    clrs = map lkp . take 10 . iterate (+6) $ 160++singleH+  :: String+  -> (Align Horiz -> Box)+  -> IO ()+singleH desc f = do+  describe (desc ++ ", left") (f left)+  describe (desc ++ ", center") (f center)+  describe (desc ++ ", right") (f right)++singleV+  :: String+  -> (Align Vert -> Box)+  -> IO ()+singleV desc f = do+  describe (desc ++ ", top") (f top)+  describe (desc ++ ", center") (f center)+  describe (desc ++ ", bottom") (f bottom)++single+  :: String+  -> (Align Vert -> Align Horiz -> Box)+  -> IO ()+single desc f = do+  singleV (desc ++ ", left") (\av -> f av left)+  singleV (desc ++ ", center") (\av -> f av center)+  singleV (desc ++ ", right") (\av -> f av right)++  singleH (desc ++ ", top") (f top)+  singleH (desc ++ ", center") (f center)+  singleH (desc ++ ", bottom") (f bottom)+++tests :: IO ()+tests = do+  describe "narrow box" . chunk $ narrow+  describe "medium box" . chunk $ midwidth+  describe "wide box" . chunk $ wide++  testHoriz "catV" catV+  testVert "catH" catH++  testVert "sepH" (\bk av bxs -> sepH bk 1 av bxs)+  testHoriz "sepV" (\bk ah bxs -> sepV bk 1 ah bxs)++  testVert "punctuateH" (\bk av bxs -> punctuateH bk av " " bxs)+  testHoriz "punctuateV" (\bk ah bxs -> punctuateV bk ah " " bxs)++  let green = backgroundFromChunk b_green++  testHoriz "column" (\bk ah bxs -> catV defaultBackground left+                        (column bk ah bxs))++  describe "original box for following tests, 10x10" testBox++  single "view, 3x3"+    (\av ah -> view (Height 3) (Width 3) av ah testBox)+  singleH "viewH, 3" (\ah -> viewH 3 ah testBox)+  singleV "viewV, 3" (\av -> viewV 3 av testBox)++  single "grow, 13x13"+    (\av ah -> grow green (Height 13) (Width 13) av ah testBox)+  singleH "growH, 13" (\ah -> growH green 13 ah testBox)+  singleV "growV, 13" (\av -> growV green 13 av testBox)++  single "resize, 13x13"+    (\av ah -> resize green (Height 13) (Width 13) av ah testBox)+  singleH "resizeH, 13" (\ah -> resizeH green 13 ah testBox)+  singleV "resizeV, 13" (\av -> resizeV green 13 av testBox)++  single "resize, 7x7"+    (\av ah -> resize green (Height 7) (Width 7) av ah testBox)+  singleH "resizeH, 7" (\ah -> resizeH green 7 ah testBox)+  singleV "resizeV, 7" (\av -> resizeV green 7 av testBox)+
+ test/rainbox-mosaic.hs view
@@ -0,0 +1,20 @@+-- | Usage:+--+-- Input the size parameter as $1.  Will generate a random box and print+-- it out. Always uses colors.+module Main where++import Test.QuickCheck.Gen+import Rainbox.Box.PrimitivesTests+import System.Environment+import System.Random+import Rainbox.Box+import System.Console.Rainbow++main :: IO ()+main = do+  g <- newStdGen+  s:[] <- getArgs+  let bx = unGen genBox g (read s)+  e <- termFromEnv+  putChunks e . render $ bx
+ test/rainbox-test.hs view
@@ -0,0 +1,11 @@+module Main where++import qualified RainboxDir+import qualified RainboxTests+import Test.Tasty++main :: IO ()+main = defaultMain $ testGroup "rainbox"+  [ RainboxDir.tests+  , RainboxTests.tests+  ]
+ test/rainbox-visual.hs view
@@ -0,0 +1,10 @@+-- | Rainbox visual tests.+--+-- Tests that are intended to be run and then examined visually.++module Main where++import Visual++main :: IO ()+main = Visual.tests