diff --git a/src/EchoHub.Client/UI/ChatRenderer.cs b/src/EchoHub.Client/UI/ChatRenderer.cs index 29b4284..d229ca6 100644 --- a/src/EchoHub.Client/UI/ChatRenderer.cs +++ b/src/EchoHub.Client/UI/ChatRenderer.cs @@ -90,22 +90,17 @@ public partial class ChatLine } /// - /// Returns true if a line contains color tags (new format or legacy ANSI). + /// Returns true if a line contains printable color tags. /// public static bool HasColorTags(string text) => - text.Contains("{F:") || text.Contains("{B:") || text.Contains("{X}") || text.Contains('\x1b'); + text.Contains("{F:") || text.Contains("{B:") || text.Contains("{X}"); /// - /// Parse a string containing color tags into colored segments. - /// Supports the new printable format ({F:RRGGBB}, {B:RRGGBB}, {X}) - /// and legacy ANSI format (\x1b[38;2;R;G;Bm, \x1b[48;2;R;G;Bm, \x1b[0m). + /// Parse a string containing printable color tags into colored segments. + /// Format: {F:RRGGBB} (foreground), {B:RRGGBB} (background), {X} (reset). /// public static ChatLine FromColoredText(string text, Attribute? defaultAttr = null) { - // Detect which format is used and pick the right regex - var regex = text.Contains('\x1b') ? AnsiColorRegex() : ColorTagRegex(); - bool isAnsi = text.Contains('\x1b'); - var segments = new List(); int lastIndex = 0; Color? currentFg = null; @@ -121,7 +116,7 @@ public partial class ChatLine return new Attribute(fg, bg); } - foreach (Match match in regex.Matches(text)) + foreach (Match match in ColorTagRegex().Matches(text)) { if (match.Index > lastIndex) { @@ -130,45 +125,22 @@ public partial class ChatLine segments.Add(new ChatSegment(t, BuildAttr())); } - if (isAnsi) + if (match.Groups[1].Success) { - // Legacy ANSI format - if (match.Groups[1].Value == "0") - { - currentFg = null; - currentBg = null; - } - else if (match.Groups[2].Success) - { - var r = int.Parse(match.Groups[3].Value); - var g = int.Parse(match.Groups[4].Value); - var b = int.Parse(match.Groups[5].Value); - if (match.Groups[2].Value == "38;2") - currentFg = new Color(r, g, b); - else - currentBg = new Color(r, g, b); - } + // Reset {X} + currentFg = null; + currentBg = null; } - else + else if (match.Groups[2].Success) { - // New printable tag format: {F:RRGGBB}, {B:RRGGBB}, {X} - if (match.Groups[6].Success) - { - // Reset {X} - currentFg = null; - currentBg = null; - } - else if (match.Groups[7].Success) - { - var hex = match.Groups[8].Value; - var r = Convert.ToInt32(hex[..2], 16); - var g = Convert.ToInt32(hex[2..4], 16); - var b = Convert.ToInt32(hex[4..6], 16); - if (match.Groups[7].Value == "F") - currentFg = new Color(r, g, b); - else - currentBg = new Color(r, g, b); - } + var hex = match.Groups[3].Value; + var r = Convert.ToInt32(hex[..2], 16); + var g = Convert.ToInt32(hex[2..4], 16); + var b = Convert.ToInt32(hex[4..6], 16); + if (match.Groups[2].Value == "F") + currentFg = new Color(r, g, b); + else + currentBg = new Color(r, g, b); } lastIndex = match.Index + match.Length; @@ -184,11 +156,7 @@ public partial class ChatLine return segments.Count > 0 ? new ChatLine(segments) : new ChatLine(""); } - // Legacy: \x1b[0m, \x1b[38;2;R;G;Bm, \x1b[48;2;R;G;Bm - [GeneratedRegex(@"\x1b\[(?:(0)|(?:(38;2|48;2);(\d{1,3});(\d{1,3});(\d{1,3})))m")] - private static partial Regex AnsiColorRegex(); - - // New: {X} (reset), {F:RRGGBB} (foreground), {B:RRGGBB} (background) + // {X} (reset), {F:RRGGBB} (foreground), {B:RRGGBB} (background) [GeneratedRegex(@"\{(?:(X)|(?:(F|B):([0-9A-Fa-f]{6})))\}")] private static partial Regex ColorTagRegex(); } diff --git a/src/EchoHub.Server/Setup/DataMigrationService.cs b/src/EchoHub.Server/Setup/DataMigrationService.cs new file mode 100644 index 0000000..55c2095 --- /dev/null +++ b/src/EchoHub.Server/Setup/DataMigrationService.cs @@ -0,0 +1,78 @@ +using System.Text.RegularExpressions; +using EchoHub.Server.Data; +using Microsoft.EntityFrameworkCore; + +namespace EchoHub.Server.Setup; + +public static partial class DataMigrationService +{ + public static async Task RunAsync(IServiceProvider services) + { + using var scope = services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService() + .CreateLogger("EchoHub.Server.Setup.DataMigration"); + + await MigrateAnsiMessagesAsync(db, logger); + } + + private static async Task MigrateAnsiMessagesAsync(EchoHubDbContext db, ILogger logger) + { + // Load messages that contain the ESC byte (0x1B) — these have legacy ANSI color codes. + // Filter by Image type first (only images have ANSI art), then check content in memory. + var messages = await db.Messages + .Where(m => m.Type == Core.Models.MessageType.Image) + .ToListAsync(); + + var toMigrate = messages.Where(m => m.Content.Contains('\x1b')).ToList(); + + if (toMigrate.Count == 0) + return; + + logger.LogInformation("Found {Count} messages with legacy ANSI color codes. Migrating to color tag format...", toMigrate.Count); + + var modified = 0; + foreach (var message in toMigrate) + { + var converted = AnsiToColorTags(message.Content); + if (converted != message.Content) + { + message.Content = converted; + modified++; + } + } + + if (modified > 0) + { + await db.SaveChangesAsync(); + logger.LogInformation("Migrated {Count} messages from ANSI escape codes to printable color tags.", modified); + } + } + + /// + /// Convert ANSI escape codes to printable color tags. + /// \x1b[38;2;R;G;Bm → {F:RRGGBB}, \x1b[48;2;R;G;Bm → {B:RRGGBB}, \x1b[0m → {X} + /// + public static string AnsiToColorTags(string text) + { + return AnsiColorRegex().Replace(text, match => + { + if (match.Groups[1].Value == "0") + return "{X}"; + + if (match.Groups[2].Success) + { + var r = int.Parse(match.Groups[3].Value); + var g = int.Parse(match.Groups[4].Value); + var b = int.Parse(match.Groups[5].Value); + var type = match.Groups[2].Value == "38;2" ? "F" : "B"; + return $"{{{type}:{r:X2}{g:X2}{b:X2}}}"; + } + + return match.Value; + }); + } + + [GeneratedRegex(@"\x1b\[(?:(0)|(?:(38;2|48;2);(\d{1,3});(\d{1,3});(\d{1,3})))m")] + private static partial Regex AnsiColorRegex(); +} diff --git a/src/EchoHub.Server/Setup/DatabaseSetup.cs b/src/EchoHub.Server/Setup/DatabaseSetup.cs index 6b61510..fa65fbc 100644 --- a/src/EchoHub.Server/Setup/DatabaseSetup.cs +++ b/src/EchoHub.Server/Setup/DatabaseSetup.cs @@ -16,6 +16,9 @@ public static class DatabaseSetup await MigrateAsync(db, logger); await SeedDefaultChannelAsync(db, logger); + + // Run data migrations (e.g. ANSI → color tag format) + await DataMigrationService.RunAsync(services); } private static async Task MigrateAsync(EchoHubDbContext db, ILogger logger)