Holumbus-Distribution-0.1.0: source/Holumbus/Console/ServerConsole.hs
-- ----------------------------------------------------------------------------
{- |
Module : Holumbus.Console.ServerConsole
Copyright : Copyright (C) 2009 Sebastian Reese
License : MIT
Maintainer : Sebastian Reese (str@holumbus.org)
Stability : experimental
Portability: portable
Version : 0.1
This module provides a tiny and nice implementation of a little command
shell with communcation over a socket.
It is basically a copy of Holumbus.Console.Console with some changes to fit network communication.
-}
-- ----------------------------------------------------------------------------
module Holumbus.Console.ServerConsole
(
-- * Console datatype
ConsoleData
-- * Operations
, nextOption
, parseOption
, initializeConsole
, addConsoleCommand
, startServerConsole
, defaultaction
, defaultconverter
)
where
import Holumbus.Network.DoWithServer
import System.IO
import Control.Monad (forM)
import qualified Data.Map as Map
import Holumbus.Common.Utils ( handleAll )
import Data.List
-- | Starts the server listening
startServerConsole ::
ConsoleData a -- ^ the console data
-> a -- ^ the console config
-> Int -- ^ console port
-> String -- ^ a consoles prompt
-> IO ()
startServerConsole cdata conf port prompt = doWithServer port (defaultaction cdata conf prompt) defaultconverter prompt
-- | This defaultimplementaion can be used if a simple INput -> Process command -> output patern is used
defaultaction :: ConsoleData a -> a -> String -> ServerAction String
defaultaction cdata conf prompt line sender clients = do
clients' <- handleAll (\_ -> return $ [delete sender $ clients]) $ do
forM (filter (==sender) clients) $
\(Client _ handle _ _) -> do
result <- handleInput line cdata conf
if result == exitString then do
hClose handle
return . delete sender $ clients
else do
hPutStrLn handle result
hPutStr handle prompt
hFlush handle
return clients
return . concat $ clients'
-- | default string to a converter. Converts the input lines into desired format. Here String
defaultconverter :: LineConverter String
defaultconverter = id
-- ----------------------------------------------------------------------------
-- datatypes
-- ----------------------------------------------------------------------------
-- | Map which contains all commands that the user can execute
type ConsoleData a = Map.Map String (ConsoleCommand a)
-- | Console command, only a pair of a function which will be executed
-- and a description
type ConsoleCommand a = ( Maybe (ConsoleFunction a), String )
-- | A console function. The string list represents the arguments
type ConsoleFunction a = (a -> [String] -> IO String )
-- ----------------------------------------------------------------------------
-- operations
-- ----------------------------------------------------------------------------
-- | Creates a new console datatype
initializeConsole :: ConsoleData a
initializeConsole = Map.fromList [(exitString, exitCommand), (helpString, helpCommand)]
-- | Adds a new console command to the function, an existing command with the
-- same name will be overwritten
addConsoleCommand
:: String -- ^ command string (the word the user has to enter when he wants to execute the command)
-> ConsoleFunction a -- ^ the function which should be executed
-> String -- ^ the function description
-> ConsoleData a -- ^ the old console data
-> ConsoleData a
addConsoleCommand c f d m = Map.insert c (Just f, d) m
-- | The exit function string.
exitString :: String
exitString = "exit"
-- | A dummy exit function (Just to print the help description, the command
-- is handled in the main loop.
exitCommand :: ConsoleCommand a
exitCommand = ( Nothing, "exit the console")
-- | The help function string.
helpString :: String
helpString = "help"
-- | A dummy help function (Just to print the help description, the command
-- is handled in the main loop.
helpCommand :: ConsoleCommand a
helpCommand = (Nothing, "print this help")
-- | gets the next option from the command line as string
nextOption :: [String] -> IO (Maybe String, [String])
nextOption o
= handleAll (\_ -> return (Nothing, o)) $
do
if ( null o ) then
return (Nothing, o)
else
return (Just $ head o, tail o)
-- | Simple "parser" for the commandline...
parseOption :: Read a => [String] -> IO (Maybe a, [String])
parseOption o
= handleAll (\_ -> return (Nothing, o)) $
do
if ( null o ) then
return (Nothing, o)
else
return (Just $ read $ head o, tail o)
-- | The main loop. You know... read stdin, parse the input, execute command.
-- You can quit it by the exit-command.
handleInput :: String -> ConsoleData a -> a -> IO String
handleInput line cdata conf
= do
input <- return (words line)
cmd <- return (command input)
args <- return (arguments input)
if (cmd == exitString)
then do
return exitString
else do
if (not $ null cmd) then handleCommand cdata conf cmd args else return "undefined"
where
command s = if (not $ null s) then head s else ""
arguments s = tail s
-- | Picks the command an execute the command function.
handleCommand :: ConsoleData a -> a -> String -> [String] -> IO String
handleCommand cdata conf cmd args
= do
if (cmd == helpString)
then do
printHelp cdata
else do
handleCommand' (Map.lookup cmd cdata)
where
handleCommand' Nothing = do printError
handleCommand' (Just (Nothing, _ )) = do printNoHandler
handleCommand' (Just (Just f, _ )) = do f conf args
-- | Is executed when the function has no handler
printNoHandler :: IO String
printNoHandler = return "no function handler found"
-- | Prints the "command-not-found" message.
printError :: IO String
printError = return "unknown command, try help for a list of available commands"
-- | Prints the help text.
printHelp :: ConsoleData a -> IO String
printHelp cdata = return $ "available Commands:\n"++printCommands "" (Map.toAscList cdata)
where
printCommands acc [] = acc
printCommands acc (x:xs) = printCommands (acc ++ "\n" ++ printCommand x) xs
printCommand (c, (_, t)) = (prettyCommandName 15 c) ++ " - " ++ t
-- | Does some pretty printing for the function descriptions
prettyCommandName :: Int -> String -> String
prettyCommandName n s
| n <= 0 = s
| (n > 0) && (null s) = ' ' : prettyCommandName (n-1) s
| otherwise = x : prettyCommandName (n-1) xs
where
(x:xs) = s