返回提交历史
Modified
README.md
+1
-1
Modified
README.zh-CN.md
+1
-1
Modified
XFEExtension.NetCore.XUnit/Execution/XfeRunner.cs
+1
-0
Modified
XFEExtension.NetCore.XUnit/Reporting/ConsolePresenter.cs
+206
-50
XFEstudio/XFEExtension.NetCore.XUnit
升级测试运行器控制台输出为自适应信息卡片
- 控制台输出支持 UTF-8,提升编码兼容性 - 运行信息采用自适应双栏表格,宽屏两栏、窄屏单栏 - 测试/基准结果列表引入状态徽章与耗时对齐 - 失败详情分层缩进,信息层级更清晰 - 汇总新增成功率进度条、统计数量与总用时 - 自动展示最慢测试,便于性能分析 - 基准结果与回归检测采用徽章和对齐表格 - 控制台宽度自适应范围扩大,输出更紧凑智能 - README(中英文)同步更新,描述新版特性
8bb3e71
代码差异
4 个文件
+209
-52
@@ -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
@@ -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
@@ -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
{
@@ -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)