refactor: ♻️ 2.0 (#13)

This commit is contained in:
2023-12-13 18:43:53 +08:00
committed by GitHub
parent 247e35484c
commit 19f3405a36
288 changed files with 24348 additions and 2141 deletions

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="xunit" Version="2.6.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<ProjectReference Include="../Dot/Dot.csproj"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
#if NET8_0_WINDOWS
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Dot.Color;
[Description(nameof(Ln.屏幕坐标颜色选取工具))]
[Localization(typeof(Ln))]
[SupportedOSPlatform(nameof(OSPlatform.Windows))]
// ReSharper disable once ClassNeverInstantiated.Global
internal sealed class Main : ToolBase<Option>
{
protected override Task CoreAsync()
{
Application.Run(new WinMain());
return Task.CompletedTask;
}
}
#endif

View File

@ -0,0 +1,5 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.Color;
internal sealed class Option : OptionBase;

View File

@ -0,0 +1,95 @@
#if NET8_0_WINDOWS
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Size = System.Drawing.Size;
namespace Dot.Color;
[SupportedOSPlatform(nameof(OSPlatform.Windows))]
internal sealed class WinInfo : Form
{
private const int _WINDOW_SIZE = 480; // 窗口大小
private const int _ZOOM_RATE = 16; // 缩放倍率
private readonly Graphics _graphics;
private readonly PictureBox _pbox;
private bool _disposed;
public WinInfo()
{
#pragma warning disable IDE0017
FormBorderStyle = FormBorderStyle.None;
TopMost = true;
MinimizeBox = false;
MaximizeBox = false;
Size = new Size(_WINDOW_SIZE, _WINDOW_SIZE);
StartPosition = FormStartPosition.Manual;
Location = new Point(0, 0);
_pbox = new PictureBox();
_pbox.Location = new Point(0, 0);
_pbox.Size = Size;
_pbox.Image = new Bitmap(_WINDOW_SIZE, _WINDOW_SIZE);
_graphics = Graphics.FromImage(_pbox.Image);
_graphics.InterpolationMode = InterpolationMode.NearestNeighbor; // 指定最临近插值法,禁止平滑缩放(模糊)
_graphics.CompositingQuality = CompositingQuality.HighQuality;
_graphics.SmoothingMode = SmoothingMode.None;
_pbox.MouseEnter += PboxOnMouseEnter;
Controls.Add(_pbox);
#pragma warning restore IDE0017
}
~WinInfo()
{
Dispose(false);
}
public void UpdateImage(Bitmap img, int x, int y)
{
// 计算复制小图的区域
var copySize = new Size(_WINDOW_SIZE / _ZOOM_RATE, _WINDOW_SIZE / _ZOOM_RATE);
_graphics.DrawImage(img, new Rectangle(0, 0, _WINDOW_SIZE, _WINDOW_SIZE) //
, x - copySize.Width / 2 // 左移x使光标位置居中
, y - copySize.Height / 2 // 上移y使光标位置居中
, copySize.Width, copySize.Height, GraphicsUnit.Pixel);
using var pen = new Pen(System.Drawing.Color.Aqua); // 绘制准星
_graphics.DrawRectangle(pen, _WINDOW_SIZE / 2 - _ZOOM_RATE / 2 //
, _WINDOW_SIZE / 2 - _ZOOM_RATE / 2 //
, _ZOOM_RATE, _ZOOM_RATE);
// 取鼠标位置颜色
var posColor = img.GetPixel(x, y);
// 绘制底部文字信息
_graphics.FillRectangle(Brushes.Black, 0, _WINDOW_SIZE - 30, _WINDOW_SIZE, 30);
_graphics.DrawString( //
$"{Ln.单击鼠标左键复制颜色和坐标到剪贴板} X: {x} Y: {y} RGB({posColor.R},{posColor.G},{posColor.B})"
, new Font(FontFamily.GenericSerif, 10) //
, Brushes.White, 0, _WINDOW_SIZE - 20);
// 触发重绘
_pbox.Refresh();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_disposed) {
return;
}
if (disposing) {
_graphics?.Dispose();
_pbox?.Dispose();
}
_disposed = true;
}
private void PboxOnMouseEnter(object sender, EventArgs e)
{
// 信息窗口避开鼠标指针指向区域
Location = new Point(Location.X, Location.Y == 0 ? Screen.PrimaryScreen!.Bounds.Height - _WINDOW_SIZE : 0);
}
}
#endif

View File

@ -0,0 +1,84 @@
#if NET8_0_WINDOWS
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Dot.Native;
using TextCopy;
namespace Dot.Color;
[SupportedOSPlatform(nameof(OSPlatform.Windows))]
internal sealed class WinMain : Form
{
private readonly Bitmap _bmp;
private readonly WinInfo _winInfo = new(); // 小图窗口
private bool _disposed;
public WinMain()
{
// 隐藏控制台窗口,避免捕获到截屏
_ = Win32.ShowWindow(Win32.GetConsoleWindow(), Win32.SW_HIDE);
FormBorderStyle = FormBorderStyle.None;
Size = Screen.PrimaryScreen!.Bounds.Size;
StartPosition = FormStartPosition.Manual;
Location = new Point(0, 0);
Opacity = 0.01d; // 主窗体加载截图过程设置为透明避免闪烁
_bmp = new Bitmap(Size.Width, Size.Height);
using var g = Graphics.FromImage(_bmp);
g.CopyFromScreen(0, 0, 0, 0, Size);
}
~WinMain()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_disposed) {
return;
}
if (disposing) {
_bmp?.Dispose();
_winInfo?.Dispose();
}
_disposed = true;
}
protected override void OnKeyUp(KeyEventArgs e)
{
if (e.KeyCode == Keys.Escape) {
Application.Exit();
}
}
protected override void OnLoad(EventArgs e)
{
_winInfo.Show();
}
protected override void OnMouseDown(MouseEventArgs e)
{
var color = _bmp.GetPixel(e.X, e.Y);
ClipboardService.SetText($"{e.X},{e.Y} #{color.R:X2}{color.G:X2}{color.B:X2}({color.R},{color.G},{color.B})");
Application.Exit();
}
protected override void OnMouseMove(MouseEventArgs e)
{
// 移动鼠标时更新小图窗口
_winInfo.UpdateImage(_bmp, e.X, e.Y);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawImage(_bmp, 0, 0);
Opacity = 1;
}
}
#endif

View File

@ -0,0 +1,60 @@
using System.Diagnostics;
namespace Dot;
// ReSharper disable once UnusedType.Global
// ReSharper disable once UnusedMember.Global
internal static class CsxEditor
{
private static readonly string[] _imageExts = { "*.jpg", "*.jpeg" };
// ReSharper disable once UnusedMember.Local
#pragma warning disable S1144, RCS1213, IDE0051
private static void Run()
#pragma warning restore IDE0051, RCS1213, S1144
{
/*
for %%i in (*.png) do pngquant %%i --force --output %%i --skip-if-larger
for %%i in (*.jpg) do jpegtran -copy none -optimize -perfect %%i %%i
*
*/
var files = Directory.EnumerateFiles(".", "*.png"
, new EnumerationOptions {
RecurseSubdirectories = true
, AttributesToSkip = FileAttributes.ReparsePoint
, IgnoreInaccessible = true
})
.ToArray();
_ = Parallel.ForEach(files, file => {
var startInfo = new ProcessStartInfo {
FileName = "pngquant"
, Arguments
= $"\"{file}\" --force --output \"{file}\" --skip-if-larger"
};
using var p = Process.Start(startInfo);
p!.WaitForExit();
Console.WriteLine(p.ExitCode);
});
files = _imageExts.SelectMany(x => Directory.EnumerateFiles(
".", x
, new EnumerationOptions {
RecurseSubdirectories = true
, AttributesToSkip = FileAttributes.ReparsePoint
, IgnoreInaccessible = true
}))
.ToArray();
_ = Parallel.ForEach(files, file => {
var startInfo = new ProcessStartInfo {
FileName = "jpegtran"
, Arguments = $"-copy none -optimize -perfect \"{file}\" \"{file}\""
};
using var p = Process.Start(startInfo);
p!.WaitForExit();
Console.WriteLine(p.ExitCode);
});
}
}

View File

