My GHC’s default getLine behaviour isn’t very pretty — pressing arrow keys or even backspace during input results in question marks and escape characters instead of cursor movement or deletion of characters. That’s why I decided to use readline for my MUD client, because readline handles these key presses nicely, comes preinstalled with GHC and has plenty of other features, such as a history. But boy, was it hard to get readline to do what I wanted!
The first problem was that readline wants to know exactly what the current prompt is. When communicating with a MUD, the MUD decides what the prompt is at any time, if any. If I didn’t tell readline explicitly about the prompt whenever it changed, it would delete the entire prompt whenever the user hit Control-U to remove input. This shouldn’t be hard to fix; it’s fairly easy to determine what part of the MUD’s output is the prompt. But in the Haskell wrapper library
System.Console.Readline, there is no function that maps to
rl_set_prompt. Instead, a prompt can only be specified at the
readline call site, and that function blocks until the user has finished writing. In other words, it was impossible to change the prompt in between
readline calls. It took me a while to discover that I could abuse
rl_message instead of
Then emptying the line buffer proved troublesome; simply calling
rl_delete_text(0, n) would leave the user unable to type any new characters. After about three hours’ full-time debugging I found out this is because the cursor location (
rl_point) isn’t automatically set to 0 when removing all text. In other words, the line buffer would be the empty string, but the cursor would still be (internally, not visibly) at, say, position 10.
Here is the final code for writing text to the terminal, preserving input the user hasn’t confirmed yet, and pushing it forward whenever new text is written:
module WriteToTTY where import System.Console.Readline import Control.Monad (when) import Data.List (elemIndices) writeToTTY :: String -> IO () writeToTTY msg = do -- Find out which part of the message is the prompt. let (prePrompt, prompt) = splitAtPrompt msg -- Empty buffer. buf <- getLineBuffer pt <- getPoint setLineBuffer "" setPoint 0 -- AAARGH redisplay Print prePrompt. when (prePrompt > (String, String) splitAtPrompt cs = case elemIndices 'n' cs of  -> ("", cs) is -> splitAt (last is + 1) cs
Here is a program to test module WriteToTTY. Run it with
runhaskell. You can also remove the newline character in the
let msg; then the prompt will replace itself. Pretty cool!
module Main where import System.Console.Readline import Control.Concurrent import WriteToTTY main :: IO () main = do forkIO (output 0) input output :: Int -> IO () output n = do let msg = ("n" ++ show n ++ "> ") writeToTTY msg threadDelay 1000000 output (n + 1) input :: IO () input = do maybeLine <- readline "" putStrLn ("Read " ++ show maybeLine) case maybeLine of Nothing -> return () Just line -> do addHistory line input
Leave a comment!
Martijn loves to receive comments! Add yours by filling out the fields below.