mirror of
https://github.com/aphrody-labs/PowerShell.git
synced 2026-10-14 05:39:11 +00:00
Add CustomTableHeaderLabel formatting to differentiate table header labels that are not property names (#17346)
This commit is contained in:
1 parent
50b8f130f4
commit
d5ed53eba2
10 files changed
+103
-41
No files matched your search
@@ -1,5 +1,6 @@
|
||||
[
|
||||
"PSCommandNotFoundSuggestion",
|
||||
"PSCustomTableHeaderLabelDecoration",
|
||||
"PSLoadAssemblyFromNativeCode",
|
||||
"PSNativeCommandErrorActionPreference",
|
||||
"PSSubsystemPluginModel"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[
|
||||
"PSCommandNotFoundSuggestion",
|
||||
"PSCustomTableHeaderLabelDecoration",
|
||||
"PSLoadAssemblyFromNativeCode",
|
||||
"PSNativeCommandErrorActionPreference",
|
||||
"PSSubsystemPluginModel"
|
||||
|
||||
@@ -1003,16 +1003,18 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
// create arrays for widths and alignment
|
||||
Span<int> columnWidths = columns <= StackAllocThreshold ? stackalloc int[columns] : new int[columns];
|
||||
Span<int> alignment = columns <= StackAllocThreshold ? stackalloc int[columns] : new int[columns];
|
||||
Span<bool> headerMatchesProperty = columns <= StackAllocThreshold ? stackalloc bool[columns] : new bool[columns];
|
||||
|
||||
int k = 0;
|
||||
foreach (TableColumnInfo tci in this.CurrentTableHeaderInfo.tableColumnInfoList)
|
||||
{
|
||||
columnWidths[k] = (columnWidthsHint != null) ? columnWidthsHint[k] : tci.width;
|
||||
alignment[k] = tci.alignment;
|
||||
headerMatchesProperty[k] = tci.HeaderMatchesProperty;
|
||||
k++;
|
||||
}
|
||||
|
||||
this.Writer.Initialize(0, _consoleWidth, columnWidths, alignment, this.CurrentTableHeaderInfo.hideHeader);
|
||||
this.Writer.Initialize(0, _consoleWidth, columnWidths, alignment, headerMatchesProperty, this.CurrentTableHeaderInfo.hideHeader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1241,7 +1243,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
alignment[k] = TextAlignment.Left;
|
||||
}
|
||||
|
||||
this.Writer.Initialize(0, columnsOnTheScreen, columnWidths, alignment, false, GetConsoleWindowHeight(this.InnerCommand._lo.RowNumber));
|
||||
this.Writer.Initialize(leftMarginIndent: 0, columnsOnTheScreen, columnWidths, alignment, headerMatchesProperty: null, suppressHeader: false, screenRows: GetConsoleWindowHeight(this.InnerCommand._lo.RowNumber));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -172,7 +172,11 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
ci.width = colHeader.width;
|
||||
ci.alignment = colHeader.alignment;
|
||||
if (colHeader.label != null)
|
||||
{
|
||||
ci.HeaderMatchesProperty = so.Properties[colHeader.label.text] is not null || !ExperimentalFeature.IsEnabled(ExperimentalFeature.PSCustomTableHeaderLabelDecoration);
|
||||
|
||||
ci.label = this.dataBaseInfo.db.displayResourceManagerCache.GetTextTokenString(colHeader.label);
|
||||
}
|
||||
}
|
||||
|
||||
if (ci.alignment == TextAlignment.Undefined)
|
||||
|
||||
@@ -207,6 +207,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
public int alignment = TextAlignment.Left;
|
||||
public string label = null;
|
||||
public string propertyName = null;
|
||||
public bool HeaderMatchesProperty = true;
|
||||
}
|
||||
|
||||
internal sealed class ListViewHeaderInfo : ShapeInfo
|
||||
|
||||
@@ -366,6 +366,17 @@ namespace System.Management.Automation
|
||||
|
||||
private string _tableHeader = "\x1b[32;1m";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the style for custom table headers.
|
||||
/// </summary>
|
||||
public string CustomTableHeaderLabel
|
||||
{
|
||||
get => _customTableHeaderLabel;
|
||||
set => _customTableHeaderLabel = ValidateNoContent(value);
|
||||
}
|
||||
|
||||
private string _customTableHeaderLabel = "\x1b[32;1;3m";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the accent style for errors.
|
||||
/// </summary>
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
internal int startCol = 0;
|
||||
internal int width = 0;
|
||||
internal int alignment = TextAlignment.Left;
|
||||
internal bool HeaderMatchesProperty = true;
|
||||
}
|
||||
/// <summary>
|
||||
/// Class containing information about the tabular layout.
|
||||
@@ -82,9 +83,10 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
/// <param name="screenColumns">Number of character columns on the screen.</param>
|
||||
/// <param name="columnWidths">Array of specified column widths.</param>
|
||||
/// <param name="alignment">Array of alignment flags.</param>
|
||||
/// <param name="headerMatchesProperty">Array of flags where the header label matches a property name.</param>
|
||||
/// <param name="suppressHeader">If true, suppress header printing.</param>
|
||||
/// <param name="screenRows">Number of rows on the screen.</param>
|
||||
internal void Initialize(int leftMarginIndent, int screenColumns, Span<int> columnWidths, ReadOnlySpan<int> alignment, bool suppressHeader, int screenRows = int.MaxValue)
|
||||
internal void Initialize(int leftMarginIndent, int screenColumns, Span<int> columnWidths, ReadOnlySpan<int> alignment, ReadOnlySpan<bool> headerMatchesProperty, bool suppressHeader, int screenRows = int.MaxValue)
|
||||
{
|
||||
if (leftMarginIndent < 0)
|
||||
{
|
||||
@@ -139,6 +141,11 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
_si.columnInfo[k].startCol = startCol;
|
||||
_si.columnInfo[k].width = columnWidths[k];
|
||||
_si.columnInfo[k].alignment = alignment[k];
|
||||
if (!headerMatchesProperty.IsEmpty)
|
||||
{
|
||||
_si.columnInfo[k].HeaderMatchesProperty = headerMatchesProperty[k];
|
||||
}
|
||||
|
||||
startCol += columnWidths[k] + ScreenInfo.separatorCharacterCount;
|
||||
}
|
||||
}
|
||||
@@ -156,7 +163,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
|
||||
foreach (string line in _header)
|
||||
{
|
||||
lo.WriteLine(style == string.Empty ? line : style + line + reset);
|
||||
lo.WriteLine(line);
|
||||
}
|
||||
|
||||
return _header.Count;
|
||||
@@ -234,35 +241,21 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
|
||||
if (multiLine)
|
||||
{
|
||||
foreach (string line in GenerateTableRow(values, currentAlignment, lo.DisplayCells))
|
||||
foreach (string line in GenerateTableRow(values, currentAlignment, lo.DisplayCells, isHeader))
|
||||
{
|
||||
generatedRows?.Add(line);
|
||||
if (isHeader)
|
||||
{
|
||||
lo.WriteLine(style == string.Empty ? line : style + line + reset);
|
||||
}
|
||||
else
|
||||
{
|
||||
lo.WriteLine(line);
|
||||
}
|
||||
lo.WriteLine(line);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string line = GenerateRow(values, currentAlignment, dc);
|
||||
string line = GenerateRow(values, currentAlignment, dc, isHeader);
|
||||
generatedRows?.Add(line);
|
||||
if (isHeader)
|
||||
{
|
||||
lo.WriteLine(style == string.Empty ? line : style + line + reset);
|
||||
}
|
||||
else
|
||||
{
|
||||
lo.WriteLine(line);
|
||||
}
|
||||
lo.WriteLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
private string[] GenerateTableRow(string[] values, ReadOnlySpan<int> alignment, DisplayCells ds)
|
||||
private string[] GenerateTableRow(string[] values, ReadOnlySpan<int> alignment, DisplayCells ds, bool isHeader)
|
||||
{
|
||||
// select the active columns (skip hidden ones)
|
||||
Span<int> validColumnArray = _si.columnInfo.Length <= OutCommandInner.StackAllocThreshold ? stackalloc int[_si.columnInfo.Length] : new int[_si.columnInfo.Length];
|
||||
@@ -291,8 +284,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
}
|
||||
|
||||
// obtain a set of tokens for each field
|
||||
scArray[k] = GenerateMultiLineRowField(values[validColumnArray[k]], validColumnArray[k],
|
||||
alignment[validColumnArray[k]], ds, addPadding);
|
||||
scArray[k] = GenerateMultiLineRowField(values[validColumnArray[k]], validColumnArray[k], alignment[validColumnArray[k]], ds, addPadding);
|
||||
|
||||
// NOTE: the following padding operations assume that we
|
||||
// pad with a blank (or any character that ALWAYS maps to a single screen cell
|
||||
@@ -323,7 +315,9 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
for (int k = 0; k < scArray.Length; k++)
|
||||
{
|
||||
if (scArray[k].Count > screenRows)
|
||||
{
|
||||
screenRows = scArray[k].Count;
|
||||
}
|
||||
}
|
||||
|
||||
// column headers can span multiple rows if the width of the column is shorter than the header text like:
|
||||
@@ -335,7 +329,6 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
// 1 2 3
|
||||
//
|
||||
// To ensure we don't add whitespace to the end, we need to determine the last column in each row with content
|
||||
|
||||
System.Span<int> lastColWithContent = screenRows <= OutCommandInner.StackAllocThreshold ? stackalloc int[screenRows] : new int[screenRows];
|
||||
for (int row = 0; row < screenRows; row++)
|
||||
{
|
||||
@@ -390,17 +383,36 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
for (int row = 0; row < screenRows; row++)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// for a given row, walk the columns
|
||||
for (int col = 0; col < scArray.Length; col++)
|
||||
{
|
||||
string value = scArray[col][row];
|
||||
|
||||
// if the column is the last column with content, we need to trim trailing whitespace, unless there is only one row
|
||||
if (col == lastColWithContent[row] && screenRows > 1)
|
||||
{
|
||||
sb.Append(scArray[col][row].TrimEnd());
|
||||
value = value.TrimEnd();
|
||||
}
|
||||
else
|
||||
|
||||
if (isHeader)
|
||||
{
|
||||
sb.Append(scArray[col][row]);
|
||||
if (_si.columnInfo[col].HeaderMatchesProperty)
|
||||
{
|
||||
sb.Append(PSStyle.Instance.Formatting.TableHeader);
|
||||
}
|
||||
else if (value.Length > 0)
|
||||
{
|
||||
// after the first column, each additional column starts with a whitespace for separation
|
||||
value = value.Insert(col == 0 ? 0 : 1, PSStyle.Instance.Formatting.CustomTableHeaderLabel);
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(value);
|
||||
|
||||
if (isHeader)
|
||||
{
|
||||
sb.Append(PSStyle.Instance.Reset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +439,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
return sc;
|
||||
}
|
||||
|
||||
private string GenerateRow(string[] values, ReadOnlySpan<int> alignment, DisplayCells dc)
|
||||
private string GenerateRow(string[] values, ReadOnlySpan<int> alignment, DisplayCells dc, bool isHeader)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -462,9 +474,14 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
}
|
||||
|
||||
string rowField = GenerateRowField(values[k], _si.columnInfo[k].width, alignment[k], dc, addPadding);
|
||||
if (isHeader)
|
||||
{
|
||||
sb.Append(PSStyle.Instance.Formatting.TableHeader);
|
||||
}
|
||||
|
||||
sb.Append(rowField);
|
||||
|
||||
if (rowField is not null && rowField.Contains(ValueStringDecorated.ESC) && !rowField.AsSpan().TrimEnd().EndsWith(PSStyle.Instance.Reset))
|
||||
if (isHeader || (rowField is not null && rowField.Contains(ValueStringDecorated.ESC) && !rowField.AsSpan().TrimEnd().EndsWith(PSStyle.Instance.Reset)))
|
||||
{
|
||||
// Reset the console output if the content of this column contains ESC
|
||||
sb.Append(PSStyle.Instance.Reset);
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace System.Management.Automation
|
||||
|
||||
internal const string EngineSource = "PSEngine";
|
||||
internal const string PSNativeCommandErrorActionPreferenceFeatureName = "PSNativeCommandErrorActionPreference";
|
||||
internal const string PSCustomTableHeaderLabelDecoration = "PSCustomTableHeaderLabelDecoration";
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -116,6 +117,9 @@ namespace System.Management.Automation
|
||||
new ExperimentalFeature(
|
||||
name: PSNativeCommandErrorActionPreferenceFeatureName,
|
||||
description: "Native commands with non-zero exit codes issue errors according to $ErrorActionPreference when $PSNativeCommandUseErrorActionPreference is $true"),
|
||||
new ExperimentalFeature(
|
||||
name: PSCustomTableHeaderLabelDecoration,
|
||||
description: "Formatting differentiation for table header labels that aren't property members"),
|
||||
};
|
||||
|
||||
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
|
||||
|
||||
@@ -876,10 +876,10 @@ Describe 'Table color tests' {
|
||||
$PSStyle.OutputRendering = $originalRendering
|
||||
}
|
||||
|
||||
It 'Table header should use FormatAccent' {
|
||||
It 'Table header should use TableHeader' {
|
||||
([pscustomobject]@{foo = 1} | Format-Table | Out-String).Trim() | Should -BeExactly @"
|
||||
$($PSStyle.Formatting.FormatAccent)foo$($PSStyle.Reset)
|
||||
$($PSStyle.Formatting.FormatAccent)---$($PSStyle.Reset)
|
||||
$($PSStyle.Formatting.TableHeader)foo$($PSStyle.Reset)
|
||||
$($PSStyle.Formatting.TableHeader)---$($PSStyle.Reset)
|
||||
1
|
||||
"@
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ Describe 'Tests for $PSStyle automatic variable' {
|
||||
$formattingDefaults = @{
|
||||
FormatAccent = "`e[32;1m"
|
||||
TableHeader = "`e[32;1m"
|
||||
CustomTableHeaderLabel = "`e[32;1;3m"
|
||||
ErrorAccent = "`e[36;1m"
|
||||
Error = "`e[31;1m"
|
||||
Debug = "`e[33;1m"
|
||||
@@ -191,7 +192,7 @@ Describe 'Tests for $PSStyle automatic variable' {
|
||||
$PSStyle.OutputRendering = 'Ansi'
|
||||
$PSStyle.Formatting.TableHeader = ''
|
||||
$out = $PSVersionTable | Format-Table | Out-String
|
||||
$out.Contains("`e[") | Should -BeFalse
|
||||
$out.Replace($PSStyle.Reset,'').Contains("`e[") | Should -BeFalse
|
||||
}
|
||||
finally {
|
||||
$PSStyle.OutputRendering = $oldRender
|
||||
@@ -199,6 +200,25 @@ Describe 'Tests for $PSStyle automatic variable' {
|
||||
}
|
||||
}
|
||||
|
||||
It '$PSStyle.Formatting.CustomTableHeaderLabel is applied to Format-Table' {
|
||||
$old = $PSStyle.Formatting.CustomTableHeaderLabel
|
||||
$oldRender = $PSStyle.OutputRendering
|
||||
|
||||
try {
|
||||
$PSStyle.OutputRendering = 'Ansi'
|
||||
$PSStyle.Formatting.CustomTableHeaderLabel = $PSStyle.Foreground.Blue + $PSStyle.Background.White + $PSStyle.Bold
|
||||
$out = Get-Process pwsh | Select-Object -First 1 | Format-Table | Out-String
|
||||
$format = $PSStyle.Formatting.CustomTableHeaderLabel.Replace('[',"``[")
|
||||
$header = $PSStyle.Formatting.TableHeader.Replace('[',"``[")
|
||||
$reset = $PSStyle.Reset.Replace('[',"``[")
|
||||
$out | Should -BeLike "*${format}*NPM(K)${reset}*${format}*PM(M)${reset}*${format}*WS(M)${reset}*${format}*CPU(s)${reset}*${header}*Id${reset}*${header}*SI${reset}*${header}*ProcessName${reset}*"
|
||||
}
|
||||
finally {
|
||||
$PSStyle.OutputRendering = $oldRender
|
||||
$PSStyle.Formatting.CustomTableHeaderLabel = $old
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should fail if setting formatting contains printable characters: <member>.<submember>' -TestCases @(
|
||||
@{ Submember = 'Reset' }
|
||||
@{ Submember = 'BlinkOff' }
|
||||
@@ -215,6 +235,7 @@ Describe 'Tests for $PSStyle automatic variable' {
|
||||
@{ Submember = 'Strikethrough' }
|
||||
@{ Member = 'Formatting'; Submember = 'FormatAccent' }
|
||||
@{ Member = 'Formatting'; Submember = 'TableHeader' }
|
||||
@{ Member = 'Formatting'; Submember = 'CustomTableHeaderLabel' }
|
||||
@{ Member = 'Formatting'; Submember = 'ErrorAccent' }
|
||||
@{ Member = 'Formatting'; Submember = 'Error' }
|
||||
@{ Member = 'Formatting'; Submember = 'Warning' }
|
||||
@@ -380,8 +401,8 @@ Describe 'Handle strings with escape sequences in formatting' {
|
||||
|
||||
It 'Truncation for strings with no escape sequences' {
|
||||
$expected = @"
|
||||
`e[32;1mName Role YIR`e[0m
|
||||
`e[32;1m---- ---- ---`e[0m
|
||||
`e[32;1mName `e[0m`e[32;1m Role `e[0m`e[32;1m YIR`e[0m
|
||||
`e[32;1m---- `e[0m `e[32;1m---- `e[0m `e[32;1m---`e[0m
|
||||
Bob Saggat Developer 2
|
||||
John Seym… Sw Engineer 6
|
||||
Billy Bob… Senior DevOps … 13
|
||||
@@ -398,8 +419,8 @@ Billy Bob… Senior DevOps … 13
|
||||
|
||||
It "Truncation for strings with escape sequences - TableView-1" {
|
||||
$expected = @"
|
||||
`e[32;1mName Role YIR`e[0m
|
||||
`e[32;1m---- ---- ---`e[0m
|
||||
`e[32;1mName `e[0m`e[32;1m Role `e[0m`e[32;1m YIR`e[0m
|
||||
`e[32;1m---- `e[0m `e[32;1m---- `e[0m `e[32;1m---`e[0m
|
||||
`e[32mBob Saggat`e[39m`e[0m Developer 2
|
||||
`e[33mJohn Seym…`e[0m Sw Engineer 6
|
||||
`e[31mBilly Bob…`e[0m Senior DevOps … 13
|
||||
@@ -420,8 +441,8 @@ Billy Bob… Senior DevOps … 13
|
||||
|
||||
It "Truncation for strings with escape sequences - TableView-2" {
|
||||
$expected = @"
|
||||
`e[32;1mName Role YIR`e[0m
|
||||
`e[32;1m---- ---- ---`e[0m
|
||||
`e[32;1mName `e[0m`e[32;1m Role `e[0m`e[32;1m YIR`e[0m
|
||||
`e[32;1m---- `e[0m `e[32;1m---- `e[0m `e[32;1m---`e[0m
|
||||
`e[32mBob Saggat`e[39m`e[0m Developer`e[0m 2
|
||||
`e[33mJohn Seym…`e[0m `e[1;33mSw Engineer`e[0m 6
|
||||
`e[31mBilly Bob…`e[0m `e[42m`e[1;33mSenior DevOps …`e[0m 13
|
||||
|
||||
Reference in new issue
Block a user