XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.XUnit

【DLL】提供方便快捷的测试,无需编写Main方法,可直接添加特性在类或方法上进行测试

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEExtension.NetCore.XUnit

升级测试运行器控制台输出为自适应信息卡片

- 控制台输出支持 UTF-8,提升编码兼容性 - 运行信息采用自适应双栏表格,宽屏两栏、窄屏单栏 - 测试/基准结果列表引入状态徽章与耗时对齐 - 失败详情分层缩进,信息层级更清晰 - 汇总新增成功率进度条、统计数量与总用时 - 自动展示最慢测试,便于性能分析 - 基准结果与回归检测采用徽章和对齐表格 - 控制台宽度自适应范围扩大,输出更紧凑智能 - README(中英文)同步更新,描述新版特性

8bb3e71
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

4 个文件 +209 -52
Modified README.md +1 -1
@@ -33,7 +33,7 @@ dotnet run -c Release -- --filter Calculator --category Unit
33 33
34 34 Different classes run in parallel by default while methods in one class remain serial. Use `[Collection]`, `[NonParallel]`, `[Timeout]`, and `[Isolated]` for shared resources and process isolation. Results are exported as console output, JSON, and JUnit XML.
35 35
36 The console automatically selects Simplified Chinese when the current UI culture is Chinese and otherwise falls back to English. Override it at any time with `--language en`, `--language zh`, or `--language auto`. The console includes a run header, colored status lines, failure details, an aligned benchmark table, environment information, convergence warnings, and a compact summary; redirected output remains plain text and honors `NO_COLOR`.
36 The console automatically selects Simplified Chinese when the current UI culture is Chinese and otherwise falls back to English. Override it at any time with `--language en`, `--language zh`, or `--language auto`. The adaptive interface includes a two-column run card, encoding-safe status badges, aligned test durations, hierarchical failure details, success-rate and slow-test summaries, benchmark environment information, convergence warnings, and a detailed benchmark table. Redirected output remains plain text and honors `NO_COLOR`.
37 37
38 38 ## Benchmarks
39 39
Modified README.zh-CN.md +1 -1
@@ -31,7 +31,7 @@ dotnet run -c Release -- --filter Calculator --category Unit
31 31
32 32 默认情况下,不同测试类并行、同一类内串行。可通过 `[Collection]`、`[NonParallel]`、`[Timeout]` 和 `[Isolated]` 管理共享资源及子进程隔离。结果会导出到控制台、JSON 和 JUnit XML。
33 33
34 控制台会根据当前用户界面区域自动识别语言:中文区域使用简体中文,其他及无法识别的区域默认使用英文。可随时通过 `--language en`、`--language zh` 或 `--language auto` 手动覆盖。新版控制台提供运行信息、彩色状态、失败详情、对齐的基准表格、环境信息、收敛警告和汇总;重定向时保持纯文本,并支持 `NO_COLOR`。
34 控制台会根据当前用户界面区域自动识别语言:中文区域使用简体中文,其他及无法识别的区域默认使用英文。可随时通过 `--language en`、`--language zh` 或 `--language auto` 手动覆盖。自适应界面提供双栏运行卡片、编码安全的状态徽章、对齐的测试耗时、层级化失败详情、成功率与最慢测试汇总,以及包含环境和收敛警告的详细基准表格。重定向时保持纯文本,并支持 `NO_COLOR`。
35 35
36 36 ## 性能基准
37 37
Modified XFEExtension.NetCore.XUnit/Execution/XfeRunner.cs +1 -0
@@ -56,6 +56,7 @@ public static partial class XFERunner
56 56 /// <returns>表示运行完成的任务;结果为 <see cref="SuccessExitCode"/> 等标准退出码之一。</returns>
57 57 public static async Task<int> RunAsync(string[] args, XfeRegistry registry, CancellationToken cancellationToken = default)
58 58 {
59 ConsolePresenter.ConfigureConsole();
59 60 var presenter = new ConsolePresenter(new ConsoleLocalizer(ConsoleLanguage.Auto));
60 61 try
61 62 {
Modified XFEExtension.NetCore.XUnit/Reporting/ConsolePresenter.cs +206 -50
@@ -1,11 +1,24 @@
1 1 using System.Reflection;
2 2 using System.Runtime.InteropServices;
3 using System.Text;
3 4 using XFEExtension.NetCore.XUnit.Runtime;
4 5
5 6 namespace XFEExtension.NetCore.XUnit.Reporting;
6 7
7 8 internal sealed class ConsolePresenter(ConsoleLocalizer text)
8 9 {
10 public static void ConfigureConsole()
11 {
12 try
13 {
14 if (Console.OutputEncoding.CodePage != Encoding.UTF8.CodePage)
15 Console.OutputEncoding = new UTF8Encoding(false);
16 }
17 catch
18 {
19 }
20 }
21
9 22 public void PrintHeader(bool runTests, bool runBenchmarks, int testCount, int benchmarkCount, XfeRunSettings settings)
10 23 {
11 24 var version = typeof(ConsolePresenter).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
@@ -23,20 +36,31 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
23 36 : text.Select("Off", "关闭");
24 37
25 38 WriteTitle($"XFE Test Runner {version}");
26 WriteMetadata(text.Select("Mode", "模式"), mode);
27 WriteMetadata(text.Select("Language", "语言"), text.LanguageName);
28 WriteMetadata(text.Select("Discovered", "已发现"), discovered);
39 var metadata = new List<(string Label, string Value)>
40 {
41 (text.Select("Mode", "模式"), mode),
42 (text.Select("Runtime", "运行时"), RuntimeInformation.FrameworkDescription),
43 (text.Select("Language", "语言"), text.LanguageName),
44 (text.Select("Platform", "平台"), $"{RuntimeInformation.OSDescription} · {RuntimeInformation.ProcessArchitecture}"),
45 (text.Select("Discovered", "已发现"), discovered)
46 };
29 47 if (runTests)
30 WriteMetadata(text.Select("Parallel", "并行"), parallelism);
31 WriteMetadata(text.Select("Runtime", "运行时"), RuntimeInformation.FrameworkDescription);
32 WriteMetadata(text.Select("Platform", "平台"), $"{RuntimeInformation.OSDescription} · {RuntimeInformation.ProcessArchitecture}");
48 metadata.Add((text.Select("Parallel", "并行"), parallelism));
49 else if (runBenchmarks)
50 metadata.Add((
51 text.Select("Job", "作业"),
52 text.Select(
53 $"{settings.Benchmark.TargetIterationMilliseconds} ms target · {settings.Benchmark.MinIterationCount}-{settings.Benchmark.MaxIterationCount} samples",
54 $"目标 {settings.Benchmark.TargetIterationMilliseconds} ms · {settings.Benchmark.MinIterationCount}-{settings.Benchmark.MaxIterationCount} 个样本")));
55 WriteMetadataGrid(metadata);
33 56 WriteRule('╰', '─', '╯');
34 57 Console.WriteLine();
35 58 }
36 59
37 60 public void PrintList(IReadOnlyList<TestDescriptor> tests, IReadOnlyList<BenchmarkDescriptor> benchmarks)
38 61 {
39 WriteSection(text.Select("Discovered work", "发现的项目"));
62 var itemCount = tests.Count + benchmarks.Count;
63 WriteSection(text.Select("Discovered work", "发现的项目"), text.Select($"{itemCount} {(itemCount == 1 ? "item" : "items")}", $"{itemCount} 项"));
40 64 foreach (var test in tests)
41 65 WriteListItem(text.Select("TEST", "测试"), test.Id, test.DisplayName, ConsoleColor.Cyan);
42 66 foreach (var benchmark in benchmarks)
@@ -51,7 +75,7 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
51 75
52 76 public void PrintTests(TestRunSummary summary)
53 77 {
54 WriteSection(text.Select("Test results", "测试结果"));
78 WriteSection(text.Select("Test results", "测试结果"), text.Select($"{summary.Total} {(summary.Total == 1 ? "test" : "tests")}", $"{summary.Total} 个测试"));
55 79 if (summary.Results.Count == 0)
56 80 {
57 81 WriteColoredLine(text.Select(" No tests matched the current filters.", " 没有测试符合当前筛选条件。"), ConsoleColor.Yellow);
@@ -60,19 +84,24 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
60 84 return;
61 85 }
62 86
87 var consoleWidth = SafeConsoleWidth();
88 const int statusWidth = 10;
89 const int durationWidth = 14;
90 var nameWidth = Math.Max(20, consoleWidth - statusWidth - durationWidth - 4);
91 var headers = new[]
92 {
93 text.Select("Status", "状态"),
94 text.Select("Test", "测试"),
95 text.Select("Duration", "耗时")
96 };
97 WriteTestRow(headers[0], headers[1], headers[2], statusWidth, nameWidth, durationWidth, ConsoleColor.Cyan, false);
98 WriteTableRule([statusWidth, nameWidth, durationWidth]);
99
63 100 foreach (var result in summary.Results)
64 101 {
65 102 var color = OutcomeColor(result.Outcome);
66 var marker = result.Outcome switch
67 {
68 TestOutcome.Passed => "✓",
69 TestOutcome.Skipped => "↷",
70 TestOutcome.TimedOut => "⌛",
71 _ => "✗"
72 };
73 WriteColored($" {marker} {PadDisplay(text.Outcome(result.Outcome), 7)}", color);
74 Console.Write($" {result.DisplayName}");
75 WriteColoredLine($" {FormatDuration(result.TotalDuration)}", ConsoleColor.DarkGray);
103 var duration = result.Outcome == TestOutcome.Skipped ? "—" : FormatDuration(result.TotalDuration);
104 WriteTestRow(text.Outcome(result.Outcome), result.DisplayName, duration, statusWidth, nameWidth, durationWidth, color, true);
76 105
77 106 if (!string.IsNullOrWhiteSpace(result.Message))
78 107 {
@@ -89,11 +118,12 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
89 118
90 119 Console.WriteLine();
91 120 PrintTestSummary(summary);
121 PrintSlowestTests(summary);
92 122 }
93 123
94 124 public void PrintBenchmarks(BenchmarkRunSummary summary)
95 125 {
96 WriteSection(text.Select("Benchmark results", "基准结果"));
126 WriteSection(text.Select("Benchmark results", "基准结果"), text.Select($"{summary.Benchmarks.Count} {(summary.Benchmarks.Count == 1 ? "benchmark" : "benchmarks")}", $"{summary.Benchmarks.Count} 个基准"));
97 127 if (summary.Benchmarks.Count == 0)
98 128 {
99 129 WriteColoredLine(text.Select(" No benchmarks matched the current filters.", " 没有基准符合当前筛选条件。"), ConsoleColor.Yellow);
@@ -166,35 +196,41 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
166 196 foreach (var benchmark in summary.Benchmarks.Where(static benchmark => benchmark.Warnings.Count > 0))
167 197 {
168 198 Console.WriteLine();
169 WriteColoredLine($" ⚠ {benchmark.DisplayName}", ConsoleColor.Yellow);
199 WriteColoredLine($" [!] {benchmark.DisplayName}", ConsoleColor.Yellow);
170 200 foreach (var warning in benchmark.Warnings)
171 201 Console.WriteLine($" {text.Warning(warning)}");
172 202 }
173 203
174 204 Console.WriteLine();
175 WriteRule('─', '─', '─');
176 205 var convergence = summary.Benchmarks.Count(static benchmark => benchmark.Statistics.Converged);
177 206 var resultText = summary.RegressionDetected
178 207 ? text.Select("Performance regression detected", "检测到性能回归")
179 208 : text.Select("No gated regression detected", "未检测到门禁性能回归");
180 WriteColoredLine(
181 text.Select(
182 $"{(summary.Benchmarks.Count == 1 ? "Benchmark" : "Benchmarks")}: {summary.Benchmarks.Count} Converged: {convergence} Duration: {FormatDuration(summary.Duration)} {resultText}",
183 $"基准:{summary.Benchmarks.Count} 已收敛:{convergence} 用时:{FormatDuration(summary.Duration)} {resultText}"),
209 WriteSection(text.Select("Benchmark summary", "基准汇总"));
210 WriteColored($" {PadDisplay(text.Select("Result", "结果"), 14)}", ConsoleColor.DarkGray);
211 WriteBadge(summary.RegressionDetected ? text.Select("FAILED", "回归") : text.Select("PASSED", "通过"), 10,
184 212 summary.RegressionDetected ? ConsoleColor.Red : ConsoleColor.Green);
213 Console.Write(" ");
214 WriteColoredLine(resultText, summary.RegressionDetected ? ConsoleColor.Red : ConsoleColor.Green);
215 WriteColored($" {PadDisplay(text.Select("Benchmarks", "基准"), 14)}", ConsoleColor.DarkGray);
216 WriteColored(text.Select($"{summary.Benchmarks.Count} total", $"总计 {summary.Benchmarks.Count}"), ConsoleColor.White);
217 Console.Write(" ");
218 WriteColored(text.Select($"{convergence} converged", $"已收敛 {convergence}"), ConsoleColor.Green);
219 Console.Write(" ");
220 WriteColoredLine(text.Select($"Duration {FormatDuration(summary.Duration)}", $"用时 {FormatDuration(summary.Duration)}"), ConsoleColor.White);
185 221 }
186 222
187 223 public void PrintArtifacts(string path)
188 224 {
189 225 Console.WriteLine();
190 WriteColored(text.Select("Artifacts", "报告产物"), ConsoleColor.Cyan);
191 Console.WriteLine($": {Path.GetFullPath(path)}");
226 WriteSection(text.Select("Artifacts", "报告产物"));
227 WriteColoredLine($" {Path.GetFullPath(path)}", ConsoleColor.DarkGray);
192 228 }
193 229
194 230 public void PrintHelp()
195 231 {
196 232 WriteTitle("XFE Test Runner 4.0");
197 WriteMetadata(text.Select("Usage", "用法"), text.Select("<test application> [options]", "<测试程序> [选项]"));
233 WriteMetadataGrid([(text.Select("Usage", "用法"), text.Select("<test application> [options]", "<测试程序> [选项]"))]);
198 234 WriteRule('╰', '─', '╯');
199 235 Console.WriteLine();
200 236 WriteHelpSection(text.Select("Run selection", "运行选择"),
@@ -240,21 +276,61 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
240 276
241 277 private void PrintTestSummary(TestRunSummary summary)
242 278 {
243 WriteRule('─', '─', '─');
244 WriteColored(text.Select($"Total {summary.Total}", $"总计 {summary.Total}"), ConsoleColor.White);
245 Console.Write(" ");
246 WriteColored(text.Select($"Passed {summary.Passed}", $"通过 {summary.Passed}"), ConsoleColor.Green);
247 Console.Write(" ");
248 WriteColored(text.Select($"Failed {summary.Failed}", $"失败 {summary.Failed}"), summary.Failed == 0 ? ConsoleColor.DarkGray : ConsoleColor.Red);
249 Console.Write(" ");
250 WriteColored(text.Select($"Skipped {summary.Skipped}", $"跳过 {summary.Skipped}"), ConsoleColor.Yellow);
251 Console.WriteLine(text.Select($" Duration {FormatDuration(summary.Duration)}", $" 用时 {FormatDuration(summary.Duration)}"));
279 WriteSection(text.Select("Run summary", "运行汇总"));
280 var executed = summary.Passed + summary.Failed;
281 var successRate = executed == 0 ? 0d : summary.Passed * 100d / executed;
282 var resultLabel = summary.Failed == 0 ? text.Select("PASSED", "通过") : text.Select("FAILED", "失败");
283
284 WriteColored($" {PadDisplay(text.Select("Result", "结果"), 14)}", ConsoleColor.DarkGray);
285 WriteBadge(resultLabel, 10, summary.Failed == 0 ? ConsoleColor.Green : ConsoleColor.Red);
286 Console.Write(" ");
287 WriteColored(text.Select($"Duration {FormatDuration(summary.Duration)}", $"用时 {FormatDuration(summary.Duration)}"), ConsoleColor.White);
288 Console.WriteLine();
289
290 WriteColored($" {PadDisplay(text.Select("Tests", "测试"), 14)}", ConsoleColor.DarkGray);
291 WriteColored(text.Select($"{summary.Total} total", $"总计 {summary.Total}"), ConsoleColor.White);
292 Console.Write(" ");
293 WriteColored(text.Select($"{summary.Passed} passed", $"通过 {summary.Passed}"), ConsoleColor.Green);
294 Console.Write(" ");
295 WriteColored(text.Select($"{summary.Failed} failed", $"失败 {summary.Failed}"), summary.Failed == 0 ? ConsoleColor.DarkGray : ConsoleColor.Red);
296 Console.Write(" ");
297 WriteColoredLine(text.Select($"{summary.Skipped} skipped", $"跳过 {summary.Skipped}"), ConsoleColor.Yellow);
298
299 var barWidth = Math.Clamp(SafeConsoleWidth() - 36, 20, 48);
300 var filled = executed == 0 ? 0 : Math.Clamp((int)Math.Round(barWidth * successRate / 100d), 0, barWidth);
301 WriteColored($" {PadDisplay(text.Select("Success rate", "成功率"), 14)}", ConsoleColor.DarkGray);
302 WriteColored("[", ConsoleColor.DarkGray);
303 WriteColored(new string('█', filled), summary.Failed == 0 ? ConsoleColor.Green : ConsoleColor.Yellow);
304 WriteColored(new string('·', barWidth - filled), ConsoleColor.DarkGray);
305 WriteColored("]", ConsoleColor.DarkGray);
306 WriteColoredLine($" {successRate:F1}%", summary.Failed == 0 ? ConsoleColor.Green : ConsoleColor.Yellow);
307 }
308
309 private void PrintSlowestTests(TestRunSummary summary)
310 {
311 var slowest = summary.Results
312 .Where(static result => result.Outcome != TestOutcome.Skipped)
313 .OrderByDescending(static result => result.TotalDuration)
314 .Take(3)
315 .ToArray();
316 if (slowest.Length == 0)
317 return;
318
319 Console.WriteLine();
320 WriteColoredLine(text.Select(" Slowest tests", " 最慢测试"), ConsoleColor.DarkCyan);
321 var nameWidth = Math.Max(20, SafeConsoleWidth() - 24);
322 for (var index = 0; index < slowest.Length; index++)
323 {
324 WriteColored($" {index + 1}. ", ConsoleColor.DarkGray);
325 WriteColored(PadDisplay(TruncateDisplay(slowest[index].DisplayName, nameWidth), nameWidth), ConsoleColor.Gray);
326 WriteColoredLine(PadLeftDisplay(FormatDuration(slowest[index].TotalDuration), 16), ConsoleColor.DarkGray);
327 }
252 328 }
253 329
254 330 private void WriteTitle(string title)
255 331 {
256 332 var width = SafeConsoleWidth();
257 var suffixLength = Math.Max(1, width - DisplayWidth(title) - 4);
333 var suffixLength = Math.Max(1, width - DisplayWidth(title) - 5);
258 334 WriteColored($"╭─ {title} ", ConsoleColor.Cyan);
259 335 WriteColoredLine(new string('─', suffixLength) + "╮", ConsoleColor.DarkCyan);
260 336 }
@@ -262,33 +338,81 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
262 338 private static void WriteMetadata(string label, string value, string indent = "│ ")
263 339 {
264 340 Console.Write(indent);
265 Console.Write(PadDisplay(label, 12));
266 Console.WriteLine(value);
341 WriteColored(PadDisplay(label, 12), ConsoleColor.DarkGray);
342 WriteColoredLine(value, ConsoleColor.White);
267 343 }
268 344
269 private void WriteSection(string title)
345 private static void WriteMetadataGrid(IReadOnlyList<(string Label, string Value)> metadata)
270 346 {
271 WriteColoredLine(title, ConsoleColor.Cyan);
272 WriteColoredLine(new string('─', Math.Min(SafeConsoleWidth(), Math.Max(32, DisplayWidth(title) + 12))), ConsoleColor.DarkCyan);
347 var width = SafeConsoleWidth();
348 if (width < 120)
349 {
350 foreach (var item in metadata)
351 WriteMetadataBoxRow(item, null, width);
352 return;
353 }
354
355 for (var index = 0; index < metadata.Count; index += 2)
356 WriteMetadataBoxRow(metadata[index], index + 1 < metadata.Count ? metadata[index + 1] : null, width);
357 }
358
359 private static void WriteMetadataBoxRow((string Label, string Value) left, (string Label, string Value)? right, int width)
360 {
361 var contentWidth = width - 4;
362 Console.Write("│ ");
363 if (right.HasValue)
364 {
365 var leftWidth = (contentWidth - 3) / 2;
366 var rightWidth = contentWidth - 3 - leftWidth;
367 WriteMetadataCell(left, leftWidth);
368 WriteColored(" │ ", ConsoleColor.DarkCyan);
369 WriteMetadataCell(right.Value, rightWidth);
370 }
371 else
372 {
373 WriteMetadataCell(left, contentWidth);
374 }
375 Console.WriteLine(" │");
376 }
377
378 private static void WriteMetadataCell((string Label, string Value) item, int width)
379 {
380 const int labelWidth = 12;
381 var valueWidth = Math.Max(1, width - labelWidth);
382 WriteColored(PadDisplay(TruncateDisplay(item.Label, labelWidth), labelWidth), ConsoleColor.DarkGray);
383 WriteColored(PadDisplay(TruncateDisplay(item.Value, valueWidth), valueWidth), ConsoleColor.White);
384 }
385
386 private void WriteSection(string title, string? suffix = null)
387 {
388 var heading = suffix is null ? title : $"{title} · {suffix}";
389 heading = TruncateDisplay(heading, Math.Max(10, SafeConsoleWidth() - 5));
390 var prefix = $"── {heading} ";
391 WriteColored(prefix, ConsoleColor.Cyan);
392 WriteColoredLine(new string('─', Math.Max(1, SafeConsoleWidth() - DisplayWidth(prefix))), ConsoleColor.DarkCyan);
273 393 }
274 394
275 395 private void WriteListItem(string kind, string id, string displayName, ConsoleColor color)
276 396 {
277 WriteColored($" {PadDisplay(kind, 7)}", color);
278 Console.WriteLine($" {displayName} [{id}]");
397 WriteBadge(kind, 9, color);
398 Console.Write(" ");
399 WriteColored(displayName, ConsoleColor.White);
400 WriteColoredLine($" {id}", ConsoleColor.DarkGray);
279 401 }
280 402
281 403 private static void WriteDetail(string label, string value, ConsoleColor color)
282 404 {
283 WriteColored($" {label}: ", color);
284 Console.WriteLine(value);
405 Console.Write(new string(' ', 12));
406 WriteColored($"└─ {label} ", color);
407 WriteColoredLine(value, ConsoleColor.Gray);
285 408 }
286 409
287 410 private static void WriteBlock(string label, string value, ConsoleColor color)
288 411 {
289 WriteColoredLine($" {label}:", color);
412 var indent = new string(' ', 12);
413 WriteColoredLine($"{indent}└─ {label}", color);
290 414 foreach (var line in value.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n'))
291 Console.WriteLine($" {line}");
415 WriteColoredLine($"{indent} {line}", ConsoleColor.DarkGray);
292 416 }
293 417
294 418 private static void WriteHelpSection(string title, params (string Option, string Description)[] items)
@@ -303,6 +427,25 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
303 427 Console.WriteLine();
304 428 }
305 429
430 private static void WriteTestRow(string status, string name, string duration, int statusWidth, int nameWidth, int durationWidth, ConsoleColor color, bool badge)
431 {
432 if (badge)
433 WriteBadge(status, statusWidth, color);
434 else
435 WriteColored(PadDisplay(status, statusWidth), color);
436 Console.Write(" ");
437 WriteColored(PadDisplay(TruncateDisplay(name, nameWidth), nameWidth), badge ? ConsoleColor.Gray : color);
438 Console.Write(" ");
439 WriteColoredLine(PadLeftDisplay(duration, durationWidth), badge ? ConsoleColor.DarkGray : color);
440 }
441
442 private static void WriteBadge(string value, int width, ConsoleColor color)
443 {
444 var innerWidth = Math.Max(1, width - 2);
445 var content = CenterDisplay(TruncateDisplay(value, innerWidth), innerWidth);
446 WriteColored($"[{content}]", color);
447 }
448
306 449 private static void WriteTableRow(IReadOnlyList<string> values, IReadOnlyList<int> widths, ConsoleColor color)
307 450 {
308 451 for (var index = 0; index < values.Count; index++)
@@ -364,7 +507,7 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
364 507 try
365 508 {
366 509 var width = Console.WindowWidth;
367 return width > 0 ? Math.Clamp(width - 1, 96, 150) : 120;
510 return width > 0 ? Math.Clamp(width - 1, 72, 200) : 120;
368 511 }
369 512 catch
370 513 {
@@ -380,6 +523,19 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
380 523 return value + new string(' ', padding);
381 524 }
382 525
526 private static string PadLeftDisplay(string value, int width)
527 {
528 var padding = Math.Max(0, width - DisplayWidth(value));
529 return new string(' ', padding) + value;
530 }
531
532 private static string CenterDisplay(string value, int width)
533 {
534 var padding = Math.Max(0, width - DisplayWidth(value));
535 var left = padding / 2;
536 return new string(' ', left) + value + new string(' ', padding - left);
537 }
538
383 539 private static string TruncateDisplay(string value, int width)
384 540 {
385 541 if (DisplayWidth(value) <= width)