packages feed

ansiform-1.0.0.1: examples/Demo.hs

-- | Interactive gallery for the Ansiform library.
--
--   Run with:
--
--   > cabal run ansiform-demo
module Main where

import Ansiform
import Data.Char (isSpace, toLower)
import Data.List (elemIndex)
import Data.Maybe (listToMaybe)

--------------------------------------------------------------------------------
-- Application
--------------------------------------------------------------------------------

-- Version of this demo, following the package version.
demoVersion :: String
demoVersion = "1.0.0"

-- The philosophers shown in the Groups screen.
data Philosopher
    = Socrates
    | Plato
    | Aristotle
    | Diogenes
    | Epicurus
    | Cicero
    | Seneca
    | Lucretius
    | MarcusAurelius
    | Virgil
    | Hobbes
    | Spinoza
    | Descartes
    | Locke
    | Hume
    deriving (Eq, Show)

-- The demo is a small screen machine.
data Screen
    = Menu
    | List
    | Groups
    | GroupsPrompt
    | Tables
    | Rich
    | Article
    | Blocks
    | PhilosopherPage Philosopher
    deriving (Eq, Show)

main :: IO ()
main = withImmediateInput $ do
    state <- start (screenPage Menu) demoSettings
    loop Menu state

loop :: Screen -> State -> IO ()
loop screen state = do
    key <- readKeyPress
    case fmap toLower key of
        Nothing -> finish
        Just 'l' -> open List state
        Just 'g' -> open Groups state
        Just 't' -> open Tables state
        Just 'h' -> open Rich state
        Just 'a' -> open Article state
        Just 'c' -> case screen of
            Groups -> choosePhilosopher state
            _ -> open Blocks state
        Just 'r' -> do
            state' <- redraw state
            loop screen state'
        Just 'b' -> open (backScreen screen) state
        Just 'x' -> finish
        Just '\n' -> loop screen state
        Just ' ' -> loop screen state
        Just _ -> loop screen state
  where
    open next st = do
        st' <- draw (screenPage next) st
        loop next st'

    choosePhilosopher st = do
        (st', input) <-
            readForm
                (screenPage GroupsPrompt)
                [promptAt 2 2 promptInputColumn]
                st
        open (chosenScreen (either (const Nothing) listToMaybe input)) st'

    chosenScreen input = maybe Groups PhilosopherPage (input >>= philosopherByNumber . trim)

    finish = putStrLn "\nAnsiform demo finished."

-- Returns to the screen that contains the current one.
backScreen :: Screen -> Screen
backScreen (PhilosopherPage _) = Groups
backScreen _ = Menu

demoSettings :: Settings
demoSettings = settings [minSpace 2]

--------------------------------------------------------------------------------
-- Reusable components
--------------------------------------------------------------------------------

demoTheme :: Theme
demoTheme = defaultTheme

header :: Section
header =
    sectionTop
        1
        [ printerSolidLineCenter
            0
            1
            (themeBar demoTheme)
            [ blockSimple "Ansiform" (themeHeader demoTheme)
            , blockSimple demoVersion (themeHeader demoTheme)
            ]
        , printerEmpty 1
        , printerLineCenter
            4
            [ blockSimple "CLI UI Library" (themeSubtitle demoTheme)
            , blockSimple "Gallery" (themeMuted demoTheme)
            , blockSimple "Demo" (themeAccent demoTheme)
            ]
        ]

bottom :: Section
bottom = sectionRule ruleBottom

footer :: String -> [[Block]] -> Section
footer message commandLines =
    sectionBodyRule
        1
        ruleFooter
        ( [ printerLeft 2 (blockSimple message (themeFooter demoTheme))
          , printerEmpty 1
          ]
            ++ fmap (printerLineCenter 4) commandLines
        )

-- Footer with a single prompt line above the command keys.
footerPrompt :: String -> [[Block]] -> Section
footerPrompt message commandLines =
    sectionBodyRule
        1
        ruleFooter
        ( [ printerLeft promptOffset (blockSimple message (themeFooter demoTheme))
          , printerEmpty 1
          ]
            ++ fmap (printerLineCenter 4) commandLines
        )

promptOffset :: Offset
promptOffset = 2

promptMessage :: String
promptMessage = "Choose a philosopher by number and press Enter: "

-- The input column is the prompt offset plus its display width.
promptInputColumn :: Offset
promptInputColumn = formNumber (unNumber promptOffset + textWidth promptMessage)

ruleBody, ruleFooter, ruleBottom :: Rule
ruleBody = rule '%' (themeRule demoTheme)
ruleFooter = rule ':' (themeRule demoTheme)
ruleBottom = ruleDefault (themeRule demoTheme)

--------------------------------------------------------------------------------
-- Screens
--------------------------------------------------------------------------------

screenPage :: Screen -> Page
screenPage screen = page [header, screenBody screen, screenFooter screen, bottom]

screenBody :: Screen -> Section
screenBody Menu = sectionBodyRule 1 ruleBody [printerColumn 8 menuBlocks]
screenBody List = sectionBodyRule 1 ruleBody [printerList 8 6 composers]
screenBody Groups = sectionBodyRule 1 ruleBody [printerGroups' 4 groups]
screenBody Tables = sectionBodyRule 1 ruleBody [printerTableFormat' demoTablesFormat 4 1 simpleTable]
screenBody Rich = sectionBodyRule 1 ruleBody [printerTableBordered BordersLight defaultTableFormat 8 0 richTable]
screenBody Article = sectionBodyRule 1 ruleBody [printerArticle 2 0 article]
screenBody Blocks = blocksBody
screenBody (PhilosopherPage p) =
    sectionBodyRule 1 ruleBody [printerArticle 2 0 (philosopherArticle p)]
screenBody GroupsPrompt = screenBody Groups

screenFooter :: Screen -> Section
screenFooter Menu = footer "Choose an option above: " menuCommands
screenFooter Groups = footer "Press C to choose a philosopher by number." groupsCommands
screenFooter GroupsPrompt =
    footerPrompt promptMessage pageCommands
screenFooter _ = footer "Press B to go back or R to reload." pageCommands

menuCommands, pageCommands, groupsCommands :: [[Block]]
menuCommands =
    [
        [ blockMenuStyle "L" "List" (themeAccent demoTheme)
        , blockMenuStyle "G" "Groups" (themeAccent demoTheme)
        , blockMenuStyle "T" "Tables" (themeAccent demoTheme)
        , blockMenuStyle "H" "Rich table" (themeAccent demoTheme)
        ]
    ,
        [ blockMenuStyle "A" "Article" (themeAccent demoTheme)
        , blockMenuStyle "C" "Content builders" (themeAccent demoTheme)
        , blockMenuStyle "X" "Exit" (themeDanger demoTheme)
        ]
    ]
pageCommands =
    [
        [ blockMenuStyle "B" "Back" (themeAccent demoTheme)
        , blockMenuStyle "R" "Reload" (themeAccent demoTheme)
        , blockMenuStyle "X" "Exit" (themeDanger demoTheme)
        ]
    ]
groupsCommands =
    [
        [ blockMenuStyle "C" "Choose" (themeAccent demoTheme)
        , blockMenuStyle "B" "Back" (themeAccent demoTheme)
        , blockMenuStyle "R" "Reload" (themeAccent demoTheme)
        ]
    , [blockMenuStyle "X" "Exit" (themeDanger demoTheme)]
    ]

--------------------------------------------------------------------------------
-- Menu and content
--------------------------------------------------------------------------------

menuBlocks :: [Block]
menuBlocks =
    [ blockMenuStyle "L" "List composers" (themeAccent demoTheme)
    , blockMenuStyle "G" "Group philosophers" (themeAccent demoTheme)
    , blockMenuStyle "T" "Tabulate composers" (themeAccent demoTheme)
    , blockMenuStyle "H" "Rich flight table" (themeAccent demoTheme)
    , blockMenuStyle "A" "Article about Ansiform" (themeAccent demoTheme)
    , blockMenuStyle "C" "Content builders" (themeAccent demoTheme)
    , blockMenuStyle "X" "Exit" (themeDanger demoTheme)
    ]

blocksBody :: Section
blocksBody =
    sectionBodyRule
        1
        ruleBody
        [ printerItem (item "A single Item" (solid cyan))
        , printerEmpty 1
        , printerColumn
            4
            [ blockSimple "Content builders" (themeSubtitle demoTheme)
            , blockVal "Version" demoVersion cyan
            , blockBars ["Left", "Center", "Right"] styleDefault (normal magenta)
            , blockSlash "Input" "Output" blue
            , blockCycle
                [ ("Cycle", bold green)
                , (" with ", styleDefault)
                , ("styles", bold green)
                ]
            , blockSeq ["A", "B", "C"] (normal blue) " -> " (normal black')
            , blockAssoc' ["Third", "Second", "First"] styleDefault (normal magenta)
            , blockMarked 'k' "marked letter" (bold green)
            ]
        , printerEmpty 1
        , printerLeft 4 (blockSimple "left aligned line" (normal green))
        , printerCenter 4 (blockSimple "center aligned line" (normal yellow))
        , printerRight 4 (blockSimple "right aligned line" (normal cyan))
        ]

composers :: [Block]
composers =
    [ blockEnum "00" "Tchaikovsky" blue
    , blockEnum "01" "Sibelius" blue
    , blockEnum "02" "Mussorgsky" blue
    , blockEnum "03" "Brahms" blue
    , blockEnum "04" "Chopin" blue
    , blockEnum "05" "Offenbach" blue
    , blockEnum "06" "Khachaturian" blue
    , blockEnum "07" "Holst" blue
    , blockEnum "08" "Bruckner" blue
    , blockEnum "09" "Prokofiev" blue
    , blockEnum "10" "Rimsky-Korsakov" blue
    , blockEnum "11" "Suppe" blue
    ]

--------------------------------------------------------------------------------
-- Philosophers
--------------------------------------------------------------------------------

philosopherGroups :: [(String, [Philosopher])]
philosopherGroups =
    [ ("Greek", [Socrates, Plato, Aristotle, Diogenes, Epicurus])
    , ("Roman", [Cicero, Seneca, Lucretius, MarcusAurelius, Virgil])
    , ("Enlightenment", [Hobbes, Spinoza, Descartes, Locke, Hume])
    ]

philosophers :: [Philosopher]
philosophers = concatMap snd philosopherGroups

groups :: [Group]
groups =
    [ groupSimple
        (blockSimple title (normal yellow))
        [philosopherBlock p | p <- ps]
    | (title, ps) <- philosopherGroups
    ]

philosopherBlock :: Philosopher -> Block
philosopherBlock p =
    blockEnum (philosopherNumber p) (philosopherName p) (philosopherColor p)

philosopherName :: Philosopher -> String
philosopherName p = case p of
    Socrates -> "Socrates"
    Plato -> "Plato"
    Aristotle -> "Aristotle"
    Diogenes -> "Diogenes"
    Epicurus -> "Epicurus"
    Cicero -> "Cicero"
    Seneca -> "Seneca"
    Lucretius -> "Lucretius"
    MarcusAurelius -> "Marcus Aurelius"
    Virgil -> "Virgil"
    Hobbes -> "Hobbes"
    Spinoza -> "Spinoza"
    Descartes -> "Descartes"
    Locke -> "Locke"
    Hume -> "Hume"

philosopherColor :: Philosopher -> Color
philosopherColor p = case p of
    Cicero -> magenta
    Locke -> green
    _ -> black

philosopherNumber :: Philosopher -> String
philosopherNumber p = case elemIndex p philosophers of
    Just i -> pad2 (i + 1)
    Nothing -> "??"

pad2 :: Int -> String
pad2 n
    | n < 10 = '0' : show n
    | otherwise = show n

philosopherByNumber :: String -> Maybe Philosopher
philosopherByNumber s = case reads s :: [(Int, String)] of
    [(n, "")] -> lookup n [(read (philosopherNumber p) :: Int, p) | p <- philosophers]
    _ -> Nothing

trim :: String -> String
trim = f . f
  where
    f = reverse . dropWhile isSpace

--------------------------------------------------------------------------------
-- Tables
--------------------------------------------------------------------------------

richTable :: Maybe Table
richTable = tableBlocks richHeader richRows

richHeader :: [Block]
richHeader =
    [ blockSimple "Flight" (themeTableHeader demoTheme)
    , blockSimple "Company" (normal yellow)
    , blockAssoc ["Origin", "Destination"] (normal yellow) (normal magenta)
    , blockSimple "Departure" (normal cyan)
    , blockSimple "Arrival" (normal red)
    ]

richRows :: [[Block]]
richRows =
    [
        [ blockSimple "1929" (bold green)
        , blockSimple "American Airlines" (normal green)
        , blockAssoc ["PORTLAND", "CHARLOTTE"] styleDefault (normal magenta)
        , blockSimple "22:20" (normal cyan)
        , blockSimple "06:09" (normal red)
        ]
    ,
        [ blockSimple "710" (bold green)
        , blockSimple "Arik Air" (normal green)
        , blockAssoc ["LAGOS", "ABUJA"] styleDefault (normal magenta)
        , blockSimple "06:00" (normal cyan)
        , blockSimple "07:15" (normal red)
        ]
    ,
        [ blockSimple "309" (bold green)
        , blockSimple "British Airways" (normal green)
        , blockAssoc ["PARIS", "LONDON"] styleDefault (normal magenta)
        , blockSimple "12:10" (normal cyan)
        , blockSimple "12:30" (normal red)
        ]
    ,
        [ blockSimple "3002" (bold green)
        , blockSimple "EgyptAir" (normal green)
        , blockAssoc ["CAIRO", "DUBAI"] styleDefault (normal magenta)
        , blockSimple "19:15" (normal cyan)
        , blockSimple "00:35" (normal red)
        ]
    ,
        [ blockSimple "5139" (bold green)
        , blockSimple "AerCaribe Peru" (normal green)
        , blockAssoc ["BOGOTA", "LIMA"] styleDefault (normal magenta)
        , blockSimple "06:00" (normal cyan)
        , blockSimple "23:00" (normal red)
        ]
    ]

simpleTable :: Maybe Table
simpleTable = tableSimple simpleHeader (themeTableBody demoTheme) simpleRows

-- Right aligns the numeric columns and truncates long composer names.
demoTablesFormat :: TableFormat
demoTablesFormat =
    tableFormat
        [ defaultColumnSpec
            { columnMaxWidth = Just 11
            , columnEllipsis = EllipsisChar '…'
            }
        , defaultColumnSpec
        , defaultColumnSpec{columnAlign = AlignRight}
        , defaultColumnSpec{columnAlign = AlignRight}
        , defaultColumnSpec{columnAlign = AlignRight}
        ]

simpleHeader :: [Block]
simpleHeader =
    [ blockSimple "Composer" (bold white)
    , blockSimple "Country" (bold cyan)
    , blockSimple "Born" (bold green)
    , blockSimple "Died" (bold green)
    , blockSimple "Works" (bold yellow)
    ]

simpleRows :: [[String]]
simpleRows =
    [ ["Tchaikovsky", "Russia", "1840", "1893", "6"]
    , ["Sibelius", "Finland", "1865", "1957", "7"]
    , ["Mussorgsky", "Russia", "1839", "1881", "5"]
    , ["Brahms", "Germany", "1833", "1897", "4"]
    , ["Chopin", "Poland", "1810", "1849", "6"]
    , ["Offenbach", "Germany", "1819", "1880", "5"]
    , ["Holst", "England", "1874", "1934", "5"]
    , ["Prokofiev", "Russia", "1891", "1953", "7"]
    ]

--------------------------------------------------------------------------------
-- Article
--------------------------------------------------------------------------------

article :: Article
article = [paragraph1, paragraph2, paragraph3]

paragraph1, paragraph2, paragraph3 :: Paragraph
paragraph1 =
    [ blockSimple "Ansiform " textBold
    , blockSimple "is a formatting DSL for command line interfaces. " textRegular
    , blockSimple "Builders are pure, " textItalic
    , blockSimple "pages are immutable, " textUnderline
    , blockSimple "and drawing is the only layer that talks to the terminal." textRegular
    ]
paragraph2 =
    [ blockSimple "Layout adapts to the terminal width. " textRegular
    , blockSimple "Tables reject malformed input, " (textMark yellow)
    , blockSimple "and paragraphs always make progress, " (textColor cyan)
    , blockSimple "even when a single word is wider than the screen." textRegular
    , blockSimple " Lists flow into columns, " textItalic
    , blockSimple "groups share space evenly, " textBold
    , blockSimple "and tables can mix independently styled cells." textRegular
    ]
paragraph3 =
    [ blockSimple "The standard interaction is immediate: " textRegular
    , blockSimple "press a key and the page changes, " textBold
    , blockSimple "without a prompt and without Enter. " textRegular
    , blockSimple "Styles are suppressed on redirected output, " textItalic
    , blockSimple "and wide characters, combining marks and emoji " textRegular
    , blockSimple "keep their alignment " (textMark cyan)
    , blockSimple "because layout measures terminal columns, not code points." textRegular
    ]

--------------------------------------------------------------------------------
-- Philosopher articles
--------------------------------------------------------------------------------

ciceroArticle :: Article
ciceroArticle = [ciceroParagraph1, ciceroParagraph2]

ciceroParagraph1, ciceroParagraph2 :: Paragraph
ciceroParagraph1 =
    [ blockSimple "Marcus Tullius Cicero " textBold
    , blockSimple "(106-43 BC) was a Roman statesman, orator and philosopher. " textRegular
    , blockSimple "He was consul in 63 BC " textItalic
    , blockSimple "and is known for the Catilinarian orations and for his many speeches." textRegular
    ]
ciceroParagraph2 =
    [ blockSimple "His philosophical works, " textRegular
    , blockSimple "such as De Re Publica and De Officiis, " (textMark yellow)
    , blockSimple "helped bring Greek philosophy into Latin. " textRegular
    , blockSimple "He was killed during the proscriptions of 43 BC." (textColor cyan)
    ]

lockeArticle :: Article
lockeArticle = [lockeParagraph1, lockeParagraph2]

lockeParagraph1, lockeParagraph2 :: Paragraph
lockeParagraph1 =
    [ blockSimple "John Locke " textBold
    , blockSimple "(1632-1704) was an English philosopher and physician. " textRegular
    , blockSimple "He is a central figure of empiricism " textItalic
    , blockSimple "and argued that the mind begins as a blank slate." textRegular
    ]
lockeParagraph2 =
    [ blockSimple "His Two Treatises of Government " textRegular
    , blockSimple "defended natural rights and government by consent. " (textMark yellow)
    , blockSimple "The Essay Concerning Human Understanding " textItalic
    , blockSimple "shaped modern epistemology and liberal thought." (textColor cyan)
    ]

philosopherArticle :: Philosopher -> Article
philosopherArticle p = case p of
    Socrates ->
        bio
            "Socrates "
            "(c. 470-399 BC) was an Athenian philosopher. "
            "He examined ethical concepts through dialogue and questioning. "
            "He wrote nothing and is known mainly through Plato."
    Plato ->
        bio
            "Plato "
            "(c. 427-347 BC) was an Athenian philosopher. "
            "He developed the theory of Forms and founded the Academy. "
            "His dialogues include the Republic and the Symposium."
    Aristotle ->
        bio
            "Aristotle "
            "(384-322 BC) was a Greek philosopher and student of Plato. "
            "He worked on logic, ethics, politics and natural science. "
            "He also tutored Alexander the Great."
    Diogenes ->
        bio
            "Diogenes "
            "(c. 412-323 BC) was a Cynic philosopher. "
            "He rejected wealth and convention and lived with extreme simplicity. "
            "His anecdotes illustrate independence and moral frankness."
    Epicurus ->
        bio
            "Epicurus "
            "(341-270 BC) was a Greek philosopher. "
            "He founded a school known as the Garden and taught that pleasure is the absence of pain. "
            "His atomism influenced later materialist thought."
    Cicero -> ciceroArticle
    Seneca ->
        bio
            "Seneca "
            "(c. 4 BC-AD 65) was a Roman Stoic philosopher and statesman. "
            "He wrote letters and essays on ethics and advised Nero. "
            "He was forced to commit suicide in AD 65."
    Lucretius ->
        bio
            "Lucretius "
            "(c. 99-55 BC) was a Roman poet and philosopher. "
            "His poem De Rerum Natura explains Epicurean atomism. "
            "It is a major source for ancient materialist ideas."
    MarcusAurelius ->
        bio
            "Marcus Aurelius "
            "(AD 121-180) was a Roman emperor and Stoic philosopher. "
            "His Meditations record private reflections on duty and self-control. "
            "He is often called the philosopher emperor."
    Virgil ->
        bio
            "Virgil "
            "(70-19 BC) was a Roman poet. "
            "He wrote the Eclogues, the Georgics and the Aeneid. "
            "The Aeneid became a foundational text of Roman literature."
    Hobbes ->
        bio
            "Thomas Hobbes "
            "(1588-1679) was an English philosopher. "
            "He is known for Leviathan and for social contract theory. "
            "He argued for a strong sovereign to prevent civil war."
    Spinoza ->
        bio
            "Baruch Spinoza "
            "(1632-1677) was a Dutch philosopher. "
            "His Ethics presents a rationalist and monist system. "
            "He defended freedom of thought and religious toleration."
    Descartes ->
        bio
            "René Descartes "
            "(1596-1650) was a French philosopher and mathematician. "
            "He is known for the cogito and for mind-body dualism. "
            "He helped shape modern rationalism and the scientific method."
    Locke -> lockeArticle
    Hume ->
        bio
            "David Hume "
            "(1711-1776) was a Scottish philosopher and historian. "
            "He developed empiricism and an influential analysis of causation. "
            "His works include A Treatise of Human Nature and An Enquiry Concerning Human Understanding."

bio :: String -> String -> String -> String -> Article
bio name dates role detail =
    [
        [ blockSimple name textBold
        , blockSimple dates textRegular
        , blockSimple role textItalic
        , blockSimple detail textRegular
        ]
    ]