diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2062021..6ef055f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added public access to GetValues enumeration extension
- Added extensions for getting JsonReader values as long or ulong
- Added DateTimeUtils methods for creating DateTimes from epoch seconds or milliseconds
+ - Added AnsiToHtml converter for visualizing ANSI logs
### Changed
- Fixed exception trying to get DHCP status of network interfaces on Linux
diff --git a/ICD.Common.Utils.Tests/Converters/AnsiToHtmlTest.cs b/ICD.Common.Utils.Tests/Converters/AnsiToHtmlTest.cs
new file mode 100644
index 0000000..4d14e5a
--- /dev/null
+++ b/ICD.Common.Utils.Tests/Converters/AnsiToHtmlTest.cs
@@ -0,0 +1,22 @@
+using ICD.Common.Utils.Converters;
+using NUnit.Framework;
+
+namespace ICD.Common.Utils.Tests.Converters
+{
+ [TestFixture]
+ public sealed class AnsiToHtmlTest
+ {
+ [TestCase("black\x1b[37mwhite",
+ @"blackwhite")]
+ [TestCase("black\x1b[0mblack",
+ @"blackblack")]
+ [TestCase("black\x1b[37mwhite\x1b[0m",
+ @"blackwhite")]
+ [TestCase("\x1b[30mblack\x1b[37mwhite",
+ @"blackwhite")]
+ public void ConvertTest(string ansi, string expected)
+ {
+ Assert.AreEqual(expected, AnsiToHtml.Convert(ansi));
+ }
+ }
+}
diff --git a/ICD.Common.Utils/Converters/AnsiToHtml.cs b/ICD.Common.Utils/Converters/AnsiToHtml.cs
new file mode 100644
index 0000000..6dac84d
--- /dev/null
+++ b/ICD.Common.Utils/Converters/AnsiToHtml.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+
+namespace ICD.Common.Utils.Converters
+{
+ public static class AnsiToHtml
+ {
+ ///
+ /// Matches ANSI escape codes, e.g. \x1b[31m and \x1b[30;1m
+ ///
+ private const string ANSI_PATTERN = "(?'match'\x01b\\[(?'code'[\\d;]+)m)";
+
+ private const string CODE_RESET = "0";
+
+ ///
+ /// Matches ANSI escape codes to HTML styles.
+ /// Color values are taken from PuTTY.
+ ///
+ private static readonly Dictionary s_Colors =
+ new Dictionary
+ {
+ {"30", "color:#000000"}, // Black
+ {"31", "color:#BB0000"}, // Red
+ {"32", "color:#00BB00"}, // Green
+ {"33", "color:#BBBB00"}, // Yellow
+ {"34", "color:#0000BB"}, // Blue
+ {"35", "color:#BB00BB"}, // Magenta
+ {"36", "color:#00BBBB"}, // Cyan
+ {"37", "color:#BBBBBB"}, // White
+
+ {"30;1", "color:#555555"}, // Bright Black
+ {"31;1", "color:#FF5555"}, // Bright Red
+ {"32;1", "color:#55FF55"}, // Bright Green
+ {"33;1", "color:#FFFF55"}, // Bright Yellow
+ {"34;1", "color:#5555FF"}, // Bright Blue
+ {"35;1", "color:#FF55FF"}, // Bright Magenta
+ {"36;1", "color:#55FFFF"}, // Bright Cyan
+ {"37;1", "color:#FFFFFF"}, // Bright White
+ };
+
+ ///
+ /// Converts the input ansi string into html with color attributes.
+ ///
+ ///
+ ///
+ public static string Convert(string ansi)
+ {
+ int depth = 0;
+
+ // Hack - Append a reset to close any open spans
+ ansi += "\x1b[0m";
+
+ return RegexUtils.ReplaceGroup(ansi, ANSI_PATTERN, "match", match =>
+ {
+ string code = match.Groups["code"].Value;
+
+ // Reset code - close all of the open spans.
+ if (code == CODE_RESET)
+ {
+ string output = StringUtils.Repeat("", depth);
+ depth = 0;
+ return output;
+ }
+
+ string style;
+ if (!s_Colors.TryGetValue(code, out style))
+ return match.Value;
+
+ depth++;
+ return string.Format("", style);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj
index 432936a..bc0cfbb 100644
--- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj
+++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj
@@ -86,6 +86,7 @@
+