Skip to content

Commit b321382

Browse files
- Refactor 2 projects into single project
- Use Format-Table as formatter rather than built-in logic - Enabling styling via Format-Table's Properties feature - Fixing bug where columns were not aligned to right when appropriate - Reduce code base to maintain.
1 parent 2078bf3 commit b321382

26 files changed

+1243
-1505
lines changed

ConsoleGuiTools.Common.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
22
<PropertyGroup>
3-
<VersionPrefix>0.7.7</VersionPrefix>
3+
<VersionPrefix>0.8.0</VersionPrefix>
44
<VersionSuffix></VersionSuffix>
55
<Company>Microsoft</Company>
66
<Copyright>© Microsoft Corporation.</Copyright>

src/Microsoft.PowerShell.ConsoleGuiTools/ConsoleGui.cs

+259-363
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.PowerShell.ConsoleGuiTools;
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.ObjectModel;
9+
using System.Data;
10+
using System.Linq;
11+
using System.Management.Automation;
12+
using Microsoft.PowerShell.ConsoleGuiTools.Models;
13+
14+
/// <summary>
15+
/// Helper class for formatting objects passed pipeline object into text.
16+
/// </summary>
17+
internal static class FormatHelper
18+
{
19+
/// <summary>
20+
/// Formats the output of a command as a table with the selected properties of the object in each column.
21+
/// The object type determines the default layout and properties that are displayed in each column.
22+
/// You can use the Property parameter to select the properties that you want to display.
23+
/// </summary>
24+
/// <param name="inputs">Collection of <see cref="PSObject"/></param>
25+
/// <param name="properties">Specifies the object properties that appear in the display and the order in which they appear.
26+
/// Type one or more property names, separated by commas, or use a hash table to display a calculated property.
27+
/// Wildcards are permitted.</param>
28+
/// <returns><see cref="Table"/> data transfer object that contains header and rows in string.</returns>
29+
/// <remarks>
30+
/// <c>Format-Table</c> Powershell command is used to format the inputs objects as a table.
31+
/// </remarks>
32+
internal static Table FormatTable(IReadOnlyList<PSObject> inputs, bool force, object[] properties = null)
33+
{
34+
if (inputs.Count == 0)
35+
{
36+
return Table.Empty;
37+
}
38+
39+
using var ps = PowerShell.Create(RunspaceMode.CurrentRunspace);
40+
ps.AddCommand("Format-Table");
41+
ps.AddParameter("AutoSize");
42+
43+
if (properties != null)
44+
{
45+
ps.AddParameter("Property", properties);
46+
}
47+
48+
if (force == true)
49+
{
50+
ps.AddParameter("Force");
51+
}
52+
53+
ps.AddParameter("InputObject", inputs);
54+
55+
// Format-Table's output objects are internal to Powershell,
56+
// we cannot use them here, so we need to convert it to a string as workaround.
57+
ps.AddCommand("Out-String");
58+
59+
var results = ps.Invoke();
60+
var text = results.FirstOrDefault()?.BaseObject.ToString();
61+
62+
var lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
63+
.Where(line => string.IsNullOrEmpty(line) == false).ToList();
64+
65+
/*
66+
* Format-Table sometimes outputs a label on the top based on input's data type.
67+
* We need to detect and skip the label and extract only header and rows.
68+
* Strategy is to detect the index of the line under the header with dashes and spaces ('---- -- --- ').
69+
*/
70+
71+
static bool isHeaderLine(string text) => text.Contains('-') && text.All(c => c == '-' || c == ' ');
72+
73+
var headerLineIndex = lines.FindIndex(isHeaderLine);
74+
75+
if (headerLineIndex == -1)
76+
{
77+
// unexpected result, return the whole text
78+
headerLineIndex = 1;
79+
}
80+
81+
return new Table
82+
{
83+
Header = lines.Skip(headerLineIndex - 1).FirstOrDefault(),
84+
HeaderLine = lines.Skip(headerLineIndex).FirstOrDefault(),
85+
Rows = lines.Skip(headerLineIndex + 1)
86+
};
87+
}
88+
}

src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDataSource.cs

+51-54
Original file line numberDiff line numberDiff line change
@@ -4,80 +4,77 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Linq;
8+
9+
using Microsoft.PowerShell.ConsoleGuiTools.Models;
710

811
using NStack;
912

1013
using Terminal.Gui;
1114

