<refactor> ui美化...

This commit is contained in:
2022-12-08 16:42:59 +08:00
parent f3d250ae87
commit 0b4e582bbd
20 changed files with 468 additions and 321 deletions

View File

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@ -0,0 +1,17 @@
Hotkey,^d,start
return
start:
loop,100{
send,{AppsKey}
Sleep,100
send,{Up}
Sleep,100
send,{Up}
Sleep,100
Send,{Enter}
Sleep,500
Send,{Esc},
Sleep,100
Send,{Down}
Sleep,100
}

View File

@ -5,7 +5,7 @@ public sealed class Main : ToolBase<Option>
{
public Main(Option opt) : base(opt) { }
public override Task Run()
protected override Task Core()
{
Application.Run(new WinMain());
return Task.CompletedTask;

View File

@ -45,6 +45,39 @@ public abstract class FilesTool<TOption> : ToolBase<TOption> where TOption : Dir
return fileList;
}
protected override async Task Core()
{
if (!Opt.WriteMode) AnsiConsole.MarkupLine("[gray]{0}[/]", Str.ExerciseMode);
IEnumerable<string> fileList;
await AnsiConsole.Progress()
.Columns(new ProgressBarColumn() //
, new ElapsedTimeColumn() //
, new PercentageColumn() //
, new SpinnerColumn() //
, new TaskDescriptionColumn { Alignment = Justify.Left } //
)
.StartAsync(async ctx => {
var taskSearchfile = ctx.AddTask(Str.SearchingFile).IsIndeterminate();
_childTask = ctx.AddTask("-/-", false);
fileList = EnumerateFiles(Opt.Path, Opt.Filter, out _excludeCnt);
_totalCnt = fileList.Count();
taskSearchfile.IsIndeterminate(false);
taskSearchfile.Increment(100);
_childTask.MaxValue = _totalCnt;
_childTask.StartTask();
await Parallel.ForEachAsync(fileList, FileHandle);
});
var grid = new Grid().AddColumn(new GridColumn().NoWrap().PadRight(16))
.AddColumn(new GridColumn().Alignment(Justify.Right));
foreach (var kv in _writeStats.OrderByDescending(x => x.Value).ThenBy(x => x.Key))
grid.AddRow(kv.Key, kv.Value.ToString());
AnsiConsole.Write(new Panel(grid).Header(Str.WriteFileStats));
}
protected FileStream CreateTempFile(out string file)
{
@ -94,37 +127,4 @@ public abstract class FilesTool<TOption> : ToolBase<TOption> where TOption : Dir
{
_writeStats.AddOrUpdate(key, 1, (_, oldValue) => oldValue + 1);
}
public override async Task Run()
{
if (!Opt.WriteMode) AnsiConsole.MarkupLine("[gray]{0}[/]", Str.ExerciseMode);
IEnumerable<string> fileList;
await AnsiConsole.Progress()
.Columns(new ProgressBarColumn() //
, new ElapsedTimeColumn() //
, new PercentageColumn() //
, new SpinnerColumn() //
, new TaskDescriptionColumn { Alignment = Justify.Left } //
)
.StartAsync(async ctx => {
var taskSearchfile = ctx.AddTask(Str.SearchingFile).IsIndeterminate();
_childTask = ctx.AddTask("-/-", false);
fileList = EnumerateFiles(Opt.Path, Opt.Filter, out _excludeCnt);
_totalCnt = fileList.Count();
taskSearchfile.IsIndeterminate(false);
taskSearchfile.Increment(100);
_childTask.MaxValue = _totalCnt;
_childTask.StartTask();
await Parallel.ForEachAsync(fileList, FileHandle);
});
var grid = new Grid().AddColumn(new GridColumn().NoWrap().PadRight(16))
.AddColumn(new GridColumn().Alignment(Justify.Right));
foreach (var kv in _writeStats.OrderByDescending(x => x.Value).ThenBy(x => x.Key))
grid.AddRow(kv.Key, kv.Value.ToString());
AnsiConsole.Write(new Panel(grid).Header(Str.WriteFileStats));
}
}

View File

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using NSExt.Extensions;
@ -6,13 +7,9 @@ namespace Dot.Git;
public class Main : ToolBase<Option>
{
private const int _POS_Y_MSG = 74; //git command rsp 显示的位置 y
private const int _POST_Y_LOADING = 70; //loading 动画显示的位置 y
private const int _REP_PATH_LENGTH_LIMIT = 32; //仓库路径长度显示截断阈值
private (int x, int y) _cursorPosBackup; //光标位置备份
private readonly Encoding _gitOutputEnc; //git command rsp 编码
private List<string> _repoPathList; //仓库目录列表
private readonly Encoding _gitOutputEnc; //git command rsp 编码
private ConcurrentDictionary<string, StringBuilder> _repoRsp; //仓库信息容器
private ConcurrentDictionary<string, TaskStatusColumn.Statues> _repoStatus;
public Main(Option opt) : base(opt)
{
@ -23,25 +20,24 @@ public class Main : ToolBase<Option>
}
private async ValueTask DirHandle(string dir, CancellationToken cancelToken)
private async ValueTask DirHandle(KeyValuePair<string, ProgressTask> payload, CancellationToken _)
{
var row = _repoPathList.FindIndex(x => x == dir); // 行号
var tAnimate = LoadingAnimate(_POST_Y_LOADING, _cursorPosBackup.y + row, out var cts);
payload.Value.StartTask();
payload.Value.State.Status(TaskStatusColumn.Statues.Executing);
// 打印 git command rsp
void ExecRspReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data is null) return;
var msg = Encoding.UTF8.GetString(_gitOutputEnc.GetBytes(e.Data));
ConcurrentWrite(_POS_Y_MSG, _cursorPosBackup.y + row, new string(' ', Console.WindowWidth - _POS_Y_MSG));
ConcurrentWrite(_POS_Y_MSG, _cursorPosBackup.y + row, msg);
_repoRsp[payload.Key].Append(msg.EscapeMarkup());
}
// 启动git进程
{
var startInfo = new ProcessStartInfo {
CreateNoWindow = true
, WorkingDirectory = dir
, WorkingDirectory = payload.Key
, FileName = "git"
, Arguments = Opt.Args
, UseShellExecute = false
@ -54,58 +50,78 @@ public class Main : ToolBase<Option>
p.BeginOutputReadLine();
p.BeginErrorReadLine();
await p.WaitForExitAsync();
payload.Value.IsIndeterminate(false);
if (p.ExitCode == 0) {
payload.Value.State.Status(TaskStatusColumn.Statues.Succeed);
_repoStatus.AddOrUpdate(payload.Key, _ => TaskStatusColumn.Statues.Succeed
, (_, _) => TaskStatusColumn.Statues.Succeed);
payload.Value.Increment(100);
}
else {
payload.Value.State.Status(TaskStatusColumn.Statues.Failed);
_repoStatus.AddOrUpdate(payload.Key, _ => TaskStatusColumn.Statues.Failed
, (_, _) => TaskStatusColumn.Statues.Failed);
payload.Value.Increment(0);
}
}
cts.Cancel();
await tAnimate;
cts.Dispose();
}
private void StashCurorPos()
{
_cursorPosBackup = Console.GetCursorPosition();
}
public override async Task Run()
protected override async Task Core()
{
// 查找git仓库目录
{
Console.Write(Str.FindGitReps, Opt.Path);
StashCurorPos();
var progressBar = new ProgressBarColumn { Width = 10 };
await AnsiConsole.Progress()
.Columns(progressBar //
, new ElapsedTimeColumn() //
, new SpinnerColumn() //
, new TaskStatusColumn() //
, new TaskDescriptionColumn { Alignment = Justify.Left } //
)
.StartAsync(async ctx => {
var taskFinder = ctx.AddTask(string.Format(Str.FindGitReps, Opt.Path)).IsIndeterminate();
var paths = Directory.GetDirectories(Opt.Path, ".git" //
, new EnumerationOptions //
{
MaxRecursionDepth = Opt.MaxRecursionDepth
, RecurseSubdirectories = true
, IgnoreInaccessible = true
, AttributesToSkip
= FileAttributes.ReparsePoint
})
.Select(x => Directory.GetParent(x)!.FullName);
var tAnimate = LoadingAnimate(_cursorPosBackup.x, _cursorPosBackup.y, out var cts);
_repoPathList = Directory.GetDirectories(Opt.Path, ".git" //
, new EnumerationOptions //
{
MaxRecursionDepth = Opt.MaxRecursionDepth
, RecurseSubdirectories = true
, IgnoreInaccessible = true
, AttributesToSkip = FileAttributes.ReparsePoint
})
.Select(x => Directory.GetParent(x)!.FullName)
.ToList();
cts.Cancel();
await tAnimate;
cts.Dispose();
_repoRsp = new ConcurrentDictionary<string, StringBuilder>();
_repoStatus = new ConcurrentDictionary<string, TaskStatusColumn.Statues>();
var tasks = new Dictionary<string, ProgressTask>();
foreach (var path in paths) {
_repoRsp.TryAdd(path, new StringBuilder());
_repoStatus.TryAdd(path, default);
var task = ctx.AddTask(new DirectoryInfo(path).Name, false).IsIndeterminate();
tasks.Add(path, task);
}
taskFinder.IsIndeterminate(false);
taskFinder.Increment(100);
taskFinder.State.Status(TaskStatusColumn.Statues.Succeed);
await Parallel.ForEachAsync(tasks, DirHandle);
});
var table = new Table().AddColumn(new TableColumn(Str.Repository) { Width = 50 })
.AddColumn(new TableColumn(Str.Command))
.AddColumn(new TableColumn(Str.Response) { Width = 50 })
.Caption(
$"{Str.ZeroCode}: [green]{_repoStatus.Count(x => x.Value == TaskStatusColumn.Statues
.Succeed)}[/]/{_repoStatus.Count}");
foreach (var repo in _repoRsp) {
var status = _repoStatus[repo.Key].Desc();
table.AddRow(status.Replace(_repoStatus[repo.Key].ToString(), new DirectoryInfo(repo.Key).Name), Opt.Args
, status.Replace(_repoStatus[repo.Key].ToString(), repo.Value.ToString()));
}
// 打印git仓库目录
{
Console.WriteLine(Str.Ok);
StashCurorPos();
var i = 0;
Console.WriteLine( //
string.Join(Environment.NewLine
, _repoPathList.Select(
x => $"{++i}: {new DirectoryInfo(x).Name.Sub(0, _REP_PATH_LENGTH_LIMIT)}"))
//
);
}
// 并行执行git命令
await Parallel.ForEachAsync(_repoPathList, DirHandle);
Console.SetCursorPosition(_cursorPosBackup.x, _cursorPosBackup.y + _repoPathList.Count);
AnsiConsole.Write(table);
}
}

