Add CustomTableHeaderLabel formatting to differentiate table header labels that are not property names (#17346)

This commit is contained in:
Steve Lee authored and GitHub committed 2022-10-03 15:05:11 -07:00
1 parent 50b8f130f4
commit d5ed53eba2
10 files changed
+103 -41

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
[
"PSCommandNotFoundSuggestion",
"PSCustomTableHeaderLabelDecoration",
"PSLoadAssemblyFromNativeCode",
"PSNativeCommandErrorActionPreference",
"PSSubsystemPluginModel"
+1
View File
@@ -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