generalise console color logic for reuse (#495)

This commit is contained in:
Jesse Plamondon-Willard 2018-05-10 00:47:20 -04:00
parent 5a2755bfcc
commit 02c02a55ee
9 changed files with 196 additions and 129 deletions

View File

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
namespace StardewModdingAPI.Internal.ConsoleWriting
{
/// <summary>Provides a wrapper for writing color-coded text to the console.</summary>
internal class ColorfulConsoleWriter
{
/*********
** Properties
*********/
/// <summary>The console text color for each log level.</summary>
private readonly IDictionary<ConsoleLogLevel, ConsoleColor> Colors;
/// <summary>Whether the current console supports color formatting.</summary>
private readonly bool SupportsColor;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="platform">The target platform.</param>
/// <param name="colorScheme">The console color scheme to use.</param>
public ColorfulConsoleWriter(Platform platform, MonitorColorScheme colorScheme)
{
this.SupportsColor = this.TestColorSupport();
this.Colors = this.GetConsoleColorScheme(platform, colorScheme);
}
/// <summary>Write a message line to the log.</summary>
/// <param name="message">The message to log.</param>
/// <param name="level">The log level.</param>
public void WriteLine(string message, ConsoleLogLevel level)
{
if (this.SupportsColor)
{
if (level == ConsoleLogLevel.Critical)
{
Console.BackgroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(message);
Console.ResetColor();
}
else
{
Console.ForegroundColor = this.Colors[level];
Console.WriteLine(message);
Console.ResetColor();
}
}
else
Console.WriteLine(message);
}
/*********
** Private methods
*********/
/// <summary>Test whether the current console supports color formatting.</summary>
private bool TestColorSupport()
{
try
{
Console.ForegroundColor = Console.ForegroundColor;
return true;
}
catch (Exception)
{
return false; // Mono bug
}
}
/// <summary>Get the color scheme to use for the current console.</summary>
/// <param name="platform">The target platform.</param>
/// <param name="colorScheme">The console color scheme to use.</param>
private IDictionary<ConsoleLogLevel, ConsoleColor> GetConsoleColorScheme(Platform platform, MonitorColorScheme colorScheme)
{
// auto detect color scheme
if (colorScheme == MonitorColorScheme.AutoDetect)
{
colorScheme = platform == Platform.Mac
? MonitorColorScheme.LightBackground // MacOS doesn't provide console background color info, but it's usually white.
: ColorfulConsoleWriter.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground;
}
// get colors for scheme
switch (colorScheme)
{
case MonitorColorScheme.DarkBackground:
return new Dictionary<ConsoleLogLevel, ConsoleColor>
{
[ConsoleLogLevel.Trace] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Debug] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Info] = ConsoleColor.White,
[ConsoleLogLevel.Warn] = ConsoleColor.Yellow,
[ConsoleLogLevel.Error] = ConsoleColor.Red,
[ConsoleLogLevel.Alert] = ConsoleColor.Magenta
};
case MonitorColorScheme.LightBackground:
return new Dictionary<ConsoleLogLevel, ConsoleColor>
{
[ConsoleLogLevel.Trace] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Debug] = ConsoleColor.DarkGray,
[ConsoleLogLevel.Info] = ConsoleColor.Black,
[ConsoleLogLevel.Warn] = ConsoleColor.DarkYellow,
[ConsoleLogLevel.Error] = ConsoleColor.Red,
[ConsoleLogLevel.Alert] = ConsoleColor.DarkMagenta
};
default:
throw new NotSupportedException($"Unknown color scheme '{colorScheme}'.");
}
}
/// <summary>Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'.</summary>
/// <param name="color">The color to check.</param>
private static bool IsDark(ConsoleColor color)
{
switch (color)
{
case ConsoleColor.Black:
case ConsoleColor.Blue:
case ConsoleColor.DarkBlue:
case ConsoleColor.DarkMagenta: // Powershell
case ConsoleColor.DarkRed:
case ConsoleColor.Red:
return true;
default:
return false;
}
}
}
}

View File

@ -0,0 +1,27 @@
namespace StardewModdingAPI.Internal.ConsoleWriting
{
/// <summary>The log severity levels.</summary>
internal enum ConsoleLogLevel
{
/// <summary>Tracing info intended for developers.</summary>
Trace,
/// <summary>Troubleshooting info that may be relevant to the player.</summary>
Debug,
/// <summary>Info relevant to the player. This should be used judiciously.</summary>
Info,
/// <summary>An issue the player should be aware of. This should be used rarely.</summary>
Warn,
/// <summary>A message indicating something went wrong.</summary>
Error,
/// <summary>Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue.</summary>
Alert,
/// <summary>A critical issue that generally signals an immediate end to the application.</summary>
Critical
}
}

View File

