haskell / haskeline

A Haskell library for line input in command-line programs.
https://hackage.haskell.org/package/haskeline
BSD 3-Clause "New" or "Revised" License
221 stars 75 forks source link

ANSI Color Support #63

Open Lokathor opened 7 years ago

Lokathor commented 7 years ago

I would like to mix haskeline with the ansi-terminal package to get cross platform colored text support that also has good command line support. However, it seems that the printer function that you obtain from getExternalPrint is actually only an action to queue text for later printing, so directly mixing it with the setSGR function from ansi-terminal doesn't work at all.

Example:

-- base
import Control.Monad (forever)
import Control.Concurrent (threadDelay)
-- haskeline
import System.Console.Haskeline
-- async
import Control.Concurrent.Async
-- transformers
import Control.Monad.IO.Class (liftIO)
-- ansi-terminal
import System.Console.ANSI

counter :: (String -> IO ()) -> IO ()
counter printer = go 0 infColorList where
    infColorList = cycle [Red,Green,Yellow,Blue,Magenta,Cyan,White]
    go n (c:cs) = do
        -- This part should print each message 1 second apart, and also in
        -- different colors. That's not what happens though. Instead, there are
        -- no colors at all. Also, the printing rate is a bit unsteady, but
        -- that's more of a secondary problem.
        threadDelay 1000000
        setSGR [SetColor Foreground Vivid c]
        printer $ show n ++ "\n"
        setSGR [Reset]
        go (n+1) cs

main = runInputT defaultSettings $ do
    printer <- getExternalPrint
    outputStrLn "Spawning a counting thread..."
    (liftIO . async) (counter printer)
    loop
  where
    loop :: InputT IO ()
    loop = do
        input <- getInputLine "> "
        case input of
            Nothing -> return ()
            Just "quit" -> return ()
            Just line -> do
                outputStrLn $ "you typed:" ++ line
                loop

Now I would ideally like to be able to encode the color changes into the String without using the normal raw escape sequences (which don't work on Windows anyway), ideally in a human-readable way so that users would be able to take advantage of this with their inputs. Then an escape sequence aware printing action can switch colors as it prints out the String. Here's an excerpt of the full example of what I mean.

formatString :: String -> IO ()
formatString [] = setSGR [Reset]
formatString ('@':'@':more) = putChar '@' >> formatString more
formatString ('@':c:more) = do
    case c of
        'k' -> setSGR [SetColor Foreground Dull Black]
        'K' -> setSGR [SetColor Foreground Vivid Black]
        'r' -> setSGR [SetColor Foreground Dull Red]
        'R' -> setSGR [SetColor Foreground Vivid Red]
        -- more colors here, you get the idea
        _ -> putChar '@' >> putChar c
    formatString more
formatString ('$':'$':more) = putChar '$' >> formatString more
formatString ('$':c:more) = do
    case c of
        'k' -> setSGR [SetColor Background Dull Black]
        'K' -> setSGR [SetColor Background Vivid Black]
        'r' -> setSGR [SetColor Background Dull Red]
        'R' -> setSGR [SetColor Background Vivid Red]
        -- more colors here, you get the idea
        _ -> putChar '$' >> putChar c
    formatString more
formatString (other:more) = do
    putChar other
    formatString more

This would probably necessitate that the printer that you get from getExternalPrint actually do the printing by blocking until the printing happens instead of just queuing it up for later. Or perhaps getExternalPrint can stay as it is (non-blocking) with updated docs, and then a new blocking printer function can be made available via a new InputT action.

xaverdh commented 7 years ago

While I like the idea of having colors in both the prompt as well as the printing functions, I don't quite understand why you propose to encode that information in the String instead of passing more structured data types to the relevant functions. Something along the lines of data FmtString = FmtPlain ... | FmtSetSGR ... | ... (or just take a representation from a popular library) If you want to encode this in your own escape sequences, you can then easily code this yourself.

Disclaimer: I don't work on this library at all, just interested in better color support as well.

Lokathor commented 7 years ago

I don't think that having escape sequences be part of the string are the only way to possibly do it, but it should be possible to set it up like that at some part of the data pipeline because that way the user can type in color format strings.

Probably it could be more structured internally with a parsing function that accepts user input and turns it into the structured format.

mpilgrem commented 4 years ago

A point of detail: in respect of 'normal raw escape sequences', they did not work on legacy Windows but they do work on Windows 10.

Lokathor commented 4 years ago

If the program enables them, yes. They are not on by default.