PowerLine.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Text.RegularExpressions;
 
namespace PowerLine
{
    public static class AnsiHelper
    {
        public static string GetCode(this ConsoleColor? color, bool forBackground = false)
        {
            string colorCode = color == null ? "Default" : color.ToString();
 
            return forBackground ? Background[colorCode] : Foreground[colorCode];
 
        }
        public static Dictionary<string, string> Foreground = new Dictionary<string, string>
            {
                {"Clear", "\u001B[39m"},
                {"Black", "\u001B[30m"}, { "DarkGray", "\u001B[90m"},
                {"DarkRed", "\u001B[31m"}, { "Red", "\u001B[91m"},
                {"DarkGreen", "\u001B[32m"}, { "Green", "\u001B[92m"},
                {"DarkYellow", "\u001B[33m"}, { "Yellow", "\u001B[93m"},
                {"DarkBlue", "\u001B[34m"}, { "Blue", "\u001B[94m"},
                {"DarkMagenta", "\u001B[35m"}, { "Magenta", "\u001B[95m"},
                {"DarkCyan", "\u001B[36m"}, { "Cyan", "\u001B[96m"},
                {"Gray", "\u001B[37m"}, { "White", "\u001B[97m"}
            };
 
        public static Dictionary<string, string> Background = new Dictionary<string, string>
            {
                {"Clear", "\u001B[49m"},
                {"Black", "\u001B[40m"}, {"DarkGray", "\u001B[100m"},
                {"DarkRed", "\u001B[41m"}, {"Red", "\u001B[101m"},
                {"DarkGreen", "\u001B[42m"}, {"Green", "\u001B[102m"},
                {"DarkYellow", "\u001B[43m"}, {"Yellow", "\u001B[103m"},
                {"DarkBlue", "\u001B[44m"}, {"Blue", "\u001B[104m"},
                {"DarkMagenta", "\u001B[45m"}, {"Magenta", "\u001B[105m"},
                {"DarkCyan", "\u001B[46m"}, {"Cyan", "\u001B[106m"},
                {"Gray", "\u001B[47m"}, {"White", "\u001B[107m"},
            };
 
 
        public static string WriteAnsi(ConsoleColor? foreground, ConsoleColor? background, string value, bool clear = false)
        {
            var output = new StringBuilder();
 
            output.Append(background.GetCode(true));
            output.Append(foreground.GetCode());
 
            output.Append(value);
            if (clear)
            {
                output.Append(Background["Clear"]);
                output.Append(Foreground["Clear"]);
            }
            return output.ToString();
        }
 
        public static string GetString(object @object)
        {
            var scriptBlock = @object as ScriptBlock;
            return (string)LanguagePrimitives.ConvertTo(scriptBlock != null ? scriptBlock.Invoke() : @object, typeof(string));
        }
 
        static AnsiHelper()
        {
            Console.ResetColor();
            Foreground.Add("Default", Foreground[Console.ForegroundColor.ToString()]);
            Background.Add("Default", Background[Console.BackgroundColor.ToString()]);
        }
 
        public struct EscapeCodes
        {
            public static readonly string Esc = "\u001B[";
            public static readonly string Clear = "\u001B[0m";
            public static readonly string PromptLocation = "\u001B[s";
            public static readonly string Recall = "\u001B[u";
        };
    }
    public class Block
    {
        private string _text;
        /// <summary>
        /// Gets or sets the object. The Object will be converted to string when it's set, and this property always returns a string.
        /// </summary>
        /// <value>A string</value>
        public object Object
        {
            get
            {
                return _text;
            }
            set
            {
                _text = (string)LanguagePrimitives.ConvertTo(value, typeof(string));
 
                // If there's actually no output, report a negative length to ignore this block
                if (string.IsNullOrEmpty(_text))
                {
                    Length = -1;
                }
                else
                {
                    // The Length is measured without escape sequences (Esc + non-letters + any letter)
                    Length = _escapeCode.Replace(_text, "").Length;
                }
            }
        }
        private Regex _escapeCode = new Regex("\u001B\\P{L}+\\p{L}", RegexOptions.Compiled);
 