12-
namespace OutGridView.Cmdlet
13-
{
14-
internal sealed class GridViewDataSource : IListDataSource
15-
{
16-
public List<GridViewRow> GridViewRowList { get; set; }
15+
namespace Microsoft.PowerShell.ConsoleGuiTools;
1716

18-
public int Count => GridViewRowList.Count;
17+
internal sealed class GridViewDataSource(IEnumerable<GridViewRow> gridViewRowList) : IListDataSource
18+
{
19+
internal List<GridViewRow> GridViewRowList { get; init; } = gridViewRowList.ToList();
1920

20-
public GridViewDataSource(List<GridViewRow> itemList)
21-
{
22-
GridViewRowList = itemList;
23-
}
21+
public int Count => GridViewRowList.Count;
2422

25-
public int Length { get; }
23+
public int Length { get; }
2624

27-
public void Render(ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start)
28-
{
29-
container.Move(col, line);
30-
RenderUstr(driver, GridViewRowList[item].DisplayString, col, line, width);
31-
}
25+
public void Render(ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start)
26+
{
27+
container.Move(col, line);
28+
RenderUstr(driver, GridViewRowList[item].DisplayString, width);
29+
}
3230

33-
public bool IsMarked(int item) => GridViewRowList[item].IsMarked;
31+
public bool IsMarked(int item) => GridViewRowList[item].IsMarked;
3432

35-
public void SetMark(int item, bool value)
33+
public void SetMark(int item, bool value)
34+
{
35+
var oldValue = GridViewRowList[item].IsMarked;
36+
GridViewRowList[item].IsMarked = value;
37+
var args = new RowMarkedEventArgs()
3638
{
37-
var oldValue = GridViewRowList[item].IsMarked;
38-
GridViewRowList[item].IsMarked = value;
39-
var args = new RowMarkedEventArgs()
40-
{
41-
Row = GridViewRowList[item],
42-
OldValue = oldValue
43-
};
44-
MarkChanged?.Invoke(this, args);
45-
}
39+
Row = GridViewRowList[item],
40+
OldValue = oldValue
41+
};
42+
MarkChanged?.Invoke(this, args);
43+
}
4644

47-
public sealed class RowMarkedEventArgs : EventArgs
48-
{
49-
public GridViewRow Row { get; set; }
50-
public bool OldValue { get; set; }
45+
public sealed class RowMarkedEventArgs : EventArgs
46+
{
47+
public GridViewRow Row { get; set; }
48+
public bool OldValue { get; set; }
5149

52-
}
50+
}
5351

54-
public event EventHandler<RowMarkedEventArgs> MarkChanged;
52+
public event EventHandler<RowMarkedEventArgs> MarkChanged;
5553

56-
public IList ToList()
54+
public IList ToList()
55+
{
56+
return GridViewRowList;
57+
}
58+
59+
// A slightly adapted method from gui.cs: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
60+
private static void RenderUstr(ConsoleDriver driver, ustring ustr, int width)
61+
{
62+
int used = 0;
63+
int index = 0;
64+
while (index < ustr.Length)
5765
{
58-
return GridViewRowList;
66+
(var rune, var size) = Utf8.DecodeRune(ustr, index, index - ustr.Length);
67+
var count = Rune.ColumnWidth(rune);
68+
if (used + count > width) break;
69+
driver.AddRune(rune);
70+
used += count;
71+
index += size;
5972
}
6073

61-
// A slightly adapted method from gui.cs: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
62-
private static void RenderUstr(ConsoleDriver driver, ustring ustr, int col, int line, int width)
74+
while (used < width)
6375
{
64-
int used = 0;
65-
int index = 0;
66-
while (index < ustr.Length)
67-
{
68-
(var rune, var size) = Utf8.DecodeRune(ustr, index, index - ustr.Length);
69-
var count = Rune.ColumnWidth(rune);
70-
if (used + count > width) break;
71-
driver.AddRune(rune);
72-
used += count;
73-
index += size;
74-
}
75-
76-
while (used < width)
77-
{
78-
driver.AddRune(' ');
79-
used++;
80-
}
76+
driver.AddRune(' ');
77+
used++;
8178
}
8279
}
8380
}

src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDetails.cs

-19
This file was deleted.

src/Microsoft.PowerShell.ConsoleGuiTools/GridViewHelpers.cs

+42-48
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,59 @@
22
// Licensed under the MIT License.
33

44
using System.Collections.Generic;
5+
using System.Data;
56
using System.Linq;
6-
using System.Text;
77
using System.Text.RegularExpressions;
88

9-
namespace OutGridView.Cmdlet
9+
using Microsoft.PowerShell.ConsoleGuiTools.Models;
10+
11+
namespace Microsoft.PowerShell.ConsoleGuiTools;
12+
13+
internal static class GridViewHelpers
1014
{
11-
internal sealed class GridViewHelpers
15+
// Add all items already selected plus any that match the filter
16+
// The selected items should be at the top of the list, in their original order
17+
internal static List<GridViewRow> FilterData(List<GridViewRow> listToFilter, string filter)
1218
{
13-
// Add all items already selected plus any that match the filter
14-
// The selected items should be at the top of the list, in their original order
15-
public static List<GridViewRow> FilterData(List<GridViewRow> listToFilter, string filter)
19+
var filteredList = new List<GridViewRow>();
20+
if (string.IsNullOrEmpty(filter))
1621
{
17-
var filteredList = new List<GridViewRow>();
18-
if (string.IsNullOrEmpty(filter))
19-
{
20-
return listToFilter;
21-
}
22-
23-
filteredList.AddRange(listToFilter.Where(gvr => gvr.IsMarked));
24-
filteredList.AddRange(listToFilter.Where(gvr => !gvr.IsMarked && Regex.IsMatch(gvr.DisplayString, filter, RegexOptions.IgnoreCase)));
25-
26-
return filteredList;
22+
return listToFilter;
2723
}
2824

29-
public static string GetPaddedString(List<string> strings, int offset, int[] listViewColumnWidths)
30-
{
31-
var builder = new StringBuilder();
32-
if (offset > 0)
33-
{
34-
builder.Append(string.Empty.PadRight(offset));
35-
}
25+
filteredList.AddRange(listToFilter.Where(gvr => gvr.IsMarked));
26+
filteredList.AddRange(listToFilter.Where(gvr => !gvr.IsMarked && Regex.IsMatch(gvr.DisplayString, filter, RegexOptions.IgnoreCase)));
3627

37-
for (int i = 0; i < strings.Count; i++)
38-
{
39-
if (i > 0)
40-
{
41-
// Add a space between columns
42-
builder.Append(' ');
43-
}
28+
return filteredList;
29+
}
4430

45-
// Replace any newlines with encoded newline/linefeed (`n or `r)
46-
// Note we can't use Environment.Newline because we don't know that the
47-
// Command honors that.
48-
strings[i] = strings[i].Replace("\r", "`r");
49-
strings[i] = strings[i].Replace("\n", "`n");
31+
/// <summary>
32+
/// Creates the header and data source for the GridView.
33+
/// </summary>
34+
/// <param name="listViewOffset"> Dictates where the header should actually start considering
35+
/// some offset is needed to factor in the checkboxes
36+
/// </param>
37+
/// <param name="applicationData"></param>
38+
/// <param name="leftMargin">Dictates where the header should actually start considering some offset is needed to factor in the checkboxes</param>
39+
/// <returns><see cref="GridViewHeader"/> and <see cref="GridViewDataSource"/> from commandlet inputs.</returns>
40+
internal static (GridViewHeader Header, GridViewDataSource DataSource) CreateGridViewInputs(int listViewOffset, int leftMargin, ApplicationData applicationData, object[] properties)
41+
{
42+
var table = FormatHelper.FormatTable(applicationData.Input, applicationData.Force, properties);
5043

51-
// If the string won't fit in the column, append an ellipsis.
52-
if (strings[i].Length > listViewColumnWidths[i])
53-
{
54-
builder.Append(strings[i], 0, listViewColumnWidths[i] - 3);
55-
builder.Append("...");
56-
}
57-
else
58-
{
59-
builder.Append(strings[i].PadRight(listViewColumnWidths[i]));
60-
}
61-
}
44+
var gridViewHeader = new GridViewHeader
45+
{
46+
HeaderText = string.Concat(new string(' ', listViewOffset), table.Header),
47+
HeaderUnderLine = string.Concat(new string(' ', listViewOffset), table.HeaderLine)
48+
};
6249

63-
return builder.ToString();
64-
}
50+
var gridViewDataSource = new GridViewDataSource(table.Rows.Select((line, index) => new GridViewRow
51+
{
52+
DisplayString = line,
53+
OriginalIndex = index
54+
}));
55+
56+
return (
57+
gridViewHeader,
58+
gridViewDataSource);
6559
}
6660
}

src/Microsoft.PowerShell.ConsoleGuiTools/GridViewRow.cs

-13
This file was deleted.

src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.csproj

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<Import Project="..\..\ConsoleGuiTools.Common.props" />
33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
@@ -11,6 +11,7 @@
1111
- Add ';https://api.nuget.org/v3/index.json' to the end of the RestoreSources property group below
1212
- Uncomment the RestoreSources property group below
1313
-->
14+
<!-- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> -->
1415
<!-- <RestoreSources>$(RestoreSources);../../../gui-cs/Terminal.Gui/Terminal.Gui/bin/Debug</RestoreSources> -->
1516
</PropertyGroup>
1617

@@ -20,10 +21,6 @@
2021
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.23" />
2122
</ItemGroup>
2223

23-
<ItemGroup>
24-
<ProjectReference Include ="../Microsoft.PowerShell.OutGridView.Models/Microsoft.PowerShell.OutGridView.Models.csproj" />
25-
</ItemGroup>
26-
2724
<ItemGroup>
2825
<None Update="Microsoft.PowerShell.ConsoleGuiTools.psd1" CopyToOutputDirectory="PreserveNewest" />
2926
</ItemGroup>

0 commit comments

Comments
 (0)