"Wojciech Moczydlowski, Jr" <[EMAIL PROTECTED]> writes:

>                          However, backspace doesn't work, no matter which
> function(getContents, getLine) I use, which makes debugging and normal work
> with the program a nightmare. Does anyone know any workaround (i.e. sensible
> replacement to getLine) apart from switching to ghc and using the readline 
> library?

I recently posted the attached "SimpleLineEditor" module to the Haskell
libraries mailing list.  To use the module in Hugs, you probably need
to run it through cpp before loading it.  Then, call
    initialise     :: IO ()
before reading the terminal, and
    restore        :: IO ()
afterwards, and the replacement for Prelude.getLine is
    getLineEdited  :: String -> IO (Maybe String)
where the string argument is the prompt for the user.

Regards,
    Malcolm
{-# OPTIONS -cpp #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  System.Console.SimpleLineEditor
-- Copyright   :  (c) 2000,2003, Malcolm Wallace
-- License     :  GPL (if it depends on readline, which is GPL)
--                BSD (otherwise)
--
-- Maintainer  :  [EMAIL PROTECTED]
-- Stability   :  experimental
-- Portability :  non-portable (unix-specific at the moment)
--
-- A simple line editor, using the GNU readline library if available,
-- or a small emulation otherwise.
--
-----------------------------------------------------------------------------

module SimpleLineEditor
  ( initialise          --      :: IO ()
  , restore             --      :: IO ()
  , getLineEdited       --      :: String -> IO (Maybe String)
  , delChars            --      :: String -> IO ()
  , testIt              --      :: IO ()
  ) where

import IO
import Monad (when)
import Char
import System.IO.Unsafe (unsafePerformIO)
import Data.IORef
import Maybe
import System (system)
#if USE_READLINE
import Readline
#endif

-- | Set up the environment so that the terminal passes characters directly
--   into the Haskell program, for immediate interpretation by the line editor.
initialise :: IO ()
initialise = do
    -- The following call is probably non-portable.  Better suggestions?
    -- Note, we turn OFF terminal echoing of input characters
#if ! USE_READLINE
    system("stty -icanon min 1 -echo")
#endif
    hSetBuffering stdout NoBuffering
    hSetBuffering stdin  NoBuffering
#if USE_READLINE
    Readline.initialize
#endif

-- | Restore the environment so that the terminal is usable in normal
--   mode once again.
restore :: IO ()
restore = do
    -- The following call is probably non-portable.  Better suggestions?
    -- Note, we assume the terminal DOES NOT echo any input character
#if ! USE_READLINE
    system("stty icanon echo")
#endif
    hSetBuffering stdout LineBuffering
    hSetBuffering stdin  LineBuffering

-- | Remove the given string from immediately behind (to the left of) the
--   current cursor position.
delChars :: String -> IO ()
delChars []     = return ()
delChars (_:xs) = do putStr "\BS \BS"
                     delChars xs

-- | 'getLineEdited p' uses the string 'p' as a prompt, and returns a line
--   of input from the user.  The user can edit the line in-place before
--   completion, using common readline-like command keys.  (The real readline
--   is used when available, or otherwise a simplified emulation.)

#if USE_READLINE

getLineEdited :: String -> IO (Maybe String)
getLineEdited prompt = do
  ms <- readline prompt
  case ms of 
    Nothing -> return ms
    Just s  -> when (not (all isSpace s)) (addHistory s) >> return ms

#else

-- nasty imperative state holds the command history
history :: IORef [String]
history = unsafePerformIO (newIORef [])

getLineEdited :: String -> IO (Maybe String)
getLineEdited prompt = do
    putStr prompt
    previous <- readIORef history
    ms <- gl "" 0 ([],previous)
    case ms of 
      Nothing -> return ms
      Just s  -> do when (not (all isSpace s))
                         (writeIORef history (reverse s: previous))
                    return ms
  where
    gl s 0 hist = do    -- s is accumulated line (in reverse)
                        -- 0 is cursor position FROM THE END of the string
      cmd <- lineCmd
      case cmd of
        Char c   -> putChar c >> gl (c:s) 0 hist
        Accept   -> return (Just (reverse s))
        Cancel   -> return Nothing
        Delete L -> if null s then gl s 0 hist
                    else delChars "_" >> gl (tail s) 0 hist
        Delete Begin -> delChars s >> gl "" 0 hist
        Move L   -> if not (null s) then putStr ("\BS") >> gl s 1 hist
                    else gl s 0 hist
        History  -> case hist of
                      (fut, [])     -> gl s 0 hist
                      (fut, p:past) -> do delChars s
                                          putStr (reverse p)
                                          gl p 0 (s:fut, past)
        Future   -> case hist of
                      ([], past)    -> gl s 0 hist
                      (f:fut, past) -> do delChars s
                                          putStr (reverse f)
                                          gl f 0 (fut, s:past)
        _        -> gl s 0 hist

    gl s n hist = do    -- s is accumulated line, n(/=0) is cursor position
      cmd <- lineCmd
      case cmd of
        Char c   -> do putStr (c: reverse (take n s))
                       putStr (replicate n '\BS')
                       gl (take n s ++ c: drop n s) n hist
        Accept   -> return (Just (reverse s))
        Cancel   -> return Nothing
        Move R   -> do let n1 = n-1
                       putStr (reverse (take n s)++" ")
                       putStr (replicate n '\BS')
                       gl s n1 hist
        Delete R -> do let n1 = n-1
                       putStr (reverse (take n1 s) ++ " ")
                       putStr (replicate (n1+1) '\BS')
                       gl (take n1 s ++ drop n s) n1 hist
        Move L   -> do let n1 = n+1
                       if n1 <= length s then do
                           putStr ('\BS':reverse (take n1 s))
                           putStr (replicate n1 '\BS')
                           gl s n1 hist
                         else do
                           putStr (reverse s++" ")
                           putStr (replicate n1 '\BS')
                           gl s n hist
        Delete L -> do let n1 = n+1
                       if n1 <= length s then do
                           putStr ('\BS':reverse (take n s)++" ")
                           putStr (replicate n1 '\BS')
                           gl (take n s ++ drop n1 s) n hist
                         else do
                           putStr (reverse s++" ")
                           putStr (replicate n1 '\BS')
                           gl s n hist
        History  -> case hist of
                      (fut, [])     -> gl s n hist
                      (fut, p:past) -> do putStr (replicate n ' ')
                                          delChars s
                                          putStr (reverse p)
                                          gl p 0 (s:fut, past)
        Future   -> case hist of
                      ([], past)    -> gl s n hist
                      (f:fut, past) -> do putStr (replicate n ' ')
                                          delChars s
                                          putStr (reverse f)
                                          gl f 0 (fut, s:past)
        _        -> gl s n hist


-- Define a mini-command language, to separate the lexing of input
-- commands from their interpretation.  Note there is room for expansion
-- here, e.g. commands include word-at-a-time movement, but we don't
-- currently have a key binding for that.
data LineCmd = Char Char | Move Cursor | Delete Cursor
             | Accept | Cancel | History | Future | NoOp
data Cursor  = L | R | WordL | WordR | Begin | End

-- This little lexer for keystrokes does a reasonable job, but there
-- are plenty of problems.  E.g. the backspace key might generate a
-- ^H character and not display it, which results in a mismatched cursor
-- position.  Behaviour is highly dependent on terminal settings I imagine.
lineCmd :: IO LineCmd
lineCmd = do
    c <- hGetChar stdin
    case c of
      '\n'   -> putChar '\n' >> return Accept
      '\^K'  -> putChar '\n' >> return Cancel
      '\DEL' -> return (Delete L)
      '\BS'  -> return (Delete L)
      '\^H'  -> return (Delete L)
      '\^L'  -> return (Move R)
      '\^[' -> do
        c <- hGetChar stdin
        case c of
          'k' -> return History
          'j' -> return Future
          '[' -> do
              c <- hGetChar stdin
              case c of
                'D' -> return (Move L)
                'C' -> return (Move R)
                'A' -> return History
                'B' -> return Future
                '3' -> do c <- hGetChar stdin
                          case c of
                            '~' -> return (Delete R)
                            _   -> return NoOp
                '4' -> do c <- hGetChar stdin
                          case c of
                            '~' -> return (Move End)
                            _   -> return NoOp
                '1' -> do c <- hGetChar stdin
                          case c of
                            '~' -> return (Move Begin)
                            _   -> return NoOp
                _   -> return NoOp
          'O' -> do
              c <- hGetChar stdin
              case c of
                'D' -> return (Move L)
                'C' -> return (Move R)
                'A' -> return History
                'B' -> return Future
                _   -> return NoOp
          _   -> return NoOp
      _ -> return (Char c)

#endif -- USE_READLINE

-- | A simple interactive test for the line-editing functionality.

-- (This illustrates the necessary use of 'initialise' and 'restore'
--  as brackets around the editing loop.)
testIt = initialise >> loop >> restore
  where loop = do l <- getLineEdited "prompt> "
                  when (isJust l) (putStrLn (fromJust l))
                  when (l/=Just "quit") loop

Reply via email to