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 ? "Clear" : 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(AnsiHelper.Background["Clear"]); output.Append(AnsiHelper.Foreground["Clear"]); } return output.ToString(); } public static string GetString(object @object) { return (string)LanguagePrimitives.ConvertTo(@object is ScriptBlock ? ((ScriptBlock)@object).Invoke() : @object, typeof(string)); } public struct EscapeCodes { public static readonly string ESC = "\u001B["; public static readonly string Clear = "\u001B[0m"; public static readonly string Store = "\u001B[s"; public static readonly string Recall = "\u001B[u"; }; } public class Block { public static string LeftCap = "\ue0b0"; // right-pointing arrow public static string RightCap = "\ue0b2"; // left-pointing arrow public static string LeftSep = "\ue0b1"; // left open > public static string RightSep = "\ue0b3"; // right open < 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 ConsoleColor? BackgroundColor { get; set; } public ConsoleColor? ForegroundColor { get; set; } public object Object { get; set; } public bool Clear { get; set; } public Block() { } // copy constructor public Block(Block block) { BackgroundColor = block.BackgroundColor; ForegroundColor = block.ForegroundColor; Object = block.Object; } public Block(IDictionary values) { foreach (string key in values.Keys) { var pattern = "^" + Regex.Escape(key); if ("bg".Equals(key, System.StringComparison.InvariantCultureIgnoreCase) || Regex.IsMatch("BackgroundColor", pattern, RegexOptions.IgnoreCase)) { BackgroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), values[key].ToString(), true); } else if ("fg".Equals(key, System.StringComparison.InvariantCultureIgnoreCase) || 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, Content, and Clear"); } } } /// <summary> /// Get the Object rendered to text. /// With special handling for ScriptBlocks and Blocks, and ScriptBlocks which output Blocks... /// </summary> /// <returns></returns> public string GetObjectText() { object value = Object; if (Object is ScriptBlock) { value = ((ScriptBlock)Object).Invoke(); } if (Object is IEnumerable<ScriptBlock>) { value = ((IEnumerable<ScriptBlock>)Object) .SelectMany(block => block.Invoke() .Select(pso => pso.BaseObject)) .Select(o => o is Block ? ((Block)o).GetObjectText() : o); } if (value is Block) { value = ((Block)value).GetObjectText(); } if (value is IEnumerable<Block>) { value = ((IEnumerable<Block>)value).Select(block => block.GetObjectText()); } if (value is IEnumerable<object>) { value = ((IEnumerable<object>)value).Select(o => o is Block ? ((Block)o).GetObjectText() : o); } return (string)LanguagePrimitives.ConvertTo(value, typeof(string)); } public override string ToString() { object value = Object; string text = null; if (Object is ScriptBlock) { value = ((ScriptBlock)Object).Invoke(); } if (value is Block) { return value.ToString(); } if (Object is IEnumerable<ScriptBlock>) { text = AnsiHelper.GetString(((IEnumerable<ScriptBlock>)Object) .SelectMany(block => block.Invoke() .Select(pso => pso.BaseObject)) .Select(o => o is Block ? ((Block)o).ToString() : AnsiHelper.GetString(o))); } if (value is IEnumerable<Block>) { text = AnsiHelper.GetString(((IEnumerable<Block>)value).Select(block => block.ToString())); } if (value is IEnumerable<object>) { text = AnsiHelper.GetString(((IEnumerable<object>)value).Select(o => o is Block ? ((Block)o).ToString() : AnsiHelper.GetString(o))); } return AnsiHelper.WriteAnsi(ForegroundColor, BackgroundColor, text ?? GetObjectText(), Clear); } // 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; // Console.WriteLine("Is " + GetType().FullName + " '" + Content + "' == " + obj.GetType().FullName + " '" + other.Content + "'"); return Object == other.Object && ForegroundColor == other.ForegroundColor && BackgroundColor == other.BackgroundColor; } // override object.GetHashCode public override int GetHashCode() { return Object.GetHashCode() + BackgroundColor.GetHashCode() + ForegroundColor.GetHashCode(); } } public class BlockCache : Block { private string _text; new public string Object { get { return _text ?? GetObjectText(); } set { _text = value; base.Object = value; Length = _text.Length; } } public int Length { get; private set; } public static BlockCache Column = new BlockCache() { Object = "\t" }; public static BlockCache Prompt = new BlockCache() { Object = AnsiHelper.EscapeCodes.Store }; public BlockCache() { } public BlockCache(Block block) { BackgroundColor = block.BackgroundColor; ForegroundColor = block.ForegroundColor; Object = block.GetObjectText(); Length = Object.Length; } public override string ToString() { return AnsiHelper.WriteAnsi(ForegroundColor, BackgroundColor, Object, Clear); } } public static class Cacher { public static BlockCache Cache(this Block block) { return block is BlockCache ? (BlockCache)block : new BlockCache(block); } } public class Line : List<Block> { public Line() { } public Line(params Block[] blocks) : base(blocks) { } public Line(object[] blocks) { AddRange(blocks.Select(b => b is Hashtable ? new Block((Hashtable)b) : (Block)b)); } public override string ToString() { // Initialize variables ... var width = Console.BufferWidth; var leftLength = 0; var rightLength = 0; // Precalculate all the text and remove empty blocks var ValidBlocks = this.Select(e => e.Cache()).Where(e => e.Length > 0).ToArray(); var output = new StringBuilder(); // Output each block with appropriate separators and caps for (int l = 0; l < ValidBlocks.Length; l++) { var block = ValidBlocks[l]; // Console.WriteLine("Is '" + block + "' a Column? " + BlockCache.Column.Equals(block) + " or a Prompt? " + BlockCache.Prompt.Equals(block)); if (BlockCache.Column.Equals(block)) { // the length of the second column rightLength = ValidBlocks.Skip(l + 1).Sum(e => e.Length + 1) - 1; var space = width - rightLength; // Output a cap on the left if there isn't one already if (l > 0 && !BlockCache.Prompt.Equals(ValidBlocks[l - 1])) { // Use the Background of the previous block as the foreground output.Append(AnsiHelper.WriteAnsi(ValidBlocks[l - 1].BackgroundColor, null, Block.LeftCap, true)); } output.Append(AnsiHelper.EscapeCodes.ESC + space + "G"); if (l < ValidBlocks.Length) { // the right cap uses the background of the next block as it's foreground output.Append(AnsiHelper.WriteAnsi(ValidBlocks[l + 1].BackgroundColor, null, Block.RightCap)); } } else if (BlockCache.Prompt.Equals(block)) { output.Append(block.ToString()); } else { if (leftLength == 0 && rightLength == 0) { // On a new line, recalculate the length of the "left-aligned" line leftLength = ValidBlocks.TakeWhile(e => !BlockCache.Column.Equals(e)).Sum(e => e.Length + 1); } output.Append(block.ToString()); // Write a separator between blocks if (l + 1 < ValidBlocks.Length && !BlockCache.Column.Equals(ValidBlocks[l + 1])) { // if the next block is the sambe background color, use a > if (block.BackgroundColor == ValidBlocks[l + 1].BackgroundColor) { output.Append(rightLength > 0 ? Block.RightSep : Block.LeftSep); } else { if (rightLength > 0) { output.Append(AnsiHelper.WriteAnsi(ValidBlocks[l + 1].BackgroundColor, block.BackgroundColor, Block.RightCap)); } else { output.Append(AnsiHelper.WriteAnsi(block.BackgroundColor, ValidBlocks[l + 1].BackgroundColor, Block.LeftCap)); } } } } } // Output a cap on the left if we didn't already if (rightLength == 0 && leftLength > 0) { output.Append(AnsiHelper.WriteAnsi(ValidBlocks.Last().BackgroundColor, null, Block.LeftCap)); } return output.ToString(); } } public class Prompt : List<Line> { public bool SetTitle { get; set; } public bool SetCurrentDirectory { get; set; } public int PrefixLines { get; set; } public Prompt() { } public Prompt(int prefixLines, params Line[] lines) : base(lines) { PrefixLines = prefixLines; } public Prompt(params Line[] lines) : base(lines) { } public Prompt(object[] lines) { if (lines.First() is int) { PrefixLines = (int)lines.First(); lines = lines.Skip(1).ToArray(); } AddRange(lines.Select(b => b is Block[] ? new Line((Block[])b) : b is object[] ? new Line((object[])b) : (Line)b)); } public override string ToString() { 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", this)); if (this.Any(line => line.Any(block => BlockCache.Prompt.Equals(block)))) { output.Append(AnsiHelper.EscapeCodes.Recall); } output.Append(AnsiHelper.Foreground["Clear"]); // Default output.Append(AnsiHelper.Background["Clear"]); // Default return output.ToString(); } } } |