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)