From 71c58d10efb6ac9a406e0a539ecb583ee2dcaf7a Mon Sep 17 00:00:00 2001 From: Chenfeng Bao Date: Tue, 23 Jul 2024 17:22:38 -0400 Subject: [PATCH] use ANSI escape codes to control all console text colours --- src/D2L.Bmx/ConsoleWriter.cs | 36 +++++++++++++++++++++++++++++++- src/D2L.Bmx/OktaAuthenticator.cs | 9 ++++---- src/D2L.Bmx/Program.cs | 12 +++++------ src/D2L.Bmx/UpdateChecker.cs | 25 ++-------------------- src/D2L.Bmx/WriteHandler.cs | 2 +- 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/D2L.Bmx/ConsoleWriter.cs b/src/D2L.Bmx/ConsoleWriter.cs index c516e47f..66d531f0 100644 --- a/src/D2L.Bmx/ConsoleWriter.cs +++ b/src/D2L.Bmx/ConsoleWriter.cs @@ -2,11 +2,16 @@ namespace D2L.Bmx; internal interface IConsoleWriter { void WriteParameter( string description, string value, ParameterSource source ); + void WriteUpdateMessage( string text ); + void WriteWarning( string text ); + void WriteError( string text ); } // 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 +// See https://github.com/dotnet/runtime/issues/83146. +// Furthermore, ANSI escape codes give us greater control over the spread of custom background colour. +// TODO: use a library to manage ANSI codes and NO_COLOR? 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 @@ -21,4 +26,33 @@ void IConsoleWriter.WriteParameter( string description, string value, ParameterS // source: grey / bright black Console.Error.WriteLine( $"\x1b[0m{description}: \x1b[96m{value} \x1b[90m(from {source})\x1b[0m" ); } + + void IConsoleWriter.WriteUpdateMessage( string text ) { + if( _noColor || !VirtualTerminal.TryEnableOnStderr() ) { + Console.Error.WriteLine( text ); + } + string[] lines = text.Split( '\n' ); + int maxLineLength = lines.Max( l => l.Length ); + foreach( string line in lines ) { + string paddedLine = line.PadRight( maxLineLength ); + Console.Error.WriteLine( $"\x1b[0m\x1b[30;47m{paddedLine}\x1b[0m" ); + } + Console.Error.WriteLine(); + } + + void IConsoleWriter.WriteWarning( string text ) { + if( _noColor || !VirtualTerminal.TryEnableOnStderr() ) { + Console.Error.WriteLine( text ); + } + // bright yellow - 93 + Console.Error.WriteLine( $"\x1b[0m\x1b[93m{text}\x1b[0m" ); + } + + void IConsoleWriter.WriteError( string text ) { + if( _noColor || !VirtualTerminal.TryEnableOnStderr() ) { + Console.Error.WriteLine( text ); + } + // bright red - 91 + Console.Error.WriteLine( $"\x1b[0m\x1b[91m{text}\x1b[0m" ); + } } diff --git a/src/D2L.Bmx/OktaAuthenticator.cs b/src/D2L.Bmx/OktaAuthenticator.cs index b82ae026..8d480164 100644 --- a/src/D2L.Bmx/OktaAuthenticator.cs +++ b/src/D2L.Bmx/OktaAuthenticator.cs @@ -91,11 +91,10 @@ mfaFactor is OktaMfaQuestionFactor // Security question factor is a static value if( File.Exists( BmxPaths.CONFIG_FILE_NAME ) ) { CacheOktaSession( user, org, sessionResp.Id, sessionResp.ExpiresAt ); } else { - Console.ResetColor(); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Error.WriteLine( "No config file found. Your Okta session will not be cached. " + - "Consider running `bmx configure` if you own this machine." ); - Console.ResetColor(); + consoleWriter.WriteWarning( + "No config file found. Your Okta session will not be cached. " + + "Consider running `bmx configure` if you own this machine." + ); } return new AuthenticatedOktaApi( Org: org, User: user, Api: oktaApi ); } diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 1b4b8065..3ae8fa3f 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -246,7 +246,7 @@ } if( context.ParseResult.CommandResult.Command != updateCommand ) { - var updateChecker = new UpdateChecker( new GitHubClient(), new VersionProvider() ); + var updateChecker = new UpdateChecker( new GitHubClient(), new VersionProvider(), new ConsoleWriter() ); await updateChecker.CheckForUpdatesAsync(); } @@ -258,17 +258,15 @@ order: MiddlewareOrder.ExceptionHandler + 1 ) .UseExceptionHandler( ( exception, context ) => { - Console.ResetColor(); - Console.ForegroundColor = ConsoleColor.Red; + IConsoleWriter consoleWriter = new ConsoleWriter(); if( exception is BmxException ) { - Console.Error.WriteLine( exception.Message ); + consoleWriter.WriteError( exception.Message ); } else { - Console.Error.WriteLine( "BMX exited with unexpected internal error" ); + consoleWriter.WriteError( "BMX exited with unexpected internal error" ); } if( Environment.GetEnvironmentVariable( "BMX_DEBUG" ) == "1" ) { - Console.Error.WriteLine( exception ); + consoleWriter.WriteError( exception.ToString() ); } - Console.ResetColor(); } ) .Build() .InvokeAsync( args ); diff --git a/src/D2L.Bmx/UpdateChecker.cs b/src/D2L.Bmx/UpdateChecker.cs index bb04c41d..2703a5b4 100644 --- a/src/D2L.Bmx/UpdateChecker.cs +++ b/src/D2L.Bmx/UpdateChecker.cs @@ -5,7 +5,7 @@ namespace D2L.Bmx; -internal class UpdateChecker( IGitHubClient github, IVersionProvider versionProvider ) { +internal class UpdateChecker( IGitHubClient github, IVersionProvider versionProvider, IConsoleWriter consoleWriter ) { public async Task CheckForUpdatesAsync() { try { var updateCheckCache = GetUpdateCheckCacheOrNull(); @@ -29,7 +29,7 @@ public async Task CheckForUpdatesAsync() { Version? localVersion = versionProvider.GetAssemblyVersion(); if( latestVersion > localVersion ) { string? displayVersion = versionProvider.GetInformationalVersion() ?? localVersion.ToString(); - DisplayUpdateMessage( + consoleWriter.WriteUpdateMessage( $""" A new BMX release is available: v{latestVersion} (current: {displayVersion}) Run "bmx update" now to upgrade. @@ -41,27 +41,6 @@ public async Task CheckForUpdatesAsync() { } } - private static void DisplayUpdateMessage( string message ) { - var originalBackgroundColor = Console.BackgroundColor; - var originalForegroundColor = Console.ForegroundColor; - - Console.BackgroundColor = ConsoleColor.Gray; - Console.ForegroundColor = ConsoleColor.Black; - - string[] lines = message.Split( "\n" ); - int consoleWidth = Console.WindowWidth; - - foreach( string line in lines ) { - Console.Error.Write( line.PadRight( consoleWidth ) ); - Console.Error.WriteLine(); - } - - Console.BackgroundColor = originalBackgroundColor; - Console.ForegroundColor = originalForegroundColor; - Console.ResetColor(); - Console.Error.WriteLine(); - } - private static void SetUpdateCheckCache( Version version ) { var cache = new UpdateCheckCache( VersionName: version, diff --git a/src/D2L.Bmx/WriteHandler.cs b/src/D2L.Bmx/WriteHandler.cs index 6c4886a0..ceb135d3 100644 --- a/src/D2L.Bmx/WriteHandler.cs +++ b/src/D2L.Bmx/WriteHandler.cs @@ -118,7 +118,7 @@ bool useCredentialProcess } parser.WriteFile( awsConfigFilePath, awsConfig, Utf8 ); if( foundCredentialProcess ) { - Console.WriteLine( + consoleWriter.WriteWarning( """ An existing profile with the same name using the `credential_process` setting was found in the default config file. The setting has been removed, and static credentials will be used for the profile.