packages feed

ansiform-0.9.0.5: examples/Tutorial.hs

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

import           Data.Char (toLower)
import           Ansiform

--------------------------------------------------------------------------------
-- 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