        /// <summary>
        /// Gets or Sets the background color for the block
        /// </summary>
        public ConsoleColor? BackgroundColor { get; set; }
 
        /// <summary>
        /// Gets or Sets the foreground color for the block
        /// </summary>
        public ConsoleColor? ForegroundColor { get; set; }
 
        /// <summary>
        /// Gets the length of the text representation (without ANSI escape sequences).
        /// </summary>
        public int Length { get; private set; }
 
        /// <summary>
        /// This constructor is here so we can allow partial matches to the property names.
        /// </summary>
        /// <param name="values"></param>
        public Block(IDictionary values) : this()
        {
            foreach (string key in values.Keys)
            {
                var pattern = "^" + Regex.Escape(key);
                if ("bg".Equals(key, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch("BackgroundColor", pattern, RegexOptions.IgnoreCase))
                {
                    BackgroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), values[key].ToString(), true);
                }
                else if ("fg".Equals(key, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch("ForegroundColor", pattern, RegexOptions.IgnoreCase))
                {
                    ForegroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), values[key].ToString(), true);
                }
                else if (Regex.IsMatch("text", pattern, RegexOptions.IgnoreCase) || Regex.IsMatch("Content", pattern, RegexOptions.IgnoreCase) || Regex.IsMatch("Object", pattern, RegexOptions.IgnoreCase))
                {
                    Object = values[key];
                }
                //else if (Regex.IsMatch("Clear", pattern, RegexOptions.IgnoreCase))
                //{
                // Clear = (bool)values[key];
                //}
                else
                {
                    throw new ArgumentException("Unknown key '" + key + "' in hashtable. Allowed values are BackgroundColor, ForegroundColor, and Object (also called Content or Text)");
                }
            }
        }
        // Make sure we can output plain text
        public Block(string text) : this()
        {
            Object = text;
        }
 
        // Make sure we support the default ctor
        public Block() { Length = -1; }
 
        public override string ToString()
        {
            // If there's nothing but escape codes, don't bother outputting new colors
            if (Length == 0)
            {
                return (string)Object;
            }
            return AnsiHelper.WriteAnsi(ForegroundColor, BackgroundColor, (string)Object);
        }
 
        // override object.Equals
        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
            {
                // Console.WriteLine(GetType().FullName + " is not " + obj.GetType().FullName);
                return false;
            }
 
            var other = obj as Block;
 
            return other != null && (Object == other.Object && ForegroundColor == other.ForegroundColor && BackgroundColor == other.BackgroundColor);
        }
 
        // override object.GetHashCode
        public override int GetHashCode()
        {
            return Object.GetHashCode();
        }
    }
 
    /// <summary>
    /// The block factory is a Block which supports scriptblocks that output blocks ...
    /// </summary>
    public class BlockFactory
    {
        /// <summary>
        /// Gets or Sets the background color for the block
        /// </summary>
        public ConsoleColor? DefaultBackgroundColor { get; set; }
 
        /// <summary>
        /// Gets or Sets the foreground color for the block
        /// </summary>
        public ConsoleColor? DefaultForegroundColor { get; set; }
 
        /// <summary>
        /// Gets or Sets the object to be rendered.
        /// Can be any object, but with particular support for nested lists of objects, blocks, or ScriptBlocks which output them.
        /// </summary>
        public object Object { get; set; }
 
        /// <summary>
        /// This constructor is here so we can allow partial matches to the property names.
        /// </summary>
        /// <param name="values"></param>
        public BlockFactory(IDictionary values)
        {
            foreach (string key in values.Keys)
            {
                var pattern = "^" + Regex.Escape(key);
                if ("bg".Equals(key, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch("BackgroundColor", pattern, RegexOptions.IgnoreCase) || Regex.IsMatch("DefaultBackgroundColor", pattern, RegexOptions.IgnoreCase))
                {
                    DefaultBackgroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), values[key].ToString(), true);
                }
                else if ("fg".Equals(key, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch("ForegroundColor", pattern, RegexOptions.IgnoreCase) || Regex.IsMatch("DefaultForegroundColor", pattern, RegexOptions.IgnoreCase))
                {
                    DefaultForegroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), values[key].ToString(), true);
                }
                else if (Regex.IsMatch("text", pattern, RegexOptions.IgnoreCase) || Regex.IsMatch("Content", pattern, RegexOptions.IgnoreCase) || Regex.IsMatch("Object", pattern, RegexOptions.IgnoreCase))
                {
                    Object = values[key];
                }
                //else if (Regex.IsMatch("Clear", pattern, RegexOptions.IgnoreCase))
                //{
                // Clear = (bool)values[key];
                //}
                else
                {
                    throw new ArgumentException("Unknown key '" + key + "' in " + values.GetType().Name + ". Allowed values are BackgroundColor (or bg), ForegroundColor (or fg), and Object (also called Content or Text)");
                }
            }
        }
 
        public BlockFactory(Block block)
        {
            Object = block;
        }
 
        public BlockFactory(params Block[] blocks)
        {
            Object = blocks;
        }
 
        public Block[] GetBlocks()
        {
            // There are four allowed values:
            // 1. A scriptblock, which outputs one of the other possibilities
            // 2. One or more blocks
            // 3. Things we'll convert to text
            var cache = Object;
            Block[] blocks;
 
            // if it's a scriptblock, get the output
            if (cache is ScriptBlock)
            {
                cache = ((ScriptBlock)cache).Invoke();
            }
 
            // Try to convert it to blocks
            try
            {
                blocks = LanguagePrimitives.ConvertTo<Block[]>(cache);
                foreach (var block in blocks)
                {
                    block.BackgroundColor = block.BackgroundColor ?? DefaultBackgroundColor;
                    block.ForegroundColor = block.ForegroundColor ?? DefaultForegroundColor;
                }
            }
            catch
            {
                // If all else fails, make new ones using our default colors
                blocks = LanguagePrimitives.ConvertTo<string[]>(cache)
                                            .Select(o => new Block
                                            {
                                                Object = o,
                                                BackgroundColor = DefaultBackgroundColor,
                                                ForegroundColor = DefaultForegroundColor
                                            }).ToArray();
            }
            return blocks;
        }
    }
 
    public class Column
    {
        /// <summary>
        /// Gets the blocks
        /// </summary>
        public List<BlockFactory> Blocks { get; private set; }
 
        public Column()
        {
            Blocks = new List<BlockFactory>();
            Length = -1;
        }
 
        public Column(params BlockFactory[] blocks) : this()
        {
            Blocks.AddRange(blocks);
        }
 
        public Column(params Block[] blocks) : this()
        {
            // Convert BlockBase to BlockFactory
            Blocks.AddRange(blocks.Select(b => new BlockFactory(b)));
        }
 
        public Column(params object[] blocks) : this()
        {
            foreach (object block in blocks)
            {
                Blocks.AddRange(LanguagePrimitives.ConvertTo<BlockFactory[]>(block));
            }
        }
 
        public ConsoleColor? StartBackgroundColor { get; private set; }
 
        public ConsoleColor? EndBackgroundColor { get; private set; }
 
        public int Length { get; private set; }
 
        private Block[] ValidBlocks { get; set; }
 
        public Block[] PreCalculateValues()
        {
            // Calculate all the text and remove empty blocks
            ValidBlocks = Blocks.SelectMany(factory => factory.GetBlocks()).Where(e => e.Length >= 0).ToArray();
            Length = -1;
            if (ValidBlocks.Any())
            {
                Block block;
                StartBackgroundColor = (block = ValidBlocks.First(b => b.BackgroundColor != null)) == null ? null : block.BackgroundColor;
                EndBackgroundColor = (block = ValidBlocks.Last(b => b.BackgroundColor != null)) == null ? null : block.BackgroundColor;
                Length = ValidBlocks.Sum(b => b.Length) + (ValidBlocks.Length - 1);
            }
            return ValidBlocks;
        }
 
        public string ToString(string separator, string colorSeparator, bool rightJustified = false)
        {
            // Initialize variables ...
            var output = new StringBuilder();
            PreCalculateValues();
 
            for (int l = 0; l < ValidBlocks.Length; l++)
            {
                var block = ValidBlocks[l];
                output.Append(block);
 
                // Write a separator between blocks, unless the next one has no (non-escape) text
                if (l < ValidBlocks.Length - 1 && ValidBlocks[l + 1].Length > 0)
                {
                    // if the colors are the same, use the separator
                    if (block.BackgroundColor == ValidBlocks[l + 1].BackgroundColor)
                    {
                        output.Append(separator);
                    }
                    // if they're different, use the colorSeparator
                    else
                    {
                        if (rightJustified)
                        {
                            output.Append(AnsiHelper.WriteAnsi(ValidBlocks[l + 1].BackgroundColor, block.BackgroundColor, colorSeparator));
                        }
                        else
                        {
                            output.Append(AnsiHelper.WriteAnsi(block.BackgroundColor, ValidBlocks[l + 1].BackgroundColor, colorSeparator));
                        }
                    }
                }
            }
            // clear colors at the end of each column
            output.Append(AnsiHelper.Foreground["Default"]);
            output.Append(AnsiHelper.Background["Default"]);
            return output.ToString();
        }
 
        public override string ToString()
        {
            return ToString(Prompt.Separator, Prompt.ColorSeparator);
        }
    }
 
 
    public class Line
    {
        public List<Column> Columns { get; private set; }
 
        public Line()
        {
            Columns = new List<Column>();
        }
 
        public Line(params Column[] blocks) : this()
        {
            Columns.AddRange(blocks);
        }
 
        public Line(object[] columns) : this()
        {
            Column[] cols;
            if (LanguagePrimitives.TryConvertTo(columns, out cols))
            {
                Columns.AddRange(cols);
                return;
            }
 
            // Console.WriteLine("Fallback to single column");
            Column column;
            if (LanguagePrimitives.TryConvertTo(columns, out column))
            {
                Columns.Add(column);
                return;
            }
 
            // Console.WriteLine("Fallback to casting one at a time");
            foreach (object col in columns)
            {
                if (col == null)
                {
                    Columns.Add(new Column());
                    continue;
                }
 
                if (LanguagePrimitives.TryConvertTo(columns, out column))
                {
                    Columns.Add(column);
                    continue;
                }
 
                // Console.WriteLine("Fallback to block factories");
                // This should let us skip explicitly having columns
                BlockFactory[] factories;
                if (LanguagePrimitives.TryConvertTo(columns, out factories))
                {
                    Columns.Add(new Column(factories));
                    continue;
                }
 
                // Console.WriteLine("Fallback to a single block factory");
                BlockFactory factory;
                if (LanguagePrimitives.TryConvertTo(columns, out factory))
                {
                    Columns.Add(new Column(factory));
                    continue;
                }
            }
        }
 
        public List<Column> PreCalculateValues()
        {
            // Calculate all the text and remove empty blocks
            foreach (var column in Columns)
            {
                if (column != null)
                {
                    column.PreCalculateValues();
                }
            }
 
            return Columns;
        }
 
        public override string ToString()
        {
            return ToString(Console.BufferWidth);
        }
 
        public string ToString(int width)
        {
            var columns = PreCalculateValues();
 
            var output = new StringBuilder();
            // Output each block with appropriate separators and caps
            for (int l = 0; l < columns.Count;)
            {
                var column = columns[l];
                // Use null columns as spacers
                if (column != null && column.Length > 0)
                {
                    string text = column.ToString(Prompt.Separator, Prompt.ColorSeparator);
                    output.Append(text);
                    output.Append(AnsiHelper.WriteAnsi(column.EndBackgroundColor, null, Prompt.ColorSeparator));
                }
 
                // Force the prompt location to the end of the first column
                output.Append(AnsiHelper.EscapeCodes.PromptLocation);
 
                // CURRENTLY we only support two columns, so ...
                // if there are more columns, the next one is right-aligned
                if (columns.Count > ++l)
                {
                    column = columns[l];
                    // Use null columns as spacers
                    if (column != null && column.Length > 0)
                    {
                        // Move to the start location for the next column
                        output.Append(AnsiHelper.EscapeCodes.Esc + (width - column.Length) + "G");
 
                        output.Append(AnsiHelper.WriteAnsi(column.StartBackgroundColor, null, Prompt.ReverseColorSeparator));
                        output.Append(column.ToString(Prompt.ReverseSeparator, Prompt.ReverseColorSeparator, true));
                    }
                }
 
                if (columns.Count > ++l)
                {
                    // Because we only support two columns, if there are still more columns, they must go on the next line
                    output.Append("\n");
                }
            }
 
            return output.ToString();
        }
    }
 
    public class Prompt
    {
        public static string ColorSeparator = "\u258C"; // ▌
        public static string ReverseColorSeparator = "\u2590"; // ▐
        public static string Separator = "\u25BA"; // ►
        public static string ReverseSeparator = "\u25C4"; // ◄
        public static string Branch = "\ue0a0"; // Branch symbol
        public static string Lock = "\ue0a2"; // Padlock
        public static string Gear = "\u26ef"; // The settings icon, I use it for debug
        public static string Power = "\u26a1"; // The Power lightning-bolt icon
 
        public List<Line> Lines { get; private set; }
        public ScriptBlock Title { get; set; }
        public bool SetCurrentDirectory { get; set; }
        public bool UseAnsiEscapes { get; set; }
        public int PrefixLines { get; set; }
 
        public Prompt()
        {
            Lines = new List<Line>();
        }
 
        public Prompt(int prefixLines, params Line[] lines) : this()
        {
            PrefixLines = prefixLines;
            Lines.AddRange(lines);
        }
 
        public Prompt(params Line[] lines) : this()
        {
            Lines.AddRange(lines);
        }
 
        public Prompt(object[] lines) : this()
        {
            if (lines.First() is int)
            {
                PrefixLines = (int)lines.First();
                lines = lines.Skip(1).ToArray();
            }
 
            Line[] lns;
            if (LanguagePrimitives.TryConvertTo(lines, out lns))
            {
                Lines.AddRange(lns);
                return;
            }
 
            Line ln;
            if (LanguagePrimitives.TryConvertTo(lines, out ln))
            {
                Lines.Add(ln);
                return;
            }
 
            foreach (object line in lines)
            {
                Lines.Add(LanguagePrimitives.ConvertTo<Line>(line));
                continue;
            }
        }
 
        public override string ToString()
        {
            return ToString(Console.BufferWidth);
        }
 
        public string ToString(int width)
        {
            var output = new StringBuilder();
 
            // Move up to previous line(s)
            if (PrefixLines != 0)
            {
                output.Append(AnsiHelper.EscapeCodes.Esc + Math.Abs(PrefixLines) + "A");
            }
 
            output.Append(string.Join("\n", Lines.Select(l => l.ToString(width))));
            output.Append(AnsiHelper.Foreground["Default"]);
            output.Append(AnsiHelper.Background["Default"]);
            output.Append(AnsiHelper.EscapeCodes.Recall);
 
            return output.ToString();
        }
 
    }
}