packages feed

hscurses-1.5.0.0: example/ContactManager.hs

-- Copyright (c) 2005-2011 Stefan Wehr (http://www.stefanwehr.de)
--
-- Permission is hereby granted, free of charge, to any person obtaining a
-- copy of this software and associated documentation files (the
-- "Software"), to deal in the Software without restriction, including
-- without limitation the rights to use, copy, modify, merge, publish,
-- distribute, sublicense, and/or sell copies of the Software, and to
-- permit persons to whom the Software is furnished to do so, subject to
-- the following conditions:
--
-- The above copyright notice and this permission notice shall be included
-- in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

-- Simple addressbook application to show the capabilities of the hscurses
-- library, especially its widget set.

module Main where

{-
TODO:

\* save
\* add
-}

import Prelude hiding ((<>))

import Control.Exception
import Control.Monad.State
import Data.List (sort)
import System.Environment (getArgs, getProgName)
import System.Exit (exitFailure)
import Text.PrettyPrint.HughesPJ

import qualified UI.HSCurses.Curses as Curses
import qualified UI.HSCurses.CursesHelper as CursesH
import UI.HSCurses.Logging
import UI.HSCurses.Widgets

type Name = String
type Email = String
type Address = String
type ZIPCode = String
type City = String
type Province = String
type Country = String
type PhoneNumber = String

data Contact = Contact
    { lastName :: Name
    , firstName :: Name
    , emailAddress :: Email
    , address :: Address
    , zipCode :: ZIPCode
    , city :: City
    , province :: Province
    , country :: Country
    , phoneNumber :: PhoneNumber
    }
    deriving (Show, Read, Eq, Ord)

emptyContact =
    Contact
        { lastName = ""
        , firstName = ""
        , emailAddress = ""
        , address = ""
        , zipCode = ""
        , city = ""
        , province = ""
        , country = ""
        , phoneNumber = ""
        }

pprContact c =
    pprLine (combine (lastName c) ", " (firstName c)) $
        pprLine (address c) $
            pprLine (combine (zipCode c) " " (city c)) $
                pprLine (province c) $
                    pprLine (country c) $
                        pprLine (phoneNumber c) $
                            pprLine (emailAddress c) $
                                empty
  where
    pprLine :: String -> Doc -> Doc
    pprLine [] = (<>) empty
    pprLine s = ($$) (text s)
    combine [] _ s2 = s2
    combine s1 _ [] = s1
    combine s1 delim s2 = s1 ++ delim ++ s2

contactToLabelValueList :: Contact -> [(String, String)]
contactToLabelValueList c =
    [ ("Last Name", lastName c)
    , ("First Name", firstName c)
    , ("Email", emailAddress c)
    , ("Address", address c)
    , ("ZIP Code", zipCode c)
    , ("City", city c)
    , ("State/Province", province c)
    , ("Country", country c)
    , ("Phone", phoneNumber c)
    ]

readContacts :: FilePath -> IO [Contact]
readContacts f =
    do
        s <- readFile f
        case reads s of
            [(contacts, [])] -> return (sort contacts)
            _ -> error ("corrupt contact file: " ++ f)

writeContacts :: FilePath -> [Contact] -> IO ()
writeContacts f contacts =
    writeFile f (show contacts)

sampleContacts =
    [ emptyContact
        { lastName = "Wehr"
        , firstName = "Stefan"
        , emailAddress = "mail AT stefanwehr DOT de"
        , address = "28 Loch Maree St"
        , zipCode = "2032"
        , city = "Kingsford"
        , province = "NSW"
        , country = "Australia"
        }
    , emptyContact
        { lastName = "Thorpe"
        , firstName = "Ian"
        , emailAddress = "ian@aol7.com.au"
        , city = "Perth"
        , country = "Australia"
        }
    , emptyContact
        { lastName = "Gates"
        , firstName = "Bill"
        , emailAddress = "billy@microsoft.com"
        }
    , emptyContact
        { lastName = "Stewart"
        , firstName = "Don"
        , address = "CSE, UNSW, 501-16, k17 building"
        , city = "Sydney"
        , country = "Australia"
        }
    ]

title = "contact-manager"

help = "q:quit, d:delete, a:add"

data CMState = CMState
    { cm_styles :: [CursesH.CursesStyle]
    , cm_contacts :: [Contact]
    }
type CM = StateT CMState IO

runCM :: [CursesH.CursesStyle] -> [Contact] -> CM a -> IO a
runCM stys contacts cm =
    evalStateT
        cm
        ( CMState
            { cm_styles = stys
            , cm_contacts = contacts
            }
        )

nthStyle :: Int -> CM CursesH.CursesStyle
nthStyle n =
    do
        cs <- gets cm_styles
        return $ cs !! n

getSize = liftIO $ Curses.scrSize

styles =
    [ CursesH.defaultStyle
    , CursesH.AttributeStyle [CursesH.Bold] CursesH.GreenF CursesH.DarkBlueB
    ]

defStyle = nthStyle 0
lineStyle = nthStyle 1

lineDrawingStyle =
    do
        sty <- lineStyle
        return $ mkDrawingStyle sty

lineOptions =
    do
        sz <- getSize
        ds <- lineDrawingStyle
        return $
            TWOptions
                { twopt_size = TWSizeFixed (1, getWidth sz)
                , twopt_style = ds
                , twopt_halign = AlignLeft
                }

type ToplineWidget = TextWidget
type MidlineWidget = TextWidget
type BotlineWidget = TextWidget
type MsglineWidget = TableWidget
type ContactListWidget = TableWidget
type ContactDetailsWidget = TextWidget
type ContactEditWidget = TableWidget

mkToplineWidget =
    do
        opts <- lineOptions
        return $
            newTextWidget
                (opts {twopt_halign = AlignCenter})
                title

mkMidlineWidget :: ContactListWidget -> CM MidlineWidget
mkMidlineWidget listWidget =
    do
        opts <- lineOptions
        contacts <- gets cm_contacts
        let s = case tbw_pos listWidget of
                Nothing -> show (length contacts)
                Just (row, _) -> show (1 + row) ++ "/" ++ show (length contacts)
        return $ newTextWidget (opts {twopt_halign = AlignRight}) s

mkBotlineWidget =
    do
        opts <- lineOptions
        return $ newTextWidget opts help

-- We need to insert a dummy widget at the lower-right corner of the window,
-- i.e. at the lower-right corner of the message line. Otherwise, an
-- error occurs because drawing a character to this position moves the
-- cursor to the next line, which doesn't exist.
mkMsglineWidget =
    do
        sz <- getSize
        let width = getWidth sz
            opts =
                TWOptions
                    { twopt_size = TWSizeFixed (1, width - 1)
                    , twopt_style = defaultDrawingStyle
                    , twopt_halign = AlignLeft
                    }
            tw = newTextWidget opts "msgline"
            row = [TableCell tw, TableCell $ EmptyWidget (1, 1)]
            tabOpts = defaultTBWOptions {tbwopt_minSize = (1, width)}
        return $ newTableWidget tabOpts [row]

nlines = 4

contactListHeight (h, _) = (h - nlines) `div` 2

contactDetailsHeight (h, _) =
    let n = h - nlines
     in n `div` 2 + (n `mod` 2)

contactListOptions =
    do
        sz <- getSize
        return $
            TBWOptions
                { tbwopt_fillCol = Nothing
                , tbwopt_fillRow = None
                , tbwopt_activeCols = [0]
                , tbwopt_minSize = (contactListHeight sz, getWidth sz)
                }

contactDetailsOptions =
    do
        sz <- getSize
        return $
            TWOptions
                { twopt_size =
                    TWSizeFixed
                        ( contactDetailsHeight sz
                        , getWidth sz
                        )
                , twopt_style = defaultDrawingStyle
                , twopt_halign = AlignLeft
                }

mkContactListWidget :: CM ContactListWidget
mkContactListWidget =
    do
        contacts <- gets cm_contacts
        sz <- getSize
        let lines = alignRows (map contactLine contacts) ' ' "  "
            rows = map (contactRow $ getWidth sz) lines
        opts <- contactListOptions
        return $ newTableWidget opts rows
  where
    contactLine c = [lastName c, firstName c, emailAddress c]
    contactRow w s =
        [ TableCell $
            newTextWidget
                (defaultTWOptions {twopt_size = TWSizeFixed (1, w)})
                s
        ]
    lastRow = [TableCell (EmptyWidget (0, 0))]

mkContactDetailsWidget :: ContactListWidget -> CM ContactDetailsWidget
mkContactDetailsWidget listWidget =
    do
        contacts <- gets cm_contacts
        let contact = case tbw_pos listWidget of
                Nothing -> ""
                Just (row, _) ->
                    let c = contacts !! row
                     in show $ pprContact c
        opts <- contactDetailsOptions
        return $ newTextWidget opts contact

mkContactEditWidget :: Contact -> CM ContactEditWidget
mkContactEditWidget contact =
    let l = contactToLabelValueList contact
        rows = map mkRow l
     in do
            sz <- getSize
            let opts =
                    TBWOptions
                        { tbwopt_fillCol = Just 1
                        , tbwopt_fillRow = None
                        , tbwopt_activeCols = [1]
                        , tbwopt_minSize = (getHeight sz - 3, getWidth sz)
                        }
            return $ newTableWidget opts rows
  where
    mkRow (label, value) =
        let labelW = newTextWidget defaultTWOptions label
            valueW = newEditWidget defaultEWOptions value
         in [TableCell labelW, ActiveTableCell valueW]

mkMainEditWidget contact =
    do
        tlw <- mkToplineWidget
        blw <- mkBotlineWidget
        msglw <- mkMsglineWidget
        ew <- mkContactEditWidget contact
        return $ MainEditWidget tlw blw msglw ew

data MainEditWidget = MainEditWidget
    { toplineEditWidget :: ToplineWidget
    , botlineEditWidget :: BotlineWidget
    , msglineEditWidget :: MsglineWidget
    , contactEditWidget :: ContactEditWidget
    }

mkMainWidget =
    do
        tlw <- mkToplineWidget
        clw <- mkContactListWidget
        mlw <- mkMidlineWidget clw
        cdw <- mkContactDetailsWidget clw
        blw <- mkBotlineWidget
        msglw <- mkMsglineWidget
        return $ MainWidget tlw mlw blw msglw clw cdw

instance Widget MainEditWidget where
    draw pos sz hint w = draw pos sz hint (mkRealMainEditWidget (Just sz) w)
    minSize w = minSize (mkRealMainEditWidget Nothing w)

mkRealMainEditWidget :: (Maybe Size) -> MainEditWidget -> TableWidget
mkRealMainEditWidget msz w =
    let cells =
            [ TableCell $ toplineEditWidget w
            , TableCell $ contactEditWidget w
            , TableCell $ botlineEditWidget w
            , TableCell $ msglineEditWidget w
            ]
        rows = map singletonRow cells
        opts = case msz of
            Nothing -> defaultTBWOptions
            Just sz -> defaultTBWOptions {tbwopt_minSize = sz}
     in newTableWidget opts rows

data MainWidget = MainWidget
    { toplineWidget :: ToplineWidget
    , midlineWidget :: MidlineWidget
    , botlineWidget :: BotlineWidget
    , msglineWidget :: MsglineWidget
    , contactListWidget :: ContactListWidget
    , contactDetailsWidget :: ContactDetailsWidget
    }

instance Widget MainWidget where
    draw pos sz hint w = draw pos sz hint (mkRealMainWidget (Just sz) w)
    minSize w = minSize (mkRealMainWidget Nothing w)

mkRealMainWidget msz w =
    let cells =
            [ TableCell $ toplineWidget w
            , TableCell $ contactListWidget w
            , TableCell $ midlineWidget w
            , TableCell $ contactDetailsWidget w
            , TableCell $ botlineWidget w
            , TableCell $ msglineWidget w
            ]
        rows = map singletonRow cells
        opts = case msz of
            Nothing -> defaultTBWOptions
            Just sz -> defaultTBWOptions {tbwopt_minSize = sz}
     in newTableWidget opts rows

updateStateDependentWidgets :: MainWidget -> ContactListWidget -> CM MainWidget
updateStateDependentWidgets w listWidget =
    do
        detailsWidget <- mkContactDetailsWidget listWidget
        midlineWidget <- mkMidlineWidget listWidget
        return $
            w
                { contactListWidget = listWidget
                , contactDetailsWidget = detailsWidget
                , midlineWidget = midlineWidget
                }

move :: Direction -> MainWidget -> CM MainWidget
move dir w =
    do
        sz <- getSize
        let listWidget = tableWidgetMove dir sz (contactListWidget w)
        updateStateDependentWidgets w listWidget

delete w =
    let lw = contactListWidget w
     in case tbw_pos lw of
            Nothing -> return w
            Just (row, _) ->
                let lw' = tableWidgetDeleteRow row lw
                 in do
                        modify
                            ( \s ->
                                s
                                    { cm_contacts =
                                        deleteAt row (cm_contacts s)
                                    }
                            )
                        updateStateDependentWidgets w lw'

{-
editEventloop w ewm =
    do k <- CursesH.getKey (resize mkMainEditWidget)
       case k of
         Curses.KeyChar 'q' -> return w
         Curses.KeyChar '\r' ->
             do debug "editing..."
                sz <- getSize
                let ewm' = mkRealMainEditWidget (Just sz) ewm
                    (epos, esz) = getCellInfo (0,0) sz ewm' (1,0)
                    ew = contactEditWidget ewm
                (ew', res) <-
                    tableWidgetActivateCurrent (redraw ewm) epos esz DHFocus ew
                editEventloop w ewm
         _ -> editEventloop w ewm
-}

edit w =
    let lw = contactListWidget w
     in case tbw_pos lw of
            Nothing -> return w
            Just (row, _) ->
                do
                    contacts <- gets cm_contacts
                    let c = contacts !! row
                    ew <- mkMainEditWidget c
                    redraw ew
                    return w

-- editEventloop w ew

resize :: (Widget w) => CM w -> CM ()
resize f =
    do
        liftIO $ do
            Curses.endWin
            Curses.resetParams
            Curses.cursSet Curses.CursorInvisible
            Curses.refresh
        w <- f
        redraw w

redraw :: (Widget w) => w -> CM ()
redraw w =
    do
        sz <- getSize
        liftIO $ draw (0, 0) sz DHNormal w
        liftIO $ Curses.refresh

eventloop w =
    do
        k <- CursesH.getKey (resize mkMainWidget)
        debug ("Got key " ++ show k)
        case k of
            Curses.KeyChar 'q' -> return ()
            Curses.KeyChar 'd' -> process $ delete w
            Curses.KeyChar 'e' -> process $ edit w
            Curses.KeyUp -> process $ move DirUp w
            Curses.KeyDown -> process $ move DirDown w
            _ -> eventloop w
  where
    process f =
        do
            w' <- f
            redraw w'
            eventloop w'
cmMain :: CM ()
cmMain =
    do
        w <- mkMainWidget
        redraw w
        eventloop w

main :: IO ()
main =
    do
        args <- getArgs
        contacts <-
            if length args /= 1
                then do
                    p <- getProgName
                    putStrLn ("Usage: " ++ p ++ " contact-file")
                    exitFailure
                else readContacts (args !! 0)
        runCurses contacts `finally` CursesH.end
  where
    runCurses contacts =
        do
            CursesH.start
            cstyles <- CursesH.convertStyles styles
            Curses.cursSet Curses.CursorInvisible
            runCM cstyles contacts cmMain