diff --git a/src/ConsoleBuffer/Buffer.cs b/src/ConsoleBuffer/Buffer.cs index 9253343..a371a8e 100644 --- a/src/ConsoleBuffer/Buffer.cs +++ b/src/ConsoleBuffer/Buffer.cs @@ -18,6 +18,7 @@ public sealed class Buffer : INotifyPropertyChanged private long receivedCharacters; private long wrapCharacter; private int currentChar; + private Character characterTemplate = new Character { Glyph = 0x20 }; /// /// we store X/Y as 0-offset indexes for convenience. escape codes will pass these around as 1-offset (top left is 1,1) @@ -72,17 +73,11 @@ public Buffer(short width, short height) this.CursorVisible = this.CursorBlink = true; this.cursorX = this.cursorY = 0; - var defaultChar = new Character() - { - Options = Character.BasicColorOptions(Commands.SetGraphicsRendition.Colors.White, Commands.SetGraphicsRendition.Colors.Black), - Glyph = 0x20, - }; + this.HandleSGR(new Commands.SetGraphicsRendition(string.Empty)); for (var y = 0; y < this.Height; ++y) { - var line = new Line(null); - line.Set(0, defaultChar); - this.lines.PushBack(line); + this.lines.PushBack(new Line()); } this.topVisibleLine = 0; this.bottomVisibleLine = this.MaxCursorY; @@ -152,30 +147,9 @@ private void PrintAtCursor(int ch) this.wrapCharacter = -1; } - // if we have an explicit value for the current cell hold on to it, otherwise inherit from the previous cell since - // we don't know what other activity has been done with cursor shuffling/SGR/etc. - var cell = this.lines[this.CurrentLine].Get(this.cursorX); - if (cell.ForegroundExplicit || cell.BackgroundExplicit || (this.cursorX == 0 && this.cursorY == 0)) - { - cell = this.ColorCharacter(cell); - cell.Glyph = ch; - this.lines[this.CurrentLine].Set(this.cursorX, cell); - } - else - { - var x = this.cursorX; - var y = this.cursorY; - if (x == 0) - { - --y; - x = this.MaxCursorX; - } - var line = this.lines[this.topVisibleLine + y]; - var inheritChar = line.Get(x); - inheritChar = this.ColorCharacter(inheritChar); - inheritChar.Glyph = ch; - this.lines[this.CurrentLine].Set(this.cursorX, inheritChar); - } + var newChar = this.characterTemplate; + newChar.Glyph = ch; + this.lines[this.CurrentLine].Set(this.cursorX, newChar); if (this.cursorX == this.MaxCursorX) { @@ -405,11 +379,51 @@ private void HandleSetCursorPosition(Commands.SetCursorPosition scp) private void HandleSGR(Commands.SetGraphicsRendition sgr) { - var updatedCell = this.lines[this.CurrentLine].Get(this.cursorX); - if (sgr.ForegroundBright == Commands.SetGraphicsRendition.FlagValue.Set) updatedCell.Options |= Character.ForegroundBrightFlag; - if (sgr.ForegroundBright == Commands.SetGraphicsRendition.FlagValue.Unset) updatedCell.Options &= ~Character.ForegroundBrightFlag; + var newTemplate = this.characterTemplate; + + if (sgr.ForegroundBright == Commands.SetGraphicsRendition.FlagValue.Set) newTemplate.Options |= Character.ForegroundBrightFlag; + if (sgr.ForegroundBright == Commands.SetGraphicsRendition.FlagValue.Unset) newTemplate.Options &= ~Character.ForegroundBrightFlag; + if (sgr.BackgroundBright == Commands.SetGraphicsRendition.FlagValue.Set) newTemplate.Options |= Character.BackgroundBrightFlag; + if (sgr.BackgroundBright == Commands.SetGraphicsRendition.FlagValue.Unset) newTemplate.Options &= ~Character.BackgroundBrightFlag; + if (sgr.Underline == Commands.SetGraphicsRendition.FlagValue.Set) newTemplate.Options |= Character.UnderlineFlag; + if (sgr.Underline == Commands.SetGraphicsRendition.FlagValue.Unset) newTemplate.Options &= ~Character.UnderlineFlag; + if (sgr.Inverse == Commands.SetGraphicsRendition.FlagValue.Set) newTemplate.Options |= Character.InverseFlag; + if (sgr.Inverse == Commands.SetGraphicsRendition.FlagValue.Unset) newTemplate.Options &= ~Character.InverseFlag; + + if (sgr.HaveBasicForeground) + { + newTemplate.Options &= ~Character.ForegroundExtendedFlag; + newTemplate.Options |= Character.ForegroundBasicColorFlag; + newTemplate.Options &= ~Character.ForegroundColorMask; + newTemplate.Options |= Character.GetColorFlags(sgr.BasicForegroundColor, false); + } + else if (sgr.HaveForeground) + { + newTemplate.Options &= ~Character.ForegroundBasicColorFlag; + newTemplate.Options &= ~Character.ForegroundColorMask; + newTemplate.Options |= Character.ForegroundExtendedFlag; + newTemplate.Foreground = sgr.ForegroundColor; + } - this.lines[this.CurrentLine].Set(this.cursorX, updatedCell); + if (sgr.HaveBasicBackground) + { + newTemplate.Options &= ~Character.BackgroundExtendedFlag; + newTemplate.Options |= Character.BackgroundBasicColorFlag; + newTemplate.Options &= ~Character.BackgroundColorMask; + newTemplate.Options |= Character.GetColorFlags(sgr.BasicBackgroundColor, true); + } + else if (sgr.HaveBackground) + { + newTemplate.Options &= ~Character.BackgroundBasicColorFlag; + newTemplate.Options &= ~Character.BackgroundColorMask; + newTemplate.Options |= Character.BackgroundExtendedFlag; + newTemplate.Background = sgr.BackgroundColor; + } + + if (newTemplate.HasBasicForegroundColor) newTemplate.Foreground = this.GetColorInfoFromBasicColor(newTemplate.BasicForegroundColor, newTemplate.ForegroundBright); + if (newTemplate.HasBasicBackgroundColor) newTemplate.Background = this.GetColorInfoFromBasicColor(newTemplate.BasicBackgroundColor, newTemplate.BackgroundBright); + + this.characterTemplate = newTemplate; } private void HandleSetMode(Commands.SetMode sm) @@ -436,7 +450,7 @@ private void ScrollDown(int lines = 1) --lines; if (this.bottomVisibleLine == this.lines.Capacity - 1) { - this.lines.PushBack(new Line(this.lines[this.bottomVisibleLine])); // will force an old line from the buffer; + this.lines.PushBack(new Line()); // will force an old line from the buffer; } else { @@ -444,39 +458,12 @@ private void ScrollDown(int lines = 1) ++this.bottomVisibleLine; if (this.lines.Size <= this.bottomVisibleLine) { - this.lines.PushBack(new Line(this.lines[this.bottomVisibleLine - 1])); + this.lines.PushBack(new Line()); } } } } - /// - /// Fills in the R/G/B values for a character based on the associated flags. - /// - /// Character to color. - /// The colored version of the character. - private Character ColorCharacter(Character ch) - { - var ret = new Character(ch); - if (ret.Inverse) - { - // NB: leaves the inverse bit on background, shouldn't matter. - ret.Foreground = ch.Background; - ret.Background = ch.Foreground; - } - - if (ch.HasBasicForegroundColor) - { - ret.Foreground = this.GetColorInfoFromBasicColor(ret.BasicForegroundColor, ret.ForegroundBright); - } - if (ch.HasBasicBackgroundColor) - { - ret.Background = this.GetColorInfoFromBasicColor(ret.BasicBackgroundColor, ret.BackgroundBright); - } - - return ret; - } - private Character.ColorInfo GetColorInfoFromBasicColor(Commands.SetGraphicsRendition.Colors basicColor, bool isBright) { var paletteOffset = isBright ? 8 : 0; diff --git a/src/ConsoleBuffer/Character.cs b/src/ConsoleBuffer/Character.cs index f4b0840..5b6bf3c 100644 --- a/src/ConsoleBuffer/Character.cs +++ b/src/ConsoleBuffer/Character.cs @@ -1,6 +1,7 @@ namespace ConsoleBuffer { using System; + using System.Text; // XXX: Gonna end up with a lot of these and they're really freakin' big. // could consider a morphable type with different sizes to avoid the (currently) 12 bytes-per-character issue. @@ -25,9 +26,9 @@ public override string ToString() // traditional colors occupy 3 bits, we keep two sets (foreground + background). // the actual colors are declared in the SGR command. - private const short ForegroundColorMask = 0x0007; + internal const short ForegroundColorMask = 0x0007; private const short BackgroundBitShift = 3; - private const short BackgroundColorMask = ForegroundColorMask << BackgroundBitShift; + internal const short BackgroundColorMask = ForegroundColorMask << BackgroundBitShift; // flags internal const short ForegroundBasicColorFlag = 0x0001 << 6; internal const short BackgroundBasicColorFlag = 0x0002 << 6; @@ -35,83 +36,55 @@ public override string ToString() internal const short BackgroundBrightFlag = 0x0008 << 6; internal const short UnderlineFlag = 0x0010 << 6; internal const short InverseFlag = 0x0020 << 6; - internal const short ForegroundExplicitFlag = 0x0040 << 6; - internal const short BackgroundExplicitFlag = 0x0080 << 6; - internal const short ExplicitFlags = (ForegroundExplicitFlag | BackgroundBrightFlag); - internal const short ForegroundExtendedFlag = 0x0100 << 6; - internal const short BackgroundExtendedFlag = unchecked((short)(0x0200 << 6)); + internal const short ForegroundExtendedFlag = 0x0040 << 6; + internal const short BackgroundExtendedFlag = 0x0080 << 6; internal short Options; - internal static short BasicColorOptions(Commands.SetGraphicsRendition.Colors foreground = Commands.SetGraphicsRendition.Colors.None, - Commands.SetGraphicsRendition.Colors background = Commands.SetGraphicsRendition.Colors.None) + internal static short GetColorFlags(Commands.SetGraphicsRendition.Colors color, bool background) { - short options = 0; - if (foreground != Commands.SetGraphicsRendition.Colors.None) + var options = (short)color; +#if DEBUG + if (options < (short)Commands.SetGraphicsRendition.Colors.Black || options > (short)Commands.SetGraphicsRendition.Colors.White) { - options |= (short)((short)foreground | ForegroundExplicitFlag | ForegroundBasicColorFlag); - } - if (background != Commands.SetGraphicsRendition.Colors.None) - { - options |= (short)(((short)background << BackgroundBitShift) | BackgroundExplicitFlag | BackgroundBasicColorFlag); + throw new ArgumentOutOfRangeException(nameof(color)); } +#endif - return options; + return background ? (short)(options << BackgroundBitShift) : options; } internal Commands.SetGraphicsRendition.Colors BasicForegroundColor => (Commands.SetGraphicsRendition.Colors)(this.Options & ForegroundColorMask); internal bool HasBasicForegroundColor => (this.Options & ForegroundBasicColorFlag) != 0; - internal Commands.SetGraphicsRendition.Colors BasicBackgroundColor => (Commands.SetGraphicsRendition.Colors)(this.Options & BackgroundColorMask); + internal Commands.SetGraphicsRendition.Colors BasicBackgroundColor => (Commands.SetGraphicsRendition.Colors)((this.Options & BackgroundColorMask) >> BackgroundBitShift); internal bool HasBasicBackgroundColor => (this.Options & BackgroundBasicColorFlag) != 0; internal bool ForegroundBright => (this.Options & ForegroundBrightFlag) != 0; internal bool BackgroundBright => (this.Options & BackgroundBrightFlag) != 0; - internal bool Underline => (this.Options & UnderlineFlag) != 0; + // this is the only property we cannot handle rendering of internally by setting appropriate RGB color values. + public bool Underline => (this.Options & UnderlineFlag) != 0; internal bool Inverse => (this.Options & InverseFlag) != 0; - internal bool ForegroundExplicit => (this.Options & ForegroundExplicitFlag) != 0; - internal bool BackgroundExplicit => (this.Options & BackgroundExplicitFlag) != 0; internal bool ForegroundExtended => (this.Options & ForegroundExtendedFlag) != 0; internal bool BackgroundExtended => (this.Options & BackgroundExtendedFlag) != 0; - internal short InheritedOptions => (short)(this.Options & ~(ForegroundExplicitFlag | BackgroundExplicitFlag)); - /// /// The unicode glyph for this character. /// public int Glyph { get; set; } // XXX: a single int isn't sufficient to represent emoji with ZWJ. fix later. - public Character(Character parent) - { - this.Foreground = parent.Foreground; - this.Background = parent.Background; - this.Glyph = parent.Glyph; - this.Options = parent.Options; - this.Options &= ~ExplicitFlags; - } - - public Character(Character parent, Commands.SetGraphicsRendition sgr) - : this(parent) + public override string ToString() { - switch (sgr.ForegroundBright) - { - case Commands.SetGraphicsRendition.FlagValue.Set: - this.Options |= (ForegroundBrightFlag | ForegroundExplicitFlag); - break; - case Commands.SetGraphicsRendition.FlagValue.Unset: - this.Options &= ~ForegroundBrightFlag; - this.Options |= ForegroundExplicitFlag; - break; - } - - switch (sgr.BackgroundBright) - { - case Commands.SetGraphicsRendition.FlagValue.Set: - this.Options |= (BackgroundBrightFlag | BackgroundExplicitFlag); - break; - case Commands.SetGraphicsRendition.FlagValue.Unset: - this.Options &= ~BackgroundBrightFlag; - this.Options |= BackgroundExplicitFlag; - break; - } + var sb = new StringBuilder(); + sb.Append($"'{(char)this.Glyph}' (opt:"); + if (this.ForegroundBright) sb.Append(" bright"); + if (this.Underline) sb.Append(" ul"); + if (this.Inverse) sb.Append(" inv"); + if (this.BackgroundBright) sb.Append(" bgBright"); + if (this.HasBasicForegroundColor) sb.Append($" fg:{this.BasicForegroundColor}"); + if (this.HasBasicBackgroundColor) sb.Append($" bg:{this.BasicBackgroundColor}"); + if (this.ForegroundExtended) sb.Append($" efg:#{this.Foreground.R:x2}{this.Foreground.G:x2}{this.Foreground.B:x2}"); + if (this.BackgroundExtended) sb.Append($" ebg:#{this.Background.R:x2}{this.Background.G:x2}{this.Background.B:x2}"); + sb.Append(')'); + return sb.ToString(); } } } diff --git a/src/ConsoleBuffer/Commands/SetGraphicsRendition.cs b/src/ConsoleBuffer/Commands/SetGraphicsRendition.cs index 88ff773..ce3868d 100644 --- a/src/ConsoleBuffer/Commands/SetGraphicsRendition.cs +++ b/src/ConsoleBuffer/Commands/SetGraphicsRendition.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace ConsoleBuffer.Commands { public sealed class SetGraphicsRendition : ControlSequence @@ -39,6 +41,11 @@ public enum Colors : short public SetGraphicsRendition(string bufferData) : base(bufferData) { +#if DEBUG + // XXX: remove later. + Trace.WriteLine($"SGR: ^[[{bufferData}m"); +#endif + this.ForegroundBright = FlagValue.None; this.BackgroundBright = FlagValue.None; this.Underline = FlagValue.None; @@ -53,7 +60,8 @@ public SetGraphicsRendition(string bufferData) : base(bufferData) var p = 0; while (p < this.Parameters.Count) { - switch (this.ParameterToNumber(p, defaultValue: -1)) + var pValue = this.ParameterToNumber(p, defaultValue: -1); + switch (pValue) { case 0: this.SetDefault(); @@ -65,6 +73,64 @@ public SetGraphicsRendition(string bufferData) : base(bufferData) case 22: this.ForegroundBright = FlagValue.Unset; break; + case 4: + this.Underline = FlagValue.Set; + break; + case 24: + this.Underline = FlagValue.Unset; + break; + case 7: + this.Inverse = FlagValue.Set; + break; + case 27: + this.Inverse = FlagValue.Unset; + break; + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: + this.HaveBasicForeground = true; + this.BasicForegroundColor = (Colors)(pValue - 30); + break; + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: + this.HaveBasicBackground = true; + this.BasicBackgroundColor = (Colors)(pValue - 40); + break; + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: + this.HaveBasicForeground = true; + this.ForegroundBright = FlagValue.Set; // XXX: idk if this is right + this.BasicForegroundColor = (Colors)(pValue - 90); + break; + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: + this.HaveBasicBackground = true; + this.BackgroundBright = FlagValue.Set; // same as above. + this.BasicBackgroundColor = (Colors)(pValue - 100); + break; } ++p; @@ -82,5 +148,10 @@ private void SetDefault() this.BasicForegroundColor = Colors.White; this.BasicBackgroundColor = Colors.Black; } + + public override string ToString() + { + return $"^[[{string.Join(";", this.Parameters)}m"; + } } } diff --git a/src/ConsoleBuffer/Line.cs b/src/ConsoleBuffer/Line.cs index c0d2546..c6599fc 100644 --- a/src/ConsoleBuffer/Line.cs +++ b/src/ConsoleBuffer/Line.cs @@ -8,23 +8,10 @@ public sealed class Line : IEnumerable { private readonly List chars; - public Line(Line previous) + public Line() { var hintSize = 80; - // our first character should inherit attributes of the last line's character. - var lastCh = new Character(); - if (previous != null) - { - lastCh = previous.chars[previous.chars.Count - 1]; - hintSize = previous.chars.Count; - } - lastCh.Glyph = 0x20; - lastCh.Options = lastCh.InheritedOptions; - - this.chars = new List(hintSize) - { - lastCh - }; + this.chars = new List(hintSize); } /// @@ -50,10 +37,7 @@ public Character Get(int pos) return this.chars[pos]; } - var ch = this.chars[this.chars.Count - 1]; - ch.Glyph = 0x20; - ch.Options = ch.InheritedOptions; - return ch; + return new Character { Glyph = 0x20 }; } /// @@ -80,12 +64,9 @@ public void Clear() private void Extend(int pos) { - var newChar = this.chars[this.chars.Count - 1]; - newChar.Glyph = 0x20; - newChar.Options = newChar.InheritedOptions; while (this.chars.Count <= pos) { - this.chars.Add(newChar); + this.chars.Add(new Character { Glyph = 0x20 }); } } diff --git a/test/ConsoleBufferTests/BufferTests.cs b/test/ConsoleBufferTests/BufferTests.cs index 2dfed15..9f11c18 100644 --- a/test/ConsoleBufferTests/BufferTests.cs +++ b/test/ConsoleBufferTests/BufferTests.cs @@ -69,16 +69,44 @@ public void MaxBufferSize() public void BrightForegroundText() { var buffer = new ConsoleBuffer.Buffer(DefaultColumns, DefaultRows); - buffer.AppendString("\x1b[1mhello\n"); + buffer.AppendString("\x1b[1mbb\x1b[2mn\x1b[1mb\x1b[2mnnnn\n"); var surface = new RenderTest(); surface.OnChar = (c, x, y) => { - if (c.Glyph != 0x20) - { - Assert.AreEqual(buffer.Palette["white"], c.Foreground); - } + if (c.Glyph == 'b') Assert.AreEqual(buffer.Palette["white"], c.Foreground); + if (c.Glyph == 'n') Assert.AreEqual(buffer.Palette["silver"], c.Foreground); }; buffer.Render(surface); } + + [TestMethod] + public void BasicColorTest() + { + var surface = new RenderTest(); + + var buffer = new ConsoleBuffer.Buffer(DefaultColumns, DefaultRows); + for (var fg = 0; fg < 16; ++fg) + { + for (var bg = 0; bg < 16; ++bg) + { + buffer.AppendString("\x1b[2J\x1b[m"); + buffer.AppendString("\x1b["); + if (fg < 8) buffer.AppendString($"3{fg}"); + else buffer.AppendString($"9{fg - 8}"); + if (bg < 8) buffer.AppendString($";4{bg}m"); + else buffer.AppendString($";10{bg - 8}m"); + buffer.AppendString("c\r\n"); + + surface.OnChar = (c, x, y) => + { + if (c.Glyph != 'c') return; + Assert.AreEqual(buffer.Palette[fg], c.Foreground); + Assert.AreEqual(buffer.Palette[bg], c.Background); + }; + + buffer.Render(surface); + } + } + } } } diff --git a/test/ConsoleBufferTests/SequenceParserTests.cs b/test/ConsoleBufferTests/SequenceParserTests.cs index e471e8b..ce59950 100644 --- a/test/ConsoleBufferTests/SequenceParserTests.cs +++ b/test/ConsoleBufferTests/SequenceParserTests.cs @@ -233,6 +233,28 @@ public void SGRBold(string data, ConsoleBuffer.Commands.SetGraphicsRendition.Fla Assert.AreEqual(expectedValue, cmd.ForegroundBright); } + [TestMethod] + [DataRow("4", ConsoleBuffer.Commands.SetGraphicsRendition.FlagValue.Set)] + [DataRow("24", ConsoleBuffer.Commands.SetGraphicsRendition.FlagValue.Unset)] + public void SGRUnderline(string data, ConsoleBuffer.Commands.SetGraphicsRendition.FlagValue expectedValue) + { + var parser = this.EnsureCommandParses($"\x1b[{data}m"); + var cmd = parser.Command as ConsoleBuffer.Commands.SetGraphicsRendition; + Assert.IsNotNull(cmd); + Assert.AreEqual(expectedValue, cmd.Underline); + } + + [TestMethod] + [DataRow("7", ConsoleBuffer.Commands.SetGraphicsRendition.FlagValue.Set)] + [DataRow("27", ConsoleBuffer.Commands.SetGraphicsRendition.FlagValue.Unset)] + public void SGRInverse(string data, ConsoleBuffer.Commands.SetGraphicsRendition.FlagValue expectedValue) + { + var parser = this.EnsureCommandParses($"\x1b[{data}m"); + var cmd = parser.Command as ConsoleBuffer.Commands.SetGraphicsRendition; + Assert.IsNotNull(cmd); + Assert.AreEqual(expectedValue, cmd.Inverse); + } + private SequenceParser EnsureCommandParses(string command) { var parser = new SequenceParser();