@ -0,0 +1,35 @@
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Dot;
internal abstract class DirOption : OptionBase
{
[CommandOption("-e|--exclude")]
[Description(nameof(Ln.排除路径的正则表达式))]
[Localization(typeof(Ln))]
public string[] ExcludeRegexes { get; set; }
[CommandOption("-f|--filter")]
[Description(nameof(Ln.文件通配符))]
[Localization(typeof(Ln))]
[DefaultValue("*")]
public string Filter { get; set; }
[CommandOption("-d|--max-depth")]
[Description(nameof(Ln.目录检索深度))]
[Localization(typeof(Ln))]
[DefaultValue(int.MaxValue)]
public int MaxRecursionDepth { get; set; }
[CommandArgument(0, "[PATH]")]
[Description(nameof(Ln.要处理的目录路径))]
[Localization(typeof(Ln))]
[DefaultValue(".")]
public string Path { get; set; }
[CommandOption("-w|--write")]
[Description(nameof(Ln.启用写入模式))]
[Localization(typeof(Ln))]
[DefaultValue(false)]
public bool WriteMode { get; set; }
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>dot</AssemblyName>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<OutputType>Exe</OutputType>
<PublishSingleFile>true</PublishSingleFile>
<RootNamespace>Dot</RootNamespace>
<UseWindowsForms Condition="'$(TargetFramework)' == 'net8.0-windows'">true</UseWindowsForms>
</PropertyGroup>
<Import Project="$(SolutionDir)/build/code.quality.props"/>
<Import Project="$(SolutionDir)/build/copy.pkg.xml.comment.files.targets"/>
<Import Project="$(SolutionDir)/build/prebuild.targets"/>
<ItemGroup>
<PackageReference Include="NSExt" Version="1.1.0"/>
<PackageReference Include="Spectre.Console.Cli.NS" Version="0.45.1-preview.0.124"/>
<PackageReference Include="Spectre.Console.NS" Version="0.45.1-preview.0.124"/>
<PackageReference Condition="'$(TargetFramework)' == 'net8.0-windows'" Include="TextCopy" Version="6.2.1"/>
</ItemGroup>
<ItemGroup>
<None Update="*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,147 @@
using System.Collections.Concurrent;
// ReSharper disable once RedundantUsingDirective
using Panel = Spectre.Console.Panel;
namespace Dot;
internal abstract class FilesTool<TOption> : ToolBase<TOption>
where TOption : DirOption
{
// ReSharper disable once StaticMemberInGenericType
private readonly object _lock = new(); // 线程锁
private readonly ConcurrentDictionary<string, int> _writeStats = new(); // 写入统计:后缀,数量
private int _breakCnt; // 跳过文件数
private ProgressTask _childTask; // 子任务进度
private int _excludeCnt; // 排除文件数
private int _readCnt; // 读取文件数
private int _totalCnt; // 总文件数
private int _writeCnt; // 写入文件数
protected static FileStream CreateTempFile(out string file)
{
file = Path.Combine(Path.GetTempPath(), $"{System.Guid.NewGuid()}.tmp");
return OpenFileStream(file, FileMode.OpenOrCreate, FileAccess.Write);
}
protected static FileStream OpenFileStream(string file, FileMode mode, FileAccess access
, FileShare share = FileShare.Read)
{
FileStream fsr = null;
try {
fsr = new FileStream(file, mode, access, share);
}
catch (UnauthorizedAccessException) {
try {
File.SetAttributes(file, new FileInfo(file).Attributes & ~FileAttributes.ReadOnly);
fsr = new FileStream(file, mode, access, share);
}
catch {
// ignored
}
}
catch (IOException) {
// ignored
}
return fsr;
}
protected override Task CoreAsync()
{
return !Directory.Exists(Opt.Path)
? throw new ArgumentException($"{Ln.指定的路径不存在}: {Opt.Path}", "PATH")
: CoreInternalAsync();
}
protected abstract ValueTask FileHandleAsync(string file, CancellationToken cancelToken);
protected void ShowMessage(int readCnt, int writeCnt, int breakCnt)
{
lock (_lock) {
_readCnt += readCnt;
_writeCnt += writeCnt;
_breakCnt += breakCnt;
if (readCnt > 0) {
_childTask.Increment(1);
}
_childTask.Description
= $"{Ln.读取}: [green]{_readCnt}[/]/{_totalCnt}, {Ln.启用写入模式}: [red]{_writeCnt}[/], {Ln.跳过}: [gray]{_breakCnt}[/], {Ln.排除}: [yellow]{_excludeCnt}[/]";
}
}
protected void UpdateStats(string key)
{
_ = _writeStats.AddOrUpdate(key, 1, (_, oldValue) => oldValue + 1);
}
private async Task CoreInternalAsync()
{
if (!Opt.WriteMode) {
AnsiConsole.MarkupLine(CultureInfo.InvariantCulture, "[gray]{0}[/]", Ln._不会真实修改文件);
}
IEnumerable<string> fileList;
await AnsiConsole.Progress()
.Columns( //
new ProgressBarColumn() //
, new ElapsedTimeColumn() //
, new PercentageColumn() //
, new SpinnerColumn() //
, new TaskDescriptionColumn { Alignment = Justify.Left }) //
.StartAsync(ctx => {
var taskSearchFile = ctx.AddTask(Ln.).IsIndeterminate();
_childTask = ctx.AddTask("-/-", false);
fileList = EnumerateFiles(Opt.Path, Opt.Filter, out _excludeCnt);
_totalCnt = fileList.Count();
taskSearchFile.StopTask();
_childTask.MaxValue = _totalCnt;
_childTask.StartTask();
return Parallel.ForEachAsync(fileList, FileHandleAsync);
})
.ConfigureAwait(false);
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(CultureInfo.InvariantCulture));
}
AnsiConsole.Write(new Panel(grid).Header(Ln.));
}
// ReSharper disable once ReturnTypeCanBeEnumerable.Local
private string[] EnumerateFiles(string path, string searchPattern, out int excludeCnt)
{
var exCnt = 0;
// 默认排除.git 、 node_modules 目录
if (Opt.ExcludeRegexes?.FirstOrDefault() is null) {
Opt.ExcludeRegexes = new[] { @"\.git", "node_modules" };
}
var excludeRegexes = Opt.ExcludeRegexes.Select(x => new Regex(x));
var fileList = Directory.EnumerateFiles(path, searchPattern
, new EnumerationOptions {
RecurseSubdirectories = true
, AttributesToSkip
= FileAttributes.ReparsePoint
, IgnoreInaccessible = true
, MaxRecursionDepth = Opt.MaxRecursionDepth
})
.Where(x => {
if (!excludeRegexes.Any(y => y.IsMatch(x))) {
return true;
}
++exCnt;
return false;
})
.ToArray();
excludeCnt = exCnt;
return fileList;
}
}

199
src/backend/Dot/Get/Main.cs Normal file
View File

@ -0,0 +1,199 @@
// ReSharper disable ClassNeverInstantiated.Global
using System.Net.Http.Headers;
using NSExt.Extensions;
namespace Dot.Get;
[Description(nameof(Ln.多线程下载工具))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
private const string _PART = "part";
private static readonly Regex _partRegex = new($"(\\d+)\\.{_PART}", RegexOptions.Compiled);
protected override async Task CoreAsync()
{
using var http = new HttpClient();
string attachment = default;
long contentLength = default;
var table = new Table().AddColumn(Ln.).AddColumn(Ln.).AddRow("网络地址", Opt.);
await AnsiConsole.Status()
.AutoRefresh(true)
.Spinner(Spinner.Known.Default)
.StartAsync($"{Ln.请求元数据}: {Opt.网络地址}", async _ => {
using var headRsp = await http.SendAsync(new HttpRequestMessage(HttpMethod.Head, Opt.))
.ConfigureAwait(false);
using var content = headRsp.Content;
contentLength = content.Headers.ContentLength ?? 0;
attachment = content.Headers.ContentDisposition?.FileName ??
Opt.[(Opt..LastIndexOf('/') + 1)..];
foreach (var kv in content.Headers) {
#pragma warning disable IDE0058
table.AddRow(kv.Key, string.Join(Environment.NewLine, kv.Value));
#pragma warning restore IDE0058
}
})
.ConfigureAwait(false);
AnsiConsole.Write(table);
var timer = DateTime.UtcNow;
var mainFilePath = BuildFilePath(Opt.Output, attachment);
await AnsiConsole.Progress()
.Columns( //
new ProgressBarColumn() //
, new SpinnerColumn() //
, new DownloadedColumn() //
, new TransferSpeedColumn() //
, new PercentageColumn() //
, new TaskDescriptionColumn() //
, new RemainingTimeColumn()) //
.StartAsync(async ctx => {
var tParent = ctx.AddTask($"{Ln.总进度} {Ln.剩余时间}:").IsIndeterminate();
// 未知文件长度,单线程下载;
if (contentLength == 0) {
await using var nets = await http.GetStreamAsync(Opt.).ConfigureAwait(false);
await using var fs
= new FileStream(mainFilePath, FileMode.CreateNew, FileAccess.Write
, FileShare.None);
tParent.MaxValue = Opt._千字节 + 1; // 由于文件长度未知, 进度条终点永远至为当前长度+1
StreamCopy(nets, fs, x => {
tParent.MaxValue += x;
tParent.Increment(x);
});
tParent.MaxValue = tParent.Value; // 写完了
_ = tParent.IsIndeterminate(false);
tParent.StopTask();
}
// 已知文件长度,多线程下载:
else {
_ = tParent.IsIndeterminate(false);
tParent.MaxValue = contentLength;
var chunkSize = contentLength / Opt.;
async ValueTask BodyActionAsync(int i, CancellationToken cancellationToken)
{
var tChild = ctx.AddTask( //
$"{Ln.线程}{i} {Ln.剩余时间}:", maxValue: chunkSize);
using var getReq = new HttpRequestMessage(HttpMethod.Get, Opt.);
var startPos = i * chunkSize;
var endPos = startPos + chunkSize - 1;
if (i == Opt. - 1) {
endPos += contentLength % chunkSize;
}
getReq.Headers.Range = new RangeHeaderValue(startPos, endPos);
// ReSharper disable once AccessToDisposedClosure
using var getRsp = await http
.SendAsync(
getReq, HttpCompletionOption.ResponseHeadersRead
, cancellationToken)
.ConfigureAwait(false);
WritePart(getRsp, mainFilePath, i, startPos, endPos, x => {
tChild.Increment(x);
tParent.Increment(x);
});
}
await Parallel.ForAsync(0, Opt.
, new ParallelOptions { MaxDegreeOfParallelism = Opt. } //
, BodyActionAsync)
.ConfigureAwait(false);
MergeParts(mainFilePath);
}
})
.ConfigureAwait(false);
AnsiConsole.MarkupLine($"{Ln.下载完成}, {Ln.累计耗时}: {DateTime.UtcNow - timer}, {Ln.文件保存位置}: {mainFilePath}");
}
/// <summary>
/// 给定一个路径(存在的目录,或者存在的目录+存在或不存在的文件).
/// </summary>
/// <param name="path">存在的目录,或者存在的目录+存在或不存在的文件.</param>
/// <param name="file">要写入的文件名.</param>
/// <returns>返回一个可写的文件完整路径.</returns>
private static string BuildFilePath(string path, string file)
{
// path 是一个存在的文件,已追加尾标
if (GetUsablePath(ref path)) {
return path;
}
// ReSharper disable once InvertIf
if (Directory.Exists(path)) { // path 是一个存在的目录。
path = Path.Combine(path, file); // 构建文件路径
_ = GetUsablePath(ref path); // 追加序号。
return path;
}
// path 是一个不存在的目录或者文件 ,视为不存在的文件
return path;
}
private static bool GetUsablePath(ref string path)
{
var dir = Path.GetDirectoryName(path);
var name = Path.GetFileNameWithoutExtension(path);
var ext = Path.GetExtension(path);
var ret = false;
for (var i = 1; /**/; ++i) {
if (File.Exists(path)) {
path = Path.Combine(dir!, $"{name}({i}){ext}");
ret = true;
continue;
}
break;
}
return ret;
}
private static void MergeParts(string mainFilePath)
{
var files = Directory.GetFiles( //
Path.GetDirectoryName(mainFilePath)! //
, $"{Path.GetFileName(mainFilePath)}.*.{_PART}", SearchOption.TopDirectoryOnly)
.OrderBy(x => x)
.ToArray();
using var fs = File.Create(mainFilePath);
fs.SetLength(_partRegex.Match(files[^1]).Groups[1].Value.Int64());
foreach (var file in files) {
using var fsc = File.OpenRead(file);
fsc.CopyTo(fs);
fsc.Close();
File.Delete(file);
}
}
private void StreamCopy(Stream source, FileStream dest, Action<int> rateHandle)
{
Span<byte> buf = stackalloc byte[Opt._千字节];
int read;
while ((read = source.Read(buf)) != 0) {
dest.Write(read == Opt._千字节 ? buf : buf[..read]);
rateHandle(read);
}
}
private void WritePart(HttpResponseMessage rsp, string mainFilePath //
, int no, long startPos, long endPos //
, Action<int> rateHandle)
{
Span<byte> buf = stackalloc byte[Opt._千字节];
using var stream = rsp.Content.ReadAsStream();
int read;
var file = $"{mainFilePath}.{no}.{startPos}-{endPos}.{_PART}";
using var fs = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None);
while ((read = stream.Read(buf)) != 0) {
fs.Write(read == Opt._千字节 ? buf : buf[..read]);
rateHandle(read);
}
}
}

View File

