Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats – natively work with DOCX, XLSX, PPT, PDF, images and more

A .NET Color Console Helper


:P
On this page:

I recently started posting shorter and more basic posts on this blog with some small, but practical things that I've found useful. Incidentally over the years of blogging some of the most popular posts here have been these short and often very simple tips rather than the long form articles which tend to have high short term popularity but less long term appeal. So in the spirit of short posts here's one about a simple ColorConsole helper class I use to add some color to my Console applications more easily.

Color me Console

In this post I'll discuss a small Console helper class I've been using to make it easier and more consistent to use colors with the .NET Console command. While colors are easy enough to access in the Console, to switch colors is a bit of a pain with the plain Console API. The simple class I present here makes it easier to write a line or string in a given color, write a line with multiple colors (using simple [color]text[/color] templating) and an easy way to create a header. The following is a very simple ColorConsole class that provides some useful color helpers:

  • WriteLine() and Write() methods with Color parameters
  • WriteError(), WriteInfo(), WriteSuccess() and WriteWarning() methods
  • A color template expansion WriteEmbeddedColorLine() function
  • A header generation routine

The Write methods let me quickly write output in a specific color without worrying about setting and resetting the color. The output is written with the specified color and the color is always reset to previously active color.

The high level wrappers like WriteError() and WriteSuccess() provide an alternative the raw Write methods and are more explicit about the intent of the message. It makes colors more consistent with color choices for common situations like error or informational statements.

I often find myself writing Console output that requires multiple more than a single color in a line of text when highlighting values over labels or displaying multiple values of difference importance. I can use multiple Write() statements with colors for this, but to make this easier to read I created a templated method that allows delimiting text with [color]text[/color] delimiters in a string.

ColorConsole.WriteEmbeddedColorLine($"Site Url: [darkcyan]{ServerConfig.GetHttpUrl()}[/darkcyan] [darkgray](binding: {HostUrl})[darkgray]");

Try it out

To use the class looks something like this:

static void Main(string[] args)
{
    ColorConsole.WriteWrappedHeader("Color Console Examples");

    Console.WriteLine("\nUsing a splash of color in your Console code more easily... (plain text)\n");

    ColorConsole.WriteLine("Color me this - in Red", ConsoleColor.Red);

    ColorConsole.WriteWrappedHeader("Off with their green Heads!", headerColor: ConsoleColor.Green);


    ColorConsole.WriteWarning("\nWorking...\n");

    Console.WriteLine("Writing some mixed colors: (plain text)");
    ColorConsole.WriteEmbeddedColorLine(
        "Launch the site with [darkcyan]https://localhost:5200[/darkcyan] and press [yellow]Ctrl-c[/yellow] to exit.\n");


    ColorConsole.WriteSuccess("The operation completed successfully.");
}

which produces the following output:

Code

Here's the ColorConsole class:

/// <summary>
/// Console Color Helper class that provides coloring to individual commands
/// </summary>
public static class ColorConsole
{
    /// <summary>
    /// WriteLine with color
    /// </summary>
    /// <param name="text"></param>
    /// <param name="color"></param>
    public static void WriteLine(string text, ConsoleColor? color = null)
    {
        if (color.HasValue)
        {
            var oldColor = System.Console.ForegroundColor;
            if (color == oldColor)
                Console.WriteLine(text);
            else
            {
                Console.ForegroundColor = color.Value;
                Console.WriteLine(text);
                Console.ForegroundColor = oldColor;
            }
        }
        else
            Console.WriteLine(text);
    }

    /// <summary>
    /// Writes out a line with a specific color as a string
    /// </summary>
    /// <param name="text">Text to write</param>
    /// <param name="color">A console color. Must match ConsoleColors collection names (case insensitive)</param>
    public static void WriteLine(string text, string color)
    {
        if (string.IsNullOrEmpty(color))
        {
            WriteLine(text);
            return;
        }

        if (!Enum.TryParse(color, true, out ConsoleColor col))
        {
            WriteLine(text);
        }
        else
        {
            WriteLine(text, col);
        }
    }

    /// <summary>
    /// Write with color
    /// </summary>
    /// <param name="text"></param>
    /// <param name="color"></param>
    public static void Write(string text, ConsoleColor? color = null)
    {
        if (color.HasValue)
        {
            var oldColor = System.Console.ForegroundColor;
            if (color == oldColor)
                Console.Write(text);
            else
            {
                Console.ForegroundColor = color.Value;
                Console.Write(text);
                Console.ForegroundColor = oldColor;
            }
        }
        else
            Console.Write(text);
    }

    /// <summary>
    /// Writes out a line with color specified as a string
    /// </summary>
    /// <param name="text">Text to write</param>
    /// <param name="color">A console color. Must match ConsoleColors collection names (case insensitive)</param>
    public static void Write(string text, string color)
    {
        if (string.IsNullOrEmpty(color))
        {
            Write(text);
            return;
        }

        if (!ConsoleColor.TryParse(color, true, out ConsoleColor col))
        {
            Write(text);
        }
        else
        {
            Write(text, col);
        }
    }

    #region Wrappers and Templates


