Skip to content

Commit

Permalink
plumb through initial color settings without parsing colors
Browse files Browse the repository at this point in the history
  • Loading branch information
doubleyewdee committed Nov 25, 2018
1 parent 83d602c commit 722cf4a
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 135 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ but for a light overview:
really. The UWP platform has some issues with UWP apps being able to invoke external processes.
This needs some work from the console/Windows/UWP teams (I guess?) to address this for now.

## Color Management
In deciding how to manage the delta between rendering a "presentation-free" buffer in the library
and leacving presentation to the application it made sense to support the "xterm palette." A user
can provide a palette to the buffer, which will be used when characters are rendered to set the
appropriate RGB values for either "classic" (16 color) terminals or 256 color palette xterm values.
This means that the RGB values are always set by the library for the rendering library to use as
desired. Given that more than one escape sequence can reference color palettes in a plethora of
ways this felt like the most appropriate choice.

As a consequence of this decision, however, runtime palette changes won't be reflected in text
that has already been rendered. This can be considered a feature or a bug, depending on your mood.

## Dev notes

Some cool sites I've found while putting this together.
Expand Down
95 changes: 92 additions & 3 deletions src/ConsoleBuffer/Buffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public sealed class Buffer : INotifyPropertyChanged
/// </summary>
public bool CursorBlink { get; private set; }

/// <summary>
/// The xterm palette to use when mapping color names/offsets to RGB values in characters.
/// </summary>
public XtermPalette Palette { get; set; }

private int topVisibleLine;
private int bottomVisibleLine;

Expand Down Expand Up @@ -66,9 +71,17 @@ public Buffer(short width, short height)
this.CursorVisible = this.CursorBlink = true;
this.cursorX = this.cursorY = 0;

var defaultChar = new Character()
{
Options = Character.BasicColorOptions(Character.White, Character.Black),
Glyph = 0x20,
};

for (var y = 0; y < this.Height; ++y)
{
this.lines.PushBack(new Line(null));
var line = new Line(null);
line.Set(0, defaultChar);
this.lines.PushBack(line);
}
this.topVisibleLine = 0;
this.bottomVisibleLine = this.MaxCursorY;
Expand Down Expand Up @@ -115,7 +128,6 @@ public void Append(byte[] bytes, int length)
/// <param name="ch"></param>
private void PrintAtCursor(int ch)
{

if (this.cursorX == this.MaxCursorX && this.wrapCharacter == this.receivedCharacters - 1)
{
if (this.cursorY == this.MaxCursorY)
Expand All @@ -130,7 +142,31 @@ private void PrintAtCursor(int ch)
this.wrapCharacter = -1;
}

this.lines[this.CurrentLine].Set(this.cursorX, new Character { Glyph = ch });
// 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);
}

if (this.cursorX == this.MaxCursorX)
{
// if we hit the end of line and our next character is also printable or a backspace we will do an implicit line-wrap.
Expand Down Expand Up @@ -400,6 +436,59 @@ private void ScrollDown(int lines = 1)
}
}

/// <summary>
/// Fills in the R/G/B values for a character based on the associated flags.
/// </summary>
/// <param name="ch">Character to color.</param>
/// <returns>The colored version of the character.</returns>
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.ForegroundColor, ret.ForegroundBright);
}
if (ch.HasBasicBackgroundColor)
{
ret.Background = this.GetColorInfoFromBasicColor(ret.BackgroundColor, ret.BackgroundBright);
}

return ret;
}

private Character.ColorInfo GetColorInfoFromBasicColor(short basicColor, bool isBright)
{
var paletteOffset = isBright ? 8 : 0;
switch (basicColor)
{
case Character.Black:
return this.Palette[0 + paletteOffset];
case Character.Red:
return this.Palette[1 + paletteOffset];
case Character.Green:
return this.Palette[2 + paletteOffset];
case Character.Yellow:
return this.Palette[3 + paletteOffset];
case Character.Blue:
return this.Palette[4 + paletteOffset];
case Character.Magenta:
return this.Palette[5 + paletteOffset];
case Character.Cyan:
return this.Palette[6 + paletteOffset];
case Character.White:
return this.Palette[7 + paletteOffset];
default:
throw new InvalidOperationException("Unexpected color value.");
}
}