@ -1,4 +1,4 @@
namespace StardewModdingAPI.Framework.Models
namespace StardewModdingAPI.Internal.ConsoleWriting
{
/// <summary>A monitor color scheme to use.</summary>
internal enum MonitorColorScheme

View File

@ -9,9 +9,12 @@
<Import_RootNamespace>SMAPI.Internal</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ColorfulConsoleWriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EnvironmentUtility.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\LogLevel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\ModInfoModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\ModSeachModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\MonitorColorScheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Platform.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SemanticVersionImpl.cs" />
</ItemGroup>

View File

@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.Logging
/*********
** Accessors
*********/
/// <summary>Whether the current console supports color formatting.</summary>
public bool SupportsColor { get; }
/// <summary>The event raised when a message is written to the console directly.</summary>
public event Action<string> OnMessageIntercepted;
@ -32,9 +29,6 @@ namespace StardewModdingAPI.Framework.Logging
this.Output = new InterceptingTextWriter(Console.Out);
this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line);
Console.SetOut(this.Output);
// test color support
this.SupportsColor = this.TestColorSupport();
}
/// <summary>Get an exclusive lock and write to the console output without interception.</summary>
@ -61,26 +55,5 @@ namespace StardewModdingAPI.Framework.Logging
Console.SetOut(this.Output.Out);
this.Output.Dispose();
}
/*********
** private methods
*********/
/// <summary>Test whether the current console supports color formatting.</summary>
private bool TestColorSupport()
{
try
{
this.ExclusiveWriteWithoutInterception(() =>
{
Console.ForegroundColor = Console.ForegroundColor;
});
return true;
}
catch (Exception)
{
return false; // Mono bug
}
}
}
}

View File

