Skip to content

Commit

Permalink
use ANSI escape codes to control colours of parameters we print
Browse files Browse the repository at this point in the history
  • Loading branch information
cfbao committed Jul 23, 2024
1 parent b1d537c commit 2f7848c
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 7 deletions.
21 changes: 14 additions & 7 deletions src/D2L.Bmx/ConsoleWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ internal interface IConsoleWriter {
void WriteParameter( string description, string value, ParameterSource source );
}

// We use ANSI escape codes to control colours, because .NET's `Console.ForegroundColor` only targets stdout,
// if stdout is redirected (e.g. typical use case for `bmx print`), we won't get any coloured text on stderr.
// https://github.com/dotnet/runtime/issues/83146
internal class ConsoleWriter : IConsoleWriter {
// .NET runtime subscribes to the informal standard from https://no-color.org/. We should too.
// https://github.com/dotnet/runtime/blob/v9.0.0-preview.6.24327.7/src/libraries/Common/src/System/Console/ConsoleUtils.cs#L32-L34
private static readonly bool _noColor = Environment.GetEnvironmentVariable( "NO_COLOR" ) == "1";

void IConsoleWriter.WriteParameter( string description, string value, ParameterSource source ) {
Console.ResetColor();
Console.Error.Write( $"{description}: " );
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Error.Write( value );
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Error.WriteLine( $" (from {source})" );
Console.ResetColor();
if( _noColor || !VirtualTerminal.TryEnableOnStderr() ) {
Console.Error.WriteLine( $"{description}: {value} (from {source})" );
}
// description: default
// value: bright cyan
// source: grey / bright black
Console.Error.WriteLine( $"\x1b[0m{description}: \x1b[96m{value} \x1b[90m(from {source})\x1b[0m" );
}
}
54 changes: 54 additions & 0 deletions src/D2L.Bmx/VirtualTerminal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Runtime.InteropServices;

namespace D2L.Bmx;

// The code here exists because legacy Windows console doesn't support ANSI escape codes by default.
// We can consider deleting everything here once Windows 10 is out of support and
// Windows Terminal becomes the default console on all supported Windows versions.
internal static partial class VirtualTerminal {
private const string Kernel32 = "kernel32.dll";
private const int STD_ERROR_HANDLE = -12;
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;

private static bool _enablingAttempted = false;
private static bool _enabled = false;

// https://learn.microsoft.com/en-us/windows/console/classic-vs-vt
public static bool TryEnableOnStderr() {
if( _enablingAttempted ) {
return _enabled;
}
_enablingAttempted = true;

if( !RuntimeInformation.IsOSPlatform( OSPlatform.Windows ) ) {
// POSIX systems all support ANSI escape codes
_enabled = true;
return true;
}
nint stderrHandle = GetStdHandle( STD_ERROR_HANDLE );
if( !GetConsoleMode( stderrHandle, out int mode ) ) {
_enabled = false;
return false;
}
if( ( mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING ) == ENABLE_VIRTUAL_TERMINAL_PROCESSING ) {
_enabled = true;
return true;
}
_enabled = SetConsoleMode( stderrHandle, (int)( mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING ) );
return _enabled;
}

// https://learn.microsoft.com/en-us/windows/console/getstdhandle
[LibraryImport( Kernel32 )]
private static partial IntPtr GetStdHandle( int nStdHandle );

// https://learn.microsoft.com/en-us/windows/console/getconsolemode
[LibraryImport( Kernel32 )]
[return: MarshalAs( UnmanagedType.Bool )]
private static partial bool GetConsoleMode( IntPtr handle, out int mode );

// https://learn.microsoft.com/en-us/windows/console/setconsolemode
[LibraryImport( Kernel32 )]
[return: MarshalAs( UnmanagedType.Bool )]
private static partial bool SetConsoleMode( IntPtr handle, int mode );
}

0 comments on commit 2f7848c

Please sign in to comment.