using System.Text; using System.Text.RegularExpressions; namespace CVRManager.Services; public static class MlLogColours { const string Esc = "\u001b["; const string Reset = "\u001b[0m"; const long MaxScan = 16L << 20; static readonly Regex Sgr = new(@"\u001b\[([0-9;]*)m", RegexOptions.Compiled); static readonly Regex AnyEsc = new(@"\u001b\[[0-9;?]*[A-Za-z]", RegexOptions.Compiled); static readonly Regex Stamp = new(@"^\[(\d\d:\d\d:\d\d\.\d{3})\] (.*)$", RegexOptions.Compiled); sealed class FileIndex { public long Offset; public string? State; public readonly Dictionary Lines = new(StringComparer.Ordinal); } static readonly Dictionary Files = new(StringComparer.OrdinalIgnoreCase); static readonly object Gate = new(); static long version; public static long Refresh(params string[] paths) { lock (Gate) { foreach (var path in paths) Scan(path); return version; } } static void Scan(string path) { if (!Files.TryGetValue(path, out var index)) Files[path] = index = new FileIndex(); try { if (!File.Exists(path)) { if (index.Offset != 0 || index.Lines.Count > 0) { Files[path] = new FileIndex(); version++; } return; } using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); var length = fs.Length; if (length < index.Offset) { Files[path] = index = new FileIndex(); version++; } if (length == index.Offset) return; var start = Math.Max(index.Offset, length - MaxScan); var buffer = new byte[length - start]; fs.Seek(start, SeekOrigin.Begin); fs.ReadExactly(buffer); var end = Array.LastIndexOf(buffer, (byte)'\n'); if (end < 0) return; var text = Encoding.UTF8.GetString(buffer, 0, end + 1); if (start > index.Offset) index.State = null; index.Offset = start + end + 1; foreach (var raw in text.Split('\n')) Add(index, raw.TrimEnd('\r')); version++; } catch (Exception e) when (e is IOException or UnauthorizedAccessException) { } } static void Add(FileIndex index, string line) { var hasEsc = line.IndexOf('\u001b') >= 0; if (!hasEsc && index.State == null) return; var styled = index.State == null ? line : Esc + index.State + "m" + line; foreach (Match m in Sgr.Matches(line)) { var codes = m.Groups[1].Value; index.State = codes is "" or "0" or "39" ? null : codes; } if (index.State != null) styled += Reset; var plain = AnyEsc.Replace(line, ""); if (plain.Trim().Length > 0) index.Lines[plain] = styled; } public static string Colour(string mlText, params string[] paths) { var sb = new StringBuilder(mlText.Length * 2); lock (Gate) { foreach (var raw in mlText.Split('\n')) { var line = raw.TrimEnd('\r'); string? styled = null; for (var i = paths.Length - 1; i >= 0 && styled == null; i--) if (Files.TryGetValue(paths[i], out var index)) index.Lines.TryGetValue(line, out styled); if (styled == null && Stamp.Match(line) is { Success: true } m) styled = $"{Esc}38;2;211;211;211m[{Reset}{Esc}38;2;50;205;50m{m.Groups[1].Value}{Reset}{Esc}38;2;211;211;211m] {m.Groups[2].Value}{Reset}"; sb.Append(styled ?? line).Append('\n'); } } if (sb.Length > 0 && !mlText.EndsWith('\n')) sb.Length--; return sb.ToString(); } }