<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 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 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 Main(Option opt) : base(opt) { }
public override Task Run() protected override Task Core()
{ {
Application.Run(new WinMain()); Application.Run(new WinMain());
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -45,6 +45,39 @@ public abstract class FilesTool<TOption> : ToolBase<TOption> where TOption : Dir
return fileList; 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) 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); _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.Diagnostics;
using System.Text; using System.Text;
using NSExt.Extensions; using NSExt.Extensions;
@ -6,13 +7,9 @@ namespace Dot.Git;
public class Main : ToolBase<Option> public class Main : ToolBase<Option>
{ {
private const int _POS_Y_MSG = 74; //git command rsp 显示的位置 y private readonly Encoding _gitOutputEnc; //git command rsp 编码
private const int _POST_Y_LOADING = 70; //loading 动画显示的位置 y private ConcurrentDictionary<string, StringBuilder> _repoRsp; //仓库信息容器
private const int _REP_PATH_LENGTH_LIMIT = 32; //仓库路径长度显示截断阈值 private ConcurrentDictionary<string, TaskStatusColumn.Statues> _repoStatus;
private (int x, int y) _cursorPosBackup; //光标位置备份
private readonly Encoding _gitOutputEnc; //git command rsp 编码
private List<string> _repoPathList; //仓库目录列表
public Main(Option opt) : base(opt) 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); // 行号 payload.Value.StartTask();
var tAnimate = LoadingAnimate(_POST_Y_LOADING, _cursorPosBackup.y + row, out var cts); payload.Value.State.Status(TaskStatusColumn.Statues.Executing);
// 打印 git command rsp // 打印 git command rsp
void ExecRspReceived(object sender, DataReceivedEventArgs e) void ExecRspReceived(object sender, DataReceivedEventArgs e)
{ {
if (e.Data is null) return; if (e.Data is null) return;
var msg = Encoding.UTF8.GetString(_gitOutputEnc.GetBytes(e.Data)); var msg = Encoding.UTF8.GetString(_gitOutputEnc.GetBytes(e.Data));
ConcurrentWrite(_POS_Y_MSG, _cursorPosBackup.y + row, new string(' ', Console.WindowWidth - _POS_Y_MSG)); _repoRsp[payload.Key].Append(msg.EscapeMarkup());
ConcurrentWrite(_POS_Y_MSG, _cursorPosBackup.y + row, msg);
} }
// 启动git进程 // 启动git进程
{ {
var startInfo = new ProcessStartInfo { var startInfo = new ProcessStartInfo {
CreateNoWindow = true CreateNoWindow = true
, WorkingDirectory = dir , WorkingDirectory = payload.Key
, FileName = "git" , FileName = "git"
, Arguments = Opt.Args , Arguments = Opt.Args
, UseShellExecute = false , UseShellExecute = false
@ -54,58 +50,78 @@ public class Main : ToolBase<Option>
p.BeginOutputReadLine(); p.BeginOutputReadLine();
p.BeginErrorReadLine(); p.BeginErrorReadLine();
await p.WaitForExitAsync(); 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仓库目录 var progressBar = new ProgressBarColumn { Width = 10 };
{ await AnsiConsole.Progress()
Console.Write(Str.FindGitReps, Opt.Path); .Columns(progressBar //
StashCurorPos(); , 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); _repoRsp = new ConcurrentDictionary<string, StringBuilder>();
_repoPathList = Directory.GetDirectories(Opt.Path, ".git" // _repoStatus = new ConcurrentDictionary<string, TaskStatusColumn.Statues>();
, new EnumerationOptions // var tasks = new Dictionary<string, ProgressTask>();
{ foreach (var path in paths) {
MaxRecursionDepth = Opt.MaxRecursionDepth _repoRsp.TryAdd(path, new StringBuilder());
, RecurseSubdirectories = true _repoStatus.TryAdd(path, default);
, IgnoreInaccessible = true var task = ctx.AddTask(new DirectoryInfo(path).Name, false).IsIndeterminate();
, AttributesToSkip = FileAttributes.ReparsePoint tasks.Add(path, task);
}) }
.Select(x => Directory.GetParent(x)!.FullName)
.ToList(); taskFinder.IsIndeterminate(false);
cts.Cancel(); taskFinder.Increment(100);
await tAnimate; taskFinder.State.Status(TaskStatusColumn.Statues.Succeed);
cts.Dispose();
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仓库目录 AnsiConsole.Write(table);
{
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);
} }
} }

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 Main(Option opt) : base(opt) { }
public override Task Run() protected override Task Core()
{ {
var guid = System.Guid.NewGuid().ToString(); var guid = System.Guid.NewGuid().ToString();
if (Opt.Upper) guid = guid.ToUpper(); 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 Main(Option opt) : base(opt) { }
public override async Task Run() protected override async Task Core()
{ {
foreach (var item in NetworkInterface.GetAllNetworkInterfaces()) { foreach (var item in NetworkInterface.GetAllNetworkInterfaces()) {
if (item.NetworkInterfaceType != NetworkInterfaceType.Ethernet || if (item.NetworkInterfaceType != NetworkInterfaceType.Ethernet ||

View File

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

View File

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

View File

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

View File

@ -14,13 +14,8 @@ Type[] LoadVerbs()
async Task Run(object args) async Task Run(object args)
{ {
if (args is not OptionBase option) return; if (args is not OptionBase option) return;
var tool = ToolsFactory.Create(option); var tool = ToolsFactory.Create(option);
await tool.Run(); 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 Main(Option opt) : base(opt) { }
public override Task Run() protected override Task Core()
{ {
unsafe { unsafe {
var pSource = stackalloc char[_charTable.Sum(x => x.Length)]; var pSource = stackalloc char[_charTable.Sum(x => x.Length)];

View File

@ -99,7 +99,7 @@ html-decode: {o.HtmlDecode}
Console.WriteLine(outputTemp); 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()) Opt.Text = await ClipboardService.GetTextAsync();
if (Opt.Text.NullOrEmpty()) throw new ArgumentException(Str.InputTextIsEmpty); 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; using System.Net.Sockets;
namespace Dot.Time; namespace Dot.Time;
public sealed class Main : ToolBase<Option> public sealed class Main : ToolBase<Option>
{ {
private record Server private const int _MAX_DEGREE_OF_PARALLELISM = 10;
{ private const int _NTP_PORT = 123;
public int ConsoleRowIndex;
public TimeSpan Offset;
public ServerStatues Status;
}
private enum ServerStatues : byte private readonly string[] _ntpServers = {
{ "ntp.ntsc.ac.cn", "cn.ntp.org.cn", "edu.ntp.org.cn", "cn.pool.ntp.org"
Ready , "time.pool.aliyun.com", "time1.aliyun.com", "time2.aliyun.com"
, Connecting , "time3.aliyun.com", "time4.aliyun.com", "time5.aliyun.com"
, Succeed , "time6.aliyun.com", "time7.aliyun.com", "time1.cloud.tencent.com"
, Failed , "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 int _successCnt;
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;
public Main(Option opt) : base(opt) 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);
}
}
private TimeSpan GetNtpOffset(string server) 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(); payload.Value.StartTask();
Console.CursorVisible = false; payload.Value.State.Status(TaskStatusColumn.Statues.Connecting);
var row = // var offset = GetNtpOffset(payload.Key);
_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);
if (offset == TimeSpan.Zero) { if (offset == TimeSpan.Zero) {
ChangeStatus(server, ServerStatues.Failed); payload.Value.State.Status(TaskStatusColumn.Statues.Failed);
payload.Value.IsIndeterminate(false).Increment(0);
} }
else { else {
payload.Value.State.Status(TaskStatusColumn.Statues.Succeed);
payload.Value.State.Result(offset);
Interlocked.Increment(ref _successCnt); Interlocked.Increment(ref _successCnt);
ChangeStatus(server, ServerStatues.Succeed, offset); payload.Value.IsIndeterminate(false).Increment(100);
} }
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
@ -179,46 +106,67 @@ public sealed class Main : ToolBase<Option>
Win32.SetLocalTime(timeToSet); 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() public override async Task Run()
{ {
PrintTemplate(); await Core();
var tLoading = DrawLoading(); 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 await AnsiConsole.Console.Input.ReadKeyAsync(true, cts.Token);
, new ParallelOptions { MaxDegreeOfParallelism = _MAX_DEGREE_OF_PARALLELISM } cts.Cancel();
, (server, _) => ServerHandle(server)); await task;
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;
} }
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) 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 virtual async Task Run()
public abstract Task Run(); {
await Core();
if (Opt.KeepSession) {
AnsiConsole.MarkupLine(Str.PressAnyKey);
AnsiConsole.Console.Input.ReadKey(true);
}
}
} }