mirror of
https://github.com/nsnail/dot.git
synced 2025-08-03 02:18:00 +08:00
refactor: ♻️ 2.0 (#13)
This commit is contained in:
14
src/backend/Dot.Tests/Dot.Tests.csproj
Normal file
14
src/backend/Dot.Tests/Dot.Tests.csproj
Normal 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>
|
21
src/backend/Dot/Color/Main.cs
Normal file
21
src/backend/Dot/Color/Main.cs
Normal 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
|
5
src/backend/Dot/Color/Option.cs
Normal file
5
src/backend/Dot/Color/Option.cs
Normal file
@ -0,0 +1,5 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace Dot.Color;
|
||||
|
||||
internal sealed class Option : OptionBase;
|
95
src/backend/Dot/Color/WinInfo.cs
Normal file
95
src/backend/Dot/Color/WinInfo.cs
Normal 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
|
84
src/backend/Dot/Color/WinMain.cs
Normal file
84
src/backend/Dot/Color/WinMain.cs
Normal 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
|
60
src/backend/Dot/CsxEditor.cs
Normal file
60
src/backend/Dot/CsxEditor.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
35
src/backend/Dot/DirOption.cs
Normal file
35
src/backend/Dot/DirOption.cs
Normal 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; }
|
||||
}
|
24
src/backend/Dot/Dot.csproj
Normal file
24
src/backend/Dot/Dot.csproj
Normal 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>
|
147
src/backend/Dot/FilesTool.cs
Normal file
147
src/backend/Dot/FilesTool.cs
Normal 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
199
src/backend/Dot/Get/Main.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
36
src/backend/Dot/Get/Option.cs
Normal file
36
src/backend/Dot/Get/Option.cs
Normal 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
128
src/backend/Dot/Git/Main.cs
Normal 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();
|
||||
}
|
||||
}
|
31
src/backend/Dot/Git/Option.cs
Normal file
31
src/backend/Dot/Git/Option.cs
Normal 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; }
|
||||
}
|
9
src/backend/Dot/Git/ProgressTaskStateExtensions.cs
Normal file
9
src/backend/Dot/Git/ProgressTaskStateExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
58
src/backend/Dot/Git/TaskStatusColumn.cs
Normal file
58
src/backend/Dot/Git/TaskStatusColumn.cs
Normal 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);
|
||||
}
|
||||
}
|
26
src/backend/Dot/Guid/Main.cs
Normal file
26
src/backend/Dot/Guid/Main.cs
Normal 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;
|
||||
}
|
||||
}
|
13
src/backend/Dot/Guid/Option.cs
Normal file
13
src/backend/Dot/Guid/Option.cs
Normal 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; }
|
||||
}
|
3
src/backend/Dot/IOption.cs
Normal file
3
src/backend/Dot/IOption.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Dot;
|
||||
|
||||
internal interface IOption;
|
35
src/backend/Dot/IP/Main.cs
Normal file
35
src/backend/Dot/IP/Main.cs
Normal 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);
|
||||
}
|
||||
}
|
5
src/backend/Dot/IP/Option.cs
Normal file
5
src/backend/Dot/IP/Option.cs
Normal file
@ -0,0 +1,5 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace Dot.IP;
|
||||
|
||||
internal sealed class Option : OptionBase;
|
91
src/backend/Dot/Json/Main.cs
Normal file
91
src/backend/Dot/Json/Main.cs
Normal 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);
|
||||
}
|
||||
}
|
30
src/backend/Dot/Json/Option.cs
Normal file
30
src/backend/Dot/Json/Option.cs
Normal 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; }
|
||||
}
|
72
src/backend/Dot/Native/KeyboardHook.cs
Normal file
72
src/backend/Dot/Native/KeyboardHook.cs
Normal 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
|
77
src/backend/Dot/Native/MouseHook.cs
Normal file
77
src/backend/Dot/Native/MouseHook.cs
Normal 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
|
174
src/backend/Dot/Native/VkCode.cs
Normal file
174
src/backend/Dot/Native/VkCode.cs
Normal 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; // 缩放键
|
||||
}
|
165
src/backend/Dot/Native/Win32.cs
Normal file
165
src/backend/Dot/Native/Win32.cs
Normal 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;
|
||||
}
|
||||
}
|
15
src/backend/Dot/OptionBase.cs
Normal file
15
src/backend/Dot/OptionBase.cs
Normal 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; }
|
||||
}
|
53
src/backend/Dot/Program.cs
Normal file
53
src/backend/Dot/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
66
src/backend/Dot/Pwd/Main.cs
Normal file
66
src/backend/Dot/Pwd/Main.cs
Normal 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;
|
||||
}
|
||||
}
|
27
src/backend/Dot/Pwd/Option.cs
Normal file
27
src/backend/Dot/Pwd/Option.cs
Normal 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; }
|
||||
}
|
57
src/backend/Dot/Rbom/Main.cs
Normal file
57
src/backend/Dot/Rbom/Main.cs
Normal 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;
|
||||
}
|
||||
}
|
5
src/backend/Dot/Rbom/Option.cs
Normal file
5
src/backend/Dot/Rbom/Option.cs
Normal file
@ -0,0 +1,5 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace Dot.Rbom;
|
||||
|
||||
internal sealed class Option : DirOption;
|
164
src/backend/Dot/Text/Main.cs
Normal file
164
src/backend/Dot/Text/Main.cs
Normal 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;
|
||||
}
|
||||
}
|
11
src/backend/Dot/Text/Option.cs
Normal file
11
src/backend/Dot/Text/Option.cs
Normal 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; }
|
||||
}
|
173
src/backend/Dot/Time/Main.cs
Normal file
173
src/backend/Dot/Time/Main.cs
Normal 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;
|
||||
}
|
||||
}
|
19
src/backend/Dot/Time/Option.cs
Normal file
19
src/backend/Dot/Time/Option.cs
Normal 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; }
|
||||
}
|
24
src/backend/Dot/Time/ProgressTaskStateExtensions.cs
Normal file
24
src/backend/Dot/Time/ProgressTaskStateExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
24
src/backend/Dot/Time/TaskResultColumn.cs
Normal file
24
src/backend/Dot/Time/TaskResultColumn.cs
Normal 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);
|
||||
}
|
||||
}
|
58
src/backend/Dot/Time/TaskStatusColumn.cs
Normal file
58
src/backend/Dot/Time/TaskStatusColumn.cs
Normal 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);
|
||||
}
|
||||
}
|
67
src/backend/Dot/ToLf/Main.cs
Normal file
67
src/backend/Dot/ToLf/Main.cs
Normal 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);
|
||||
}
|
||||
}
|
5
src/backend/Dot/ToLf/Option.cs
Normal file
5
src/backend/Dot/ToLf/Option.cs
Normal file
@ -0,0 +1,5 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace Dot.ToLf;
|
||||
|
||||
internal sealed class Option : DirOption;
|
25
src/backend/Dot/ToolBase.cs
Normal file
25
src/backend/Dot/ToolBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
63
src/backend/Dot/Tran/BaiduSignCracker.cs
Normal file
63
src/backend/Dot/Tran/BaiduSignCracker.cs
Normal 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;
|
||||
}
|
||||
}
|
69
src/backend/Dot/Tran/Dto/BaiduTranslateResultDto.cs
Normal file
69
src/backend/Dot/Tran/Dto/BaiduTranslateResultDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
54
src/backend/Dot/Tran/Main.cs
Normal file
54
src/backend/Dot/Tran/Main.cs
Normal 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
|
4
src/backend/Dot/Tran/Option.cs
Normal file
4
src/backend/Dot/Tran/Option.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace Dot.Tran;
|
||||
|
||||
// ReSharper disable once ClassNeverInstantiated.Global
|
||||
internal sealed class Option : OptionBase;
|
253
src/backend/Dot/Tran/WinMain.cs
Normal file
253
src/backend/Dot/Tran/WinMain.cs
Normal 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
|
60
src/backend/Dot/Trim/Main.cs
Normal file
60
src/backend/Dot/Trim/Main.cs
Normal 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;
|
||||
}
|
||||
}
|
5
src/backend/Dot/Trim/Option.cs
Normal file
5
src/backend/Dot/Trim/Option.cs
Normal file
@ -0,0 +1,5 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace Dot.Trim;
|
||||
|
||||
internal sealed class Option : DirOption;
|
6
src/backend/GlobalUsings.cs
Normal file
6
src/backend/GlobalUsings.cs
Normal 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;
|
Reference in New Issue
Block a user