// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.IO; namespace SharpEmu.Logging; public sealed class ConsoleLogSink : ISharpEmuLogSink { private readonly object _sync = new(); public ConsoleLogSink(bool useColors = true, bool includeTimestamp = false) { UseColors = useColors; IncludeTimestamp = includeTimestamp; } public bool UseColors { get; set; } public bool IncludeTimestamp { get; set; } public void Write(in LogEntry entry) { lock (_sync) { var writer = entry.Level >= LogLevel.Error ? Console.Error : Console.Out; if (IncludeTimestamp) { writer.Write('['); writer.Write(entry.Timestamp.ToString("HH:mm:ss.fff")); writer.Write(']'); } var levelLabel = ToLevelLabel(entry.Level); WriteLevelSegment(writer, levelLabel, entry.Level); writer.Write('['); writer.Write(entry.Category); writer.Write(']'); writer.Write(' '); writer.Write(entry.SourceFileName); if (entry.SourceLine > 0) { writer.Write(':'); writer.Write(entry.SourceLine); } writer.Write(' '); WriteMessageSegment(writer, entry.Message); writer.WriteLine(); if (entry.Exception is not null) { writer.WriteLine(entry.Exception); } } } private static string ToLevelLabel(LogLevel level) { return level switch { LogLevel.Trace => "TRACE", LogLevel.Debug => "DEBUG", LogLevel.Info => "INFO", LogLevel.Warning => "WARNING", LogLevel.Error => "ERROR", LogLevel.Critical => "CRITICAL", _ => "LOG", }; } private void WriteLevelSegment(TextWriter writer, string label, LogLevel level) { if (!UseColors) { writer.Write('['); writer.Write(label); writer.Write(']'); return; } var originalColor = Console.ForegroundColor; Console.ForegroundColor = GetLevelColor(level); writer.Write('['); writer.Write(label); writer.Write(']'); Console.ForegroundColor = originalColor; } private static ConsoleColor GetLevelColor(LogLevel level) { return level switch { LogLevel.Trace => ConsoleColor.DarkGray, LogLevel.Debug => ConsoleColor.Gray, LogLevel.Info => ConsoleColor.Blue, LogLevel.Warning => ConsoleColor.Yellow, LogLevel.Error => ConsoleColor.Red, LogLevel.Critical => ConsoleColor.DarkRed, _ => ConsoleColor.White, }; } private void WriteMessageSegment(TextWriter writer, string message) { if (!UseColors || !TryGetMessageColor(message, out var messageColor)) { writer.Write(message); return; } var originalColor = Console.ForegroundColor; Console.ForegroundColor = messageColor; writer.Write(message); Console.ForegroundColor = originalColor; } private static bool TryGetMessageColor(string message, out ConsoleColor color) { if (string.IsNullOrWhiteSpace(message)) { color = default; return false; } if (ContainsIgnoreCase(message, "unresolved import") || ContainsIgnoreCase(message, "unresolved symbol") || ContainsIgnoreCase(message, "hot_unresolved_imports=") || ContainsIgnoreCase(message, "unresolved_imports_hit=")) { color = ConsoleColor.Red; return true; } if (ContainsIgnoreCase(message, "syscall") || ContainsIgnoreCase(message, "UnhandledSyscall")) { color = ConsoleColor.DarkYellow; return true; } if (ContainsIgnoreCase(message, "Import trace") || ContainsIgnoreCase(message, "import_stub_return") || ContainsIgnoreCase(message, "last_import=") || ContainsIgnoreCase(message, "hot_imports=") || ContainsIgnoreCase(message, "resolved import")) { color = ConsoleColor.Blue; return true; } color = default; return false; } private static bool ContainsIgnoreCase(string text, string value) { return text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0; } }