More fun with the Whidbey Console. A small ANSI support method.

The new Console in Whidbey really is fun.  I have some small complains, but I'm guessing I could write enough code to get around them.  My biggest complaint is that you aren't given good enough integration with WriteConsoleOutput(...).  In fact they don't even use WriteConsoleOutput, but instead use WriteFile on the output handle.  They probably do this because of output redirection and the what not and the reliance that everything works with a TextWriter.  WriteConsoleOutput will only write to a console handle and if you give it a non console handle it fails.

What do you get from WriteConsoleOutput?  A lot.  You get the opportunity to specify attributes per character, enabling you to most efficiently write colored text to the screen.  Otherwise you have to do a lot of Foreground/Background color sets.  Since we can't get access to WriteConsoleOutput (well, we could, but we won't since that would defeat the purpose of half the new console stuff), we'll instead go for something a bit different.  Namely, limited ANSI terminal code support. 

How does ANSI terminal coding work?  Well, you specify an escape sequence of the form “\e[” or in C# “\x1B[” since \e isn't a character escape by default.  We'll only be supporting colors for now, so the key sequence accepted is of the form “\e[#,(optional #, optional #, ...)mThe Text To Color”.  You can specify as many codes or numbers as you want.  I'll also implement ConsoleColor names in place of numbers.  However, since ConsoleColor doesn't differentiate between background and foreground colors, the first color (if not a number) will be the foreground and the second the background.  That is simply how it works.  You can also specify the \e[0m flags which specify that the default colors should be used.  These are normally defined as white on black I think, but I've taken them to mean the colors that were set when the program was started or at least the colors that were set the first time you called WriteColoredLine (aka, there is no global handling, though we could have used ResetColor to force the console to grab the defaults).  Anyway, attached is a small sample program.  I'm thinking about adding some additional support for other flags, namely bold, dark, concealed, and reverse.  I also think Blink would be fairly easy, but some enhancements would have to be made to add an animation controller and synchronized writing.  You would never be able to write to Console.WriteLine and would instead need to use some helper class for all output (Terminal.WriteLine?) so that while the buffer was being updated with animations you couldn't write to the screen.  As soon as the animation frame was done your text would go to the terminal.  Anway, I'm having too much fun with this, so I”ll leave you to your own designs.

using System;
using System.Text.RegularExpressions;

public class ConsoleQuickColor {
    private static void WriteColoredLine(string writeColored) {
        WriteColored(writeColored + "\n");
    }
   
    private static void WriteColored(string writeColored) {
        ConsoleColor foreColor = Console.ForegroundColor;
        ConsoleColor backColor = Console.BackgroundColor;
       
        if ( !writeColored.StartsWith("\x1B") ) {
            writeColored = "\x1B[0m" + writeColored;
        }
       
        MatchCollection matches = Regex.Matches(writeColored, "\\e\\[(?<colorCodes>.+?)m(?<textToWrite>.+?)(?=(\\e\\[|\\z))", RegexOptions.Singleline | RegexOptions.Multiline);

        Nullable<ConsoleColor> currentFore = null;
        Nullable<ConsoleColor> currentBack = null;
        foreach(Match m in matches) {
            string[] codesToProcess = m.Groups["colorCodes"].Value.Split(';');
            string textToWrite = m.Groups["textToWrite"].Value;
           
            for(int i = 0; i < codesToProcess.Length; i++) {
                switch(codesToProcess[i]) {
                    case "Reset":
                    case "Clear":
                    case "0":
                        currentFore = null;
                        currentBack = null;
                        break;
                    case "30":
                        currentFore = ConsoleColor.Black;
                        break;
                    case "31":
                        currentFore = ConsoleColor.Red;
                        break;
                    case "32":
                        currentFore = ConsoleColor.Green;
                        break;
                    case "33":
                        currentFore = ConsoleColor.Yellow;
                        break;
                    case "34":
                        currentFore = ConsoleColor.Blue;
                        break;
                    case "35":
                        currentFore = ConsoleColor.Magenta;
                        break;
                    case "36":
                        currentFore = ConsoleColor.Cyan;
                        break;
                    case "37":
                        currentFore = ConsoleColor.White;
                        break;
                    case "40":
                        currentBack = ConsoleColor.Black;
                        break;
                    case "41":
                        currentBack = ConsoleColor.Red;
                        break;
                    case "42":
                        currentBack = ConsoleColor.Green;
                        break;
                    case "43":
                        currentBack = ConsoleColor.Yellow;
                        break;
                    case "44":
                        currentBack = ConsoleColor.Blue;
                        break;
                    case "45":
                        currentBack = ConsoleColor.Magenta;
                        break;
                    case "46":
                        currentBack = ConsoleColor.Cyan;
                        break;
                    case "47":
                        currentBack = ConsoleColor.White;
                        break;
                    default:
                        int numericValue = 0;
                        if ( !int.TryParse(codesToProcess[i], out numericValue) ) {
                            if ( i == 0 ) {
                                try {
                                    currentFore = (ConsoleColor) Enum.Parse(typeof(ConsoleColor), codesToProcess[i], true);
                                } catch {}
                            } else if ( i == 1 ) {
                                try {
                                    currentBack = (ConsoleColor) Enum.Parse(typeof(ConsoleColor), codesToProcess[i], true);
                                } catch {}
                            }
                        }
                        break;
                }
            }
           
            Console.ForegroundColor = (currentFore.HasValue) ? currentFore.Value : foreColor;
            Console.BackgroundColor = (currentBack.HasValue) ? currentBack.Value : backColor;
            Console.Write(textToWrite);
        }
       
        Console.ForegroundColor = foreColor;
        Console.BackgroundColor = backColor;
    }
   
    private static void Main(string[] args) {
        for(int i = 0; i < 16; i++) {
            for(int j = 0; j < 16; j++) {
                WriteColored("\x1B[" + ((ConsoleColor) i).ToString() + ";" + ((ConsoleColor) j).ToString() + "m Hi! ");
            }
            Console.WriteLine();
        }
   
        WriteColoredLine("\n\n\x1B[Blue;BlackmBlue on Black");
        WriteColoredLine("This should use the default text");
        WriteColoredLine("\x1B[40;40;40;37;34;45mA really stupid sequence.  We'll take the last\n");
    }
}

Published Friday, April 30, 2004 4:44 AM by Justin Rogers
Filed under:

Comments

No Comments

Leave a Comment

(required) 
(required) 
(optional)
(required)