Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User/emray/refactor lean #256

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ConsoleGuiTools.Common.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>0.7.7</VersionPrefix>
<VersionPrefix>0.8.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
<Company>Microsoft</Company>
<Copyright>© Microsoft Corporation.</Copyright>
Expand Down
180 changes: 39 additions & 141 deletions src/Microsoft.PowerShell.ConsoleGuiTools/ConsoleGui.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;

using OutGridView.Models;
using Microsoft.PowerShell.ConsoleGuiTools.Models;

using Terminal.Gui;

namespace OutGridView.Cmdlet
namespace Microsoft.PowerShell.ConsoleGuiTools
{

internal sealed class ConsoleGui : IDisposable
{
private const string FILTER_LABEL = "Filter";
Expand All @@ -32,41 +32,39 @@ internal sealed class ConsoleGui : IDisposable
private GridViewDataSource _inputSource;

// _listViewSource is a filtered copy of _inputSource that ListView.Source is set to.
// Changes to IsMarked are propogated back to _inputSource.
// Changes to IsMarked are propagated back to _inputSource.
private GridViewDataSource _listViewSource;
private ApplicationData _applicationData;
private GridViewDetails _gridViewDetails;

public HashSet<int> Start(ApplicationData applicationData)
public IEnumerable<int> Start(ApplicationData applicationData)
{
_applicationData = applicationData;
// Note, in Terminal.Gui v2, this property is renamed to Application.UseNetDriver, hence
// using that terminology here.
Application.UseSystemConsole = _applicationData.UseNetDriver;
Application.Init();
_gridViewDetails = new GridViewDetails
{

var (gridViewHeader, gridViewDataSource) = GridViewHelpers.CreateGridViewInputs(
// If OutputMode is Single or Multiple, then we make items selectable. If we make them selectable,
// 2 columns are required for the check/selection indicator and space.
ListViewOffset = _applicationData.OutputMode != OutputModeOption.None ? MARGIN_LEFT + CHECK_WIDTH : MARGIN_LEFT
};
listViewOffset: _applicationData.OutputMode != OutputModeOption.None ? MARGIN_LEFT + CHECK_WIDTH : MARGIN_LEFT,
applicationData: _applicationData,
properties: _applicationData.Properties,
leftMargin: MARGIN_LEFT
);

Window win = CreateTopLevelWindow();

// Create the headers and calculate column widths based on the DataTable
List<string> gridHeaders = _applicationData.DataTable.DataColumns.Select((c) => c.Label).ToList();
CalculateColumnWidths(gridHeaders);

// Copy the input DataTable into our master ListView source list; upon exit any items
// that are IsMarked are returned (if Outputmode is set)
_inputSource = LoadData();
_inputSource = gridViewDataSource;

if (!_applicationData.MinUI)
{
// Add Filter UI
AddFilter(win);
// Add Header UI
AddHeaders(win, gridHeaders);
AddHeaders(win, gridViewHeader);
}

// Add ListView
Expand All @@ -76,8 +74,8 @@ public HashSet<int> Start(ApplicationData applicationData)
AddStatusBar(!_applicationData.MinUI);

// We *always* apply a filter, even if the -Filter parameter is not set or Filtering is not
// available. The ListView always shows a fitlered version of _inputSource even if there is no
// actual fitler.
// available. The ListView always shows a filtered version of _inputSource even if there is no
// actual filter.
ApplyFilter();

_listView.SetFocus();
Expand All @@ -86,52 +84,16 @@ public HashSet<int> Start(ApplicationData applicationData)
Application.Run();
Application.Shutdown();

// Return results of selection if required.
HashSet<int> selectedIndexes = new HashSet<int>();
// Return results of selection if required.
if (_cancelled)
{
return selectedIndexes;
return Enumerable.Empty<int>();
}

// Return any items that were selected.
foreach (GridViewRow gvr in _inputSource.GridViewRowList)
{
if (gvr.IsMarked)
{
selectedIndexes.Add(gvr.OriginalIndex);
}
}

return selectedIndexes;
}

private GridViewDataSource LoadData()
{
var items = new List<GridViewRow>();
int newIndex = 0;
for (int i = 0; i < _applicationData.DataTable.Data.Count; i++)
{
var dataTableRow = _applicationData.DataTable.Data[i];
var valueList = new List<string>();
foreach (var dataTableColumn in _applicationData.DataTable.DataColumns)
{
string dataValue = dataTableRow.Values[dataTableColumn.ToString()].DisplayValue;
valueList.Add(dataValue);
}

string displayString = GridViewHelpers.GetPaddedString(valueList, 0, _gridViewDetails.ListViewColumnWidths);

items.Add(new GridViewRow
{
DisplayString = displayString,
// We use this to keep _inputSource up to date when a filter is applied
OriginalIndex = i
});

newIndex++;
}

return new GridViewDataSource(items);
return _inputSource.GridViewRowList
.Where(gvr => gvr.IsMarked)
.Select(gvr => gvr.OriginalIndex);
}

private void ApplyFilter()
Expand Down Expand Up @@ -249,57 +211,12 @@ private void AddStatusBar(bool visible)
$"{Application.Driver} v{FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(Application)).Location).ProductVersion}", null));
}

var statusBar = new StatusBar(statusItems.ToArray());
statusBar.Visible = visible;
Application.Top.Add(statusBar);
}

