Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring CJK character support #109

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ShellProgressBar.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ class Program
private static readonly IList<IProgressBarExample> TestCases = new List<IProgressBarExample>
{
new PersistMessageExample(),
new CJKPersistMessageExample(),
new FixedDurationExample(),
new DeeplyNestedProgressBarTreeExample(),
new CJKDeeplyNestedProgressBarTreeExample(),
new NestedProgressBarPerStepProgress(),
new DrawsOnlyOnTickExample(),
new ThreadedTicksOverflowExample(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
60 changes: 60 additions & 0 deletions src/ShellProgressBar.Example/TestCases/CJKPersistMessageExample.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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,
}

}
8 changes: 7 additions & 1 deletion src/ShellProgressBar.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.24720.0
Expand All @@ -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
Expand All @@ -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
Expand Down
14 changes: 4 additions & 10 deletions src/ShellProgressBar/ProgressBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 1 addition & 15 deletions src/ShellProgressBar/ProgressBarOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

/// <summary> The foreground color of the progress bar, message and time</summary>
public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Green;

Expand Down Expand Up @@ -108,7 +94,7 @@ public bool EnableTaskBarProgress
}

/// <summary>
/// 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.
/// <para>Use case: pretty print or change the console colors, the progressbar will reset back</para>
/// </summary>
Expand Down
Loading