Make extended SGR RGB a first class, push/pop supported escape sequence.

Overhaul AnsiColorWriter to use VT sequences instead of Console API when VT support is present (huge output performance improvement from reduced context switches & associated overhead)
This commit is contained in:
Zhentar 2019-06-18 22:18:18 -05:00
Родитель fa7abbf5f4
Коммит c55cfbea94
7 изменённых файлов: 534 добавлений и 763 удалений

Просмотреть файл

@ -90,7 +90,6 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="internal\AnsiColorWriter.cs" />
<Compile Include="internal\BlockingCollectionHolder.cs" />
<Compile Include="internal\CaStringUtil.cs" />
<Compile Include="internal\CircularBuffer.cs" />

Просмотреть файл

@ -1,412 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace MS.Dbg
{
// This class interprets ANSI escape sequences to alter the color of console output. Only
// a limited set of control sequences are supported, namely a subset of SGR (Select
// Graphics Rendition) commands.
//
// See:
//
// http://bjh21.me.uk/all-escapes/all-escapes.txt
// http://en.wikipedia.org/wiki/ISO/IEC_6429
// http://en.wikipedia.org/wiki/ISO_6429
// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
//
internal class AnsiColorWriter : TextWriter
{
private const char CSI = '\x9b'; // "Control Sequence Initiator" (single character, as opposed to '\x1b' + '[')
private ConsoleColor DefaultForeground;
private ConsoleColor DefaultBackground;
private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot;
private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_hashCommands;
private ControlSequenceParseState m_state;
// We support control sequences being broken across calls to Write methods. This
// struct keeps track of state in between calls.
private struct ControlSequenceParseState
{
private int m_accum;
private bool m_isParsingParam;
private List< int > m_list;
// True if we are in the middle of parsing a sequence. Useful for when sequences
// are broken across multiple calls.
public bool IsParsing
{
get
{
return null != CurrentCommands;
}
}
private IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot;
public ControlSequenceParseState( IReadOnlyDictionary< char, Func< Action< List< int > > > > rootCommands )
{
m_commandTreeRoot = rootCommands;
CurrentCommands = null;
m_accum = 0;
m_isParsingParam = false;
m_list = null;
}
public IReadOnlyDictionary< char, Func< Action< List< int > > > > CurrentCommands;
// This can tolerate multiple Begin calls without destroying state.
public void Begin()
{
if( !IsParsing )
{
CurrentCommands = m_commandTreeRoot;
m_list = new List< int >();
m_isParsingParam = false;
}
}
public void AccumParamDigit( int digit )
{
m_accum *= 10;
m_accum += digit;
m_isParsingParam = true;
}
public void EnterParam()
{
Util.Assert( m_isParsingParam );
m_list.Add( m_accum );
m_isParsingParam = false;
m_accum = 0;
}
public List< int > FinishParsingParams()
{
if( m_isParsingParam )
EnterParam();
CurrentCommands = null;
List< int > list = m_list;
m_list = null;
return list;
}
/// <summary>
/// To recover from bad input.
/// </summary>
public void Reset()
{
CurrentCommands = null;
m_isParsingParam = false;
m_list = null;
m_accum = 0;
}
} // end class ControlSequenceParseState
public AnsiColorWriter()
{
DefaultForeground = Console.ForegroundColor;
DefaultBackground = Console.BackgroundColor;
m_commandTreeRoot = new Dictionary< char, Func< Action< List< int > > > >()
{
{ 'm', () => _SelectGraphicsRendition },
{ '#', _ProcessHashCommand },
};
m_hashCommands = new Dictionary< char, Func< Action< List< int > > > >()
{
// TROUBLE: The current definition of XTPUSHSGR and XTPOPSGR use curly
// brackets, which turns out to conflict badly with C# string formatting.
// For example, this:
//
//
// $csFmt = (New-ColorString).AppendPushFg( 'Cyan' ).Append( 'this should all be cyan: {0}' )
// [string]::format( $csFmt.ToString( $true ), 'blah' )
//
// will blow up.
//
// For now, I'm going to switch to some other characters while we see if
// we get can something worked out with xterm.
//{ '{', () => _PushSgr },
//{ '}', () => _PopSgr },
{ 'p', () => _PushSgr },
{ 'q', () => _PopSgr },
};
m_state = new ControlSequenceParseState( m_commandTreeRoot );
} // end constructor
public override Encoding Encoding { get { return Console.Out.Encoding; } }
public override void Write( char[] charBuffer, int index, int count)
{
Write( new String( charBuffer, index, count ) );
}
public override void Write( char c )
{
Write( c.ToString() );
}
public override void Write( string s )
{
int startIndex = 0;
if( m_state.IsParsing )
{
// Need to continue.
startIndex = _HandleControlSequence( s, -1 );
}
int escIndex = s.IndexOf( CSI, startIndex );
while( escIndex >= 0 )
{
//Tool.WL( "escIndex: {0}, startIndex: {1}", escIndex, startIndex );
string chunk = s.Substring( startIndex, escIndex - startIndex );
Console.Write( chunk );
startIndex = _HandleControlSequence( s, escIndex );
escIndex = s.IndexOf( CSI, startIndex );
}
if( !(startIndex >= s.Length) )
{
Console.Write( s.Substring( startIndex ) );
}
} // end Write()
/// <summary>
/// Returns the index of the first character past the control sequence (which
/// could be past the end of the string, or could be the start of another
/// control sequence).
/// </summary>
private int _HandleControlSequence( string s, int escIndex )
{
m_state.Begin(); // we may actually be continuing...
char c;
int curIndex = escIndex;
while( ++curIndex < s.Length )
{
c = s[ curIndex ];
if( (c >= '0') && (c <= '9') )
{
m_state.AccumParamDigit( ((int) c) - 0x30 );
continue;
}
else if( ';' == c )
{
m_state.EnterParam();
continue;
}
else if( ((c >= '@') && (c <= '~')) || (c == '#') )
{
if( _FindCommand( c, out Action< List< int > > command ) )
{
command( m_state.FinishParsingParams() );
return curIndex + 1;
}
}
else
{
// You're supposed to be able to have anonther character, like a space
// (0x20) before the command code character, but I'm not going to do that.
// TODO: Unfortunately, we can get into this scenario. Say somebody wrote a string to the
// output stream, and that string had control sequences. And say then somebody tried to process
// that string in a pipeline, for instance tried to find some property on it, that didn't
// exist, causing an error to be written, where the "target object" is the string... and stuff
// maybe gets truncated or ellipsis-ized... and then we're hosed. What should I do about this?
// Try to sanitize higher-level output? Seems daunting... Just reset state and ignore it? Hm.
m_state.Reset();
throw new ArgumentException( String.Format( "Invalid command sequence character at position {0} (0x{1:x}).", curIndex, (int) c ) );
}
}
// Finished parsing the whole string--the control sequence must be broken across
// strings.
return curIndex;
} // end _HandleControlSequence()
/// <summary>
/// Returns true if it successfully found a command to execute; false if we
/// need to read more characters (some commands are expressed with multiple
/// characters).
/// </summary>
private bool _FindCommand( char commandChar, out Action< List< int > > command )
{
Func< Action< List< int > > > commandSearcher;
if( !m_state.CurrentCommands.TryGetValue( commandChar, out commandSearcher ) )
{
throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) );
}
command = commandSearcher();
return command != null;
} // end _FindCommand()
// Info sources:
// http://bjh21.me.uk/all-escapes/all-escapes.txt
// http://en.wikipedia.org/wiki/ISO/IEC_6429
// http://en.wikipedia.org/wiki/ISO_6429
// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
private static ConsoleColor[] AnsiNormalColorMap =
{
// Foreground Background
ConsoleColor.Black, // 30 40
ConsoleColor.DarkRed, // 31 41
ConsoleColor.DarkGreen, // 32 42
ConsoleColor.DarkYellow, // 33 43
ConsoleColor.DarkBlue, // 34 44
ConsoleColor.DarkMagenta, // 35 45
ConsoleColor.DarkCyan, // 36 46
ConsoleColor.Gray // 37 47
};
private static ConsoleColor[] AnsiBrightColorMap =
{ // Foreground Background
ConsoleColor.DarkGray, // 90 100
ConsoleColor.Red, // 91 101
ConsoleColor.Green, // 92 102
ConsoleColor.Yellow, // 93 103
ConsoleColor.Blue, // 94 104
ConsoleColor.Magenta, // 95 105
ConsoleColor.Cyan, // 96 106
ConsoleColor.White // 97 107
};
// In addition to the color codes, I've added support for two additional
// (non-standard) codes:
//
// 56: Push fg/bg color pair
// 57: Pop fg/bg color pair
//
// However, THESE ARE DEPRECATED. Use XTPUSHSGR/XTPOPSGR instead.
//
private void _SelectGraphicsRendition( List< int > args )
{
if( 0 == args.Count )
args.Add( 0 ); // no args counts as a reset
foreach( int arg in args )
{
_ProcessSgrCode( arg );
}
} // end _SelectGraphicsRendition()
private void _PushSgr( List< int > args )
{
if( (args != null) && (args.Count != 0) )
{
throw new NotSupportedException( "Optional arguments to the XTPUSHSGR command are not currently implemented." );
}
m_colorStack.Push( new ColorPair( Console.ForegroundColor, Console.BackgroundColor ) );
} // end _PushSgr()
private void _PopSgr( List< int > args )
{
if( (args != null) && (args.Count != 0) )
{
throw new InvalidOperationException( "The XTPOPSGR command does not accept arguments." );
}
ColorPair cp = m_colorStack.Pop();
Console.ForegroundColor = cp.Foreground;
Console.BackgroundColor = cp.Background;
} // end _PopSgr()
private Action< List< int > > _ProcessHashCommand()
{
m_state.CurrentCommands = m_hashCommands;
return null;
} // end _SelectGraphicsRendition()
private class ColorPair
{
public ConsoleColor Foreground { get; private set; }
public ConsoleColor Background { get; private set; }
public ColorPair( ConsoleColor foreground, ConsoleColor background )
{
Foreground = foreground;
Background = background;
}
}
private Stack< ColorPair > m_colorStack = new Stack< ColorPair >();
private void _ProcessSgrCode( int code )
{
if( 0 == code )
{
Console.ForegroundColor = DefaultForeground;
Console.BackgroundColor = DefaultBackground;
}
else if( (code <= 37) && (code >= 30) )
{
Console.ForegroundColor = AnsiNormalColorMap[ (code - 30) ];
}
else if( (code <= 47) && (code >= 40) )
{
Console.BackgroundColor = AnsiNormalColorMap[ (code - 40) ];
}
else if( (code <= 97) && (code >= 90) )
{
Console.ForegroundColor = AnsiBrightColorMap[ (code - 90) ];
}
else if( (code <= 107) && (code >= 100) )
{
Console.BackgroundColor = AnsiBrightColorMap[ (code - 100) ];
}
else if( 56 == code ) // NON-STANDARD (I made this one up)
{
Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." );
m_colorStack.Push( new ColorPair( Console.ForegroundColor, Console.BackgroundColor ) );
}
else if( 57 == code ) // NON-STANDARD (I made this one up)
{
Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." );
ColorPair cp = m_colorStack.Pop();
Console.ForegroundColor = cp.Foreground;
Console.BackgroundColor = cp.Background;
}
else
{
throw new NotSupportedException( String.Format( "SGR code '{0}' not supported.", code ) );
}
} // end _ProcessSgrCode()
public static void Test()
{
AnsiColorWriter colorWriter = new AnsiColorWriter();
colorWriter.Write( "\u009b91mThis should be red.\u009bm This should not.\r\n" );
colorWriter.Write( "\u009b91;41mThis should be red on red.\u009bm This should not.\r\n" );
colorWriter.Write( "\u009b91;100mThis should be red on gray.\u009b" );
colorWriter.Write( "m This should not.\r\n" );
colorWriter.Write( "\u009b" );
colorWriter.Write( "9" );
colorWriter.Write( "1;10" );
colorWriter.Write( "6mThis should be red on cyan.\u009b" );
colorWriter.Write( "m This should \u009bm\u009bmnot.\r\n" );
} // end Test
} // end class AnsiColorWriter
}