private void CalculateColumnWidths(List<string> gridHeaders)
{
_gridViewDetails.ListViewColumnWidths = new int[gridHeaders.Count];
var listViewColumnWidths = _gridViewDetails.ListViewColumnWidths;

for (int i = 0; i < gridHeaders.Count; i++)
{
listViewColumnWidths[i] = gridHeaders[i].Length;
}

// calculate the width of each column based on longest string in each column for each row
foreach (var row in _applicationData.DataTable.Data)
{
int index = 0;

// use half of the visible buffer height for the number of objects to inspect to calculate widths
foreach (var col in row.Values.Take(Application.Top.Frame.Height / 2))
{
var len = col.Value.DisplayValue.Length;
if (len > listViewColumnWidths[index])
{
listViewColumnWidths[index] = len;
}
index++;
}
}

// if the total width is wider than the usable width, remove 1 from widest column until it fits
_gridViewDetails.UsableWidth = Application.Top.Frame.Width - MARGIN_LEFT - listViewColumnWidths.Length - _gridViewDetails.ListViewOffset;
int columnWidthsSum = listViewColumnWidths.Sum();
while (columnWidthsSum >= _gridViewDetails.UsableWidth)
var statusBar = new StatusBar(statusItems.ToArray())
{
int maxWidth = 0;
int maxIndex = 0;
for (int i = 0; i < listViewColumnWidths.Length; i++)
{
if (listViewColumnWidths[i] > maxWidth)
{
maxWidth = listViewColumnWidths[i];
maxIndex = i;
}
}
Visible = visible
};

listViewColumnWidths[maxIndex]--;
columnWidthsSum--;
}
Application.Top.Add(statusBar);
}

private void AddFilter(Window win)
Expand Down Expand Up @@ -360,47 +277,28 @@ private void AddFilter(Window win)
_filterField.CursorPosition = _filterField.Text.Length;
}

private void AddHeaders(Window win, List<string> gridHeaders)
private void AddHeaders(Window win, GridViewHeader gridViewHeader)
{
var header = new Label(GridViewHelpers.GetPaddedString(
gridHeaders,
_gridViewDetails.ListViewOffset,
_gridViewDetails.ListViewColumnWidths));
header.X = 0;
if (_applicationData.MinUI)
var header = new Label(gridViewHeader.HeaderText)
{
header.Y = 0;
}
else
{
header.Y = 2;
}
X = 0,
Y = _applicationData.MinUI ? 0 : 2
};

win.Add(header);

// This renders dashes under the header to make it more clear what is header and what is data
var headerLineText = new StringBuilder();
foreach (char c in header.Text)
if (_applicationData.MinUI)
{
if (c.Equals(' '))
{
headerLineText.Append(' ');
}
else
{
// When gui.cs supports text decorations, should replace this with just underlining the header
headerLineText.Append('-');
}
return;
}

if (!_applicationData.MinUI)
var headerLine = new Label(gridViewHeader.HeaderUnderLine)
{
var headerLine = new Label(headerLineText.ToString())
{
X = 0,
Y = Pos.Bottom(header)
};
win.Add(headerLine);
}
X = 0,
Y = Pos.Bottom(header)
};

win.Add(headerLine);
}

private void AddListView(Window win)
Expand Down
88 changes: 88 additions & 0 deletions src/Microsoft.PowerShell.ConsoleGuiTools/FormatHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.PowerShell.ConsoleGuiTools;

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Linq;
using System.Management.Automation;
using Microsoft.PowerShell.ConsoleGuiTools.Models;

/// <summary>
/// Helper class for formatting objects passed pipeline object into text.
/// </summary>
internal static class FormatHelper
{
/// <summary>
/// Formats the output of a command as a table with the selected properties of the object in each column.
/// The object type determines the default layout and properties that are displayed in each column.
/// You can use the Property parameter to select the properties that you want to display.
/// </summary>
/// <param name="inputs">Collection of <see cref="PSObject"/></param>
/// <param name="properties">Specifies the object properties that appear in the display and the order in which they appear.
/// Type one or more property names, separated by commas, or use a hash table to display a calculated property.
/// Wildcards are permitted.</param>
/// <returns><see cref="Table"/> data transfer object that contains header and rows in string.</returns>
/// <remarks>
/// <c>Format-Table</c> Powershell command is used to format the inputs objects as a table.
/// </remarks>
internal static Table FormatTable(IReadOnlyList<PSObject> inputs, bool force, object[] properties = null)
{
if (inputs.Count == 0)
{
return Table.Empty;
}

using var ps = PowerShell.Create(RunspaceMode.CurrentRunspace);
ps.AddCommand("Format-Table");
ps.AddParameter("AutoSize");

if (properties != null)
{
ps.AddParameter("Property", properties);
}

if (force == true)
{
ps.AddParameter("Force");
}

ps.AddParameter("InputObject", inputs);

// Format-Table's output objects are internal to Powershell,
// we cannot use them here, so we need to convert it to a string as workaround.
ps.AddCommand("Out-String");

var results = ps.Invoke();
var text = results.FirstOrDefault()?.BaseObject.ToString();

var lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
.Where(line => string.IsNullOrEmpty(line) == false).ToList();

/*
* Format-Table sometimes outputs a label on the top based on input's data type.
* We need to detect and skip the label and extract only header and rows.
* Strategy is to detect the index of the line under the header with dashes and spaces ('---- -- --- ').
*/

static bool isHeaderLine(string text) => text.Contains('-') && text.All(c => c == '-' || c == ' ');

var headerLineIndex = lines.FindIndex(isHeaderLine);

if (headerLineIndex == -1)
{
// unexpected result, return the whole text
headerLineIndex = 1;
}

return new Table
{
Header = lines.Skip(headerLineIndex - 1).FirstOrDefault(),
HeaderLine = lines.Skip(headerLineIndex).FirstOrDefault(),
Rows = lines.Skip(headerLineIndex + 1)
};
}
}
Loading