mirror of
https://github.com/nsnail/dot.git
synced 2025-06-19 05:48:16 +08:00
<refactor> ui美化...
This commit is contained in:
17
safe-delete-unused-resx.ahk
Normal file
17
safe-delete-unused-resx.ahk
Normal 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
|
||||
}
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
112
src/Git/Main.cs
112
src/Git/Main.cs
@ -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 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();
|
||||
}
|
||||
|
||||
cts.Cancel();
|
||||
await tAnimate;
|
||||
cts.Dispose();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 tAnimate = LoadingAnimate(_cursorPosBackup.x, _cursorPosBackup.y, out var cts);
|
||||
_repoPathList = Directory.GetDirectories(Opt.Path, ".git" //
|
||||
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
|
||||
, AttributesToSkip
|
||||
= FileAttributes.ReparsePoint
|
||||
})
|
||||
.Select(x => Directory.GetParent(x)!.FullName)
|
||||
.ToList();
|
||||
cts.Cancel();
|
||||
await tAnimate;
|
||||
cts.Dispose();
|
||||
.Select(x => Directory.GetParent(x)!.FullName);
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
// 打印git仓库目录
|
||||
{
|
||||
Console.WriteLine(Str.Ok);
|
||||
StashCurorPos();
|
||||
taskFinder.IsIndeterminate(false);
|
||||
taskFinder.Increment(100);
|
||||
taskFinder.State.Status(TaskStatusColumn.Statues.Succeed);
|
||||
|
||||
var i = 0;
|
||||
Console.WriteLine( //
|
||||
string.Join(Environment.NewLine
|
||||
, _repoPathList.Select(
|
||||
x => $"{++i}: {new DirectoryInfo(x).Name.Sub(0, _REP_PATH_LENGTH_LIMIT)}"))
|
||||
//
|
||||
);
|
||||
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命令
|
||||
await Parallel.ForEachAsync(_repoPathList, DirHandle);
|
||||
Console.SetCursorPosition(_cursorPosBackup.x, _cursorPosBackup.y + _repoPathList.Count);
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
}
|
14
src/Git/ProgressTaskStateExtensions.cs
Normal file
14
src/Git/ProgressTaskStateExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
35
src/Git/TaskStatusColumn.cs
Normal file
35
src/Git/TaskStatusColumn.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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 ||
|
||||
|
@ -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)
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?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: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">
|
||||
@ -130,7 +131,7 @@
|
||||
<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">
|
||||
@ -187,4 +188,25 @@
|
||||
<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>
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)];
|
||||
|
@ -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);
|
||||
|
202
src/Time/Main.cs
202
src/Time/Main.cs
@ -1,35 +1,13 @@
|
||||
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 enum ServerStatues : byte
|
||||
{
|
||||
Ready
|
||||
, Connecting
|
||||
, Succeed
|
||||
, Failed
|
||||
}
|
||||
|
||||
|
||||
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 = {
|
||||
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"
|
||||
@ -39,60 +17,21 @@ public sealed class Main : ToolBase<Option>
|
||||
, "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"
|
||||
, "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 double _offsetAvg;
|
||||
|
||||
|
||||
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"));
|
||||
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
// ReSharper disable once FunctionNeverReturns
|
||||
});
|
||||
waitObj.WaitOne();
|
||||
return;
|
||||
}
|
||||
|
||||
SetSysteTime(DateTime.Now - avgOffset);
|
||||
Console.WriteLine(Str.LocalTimeSyncDone);
|
||||
await AnsiConsole.Console.Input.ReadKeyAsync(true, cts.Token);
|
||||
cts.Cancel();
|
||||
await task;
|
||||
}
|
||||
}
|
||||
}
|
24
src/Time/ProgressTaskStateExtensions.cs
Normal file
24
src/Time/ProgressTaskStateExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
18
src/Time/TaskResultColumn.cs
Normal file
18
src/Time/TaskResultColumn.cs
Normal 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);
|
||||
}
|
||||
}
|
35
src/Time/TaskStatusColumn.cs
Normal file
35
src/Time/TaskStatusColumn.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user