CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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<string, string> Lines = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, FileIndex> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user