packages feed

ansiform-1.0.0.1: examples/Tutorial.hs

-- | Small bookmarks application used by the tutorial.
--
--   Run with:
--
--   > cabal run ansiform-tutorial
module Main where

import Ansiform
import Data.Char (toLower)

--------------------------------------------------------------------------------
-- Data model
--------------------------------------------------------------------------------

data Bookmark = Bookmark
    { bookmarkTitle :: String
    , bookmarkURL :: String
    }
    deriving (Eq, Show)

data Model = Model
    { modelBookmarks :: [Bookmark]
    , modelSelected :: Int
    }
    deriving (Eq, Show)

emptyModel :: Model
emptyModel = Model [] 0

addBookmark :: String -> String -> Model -> Model
addBookmark title url (Model bookmarks _) =
    Model (bookmarks ++ [Bookmark title url]) (length bookmarks)

deleteSelected :: Model -> Model
deleteSelected (Model [] _) = emptyModel
deleteSelected (Model bookmarks selected) =
    Model kept (clampSelection (length kept) selected)
  where
    kept = take selected bookmarks ++ drop (selected + 1) bookmarks

updateSelected :: String -> String -> Model -> Model
updateSelected title url model@(Model bookmarks selected) = case bookmarks of
    [] -> model
    _ ->
        let index = clampSelection (length bookmarks) selected
         in Model (replaceAt index (Bookmark title url) bookmarks) index

replaceAt :: Int -> a -> [a] -> [a]
replaceAt index value values =
    take index values ++ [value] ++ drop (index + 1) values

selectedBookmark :: Model -> Maybe Bookmark
selectedBookmark (Model bookmarks selected) =
    bookmarkAt (clampSelection (length bookmarks) selected) bookmarks

bookmarkAt :: Int -> [Bookmark] -> Maybe Bookmark
bookmarkAt index bookmarks = case drop index bookmarks of
    [] -> Nothing
    (bookmark : _) -> Just bookmark

moveSelection :: Int -> Model -> Model
moveSelection delta (Model bookmarks selected) =
    Model bookmarks (clampSelection (length bookmarks) (selected + delta))

clampSelection :: Int -> Int -> Int
clampSelection count index
    | count <= 0 = 0
    | otherwise = max 0 (min (count - 1) index)

--------------------------------------------------------------------------------
-- Shared components
--------------------------------------------------------------------------------

theme :: Theme
theme = defaultTheme

urlStyle :: Style
urlStyle = italic white

bookmarkSettings :: Settings
bookmarkSettings = settings [minSpace 2]

header :: Section
header =
    sectionTop
        1
        [ printerSolidLineCenter
            0
            1
            (themeBar theme)
            [blockSimple "Pocket Bookmarks" (themeHeader theme)]
        , printerEmpty 1
        , printerLineCenter
            4
            [blockSimple "A small Ansiform tutorial" (themeSubtitle theme)]
        ]

bottom :: Section
bottom = sectionRule (ruleDefault (themeRule theme))

ruleBody, ruleFooter :: Rule
ruleBody = rule '%' (themeRule theme)
ruleFooter = rule ':' (themeRule theme)

--------------------------------------------------------------------------------
-- List page
--------------------------------------------------------------------------------

listPage :: Model -> Page
listPage model = page [header, listBody model, listFooter, bottom]

listBody :: Model -> Section
listBody model =
    sectionBodyRule
        1
        ruleBody
        [ if null (modelBookmarks model)
            then
                printerLeft
                    2
                    (blockSimple "No bookmarks yet. Press A to add one." (themeMuted theme))
            else
                printerColumn
                    2
                    [ bookmarkBlock (index == modelSelected model) index bookmark
                    | (index, bookmark) <- zip [0 ..] (modelBookmarks model)
                    ]
        ]

bookmarkBlock :: Bool -> Int -> Bookmark -> Block
bookmarkBlock isSelected index (Bookmark title url) =
    blockCycle
        [ (if isSelected then "> " else "  ", style)
        , (show (index + 1) ++ ". ", style)
        , (title, style)
        , ("  " ++ url, urlStyle)
        ]
  where
    style = if isSelected then themeAccent theme else styleDefault

listFooter :: Section
listFooter =
    sectionBodyRule
        1
        ruleFooter
        [ printerLineCenter
            4
            [ blockMenuStyle "A" "Add" (themeAccent theme)
            , blockMenuStyle "J" "Down" (themeAccent theme)
            , blockMenuStyle "K" "Up" (themeAccent theme)
            , blockMenuStyle "D" "Delete" (themeDanger theme)
            , blockMenuStyle "E" "Edit" (themeAccent theme)
            , blockMenuStyle "X" "Exit" (themeDanger theme)
            ]
        ]

--------------------------------------------------------------------------------
-- Form page
--------------------------------------------------------------------------------

formPage :: String -> Page
formPage instruction = page [header, formBody, formFooter instruction, bottom]

formBody :: Section
formBody =
    sectionBodyRule
        1
        ruleBody
        [ printerLeft 2 (blockSimple "Title: " styleDefault)
        , printerEmpty 1
        , printerLeft 2 (blockSimple "URL:   " styleDefault)
        ]

formFooter :: String -> Section
formFooter instruction =
    sectionBodyRule
        1
        ruleFooter
        [ printerLineCenter
            4
            [blockSimple instruction (themeFooter theme)]
        ]

addInstruction, editInstruction :: String
addInstruction =
    "Type a value and press Enter for each field; empty fields cancel."
editInstruction =
    "Edit the values and press Enter; Esc cancels."

fieldColumn :: String -> Offset
fieldColumn label = formNumber (2 + textWidth label)

formPrompts :: [Prompt]
formPrompts =
    [ promptAt 1 2 (fieldColumn "Title: ")
    , promptAt 1 4 (fieldColumn "URL:   ")
    ]

editPrompts :: Bookmark -> [Prompt]
editPrompts (Bookmark title url) =
    [ promptWithInitial 1 2 (fieldColumn "Title: ") title
    , promptWithInitial 1 4 (fieldColumn "URL:   ") url
    ]

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

main :: IO ()
main = withImmediateInput $ do
    state <- start (listPage emptyModel) bookmarkSettings
    loop emptyModel state

loop :: Model -> State -> IO ()
loop model state = do
    key <- readKeyPress
    case fmap toLower key of
        Nothing -> pure ()
        Just 'a' -> addFlow model state
        Just 'j' -> open (moveSelection 1 model) state
        Just 'k' -> open (moveSelection (-1) model) state
        Just 'd' -> open (deleteSelected model) state
        Just 'e' -> editFlow model state
        Just 'x' -> pure ()
        _ -> loop model state
  where
    open next st = draw (listPage next) st >>= loop next

addFlow :: Model -> State -> IO ()
addFlow model state = do
    (state', result) <- readForm (formPage addInstruction) formPrompts state
    let next = case result of
            Right [title, url]
                | not (null title) && not (null url) ->
                    addBookmark title url model
            _ -> model
    draw (listPage next) state' >>= loop next

editFlow :: Model -> State -> IO ()
editFlow model state = case selectedBookmark model of
    Nothing -> draw (listPage model) state >>= loop model
    Just bookmark -> do
        (state', result) <-
            readForm
                (formPage editInstruction)
                (editPrompts bookmark)
                state
        let next = case result of
                Right [title, url]
                    | not (null title) && not (null url) ->
                        updateSelected title url model
                _ -> model
        draw (listPage next) state' >>= loop next