Add UTF-8 detection with ASCII fallback for terminal compatibility

- Detect Windows Terminal (WT_SESSION), PowerShell, and non-Windows
  terminals to enable UTF-8 output encoding automatically
- Use ★ and → when UTF-8 is supported, fall back to * and -> on cmd.exe
- Set Console.OutputEncoding = UTF8 at startup for capable terminals
This commit is contained in:
Samuel Bouchet 2026-03-14 20:46:33 +01:00
parent af13380a26
commit 240989e0ff
3 changed files with 96 additions and 14 deletions

View file

@ -35,6 +35,8 @@ public static class Program
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
UnicodeSupport.Initialize();
// --snapshot N: directly load snapshot_N save and start playing // --snapshot N: directly load snapshot_N save and start playing
int snapshotSlot = 0; int snapshotSlot = 0;
var snapshotIdx = Array.IndexOf(args, "--snapshot"); var snapshotIdx = Array.IndexOf(args, "--snapshot");
@ -521,8 +523,8 @@ public static class Program
case ResourceChangedEvent resEvt: case ResourceChangedEvent resEvt:
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}"); var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} -> {resEvt.NewValue}"); _renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
AddEventLog($"{resName}: {resEvt.OldValue} -> {resEvt.NewValue}"); AddEventLog($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
break; break;
case MessageEvent msgEvt: case MessageEvent msgEvt:
@ -758,8 +760,8 @@ public static class Program
? _loc.Get("inventory.item_used_qty", itemName, remaining.ToString()) ? _loc.Get("inventory.item_used_qty", itemName, remaining.ToString())
: _loc.Get("inventory.item_used", itemName); : _loc.Get("inventory.item_used", itemName);
_renderer.ShowMessage(usedMsg); _renderer.ShowMessage(usedMsg);
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} -> {resEvt.NewValue}"); _renderer.ShowMessage($"{resName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
AddEventLog($"{itemName} -> {resName} {resEvt.OldValue}->{resEvt.NewValue}"); AddEventLog($"{itemName} {UnicodeSupport.Arrow} {resName} {resEvt.OldValue}{UnicodeSupport.Arrow}{resEvt.NewValue}");
break; break;
case MessageEvent msgEvt: case MessageEvent msgEvt:
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? [])); _renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
@ -784,7 +786,7 @@ public static class Program
break; break;
case ResourceChangedEvent resEvt: case ResourceChangedEvent resEvt:
var cookieResName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}"); var cookieResName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
_renderer.ShowMessage($"{cookieResName}: {resEvt.OldValue} {resEvt.NewValue}"); _renderer.ShowMessage($"{cookieResName}: {resEvt.OldValue} {UnicodeSupport.Arrow} {resEvt.NewValue}");
break; break;
case MessageEvent msgEvt: case MessageEvent msgEvt:
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? [])); _renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));

View file

@ -323,7 +323,8 @@ public sealed class SpectreRenderer : IRenderer
{ {
if (_context.HasColors) if (_context.HasColors)
{ {
var panel = new Panel($"[bold yellow]* {Markup.Escape(featureName)} *[/]") var star = UnicodeSupport.Star;
var panel = new Panel($"[bold yellow]{star} {Markup.Escape(featureName)} {star}[/]")
.Border(BoxBorder.Double) .Border(BoxBorder.Double)
.BorderStyle(new Style(Color.Yellow)) .BorderStyle(new Style(Color.Yellow))
.Padding(2, 0) .Padding(2, 0)
@ -333,7 +334,7 @@ public sealed class SpectreRenderer : IRenderer
else else
{ {
Console.WriteLine("========================================"); Console.WriteLine("========================================");
Console.WriteLine($" * {featureName} *"); Console.WriteLine($" {UnicodeSupport.Star} {featureName} {UnicodeSupport.Star}");
Console.WriteLine("========================================"); Console.WriteLine("========================================");
} }
} }
@ -429,14 +430,18 @@ public sealed class SpectreRenderer : IRenderer
/// <summary> /// <summary>
/// Returns rarity star prefix for Rare and above items. /// Returns rarity star prefix for Rare and above items.
/// </summary> /// </summary>
private static string RarityStars(string rarity) => rarity.ToLowerInvariant() switch private static string RarityStars(string rarity)
{ {
"rare" => "* ", var s = UnicodeSupport.Star;
"epic" => "** ", return rarity.ToLowerInvariant() switch
"legendary" => "*** ", {
"mythic" => "**** ", "rare" => $"{s} ",
_ => "" "epic" => $"{s}{s} ",
}; "legendary" => $"{s}{s}{s} ",
"mythic" => $"{s}{s}{s}{s} ",
_ => ""
};
}
private static Color RarityColorValue(string rarity) => rarity.ToLowerInvariant() switch private static Color RarityColorValue(string rarity) => rarity.ToLowerInvariant() switch
{ {

View file

@ -0,0 +1,75 @@
using System.Text;
namespace OpenTheBox.Rendering;
/// <summary>
/// Detects whether the terminal supports UTF-8 output and provides
/// glyph accessors that return Unicode or ASCII equivalents accordingly.
/// Call <see cref="Initialize"/> once at startup.
/// </summary>
public static class UnicodeSupport
{
/// <summary>True when the terminal accepts UTF-8 characters beyond CP437.</summary>
public static bool IsUtf8 { get; private set; }
// ── Glyphs ──────────────────────────────────────────────────────────
/// <summary>Arrow: → or -></summary>
public static string Arrow => IsUtf8 ? "→" : "->";
/// <summary>Star for rarity prefix: ★ or *</summary>
public static string Star => IsUtf8 ? "★" : "*";
/// <summary>
/// Attempts to set UTF-8 output encoding and detects whether the
/// terminal actually supports it (Windows Terminal, PowerShell, etc.).
/// </summary>
public static void Initialize()
{
// Windows Terminal always sets WT_SESSION
if (Environment.GetEnvironmentVariable("WT_SESSION") is not null)
{
EnableUtf8();
return;
}
// Modern PowerShell (pwsh) or Windows PowerShell with PSModulePath
// typically supports UTF-8 when we set the encoding
if (Environment.GetEnvironmentVariable("PSModulePath") is not null)
{
EnableUtf8();
return;
}
// Non-Windows platforms (Linux, macOS) generally support UTF-8
if (!OperatingSystem.IsWindows())
{
EnableUtf8();
return;
}
// Fallback: try to set UTF-8 and check if it stuck
try
{
Console.OutputEncoding = Encoding.UTF8;
IsUtf8 = Console.OutputEncoding.CodePage == 65001;
}
catch
{
IsUtf8 = false;
}
}
private static void EnableUtf8()
{
try
{
Console.OutputEncoding = Encoding.UTF8;
}
catch
{
// Ignore if setting encoding fails
}
IsUtf8 = true;
}
}