#nullable enable using System.Buffers; namespace Terminal.Gui; /// Extensions to to support TUI text manipulation. public static class StringExtensions { /// Unpacks the last UTF-8 encoding in the string. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to decode. /// Index in string to stop at; if -1, use the buffer length. /// public static (Rune rune, int size) DecodeLastRune (this string str, int end = -1) { Rune rune = str.EnumerateRunes ().ToArray () [end == -1 ? ^1 : end]; byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ()); OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed); if (operationStatus == OperationStatus.Done) { return (rune, bytesConsumed); } return (Rune.ReplacementChar, 1); } /// Unpacks the first UTF-8 encoding in the string and returns the rune and its width in bytes. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to decode. /// Starting offset. /// Number of bytes in the buffer, or -1 to make it the length of the buffer. /// public static (Rune Rune, int Size) DecodeRune (this string str, int start = 0, int count = -1) { Rune rune = str.EnumerateRunes ().ToArray () [start]; byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ()); if (count == -1) { count = bytes.Length; } OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed); if (operationStatus == OperationStatus.Done && bytesConsumed >= count) { return (rune, bytesConsumed); } return (Rune.ReplacementChar, 1); } /// Gets the number of columns the string occupies in the terminal. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to measure. /// public static int GetColumns (this string str) { return str is null ? 0 : str.EnumerateRunes ().Sum (r => Math.Max (r.GetColumns (), 0)); } /// Gets the number of runes in the string. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to count. /// public static int GetRuneCount (this string str) { return str.EnumerateRunes ().Count (); } /// /// Determines if this of is composed entirely of ASCII /// digits. /// /// A of to check. /// /// A indicating if all elements of the are ASCII digits ( /// ) or not ( /// public static bool IsAllAsciiDigits (this ReadOnlySpan stringSpan) { return stringSpan.ToString ().All (char.IsAsciiDigit); } /// /// Determines if this of is composed entirely of ASCII /// digits. /// /// A of to check. /// /// A indicating if all elements of the are ASCII digits ( /// ) or not ( /// public static bool IsAllAsciiHexDigits (this ReadOnlySpan stringSpan) { return stringSpan.ToString ().All (char.IsAsciiHexDigit); } /// Repeats the string times. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The text to repeat. /// Number of times to repeat the text. /// The text repeated if is greater than zero, otherwise . public static string? Repeat (this string str, int n) { if (n <= 0) { return null; } if (string.IsNullOrEmpty (str) || n == 1) { return str; } return new StringBuilder (str.Length * n) .Insert (0, str, n) .ToString (); } /// Converts the string into a . /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to convert. /// public static List ToRuneList (this string str) { return str.EnumerateRunes ().ToList (); } /// Converts the string into a array. /// This is a Terminal.Gui extension method to to support TUI text manipulation. /// The string to convert. /// public static Rune [] ToRunes (this string str) { return str.EnumerateRunes ().ToArray (); } /// Converts a generic collection into a string. /// The enumerable rune to convert. /// public static string ToString (IEnumerable runes) { const int maxCharsPerRune = 2; // Max stackalloc ~2 kB const int maxStackallocTextBufferSize = 1048; Span runeBuffer = stackalloc char[maxCharsPerRune]; // Use stackalloc buffer if rune count is easily available and the count is reasonable. if (runes.TryGetNonEnumeratedCount (out int count)) { if (count == 0) { return string.Empty; } int maxRequiredTextBufferSize = count * maxCharsPerRune; if (maxRequiredTextBufferSize <= maxStackallocTextBufferSize) { Span textBuffer = stackalloc char[maxRequiredTextBufferSize]; Span remainingBuffer = textBuffer; foreach (Rune rune in runes) { int charsWritten = rune.EncodeToUtf16 (runeBuffer); ReadOnlySpan runeChars = runeBuffer [..charsWritten]; runeChars.CopyTo (remainingBuffer); remainingBuffer = remainingBuffer [runeChars.Length..]; } ReadOnlySpan text = textBuffer[..^remainingBuffer.Length]; return text.ToString (); } } // Fallback to StringBuilder append. StringBuilder stringBuilder = new(); foreach (Rune rune in runes) { int charsWritten = rune.EncodeToUtf16 (runeBuffer); ReadOnlySpan runeChars = runeBuffer [..charsWritten]; stringBuilder.Append (runeChars); } return stringBuilder.ToString (); } /// Converts a byte generic collection into a string in the provided encoding (default is UTF8) /// The enumerable byte to convert. /// The encoding to be used. /// public static string ToString (IEnumerable bytes, Encoding? encoding = null) { encoding ??= Encoding.UTF8; return encoding.GetString (bytes.ToArray ()); } }