View File

@ -0,0 +1,14 @@
namespace Dot.Git;
public static class ProgressTaskStateExtensions
{
public static TaskStatusColumn.Statues Status(this ProgressTaskState me)
{
return me.Get<TaskStatusColumn.Statues>(nameof(TaskStatusColumn));
}
public static void Status(this ProgressTaskState me, TaskStatusColumn.Statues value)
{
me.Update<TaskStatusColumn.Statues>(nameof(TaskStatusColumn), _ => value);
}
}

View File

@ -0,0 +1,35 @@
using System.ComponentModel;
using NSExt.Extensions;
using Spectre.Console.Rendering;
namespace Dot.Git;
public class TaskStatusColumn : ProgressColumn
{
public enum Statues : byte
{
[Description($"[gray]{nameof(Ready)}[/]")]
Ready
, [Description($"[yellow]{nameof(Executing)}[/]")]
Executing
, [Description($"[green]{nameof(Succeed)}[/]")]
Succeed
, [Description($"[red]{nameof(Failed)}[/]")]
Failed
}
/// <summary>
/// Gets or sets the alignment of the task description.
/// </summary>
public Justify Alignment { get; set; } = Justify.Right;
/// <inheritdoc />
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var text = task.State.Get<Statues>(nameof(TaskStatusColumn));
return new Markup(text.Desc()).Overflow(Overflow.Ellipsis).Justify(Alignment);
}
}

