diff --git a/src/ShellProgressBar.Example/Program.cs b/src/ShellProgressBar.Example/Program.cs index b947538..64aa099 100644 --- a/src/ShellProgressBar.Example/Program.cs +++ b/src/ShellProgressBar.Example/Program.cs @@ -13,8 +13,10 @@ class Program private static readonly IList TestCases = new List { new PersistMessageExample(), + new CJKPersistMessageExample(), new FixedDurationExample(), new DeeplyNestedProgressBarTreeExample(), + new CJKDeeplyNestedProgressBarTreeExample(), new NestedProgressBarPerStepProgress(), new DrawsOnlyOnTickExample(), new ThreadedTicksOverflowExample(), diff --git a/src/ShellProgressBar.Example/TestCases/CJKDeeplyNestedProgressBarTreeExample.cs b/src/ShellProgressBar.Example/TestCases/CJKDeeplyNestedProgressBarTreeExample.cs new file mode 100644 index 0000000..d2d88f8 --- /dev/null +++ b/src/ShellProgressBar.Example/TestCases/CJKDeeplyNestedProgressBarTreeExample.cs @@ -0,0 +1,66 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace ShellProgressBar.Example.Examples +{ + public class CJKDeeplyNestedProgressBarTreeExample : IProgressBarExample + { + public Task Start(CancellationToken token) + { + var random = new Random(); + + var numberOfSteps = 7; + + var overProgressOptions = new ProgressBarOptions + { + DenseProgressBar = true, + ProgressCharacter = '─', + BackgroundColor = ConsoleColor.DarkGray, + EnableTaskBarProgress = RuntimeInformation.IsOSPlatform(OSPlatform.Windows), + }; + + using (var pbar = new ProgressBar(numberOfSteps, "总体进展", overProgressOptions)) + { + var stepBarOptions = new ProgressBarOptions + { + DenseProgressBar = true, + ForegroundColor = ConsoleColor.Cyan, + ForegroundColorDone = ConsoleColor.DarkGreen, + ProgressCharacter = '─', + BackgroundColor = ConsoleColor.DarkGray, + CollapseWhenFinished = true, + }; + Parallel.For(0, numberOfSteps, (i) => + { + var workBarOptions = new ProgressBarOptions + { + DenseProgressBar = true, + ForegroundColor = ConsoleColor.Yellow, + ProgressCharacter = '─', + BackgroundColor = ConsoleColor.DarkGray, + }; + var childSteps = random.Next(1, 5); + using (var childProgress = pbar.Spawn(childSteps, $"步骤 {i} 进度", stepBarOptions)) + Parallel.For(0, childSteps, (ci) => + { + var childTicks = random.Next(50, 250); + using (var innerChildProgress = childProgress.Spawn(childTicks, $"步骤 {i}::{ci} 进度", workBarOptions)) + { + for (var r = 0; r < childTicks; r++) + { + innerChildProgress.Tick(); + Program.BusyWait(50); + } + } + childProgress.Tick(); + }); + + pbar.Tick(); + }); + } + return Task.FromResult(1); + } + } +} diff --git a/src/ShellProgressBar.Example/TestCases/CJKPersistMessageExample.cs b/src/ShellProgressBar.Example/TestCases/CJKPersistMessageExample.cs new file mode 100644 index 0000000..9381926 --- /dev/null +++ b/src/ShellProgressBar.Example/TestCases/CJKPersistMessageExample.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ShellProgressBar.Example.Examples +{ + public class CJKPersistMessageExample : ExampleBase + { + protected override Task StartAsync() + { + var options = new ProgressBarOptions + { + ForegroundColor = ConsoleColor.Yellow, + ForegroundColorDone = ConsoleColor.DarkGreen, + ForegroundColorError = ConsoleColor.Red, + BackgroundColor = ConsoleColor.DarkGray, + BackgroundCharacter = '\u2593', + WriteQueuedMessage = o => + { + var writer = o.Error ? Console.Error : Console.Out; + var c = o.Error ? ConsoleColor.DarkRed : ConsoleColor.Blue; + if (o.Line.StartsWith("报告 500")) + { + Console.ForegroundColor = ConsoleColor.Yellow; + writer.WriteLine("添加额外信息,因为可以这样做"); + + Console.ForegroundColor = c; + writer.WriteLine(o.Line); + return 2; //signal to the progressbar we wrote two messages + } + Console.ForegroundColor = c; + writer.WriteLine(o.Line); + return 1; + } + }; + var wait = TimeSpan.FromSeconds(6); + using var pbar = new FixedDurationBar(wait, "", options); + var t = new Thread(() => LongRunningTask(pbar)); + t.Start(); + + if (!pbar.CompletedHandle.WaitOne(wait.Subtract(TimeSpan.FromSeconds(.5)))) + { + pbar.WriteErrorLine($"{wait}之后,{nameof(FixedDurationBar)}没有向{nameof(FixedDurationBar.CompletedHandle)}发出信号。"); + pbar.Dispose(); + } + return Task.CompletedTask; + } + + private static void LongRunningTask(FixedDurationBar bar) + { + for (var i = 0; i < 1_000_000; i++) + { + bar.Message = $"{i} 事件"; + if (bar.IsCompleted || bar.ObservedError) break; + if (i % 500 == 0) bar.WriteLine($"向进度条上方的控制台报告 {i} 的情况"); + Thread.Sleep(1); + } + } + } +} diff --git a/src/ShellProgressBar.UnicodeSourceGenerator/ShellProgressBar.UnicodeSourceGenerator.csproj b/src/ShellProgressBar.UnicodeSourceGenerator/ShellProgressBar.UnicodeSourceGenerator.csproj new file mode 100644 index 0000000..2bcc48b --- /dev/null +++ b/src/ShellProgressBar.UnicodeSourceGenerator/ShellProgressBar.UnicodeSourceGenerator.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + true + + + + + + + + diff --git a/src/ShellProgressBar.UnicodeSourceGenerator/UnicodeUtilsSourceGenerator.cs b/src/ShellProgressBar.UnicodeSourceGenerator/UnicodeUtilsSourceGenerator.cs new file mode 100644 index 0000000..a03d6d2 --- /dev/null +++ b/src/ShellProgressBar.UnicodeSourceGenerator/UnicodeUtilsSourceGenerator.cs @@ -0,0 +1,156 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; + +using Microsoft.CodeAnalysis; + +namespace ShellProgressBar.UnicodeSourceGenerator +{ + [Generator] + public class UnicodeUtilsSourceGenerator : ISourceGenerator + { + + public void Execute(GeneratorExecutionContext context) + { + HttpClient client = new HttpClient(); + string res = client.GetStringAsync("https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt").Result; + var group = res.Split('\n') + .Select(i => i.Split('#')[0].Trim()) + .Where(i => !string.IsNullOrEmpty(i)) + .Select(i => + { + var match = new Regex(@"^([0-9a-fA-F]{4,6})(\.\.([0-9a-fA-F]{4,6}))?\s+;\s+([AFHNW]a?)").Match(i); + uint start = uint.Parse(match.Groups[1].Value, NumberStyles.HexNumber); + uint? end = null; + if (match.Groups[3].Success) + end = uint.Parse(match.Groups[3].Value, NumberStyles.HexNumber); + + var type = AsUnicodeCharacterWidthType(match.Groups[4].Value); + return new { start, end, type }; + }) + .ToList() + .GroupBy(i => IsFullWidth(i.type)); + + StringBuilder code = new StringBuilder(); + code.AppendLine("namespace System"); + code.AppendLine("{"); + code.AppendLine("internal static class UnicodeUtils"); + code.AppendLine("{"); + + //code.AppendLine("private static bool IsFullWidthCharacter(int unicode) => unicode switch"); + //code.AppendLine("{"); + //code.AppendLine(string.Join("\r\n", group.Select(i => $" {string.Join(" or ", i.Select(j => $"{(j.end.HasValue ? $"(>= {j.start} and <= {j.end})" : $"{j.start}")}"))} => {i.Key.ToString().ToLowerInvariant()},"))); + //code.AppendLine(" _ => false,"); + //code.AppendLine("};"); + + code.AppendLine("private static bool IsFullWidthCharacter(int unicode)"); + code.AppendLine("{"); + code.AppendLine(string.Join("\r\n", group.Select(i => $" if ({string.Join(" || ", i.Select(j => $"{(j.end.HasValue ? $"({j.start} <= unicode && unicode <= {j.end})" : $"(unicode == {j.start})")}"))}) return {i.Key.ToString().ToLowerInvariant()};"))); code.AppendLine(" return false;"); + code.AppendLine("}"); + code.AppendLine(); + code.AppendLine("public static int GetWidth(string str)"); + code.AppendLine("{"); + code.AppendLine(" int result = 0;"); + code.AppendLine(" for (int i = 0; i < str.Length; i++)"); + code.AppendLine(" {"); + code.AppendLine(" int unicode = 0;"); + code.AppendLine(" if (char.IsSurrogatePair(str, i))"); + code.AppendLine(" {"); + code.AppendLine(" unicode |= str[i] & 0x03FF;"); + code.AppendLine(" unicode <<= 10;"); + code.AppendLine(" i++;"); + code.AppendLine(" unicode |= str[i] & 0x03FF;"); + code.AppendLine(" unicode += 0x10000;"); + code.AppendLine(" }"); + code.AppendLine(" else"); + code.AppendLine(" {"); + code.AppendLine(" unicode = str[i];"); + code.AppendLine(" }"); + code.AppendLine(" result += IsFullWidthCharacter(unicode) ? 2 : 1;"); + code.AppendLine(" }"); + code.AppendLine(" return result;"); + code.AppendLine("}"); + + code.AppendLine("}"); + code.AppendLine("}"); + + context.AddSource("UnicodeUtils.g.cs", code.ToString()); + } + + internal static UnicodeCharacterWidthType AsUnicodeCharacterWidthType(string value) + { + //return value switch + //{ + // "A" => UnicodeCharacterWidthType.Ambiguous, + // "F" => UnicodeCharacterWidthType.Fullwidth, + // "H" => UnicodeCharacterWidthType.Halfwidth, + // "N" => UnicodeCharacterWidthType.Neutral, + // "Na" => UnicodeCharacterWidthType.Narrow, + // "W" => UnicodeCharacterWidthType.Wide, + // _ => throw new FormatException(), + //}; + switch (value) + { + case "A": + return UnicodeCharacterWidthType.Ambiguous; + case "F": + return UnicodeCharacterWidthType.Fullwidth; + case "H": + return UnicodeCharacterWidthType.Halfwidth; + case "N": + return UnicodeCharacterWidthType.Neutral; + case "Na": + return UnicodeCharacterWidthType.Narrow; + case "W": + return UnicodeCharacterWidthType.Wide; + default: + throw new FormatException(); + } + } + + internal static bool IsFullWidth(UnicodeCharacterWidthType value) + { + //return value switch + //{ + // UnicodeCharacterWidthType.Ambiguous => false, + // UnicodeCharacterWidthType.Fullwidth => true, + // UnicodeCharacterWidthType.Halfwidth => false, + // UnicodeCharacterWidthType.Neutral => false, + // UnicodeCharacterWidthType.Narrow => false, + // UnicodeCharacterWidthType.Wide => true, + // _ => throw new InvalidCastException(), + //}; + switch (value) + { + case UnicodeCharacterWidthType.Ambiguous: + case UnicodeCharacterWidthType.Halfwidth: + case UnicodeCharacterWidthType.Neutral: + case UnicodeCharacterWidthType.Narrow: + return false; + case UnicodeCharacterWidthType.Fullwidth: + case UnicodeCharacterWidthType.Wide: + return true; + default: + throw new InvalidCastException(); + } + } + + + public void Initialize(GeneratorInitializationContext context) { } + } + + internal enum UnicodeCharacterWidthType : byte + { + Ambiguous, + Fullwidth, + Halfwidth, + Neutral, + Narrow, + Wide, + } + +} diff --git a/src/ShellProgressBar.sln b/src/ShellProgressBar.sln index 6c57182..20c25f3 100644 --- a/src/ShellProgressBar.sln +++ b/src/ShellProgressBar.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.24720.0 @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShellProgressBar.Tests", "..\test\ShellProgressBar.Tests\ShellProgressBar.Tests.csproj", "{7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShellProgressBar.UnicodeSourceGenerator", "ShellProgressBar.UnicodeSourceGenerator\ShellProgressBar.UnicodeSourceGenerator.csproj", "{C8B6F7DC-2AF0-47A5-8EC9-A909360322F3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}.Release|Any CPU.Build.0 = Release|Any CPU + {C8B6F7DC-2AF0-47A5-8EC9-A909360322F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8B6F7DC-2AF0-47A5-8EC9-A909360322F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8B6F7DC-2AF0-47A5-8EC9-A909360322F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8B6F7DC-2AF0-47A5-8EC9-A909360322F3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ShellProgressBar/ProgressBar.cs b/src/ShellProgressBar/ProgressBar.cs index a76e525..52e7026 100644 --- a/src/ShellProgressBar/ProgressBar.cs +++ b/src/ShellProgressBar/ProgressBar.cs @@ -146,12 +146,9 @@ private static void CondensedProgressBar( var maxCharacterWidth = Console.WindowWidth - (depth * 2) + 2; var truncatedMessage = StringExtensions.Excerpt(message, messageWidth - 2) + " "; var width = (Console.WindowWidth - (depth * 2) + 2) - truncatedMessage.Length; - - if (!string.IsNullOrWhiteSpace(ProgressBarOptions.ProgressMessageEncodingName)) - { - width = width + message.Length - System.Text.Encoding.GetEncoding(ProgressBarOptions.ProgressMessageEncodingName).GetBytes(message).Length; - } - + + width = width + message.Length - message.CalcStringWidth(); + var newWidth = (int) ((width * percentage) / 100d); var progBar = new string(progressCharacter, newWidth); DrawBottomHalfPrefix(indentation, depth); @@ -184,10 +181,7 @@ private static void ProgressBarBottomHalf(double percentage, DateTime startDate, var column1Width = Console.WindowWidth - durationString.Length - (depth * 2) + 2; var column2Width = durationString.Length; - if (!string.IsNullOrWhiteSpace(ProgressBarOptions.ProgressMessageEncodingName)) - { - column1Width = column1Width + message.Length - System.Text.Encoding.GetEncoding(ProgressBarOptions.ProgressMessageEncodingName).GetBytes(message).Length; - } + column1Width = column1Width + message.Length - message.CalcStringWidth(); if (progressBarOnBottom) DrawTopHalfPrefix(indentation, depth); diff --git a/src/ShellProgressBar/ProgressBarOptions.cs b/src/ShellProgressBar/ProgressBarOptions.cs index 0a02c5d..20a97ec 100644 --- a/src/ShellProgressBar/ProgressBarOptions.cs +++ b/src/ShellProgressBar/ProgressBarOptions.cs @@ -11,20 +11,6 @@ public class ProgressBarOptions private bool _enableTaskBarProgress; public static readonly ProgressBarOptions Default = new ProgressBarOptions(); - public static string ProgressMessageEncodingName { get; set; } - - public string MessageEncodingName - { - get - { - return ProgressMessageEncodingName; - } - set - { - ProgressMessageEncodingName = value; - } - } - /// The foreground color of the progress bar, message and time public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Green; @@ -108,7 +94,7 @@ public bool EnableTaskBarProgress } /// - /// Take ownership of writing a message that is intended to be displayed above the progressbar. + /// Take ownership of writing a message that is intended to be displayed above the progressbar. /// The delegate is expected to return the number of messages written to the console as a result of the string argument. /// Use case: pretty print or change the console colors, the progressbar will reset back /// diff --git a/src/ShellProgressBar/ShellProgressBar.csproj b/src/ShellProgressBar/ShellProgressBar.csproj index 0f78459..d0a1054 100644 --- a/src/ShellProgressBar/ShellProgressBar.csproj +++ b/src/ShellProgressBar/ShellProgressBar.csproj @@ -1,4 +1,4 @@ - + ShellProgressBar @@ -23,4 +23,11 @@ + + + + diff --git a/src/ShellProgressBar/StringExtensions.cs b/src/ShellProgressBar/StringExtensions.cs index bc3071b..05e5683 100644 --- a/src/ShellProgressBar/StringExtensions.cs +++ b/src/ShellProgressBar/StringExtensions.cs @@ -4,13 +4,22 @@ namespace ShellProgressBar { - internal static class StringExtensions - { - public static string Excerpt(string phrase, int length = 60) - { - if (string.IsNullOrEmpty(phrase) || phrase.Length < length) - return phrase; - return phrase.Substring(0, length - 3) + "..."; - } - } + internal static class StringExtensions + { + public static string Excerpt(string phrase, int length = 60) + { + if (string.IsNullOrEmpty(phrase) || phrase.Length < length) + return phrase; + return phrase.Substring(0, length - 3) + "..."; + } + + public static int CalcStringWidth(this string message) + { +#if NETSTANDARD1_3 + return message.Length; +#else + return UnicodeUtils.GetWidth(message); +#endif + } + } }