@ -0,0 +1,36 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Dot.Get;
internal sealed class Option : OptionBase
{
[CommandOption("-b|--buffer-size")]
[Description(nameof(Ln.缓冲区大小_千字节))]
[Localization(typeof(Ln))]
[DefaultValue(8096)]
public int _千字节 { get; set; }
[CommandArgument(0, "<URL>")]
[Description(nameof(Ln.网络地址))]
[Localization(typeof(Ln))]
public string { get; set; }
[CommandOption("-c|--chunk-number")]
[Description(nameof(Ln.下载分块数))]
[Localization(typeof(Ln))]
[DefaultValue(5)]
public int { get; set; }
[CommandOption("-m|--max-parallel")]
[Description(nameof(Ln.最大并发数量))]
[Localization(typeof(Ln))]
[DefaultValue(5)]
public int { get; set; }
[CommandOption("-o|--output")]
[Description(nameof(Ln.输出文件路径))]
[Localization(typeof(Ln))]
[DefaultValue(".")]
public string Output { get; set; }
}

128
src/backend/Dot/Git/Main.cs Normal file
View File

@ -0,0 +1,128 @@
// ReSharper disable ClassNeverInstantiated.Global
using System.Collections.Concurrent;
using System.Diagnostics;
using NSExt.Extensions;
namespace Dot.Git;
[Description(nameof(Ln.Git批量操作工具))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
private Encoding _gitOutputEnc; // git command rsp 编码
private ConcurrentDictionary<string, StringBuilder> _repoRsp; // 仓库信息容器
private ConcurrentDictionary<string, TaskStatusColumn.Statues> _repoStatus;
protected override Task CoreAsync()
{
return !Directory.Exists(Opt.Path)
? throw new ArgumentException($"{Ln.指定的路径不存在}: {Opt.Path}", "PATH")
: CoreInternalAsync();
}
private async Task CoreInternalAsync()
{
_gitOutputEnc = Encoding.GetEncoding(Opt.Git输出编码);
var progressBar = new ProgressBarColumn { Width = 10 };
await AnsiConsole.Progress()
.Columns( //
progressBar //
, new ElapsedTimeColumn() //
, new SpinnerColumn() //
, new TaskStatusColumn() //
, new TaskDescriptionColumn { Alignment = Justify.Left }) //
.StartAsync(ctx => {
var taskFinder = ctx.AddTask($"{Ln.查找此目录下所有Git仓库目录}: {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);
_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.StopTask();
taskFinder.State.Status(TaskStatusColumn.Statues.Succeed);
return Parallel.ForEachAsync(tasks, DirHandleAsync);
})
.ConfigureAwait(false);
var table = new Table().AddColumn(new TableColumn(Ln.) { Width = 50 })
.AddColumn(new TableColumn(Ln.))
.AddColumn(new TableColumn(Ln.) { Width = 50 })
.Caption(
$"{Ln.Git退出码为零的}: [green]{_repoStatus.Count(x => x.Value == TaskStatusColumn.Statues
.Succeed)}[/]/{_repoStatus.Count}");
foreach (var repo in _repoRsp) {
var status = _repoStatus[repo.Key].ResDesc<Ln>();
_ = table.AddRow( //
status.Replace(_repoStatus[repo.Key].ToString(), new DirectoryInfo(repo.Key).Name), Opt.Args
, status.Replace(_repoStatus[repo.Key].ToString(), repo.Value.ToString()));
}
AnsiConsole.Write(table);
}
private async ValueTask DirHandleAsync(KeyValuePair<string, ProgressTask> payload, CancellationToken ct)
{
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));
_ = _repoRsp[payload.Key].Append(msg.EscapeMarkup());
}
// 启动git进程
var startInfo = new ProcessStartInfo {
CreateNoWindow = true
, WorkingDirectory = payload.Key
, FileName = "git"
, Arguments = Opt.Args
, UseShellExecute = false
, RedirectStandardOutput = true
, RedirectStandardError = true
};
using var p = Process.Start(startInfo);
p!.OutputDataReceived += ExecRspReceived;
p.ErrorDataReceived += ExecRspReceived;
p.BeginOutputReadLine();
p.BeginErrorReadLine();
await p.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
if (p.ExitCode == 0) {
payload.Value.State.Status(TaskStatusColumn.Statues.Succeed);
_ = _repoStatus.AddOrUpdate(payload.Key, _ => TaskStatusColumn.Statues.Succeed
, (_, _) => TaskStatusColumn.Statues.Succeed);
payload.Value.StopTask();
}
else {
payload.Value.State.Status(TaskStatusColumn.Statues.Failed);
_ = _repoStatus.AddOrUpdate(payload.Key, _ => TaskStatusColumn.Statues.Failed
, (_, _) => TaskStatusColumn.Statues.Failed);
}
payload.Value.StopTask();
}
}

View File

@ -0,0 +1,31 @@
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.Git;
internal sealed class Option : OptionBase
{
[CommandOption("-a|--args")]
[Description(nameof(Ln.传递给Git的参数))]
[Localization(typeof(Ln))]
[DefaultValue("status")]
public string Args { get; set; }
[CommandOption("-e|--git-output-encoding")]
[Description(nameof(Ln.Git输出编码))]
[Localization(typeof(Ln))]
[DefaultValue("utf-8")]
public string Git输出编码 { get; set; }
[CommandOption("-d|--max-recursion-depth")]
[Description(nameof(Ln.目录检索深度))]
[Localization(typeof(Ln))]
[DefaultValue(int.MaxValue)]
public int MaxRecursionDepth { get; set; }
[CommandArgument(0, "[PATH]")]
[Description(nameof(Ln.要处理的目录路径))]
[Localization(typeof(Ln))]
[DefaultValue(".")]
public string Path { get; set; }
}

View File

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

View File

@ -0,0 +1,58 @@
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
using NSExt.Extensions;
using Spectre.Console.Rendering;
namespace Dot.Git;
internal sealed class TaskStatusColumn : ProgressColumn
{
public enum Statues : byte
{
/// <summary>
/// Ready
/// </summary>
[Description($"[gray]{nameof(Ready)}[/]")]
Ready = 0
,
/// <summary>
/// Executing
/// </summary>
[Description($"[yellow]{nameof(Executing)}[/]")]
Executing = 1
,
/// <summary>
/// Succeed
/// </summary>
[Description($"[green]{nameof(Succeed)}[/]")]
Succeed = 2
,
/// <summary>
/// Failed
/// </summary>
[Description($"[red]{nameof(Failed)}[/]")]
Failed = 3
}
/// <summary>
/// Gets or sets the alignment of the task description.
/// </summary>
/// <value>
/// The alignment of the task description.
/// </value>
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.ResDesc<Ln>()).Overflow(Overflow.Ellipsis).Justify(Alignment);
}
}

View File