@ -1,3 +1,5 @@
using StardewModdingAPI.Internal.ConsoleWriting;
namespace StardewModdingAPI.Framework.Models
{
/// <summary>The SMAPI configuration settings.</summary>

View File

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using StardewModdingAPI.Framework.Logging;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Internal.ConsoleWriting;
namespace StardewModdingAPI.Framework
{
@ -17,8 +15,11 @@ namespace StardewModdingAPI.Framework
/// <summary>The name of the module which logs messages using this instance.</summary>
private readonly string Source;
/// <summary>Handles writing color-coded text to the console.</summary>
private readonly ColorfulConsoleWriter ConsoleWriter;
/// <summary>Manages access to the console output.</summary>
private readonly ConsoleInterceptionManager ConsoleManager;
private readonly ConsoleInterceptionManager ConsoleInterceptor;
/// <summary>The log file to which to write messages.</summary>
private readonly LogFileManager LogFile;
@ -26,9 +27,6 @@ namespace StardewModdingAPI.Framework
/// <summary>The maximum length of the <see cref="LogLevel"/> values.</summary>
private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>() select level.ToString().Length).Max();
/// <summary>The console text color for each log level.</summary>
private readonly IDictionary<LogLevel, ConsoleColor> Colors;
/// <summary>Propagates notification that SMAPI should exit.</summary>
private readonly CancellationTokenSource ExitTokenSource;
@ -54,21 +52,21 @@ namespace StardewModdingAPI.Framework
*********/
/// <summary>Construct an instance.</summary>
/// <param name="source">The name of the module which logs messages using this instance.</param>
/// <param name="consoleManager">Manages access to the console output.</param>
/// <param name="consoleInterceptor">Intercepts access to the console output.</param>
/// <param name="logFile">The log file to which to write messages.</param>
/// <param name="exitTokenSource">Propagates notification that SMAPI should exit.</param>
/// <param name="colorScheme">The console color scheme to use.</param>
public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme)
public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme)
{
// validate
if (string.IsNullOrWhiteSpace(source))
throw new ArgumentException("The log source cannot be empty.");
// initialise
this.Colors = Monitor.GetConsoleColorScheme(colorScheme);
this.Source = source;
this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null.");
this.ConsoleManager = consoleManager;
this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme);
this.ConsoleInterceptor = consoleInterceptor;
this.ExitTokenSource = exitTokenSource;
}
@ -77,7 +75,7 @@ namespace StardewModdingAPI.Framework
/// <param name="level">The log severity level.</param>
public void Log(string message, LogLevel level = LogLevel.Debug)
{
this.LogImpl(this.Source, message, level, this.Colors[level]);
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
}
/// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
@ -92,7 +90,7 @@ namespace StardewModdingAPI.Framework
internal void Newline()
{
if (this.WriteToConsole)
this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine);
this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine);
this.LogFile.WriteLine("");
}
@ -101,7 +99,7 @@ namespace StardewModdingAPI.Framework
internal void LogUserInput(string input)
{
// user input already appears in the console, so just need to write to file
string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info);
string prefix = this.GenerateMessagePrefix(this.Source, (ConsoleLogLevel)LogLevel.Info);
this.LogFile.WriteLine($"{prefix} $>{input}");
}
@ -113,16 +111,14 @@ namespace StardewModdingAPI.Framework
/// <param name="message">The message to log.</param>
private void LogFatal(string message)
{
this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red);
this.LogImpl(this.Source, message, ConsoleLogLevel.Critical);
}
/// <summary>Write a message line to the log.</summary>
/// <param name="source">The name of the mod logging the message.</param>
/// <param name="message">The message to log.</param>
/// <param name="level">The log level.</param>
/// <param name="color">The console foreground color.</param>
/// <param name="background">The console background color (or <c>null</c> to leave it as-is).</param>
private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null)
private void LogImpl(string source, string message, ConsoleLogLevel level)
{
// generate message
string prefix = this.GenerateMessagePrefix(source, level);
@ -130,20 +126,11 @@ namespace StardewModdingAPI.Framework
string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}";
// write to console
if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace))
if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace))
{
this.ConsoleManager.ExclusiveWriteWithoutInterception(() =>
this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() =>
{
if (this.ConsoleManager.SupportsColor)
{
if (background.HasValue)
Console.BackgroundColor = background.Value;
Console.ForegroundColor = color;
Console.WriteLine(consoleMessage);
Console.ResetColor();
}
else
Console.WriteLine(consoleMessage);
this.ConsoleWriter.WriteLine(consoleMessage, level);
});
}
@ -154,72 +141,10 @@ namespace StardewModdingAPI.Framework
/// <summary>Generate a message prefix for the current time.</summary>
/// <param name="source">The name of the mod logging the message.</param>
/// <param name="level">The log level.</param>
private string GenerateMessagePrefix(string source, LogLevel level)
private string GenerateMessagePrefix(string source, ConsoleLogLevel level)
{
string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength);
return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]";
}
/// <summary>Get the color scheme to use for the current console.</summary>
/// <param name="colorScheme">The console color scheme to use.</param>
private static IDictionary<LogLevel, ConsoleColor> GetConsoleColorScheme(MonitorColorScheme colorScheme)
{
// auto detect color scheme
if (colorScheme == MonitorColorScheme.AutoDetect)
{
if (Constants.Platform == Platform.Mac)
colorScheme = MonitorColorScheme.LightBackground; // MacOS doesn't provide console background color info, but it's usually white.
else
colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground;
}
// get colors for scheme
switch (colorScheme)
{
case MonitorColorScheme.DarkBackground:
return new Dictionary<LogLevel, ConsoleColor>
{
[LogLevel.Trace] = ConsoleColor.DarkGray,
[LogLevel.Debug] = ConsoleColor.DarkGray,
[LogLevel.Info] = ConsoleColor.White,
[LogLevel.Warn] = ConsoleColor.Yellow,
[LogLevel.Error] = ConsoleColor.Red,
[LogLevel.Alert] = ConsoleColor.Magenta
};
case MonitorColorScheme.LightBackground:
return new Dictionary<LogLevel, ConsoleColor>
{
[LogLevel.Trace] = ConsoleColor.DarkGray,
[LogLevel.Debug] = ConsoleColor.DarkGray,
[LogLevel.Info] = ConsoleColor.Black,
[LogLevel.Warn] = ConsoleColor.DarkYellow,
[LogLevel.Error] = ConsoleColor.Red,
[LogLevel.Alert] = ConsoleColor.DarkMagenta
};
default:
throw new NotSupportedException($"Unknown color scheme '{colorScheme}'.");
}
}
/// <summary>Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'.</summary>
/// <param name="color">The color to check.</param>
private static bool IsDark(ConsoleColor color)
{
switch (color)
{
case ConsoleColor.Black:
case ConsoleColor.Blue:
case ConsoleColor.DarkBlue:
case ConsoleColor.DarkMagenta: // Powershell
case ConsoleColor.DarkRed:
case ConsoleColor.Red:
return true;
default:
return false;
}
}
}
}

View File

@ -1,24 +1,26 @@
using StardewModdingAPI.Internal.ConsoleWriting;
namespace StardewModdingAPI
{
/// <summary>The log severity levels.</summary>
public enum LogLevel
{
/// <summary>Tracing info intended for developers.</summary>
Trace,
Trace = ConsoleLogLevel.Trace,
/// <summary>Troubleshooting info that may be relevant to the player.</summary>
Debug,
Debug = ConsoleLogLevel.Debug,
/// <summary>Info relevant to the player. This should be used judiciously.</summary>
Info,
Info = ConsoleLogLevel.Info,
/// <summary>An issue the player should be aware of. This should be used rarely.</summary>
Warn,
Warn = ConsoleLogLevel.Warn,
/// <summary>A message indicating something went wrong.</summary>
Error,
Error = ConsoleLogLevel.Error,
/// <summary>Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue.</summary>
Alert
Alert = ConsoleLogLevel.Alert
}
}

View File

@ -125,7 +125,6 @@
<Compile Include="Framework\SContentManager.cs" />
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
<Compile Include="Framework\Models\MonitorColorScheme.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyFactory.cs" />
<Compile Include="Framework\RewriteFacades\SpriteBatchMethods.cs" />