View File

@ -7,7 +7,7 @@ public sealed class Main : ToolBase<Option>
public Main(Option opt) : base(opt) { }
public override Task Run()
protected override Task Core()
{
var guid = System.Guid.NewGuid().ToString();
if (Opt.Upper) guid = guid.ToUpper();

View File

@ -8,7 +8,7 @@ public sealed class Main : ToolBase<Option>
{
public Main(Option opt) : base(opt) { }
public override async Task Run()
protected override async Task Core()
{
foreach (var item in NetworkInterface.GetAllNetworkInterfaces()) {
if (item.NetworkInterfaceType != NetworkInterfaceType.Ethernet ||

View File

@ -53,7 +53,7 @@ public class Main : ToolBase<Option>
return text.Replace("\\\"", "\"");
}
public override async Task Run()
protected override async Task Core()
{
string result = null;
if (Opt.Compress)

View File

@ -1,190 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root" xmlns="">
<xsd:element name="root" msdata:IsDataSet="true"></xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
id="root" xmlns="">
<xsd:element name="root" msdata:IsDataSet="true"></xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="InputTextIsEmpty" xml:space="preserve">
</resheader>
<data name="InputTextIsEmpty" xml:space="preserve">
<value>The input text is empty</value>
</data>
<data name="SearchingFile" xml:space="preserve">
<data name="SearchingFile" xml:space="preserve">
<value>Find files...</value>
</data>
<data name="PathNotFound" xml:space="preserve">
<data name="PathNotFound" xml:space="preserve">
<value>The specified path "{0}" does not exist</value>
</data>
<data name="SearchingFileOK" xml:space="preserve">
<data name="SearchingFileOK" xml:space="preserve">
<value>{0} files</value>
</data>
<data name="ShowMessageTemp" xml:space="preserve">
<data name="ShowMessageTemp" xml:space="preserve">
<value>Read: {0}/{1}, processed: {2}, skipped: {3}</value>
</data>
<data name="Copied" xml:space="preserve">
<data name="Copied" xml:space="preserve">
<value>{0}(copied to clipboard)</value>
</data>
<data name="FileSearchPattern" xml:space="preserve">
<data name="FileSearchPattern" xml:space="preserve">
<value>File wildcards</value>
</data>
<data name="FolderPath" xml:space="preserve">
<data name="FolderPath" xml:space="preserve">
<value>Directory path to be processed</value>
</data>
<data name="ConvertEndOfLineToLF" xml:space="preserve">
<data name="ConvertEndOfLineToLF" xml:space="preserve">
<value>Convert newline characters to LF</value>
</data>
<data name="GuidTool" xml:space="preserve">
<data name="GuidTool" xml:space="preserve">
<value>GUID tool</value>
</data>
<data name="UseUppercase" xml:space="preserve">
<data name="UseUppercase" xml:space="preserve">
<value>Use uppercase output</value>
</data>
<data name="RandomPasswordGenerator" xml:space="preserve">
<data name="RandomPasswordGenerator" xml:space="preserve">
<value>Random password generator</value>
</data>
<data name="PwdLength" xml:space="preserve">
<data name="PwdLength" xml:space="preserve">
<value>Password length</value>
</data>
<data name="PwdGenerateTypes" xml:space="preserve">
<data name="PwdGenerateTypes" xml:space="preserve">
<value>BitSet 1[0-9]2[a-z]4[A-Z]8[ascii.0x21-0x2F]</value>
</data>
<data name="RemoveTrailingWhiteSpaces" xml:space="preserve">
<data name="RemoveTrailingWhiteSpaces" xml:space="preserve">
<value>Remove line breaks and spaces at the end of the file</value>
</data>
<data name="TrimUtf8Bom" xml:space="preserve">
<data name="TrimUtf8Bom" xml:space="preserve">
<value>Remove the uf8 bom of the file</value>
</data>
<data name="TextTobeProcessed" xml:space="preserve">
<data name="TextTobeProcessed" xml:space="preserve">
<value>Text to be processed (clipboard value is taken by default)</value>
</data>
<data name="PressAnyKey" xml:space="preserve">
<data name="PressAnyKey" xml:space="preserve">
<value>Press any key to continue...</value>
</data>
<data name="NoFileToBeProcessed" xml:space="preserve">
<data name="NoFileToBeProcessed" xml:space="preserve">
<value>No documents to be processed</value>
</data>
<data name="TimeoutMillSecs" xml:space="preserve">
<data name="TimeoutMillSecs" xml:space="preserve">
<value>Timeout for connecting to the NTP server (milliseconds)</value>
</data>
<data name="SyncToLocalTime" xml:space="preserve">
<data name="SyncToLocalTime" xml:space="preserve">
<value>Synchronize local time</value>
</data>
<data name="NtpReceiveDone" xml:space="preserve">
<data name="NtpReceiveDone" xml:space="preserve">
<value>Success {0}/{1}, the average value of the clock offset of the machine:{2}ms</value>
</data>
<data name="NtpServerCount" xml:space="preserve">
<data name="NtpServerCount" xml:space="preserve">
<value>{0}/{1} NTP servers</value>
</data>
<data name="NtpCalling" xml:space="preserve">
<data name="NtpCalling" xml:space="preserve">
<value>{0} In communication...</value>
</data>
<data name="LocalTimeOffset" xml:space="preserve">
<data name="LocalTimeOffset" xml:space="preserve">
<value>{0}, local clock offset: {1} ms</value>
</data>
<data name="LocalTimeSyncDone" xml:space="preserve">
<data name="LocalTimeSyncDone" xml:space="preserve">
<value>Local time has been synchronized</value>
</data>
<data name="Server" xml:space="preserve">
<data name="Server" xml:space="preserve">
<value>Server</value>
</data>
<data name="Status" xml:space="preserve">
<data name="Status" xml:space="preserve">
<value>Status</value>
</data>
<data name="LocalClockOffset" xml:space="preserve">
<data name="LocalClockOffset" xml:space="preserve">
<value>Local clock offset</value>
</data>
<data name="TimeTool" xml:space="preserve">
<data name="TimeTool" xml:space="preserve">
<value>Time synchronization tool</value>
</data>
<data name="TextTool" xml:space="preserve">
<data name="TextTool" xml:space="preserve">
<value>Text encoding tool</value>
</data>
<data name="ScreenPixelTool" xml:space="preserve">
<data name="ScreenPixelTool" xml:space="preserve">
<value>Screen coordinate color selection tool</value>
</data>
<data name="ClickCopyColor" xml:space="preserve">
<data name="ClickCopyColor" xml:space="preserve">
<value>Click the left mouse button to copy the colors and coordinates to the clipboard</value>
</data>
<data name="PublicIP" xml:space="preserve">
<data name="PublicIP" xml:space="preserve">
<value>Public network ip: </value>
</data>
<data name="ServerTime" xml:space="preserve">
<data name="ServerTime" xml:space="preserve">
<value>Synchronize local time</value>
</data>
<data name="Ip" xml:space="preserve">
<data name="Ip" xml:space="preserve">
<value>IP tools</value>
</data>
<data name="KeepSession" xml:space="preserve">
<data name="KeepSession" xml:space="preserve">
<value>Keep the session after executing the command</value>
</data>
<data name="NtpServerTime" xml:space="preserve">
<data name="NtpClock" xml:space="preserve">
<value>NTP server standard clock: {0}</value>
</data>
<data name="GitTool" xml:space="preserve">
<data name="GitTool" xml:space="preserve">
<value>Git batch operation tool</value>
</data>
<data name="GitArgs" xml:space="preserve">
<data name="GitArgs" xml:space="preserve">
<value>Parameters passed to Git</value>
</data>
<data name="Ok" xml:space="preserve">
<data name="Ok" xml:space="preserve">
<value>OK</value>
</data>
<data name="FindGitReps" xml:space="preserve">
<data name="FindGitReps" xml:space="preserve">
<value>Find all git repository directories under "{0}"...</value>
</data>
<data name="GitOutputEncoding" xml:space="preserve">
<data name="GitOutputEncoding" xml:space="preserve">
<value>Git output encoding</value>
</data>
<data name="MaxRecursionDepth" xml:space="preserve">
<data name="MaxRecursionDepth" xml:space="preserve">
<value>Directory search depth</value>
</data>
<data name="InvalidJsonString" xml:space="preserve">
<data name="InvalidJsonString" xml:space="preserve">
<value>Clipboard does not contain correct Json string</value>
</data>
<data name="Json" xml:space="preserve">
<data name="Json" xml:space="preserve">
<value>JsonTools</value>
</data>
<data name="CompressJson" xml:space="preserve">
<data name="CompressJson" xml:space="preserve">
<value>Compress Json text</value>
</data>
<data name="FormatJson" xml:space="preserve">
<data name="FormatJson" xml:space="preserve">
<value>Format JSON text</value>
</data>
<data name="GeneratorClass" xml:space="preserve">
<data name="GeneratorClass" xml:space="preserve">
<value>generate entity classes</value>
</data>
<data name="JsonToString" xml:space="preserve">
<data name="JsonToString" xml:space="preserve">
<value>Json text escaped into a string</value>
</data>
<data name="WriteMode" xml:space="preserve">
<data name="WriteMode" xml:space="preserve">
<value>Enable write mode</value>
</data>
<data name="ExerciseMode" xml:space="preserve">
<data name="ExerciseMode" xml:space="preserve">
<value>Read-only mode, the file will not be modified in real time!</value>
</data>
<data name="Read" xml:space="preserve">
<data name="Read" xml:space="preserve">
<value>read</value>
</data>
<data name="Write" xml:space="preserve">
<data name="Write" xml:space="preserve">
<value>write</value>
</data>
<data name="Break" xml:space="preserve">
<data name="Break" xml:space="preserve">
<value>skip</value>
</data>
<data name="WriteFileStats" xml:space="preserve">
<data name="WriteFileStats" xml:space="preserve">
<value>Write statistics</value>
</data>
<data name="LocalClock" xml:space="preserve">
<value>local clock</value>
</data>
<data name="ExcludePathRegexes" xml:space="preserve">
<value>Regular expression to exclude paths</value>
</data>
<data name="Exclude" xml:space="preserve">
<value>exclude</value>
</data>
<data name="Repository" xml:space="preserve">
<value>storehouse</value>
</data>
<data name="Command" xml:space="preserve">
<value>Order</value>
</data>
<data name="Response" xml:space="preserve">
<value>response</value>
</data>
<data name="ZeroCode" xml:space="preserve">
<value>Git exit code is zero</value>
</data>
</root>

View File

@ -82,6 +82,9 @@
<data name="SyncToLocalTime" xml:space="preserve">
<value>同步本机时间</value>
</data>
<data name="LocalClock" xml:space="preserve">
<value>本机时钟</value>
</data>
<data name="ServerTime" xml:space="preserve">
<value>同步本机时间</value>
</data>
@ -161,8 +164,8 @@
<data name="LocalTimeOffset" xml:space="preserve">
<value>{0}, 本机时钟偏移: {1} ms</value>
</data>
<data name="NtpServerTime" xml:space="preserve">
<value>NTP 服务器标准时钟: {0}</value>
<data name="NtpClock" xml:space="preserve">
<value>NTP 标准时钟</value>
</data>
<data name="LocalTimeSyncDone" xml:space="preserve">
<value>本机时间已同步</value>
@ -186,7 +189,7 @@
<value>OK</value>
</data>
<data name="FindGitReps" xml:space="preserve">
<value>查找 "{0}" 下所有git仓库目录... </value>
<value>查找 "{0}" 下所有git仓库目录 </value>
</data>
<data name="ExerciseMode" xml:space="preserve">
<value>只读模式, 不会真实修改文件!</value>
@ -205,4 +208,16 @@
<data name="WriteFileStats" xml:space="preserve">
<value>写入统计</value>
</data>
<data name="Repository" xml:space="preserve">
<value>仓库</value>
</data>
<data name="Command" xml:space="preserve">
<value>命令</value>
</data>
<data name="Response" xml:space="preserve">
<value>响应</value>
</data>
<data name="ZeroCode" xml:space="preserve">
<value>Git退出码为零的</value>
</data>
</root>

View File

@ -14,13 +14,8 @@ Type[] LoadVerbs()
async Task Run(object args)
{
if (args is not OptionBase option) return;
var tool = ToolsFactory.Create(option);
await tool.Run();
if (option!.KeepSession) {
AnsiConsole.MarkupLine(Str.PressAnyKey);
AnsiConsole.Console.Input.ReadKey(true);
}
}

View File

@ -15,7 +15,7 @@ public sealed class Main : ToolBase<Option>
public Main(Option opt) : base(opt) { }
public override Task Run()
protected override Task Core()
{
unsafe {
var pSource = stackalloc char[_charTable.Sum(x => x.Length)];

View File

@ -99,7 +99,7 @@ html-decode: {o.HtmlDecode}
Console.WriteLine(outputTemp);
}
public override async Task Run()
protected override async Task Core()
{
if (Opt.Text.NullOrEmpty()) Opt.Text = await ClipboardService.GetTextAsync();
if (Opt.Text.NullOrEmpty()) throw new ArgumentException(Str.InputTextIsEmpty);

View File

@ -1,98 +1,37 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets;
namespace Dot.Time;
public sealed class Main : ToolBase<Option>
{
private record Server
{
public int ConsoleRowIndex;
public TimeSpan Offset;
public ServerStatues Status;
}
private const int _MAX_DEGREE_OF_PARALLELISM = 10;
private const int _NTP_PORT = 123;
private enum ServerStatues : byte
{
Ready
, Connecting
, Succeed
, Failed
}
private readonly string[] _ntpServers = {
"ntp.ntsc.ac.cn", "cn.ntp.org.cn", "edu.ntp.org.cn", "cn.pool.ntp.org"
, "time.pool.aliyun.com", "time1.aliyun.com", "time2.aliyun.com"
, "time3.aliyun.com", "time4.aliyun.com", "time5.aliyun.com"
, "time6.aliyun.com", "time7.aliyun.com", "time1.cloud.tencent.com"
, "time2.cloud.tencent.com", "time3.cloud.tencent.com"
, "time4.cloud.tencent.com", "time5.cloud.tencent.com", "ntp.sjtu.edu.cn"
, "ntp.neu.edu.cn", "ntp.bupt.edu.cn", "ntp.shu.edu.cn", "pool.ntp.org"
, "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"
, "asia.pool.ntp.org", "time1.google.com", "time2.google.com"
, "time3.google.com", "time4.google.com", "time.apple.com"
, "time1.apple.com", "time2.apple.com", "time3.apple.com"
, "time4.apple.com", "time5.apple.com", "time6.apple.com"
, "time7.apple.com", "time.windows.com", "time.nist.gov"
, "time-nw.nist.gov", "time-a.nist.gov", "time-b.nist.gov"
, "stdtime.gov.hk"
};
private double _offsetAvg;
private const int _MAX_DEGREE_OF_PARALLELISM = 10;
private const int _NTP_PORT = 123;
private const string _OUTPUT_TEMP = "{0,-30} {1,20} {2,20}";
private static readonly object _lock = new();
private int _procedCnt;
private readonly int _serverCnt;
private readonly string[] _srvAddr = {
"ntp.ntsc.ac.cn", "cn.ntp.org.cn", "edu.ntp.org.cn", "cn.pool.ntp.org"
, "time.pool.aliyun.com", "time1.aliyun.com", "time2.aliyun.com"
, "time3.aliyun.com", "time4.aliyun.com", "time5.aliyun.com"
, "time6.aliyun.com", "time7.aliyun.com", "time1.cloud.tencent.com"
, "time2.cloud.tencent.com", "time3.cloud.tencent.com"
, "time4.cloud.tencent.com", "time5.cloud.tencent.com", "ntp.sjtu.edu.cn"
, "ntp.neu.edu.cn", "ntp.bupt.edu.cn", "ntp.shu.edu.cn", "pool.ntp.org"
, "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"
, "asia.pool.ntp.org", "time1.google.com", "time2.google.com"
, "time3.google.com", "time4.google.com", "time.apple.com", "time1.apple.com"
, "time2.apple.com", "time3.apple.com", "time4.apple.com", "time5.apple.com"
, "time6.apple.com", "time7.apple.com", "time.windows.com", "time.nist.gov"
, "time-nw.nist.gov", "time-a.nist.gov", "time-b.nist.gov", "stdtime.gov.hk"
};
private readonly Dictionary<string, Server> _srvStatus;
private int _successCnt;
private int _successCnt;
public Main(Option opt) : base(opt)
{
_serverCnt = _srvAddr.Length;
var i = 0;
_srvStatus = _srvAddr.ToDictionary(
x => x, _ => new Server { Status = ServerStatues.Ready, ConsoleRowIndex = ++i });
}
private static void ChangeStatus(KeyValuePair<string, Server> server, ServerStatues status
, TimeSpan offset = default)
{
server.Value.Status = status;
server.Value.Offset = offset;
DrawTextInConsole(0, server.Value.ConsoleRowIndex
, string.Format(_OUTPUT_TEMP, server.Key, server.Value.Status
, status == ServerStatues.Succeed ? server.Value.Offset : string.Empty));
}
private async Task DrawLoading()
{
char[] loading = { '-', '\\', '|', '/' };
var loadingIndex = 0;
while (true) {
if (Volatile.Read(ref _procedCnt) == _serverCnt) break;
await Task.Delay(100);
++loadingIndex;
for (var i = 0; i != _serverCnt; ++i)
DrawTextInConsole(
34, i + 1
, _srvStatus[_srvAddr[i]].Status is ServerStatues.Succeed or ServerStatues.Failed
? " "
: loading[loadingIndex % 4].ToString());
}
Debug.WriteLine(Environment.CurrentManagedThreadId + ":" + DateTime.Now.ToString("O"));
}
private static void DrawTextInConsole(int left, int top, string text)
{
lock (_lock) {
Console.SetCursorPosition(left, top);
Console.Write(text);
}
}
public Main(Option opt) : base(opt) { }
private TimeSpan GetNtpOffset(string server)
@ -132,32 +71,20 @@ public sealed class Main : ToolBase<Option>
}
}
private void PrintTemplate()
private ValueTask ServerHandle(KeyValuePair<string, ProgressTask> payload, CancellationToken _)
{
Console.Clear();
Console.CursorVisible = false;
var row = //
_srvStatus.Select(x //
=> string.Format(_OUTPUT_TEMP, x.Key, x.Value.Status
, x.Value.Offset == TimeSpan.Zero ? string.Empty : x.Value.Offset));
Console.WriteLine(_OUTPUT_TEMP, Str.Server, Str.Status, Str.LocalClockOffset);
Console.WriteLine(string.Join(Environment.NewLine, row));
}
private ValueTask ServerHandle(KeyValuePair<string, Server> server)
{
ChangeStatus(server, ServerStatues.Connecting);
var offset = GetNtpOffset(server.Key);
Interlocked.Increment(ref _procedCnt);
payload.Value.StartTask();
payload.Value.State.Status(TaskStatusColumn.Statues.Connecting);
var offset = GetNtpOffset(payload.Key);
if (offset == TimeSpan.Zero) {
ChangeStatus(server, ServerStatues.Failed);
payload.Value.State.Status(TaskStatusColumn.Statues.Failed);
payload.Value.IsIndeterminate(false).Increment(0);
}
else {
payload.Value.State.Status(TaskStatusColumn.Statues.Succeed);
payload.Value.State.Result(offset);
Interlocked.Increment(ref _successCnt);
ChangeStatus(server, ServerStatues.Succeed, offset);
payload.Value.IsIndeterminate(false).Increment(100);
}
return ValueTask.CompletedTask;
@ -179,46 +106,67 @@ public sealed class Main : ToolBase<Option>
Win32.SetLocalTime(timeToSet);
}
[SuppressMessage("ReSharper", "AccessToDisposedClosure")]
protected override async Task Core()
{
await AnsiConsole.Progress()
.Columns(new TaskDescriptionColumn() //
, new ProgressBarColumn() //
, new ElapsedTimeColumn() //
, new SpinnerColumn() //
, new TaskStatusColumn() //
, new TaskResultColumn())
.StartAsync(async ctx => {
var tasks = new Dictionary<string, ProgressTask>();
foreach (var server in _ntpServers) {
var t = ctx.AddTask(server, false).IsIndeterminate();
tasks.Add(server, t);
}
await Parallel.ForEachAsync(
tasks, new ParallelOptions { MaxDegreeOfParallelism = _MAX_DEGREE_OF_PARALLELISM }
, ServerHandle);
_offsetAvg = tasks.Where(x => x.Value.State.Status() == TaskStatusColumn.Statues.Succeed)
.Average(x => x.Value.State.Result().TotalMilliseconds);
});
AnsiConsole.MarkupLine(Str.NtpReceiveDone, $"[green]{_successCnt}[/]", _ntpServers.Length
, $"[yellow]{_offsetAvg:f2}[/]");
if (Opt.Sync) {
SetSysteTime(DateTime.Now.AddMilliseconds(-_offsetAvg));
AnsiConsole.MarkupLine($"[green]{Str.LocalTimeSyncDone}[/]");
}
}
public override async Task Run()
{
PrintTemplate();
var tLoading = DrawLoading();
await Core();
if (Opt.KeepSession) {
var table = new Table().HideHeaders()
.AddColumn(new TableColumn(string.Empty))
.AddColumn(new TableColumn(string.Empty))
.Caption(Str.PressAnyKey)
.AddRow(Str.NtpClock, DateTime.Now.AddMilliseconds(-_offsetAvg).ToString("O"))
.AddRow(Str.LocalClock, DateTime.Now.ToString("O"));
var cts = new CancellationTokenSource();
var task = AnsiConsole.Live(table)
.StartAsync(async ctx => {
while (!cts.IsCancellationRequested) {
ctx.UpdateTarget(
table.UpdateCell(
0, 1, DateTime.Now.AddMilliseconds(-_offsetAvg).ToString("O"))
.UpdateCell(1, 1, DateTime.Now.ToString("O")));
await Task.Delay(100);
}
});
await Parallel.ForEachAsync(_srvStatus
, new ParallelOptions { MaxDegreeOfParallelism = _MAX_DEGREE_OF_PARALLELISM }
, (server, _) => ServerHandle(server));
await tLoading;
var avgOffset = TimeSpan.FromTicks((long)_srvStatus //
.Where(x => x.Value.Status == ServerStatues.Succeed)
.Average(x => x.Value.Offset.Ticks));
Console.SetCursorPosition(0, _serverCnt + 1);
Console.WriteLine(Str.NtpReceiveDone, _successCnt, _serverCnt, avgOffset.TotalMilliseconds);
if (!Opt.Sync) {
if (!Opt.KeepSession) return;
var waitObj = new ManualResetEvent(false);
var _ = Task.Run(async () => {
var top = Console.GetCursorPosition().Top;
while (true) {
Console.SetCursorPosition(0, top);
Console.Write(Str.NtpServerTime, (DateTime.Now - avgOffset).ToString("O"));
waitObj.Set();
await Task.Delay(100);
}
// ReSharper disable once FunctionNeverReturns
});
waitObj.WaitOne();
return;
await AnsiConsole.Console.Input.ReadKeyAsync(true, cts.Token);
cts.Cancel();
await task;
}
SetSysteTime(DateTime.Now - avgOffset);
Console.WriteLine(Str.LocalTimeSyncDone);
}
}

View File

@ -0,0 +1,24 @@
namespace Dot.Time;
public static class ProgressTaskStateExtensions
{
public static TimeSpan Result(this ProgressTaskState me)
{
return me.Get<TimeSpan>(nameof(TaskResultColumn));
}
public static void Result(this ProgressTaskState me, TimeSpan value)
{
me.Update<TimeSpan>(nameof(TaskResultColumn), _ => value);
}
public static TaskStatusColumn.Statues Status(this ProgressTaskState me)
{
return me.Get<TaskStatusColumn.Statues>(nameof(TaskStatusColumn));
}
public static void Status(this ProgressTaskState me, TaskStatusColumn.Statues value)
{
me.Update<TaskStatusColumn.Statues>(nameof(TaskStatusColumn), _ => value);
}
}

View File

@ -0,0 +1,18 @@
using Spectre.Console.Rendering;
namespace Dot.Time;
public class TaskResultColumn : ProgressColumn
{
/// <summary>
/// Gets or sets the alignment of the task description.
/// </summary>
public Justify Alignment { get; set; } = Justify.Right;
/// <inheritdoc />
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var text = task.State.Get<TimeSpan>(nameof(TaskResultColumn));
return new Markup(text.ToString()).Overflow(Overflow.Ellipsis).Justify(Alignment);
}
}

View File

@ -0,0 +1,35 @@
using System.ComponentModel;
using NSExt.Extensions;
using Spectre.Console.Rendering;
namespace Dot.Time;
public class TaskStatusColumn : ProgressColumn
{
public enum Statues : byte
{
[Description($"[gray]{nameof(Ready)}[/]")]
Ready
, [Description($"[yellow]{nameof(Connecting)}[/]")]
Connecting
, [Description($"[green]{nameof(Succeed)}[/]")]
Succeed
, [Description($"[red]{nameof(Failed)}[/]")]
Failed
}
/// <summary>
/// Gets or sets the alignment of the task description.
/// </summary>
public Justify Alignment { get; set; } = Justify.Right;
/// <inheritdoc />
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var text = task.State.Get<Statues>(nameof(TaskStatusColumn));
return new Markup(text.Desc()).Overflow(Overflow.Ellipsis).Justify(Alignment);
}
}

View File

@ -27,6 +27,8 @@ public abstract class ToolBase<TOption> : ITool where TOption : OptionBase
}
}
protected abstract Task Core();
protected static Task LoadingAnimate(int x, int y, out CancellationTokenSource cts)
{
@ -50,6 +52,12 @@ public abstract class ToolBase<TOption> : ITool where TOption : OptionBase
});
}
public abstract Task Run();
public virtual async Task Run()
{
await Core();
if (Opt.KeepSession) {
AnsiConsole.MarkupLine(Str.PressAnyKey);
AnsiConsole.Console.Input.ReadKey(true);
}
}
}