#nullable enable using System.Globalization; using Wcwidth; namespace Terminal.Gui; /// Extends to support TUI text manipulation. public static class RuneExtensions { /// Maximum Unicode code point. public static readonly int MaxUnicodeCodePoint = 0x10FFFF; /// Reports if the provided array of bytes can be encoded as UTF-8. /// The byte array to probe. /// true if is valid; otherwise, false. public static bool CanBeEncodedAsRune (byte [] buffer) { string str = Encoding.Unicode.GetString (buffer); foreach (Rune rune in str.EnumerateRunes ()) { if (rune == Rune.ReplacementChar) { return false; } } return true; } /// Attempts to decode the rune as a surrogate pair to UTF-16. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The rune to decode. /// The chars if the rune is a surrogate pair. Null otherwise. /// if the rune is a valid surrogate pair; otherwise. public static bool DecodeSurrogatePair (this Rune rune, out char []? chars) { bool isSingleUtf16CodeUnit = rune.IsBmp; if (isSingleUtf16CodeUnit) { chars = null; return false; } const int maxCharsPerRune = 2; Span charBuffer = stackalloc char[maxCharsPerRune]; int charsWritten = rune.EncodeToUtf16 (charBuffer); if (charsWritten >= 2 && char.IsSurrogatePair (charBuffer [0], charBuffer [1])) { chars = charBuffer [..charsWritten].ToArray (); return true; } chars = null; return false; } /// Writes into the destination buffer starting at offset the UTF8 encoded version of the rune. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The rune to encode. /// The destination buffer. /// Starting offset to look into. /// Number of bytes valid in the buffer, or -1 to make it the length of the buffer. /// he number of bytes written into the destination buffer. public static int Encode (this Rune rune, byte [] dest, int start = 0, int count = -1) { const int maxUtf8BytesPerRune = 4; Span bytes = stackalloc byte[maxUtf8BytesPerRune]; int writtenBytes = rune.EncodeToUtf8 (bytes); int bytesToCopy = count == -1 ? writtenBytes : Math.Min (count, writtenBytes); int bytesWritten = 0; for (int i = 0; i < bytesToCopy; i++) { if (bytes [i] == '\0') { break; } dest [start + i] = bytes [i]; bytesWritten++; } return bytesWritten; } /// Attempts to encode (as UTF-16) a surrogate pair. /// The high surrogate code point. /// The low surrogate code point. /// The encoded rune. /// if the encoding succeeded; otherwise. public static bool EncodeSurrogatePair (char highSurrogate, char lowSurrogate, out Rune result) { result = default (Rune); if (char.IsSurrogatePair (highSurrogate, lowSurrogate)) { result = (Rune)char.ConvertToUtf32 (highSurrogate, lowSurrogate); return true; } return false; } /// Gets the number of columns the rune occupies in the terminal. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The rune to measure. /// /// The number of columns required to fit the rune, 0 if the argument is the null character, or -1 if the value is /// not printable, otherwise the number of columns that the rune occupies. /// public static int GetColumns (this Rune rune) { return UnicodeCalculator.GetWidth (rune); } /// Get number of bytes required to encode the rune, based on the provided encoding. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The rune to probe. /// The encoding used; the default is UTF8. /// The number of bytes required. public static int GetEncodingLength (this Rune rune, Encoding? encoding = null) { encoding ??= Encoding.UTF8; const int maxCharsPerRune = 2; // Get characters with UTF16 to keep that part independent of selected encoding. Span charBuffer = stackalloc char[maxCharsPerRune]; int charsWritten = rune.EncodeToUtf16(charBuffer); Span chars = charBuffer[..charsWritten]; int maxEncodedLength = encoding.GetMaxByteCount (charsWritten); Span byteBuffer = stackalloc byte[maxEncodedLength]; int bytesEncoded = encoding.GetBytes (chars, byteBuffer); ReadOnlySpan encodedBytes = byteBuffer[..bytesEncoded]; if (encodedBytes [^1] == '\0') { return encodedBytes.Length - 1; } return encodedBytes.Length; } /// Returns if the rune is a combining character. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// /// public static bool IsCombiningMark (this Rune rune) { UnicodeCategory category = Rune.GetUnicodeCategory (rune); return category == UnicodeCategory.NonSpacingMark || category == UnicodeCategory.SpacingCombiningMark || category == UnicodeCategory.EnclosingMark; } /// Reports whether a rune is a surrogate code point. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The rune to probe. /// if the rune is a surrogate code point; otherwise. public static bool IsSurrogatePair (this Rune rune) { bool isSingleUtf16CodeUnit = rune.IsBmp; if (isSingleUtf16CodeUnit) { return false; } const int maxCharsPerRune = 2; Span charBuffer = stackalloc char[maxCharsPerRune]; int charsWritten = rune.EncodeToUtf16 (charBuffer); return charsWritten >= 2 && char.IsSurrogatePair (charBuffer [0], charBuffer [1]); } /// /// Ensures the rune is not a control character and can be displayed by translating characters below 0x20 to /// equivalent, printable, Unicode chars. /// /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// /// public static Rune MakePrintable (this Rune rune) { return Rune.IsControl (rune) ? new Rune (rune.Value + 0x2400) : rune; } }