/// <summary>
/// Render the currently "on-screen" area character-by-character onto the specified target.
/// </summary>
Expand Down
194 changes: 70 additions & 124 deletions src/ConsoleBuffer/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,151 +9,97 @@ namespace ConsoleBuffer
// the future.
public struct Character
{
[Flags]
public enum Options : byte
{
None = 0x00,
// we have 3 color bits which combine to represent the 8 "basic" colors of classic terminals
ColorBits = 0x07, // (0x01, 0x02, 0x04)
Black = 0x00,
Red = 0x01,
Green = 0x02,
Yellow = 0x03,
Blue = 0x04,
Magenta = 0x05,
Cyan = 0x06,
White = 0x07,

// we use 3 additional bits (currently) for flags
FlagBits = 0x38, // (0x08, 0x10, 0x20)
Bright = 0x08,
Underline = 0x10,
Inverse = 0x20,
Extended = 0x18,
// 0x28 free
// 0x38 free

// 0x40, 0x80 free bits
}

public struct ColorInfo
{
public byte R;
public byte G;
public byte B;
public Options Options;
}

public bool Bright
{
get
{
return ((byte)this.Options & (byte)Options.Bright) == (byte)Options.Bright;
}
set
{
if (value)
{
this.Options = (Options)((byte)this.Options | (byte)Options.Bright);
}
else
{
this.Options = (Options)((byte)this.Options & ~(byte)Options.Bright);
}
}
}
public ColorInfo Foreground { get; set; }
public ColorInfo Background { get; set; }

/// <summary>
/// Whether the underline bit is set. Should not apply to background colors.
/// </summary>
public bool Underline
{
get
{
return ((byte)this.Options & (byte)Options.Underline) == (byte)Options.Underline;
}
set
{
if (value)
{
this.Options = (Options)((byte)this.Options | (byte)Options.Underline);
}
else
{
this.Options = (Options)((byte)this.Options & ~(byte)Options.Underline);
}
}
}
// traditional colors occupy 3 bits, we keep two sets (foreground + background).
public const short Black = 0x0000;
public const short Red = 0x0001;
public const short Green = 0x0002;
public const short Yellow = 0x0003;
public const short Blue = 0x0004;
public const short Magenta = 0x0005;
public const short Cyan = 0x0006;
public const short White = 0x0007;
private const short ForegroundColorMask = 0x0007;
private const short BackgroundBitShift = 3;
private const short BackgroundColorMask = ForegroundColorMask << BackgroundBitShift;
// flags
private const short ForegroundBasicColorFlag = 0x0001 << 6;
private const short BackgroundBasicColorFlag = 0x0002 << 6;
private const short ForegroundBrightFlag = 0x0004 << 6;
private const short BackgroundBrightFlag = 0x0008 << 6;
private const short UnderlineFlag = 0x0010 << 6;
private const short InverseFlag = 0x0020 << 6;
private const short ForegroundExplicitFlag = 0x0040 << 6;
private const short BackgroundExplicitFlag = 0x0080 << 6;
private const short ExplicitFlags = (ForegroundExplicitFlag | BackgroundBrightFlag);
private const short ForegroundExtendedFlag = 0x0100 << 6;
private const short BackgroundExtendedFlag = unchecked((short)(0x0200 << 6));

/// <summary>
/// Whether the 'inverse' bit is set. Should not apply to background colors.
/// </summary>
public bool Inverse
{
get
{
return ((byte)this.Options & (byte)Options.Inverse) == (byte)Options.Inverse;
}
set
{
if (value)
{
this.Options = (Options)((byte)this.Options | (byte)Options.Inverse);
}
else
{
this.Options = (Options)((byte)this.Options & ~(byte)Options.Inverse);
}
}
}
public short Options;

/// <summary>
/// Whether extended (RGB) color values should be applied for this cell.
/// </summary>
public bool Extended
public static short BasicColorOptions(short foreground = -1, short background = -1)
{
short options = 0;
if (foreground > -1)
{
get
{
return ((byte)this.Options & (byte)Options.Extended) == (byte)Options.Extended;
}
set
#if DEBUG
if (foreground > White)
{
if (value)
{
this.Options = (Options)((byte)this.Options | (byte)Options.Extended);
}
else
{
this.Options = (Options)((byte)this.Options & ~(byte)Options.Extended);
}
throw new ArgumentOutOfRangeException(nameof(foreground));
}
#endif
options |= (short)(foreground | ForegroundExplicitFlag | ForegroundBasicColorFlag);
}

/// <summary>
/// Classic color value.
/// </summary>
public Options BasicColor
if (background > -1)
{
get
{
return (Options)((byte)this.Options & (byte)Options.ColorBits);
}
set
#if DEBUG
if (background > White)
{
var colorValue = (byte)value & (byte)Options.ColorBits;
var colorlessOptions = (byte)this.Options & ~(byte)Options.ColorBits;
this.Options = (Options)(colorlessOptions & colorValue);
throw new ArgumentOutOfRangeException(nameof(background));
}
#endif
options |= (short)((background << BackgroundBitShift) | BackgroundExplicitFlag | BackgroundBasicColorFlag);
}

/// <summary>
/// True if there is any classic color value set.
/// </summary>
public bool HasBasicColor => ((byte)this.Options & (byte)Options.ColorBits) != 0;
return options;
}

public ColorInfo Foreground { get; set; }
public ColorInfo Background { get; set; }
public short ForegroundColor => (short)(this.Options & ForegroundColorMask);
public bool HasBasicForegroundColor => (this.Options & ForegroundBasicColorFlag) != 0;
public short BackgroundColor => (short)(this.Options & BackgroundColorMask);
public bool HasBasicBackgroundColor => (this.Options & BackgroundBasicColorFlag) != 0;
public bool ForegroundBright => (this.Options & ForegroundBrightFlag) != 0;
public bool BackgroundBright => (this.Options & BackgroundBrightFlag) != 0;
public bool Underline => (this.Options & UnderlineFlag) != 0;
public bool Inverse => (this.Options & InverseFlag) != 0;
public bool ForegroundExplicit => (this.Options & ForegroundExplicitFlag) != 0;
public bool BackgroundExplicit => (this.Options & BackgroundExplicitFlag) != 0;
public bool ForegroundExtended => (this.Options & ForegroundExtendedFlag) != 0;
public bool BackgroundExtended => (this.Options & BackgroundExtendedFlag) != 0;

public short InheritedOptions => (short)(this.Options & ~(ForegroundExplicitFlag | BackgroundExplicitFlag));

/// <summary>
/// The unicode glyph for this character.
/// </summary>
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;
}
}
}
3 changes: 3 additions & 0 deletions src/ConsoleBuffer/Line.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public Line(Line previous)
hintSize = previous.chars.Count;
}
lastCh.Glyph = 0x20;
lastCh.Options = lastCh.InheritedOptions;

this.chars = new List<Character>(hintSize)
{
Expand Down Expand Up @@ -51,6 +52,7 @@ public Character Get(int pos)

var ch = this.chars[this.chars.Count - 1];
ch.Glyph = 0x20;
ch.Options = ch.InheritedOptions;
return ch;
}

Expand Down Expand Up @@ -80,6 +82,7 @@ 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);
Expand Down
Loading

0 comments on commit 722cf4a

Please sign in to comment.