Просмотреть файл

@ -313,13 +313,22 @@ namespace MS.Dbg
m_apparentLength += other.m_apparentLength;
return this;
}
public ColorString AppendFgRgb( byte r, byte g, byte b, ColorString other )
public ColorString AppendFgRgb( byte r, byte g, byte b, ColorString other = null)
{
_CheckReadOnly( true );
m_elements.Add( new SgrControlSequence( new[] { 38, 2, r, g, b } ) );
if( null == other )
return this;
return Append( other );
}
public ColorString AppendBgRgb( byte r, byte g, byte b, ColorString other = null )
{
_CheckReadOnly( true );
m_elements.Add( new ContentElement( $"\x01b[38;2;{r};{g};{b}m" ));
m_elements.Add( new SgrControlSequence( new[] { 48, 2, r, g, b } ) );
if( null == other )
return this;
return Append( other );
}

Просмотреть файл

@ -60,15 +60,7 @@ namespace MS.DbgShell
// Turn on virtual terminal if possible.
// This might throw - not sure how exactly (no console), but if it does, we shouldn't fail to start.
var handle = ConsoleControl.GetActiveScreenBufferHandle();
var m = ConsoleControl.GetMode(handle);
if (ConsoleControl.NativeMethods.SetConsoleMode(handle.DangerousGetHandle(), (uint)(m | ConsoleControl.ConsoleModes.VirtualTerminal)))
{
// We only know if vt100 is supported if the previous call actually set the new flag, older
// systems ignore the setting.
m = ConsoleControl.GetMode(handle);
this.SupportsVirtualTerminal = (m & ConsoleControl.ConsoleModes.VirtualTerminal) != 0;
}
SupportsVirtualTerminal = ConsoleControl.CheckVirtualTerminalSupported();
}
catch
{

Просмотреть файл

@ -7,124 +7,136 @@ using ConsoleHandle = Microsoft.Win32.SafeHandles.SafeFileHandle;
namespace MS.DbgShell
{
// (unindented a level to simplify the diff with other similar copies of this code)
internal static partial class ConsoleControl
{
// This class interprets ANSI escape sequences to alter the color of console output. Only
// a limited set of control sequences are supported, namely a subset of SGR (Select
// Graphics Rendition) commands.
//
// See:
//
// http://bjh21.me.uk/all-escapes/all-escapes.txt
// http://en.wikipedia.org/wiki/ISO/IEC_6429
// http://en.wikipedia.org/wiki/ISO_6429
// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
//
internal class AnsiColorWriter
internal static partial class ConsoleControl
{
private const char CSI = '\x9b'; // "Control Sequence Initiator" (single character, as opposed to '\x1b' + '[')
private ConsoleColor DefaultForeground;
private ConsoleColor DefaultBackground;
private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot;
private readonly IReadOnlyDictionary< char, Func< Action< List< int > > > > m_hashCommands;
private ControlSequenceParseState m_state;
// We support control sequences being broken across calls to Write methods. This
// struct keeps track of state in between calls.
private struct ControlSequenceParseState
// This class interprets ANSI escape sequences to alter the color of console output. Only
// a limited set of control sequences are supported, namely a subset of SGR (Select
// Graphics Rendition) commands.
//
// See:
//
// http://bjh21.me.uk/all-escapes/all-escapes.txt
// http://en.wikipedia.org/wiki/ISO/IEC_6429
// http://en.wikipedia.org/wiki/ISO_6429
// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
//
internal class AnsiColorWriter
{
private int m_accum;
private bool m_isParsingParam;
private List< int > m_list;
private const char CSI = '\x9b'; // "Control Sequence Initiator" (single character, as opposed to '\x1b' + '[')
private const string CSI_str = "\x9b"; //String version for printing (was sticking the char in a stackalloc span, but hit https://github.com/dotnet/roslyn/issues/35874 )
// True if we are in the middle of parsing a sequence. Useful for when sequences
// are broken across multiple calls.
public bool IsParsing
private readonly ConsoleColor DefaultForeground;
private readonly ConsoleColor DefaultBackground;
private ControlSequenceParseState m_state;
public bool VirtualTerminalSupported { get; set; }
// We support control sequences being broken across calls to Write methods. This
// struct keeps track of state in between calls.
private struct ControlSequenceParseState
{
get
private int m_accum;
private bool m_isParsingParam;
private List< int > m_list;
// True if we are in the middle of parsing a sequence. Useful for when sequences
// are broken across multiple calls.
public bool IsParsing
{
return null != CurrentCommands;
get
{
return null != CurrentCommandHandler;
}
}
}
private IReadOnlyDictionary< char, Func< Action< List< int > > > > m_commandTreeRoot;
private readonly Func< char, bool > m_commandTreeRootDelegate;
public ControlSequenceParseState( IReadOnlyDictionary< char, Func< Action< List< int > > > > rootCommands )
{
m_commandTreeRoot = rootCommands;
CurrentCommands = null;
m_accum = 0;
m_isParsingParam = false;
m_list = null;
}
public IReadOnlyDictionary< char, Func< Action< List< int > > > > CurrentCommands;
// This can tolerate multiple Begin calls without destroying state.
public void Begin()
{
if( !IsParsing )
public ControlSequenceParseState( Func< char, bool > rootCommands )
{
CurrentCommands = m_commandTreeRoot;
m_list = new List< int >();
m_commandTreeRootDelegate = rootCommands;
CurrentCommandHandler = null;
m_accum = 0;
m_isParsingParam = false;
m_list = null;
}
public Func< char, bool > CurrentCommandHandler;
// This can tolerate multiple Begin calls without destroying state.
public void Begin()
{
if( !IsParsing )
{
CurrentCommandHandler = m_commandTreeRootDelegate;
m_list = new List< int >();
m_isParsingParam = false;
}
}
public void AccumParamDigit( int digit )
{
m_accum *= 10;
m_accum += digit;
m_isParsingParam = true;
}
public void EnterParam()
{
Util.Assert( m_isParsingParam );
m_list.Add( m_accum );
m_isParsingParam = false;
m_accum = 0;
}
public List< int > FinishParsingParams()
{
if( m_isParsingParam )
EnterParam();
CurrentCommandHandler = null;
List< int > list = m_list;
m_list = null;
return list;
}
/// <summary>
/// To recover from bad input.
/// </summary>
public void Reset()
{
CurrentCommandHandler = null;
m_isParsingParam = false;
m_list = null;
m_accum = 0;
}
} // end class ControlSequenceParseState
public AnsiColorWriter()
{
DefaultForeground = ConsoleControl.ForegroundColor;
DefaultBackground = ConsoleControl.BackgroundColor;
m_state = new ControlSequenceParseState( CommandTreeRootStateHandler );
} // end constructor
private bool CommandTreeRootStateHandler( char commandChar )
{
switch( commandChar )
{
case 'm':
_SelectGraphicsRendition( m_state.FinishParsingParams() );
return true;
case '#':
m_state.CurrentCommandHandler = HashCommandStateHandler;
return false;
default:
throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) );
}
}
public void AccumParamDigit( int digit )
{
m_accum *= 10;
m_accum += digit;
m_isParsingParam = true;
}
public void EnterParam()
{
Util.Assert( m_isParsingParam );
m_list.Add( m_accum );
m_isParsingParam = false;
m_accum = 0;
}
public List< int > FinishParsingParams()
{
if( m_isParsingParam )
EnterParam();
CurrentCommands = null;
List< int > list = m_list;
m_list = null;
return list;
}
/// <summary>
/// To recover from bad input.
/// </summary>
public void Reset()
{
CurrentCommands = null;
m_isParsingParam = false;
m_list = null;
m_accum = 0;
}
} // end class ControlSequenceParseState
public AnsiColorWriter()
{
DefaultForeground = ConsoleControl.ForegroundColor;
DefaultBackground = ConsoleControl.BackgroundColor;
m_commandTreeRoot = new Dictionary< char, Func< Action< List< int > > > >()
{
{ 'm', () => _SelectGraphicsRendition },
{ '#', _ProcessHashCommand },
};
m_hashCommands = new Dictionary< char, Func< Action< List< int > > > >()
private bool HashCommandStateHandler( char commandChar )
{
// TROUBLE: The current definition of XTPUSHSGR and XTPOPSGR use curly
// brackets, which turns out to conflict badly with C# string formatting.
@ -140,256 +152,425 @@ internal static partial class ConsoleControl
// we get can something worked out with xterm.
//{ '{', () => _PushSgr },
//{ '}', () => _PopSgr },
{ 'p', () => _PushSgr },
{ 'q', () => _PopSgr },
};
m_state = new ControlSequenceParseState( m_commandTreeRoot );
} // end constructor
public void Write( ConsoleHandle handle, ReadOnlySpan< char > s, bool appendNewline )
{
if( m_state.IsParsing )
{
// Need to continue.
int startIndex = _HandleControlSequence( s, -1 );
s = s.Slice( startIndex );
}
int escIndex = s.IndexOf( CSI );
while( escIndex >= 0 )
{
//Tool.WL( "escIndex: {0}, startIndex: {1}", escIndex, startIndex );
var chunk = s.Slice( 0, escIndex );
ConsoleControl._RealWriteConsole( handle, chunk );
int startIndex = _HandleControlSequence( s, escIndex );
s = s.Slice( startIndex );
escIndex = s.IndexOf( CSI );
}
if( !s.IsEmpty )
{
ConsoleControl._RealWriteConsole( handle, s );
}
if( appendNewline )
{
ConsoleControl._RealWriteConsole( handle, ColorHostUserInterface.Crlf.AsSpan() );
}
} // end Write()
/// <summary>
/// Returns the index of the first character past the control sequence (which
/// could be past the end of the string, or could be the start of another
/// control sequence).
/// </summary>
private int _HandleControlSequence( ReadOnlySpan< char > s, int escIndex )
{
m_state.Begin(); // we may actually be continuing...
char c;
int curIndex = escIndex;
while( ++curIndex < s.Length )
{
c = s[ curIndex ];
if( (c >= '0') && (c <= '9') )
switch( commandChar )
{
m_state.AccumParamDigit( ((int) c) - 0x30 );
continue;
case 'p':
_PushSgr( m_state.FinishParsingParams() );
return true;
case 'q':
_PopSgr( m_state.FinishParsingParams() );
return true;
default:
throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) );
}
else if( ';' == c )
}
private char[] m_outputCharBuffer = new char[ 16384 ];
private int m_outputBufferPos = 0;
public void Write( ConsoleHandle handle, ReadOnlySpan< char > s, bool appendNewline )
{
m_outputBufferPos = 0;
if( m_state.IsParsing )
{
m_state.EnterParam();
continue;
// Need to continue.
int startIndex = _HandleControlSequence( s, -1 );
s = s.Slice( startIndex );
}
else if( ((c >= '@') && (c <= '~')) || (c == '#') )
int escIndex = s.IndexOf( CSI );
while( escIndex >= 0 )
{
if( _FindCommand( c, out Action< List< int > > command ) )
{
command( m_state.FinishParsingParams() );
return curIndex + 1;
}
var chunk = s.Slice( 0, escIndex );
WriteConsoleWrapper( handle, chunk );
int startIndex = _HandleControlSequence( s, escIndex );
s = s.Slice( startIndex );
escIndex = s.IndexOf( CSI );
}
if( !s.IsEmpty )
{
WriteConsoleWrapper( handle, s );
}
if( appendNewline )
{
WriteConsoleWrapper( handle, ColorHostUserInterface.Crlf.AsSpan() );
}
FinalizeWrite( handle );
} // end Write()
private void WriteConsoleWrapper( ConsoleHandle handle, ReadOnlySpan<char> value )
{
if( VirtualTerminalSupported )
{
AppendToOutputBuffer( value );
}
else
{
// You're supposed to be able to have anonther character, like a space
// (0x20) before the command code character, but I'm not going to do that.
// [zhent] the PowerShell source claims that WriteConsole breaks down past 64KB. But it also seems to think `char` is/can be 4 bytes, so I'm skeptical.
// In my tests on Windows 10, it can handle at least up to 4GB (with enough patience), so I'll leave the chunking for versions that aren't recent enough to support VT
while( value.Length > 16383 )
{
_RealWriteConsole( handle, value.Slice( 0, 16383 ) );
value = value.Slice( 16383 );
}
// TODO: Unfortunately, we can get into this scenario. Say somebody wrote a string to the
// output stream, and that string had control sequences. And say then somebody tried to process
// that string in a pipeline, for instance tried to find some property on it, that didn't
// exist, causing an error to be written, where the "target object" is the string... and stuff
// maybe gets truncated or ellipsis-ized... and then we're hosed. What should I do about this?
// Try to sanitize higher-level output? Seems daunting... Just reset state and ignore it? Hm.
m_state.Reset();
throw new ArgumentException( String.Format( "Invalid command sequence character at position {0} (0x{1:x}).", curIndex, (int) c ) );
_RealWriteConsole( handle, value );
}
}
// Finished parsing the whole string--the control sequence must be broken across
// strings.
return curIndex;
} // end _HandleControlSequence()
/// <summary>
/// Returns true if it successfully found a command to execute; false if we
/// need to read more characters (some commands are expressed with multiple
/// characters).
/// </summary>
private bool _FindCommand( char commandChar, out Action< List< int > > command )
{
Func< Action< List< int > > > commandSearcher;
if( !m_state.CurrentCommands.TryGetValue( commandChar, out commandSearcher ) )
private void AppendToOutputBuffer( ReadOnlySpan<char> value )
{
throw new NotSupportedException( String.Format( "The command code '{0}' (0x{1:x}) is not supported.", commandChar, (int) commandChar ) );
var destSpan = m_outputCharBuffer.AsSpan( m_outputBufferPos );
while( value.Length > destSpan.Length )
{
var newBuff = new char[ m_outputCharBuffer.Length * 2 ];
m_outputCharBuffer.AsSpan( 0, m_outputBufferPos ).CopyTo( newBuff.AsSpan() );
m_outputCharBuffer = newBuff;
destSpan = m_outputCharBuffer.AsSpan( m_outputBufferPos );
}
value.CopyTo( destSpan );
m_outputBufferPos += value.Length;
}
command = commandSearcher();
return command != null;
} // end _FindCommand()
// Commented out due to https://github.com/dotnet/roslyn/issues/35874
//private void AppendToOutputBuffer( char value )
//{
// Span< char > tempBuff = stackalloc char[ 1 ];
// tempBuff[ 0 ] = value;
// AppendToOutputBuffer( tempBuff );
//}
// Info sources:
// http://bjh21.me.uk/all-escapes/all-escapes.txt
// http://en.wikipedia.org/wiki/ISO/IEC_6429
// http://en.wikipedia.org/wiki/ISO_6429
// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
private static ConsoleColor[] AnsiNormalColorMap =
{
// Foreground Background
ConsoleColor.Black, // 30 40
ConsoleColor.DarkRed, // 31 41
ConsoleColor.DarkGreen, // 32 42
ConsoleColor.DarkYellow, // 33 43
ConsoleColor.DarkBlue, // 34 44
ConsoleColor.DarkMagenta, // 35 45
ConsoleColor.DarkCyan, // 36 46
ConsoleColor.Gray // 37 47
};
private static ConsoleColor[] AnsiBrightColorMap =
{ // Foreground Background
ConsoleColor.DarkGray, // 90 100
ConsoleColor.Red, // 91 101
ConsoleColor.Green, // 92 102
ConsoleColor.Yellow, // 93 103
ConsoleColor.Blue, // 94 104
ConsoleColor.Magenta, // 95 105
ConsoleColor.Cyan, // 96 106
ConsoleColor.White // 97 107
};
// In addition to the color codes, I've added support for two additional
// (non-standard) codes:
//
// 56: Push fg/bg color pair
// 57: Pop fg/bg color pair
//
// However, THESE ARE DEPRECATED. Use XTPUSHSGR/XTPOPSGR instead.
//
private void _SelectGraphicsRendition( List< int > args )
{
if( 0 == args.Count )
args.Add( 0 ); // no args counts as a reset
foreach( int arg in args )
private void FinalizeWrite( ConsoleHandle handle )
{
_ProcessSgrCode( arg );
}
} // end _SelectGraphicsRendition()
private void _PushSgr( List< int > args )
{
if( (args != null) && (args.Count != 0) )
{
throw new NotSupportedException( "Optional arguments to the XTPUSHSGR command are not currently implemented." );
if( VirtualTerminalSupported )
{
_RealWriteConsole( handle, m_outputCharBuffer.AsSpan( 0, m_outputBufferPos ) );
}
}
m_colorStack.Push( new ColorPair( Console.ForegroundColor, Console.BackgroundColor ) );
} // end _PushSgr()
/// <summary>
/// Returns the index of the first character past the control sequence (which
/// could be past the end of the string, or could be the start of another
/// control sequence).
/// </summary>
private int _HandleControlSequence( ReadOnlySpan< char > s, int escIndex )
{
m_state.Begin(); // we may actually be continuing...
int curIndex = escIndex;
while( ++curIndex < s.Length )
{
char c = s[ curIndex ];
if( (c >= '0') && (c <= '9') )
{
m_state.AccumParamDigit( ((int) c) - 0x30 );
continue;
}
else if( ';' == c )
{
m_state.EnterParam();
continue;
}
else if( ((c >= '@') && (c <= '~')) || (c == '#') )
{
if( m_state.CurrentCommandHandler( c ) )
{
return curIndex + 1;
}
}
else
{
// You're supposed to be able to have another character, like a space
// (0x20) before the command code character, but I'm not going to do that.
// TODO: Unfortunately, we can get into this scenario. Say somebody wrote a string to the
// output stream, and that string had control sequences. And say then somebody tried to process
// that string in a pipeline, for instance tried to find some property on it, that didn't
// exist, causing an error to be written, where the "target object" is the string... and stuff
// maybe gets truncated or ellipsis-ized... and then we're hosed. What should I do about this?
// Try to sanitize higher-level output? Seems daunting... Just reset state and ignore it? Hm.
m_state.Reset();
throw new ArgumentException( String.Format( "Invalid command sequence character at position {0} (0x{1:x}).", curIndex, (int) c ) );
}
}
// Finished parsing the whole string--the control sequence must be broken across
// strings.
return curIndex;
} // end _HandleControlSequence()
private void _PopSgr( List< int > args )
{
if( (args != null) && (args.Count != 0) )
// Info sources:
// http://bjh21.me.uk/all-escapes/all-escapes.txt
// http://en.wikipedia.org/wiki/ISO/IEC_6429
// http://en.wikipedia.org/wiki/ISO_6429
// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
private static ConsoleColor[] AnsiNormalColorMap =
{
throw new InvalidOperationException( "The XTPOPSGR command does not accept arguments." );
}
// Foreground Background
ConsoleColor.Black, // 30 40
ConsoleColor.DarkRed, // 31 41
ConsoleColor.DarkGreen, // 32 42
ConsoleColor.DarkYellow, // 33 43
ConsoleColor.DarkBlue, // 34 44
ConsoleColor.DarkMagenta, // 35 45
ConsoleColor.DarkCyan, // 36 46
ConsoleColor.Gray // 37 47
};
ColorPair cp = m_colorStack.Pop();
Console.ForegroundColor = cp.Foreground;
Console.BackgroundColor = cp.Background;
} // end _PopSgr()
private static ConsoleColor[] AnsiBrightColorMap =
{ // Foreground Background
ConsoleColor.DarkGray, // 90 100
ConsoleColor.Red, // 91 101
ConsoleColor.Green, // 92 102
ConsoleColor.Yellow, // 93 103
ConsoleColor.Blue, // 94 104
ConsoleColor.Magenta, // 95 105
ConsoleColor.Cyan, // 96 106
ConsoleColor.White // 97 107
};
// In addition to the color codes, I've added support for two additional
// (non-standard) codes:
//
// 56: Push fg/bg color pair
// 57: Pop fg/bg color pair
//
// However, THESE ARE DEPRECATED. Use XTPUSHSGR/XTPOPSGR instead.
//
private void _SelectGraphicsRendition( List< int > args )
{
if( 0 == args.Count )
args.Add( 0 ); // no args counts as a reset
int index = 0;
while( index < args.Count )
{
_ProcessSgrCode( args, ref index );
}
} // end _SelectGraphicsRendition()
private Action< List< int > > _ProcessHashCommand()
{
m_state.CurrentCommands = m_hashCommands;
private void _PushSgr( List< int > args )
{
if( (args != null) && (args.Count != 0) )
{
throw new NotSupportedException( "Optional arguments to the XTPUSHSGR command are not currently implemented." );
}
return null;
} // end _SelectGraphicsRendition()
m_colorCodeStack.Push( new ColorCodePair( ForegroundCode, BackgroundCode ) );
} // end _PushSgr()
private class ColorPair
{
public ConsoleColor Foreground { get; private set; }
public ConsoleColor Background { get; private set; }
public ColorPair( ConsoleColor foreground, ConsoleColor background )
private void _PopSgr( List< int > args )
{
Foreground = foreground;
Background = background;
}
}
if( (args != null) && (args.Count != 0) )
{
throw new InvalidOperationException( "The XTPOPSGR command does not accept arguments." );
}
private Stack< ColorPair > m_colorStack = new Stack< ColorPair >();
var pair = m_colorCodeStack.Pop();
ForegroundCode = pair.Foreground;
BackgroundCode = pair.Background;
} // end _PopSgr()
private struct VtColorCode
{
public VtColorCode( int commandCode ) : this()
{
CommandCode = (byte)commandCode;
}
private void _ProcessSgrCode( int code )
{
if( 0 == code )
{
ConsoleControl.ForegroundColor = DefaultForeground;
ConsoleControl.BackgroundColor = DefaultBackground;
public VtColorCode( byte commandCode, byte r, byte g, byte b )
{
CommandCode = commandCode;
R = r;
G = g;
B = b;
}
public byte CommandCode { get; }
public byte R { get; }
public byte G { get; }
public byte B { get; }
}
else if( (code <= 37) && (code >= 30) )
private struct ColorCodePair
{
ConsoleControl.ForegroundColor = AnsiNormalColorMap[ (code - 30) ];
public VtColorCode Foreground { get; }
public VtColorCode Background { get; }
public ColorCodePair( VtColorCode foreground, VtColorCode background )
{
Foreground = foreground;
Background = background;
}
}
else if( (code <= 47) && (code >= 40) )
private readonly Stack<ColorCodePair> m_colorCodeStack = new Stack<ColorCodePair>();
private VtColorCode m_activeForegroundCode = new VtColorCode( 39 );
private VtColorCode m_activeBackgroundCode = new VtColorCode( 49 );
private VtColorCode ForegroundCode
{
ConsoleControl.BackgroundColor = AnsiNormalColorMap[ (code - 40) ];
get
{
return m_activeForegroundCode;
}
set
{
m_activeForegroundCode = value;
if( VirtualTerminalSupported )
{
_WriteCodeToBuffer( value );
}
else
{
if( value.CommandCode == 0 || value.CommandCode == 39 || value.CommandCode == 38 ) //Treat RGB codes as default since we can't render them correctly
{
ConsoleControl.ForegroundColor = DefaultForeground;
}
else if( (value.CommandCode <= 37) && (value.CommandCode >= 30) )
{
ConsoleControl.ForegroundColor = AnsiNormalColorMap[ (value.CommandCode - 30) ];
}
else if( (value.CommandCode <= 97) && (value.CommandCode >= 90) )
{
ConsoleControl.ForegroundColor = AnsiBrightColorMap[ (value.CommandCode - 90) ];
}
else
{
throw new InvalidOperationException();
}
}
}
}
else if( (code <= 97) && (code >= 90) )
private VtColorCode BackgroundCode
{
ConsoleControl.ForegroundColor = AnsiBrightColorMap[ (code - 90) ];
get
{
return m_activeBackgroundCode;
}
set
{
m_activeBackgroundCode = value;
if( VirtualTerminalSupported )
{
_WriteCodeToBuffer( value );
}
else
{
if( value.CommandCode == 0 || value.CommandCode == 49 || value.CommandCode == 48 ) //Treat RGB codes as default since we can't render them correctly
{
ConsoleControl.BackgroundColor = DefaultBackground;
}
else if( (value.CommandCode <= 47) && (value.CommandCode >= 40) )
{
ConsoleControl.BackgroundColor = AnsiNormalColorMap[ (value.CommandCode - 40) ];
}
else if( (value.CommandCode <= 107) && (value.CommandCode >= 100) )
{
ConsoleControl.BackgroundColor = AnsiBrightColorMap[ (value.CommandCode - 100) ];
}
else
{
throw new InvalidOperationException();
}
}
}
}
else if( (code <= 107) && (code >= 100) )
private void _WriteCodeToBuffer( VtColorCode code )
{
ConsoleControl.BackgroundColor = AnsiBrightColorMap[ (code - 100) ];
AppendToOutputBuffer( CSI_str.AsSpan() );
AppendToOutputBuffer( sm_byteStrings[ code.CommandCode ].AsSpan() );
if( code.CommandCode == 38 || code.CommandCode == 48 )
{
AppendToOutputBuffer( ";2;".AsSpan() );
AppendToOutputBuffer( sm_byteStrings[ code.R ].AsSpan() );
AppendToOutputBuffer( ";".AsSpan() );
AppendToOutputBuffer( sm_byteStrings[ code.G ].AsSpan() );
AppendToOutputBuffer( ";".AsSpan() );
AppendToOutputBuffer( sm_byteStrings[ code.B ].AsSpan() );
}
AppendToOutputBuffer( "m".AsSpan() );
}
else if( 56 == code ) // NON-STANDARD (I made this one up)
private void _ProcessSgrCode( List< int > codes, ref int index )
{
Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." );
m_colorStack.Push( new ColorPair( ConsoleControl.ForegroundColor, ConsoleControl.BackgroundColor ) );
}
else if( 57 == code ) // NON-STANDARD (I made this one up)
var code = codes[ index ];
index++;
if( 0 == code )
{
//using default foreground/background codes instead of full reset so that the colorcodestack can maintain either one as default independently
ForegroundCode = new VtColorCode( 39 );
BackgroundCode = new VtColorCode( 49 );
}
else if( ((code <= 37) && (code >= 30)) || ((code <= 97) && (code >= 90)) || code == 39 )
{
ForegroundCode = new VtColorCode( code );
}
else if( ((code <= 47) && (code >= 40)) || ((code <= 107) && (code >= 100)) || code == 49 )
{
BackgroundCode = new VtColorCode( code );
}
else if( code == 38 )
{
ForegroundCode = _ProcessTrueColorSgrCode( code, codes, ref index );
}
else if( code == 48 )
{
BackgroundCode = _ProcessTrueColorSgrCode( code, codes, ref index );
}
else if( 56 == code ) // NON-STANDARD (I made this one up)
{
Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." );
m_colorCodeStack.Push( new ColorCodePair( ForegroundCode, BackgroundCode ) );
}
else if( 57 == code ) // NON-STANDARD (I made this one up)
{
Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." );
var pair = m_colorCodeStack.Pop();
ForegroundCode = pair.Foreground;
BackgroundCode = pair.Background;
}
else
{
throw new NotSupportedException( String.Format( "SGR code '{0}' not supported.", code ) );
}
} // end _ProcessSgrCode()
private static VtColorCode _ProcessTrueColorSgrCode( int command, List< int > codes, ref int index )
{
Util.Fail( "The 56/57 SGR codes (non-standard push/pop) are deprecated. Use XTPUSHSGR/XTPOPSGR instead." );
ColorPair cp = m_colorStack.Pop();
ConsoleControl.ForegroundColor = cp.Foreground;
ConsoleControl.BackgroundColor = cp.Background;
if( codes.Count - index < 4 ) { throw new ArgumentException( "Extended SGR sequence does not have enough arguments for RGB colors" ); }
if( codes[index] != 2 ) { throw new NotSupportedException( $"Extended SGR mode '{codes[ index + 1 ]}' not supported." ); }
index++;
return new VtColorCode( (byte)command, (byte) codes[ index++ ], (byte) codes[ index++ ], (byte) codes[ index++ ] );
}
else
{
throw new NotSupportedException( String.Format( "SGR code '{0}' not supported.", code ) );
}
} // end _ProcessSgrCode()
} // end class AnsiColorWriter
} // class ConsoleControl
//[zhent] Because the cost of .ToString() is just too much to bear.
private static readonly string[] sm_byteStrings =
{ "0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31",
"32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63",
"64","65","66","67","68","69","70","71","72","73","74","75","76","77","78","79","80","81","82","83","84","85","86","87","88","89","90","91","92","93","94","95",
"96","97","98","99","100","101","102","103","104","105","106","107","108","109","110","111","112","113","114","115","116","117","118","119","120","121","122","123","124","125","126","127",
"128","129","130","131","132","133","134","135","136","137","138","139","140","141","142","143","144","145","146","147","148","149","150","151","152","153","154","155","156","157","158","159",
"160","161","162","163","164","165","166","167","168","169","170","171","172","173","174","175","176","177","178","179","180","181","182","183","184","185","186","187","188","189","190","191",
"192","193","194","195","196","197","198","199","200","201","202","203","204","205","206","207","208","209","210","211","212","213","214","215","216","217","218","219","220","221","222","223",
"224","225","226","227","228","229","230","231","232","233","234","235","236","237","238","239","240","241","242","243","244","245","246","247","248","249","250","251","252","253","254","255"};
} // end class AnsiColorWriter
} // class ConsoleControl
} // namespace