    /// <summary>
    /// Writes a line of header text wrapped in a in a pair of lines of dashes:
    /// -----------
    /// Header Text
    /// -----------
    /// and allows you to specify a color for the header. The dashes are colored
    /// </summary>
    /// <param name="headerText">Header text to display</param>
    /// <param name="wrapperChar">wrapper character (-)</param>
    /// <param name="headerColor">Color for header text (yellow)</param>
    /// <param name="dashColor">Color for dashes (gray)</param>
    public static void WriteWrappedHeader(string headerText,
                                            char wrapperChar = '-',
                                            ConsoleColor headerColor = ConsoleColor.Yellow,
                                            ConsoleColor dashColor = ConsoleColor.DarkGray)
    {
        if (string.IsNullOrEmpty(headerText))
            return;

        string line = new string(wrapperChar, headerText.Length);

        WriteLine(line,dashColor);
        WriteLine(headerText, headerColor);
        WriteLine(line,dashColor);
    }

    private static Lazy<Regex> colorBlockRegEx = new Lazy<Regex>(
        ()=>  new Regex("\\[(?<color>.*?)\\](?<text>[^[]*)\\[/\\k<color>\\]", RegexOptions.IgnoreCase), 
        isThreadSafe: true);

    /// <summary>
    /// Allows a string to be written with embedded color values using:
    /// This is [red]Red[/red] text and this is [cyan]Blue[/blue] text
    /// </summary>
    /// <param name="text">Text to display</param>
    /// <param name="baseTextColor">Base text color</param>
    public static void WriteEmbeddedColorLine(string text, ConsoleColor? baseTextColor = null)
    {
        if (baseTextColor == null)
            baseTextColor = Console.ForegroundColor;

        if (string.IsNullOrEmpty(text))
        {
            WriteLine(string.Empty);
            return;
        }

        int at = text.IndexOf("[");
        int at2 = text.IndexOf("]");
        if (at == -1 || at2 <= at)
        {
            WriteLine(text, baseTextColor);
            return;
        }

        while (true)
        {
            var match = colorBlockRegEx.Value.Match(text);
            if (match.Length < 1)
            {
                Write(text, baseTextColor);
                break;
            }

            // write up to expression
            Write(text.Substring(0, match.Index), baseTextColor);

            // strip out the expression
            string highlightText = match.Groups["text"].Value;
            string colorVal = match.Groups["color"].Value;

            Write(highlightText, colorVal);

            // remainder of string
            text = text.Substring(match.Index + match.Value.Length);
        }

        Console.WriteLine();
    }

    #endregion

    #region Success, Error, Info, Warning Wrappers

    /// <summary>
    /// Write a Success Line - green
    /// </summary>
    /// <param name="text">Text to write out</param>
    public static void WriteSuccess(string text)
    {
        WriteLine(text, ConsoleColor.Green);
    }
    
    /// <summary>
    /// Write a Error Line - Red
    /// </summary>
    /// <param name="text">Text to write out</param>
    public static void WriteError(string text)
    {
        WriteLine(text, ConsoleColor.Red);
    }

    /// <summary>
    /// Write a Warning Line - Yellow
    /// </summary>
    /// <param name="text">Text to Write out</param>
    public static void WriteWarning(string text)
    {
        WriteLine(text, ConsoleColor.DarkYellow);
    }


    /// <summary>
    /// Write a Info Line - dark cyan
    /// </summary>
    /// <param name="text">Text to write out</param>
    public static void WriteInfo(string text)
    {
        WriteLine(text, ConsoleColor.DarkCyan);
    }

    #endregion
}

You can also find a runnable single file Console application in this Gist:

ColorConsole.cs Gist

Summary

Nothing fancy, and nothing you couldn't come up with in a few minutes yourself, but I find these super helpful and end up using this in just about any Console application now to make it easier to print help screens and provide basic status and progress information. Maybe some of you will find these useful as well...

this post created and published with the Markdown Monster Editor
Posted in C#  .NET  

The Voices of Reason


 

Paulo Morgado
July 13, 2020

# re: A .NET Color Console Helper

Console.ForegroundColor has side effects. It's cheaper to check for Nullable<T>.HasValue than setting Console.ForegroundColor when not needed.

I would go with something like:

public static void WriteLine(string text, ConsoleColor? color = null)
{
    if (!color.HasValue)
    {
        Console.WriteLine(text);
    }
    else
    {
        var oldColor = System.Console.ForegroundColor;
        var newColor = color.GetValueOrDefault();

        if (oldColor == newColor)
        {
            Console.WriteLine(text);
        }
        else
        {
            Console.ForegroundColor = newColor;

            Console.WriteLine(text);

            Console.ForegroundColor = oldColor;
        }
    }
}

Rick Strahl
July 13, 2020

# re: A .NET Color Console Helper

Thanks @Paulo. What kind of side effects are we talking about? Changing colors on multiple threads perhaps?

Makes sense not to set the value if it's not needed. Updated the sample for both WriteLine() and Write(). Good call!


Stuart Turner
July 13, 2020

# re: A .NET Color Console Helper

If you use \[(?<color>.*?)\](?<text>[^[]*)\[/\k<color>\] as your regex, then you can use match.Groups["color"].Value for the color value and match.Groups["text"] for the inner text. No need to do the extract.


Rick Strahl
July 13, 2020

# re: A .NET Color Console Helper

Thanks @Stuart - that's great. I guess I need to get up on my Regex game - I didn't even remember you can do that. Works great!


James Curran
July 15, 2020

# re: A .NET Color Console Helper

Hmmmm... I had this page opened in a tab since Monday before I got around to writing my regex example -- and Stuart sneaked in with his.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2020