@ -0,0 +1,26 @@
// ReSharper disable ClassNeverInstantiated.Global
#if NET8_0_WINDOWS
using TextCopy;
#endif
namespace Dot.Guid;
[Description(nameof(Ln.GUID工具))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
protected override Task CoreAsync()
{
var guid = System.Guid.NewGuid().ToString();
if (Opt.Upper) {
guid = guid.ToUpper(CultureInfo.InvariantCulture);
}
Console.WriteLine($"{Ln.已复制到剪贴板}: {guid}");
#if NET8_0_WINDOWS
ClipboardService.SetText(guid);
#endif
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,13 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Dot.Guid;
internal sealed class Option : OptionBase
{
[CommandOption("-u|--upper")]
[Description(nameof(Ln.使用大写输出))]
[Localization(typeof(Ln))]
[DefaultValue(false)]
public bool Upper { get; set; }
}

View File

@ -0,0 +1,3 @@
namespace Dot;
internal interface IOption;

View File

@ -0,0 +1,35 @@
// ReSharper disable ClassNeverInstantiated.Global
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace Dot.IP;
[Description(nameof(Ln.IP工具))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
private const string _HTTP_BIN_ORG_IP = "http://httpbin.org/ip";
protected override async Task CoreAsync()
{
foreach (var item in NetworkInterface.GetAllNetworkInterfaces()) {
if (item.NetworkInterfaceType != NetworkInterfaceType.Ethernet ||
item.OperationalStatus != OperationalStatus.Up) {
continue;
}
var output = string.Join( //
Environment.NewLine
, item.GetIPProperties()
.UnicastAddresses.Where(x => x.Address.AddressFamily == AddressFamily.InterNetwork)
.Select(x => $"{item.Name}: {x.Address}"));
Console.WriteLine(output);
}
using var http = new HttpClient();
Console.Write($"{Ln.公网IP}: ");
var str = await http.GetStringAsync(_HTTP_BIN_ORG_IP).ConfigureAwait(false);
Console.WriteLine(str);
}
}

View File

@ -0,0 +1,5 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.IP;
internal sealed class Option : OptionBase;

View File

@ -0,0 +1,91 @@
// ReSharper disable ClassNeverInstantiated.Global
using System.Text.Json;
using NSExt.Extensions;
#if NET8_0_WINDOWS
using TextCopy;
#endif
namespace Dot.Json;
[Description(nameof(Ln.Json工具))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
private object _inputObj;
protected override Task CoreAsync()
{
var inputText = Opt.InputText;
#if NET8_0_WINDOWS
if (inputText.NullOrWhiteSpace()) {
inputText = ClipboardService.GetText();
}
#endif
if (inputText.NullOrWhiteSpace()) {
throw new ArgumentException(Ln.);
}
try {
_inputObj = inputText.Object<object>();
}
catch (JsonException) {
try {
inputText = UnescapeString(inputText);
_inputObj = inputText.Object<object>();
return Task.CompletedTask;
}
catch (JsonException) {
// ignored
}
throw new ArgumentException(Ln.Json字符串);
}
return CoreInternalAsync();
}
private static string UnescapeString(string text)
{
return text.Replace("\\\"", "\"");
}
private async Task<string> ConvertToStringAsync()
{
var ret = await JsonCompressAsync().ConfigureAwait(false);
return ret.Replace("\"", "\\\"");
}
private async Task CoreInternalAsync()
{
string result = null;
if (Opt.Compress) {
result = await JsonCompressAsync().ConfigureAwait(false);
}
else if (Opt.ConvertToString) {
result = await ConvertToStringAsync().ConfigureAwait(false);
}
else if (Opt.Format) {
result = await JsonFormatAsync().ConfigureAwait(false);
}
if (!result.NullOrWhiteSpace()) {
#if NET8_0_WINDOWS
await ClipboardService.SetTextAsync(result!).ConfigureAwait(false);
#endif
}
}
private Task<string> JsonCompressAsync()
{
var ret = _inputObj.Json();
return Task.FromResult(ret);
}
private Task<string> JsonFormatAsync()
{
var ret = _inputObj.Json(new JsonSerializerOptions { WriteIndented = true });
return Task.FromResult(ret);
}
}

View File

@ -0,0 +1,30 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Dot.Json;
internal sealed class Option : OptionBase
{
[CommandOption("-c|--compress")]
[Description(nameof(Ln.压缩Json文本))]
[Localization(typeof(Ln))]
[DefaultValue(false)]
public bool Compress { get; set; }
[CommandOption("-s|--convert-to-string")]
[Description(nameof(Ln.Json文本转义成字符串))]
[Localization(typeof(Ln))]
[DefaultValue(false)]
public bool ConvertToString { get; set; }
[CommandOption("-f|--format")]
[Description(nameof(Ln.格式化Json文本))]
[Localization(typeof(Ln))]
[DefaultValue(true)]
public bool Format { get; set; }
[CommandArgument(0, "[INPUT TEXT]")]
[Description(nameof(Ln.要处理的文本_默认取取剪贴板值))]
[Localization(typeof(Ln))]
public string InputText { get; set; }
}

View File

@ -0,0 +1,72 @@
// ReSharper disable ClassNeverInstantiated.Global
#if NET8_0_WINDOWS
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Dot.Native;
internal sealed class KeyboardHook : IDisposable
{
private readonly nint _hookId;
private bool _disposed;
public KeyboardHook()
{
_hookId = SetHook(HookCallback);
}
~KeyboardHook()
{
Dispose(false);
}
public delegate bool KeyUpEventHandler(object sender, Win32.KbdllhooksStruct e);
public KeyUpEventHandler KeyUpEvent { get; set; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private static nint SetHook(Win32.HookProc lpfn)
{
using var process = Process.GetCurrentProcess();
using var module = process.MainModule;
return Win32.SetWindowsHookExA(Win32.WH_KEYBOARD_LL, lpfn, module!.BaseAddress, 0);
}
private void Dispose(bool disposing)
{
if (_disposed) {
return;
}
if (disposing) {
//
}
if (_hookId != default) {
_ = Win32.UnhookWindowsHookExA(_hookId);
}
_disposed = true;
}
private nint HookCallback(int nCode, nint wParam, nint lParam)
{
// ReSharper disable once InvertIf
if (nCode >= 0 && wParam == Win32.WM_KEYDOWN) {
var hookStruct = (Win32.KbdllhooksStruct)Marshal.PtrToStructure(lParam, typeof(Win32.KbdllhooksStruct))!;
if (KeyUpEvent?.Invoke(null, hookStruct) ?? false) {
return -1;
}
}
return Win32.CallNextHookEx(_hookId, nCode, wParam, lParam);
}
}
#endif

View File

@ -0,0 +1,77 @@
// ReSharper disable ClassNeverInstantiated.Global
#if NET8_0_WINDOWS
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Dot.Native;
internal sealed class MouseHook : IDisposable
{
private readonly nint _hookId;
private bool _disposed;
public MouseHook()
{
_hookId = SetHook(HookCallback);
}
~MouseHook()
{
Dispose(false);
}
public event EventHandler<MouseEventArgs> MouseMoveEvent;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private static nint SetHook(Win32.HookProc lpfn)
{
using var process = Process.GetCurrentProcess();
using var module = process.MainModule;
return Win32.SetWindowsHookExA(Win32.WH_MOUSE_LL, lpfn, module!.BaseAddress, 0);
}
private void Dispose(bool disposing)
{
if (_disposed) {
return;
}
if (disposing) {
//
}
if (_hookId != default) {
_ = Win32.UnhookWindowsHookExA(_hookId);
}
_disposed = true;
}
private nint HookCallback(int nCode, nint wParam, nint lParam)
{
// ReSharper disable once InvertIf
if (wParam == Win32.WM_MOUSEMOVE) {
var hookStruct = (Msllhookstruct)Marshal.PtrToStructure(lParam, typeof(Msllhookstruct))!;
MouseMoveEvent?.Invoke(this, new MouseEventArgs(MouseButtons.None, 0, hookStruct.X, hookStruct.Y, 0));
}
return Win32.CallNextHookEx(_hookId, nCode, wParam, lParam);
}
[StructLayout(LayoutKind.Explicit)]
private readonly struct Msllhookstruct
{
[FieldOffset(0)]
public readonly int X;
[FieldOffset(4)]
public readonly int Y;
}
}
#endif

View File

@ -0,0 +1,174 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable IdentifierTypo
// ReSharper disable CommentTypo
namespace Dot.Native;
internal static class VkCode
{
public const int VK_A = 0x41; // A 键
public const int VK_ACCEPT = 0x1E; // IME 接受
public const int VK_ADD = 0x6B; // 添加密钥
public const int VK_APPS = 0x5D; // 应用程序键 (自然键盘)
public const int VK_ATTN = 0xF6; // Attn 键
public const int VK_B = 0x42; // B 键
public const int VK_BACK = 0x08; // BACKSPACE 密钥
public const int VK_BROWSER_BACK = 0xA6; // 浏览器后退键
public const int VK_BROWSER_FAVORITES = 0xAB; // 浏览器收藏键
public const int VK_BROWSER_FORWARD = 0xA7; // 浏览器前进键
public const int VK_BROWSER_HOME = 0xAC; // 浏览器“开始”和“主页”键
public const int VK_BROWSER_REFRESH = 0xA8; // 浏览器刷新键
public const int VK_BROWSER_SEARCH = 0xAA; // 浏览器搜索键
public const int VK_BROWSER_STOP = 0xA9; // 浏览器停止键
public const int VK_C = 0x43; // C 键
public const int VK_CANCEL = 0x03; // 控制中断处理
public const int VK_CAPITAL = 0x14; // CAPS LOCK 键
public const int VK_CLEAR = 0x0C; // CLEAR 键
public const int VK_CONTROL = 0x11; // Ctrl 键
public const int VK_CONVERT = 0x1C; // IME 转换
public const int VK_CRSEL = 0xF7; // CrSel 键
public const int VK_D = 0x44; // D 键
public const int VK_DECIMAL = 0x6E; // 十进制键
public const int VK_DELETE = 0x2E; // DEL 键
public const int VK_DIVIDE = 0x6F; // 除键
public const int VK_DOWN = 0x28; // 向下键
public const int VK_E = 0x45; // E 键
public const int VK_END = 0x23; // END 键
public const int VK_EREOF = 0xF9; // 擦除 EOF 密钥
public const int VK_ESCAPE = 0x1B; // ESC 键
public const int VK_EXECUTE = 0x2B; // EXECUTE 键
public const int VK_EXSEL = 0xF8; // ExSel 密钥
public const int VK_F = 0x46; // F 键
public const int VK_F1 = 0x70; // F1 键
public const int VK_F10 = 0x79; // F10 键
public const int VK_F11 = 0x7A; // F11 键
public const int VK_F12 = 0x7B; // F12 键
public const int VK_F13 = 0x7C; // F13 键
public const int VK_F14 = 0x7D; // F14 键
public const int VK_F15 = 0x7E; // F15 键
public const int VK_F16 = 0x7F; // F16 键
public const int VK_F17 = 0x80; // F17 键
public const int VK_F18 = 0x81; // F18 键
public const int VK_F19 = 0x82; // F19 键
public const int VK_F2 = 0x71; // F2 键
public const int VK_F20 = 0x83; // F20 键
public const int VK_F21 = 0x84; // F21 键
public const int VK_F22 = 0x85; // F22 键
public const int VK_F23 = 0x86; // F23 键
public const int VK_F24 = 0x87; // F24 键
public const int VK_F3 = 0x72; // F3 键
public const int VK_F4 = 0x73; // F4 键
public const int VK_F5 = 0x74; // F5 键
public const int VK_F6 = 0x75; // F6 键
public const int VK_F7 = 0x76; // F7 键
public const int VK_F8 = 0x77; // F8 键
public const int VK_F9 = 0x78; // F9 键
public const int VK_FINAL = 0x18; // IME 最终模式
public const int VK_G = 0x47; // G 键
public const int VK_H = 0x48; // H 键
public const int VK_HANGUEL = 0x15; // IME 朝鲜文库埃尔模式 (保持兼容性;使用 VK_HANGUL)
public const int VK_HANGUL = 0x15; // IME Hanguel 模式
public const int VK_HANJA = 0x19; // IME Hanja 模式
public const int VK_HELP = 0x2F; // 帮助密钥
public const int VK_HOME = 0x24; // HOME 键
public const int VK_I = 0x49; // I 键
public const int VK_IME_OFF = 0x1A; // IME 关闭
public const int VK_IME_ON = 0x16; // IME On
public const int VK_INSERT = 0x2D; // INS 密钥
public const int VK_J = 0x4A; // J 键
public const int VK_JUNJA = 0x17; // IME Junja 模式
public const int VK_K = 0x4B; // K 键
public const int VK_KANA = 0x15; // IME Kana 模式
public const int VK_KANJI = 0x19; // IME Kanji 模式
public const int VK_L = 0x4C; // L 键
public const int VK_LAUNCH_APP1 = 0xB6; // 启动应用程序 1 键
public const int VK_LAUNCH_APP2 = 0xB7; // 启动应用程序 2 键
public const int VK_LAUNCH_MAIL = 0xB4; // 启动邮件键
public const int VK_LAUNCH_MEDIA_SELECT = 0xB5; // 选择媒体键
public const int VK_LBUTTON = 0x01; // 鼠标左键
public const int VK_LCONTROL = 0xA2; // 左 Ctrl 键
public const int VK_LEFT = 0x25; // 向左键
public const int VK_LMENU = 0xA4; // 左 Alt 键
public const int VK_LSHIFT = 0xA0; // 左 SHIFT 键
public const int VK_LWIN = 0x5B; // 左Windows键 (自然键盘)
public const int VK_M = 0x4D; // M 键
public const int VK_MBUTTON = 0x04; // 中间鼠标按钮 (三键鼠标)
public const int VK_MEDIA_NEXT_TRACK = 0xB0; // 下一曲目键
public const int VK_MEDIA_PLAY_PAUSE = 0xB3; // 播放/暂停媒体键
public const int VK_MEDIA_PREV_TRACK = 0xB1; // 上一曲目键
public const int VK_MEDIA_STOP = 0xB2; // 停止媒体键
public const int VK_MENU = 0x12; // Alt 键
public const int VK_MODECHANGE = 0x1F; // IME 模式更改请求
public const int VK_MULTIPLY = 0x6A; // 乘键
public const int VK_N = 0x4E; // N 键
public const int VK_NEXT = 0x22; // PAGE DOWN 键
public const int VK_NONAME = 0xFC; // 预留
public const int VK_NONCONVERT = 0x1D; // IME 不转换
public const int VK_NUMLOCK = 0x90; // NUM LOCK 密钥
public const int VK_NUMPAD0 = 0x60; // 数字键盘 0 键
public const int VK_NUMPAD1 = 0x61; // 数字键盘 1 键
public const int VK_NUMPAD2 = 0x62; // 数字键盘 2 键
public const int VK_NUMPAD3 = 0x63; // 数字键盘 3 键
public const int VK_NUMPAD4 = 0x64; // 数字键盘 4 键
public const int VK_NUMPAD5 = 0x65; // 数字键盘 5 键
public const int VK_NUMPAD6 = 0x66; // 数字键盘 6 键
public const int VK_NUMPAD7 = 0x67; // 数字键盘 7 键
public const int VK_NUMPAD8 = 0x68; // 数字键盘 8 键
public const int VK_NUMPAD9 = 0x69; // 数字键盘 9 键
public const int VK_O = 0x4F; // O 键
public const int VK_OEM_1 = 0xBA; // 用于其他字符;它可能因键盘而异。 对于美国标准键盘,“;:”键
public const int VK_OEM_102 = 0xE2; // <>美国标准键盘上的键,或\\|非美国 102 键键盘上的键
public const int VK_OEM_2 = 0xBF; // 用于其他字符;它可能因键盘而异。 对于美国标准键盘,“/?” key
public const int VK_OEM_3 = 0xC0; // 用于其他字符;它可能因键盘而异。 对于美国标准键盘,“~”键
public const int VK_OEM_4 = 0xDB; // 用于其他字符;它可能因键盘而异。 对于美国标准键盘,“[{”键
public const int VK_OEM_5 = 0xDC; // 用于其他字符;它可能因键盘而异。 对于美国标准键盘,“\|”键
public const int VK_OEM_6 = 0xDD; // 用于其他字符;它可能因键盘而异。 对于美国标准键盘,“]}”键
public const int VK_OEM_7 = 0xDE; // 用于其他字符;它可能因键盘而异。 对于美国标准键盘,“单引号/双引号”键
public const int VK_OEM_8 = 0xDF; // 用于其他字符;它可能因键盘而异。
public const int VK_OEM_CLEAR = 0xFE; // 清除键
public const int VK_OEM_COMMA = 0xBC; // 对于任何国家/地区,“,键
public const int VK_OEM_MINUS = 0xBD; // 对于任何国家/地区,“-”键
public const int VK_OEM_PERIOD = 0xBE; // 对于任何国家/地区,“.”键
public const int VK_OEM_PLUS = 0xBB; // 对于任何国家/地区,“+”键
public const int VK_P = 0x50; // P 键
public const int VK_PA1 = 0xFD; // PA1 键
public const int VK_PACKET = 0xE7; // 用于将 Unicode 字符当作键击传递。
public const int VK_PAUSE = 0x13; // PAUSE 键
public const int VK_PLAY = 0xFA; // 播放键
public const int VK_PRINT = 0x2A; // PRINT 键
public const int VK_PRIOR = 0x21; // PAGE UP 键
public const int VK_PROCESSKEY = 0xE5; // IME PROCESS 密钥
public const int VK_Q = 0x51; // Q 键
public const int VK_R = 0x52; // R 键
public const int VK_RBUTTON = 0x02; // 鼠标右键
public const int VK_RCONTROL = 0xA3; // 右 Ctrl 键
public const int VK_RETURN = 0x0D; // Enter 键
public const int VK_RIGHT = 0x27; // 向右键
public const int VK_RMENU = 0xA5; // 右 ALT 键
public const int VK_RSHIFT = 0xA1; // 右 SHIFT 键
public const int VK_RWIN = 0x5C; // 右Windows键 (自然键盘)
public const int VK_S = 0x53; // S 键
public const int VK_SCROLL = 0x91; // SCROLL LOCK 键
public const int VK_SELECT = 0x29; // SELECT 键
public const int VK_SEPARATOR = 0x6C; // 分隔符键
public const int VK_SHIFT = 0x10; // SHIFT 键
public const int VK_SLEEP = 0x5F; // 计算机休眠键
public const int VK_SNAPSHOT = 0x2C; // 打印屏幕键
public const int VK_SPACE = 0x20; // 空格键
public const int VK_SUBTRACT = 0x6D; // 减去键
public const int VK_T = 0x54; // T 键
public const int VK_TAB = 0x09; // Tab 键
public const int VK_U = 0x55; // U 键
public const int VK_UP = 0x26; // 向上键
public const int VK_V = 0x56; // V 键
public const int VK_VOLUME_DOWN = 0xAE; // 音量减小键
public const int VK_VOLUME_MUTE = 0xAD; // 静音键
public const int VK_VOLUME_UP = 0xAF; // 音量增加键
public const int VK_W = 0x57; // W 键
public const int VK_X = 0x58; // X 键
public const int VK_XBUTTON1 = 0x05; // X1 鼠标按钮
public const int VK_XBUTTON2 = 0x06; // X2 鼠标按钮
public const int VK_Y = 0x59; // Y 键
public const int VK_Z = 0x5A; // Z 键
public const int VK_ZOOM = 0xFB; // 缩放键
}

View File

@ -0,0 +1,165 @@
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedMethodReturnValue.Global
#pragma warning disable SA1307,SX1309
using System.Runtime.InteropServices;
namespace Dot.Native;
internal static partial class Win32
{
public const int CS_DROP_SHADOW = 0x20000;
public const int GCL_STYLE = -26;
public const int HC_ACTION = 0;
public const int INPUT_KEYBOARD = 1;
public const int KEYEVENTF_KEYUP = 0x0002;
public const int SW_HIDE = 0;
public const int WH_KEYBOARD_LL = 13;
public const int WH_MOUSE_LL = 14;
public const int WM_CHANGECBCHAIN = 0x030D;
public const int WM_DRAWCLIPBOARD = 0x308;
public const int WM_KEYDOWN = 0x0100;
public const int WM_KEYUP = 0x0101;
public const int WM_LBUTTONDOWN = 0x0201;
public const int WM_MOUSEMOVE = 0x0200;
private const string _GDI32_DLL = "gdi32.dll";
private const string _KERNEL32_DLL = "kernel32.dll";
private const string _USER32_DLL = "user32.dll";
public delegate nint HookProc(int nCode, nint wParam, nint lParam);
[LibraryImport(_USER32_DLL)]
public static partial nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
[LibraryImport(_USER32_DLL)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool ChangeClipboardChain(nint hWndRemove, nint hWndNewNext);
[LibraryImport(_USER32_DLL)]
public static partial int GetClassLongA(nint hWnd, int nIndex);
[LibraryImport(_KERNEL32_DLL)]
public static partial nint GetConsoleWindow();
[LibraryImport(_USER32_DLL)]
public static partial nint GetDesktopWindow();
[LibraryImport(_KERNEL32_DLL, StringMarshalling = StringMarshalling.Utf16)]
public static partial nint GetModuleHandleA(string lpModuleName);
[LibraryImport(_GDI32_DLL)]
public static partial uint GetPixel(nint dc, int x, int y);
[LibraryImport(_USER32_DLL)]
public static partial nint GetWindowDC(nint hWnd);
[LibraryImport(_USER32_DLL)]
public static partial int ReleaseDC(nint hWnd, nint dc);
[LibraryImport(_USER32_DLL)]
public static partial uint SendInput(uint cInputs, [MarshalAs(UnmanagedType.LPArray)] InputStruct[] inputs
, int cbSize);
[LibraryImport(_USER32_DLL)]
public static partial int SendMessageA(nint hwnd, uint wMsg, nint wParam, nint lParam);
[LibraryImport(_USER32_DLL)]
public static partial int SetClassLongA(nint hWnd, int nIndex, int dwNewLong);
[LibraryImport(_USER32_DLL)]
public static partial int SetClipboardViewer(nint hWnd);
[LibraryImport(_KERNEL32_DLL)]
public static partial void SetLocalTime(SystemTime st);
[LibraryImport(_USER32_DLL)]
public static partial nint SetWindowsHookExA(int idHook, HookProc lpfn, nint hMod, uint dwThreadId);
[LibraryImport(_USER32_DLL)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool ShowWindow(nint hWnd, int nCmdShow);
[LibraryImport(_USER32_DLL)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool UnhookWindowsHookExA(nint hhk);
[StructLayout(LayoutKind.Explicit)]
public readonly struct KbdllhooksStruct
{
[FieldOffset(0)]
public readonly uint vkCode;
[FieldOffset(16)]
private readonly nint dwExtraInfo;
[FieldOffset(8)]
private readonly uint flags;
[FieldOffset(4)]
private readonly uint scanCode;
[FieldOffset(12)]
private readonly uint time;
}
[StructLayout(LayoutKind.Explicit)]
public struct InputStruct
{
[FieldOffset(8)]
public KeybdInputStruct ki;
[FieldOffset(0)]
public uint type;
}
[StructLayout(LayoutKind.Explicit)]
public struct KeybdInputStruct
{
[FieldOffset(4)]
public uint dwFlags;
[FieldOffset(0)]
public ushort wVk;
[FieldOffset(20)]
private readonly long _; // 补位以匹配 UNION的MOUSEINPUT参数 28bytes
[FieldOffset(12)]
private readonly nint dwExtraInfo;
[FieldOffset(8)]
private readonly uint time;
[FieldOffset(2)]
private readonly ushort wScan;
}
[StructLayout(LayoutKind.Explicit)]
public ref struct SystemTime
{
[FieldOffset(6)]
public ushort wDay;
[FieldOffset(4)]
public ushort wDayOfWeek;
[FieldOffset(8)]
public ushort wHour;
[FieldOffset(14)]
public ushort wMilliseconds;
[FieldOffset(10)]
public ushort wMinute;
[FieldOffset(2)]
public ushort wMonth;
[FieldOffset(12)]
public ushort wSecond;
[FieldOffset(0)]
public ushort wYear;
}
}

View File

@ -0,0 +1,15 @@
// ReSharper disable UnusedAutoPropertyAccessor.Global
global using Spectre.Console;
global using Spectre.Console.Cli;
namespace Dot;
internal abstract class OptionBase : CommandSettings, IOption
{
[CommandOption("-k|--keep--session")]
[Description(nameof(Ln.执行命令后保留会话))]
[Localization(typeof(Ln))]
[DefaultValue(false)]
public bool { get; set; }
}

View File

@ -0,0 +1,53 @@
using Dot.Git;
#if NET8_0_WINDOWS
using System.Runtime.InteropServices;
#endif
namespace Dot;
internal static class Program
{
public static int Main(string[] args)
{
CustomCulture(ref args);
var app = new CommandApp();
app.Configure(config => {
config.AddCommand<Main>(nameof(Git).ToLower(CultureInfo.InvariantCulture));
#if NET8_0_WINDOWS
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
config.AddCommand<Color.Main>(nameof(Color).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Tran.Main>(nameof(Tran).ToLower(CultureInfo.InvariantCulture));
}
#endif
config.AddCommand<Guid.Main>(nameof(Guid).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<IP.Main>(nameof(IP).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Json.Main>(nameof(Json).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Pwd.Main>(nameof(Pwd).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Rbom.Main>(nameof(Rbom).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Trim.Main>(nameof(Trim).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Text.Main>(nameof(Text).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Time.Main>(nameof(Time).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<ToLf.Main>(nameof(ToLf).ToLower(CultureInfo.InvariantCulture));
config.AddCommand<Get.Main>(nameof(Get).ToLower(CultureInfo.InvariantCulture));
config.ValidateExamples();
});
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return app.Run(args);
}
private static void CustomCulture(ref string[] args)
{
var i = Array.IndexOf(args, "/e");
if (i < 0) {
return;
}
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(args[i + 1]);
var argsList = args.ToList();
argsList.RemoveAt(i);
argsList.RemoveAt(i);
args = argsList.ToArray();
}
}

View File

@ -0,0 +1,66 @@
// ReSharper disable ClassNeverInstantiated.Global
using NSExt.Extensions;
#if NET8_0_WINDOWS
using TextCopy;
#endif
namespace Dot.Pwd;
[Description(nameof(Ln.随机密码生成器))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
private readonly char[][] _charTable = {
"0123456789".ToCharArray() //
, "abcdefghijklmnopqrstuvwxyz".ToCharArray()
, "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray()
, "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".ToCharArray()
};
protected override Task CoreAsync()
{
unsafe {
var pSource = stackalloc char[_charTable.Sum(x => x.Length)];
var pDest = stackalloc char[Opt.Length];
var sourceLen = 0;
if (Opt.Type.HasFlag(Option.GenerateTypes.Number)) {
foreach (var c in _charTable[0]) {
*(pSource + sourceLen++) = c;
}
}
if (Opt.Type.HasFlag(Option.GenerateTypes.LowerCaseLetter)) {
foreach (var c in _charTable[1]) {
*(pSource + sourceLen++) = c;
}
}
if (Opt.Type.HasFlag(Option.GenerateTypes.UpperCaseLetter)) {
foreach (var c in _charTable[2]) {
*(pSource + sourceLen++) = c;
}
}
if (Opt.Type.HasFlag(Option.GenerateTypes.SpecialCharacter)) {
foreach (var c in _charTable[3]) {
*(pSource + sourceLen++) = c;
}
}
var randScope = new[] { 0, sourceLen };
for (var i = 0; i != Opt.Length; ++i) {
*(pDest + i) = *(pSource + randScope.Rand());
}
var result = new string(pDest, 0, Opt.Length);
Console.WriteLine($"{Ln.已复制到剪贴板}: {result}");
#if NET8_0_WINDOWS
ClipboardService.SetText(result);
#endif
}
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,27 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Dot.Pwd;
internal sealed class Option : OptionBase
{
[Flags]
public enum GenerateTypes
{
None = 0
, Number = 1
, LowerCaseLetter = 1 << 1
, UpperCaseLetter = 1 << 2
, SpecialCharacter = 1 << 3
}
[CommandArgument(1, "<PASSWORD LENGTH>")]
[Description(nameof(Ln.密码长度))]
[Localization(typeof(Ln))]
public int Length { get; set; }
[CommandArgument(0, "<GENERATE TYPE>")]
[Description(nameof(Ln.密码创建类型))]
[Localization(typeof(Ln))]
public GenerateTypes Type { get; set; }
}

View File

@ -0,0 +1,57 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.Rbom;
[Description(nameof(Ln.移除文件的UTF8_BOM))]
[Localization(typeof(Ln))]
internal sealed class Main : FilesTool<Option>
{
private readonly byte[] _utf8Bom = { 0xef, 0xbb, 0xbf };
protected override async ValueTask FileHandleAsync(string file, CancellationToken cancelToken)
{
ShowMessage(1, 0, 0);
string tmpFile = default;
await using (var fsr = OpenFileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
if (fsr is null) {
ShowMessage(0, 0, 1);
return;
}
if (CloneFileWithoutBom(fsr, ref tmpFile)) {
if (Opt.WriteMode) {
File.Copy(tmpFile, file, true);
}
ShowMessage(0, 1, 0);
UpdateStats(Path.GetExtension(file));
}
else {
ShowMessage(0, 0, 1);
}
}
if (tmpFile != default) {
File.Delete(tmpFile);
}
}
private bool CloneFileWithoutBom(FileStream fsr, ref string tempFile)
{
Span<byte> buffer = stackalloc byte[_utf8Bom.Length];
var readLen = fsr.Read(buffer);
if (readLen != _utf8Bom.Length || !buffer.SequenceEqual(_utf8Bom)) {
return false;
}
using var fsw = CreateTempFile(out tempFile);
int data;
while ((data = fsr.ReadByte()) != -1) {
fsw.WriteByte((byte)data);
}
return true;
}
}

View File

@ -0,0 +1,5 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.Rbom;
internal sealed class Option : DirOption;

View File

@ -0,0 +1,164 @@
// ReSharper disable ClassNeverInstantiated.Global
using System.Diagnostics;
using System.Security.Cryptography;
using NSExt.Extensions;
#if NET8_0_WINDOWS
using TextCopy;
#endif
namespace Dot.Text;
[Description(nameof(Ln.文本编码工具))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
#if NET8_0_WINDOWS
protected override async Task CoreAsync()
#else
protected override Task CoreAsync()
#endif
{
#if NET8_0_WINDOWS
if (Opt.Text.NullOrEmpty()) {
Opt.Text = await ClipboardService.GetTextAsync().ConfigureAwait(false);
}
#endif
if (Opt.Text.NullOrEmpty()) {
throw new ArgumentException(Ln.);
}
ParseAndShow(Opt.Text);
#if !NET8_0_WINDOWS
return Task.CompletedTask;
#endif
}
private static Output BuildOutput(string text, Encoding enc)
{
Output ret;
var inputHex = text.Hex(enc);
ret.Base64DeCodeHex = [];
ret.Base64DeCode = [];
ret.EncodingName = enc.EncodingName;
ret.Hex = inputHex.Str();
ret.Base64 = text.Base64(enc);
#pragma warning disable CA5351, CA5350
ret.Md5 = MD5.HashData(inputHex).Str();
ret.Sha1 = SHA1.HashData(inputHex).Str();
#pragma warning restore CA5350, CA5351
ret.Sha256 = SHA256.HashData(inputHex).Str();
ret.Sha512 = SHA512.HashData(inputHex).Str();
ret.UrlEncode = text.Url();
ret.UrlDecode = text.UrlDe();
ret.HtmlDecode = text.HtmlDe();
ret.HtmlEncode = text.Html();
ret.PercentUnicode = default;
ret.AndUnicode = default;
ret.BacksLantUnicode = default;
ret.UnicodeDecode = text.UnicodeDe();
if (Equals(enc, Encoding.BigEndianUnicode)) {
ret.PercentUnicode = inputHex.Str(false, "%u", 2);
ret.AndUnicode = inputHex.Str(false, ";&#x", 2)[1..] + ";";
ret.BacksLantUnicode = inputHex.Str(false, @"\u", 2);
}
if (!text.IsBase64String()) {
return ret;
}
byte[] base64DeHex = null;
try {
base64DeHex = text.Base64De();
}
catch {
// ignored
}
if (base64DeHex == null) {
return ret;
}
ret.Base64DeCodeHex = base64DeHex.Str();
ret.Base64DeCode = enc.GetString(base64DeHex);
return ret;
}
private static string BuildTemplate(Output o)
{
var sb = new StringBuilder();
_ = sb.AppendLine( //
CultureInfo.InvariantCulture
, $"{new string('-', 20)} {o.EncodingName} {new string('-', 30 - o.EncodingName.Length)}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"hex: {o.Hex}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"base64: {o.Base64}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"base64-decode-hex: {o.Base64DeCodeHex}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"base64-decode: {o.Base64DeCode}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"md5: {o.Md5}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"sha1: {o.Sha1}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"sha256: {o.Sha256}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"sha512: {o.Sha512}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"url-encode: {o.UrlEncode}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"url-decode: {o.UrlDecode}");
if (o.EncodingName.Equals(Encoding.BigEndianUnicode.EncodingName, StringComparison.OrdinalIgnoreCase)) {
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"unicode-percent: {o.PercentUnicode}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"unicode-and: {o.AndUnicode}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"unicode-back-slant: {o.BacksLantUnicode}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"unicode-decode: {o.UnicodeDecode}");
}
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"html-encode: {o.HtmlEncode}");
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"html-decode: {o.HtmlDecode}");
return sb.ToString();
}
private static void ParseAndShow(string text)
{
var ansi = BuildOutput(text, Encoding.GetEncoding("gbk"));
var utf8 = BuildOutput(text, Encoding.UTF8);
var unicodeLittleEndian = BuildOutput(text, Encoding.Unicode);
var unicodeBigEndian = BuildOutput(text, Encoding.BigEndianUnicode);
var sb = new StringBuilder();
sb.AppendLine(BuildTemplate(ansi))
.AppendLine(BuildTemplate(utf8))
.AppendLine(BuildTemplate(unicodeLittleEndian))
.AppendLine(BuildTemplate(unicodeBigEndian));
var str = sb.ToString();
Console.WriteLine(str);
#if NET8_0_WINDOWS
var file = Path.Combine(Path.GetTempPath(), $"{System.Guid.NewGuid()}.txt");
File.WriteAllText(file, str);
Process.Start("explorer", file);
#endif
}
private ref struct Output
{
public ReadOnlySpan<char> AndUnicode;
public ReadOnlySpan<char> BacksLantUnicode;
public ReadOnlySpan<char> Base64;
public ReadOnlySpan<char> Base64DeCode;
public ReadOnlySpan<char> Base64DeCodeHex;
public ReadOnlySpan<char> EncodingName;
public ReadOnlySpan<char> Hex;
public ReadOnlySpan<char> HtmlDecode;
public ReadOnlySpan<char> HtmlEncode;
public ReadOnlySpan<char> Md5;
public ReadOnlySpan<char> PercentUnicode;
public ReadOnlySpan<char> Sha1;
public ReadOnlySpan<char> Sha256;
public ReadOnlySpan<char> Sha512;
public ReadOnlySpan<char> UnicodeDecode;
public ReadOnlySpan<char> UrlDecode;
public ReadOnlySpan<char> UrlEncode;
}
}

View File

@ -0,0 +1,11 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.Text;
internal sealed class Option : OptionBase
{
[CommandArgument(0, "[INPUT TEXT]")]
[Description(nameof(Ln.要处理的文本_默认取取剪贴板值))]
[Localization(typeof(Ln))]
public string Text { get; set; }
}

View File

@ -0,0 +1,173 @@
// ReSharper disable ClassNeverInstantiated.Global
using System.Net.Sockets;
using Dot.Native;
namespace Dot.Time;
[Description(nameof(Ln.时间同步工具))]
[Localization(typeof(Ln))]
internal sealed class Main : ToolBase<Option>
{
private const int _MAX_DEGREE_OF_PARALLELISM = 10;
private const int _NTP_PORT = 123;
// ReSharper disable StringLiteralTypo
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"
};
// ReSharper restore StringLiteralTypo
private double _offsetAvg;
private int _successCnt;
protected override async Task CoreAsync()
{
await AnsiConsole.Progress()
.Columns( //
new TaskDescriptionColumn() //
, new ProgressBarColumn() //
, new ElapsedTimeColumn() //
, new SpinnerColumn() //
, new TaskStatusColumn() //
, new TaskResultColumn())
.StartAsync(async ctx => {
var tasks = _ntpServers.ToDictionary( //
server => server, server => ctx.AddTask(server, false).IsIndeterminate());
await Parallel.ForEachAsync(
tasks
, new ParallelOptions {
MaxDegreeOfParallelism
= _MAX_DEGREE_OF_PARALLELISM
}, ServerHandleAsync)
.ConfigureAwait(false);
_offsetAvg = tasks.Where(x => x.Value.State.Status() == TaskStatusColumn.Statues.Succeed)
.Average(x => x.Value.State.Result().TotalMilliseconds);
})
.ConfigureAwait(false);
AnsiConsole.MarkupLine(
$"{Ln.通讯成功} [green]{_successCnt}[/]/{_ntpServers.Length}, {Ln.本机时钟偏移平均值}: [yellow]{_offsetAvg:f2}[/] ms");
if (Opt.Sync) {
SetSystemTime(DateTime.Now.AddMilliseconds(-_offsetAvg));
AnsiConsole.MarkupLine($"[green]{Ln.本机时间已同步}[/]");
}
}
protected override async Task RunAsync()
{
await CoreAsync().ConfigureAwait(false);
if (Opt.) {
var table = new Table().HideHeaders()
.AddColumn(new TableColumn(string.Empty))
.AddColumn(new TableColumn(string.Empty))
.Caption(Ln.)
.AddRow(Ln.NTP_标准时钟, DateTime.Now.AddMilliseconds(-_offsetAvg).ToString("O"))
.AddRow(Ln., DateTime.Now.ToString("O"));
using 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, CancellationToken.None).ConfigureAwait(false);
}
});
_ = await AnsiConsole.Console.Input.ReadKeyAsync(true, cts.Token).ConfigureAwait(false);
await cts.CancelAsync().ConfigureAwait(false);
await task.ConfigureAwait(false);
}
}
private static void SetSystemTime(DateTime time)
{
var timeToSet = new Win32.SystemTime {
wDay = (ushort)time.Day
, wDayOfWeek = (ushort)time.DayOfWeek
, wHour = (ushort)time.Hour
, wMilliseconds = (ushort)time.Millisecond
, wMinute = (ushort)time.Minute
, wMonth = (ushort)time.Month
, wSecond = (ushort)time.Second
, wYear = (ushort)time.Year
};
Win32.SetLocalTime(timeToSet);
}
private TimeSpan GetNtpOffset(string server)
{
Span<byte> ntpData = stackalloc byte[48];
ntpData[0] = 0x1B;
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.ReceiveTimeout = Opt.Timeout;
try {
socket.Connect(server, _NTP_PORT);
_ = socket.Send(ntpData);
var timeBefore = DateTime.UtcNow;
_ = socket.Receive(ntpData);
var transferTime = DateTime.UtcNow - timeBefore;
var intPart = ((ulong)ntpData[40] << 24) //
| ((ulong)ntpData[41] << 16) //
| ((ulong)ntpData[42] << 8) //
| ntpData[43];
var fractPart = ((ulong)ntpData[44] << 24) //
| ((ulong)ntpData[45] << 16) //
| ((ulong)ntpData[46] << 8) //
| ntpData[47];
var from1900Ms = intPart * 1000 + fractPart * 1000 / 0x100000000L;
var onlineTime = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((long)from1900Ms) +
transferTime / 2;
return DateTime.UtcNow - onlineTime;
}
catch (Exception) {
return TimeSpan.Zero;
}
finally {
socket.Close();
}
}
private ValueTask ServerHandleAsync(KeyValuePair<string, ProgressTask> payload, CancellationToken ct)
{
payload.Value.StartTask();
payload.Value.State.Status(TaskStatusColumn.Statues.Connecting);
var offset = GetNtpOffset(payload.Key);
if (offset == TimeSpan.Zero) {
payload.Value.State.Status(TaskStatusColumn.Statues.Failed);
}
else {
payload.Value.State.Status(TaskStatusColumn.Statues.Succeed);
payload.Value.State.Result(offset);
_ = Interlocked.Increment(ref _successCnt);
}
payload.Value.StopTask();
return ValueTask.CompletedTask;
}
}

View File

@ -0,0 +1,19 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Dot.Time;
internal sealed class Option : OptionBase
{
[CommandOption("-s|--sync")]
[Description(nameof(Ln.同步本机时间))]
[Localization(typeof(Ln))]
[DefaultValue(false)]
public bool Sync { get; set; }
[CommandOption("-t|--timeout")]
[Description(nameof(Ln.连接NTP服务器超时时间_毫秒))]
[Localization(typeof(Ln))]
[DefaultValue(2000)]
public int Timeout { get; set; }
}

View File

@ -0,0 +1,24 @@
namespace Dot.Time;
internal 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,24 @@
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
using Spectre.Console.Rendering;
namespace Dot.Time;
internal sealed class TaskResultColumn : ProgressColumn
{
/// <summary>
/// Gets or sets the alignment of the task description.
/// </summary>
/// <value>
/// The alignment of the task description.
/// </value>
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,58 @@
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
using NSExt.Extensions;
using Spectre.Console.Rendering;
namespace Dot.Time;
internal sealed class TaskStatusColumn : ProgressColumn
{
public enum Statues : byte
{
/// <summary>
/// Ready
/// </summary>
[Description($"[gray]{nameof(Ready)}[/]")]
Ready = 0
,
/// <summary>
/// Connecting
/// </summary>
[Description($"[yellow]{nameof(Connecting)}[/]")]
Connecting = 1
,
/// <summary>
/// Succeed
/// </summary>
[Description($"[green]{nameof(Succeed)}[/]")]
Succeed = 2
,
/// <summary>
/// Failed
/// </summary>
[Description($"[red]{nameof(Failed)}[/]")]
Failed = 3
}
/// <summary>
/// Gets or sets the alignment of the task description.
/// </summary>
/// <value>
/// The alignment of the task description.
/// </value>
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.ResDesc<Ln>()).Overflow(Overflow.Ellipsis).Justify(Alignment);
}
}

View File

@ -0,0 +1,67 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.ToLf;
[Description(nameof(Ln.转换换行符为LF))]
[Localization(typeof(Ln))]
internal sealed class Main : FilesTool<Option>
{
protected override async ValueTask FileHandleAsync(string file, CancellationToken cancelToken)
{
ShowMessage(1, 0, 0);
var hasWrote = false;
var isBin = false;
string tmpFile;
// ReSharper disable once TooWideLocalVariableScope
int data;
await using (var fsr = OpenFileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
if (fsr is null) {
ShowMessage(0, 0, 1);
return;
}
await using var fsw = CreateTempFile(out tmpFile);
while ((data = fsr.ReadByte()) != -1) {
switch (data) {
case 0x0d when fsr.ReadByte() == 0x0a: // crlf windows
fsw.WriteByte(0x0a);
hasWrote = true;
continue;
case 0x0d: // cr macos
fsw.WriteByte(0x0a);
_ = fsr.Seek(-1, SeekOrigin.Current);
hasWrote = true;
continue;
case 0x00 or 0xff: // 非文本文件
isBin = true;
break;
default:
fsw.WriteByte((byte)data);
continue;
}
break;
}
}
#pragma warning disable S2583
if (hasWrote && !isBin) {
#pragma warning restore S2583
if (Opt.WriteMode) {
File.Copy(tmpFile, file, true);
}
ShowMessage(0, 1, 0);
UpdateStats(Path.GetExtension(file));
}
else {
ShowMessage(0, 0, 1);
}
File.Delete(tmpFile);
}
}

View File

@ -0,0 +1,5 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.ToLf;
internal sealed class Option : DirOption;

View File

@ -0,0 +1,25 @@
namespace Dot;
internal abstract class ToolBase<TOption> : Command<TOption>
where TOption : OptionBase
{
protected TOption Opt { get; private set; }
public override int Execute(CommandContext context, TOption settings)
{
Opt = settings;
RunAsync().ConfigureAwait(false).GetAwaiter().GetResult();
return 0;
}
protected abstract Task CoreAsync();
protected virtual async Task RunAsync()
{
await CoreAsync().ConfigureAwait(false);
if (Opt.) {
AnsiConsole.MarkupLine(Ln.);
_ = AnsiConsole.Console.Input.ReadKey(true);
}
}
}

View File

@ -0,0 +1,63 @@
using NSExt.Extensions;
namespace Dot.Tran;
internal static class BaiduSignCracker
{
private const int _MAGIC_NUMBER_1 = 320305;
private const int _MAGIC_NUMBER_2 = 131321201;
public static string Sign(string text)
{
var hash = text.Length > 30
? string.Concat(text.AsSpan()[..10], text.AsSpan(text.Length / 2 - 5, 10), text.AsSpan()[^10..])
: text;
var e = new List<int>(hash.Length);
for (var i = 0; i < hash.Length; i++) {
var k = (int)hash[i];
switch (k) {
case < 128:
e.Add(k);
break;
case < 2048:
e.Add((k >> 6) | 192);
break;
default:
if ((k & 64512) == 55296 && i + 1 < hash.Length && (hash[i + 1] & 64512) == 56320) {
k = 65536 + ((k & 1023) << 10) + (hash[++i] & 1023);
e.Add((k >> 18) | 240);
e.Add(((k >> 12) & 63) | 128);
}
else {
e.Add((k >> 12) | 224);
e.Add(((k >> 6) & 63) | 128);
e.Add((k & 63) | 128);
}
break;
}
}
var ret = e.Aggregate(_MAGIC_NUMBER_1, (accumulator, source) => Compute(accumulator + source, "+-a^+6"));
ret = Compute(ret, "+-3^+b+-f");
ret ^= _MAGIC_NUMBER_2;
var longRet = ret < 0 ? 1L + (ret & int.MaxValue) + int.MaxValue : ret;
longRet %= 1_000_000;
return $"{longRet}.{longRet ^ _MAGIC_NUMBER_1}";
}
private static int Compute(int number, string password)
{
unchecked {
for (var i = 0; i < password.Length - 2; i += 3) {
var c = password[i + 2];
var moveBit = c >= 'a' ? c - 87 : c.ToString().Int32();
var d = password[i + 1] == '+' ? number >>> moveBit : number << moveBit;
number = password[i] == '+' ? number + d : number ^ d;
}
}
return number;
}
}

View File

@ -0,0 +1,69 @@
// ReSharper disable InconsistentNaming
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable IdentifierTypo
#pragma warning disable IDE1006,SA1300
namespace Dot.Tran.Dto;
internal sealed record BaiduTranslateResultDto
{
public sealed record DataItem
{
public string dst { get; set; }
public int prefixWrap { get; set; }
public string result { get; set; }
public string src { get; set; }
}
public sealed record PhoneticItem
{
public string src_str { get; set; }
public string trg_str { get; set; }
}
#pragma warning disable S1144
public sealed record Root
#pragma warning restore S1144
{
public string errmsg { get; set; }
public int errno { get; set; }
public int error { get; set; }
public string errShowMsg { get; set; }
public string from { get; set; }
public long logid { get; set; }
public string query { get; set; }
public string to { get; set; }
public Trans_result trans_result { get; set; }
}
public sealed record Trans_result
{
// ReSharper disable once CollectionNeverUpdated.Global
public List<DataItem> data { get; set; }
public string from { get; set; }
public List<PhoneticItem> phonetic { get; set; }
public int status { get; set; }
public string to { get; set; }
public int type { get; set; }
}
}

View File

@ -0,0 +1,54 @@
#if NET8_0_WINDOWS
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using NSExt.Extensions;
namespace Dot.Tran;
[Description(nameof(Ln.翻译工具))]
[Localization(typeof(Ln))]
// ReSharper disable once ClassNeverInstantiated.Global
internal sealed class Main : ToolBase<Option>
{
[SupportedOSPlatform(nameof(OSPlatform.Windows))]
protected override Task CoreAsync()
{
AnsiConsole.MarkupLine(Ln.Capslock开始翻译);
AnsiConsole.MarkupLine(Ln.Esc隐藏译文);
var th = new Thread(() => {
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException += UnhandledException;
Application.ThreadException += UIThreadException;
using var frm = new WinMain();
try {
Application.Run();
}
catch (Exception ex) {
Log(ex.Json());
}
});
th.SetApartmentState(ApartmentState.STA);
th.Start();
th.Join();
return Task.CompletedTask;
}
private static void Log(string msg)
{
var file = Path.Combine(Path.GetTempPath(), $"{DateTime.Now.yyyyMMdd()}.dotlog");
File.AppendAllText(file, Environment.NewLine + msg);
}
private static void UIThreadException(object sender, ThreadExceptionEventArgs e)
{
Log(e.Json());
}
private static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Log(e.Json());
}
}
#endif

View File

@ -0,0 +1,4 @@
namespace Dot.Tran;
// ReSharper disable once ClassNeverInstantiated.Global
internal sealed class Option : OptionBase;

View File

@ -0,0 +1,253 @@
#if NET8_0_WINDOWS
using System.Net;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Dot.Native;
using Dot.Tran.Dto;
using NSExt.Extensions;
using TextCopy;
using Size = System.Drawing.Size;
namespace Dot.Tran;
[SupportedOSPlatform(nameof(OSPlatform.Windows))]
internal sealed class WinMain : Form
{
private const int _RETRY_WAIT_MIL_SEC = 1000;
private const string _TRANSLATE_API_URL = $"{_TRANSLATE_HOME_URL}/v2transapi";
private const string _TRANSLATE_HOME_URL = "https://fanyi.baidu.com";
private static readonly Regex _tokenRegex = new("token: '(\\w+)'", RegexOptions.Compiled);
private readonly HttpClient _httpClient = new(new HttpClientHandler { UseProxy = false });
private readonly KeyboardHook _keyboardHook = new();
private readonly Label _labelDest = new();
private readonly MouseHook _mouseHook = new();
private readonly Size _mouseMargin = new(10, 10);
private readonly string _stateFile = Path.Combine(Path.GetTempPath(), "dot-tran-state.tmp");
private bool _capsLockPressed;
private bool _disposed;
private nint _nextClipMonitor;
private volatile string _token;
public WinMain()
{
InitForm();
InitHook();
InitLabelDest();
InitHttpClient();
InitTokenCookie();
InitClipMonitor();
}
~WinMain()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_disposed) {
return;
}
if (disposing) {
_httpClient?.Dispose();
_labelDest?.Dispose();
_mouseHook?.Dispose();
_keyboardHook?.Dispose();
}
_ = Win32.ChangeClipboardChain(Handle, _nextClipMonitor); // 从剪贴板监视链移除本窗体
_disposed = true;
}
protected override void WndProc(ref Message m)
{
void SendToNext(Message message)
{
_ = Win32.SendMessageA(_nextClipMonitor, (uint)message.Msg, message.WParam, message.LParam);
}
switch (m.Msg) {
case Win32.WM_DRAWCLIPBOARD:
if (_capsLockPressed) {
_capsLockPressed = false;
TranslateAndShow();
}
SendToNext(m);
break;
case Win32.WM_CHANGECBCHAIN:
if (m.WParam == _nextClipMonitor) {
_nextClipMonitor = m.LParam;
}
else {
SendToNext(m);
}
break;
default:
base.WndProc(ref m);
break;
}
}
private void InitClipMonitor()
{
_nextClipMonitor = Win32.SetClipboardViewer(Handle);
}
private void InitForm()
{
AutoSize = true;
AutoSizeMode = AutoSizeMode.GrowAndShrink;
MaximumSize = Screen.FromHandle(Handle).Bounds.Size / 2;
FormBorderStyle = FormBorderStyle.None;
TopMost = true;
Visible = false;
}
private unsafe void InitHook()
{
_mouseHook.MouseMoveEvent += (_, e) => {
var point = new Point(e.X, e.Y);
point.Offset(new Point(_mouseMargin));
Location = point;
};
_keyboardHook.KeyUpEvent += (_, e) => {
switch (e.vkCode) {
case VkCode.VK_CAPITAL:
var keyInputs = new Win32.InputStruct[4];
keyInputs[0].type = Win32.INPUT_KEYBOARD;
keyInputs[0].ki.wVk = VkCode.VK_CONTROL;
keyInputs[1].type = Win32.INPUT_KEYBOARD;
keyInputs[1].ki.wVk = VkCode.VK_C;
keyInputs[2].type = Win32.INPUT_KEYBOARD;
keyInputs[2].ki.wVk = VkCode.VK_C;
keyInputs[2].ki.dwFlags = Win32.KEYEVENTF_KEYUP;
keyInputs[3].type = Win32.INPUT_KEYBOARD;
keyInputs[3].ki.wVk = VkCode.VK_CONTROL;
keyInputs[3].ki.dwFlags = Win32.KEYEVENTF_KEYUP;
#pragma warning disable IDE0058
Win32.SendInput((uint)keyInputs.Length, keyInputs, sizeof(Win32.InputStruct));
#pragma warning restore IDE0058
_capsLockPressed = true;
return true;
case VkCode.VK_ESCAPE:
Hide();
break;
default:
return false;
}
return false;
};
}
private void InitHttpClient()
{
_httpClient.DefaultRequestHeaders.Add( //
"User-Agent"
, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36");
}
private void InitLabelDest()
{
_labelDest.Font = new Font(_labelDest.Font.FontFamily, 16);
_labelDest.BorderStyle = BorderStyle.None;
_labelDest.Dock = DockStyle.Fill;
_labelDest.AutoSize = true;
Controls.Add(_labelDest);
}
private void InitTokenCookie()
{
if (File.Exists(_stateFile)) {
var lines = File.ReadLines(_stateFile).ToArray();
_token = lines[0];
_httpClient.DefaultRequestHeaders.Add(nameof(Cookie), lines[1]);
}
else {
_ = Task.Run(UpdateStateFile);
}
}
private void TranslateAndShow()
{
var clipText = ClipboardService.GetText();
if (clipText.NullOrWhiteSpace()) {
return;
}
_labelDest.Text = Ln.;
_ = Task.Run(() => {
var translateText = TranslateText(clipText);
ClipboardService.SetText(translateText);
_ = Invoke(() => _labelDest.Text = translateText);
});
var point = Cursor.Position;
point.Offset(new Point(_mouseMargin));
Location = point;
Show();
}
private string TranslateText(string sourceText)
{
while (true) {
var sign = BaiduSignCracker.Sign(sourceText);
var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>> {
new("from", "auto")
, new( //
"to"
, CultureInfo.CurrentCulture.TwoLetterISOLanguageName)
, new("query", sourceText)
, new("simple_means_flag", "3")
, new("sign", sign)
, new("token", _token)
, new("domain", "common")
});
var rsp = _httpClient.PostAsync(_TRANSLATE_API_URL, content).ConfigureAwait(false).GetAwaiter().GetResult();
var rspObj = rsp.Content.ReadAsStringAsync()
.ConfigureAwait(false)
.GetAwaiter()
.GetResult()
.Object<BaiduTranslateResultDto.Root>();
if (rspObj.error == 0) {
return string.Join(Environment.NewLine, rspObj.trans_result.data.Select(x => x.dst));
}
Console.Error.WriteLine(rspObj.Json().UnicodeDe());
Console.Error.WriteLine(rsp.Headers.Json());
// cookie or token invalid
Task.Delay(_RETRY_WAIT_MIL_SEC).ConfigureAwait(false).GetAwaiter().GetResult();
UpdateStateFile();
}
}
private void UpdateStateFile()
{
var rsp = _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, _TRANSLATE_HOME_URL))
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
var cookie = string.Join(';', rsp.Headers.First(x => x.Key == "Set-Cookie").Value.Select(x => x.Split(';')[0]));
_ = _httpClient.DefaultRequestHeaders.Remove(nameof(Cookie));
_httpClient.DefaultRequestHeaders.Add(nameof(Cookie), cookie);
var html = _httpClient.GetStringAsync(_TRANSLATE_HOME_URL).ConfigureAwait(false).GetAwaiter().GetResult();
_token = _tokenRegex.Match(html).Groups[1].Value;
File.WriteAllLines(_stateFile, new[] { _token, cookie });
}
}
#endif

View File

@ -0,0 +1,60 @@
// ReSharper disable ClassNeverInstantiated.Global
using NSExt.Extensions;
namespace Dot.Trim;
[Description(nameof(Ln.移除文件尾部换行和空格))]
[Localization(typeof(Ln))]
internal sealed class Main : FilesTool<Option>
{
private static readonly int[] _flagBytes = { 0x20, 0x0d, 0x0a };
protected override async ValueTask FileHandleAsync(string file, CancellationToken cancelToken)
{
ShowMessage(1, 0, 0);
int spacesCnt;
await using var fsrw = OpenFileStream(file, FileMode.Open, FileAccess.ReadWrite);
if (fsrw is null || fsrw.Length == 0 || (spacesCnt = GetSpacesCnt(fsrw)) == 0) {
ShowMessage(0, 0, 1);
return;
}
_ = fsrw.Seek(0, SeekOrigin.Begin);
if (!fsrw.IsTextStream()) {
ShowMessage(0, 0, 1);
return;
}
if (Opt.WriteMode) {
fsrw.SetLength(fsrw.Length - spacesCnt);
}
ShowMessage(0, 1, 0);
UpdateStats(Path.GetExtension(file));
}
private static int GetSpacesCnt(FileStream fsr)
{
var trimLen = 0;
_ = fsr.Seek(-1, SeekOrigin.End);
int data;
while ((data = fsr.ReadByte()) != -1) {
if (_flagBytes.Contains(data)) {
++trimLen;
if (fsr.Position - 2 < 0) {
break;
}
_ = fsr.Seek(-2, SeekOrigin.Current);
}
else {
break;
}
}
return trimLen;
}
}

View File

@ -0,0 +1,5 @@
// ReSharper disable ClassNeverInstantiated.Global
namespace Dot.Trim;
internal sealed class Option : DirOption;

View File

@ -0,0 +1,6 @@
global using System;
global using System.ComponentModel;
global using System.Globalization;
global using System.Text;
global using System.Text.RegularExpressions;
global using Dot.Languages;