Просмотреть файл

@ -747,10 +747,27 @@ namespace MS.DbgShell
}
}
internal static bool CheckVirtualTerminalSupported()
{
var handle = GetActiveScreenBufferHandle();
var m = GetMode( handle );
if( NativeMethods.SetConsoleMode( handle.DangerousGetHandle(), (uint) (m | ConsoleControl.ConsoleModes.VirtualTerminal) ) )
{
// We only know if vt100 is supported if the previous call actually set the new flag, older
// systems ignore the setting.
m = GetMode( handle );
if( (m & ConsoleModes.VirtualTerminal) != 0 )
{
sm_colorWriter.VirtualTerminalSupported = true;
return true;
}
}
return false;
}
#endregion
#endregion
#region Input
#region Input
@ -2758,36 +2775,19 @@ namespace MS.DbgShell
return;
}
// [danthom] Debugging note: put a breakpoint here to catch output as it
// is going out.
// If a newline gets injected between strings where a color control
// sequence is broken across them, things blow up terribly.
int cursor = 0; // This records the chopping position in output string
const int MaxBufferSize = 16383; // this is 64K/4 - 1 to account for possible width of each character.
while( cursor < output.Length )
if( ColorHostUserInterface.Crlf.AsSpan().SequenceEqual( output ) )
{
ReadOnlySpan< char > outBuffer;
if( cursor + MaxBufferSize < output.Length )
{
outBuffer = output.Slice( cursor, MaxBufferSize );
cursor += MaxBufferSize;
}
else
{
outBuffer = output.Slice( cursor );
cursor = output.Length;
}
if( ColorHostUserInterface.Crlf.AsSpan().SequenceEqual( outBuffer ) )
{
_RealWriteConsole( consoleHandle, outBuffer );
}
else
{
sm_colorWriter.Write( consoleHandle, outBuffer, newLine );
}
_RealWriteConsole( consoleHandle, output );
}
else
{
sm_colorWriter.Write( consoleHandle, output, newLine );
}
}// end WriteConsole()

Просмотреть файл

@ -46,6 +46,7 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>..\bin\Release\x64\</OutputPath>
@ -57,6 +58,7 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<!--
<PropertyGroup>