diff --git a/.gitignore b/.gitignore
index 80046ed..ba8c984 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,12 @@ bin/
obj/
publish/
*.sln
+*.slnx
+
+# IDE support (keep locally, don't track)
+/.vs/
+/.vscode/
+*.user
+*.suo
+*.sln.DotSettings
+Properties/launchSettings.json
\ No newline at end of file
diff --git a/ConsoleGuiTools.build.ps1 b/ConsoleGuiTools.build.ps1
index 3fa225b..5ba0fa6 100644
--- a/ConsoleGuiTools.build.ps1
+++ b/ConsoleGuiTools.build.ps1
@@ -23,15 +23,18 @@ task Build {
Push-Location src/Microsoft.PowerShell.ConsoleGuiTools
Invoke-BuildExec { & dotnet publish --configuration $Configuration --output publish }
- $Assets = $(
- "./publish/Microsoft.PowerShell.ConsoleGuiTools.dll",
- "./publish/Microsoft.PowerShell.ConsoleGuiTools.psd1",
- "./publish/Microsoft.PowerShell.OutGridView.Models.dll",
- "./publish/Terminal.Gui.dll",
- "./publish/NStack.dll")
- $Assets | ForEach-Object {
- Copy-Item -Force -Path $_ -Destination ../../module
+
+ # Copy all DLLs except PowerShell SDK dependencies (those are provided by PowerShell itself)
+ Get-ChildItem "./publish/*.dll" | Where-Object {
+ $_.Name -notlike "System.Management.Automation.dll" -and
+ $_.Name -notlike "Microsoft.PowerShell.Commands.Diagnostics.dll" -and
+ $_.Name -notlike "Microsoft.Management.Infrastructure.CimCmdlets.dll"
+ } | ForEach-Object {
+ Copy-Item -Force -Path $_.FullName -Destination ../../module
}
+
+ # Copy the module manifest
+ Copy-Item -Force -Path "./publish/Microsoft.PowerShell.ConsoleGuiTools.psd1" -Destination ../../module
Pop-Location
$Assets = $(
diff --git a/Directory.Packages.props b/Directory.Packages.props
index be0590b..c661351 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,8 +1,8 @@
-
-
-
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/global.json b/global.json
index 19e4d7e..a1ce647 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.405",
+ "version": "10.0.101",
"rollForward": "latestFeature",
"allowPrerelease": false
}
diff --git a/nuget.config b/nuget.config
index f003b0f..663bd86 100644
--- a/nuget.config
+++ b/nuget.config
@@ -3,5 +3,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/CachedMemberResult.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/CachedMemberResult.cs
new file mode 100644
index 0000000..14534c5
--- /dev/null
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/CachedMemberResult.cs
@@ -0,0 +1,152 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Represents a cached reflection result for a property or field member, including its value and collection details.
+///
+internal sealed class CachedMemberResult
+{
+ #region Fields
+
+ private readonly string? _representation;
+ private List? _valueAsList;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets or sets the member information (property or field) that was accessed.
+ ///
+ public MemberInfo Member { get; set; }
+
+ ///
+ /// Gets or sets the value retrieved from the member.
+ ///
+ public object? Value { get; set; }
+
+ ///
+ /// Gets or sets the parent object that contains this member.
+ ///
+ public object Parent { get; set; }
+
+ ///
+ /// Gets a value indicating whether this member's value is a collection.
+ ///
+ public bool IsCollection => _valueAsList != null;
+
+ ///
+ /// Gets the collection elements if this member's value is a collection; otherwise, .
+ ///
+ public IReadOnlyCollection? Elements => _valueAsList?.AsReadOnly();
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the class by reflecting on the specified member.
+ ///
+ /// The parent object containing the member.
+ /// The member information to retrieve the value from.
+ public CachedMemberResult(object parent, MemberInfo mem)
+ {
+ Parent = parent;
+ Member = mem;
+
+ try
+ {
+ if (mem is PropertyInfo p)
+ Value = p.GetValue(parent);
+ else if (mem is FieldInfo f)
+ Value = f.GetValue(parent);
+ else
+ throw new NotSupportedException($"Unknown {nameof(MemberInfo)} Type");
+
+ _representation = ValueToString();
+ }
+ catch (Exception)
+ {
+ Value = _representation = "Unavailable";
+ }
+ }
+
+ #endregion
+
+ #region Overrides
+
+ ///
+ /// Returns a string representation of this member in the format "MemberName: value".
+ ///
+ /// A formatted string showing the member name and value.
+ public override string ToString() => Member.Name + ": " + _representation;
+
+ #endregion
+
+ #region Private Methods
+
+ ///
+ /// Converts the member's value to a string representation, detecting collections and formatting them appropriately.
+ ///
+ /// A string representation of the value.
+ private string? ValueToString()
+ {
+ if (Value == null)
+ return "Null";
+
+ try
+ {
+ if (IsCollectionOfKnownTypeAndSize(out var elementType, out var size))
+ return $"{elementType!.Name}[{size}]";
+ }
+ catch (Exception)
+ {
+ return Value?.ToString();
+ }
+
+ return Value?.ToString();
+ }
+
+ ///
+ /// Determines whether the value is a collection of a known type and caches the collection elements.
+ ///
+ /// When this method returns, contains the element type if the value is a homogeneous collection; otherwise, .
+ /// When this method returns, contains the size of the collection if applicable; otherwise, 0.
+ /// if the value is a collection of a single known type; otherwise, .
+ private bool IsCollectionOfKnownTypeAndSize(out Type? elementType, out int size)
+ {
+ elementType = null;
+ size = 0;
+
+ if (Value is null or string)
+ return false;
+
+ if (Value is IEnumerable enumerable)
+ {
+ var list = enumerable.Cast().ToList();
+
+ var types = list.Where(v => v != null).Select(v => v!.GetType()).Distinct().ToArray();
+
+ if (types.Length == 1)
+ {
+ elementType = types[0];
+ size = list.Count;
+
+ _valueAsList = list.Select((e, i) => new CachedMemberResultElement(e, i)).ToList();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/CachedMemberResultElement.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/CachedMemberResultElement.cs
new file mode 100644
index 0000000..7efa001
--- /dev/null
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/CachedMemberResultElement.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Represents an element within a collection member result, providing indexed access to collection items.
+///
+internal sealed class CachedMemberResultElement
+{
+ #region Fields
+
+ ///
+ /// The index of this element within the collection.
+ ///
+ public int Index;
+
+ ///
+ /// The value of this collection element.
+ ///
+ public object? Value;
+
+ private readonly string _representation;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the class with the specified value and index.
+ ///
+ /// The value of the collection element.
+ /// The zero-based index of this element within the collection.
+ public CachedMemberResultElement(object? value, int index)
+ {
+ Index = index;
+ Value = value;
+
+ try
+ {
+ _representation = Value?.ToString() ?? "Null";
+ }
+ catch (Exception)
+ {
+ Value = _representation = "Unavailable";
+ }
+ }
+
+ #endregion
+
+ #region Overrides
+
+ ///
+ /// Returns a string representation of this collection element in the format "[index]: value".
+ ///
+ /// A formatted string showing the index and value.
+ public override string ToString() => $"[{Index}]: {_representation}]";
+
+ #endregion
+}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/ConsoleGui.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/ConsoleGui.cs
deleted file mode 100644
index e4385e1..0000000
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/ConsoleGui.cs
+++ /dev/null
@@ -1,443 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-
-using OutGridView.Models;
-
-using Terminal.Gui;
-
-namespace OutGridView.Cmdlet
-{
- internal sealed class ConsoleGui : IDisposable
- {
- private const string FILTER_LABEL = "Filter";
- // This adjusts the left margin of all controls
- private const int MARGIN_LEFT = 1;
- // Width of Terminal.Gui ListView selection/check UI elements (old == 4, new == 2)
- private const int CHECK_WIDTH = 2;
- private bool _cancelled;
- private Label _filterLabel;
- private TextField _filterField;
- private ListView _listView;
- // _inputSource contains the full set of Input data and tracks any items the user
- // marks. When the cmdlet exits, any marked items are returned. When a filter is
- // active, the list view shows a copy of _inputSource that includes both the items
- // matching the filter AND any items previously marked.
- private GridViewDataSource _inputSource;
-
- // _listViewSource is a filtered copy of _inputSource that ListView.Source is set to.
- // Changes to IsMarked are propogated back to _inputSource.
- private GridViewDataSource _listViewSource;
- private ApplicationData _applicationData;
- private GridViewDetails _gridViewDetails;
-
- public HashSet 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
- {
- // 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
- };
-
- Window win = CreateTopLevelWindow();
-
- // Create the headers and calculate column widths based on the DataTable
- List 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();
-
- if (!_applicationData.MinUI)
- {
- // Add Filter UI
- AddFilter(win);
- // Add Header UI
- AddHeaders(win, gridHeaders);
- }
-
- // Add ListView
- AddListView(win);
-
- // Status bar is where our key-bindings are handled
- 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.
- ApplyFilter();
-
- _listView.SetFocus();
-
- // Run the GUI.
- Application.Run();
- Application.Shutdown();
-
- // Return results of selection if required.
- HashSet selectedIndexes = new HashSet();
- if (_cancelled)
- {
- return selectedIndexes;
- }
-
- // 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();
- int newIndex = 0;
- for (int i = 0; i < _applicationData.DataTable.Data.Count; i++)
- {
- var dataTableRow = _applicationData.DataTable.Data[i];
- var valueList = new List();
- 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);
- }
-
- private void ApplyFilter()
- {
- // The ListView is always filled with a (filtered) copy of _inputSource.
- // We listen for `MarkChanged` events on this filtered list and apply those changes up to _inputSource.
-
- if (_listViewSource != null)
- {
- _listViewSource.MarkChanged -= ListViewSource_MarkChanged;
- _listViewSource = null;
- }
-
- _listViewSource = new GridViewDataSource(GridViewHelpers.FilterData(_inputSource.GridViewRowList, _applicationData.Filter ?? string.Empty));
- _listViewSource.MarkChanged += ListViewSource_MarkChanged;
- _listView.Source = _listViewSource;
- }
-
- private void ListViewSource_MarkChanged(object s, GridViewDataSource.RowMarkedEventArgs a)
- {
- _inputSource.GridViewRowList[a.Row.OriginalIndex].IsMarked = a.Row.IsMarked;
- }
-
- private static void Accept()
- {
- Application.RequestStop();
- }
-
- private void Close()
- {
- _cancelled = true;
- Application.RequestStop();
- }
-
- private Window CreateTopLevelWindow()
- {
- // Creates the top-level window to show
- var win = new Window(_applicationData.Title)
- {
- X = _applicationData.MinUI ? -1 : 0,
- Y = _applicationData.MinUI ? -1 : 0,
-
- // By using Dim.Fill(), it will automatically resize without manual intervention
- Width = Dim.Fill(_applicationData.MinUI ? -1 : 0),
- Height = Dim.Fill(_applicationData.MinUI ? -1 : 1)
- };
-
- if (_applicationData.MinUI)
- {
- win.Border.BorderStyle = BorderStyle.None;
- }
-
- Application.Top.Add(win);
- return win;
- }
-
- private void AddStatusBar(bool visible)
- {
- var statusItems = new List();
- if (_applicationData.OutputMode != OutputModeOption.None)
- {
- // Use Key.Unknown for SPACE with no delegate because ListView already
- // handles SPACE
- statusItems.Add(new StatusItem(Key.Unknown, "~SPACE~ Select Item", null));
- }
-
- if (_applicationData.OutputMode == OutputModeOption.Multiple)
- {
- statusItems.Add(new StatusItem(Key.A | Key.CtrlMask, "~CTRL-A~ Select All", () =>
- {
- // This selects only the items that match the Filter
- var gvds = _listView.Source as GridViewDataSource;
- gvds.GridViewRowList.ForEach(i => i.IsMarked = true);
- _listView.SetNeedsDisplay();
- }));
-
- // Ctrl-D is commonly used in GUIs for select-none
- statusItems.Add(new StatusItem(Key.D | Key.CtrlMask, "~CTRL-D~ Select None", () =>
- {
- // This un-selects only the items that match the Filter
- var gvds = _listView.Source as GridViewDataSource;
- gvds.GridViewRowList.ForEach(i => i.IsMarked = false);
- _listView.SetNeedsDisplay();
- }));
- }
-
- if (_applicationData.OutputMode != OutputModeOption.None)
- {
- statusItems.Add(new StatusItem(Key.Enter, "~ENTER~ Accept", () =>
- {
- if (Application.Top.MostFocused == _listView)
- {
- // If nothing was explicitly marked, we return the item that was selected
- // when ENTER is pressed in Single mode. If something was previously selected
- // (using SPACE) then honor that as the single item to return
- if (_applicationData.OutputMode == OutputModeOption.Single &&
- _inputSource.GridViewRowList.Find(i => i.IsMarked) == null)
- {
- _listView.MarkUnmarkRow();
- }
- Accept();
- }
- else if (Application.Top.MostFocused == _filterField)
- {
- _listView.SetFocus();
- }
- }));
- }
-
- statusItems.Add(new StatusItem(Key.Esc, "~ESC~ Close", () => Close()));
- if (_applicationData.Verbose || _applicationData.Debug)
- {
- statusItems.Add(new StatusItem(Key.Null, $" v{_applicationData.ModuleVersion}", null));
- statusItems.Add(new StatusItem(Key.Null,
- $"{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 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)
- {
- int maxWidth = 0;
- int maxIndex = 0;
- for (int i = 0; i < listViewColumnWidths.Length; i++)
- {
- if (listViewColumnWidths[i] > maxWidth)
- {
- maxWidth = listViewColumnWidths[i];
- maxIndex = i;
- }
- }
-
- listViewColumnWidths[maxIndex]--;
- columnWidthsSum--;
- }
- }
-
- private void AddFilter(Window win)
- {
- _filterLabel = new Label(FILTER_LABEL)
- {
- X = MARGIN_LEFT,
- Y = 0
- };
-
- _filterField = new TextField(_applicationData.Filter ?? string.Empty)
- {
- X = Pos.Right(_filterLabel) + 1,
- Y = Pos.Top(_filterLabel),
- CanFocus = true,
- Width = Dim.Fill() - 1
- };
-
- // TextField captures Ctrl-A (select all text) and Ctrl-D (delete backwards)
- // In OCGV these are used for select-all/none of items. Selecting items is more
- // common than editing the filter field so we turn them off in the filter textview.
- // BACKSPACE still works for delete backwards
- _filterField.ClearKeybinding(Key.A | Key.CtrlMask);
- _filterField.ClearKeybinding(Key.D | Key.CtrlMask);
-
- var filterErrorLabel = new Label(string.Empty)
- {
- X = Pos.Right(_filterLabel) + 1,
- Y = Pos.Top(_filterLabel) + 1,
- ColorScheme = Colors.Base,
- Width = Dim.Fill() - _filterLabel.Text.Length
- };
-
- _filterField.TextChanged += (str) =>
- {
- // str is the OLD value
- string filterText = _filterField.Text?.ToString();
- try
- {
- filterErrorLabel.Text = " ";
- filterErrorLabel.ColorScheme = Colors.Base;
- filterErrorLabel.Redraw(filterErrorLabel.Bounds);
- _applicationData.Filter = filterText;
- ApplyFilter();
-
- }
- catch (Exception ex)
- {
- filterErrorLabel.Text = ex.Message;
- filterErrorLabel.ColorScheme = Colors.Error;
- filterErrorLabel.Redraw(filterErrorLabel.Bounds);
- }
- };
-
- win.Add(_filterLabel, _filterField, filterErrorLabel);
-
- _filterField.Text = _applicationData.Filter ?? string.Empty;
- _filterField.CursorPosition = _filterField.Text.Length;
- }
-
- private void AddHeaders(Window win, List gridHeaders)
- {
- var header = new Label(GridViewHelpers.GetPaddedString(
- gridHeaders,
- _gridViewDetails.ListViewOffset,
- _gridViewDetails.ListViewColumnWidths));
- header.X = 0;
- if (_applicationData.MinUI)
- {
- header.Y = 0;
- }
- else
- {
- header.Y = 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 (c.Equals(' '))
- {
- headerLineText.Append(' ');
- }
- else
- {
- // When gui.cs supports text decorations, should replace this with just underlining the header
- headerLineText.Append('-');
- }
- }
-
- if (!_applicationData.MinUI)
- {
- var headerLine = new Label(headerLineText.ToString())
- {
- X = 0,
- Y = Pos.Bottom(header)
- };
- win.Add(headerLine);
- }
- }
-
- private void AddListView(Window win)
- {
- _listView = new ListView(_inputSource);
- _listView.X = MARGIN_LEFT;
- if (!_applicationData.MinUI)
- {
- _listView.Y = Pos.Bottom(_filterLabel) + 3; // 1 for space, 1 for header, 1 for header underline
- }
- else
- {
- _listView.Y = 1; // 1 for space, 1 for header, 1 for header underline
- }
- _listView.Width = Dim.Fill(1);
- _listView.Height = Dim.Fill();
- _listView.AllowsMarking = _applicationData.OutputMode != OutputModeOption.None;
- _listView.AllowsMultipleSelection = _applicationData.OutputMode == OutputModeOption.Multiple;
- _listView.AddKeyBinding(Key.Space, Command.ToggleChecked, Command.LineDown);
-
- win.Add(_listView);
- }
-
- public void Dispose()
- {
- if (!Console.IsInputRedirected)
- {
- // By emitting this, we fix two issues:
- // 1. An issue where arrow keys don't work in the console because .NET
- // requires application mode to support Arrow key escape sequences.
- // Esc[?1h sets the cursor key to application mode
- // See http://ascii-table.com/ansi-escape-sequences-vt-100.php
- // 2. An issue where moving the mouse causes characters to show up because
- // mouse tracking is still on. Esc[?1003l turns it off.
- // See https://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
- Console.Write("\u001b[?1h\u001b[?1003l");
- }
- }
- }
-}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDataSource.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDataSource.cs
index 4d6724a..7a6d17a 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDataSource.cs
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDataSource.cs
@@ -4,80 +4,124 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using Terminal.Gui.App;
+using Terminal.Gui.Views;
-using NStack;
+namespace Microsoft.PowerShell.ConsoleGuiTools;
-using Terminal.Gui;
-
-namespace OutGridView.Cmdlet
+///
+/// Provides a data source implementation for the grid view that manages rows and supports marking and rendering.
+///
+internal sealed class GridViewDataSource : IListDataSource
{
- internal sealed class GridViewDataSource : IListDataSource
+ ///
+ /// Gets or sets the list of rows displayed in the grid view.
+ ///
+ public List GridViewRowList { get; set; }
+
+ ///
+ public int Count => GridViewRowList.Count;
+
+ ///
+ public int MaxItemLength { get; set; }
+
+ ///
+ public bool SuspendCollectionChangedEvent { get; set; }
+
+#pragma warning disable CS0067
+ ///
+ public event NotifyCollectionChangedEventHandler? CollectionChanged;
+#pragma warning restore CS0067
+
+ ///
+ /// Initializes a new instance of the class with the specified item list.
+ ///
+ /// The list of grid view rows to display.
+ public GridViewDataSource(List itemList)
{
- public List GridViewRowList { get; set; }
+ GridViewRowList = itemList;
+ }
- public int Count => GridViewRowList.Count;
+ ///
+ public void Render(ListView listView, bool selected, int item, int col, int line, int width, int viewportX)
+ {
+ GridViewRow? row = GridViewRowList[item];
+ string displayString = row?.DisplayString ?? string.Empty;
- public GridViewDataSource(List itemList)
+ displayString = viewportX switch
{
- GridViewRowList = itemList;
- }
-
- public int Length { get; }
-
- public void Render(ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start)
+ // Truncate the start of the string to skip characters scrolled out of view
+ > 0 when displayString.Length > viewportX => displayString[viewportX..],
+ > 0 when displayString.Length <= viewportX => string.Empty,
+ _ => displayString
+ };
+
+ // Pad right of display string with spaces to fill width, or truncate if too long
+ if (displayString.Length < width)
{
- container.Move(col, line);
- RenderUstr(driver, GridViewRowList[item].DisplayString, col, line, width);
+ displayString = displayString.PadRight(width);
}
-
- public bool IsMarked(int item) => GridViewRowList[item].IsMarked;
-
- public void SetMark(int item, bool value)
+ else if (displayString.Length > width)
{
- var oldValue = GridViewRowList[item].IsMarked;
- GridViewRowList[item].IsMarked = value;
- var args = new RowMarkedEventArgs()
- {
- Row = GridViewRowList[item],
- OldValue = oldValue
- };
- MarkChanged?.Invoke(this, args);
+ displayString = displayString[..width];
}
- public sealed class RowMarkedEventArgs : EventArgs
- {
- public GridViewRow Row { get; set; }
- public bool OldValue { get; set; }
-
- }
+ listView.AddStr(displayString);
+ }
- public event EventHandler MarkChanged;
+ ///
+ public bool IsMarked(int item)
+ {
+ return item < GridViewRowList.Count && GridViewRowList[item].IsMarked;
+ }
- public IList ToList()
+ ///
+ public void SetMark(int item, bool value)
+ {
+ var oldValue = GridViewRowList[item].IsMarked;
+ GridViewRowList[item].IsMarked = value;
+ var args = new RowMarkedEventArgs
{
- return GridViewRowList;
- }
+ Row = GridViewRowList[item],
+ OldValue = oldValue
+ };
+ MarkChanged?.Invoke(this, args);
+ }
- // A slightly adapted method from gui.cs: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
- private static void RenderUstr(ConsoleDriver driver, ustring ustr, int col, int line, int width)
- {
- int used = 0;
- int index = 0;
- while (index < ustr.Length)
- {
- (var rune, var size) = Utf8.DecodeRune(ustr, index, index - ustr.Length);
- var count = Rune.ColumnWidth(rune);
- if (used + count > width) break;
- driver.AddRune(rune);
- used += count;
- index += size;
- }
-
- while (used < width)
- {
- driver.AddRune(' ');
- used++;
- }
- }
+ ///
+ /// Provides data for the event.
+ ///
+ public sealed class RowMarkedEventArgs : EventArgs
+ {
+ ///
+ /// Gets or sets the row that was marked or unmarked.
+ ///
+ public required GridViewRow Row { get; set; }
+
+ ///
+ /// Gets or sets the previous marked state of the row.
+ ///
+ public bool OldValue { get; set; }
+ }
+
+ ///
+ /// Occurs when a row's marked state changes.
+ ///
+ public event EventHandler? MarkChanged;
+
+ ///
+ /// Converts the data source to a list.
+ ///
+ /// The grid view row list as an .
+ public IList ToList() => GridViewRowList;
+
+ ///
+ /// Releases all resources used by the .
+ ///
+ public void Dispose()
+ {
+ // No resources to dispose currently
}
}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDetails.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDetails.cs
deleted file mode 100644
index 7c660f9..0000000
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewDetails.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-namespace OutGridView.Cmdlet
-{
- internal sealed class GridViewDetails
- {
- // Contains the width of each column in the grid view.
- public int[] ListViewColumnWidths { get; set; }
-
- // Dictates where the header should actually start considering
- // some offset is needed to factor in the checkboxes
- public int ListViewOffset { get; set; }
-
- // The width that is actually useable on the screen after
- // subtracting space needed for a clean UI (spaces between columns, etc).
- public int UsableWidth { get; set; }
- }
-}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewHelpers.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewHelpers.cs
index 482fe1d..44ee449 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewHelpers.cs
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewHelpers.cs
@@ -1,39 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
-namespace OutGridView.Cmdlet
+namespace Microsoft.PowerShell.ConsoleGuiTools
{
+ ///
+ /// Provides helper methods for filtering and formatting data in the grid view.
+ ///
internal sealed class GridViewHelpers
{
- // Add all items already selected plus any that match the filter
- // The selected items should be at the top of the list, in their original order
+ ///
+ /// Filters a list of grid view rows based on a regular expression pattern.
+ /// Marked items are always included and appear first in the result, followed by unmarked items that match the filter.
+ ///
+ /// The list of rows to filter.
+ /// The regular expression pattern to match against the display string. If null or empty, the original list is returned.
+ /// A filtered list with marked items first, followed by matching unmarked items.
public static List FilterData(List listToFilter, string filter)
{
- var filteredList = new List();
if (string.IsNullOrEmpty(filter))
{
return listToFilter;
}
+ var filteredList = new List();
filteredList.AddRange(listToFilter.Where(gvr => gvr.IsMarked));
- filteredList.AddRange(listToFilter.Where(gvr => !gvr.IsMarked && Regex.IsMatch(gvr.DisplayString, filter, RegexOptions.IgnoreCase)));
+ filteredList.AddRange(listToFilter.Where(gvr => !gvr.IsMarked && Regex.IsMatch(gvr.DisplayString!, filter, RegexOptions.IgnoreCase)));
return filteredList;
}
- public static string GetPaddedString(List strings, int offset, int[] listViewColumnWidths)
+ public static string GetPaddedString(List? strings, int offset, int[]? listViewColumnWidths)
{
+ if (listViewColumnWidths is null)
+ {
+ return string.Empty;
+ }
+
var builder = new StringBuilder();
if (offset > 0)
{
builder.Append(string.Empty.PadRight(offset));
}
+ if (strings == null) return builder.ToString();
for (int i = 0; i < strings.Count; i++)
{
if (i > 0)
@@ -44,12 +59,23 @@ public static string GetPaddedString(List strings, int offset, int[] lis
// Replace any newlines with encoded newline/linefeed (`n or `r)
// Note we can't use Environment.Newline because we don't know that the
- // Command honors that.
+ // command honors that.
strings[i] = strings[i].Replace("\r", "`r");
strings[i] = strings[i].Replace("\n", "`n");
- // If the string won't fit in the column, append an ellipsis.
- if (strings[i].Length > listViewColumnWidths[i])
+ // If the string doesn't fit in the column, append an ellipsis.
+ // Guard against negative or very small column widths
+ if (listViewColumnWidths[i] <= 0)
+ {
+ // Skip columns with zero or negative width (but separator already added above)
+ }
+ else if (listViewColumnWidths[i] < 4)
+ {
+ // For very small columns (< 4), just truncate without ellipsis
+ var truncateLength = Math.Min(strings[i].Length, listViewColumnWidths[i]);
+ builder.Append(strings[i], 0, truncateLength);
+ }
+ else if (strings[i].Length > listViewColumnWidths[i])
{
builder.Append(strings[i], 0, listViewColumnWidths[i] - 3);
builder.Append("...");
@@ -63,4 +89,4 @@ public static string GetPaddedString(List strings, int offset, int[] lis
return builder.ToString();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewRow.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewRow.cs
index 2ecc80e..6db806c 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewRow.cs
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/GridViewRow.cs
@@ -1,13 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-namespace OutGridView.Cmdlet
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Represents a single row in the grid view, including its display text, marked state, and original position.
+///
+public class GridViewRow
{
- public class GridViewRow
- {
- public string DisplayString { get; set; }
- public bool IsMarked { get; set; }
- public int OriginalIndex { get; set; }
- public override string ToString() => DisplayString;
- }
-}
+ ///
+ /// Gets or sets the formatted string to display for this row in the grid view.
+ ///
+ public string? DisplayString { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this row is marked (selected) by the user.
+ ///
+ public bool IsMarked { get; set; }
+
+ ///
+ /// Gets or sets the original index of this row in the source data before any filtering or sorting.
+ ///
+ public int OriginalIndex { get; set; }
+
+ ///
+ /// Returns the display string representation of this row.
+ ///
+ /// The value.
+ public override string ToString() => DisplayString ?? string.Empty;
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/Header.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/Header.cs
new file mode 100644
index 0000000..900b263
--- /dev/null
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/Header.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// A specialized view for displaying grid column headers with individual subviews for each column. This is added to
+/// the top of the Padding of the ListView.
+///
+internal sealed class Header : View
+{
+ public Header()
+ {
+ Height = 1;
+ CanFocus = false;
+ Width = Dim.Fill();
+ }
+
+ public override void EndInit()
+ {
+ base.EndInit();
+
+
+ // We are a subview of the ListView.Padding.
+ if (SuperView is Padding padding) padding.Parent?.ViewportChanged += ListViewOnViewportChanged;
+
+ return;
+
+ void ListViewOnViewportChanged(object? sender, DrawEventArgs e)
+ {
+ if (sender is ListView listView)
+ Viewport = Viewport with { X = listView.Viewport.X };
+ }
+ }
+
+ protected override void OnSubViewLayout(LayoutEventArgs args)
+ {
+ if (SuperView is Padding { Parent: ListView listView })
+ SetContentSize(GetContentSize() with { Width = listView.GetContentSize().Width });
+
+ base.OnSubViewLayout(args);
+ }
+
+ ///
+ /// Updates the header with new column strings and widths.
+ ///
+ /// The list of header strings to display.
+ /// The width of each column.
+ public void SetHeaders(List? headers, int[]? columnWidths)
+ {
+ if (headers == null || columnWidths == null)
+ return;
+
+ // Clear existing labels
+ RemoveAll();
+
+ // Create a label for each header
+ var currentX = 0;
+ for (var i = 0; i < headers.Count; i++)
+ {
+ // Skip columns with zero width
+ if (columnWidths[i] <= 0)
+ continue;
+
+ var column = new View
+ {
+ Text = headers[i],
+ X = currentX,
+ Y = 0,
+ Width = Dim.Auto(DimAutoStyle.Text),
+ Height = 1,
+ TextAlignment = Alignment.Start,
+ VerticalTextAlignment = Alignment.Start
+ };
+ column.GettingAttributeForRole += ColumnOnGettingAttributeForRole;
+
+ void ColumnOnGettingAttributeForRole(object? sender, VisualRoleEventArgs e)
+ {
+ if (e.Role == VisualRole.Normal)
+ {
+ e.Result = e.Result!.Value with { Style = TextStyle.Bold | TextStyle.Underline };
+ e.Handled = true;
+ }
+ }
+
+ Add(column);
+
+ // Move to next column position (width + 1 space separator)
+ currentX += columnWidths[i] + 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.csproj b/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.csproj
index ed25247..8ab49b7 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.csproj
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.csproj
@@ -1,7 +1,9 @@
-
+
- net8.0
+ net10.0
+ preview
+ enable
@@ -11,17 +13,16 @@
- Add ';https://api.nuget.org/v3/index.json' to the end of the RestoreSources property group below
- Uncomment the RestoreSources property group below
-->
-
+
-
-
+
-
+
@@ -33,4 +34,8 @@
true
Recommended
+
+
+ true
+
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.psd1 b/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.psd1
index 80c0fe8..4454fc5 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.psd1
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.psd1
@@ -9,7 +9,7 @@
RootModule = 'Microsoft.PowerShell.ConsoleGuiTools.dll'
# Version number of this module.
-ModuleVersion = '0.7.7'
+ModuleVersion = '0.9.0'
# Supported PSEditions
CompatiblePSEditions = @( 'Core' )
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/OutConsoleGridView.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/OutConsoleGridView.cs
new file mode 100644
index 0000000..6608b78
--- /dev/null
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/OutConsoleGridView.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using Microsoft.PowerShell.OutGridView.Models;
+using Terminal.Gui.App;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Provides the main orchestration for the Out-ConsoleGridView cmdlet, managing the Terminal.Gui application lifecycle
+/// and coordinating between the application data and the grid view window.
+///
+///
+/// This class serves as a facade that initializes the Terminal.Gui framework, creates and runs the grid view window,
+/// and handles cleanup operations. It delegates the actual UI rendering and user interaction to the
+/// class.
+///
+internal sealed class OutConsoleGridView : IDisposable
+{
+ private ApplicationData? _applicationData;
+
+ ///
+ /// Runs the grid view Terminal.Gui Application with the specified configuration and returns the indexes of selected items.
+ ///
+ ///
+ /// The application configuration containing the data table, output mode, filter settings, and other display options.
+ ///
+ ///
+ /// A containing the zero-based indexes of items selected by the user.
+ /// Returns an empty set if the user cancels the operation or if no items were selected.
+ ///
+ public HashSet Run(ApplicationData applicationData)
+ {
+ _applicationData = applicationData;
+
+ Terminal.Gui.Configuration.ConfigurationManager.Enable(Terminal.Gui.Configuration.ConfigLocations.All);
+
+ using OutGridViewWindow window = new(_applicationData);
+ using IApplication app = Application.Create().Init(driverName: _applicationData.ForceDriver);
+ HashSet? selectedIndexes = app.Run(window) as HashSet;
+ return selectedIndexes ?? [];
+ }
+
+ public void Dispose()
+ {
+ if (!Console.IsInputRedirected)
+ // By emitting this, we fix two issues:
+ // 1. An issue where arrow keys don't work in the console because .NET
+ // requires application mode to support Arrow key escape sequences.
+ // Esc[?1h sets the cursor key to application mode
+ // See http://ascii-table.com/ansi-escape-sequences-vt-100.php
+ // 2. An issue where moving the mouse causes characters to show up because
+ // mouse tracking is still on. Esc[?1003l turns it off.
+ // See https://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
+ Console.Write("\u001b[?1h\u001b[?1003l");
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/OutConsoleGridviewCmdletCommand.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/OutConsoleGridviewCmdletCommand.cs
index 94bb7e1..6481dc2 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/OutConsoleGridviewCmdletCommand.cs
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/OutConsoleGridviewCmdletCommand.cs
@@ -4,184 +4,193 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Internal;
+using Microsoft.PowerShell.OutGridView.Models;
-using OutGridView.Models;
+namespace Microsoft.PowerShell.ConsoleGuiTools;
-namespace OutGridView.Cmdlet
+///
+/// Sends output to an interactive table in a separate console window. This class is invoked by PowerShell when the
+/// Out-ConsoleGridView cmdlet is called.
+///
+[Cmdlet(VerbsData.Out, "ConsoleGridView")]
+[Alias("ocgv")]
+public class OutConsoleGridViewCmdletCommand : PSCmdlet, IDisposable
{
- [Cmdlet(VerbsData.Out, "ConsoleGridView")]
- [Alias("ocgv")]
- public class OutConsoleGridViewCmdletCommand : PSCmdlet, IDisposable
+ #region Properties
+
+ private const string DATA_NOT_QUALIFIED_FOR_GRID_VIEW = nameof(DATA_NOT_QUALIFIED_FOR_GRID_VIEW);
+ private const string ENVIRONMENT_NOT_SUPPORTED_FOR_GRID_VIEW = nameof(ENVIRONMENT_NOT_SUPPORTED_FOR_GRID_VIEW);
+
+ private readonly List _psObjects = [];
+ private readonly OutConsoleGridView _outConsoleGridView = new();
+
+ #endregion Properties
+
+ #region Input Parameters
+
+ ///
+ /// Gets or sets the current pipeline object.
+ ///
+ [Parameter(ValueFromPipeline = true, HelpMessage = "Specifies the input pipeline object")]
+ public PSObject InputObject { get; set; } = AutomationNull.Value;
+
+ ///
+ /// Gets or sets the title of the Out-ConsoleGridView window.
+ ///
+ [Parameter(HelpMessage =
+ "Specifies the text that appears in the title bar of the Out-ConsoleGridView window. By default, the title bar displays the command that invokes Out-ConsoleGridView.")]
+ [ValidateNotNullOrEmpty]
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the selected items should be written to the pipeline
+ /// and if it should be possible to select multiple or single list items.
+ ///
+ [Parameter(HelpMessage =
+ "Determines whether a single item (Single), multiple items (Multiple; default), or no items (None) will be written to the pipeline. Also determines selection behavior in the TUI.")]
+ public OutputModeOption OutputMode { set; get; } = OutputModeOption.Multiple;
+
+ ///
+ /// Gets or sets the initial value for the filter in the TUI.
+ ///
+ [Parameter(HelpMessage =
+ "Pre-populates the Filter edit box, allowing filtering to be specified on the command line. The filter uses regular expressions.")]
+ public string? Filter { set; get; }
+
+ ///
+ /// Gets or sets a value indicating whether "minimum UI" mode will be enabled.
+ ///
+ [Parameter(HelpMessage = "If specified no window frame, filter box, or status bar will be displayed in the TUI.")]
+ public SwitchParameter MinUI { set; get; }
+
+ ///
+ /// Gets or sets the Terminal.Gui driver to use.
+ ///
+ [Parameter(HelpMessage =
+ "Forces the Terminal.Gui driver to use. Valid values are 'ansi', 'windows', or 'unix'.")]
+ public string? ForceDriver { set; get; }
+
+ ///
+ /// Gets or sets a value indicating whether all properties should be displayed instead of just the default display properties.
+ ///
+ [Parameter(HelpMessage =
+ "If specified, all properties of the objects will be displayed instead of just the default display properties.")]
+ public SwitchParameter AllProperties { set; get; }
+
+ ///
+ /// Gets a value indicating whether the Verbose switch is present.
+ ///
+ public bool Verbose => MyInvocation.BoundParameters.ContainsKey("Verbose");
+
+ ///
+ /// Gets a value indicating whether the Debug switch is present.
+ ///
+ public bool Debug => MyInvocation.BoundParameters.ContainsKey("Debug");
+
+ #endregion Input Parameters
+
+ ///
+ /// Performs initialization of command execution. Validates that the environment supports grid view.
+ ///
+ protected override void BeginProcessing()
{
- #region Properties
-
- private const string DataNotQualifiedForGridView = nameof(DataNotQualifiedForGridView);
- private const string EnvironmentNotSupportedForGridView = nameof(EnvironmentNotSupportedForGridView);
-
- private List _psObjects = new List();
- private ConsoleGui _consoleGui = new ConsoleGui();
-
- #endregion Properties
-
- #region Input Parameters
-
- ///
- /// This parameter specifies the current pipeline object.
- ///
- [Parameter(ValueFromPipeline = true, HelpMessage = "Specifies the input pipeline object")]
- public PSObject InputObject { get; set; } = AutomationNull.Value;
-
- ///
- /// Gets/sets the title of the Out-GridView window.
- ///
- [Parameter(HelpMessage = "Specifies the text that appears in the title bar of the Out-ConsoleGridView window. y default, the title bar displays the command that invokes Out-ConsoleGridView.")]
- [ValidateNotNullOrEmpty]
- public string Title { get; set; }
-
- ///
- /// Get or sets a value indicating whether the selected items should be written to the pipeline
- /// and if it should be possible to select multiple or single list items.
- ///
- [Parameter(HelpMessage = "Determines whether a single item (Single), multiple items (Multiple; default), or no items (None) will be written to the pipeline. Also determines selection behavior in the GUI.")]
- public OutputModeOption OutputMode { set; get; } = OutputModeOption.Multiple;
-
- ///
- /// gets or sets the initial value for the filter in the GUI
- ///
- [Parameter(HelpMessage = "Pre-populates the Filter edit box, allowing filtering to be specified on the command line. The filter uses regular expressions.")]
- public string Filter { set; get; }
-
- ///
- /// gets or sets the whether "minimum UI" mode will be enabled
- ///
- [Parameter(HelpMessage = "If specified no window frame, filter box, or status bar will be displayed in the GUI.")]
- public SwitchParameter MinUI { set; get; }
-
- ///
- /// gets or sets the whether the Terminal.Gui System.Net.Console-based ConsoleDriver will be used instead of the
- /// default platform-specific (Windows or Curses) ConsoleDriver.
- ///
- [Parameter(HelpMessage = "If specified the Terminal.Gui System.Net.Console-based ConsoleDriver (NetDriver) will be used.")]
- public SwitchParameter UseNetDriver { set; get; }
-
- ///
- /// For the -Verbose switch
- ///
- public bool Verbose => MyInvocation.BoundParameters.TryGetValue("Verbose", out var o);
-
- ///
- /// For the -Debug switch
- ///
- public bool Debug => MyInvocation.BoundParameters.TryGetValue("Debug", out var o);
-
- #endregion Input Parameters
-
- // This method gets called once for each cmdlet in the pipeline when the pipeline starts executing
- protected override void BeginProcessing()
+ if (Console.IsInputRedirected)
{
- if (Console.IsInputRedirected)
- {
- ErrorRecord error = new ErrorRecord(
- new PSNotSupportedException("Not supported in this environment (when input is redirected)."),
- EnvironmentNotSupportedForGridView,
- ErrorCategory.NotImplemented,
- null);
-
- ThrowTerminatingError(error);
- }
- }
+ var error = new ErrorRecord(
+ new PSNotSupportedException("Not supported in this environment (when input is redirected)."),
+ ENVIRONMENT_NOT_SUPPORTED_FOR_GRID_VIEW,
+ ErrorCategory.NotImplemented,
+ null);
- // This method will be called for each input received from the pipeline to this cmdlet; if no input is received, this method is not called
- protected override void ProcessRecord()
- {
- if (InputObject == null || InputObject == AutomationNull.Value)
- {
- return;
- }
-
- if (InputObject.BaseObject is IDictionary dictionary)
- {
- // Dictionaries should be enumerated through because the pipeline does not enumerate through them.
- foreach (DictionaryEntry entry in dictionary)
- {
- ProcessObject(PSObject.AsPSObject(entry));
- }
- }
- else
- {
- ProcessObject(InputObject);
- }
+ ThrowTerminatingError(error);
}
+ }
- private void ProcessObject(PSObject input)
+ ///
+ /// Processes each input object received from the pipeline.
+ ///
+ protected override void ProcessRecord()
+ {
+ if (Equals(InputObject, AutomationNull.Value)) return;
+
+ if (InputObject.BaseObject is IDictionary dictionary)
+ // Dictionaries should be enumerated through because the pipeline does not enumerate through them.
+ foreach (DictionaryEntry entry in dictionary)
+ ProcessObject(PSObject.AsPSObject(entry));
+ else
+ ProcessObject(InputObject);
+ }
+
+ ///
+ /// Processes a single object for display in the grid view.
+ ///
+ /// The PSObject to process.
+ /// Thrown when the data type is not supported for Out-ConsoleGridView.
+ private void ProcessObject(PSObject input)
+ {
+ var baseObject = input.BaseObject;
+
+ // Throw a terminating error for types that are not supported.
+ if (baseObject is ScriptBlock ||
+ baseObject is SwitchParameter ||
+ baseObject is PSReference ||
+ baseObject is PSObject)
{
+ var error = new ErrorRecord(
+ new FormatException("Invalid data type for Out-ConsoleGridView"),
+ DATA_NOT_QUALIFIED_FOR_GRID_VIEW,
+ ErrorCategory.InvalidType,
+ null);
- object baseObject = input.BaseObject;
+ ThrowTerminatingError(error);
+ }
- // Throw a terminating error for types that are not supported.
- if (baseObject is ScriptBlock ||
- baseObject is SwitchParameter ||
- baseObject is PSReference ||
- baseObject is PSObject)
- {
- ErrorRecord error = new ErrorRecord(
- new FormatException("Invalid data type for Out-ConsoleGridView"),
- DataNotQualifiedForGridView,
- ErrorCategory.InvalidType,
- null);
+ _psObjects.Add(input);
+ }
- ThrowTerminatingError(error);
- }
+ ///
+ /// Performs final processing after all pipeline objects have been received.
+ /// Displays the console grid view and writes selected objects to the pipeline.
+ ///
+ protected override void EndProcessing()
+ {
+ base.EndProcessing();
- _psObjects.Add(input);
- }
+ // Return if no objects
+ if (_psObjects.Count == 0) return;
- // This method will be called once at the end of pipeline execution; if no input is received, this method is not called
- protected override void EndProcessing()
+ var applicationData = new ApplicationData
{
- base.EndProcessing();
-
- //Return if no objects
- if (_psObjects.Count == 0)
- {
- return;
- }
-
- var TG = new TypeGetter(this);
-
- var dataTable = TG.CastObjectsToTableView(_psObjects);
- var applicationData = new ApplicationData
- {
- Title = Title ?? "Out-ConsoleGridView",
- OutputMode = OutputMode,
- Filter = Filter,
- MinUI = MinUI,
- DataTable = dataTable,
- UseNetDriver = UseNetDriver,
- Verbose = Verbose,
- Debug = Debug,
- ModuleVersion = MyInvocation.MyCommand.Version.ToString()
- };
-
-
- var selectedIndexes = _consoleGui.Start(applicationData);
- foreach (int idx in selectedIndexes)
- {
- var selectedObject = _psObjects[idx];
- if (selectedObject == null)
- {
- continue;
- }
- WriteObject(selectedObject, false);
- }
- }
-
- public void Dispose()
+ PSObjects = _psObjects.Cast().ToList(),
+ Title = Title ?? "Out-ConsoleGridView",
+ OutputMode = OutputMode,
+ Filter = Filter,
+ MinUI = MinUI,
+ ForceDriver = ForceDriver,
+ AllProperties = AllProperties,
+ Verbose = Verbose,
+ Debug = Debug,
+ ModuleVersion = MyInvocation.MyCommand.Version.ToString()
+ };
+
+ HashSet selectedIndexes = _outConsoleGridView.Run(applicationData);
+ foreach (var idx in selectedIndexes)
{
- _consoleGui.Dispose();
- GC.SuppressFinalize(this);
+ var selectedObject = _psObjects[idx];
+
+ WriteObject(selectedObject, false);
}
}
-}
+
+ ///
+ /// Releases all resources used by the .
+ ///
+ public void Dispose()
+ {
+ _outConsoleGridView.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/OutGridViewWindow.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/OutGridViewWindow.cs
new file mode 100644
index 0000000..4ce30ff
--- /dev/null
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/OutGridViewWindow.cs
@@ -0,0 +1,491 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Management.Automation;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using Microsoft.PowerShell.OutGridView.Models;
+using Terminal.Gui.App;
+using Terminal.Gui.Configuration;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.Input;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Provides the Terminal.Gui Window implementation for displaying tabular data with filtering and selection
+/// capabilities.
+///
+internal sealed class OutGridViewWindow : Runnable>
+{
+ private const string FILTER_LABEL = "_Filter:";
+ private const int MARGIN_LEFT = 0;
+ private const int CHECK_WIDTH = 2;
+ private readonly ApplicationData _applicationData;
+ private DataTable? _dataTable;
+ private View? _filterErrorView;
+ private TextField? _filterField;
+
+ private Label? _filterLabel;
+ private Header? _header;
+ private GridViewDataSource? _inputSource;
+ private ListView? _listView;
+ private GridViewDataSource? _filteredSource;
+ private int[]? _naturalColumnWidths;
+ private StatusBar? _statusBar;
+
+ ///
+ /// Initializes a new instance of the class with the specified application data.
+ ///
+ /// The configuration and data to display in the grid view.
+ public OutGridViewWindow(ApplicationData applicationData)
+ {
+ _applicationData = applicationData;
+ Title = _applicationData.Title ?? "Out-ConsoleGridView";
+ SchemeName = SchemeManager.SchemesToSchemeName(Schemes.Base);
+ BorderStyle = Window.DefaultBorderStyle;
+
+ switch (_applicationData.MinUI)
+ {
+ case true:
+ BorderStyle = LineStyle.None;
+ break;
+ case false:
+ AddFilter();
+ break;
+ }
+
+ // Convert PSObjects to DataTable using TypeGetter which handles format data properly
+ if (_applicationData.PSObjects is { Count: > 0 })
+ {
+ var psObjects = _applicationData.PSObjects.Cast().ToList();
+ _dataTable = TypeGetter.CastObjectsToTableView(psObjects, _applicationData.AllProperties);
+ }
+ else
+ {
+ _dataTable = new DataTable([], []);
+ }
+
+ // Copy the input DataTable into our master source list
+ _inputSource = LoadData();
+
+ AddListView();
+ ApplyFilter();
+ }
+
+ protected override void OnIsRunningChanged(bool newIsRunning)
+ {
+ base.OnIsRunningChanged(newIsRunning);
+ if (!newIsRunning) return;
+
+ // We do this here, because _statusBar requires the Application to be running to
+ // access the driver information.
+ AddStatusBar();
+ _listView?.SetFocus();
+ }
+
+ ///
+ /// Gets the original indexes of all marked rows.
+ ///
+ /// A set of zero-based indexes from the original data table.
+ public HashSet GetSelectedIndexes()
+ {
+ if (_inputSource == null) return [];
+
+ var selectedIndexes = new HashSet();
+
+ foreach (var gvr in _inputSource.GridViewRowList.Where(gvr => gvr.IsMarked))
+ selectedIndexes.Add(gvr.OriginalIndex);
+
+ return selectedIndexes;
+ }
+
+ #region Data Management
+
+ ///
+ /// Loads data from the application data table into grid view rows.
+ ///
+ /// A data source containing the loaded rows.
+ private GridViewDataSource LoadData()
+ {
+ var items = new List();
+ if (_dataTable?.Data.Count == 0)
+ return new GridViewDataSource(items);
+
+ // Calculate and cache natural column widths
+ _naturalColumnWidths = CalculateNaturalColumnWidths(_dataTable?.DataColumns.Select(c => c.Label).ToList());
+
+ for (var i = 0; i < _dataTable?.Data.Count; i++)
+ {
+ var dataTableRow = _dataTable.Data[i];
+ var valueList = new List();
+ foreach (var dataTableColumn in _dataTable.DataColumns)
+ {
+ var columnKey = dataTableColumn.ToString();
+
+ // Check if the key exists in the dictionary
+ valueList.Add(dataTableRow.Values.TryGetValue(columnKey, out var value)
+ ? value.DisplayValue
+ // Key not found - this means the dictionary was populated with different keys
+ // This is a bug - let's add empty string for now to avoid crash
+ : string.Empty);
+ }
+
+ items.Add(new GridViewRow
+ {
+ DisplayString = GridViewHelpers.GetPaddedString(valueList, 0, _naturalColumnWidths),
+ OriginalIndex = i
+ });
+ }
+
+ // Anytime we load, we need to update the headers.
+ // Note, if ListView had a SourceChanged event, this could be done more automatically.
+ _header?.SetHeaders(_dataTable?.DataColumns.Select(c => c.Label).ToList(), _naturalColumnWidths);
+
+ var source = new GridViewDataSource(items);
+ source.MaxItemLength = _naturalColumnWidths!.Sum() + (_naturalColumnWidths!.Length - 1);
+ return source;
+ }
+
+ #endregion
+
+ #region Filtering
+
+ ///
+ /// Applies the current filter to the input data and updates the list view with matching rows.
+ ///
+ private void ApplyFilter()
+ {
+ GridViewRow? selectedItem = null;
+
+ if (_filteredSource != null)
+ {
+ selectedItem = _filteredSource.GridViewRowList.ElementAtOrDefault(_listView?.SelectedItem ?? 0);
+ _filteredSource.MarkChanged -= OnFilteredSourceMarkChanged;
+ _filteredSource = null;
+ }
+
+ // TODO: this is probably not needed; it is here defensively
+ _inputSource ??= LoadData();
+
+ try
+ {
+ _filteredSource = new GridViewDataSource(GridViewHelpers.FilterData(_inputSource.GridViewRowList,
+ _applicationData.Filter ?? string.Empty));
+ _filteredSource.MaxItemLength = _inputSource.MaxItemLength;
+ }
+ catch (RegexParseException ex)
+ {
+ _filterErrorView?.Text = ex.Message;
+ }
+
+ _filteredSource?.MarkChanged += OnFilteredSourceMarkChanged;
+
+ _listView?.Source = _filteredSource;
+
+ if (selectedItem is { } && _filteredSource != null)
+ {
+ var newIndex =
+ _filteredSource.GridViewRowList.FindIndex(i => i.OriginalIndex == selectedItem.OriginalIndex);
+ if (newIndex >= 0 && _listView != null)
+ _listView.SelectedItem = newIndex;
+ }
+
+ if (_listView?.SelectedItem == null && _listView is { Source.Count: > 0 })
+ _listView.SelectedItem = 0;
+ }
+
+ ///
+ /// Handles mark changed events from the filtered list view and propagates changes to the input source.
+ ///
+ /// The event sender.
+ /// The event arguments containing the row that was marked or unmarked.
+ private void OnFilteredSourceMarkChanged(object? s, GridViewDataSource.RowMarkedEventArgs a)
+ {
+ _inputSource?.GridViewRowList[a.Row.OriginalIndex].IsMarked = a.Row.IsMarked;
+ }
+
+ #endregion
+
+ #region User Actions
+
+ ///
+ /// Reloads the data with the specified AllProperties setting.
+ ///
+ private void ReloadDataWithAllProperties(bool allProperties)
+ {
+ _applicationData.AllProperties = allProperties;
+
+ // Recreate the data table with the new property settings
+ DataTable newDataTable;
+ if (_applicationData.PSObjects is { Count: > 0 })
+ {
+ var psObjects = _applicationData.PSObjects.Cast().ToList();
+ newDataTable = TypeGetter.CastObjectsToTableView(psObjects, allProperties);
+ }
+ else
+ {
+ newDataTable = new DataTable([], []);
+ }
+
+ _dataTable = newDataTable;
+
+ // Reload and reapply filter
+ _inputSource = LoadData();
+ ApplyFilter();
+
+ // Update status bar to show current state
+ UpdateStatusBar();
+
+ // Force redraw
+ SetNeedsLayout();
+ SetNeedsDraw();
+ }
+
+ ///
+ /// Updates the status bar to reflect the current AllProperties state.
+ ///
+ private void UpdateStatusBar()
+ {
+ if (_statusBar == null) return;
+
+ // Remove and recreate status bar to update the checkbox text
+ Remove(_statusBar);
+ AddStatusBar();
+ }
+
+ ///
+ /// Accepts the current selection and closes the window.
+ ///
+ private void Accept()
+ {
+ Result = GetSelectedIndexes();
+ App?.RequestStop();
+ }
+
+ ///
+ /// Cancels the operation and closes the window.
+ ///
+ private void Close()
+ {
+ Result = null;
+ App?.RequestStop();
+ }
+
+ #endregion
+
+ #region UI Construction
+
+ ///
+ /// Adds the filter text field and error display to the window.
+ ///
+ private void AddFilter()
+ {
+ _filterLabel = new Label
+ {
+ Text = FILTER_LABEL,
+ X = MARGIN_LEFT,
+ Y = 0
+ };
+
+ _filterField = new TextField
+ {
+ Text = _applicationData.Filter ?? string.Empty,
+ X = Pos.Right(_filterLabel) + 1,
+ Y = Pos.Top(_filterLabel),
+ CanFocus = true,
+ Width = Dim.Fill() - 1
+ };
+
+ _filterField.KeyBindings.Remove(Key.A.WithCtrl);
+ _filterField.KeyBindings.Remove(Key.D.WithCtrl);
+
+ _filterErrorView = new View
+ {
+ Text = string.Empty,
+ X = Pos.Right(_filterLabel) + 1,
+ Y = Pos.Top(_filterLabel) + 1,
+ Width = Dim.Fill() - _filterLabel.Text.Length,
+ Height = Dim.Auto(DimAutoStyle.Text),
+ SchemeName = SchemeManager.SchemesToSchemeName(Schemes.Error)
+ };
+
+ _filterField.TextChanged += (_, _) =>
+ {
+ var filterText = _filterField.Text;
+ try
+ {
+ _filterErrorView.Text = string.Empty;
+ _applicationData.Filter = filterText;
+ ApplyFilter();
+ }
+ catch (Exception ex)
+ {
+ _filterErrorView.Text = ex.Message;
+ }
+ };
+
+ Add(_filterLabel, _filterField, _filterErrorView);
+
+ _filterField.Text = _applicationData.Filter ?? string.Empty;
+ _filterField.InsertionPoint = _filterField.Text.Length;
+ }
+
+ ///
+ /// Adds the main list view control to the window with configured selection behavior.
+ ///
+ private void AddListView()
+ {
+ _listView = new ListView
+ {
+ X = MARGIN_LEFT,
+ Y = _filterErrorView is { } ? Pos.Bottom(_filterErrorView) : 1,
+ Width = Dim.Fill(),
+ Height = Dim.Fill(1),
+ ShowMarks = _applicationData.OutputMode != OutputModeOption.None,
+ MarkMultiple = _applicationData.OutputMode == OutputModeOption.Multiple,
+ SelectedItem = 0,
+ ViewportSettings = ViewportSettingsFlags.HasScrollBars
+ };
+
+ _listView.KeyBindings.Remove(Key.A.WithCtrl);
+
+ if (!_applicationData.MinUI) AddHeader();
+
+ _listView.Accepted += (sender, args) => Accept();
+
+ Add(_listView);
+ return;
+
+ void AddHeader()
+ {
+ _header = new Header
+ {
+ X = CHECK_WIDTH
+ };
+
+
+ _listView.Padding!.Thickness = _listView.Padding.Thickness with { Top = 1 };
+ _listView!.Padding!.Add(_header);
+ _listView.VerticalScrollBar.Y = 1;
+
+ _header?.SetHeaders(_dataTable?.DataColumns.Select(c => c.Label).ToList(), _naturalColumnWidths);
+ }
+ }
+
+
+ ///
+ /// Adds the status bar with keyboard shortcuts to the window.
+ ///
+ private void AddStatusBar()
+ {
+ var shortcuts = new List();
+ if (_applicationData.OutputMode != OutputModeOption.None)
+ shortcuts.Add(new Shortcut(Key.Space, "Select", null));
+
+ if (_applicationData.OutputMode == OutputModeOption.Multiple)
+ {
+ shortcuts.Add(new Shortcut(Key.A.WithCtrl, "Sel. All", () =>
+ {
+ _listView?.MarkAll(true);
+ _listView?.SetNeedsDraw();
+ }));
+
+ shortcuts.Add(new Shortcut(Key.D.WithCtrl, "Sel. None", () =>
+ {
+ _listView?.MarkAll(false);
+ _listView?.SetNeedsDraw();
+ }));
+ }
+
+ if (_applicationData.OutputMode != OutputModeOption.None)
+ shortcuts.Add(new Shortcut(Key.Enter, "Accept", () =>
+ {
+ if (MostFocused == _filterField)
+ {
+ _listView!.SetFocus();
+ }
+ }));
+
+ shortcuts.Add(new Shortcut(Key.Esc, "Close", Close));
+
+ var allPropertiesShortcut = new Shortcut
+ {
+ CommandView = new CheckBox
+ {
+ Title = "A_ll Properties",
+ Value = _applicationData.AllProperties ? CheckState.Checked : CheckState.UnChecked,
+ CanFocus = false,
+ MouseHighlightStates = MouseState.None
+ },
+ CanFocus = false,
+ BindKeyToApplication = true
+ };
+
+ allPropertiesShortcut.Accepting += (_, e) =>
+ {
+ ReloadDataWithAllProperties(!_applicationData.AllProperties);
+ e.Handled = true;
+ };
+
+ shortcuts.Add(allPropertiesShortcut);
+
+
+ if (_applicationData.Verbose || _applicationData.Debug)
+ {
+ shortcuts.Add(new Shortcut(Key.Empty, $" v{_applicationData.ModuleVersion}", null));
+ var tgFileVersionInfo = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(Application))!.Location);
+ var tgVersion = tgFileVersionInfo?.FileVersion ?? "no version found";
+ //if (tgFileVersionInfo is { IsPreRelease: true })
+ {
+ tgVersion = tgFileVersionInfo?.ProductVersion?[..tgFileVersionInfo.ProductVersion.IndexOf('+')] ??
+ tgVersion;
+ }
+ shortcuts.Add(new Shortcut(Key.Empty, $"{App?.Driver?.GetName()} v{tgVersion}", null));
+ }
+
+ _statusBar = new StatusBar(shortcuts);
+ Add(_statusBar);
+ }
+
+ #endregion
+
+ #region Layout Calculation
+
+ ///
+ /// Calculates the natural column widths needed to display all data without truncation.
+ ///
+ /// The column headers for the grid.
+ /// An array of column widths where each width is the maximum needed for that column.
+ private int[] CalculateNaturalColumnWidths(List? gridHeaders)
+ {
+ if (gridHeaders is null || _dataTable is null)
+ return [];
+
+ var columnWidths = new int[gridHeaders.Count];
+
+ // Start with header widths
+ for (var i = 0; i < gridHeaders.Count; i++)
+ columnWidths[i] = gridHeaders[i].Length;
+
+ // Expand to fit data
+ foreach (var row in _dataTable.Data)
+ for (var i = 0; i < _dataTable.DataColumns.Count; i++)
+ {
+ var columnKey = _dataTable.DataColumns[i].ToString();
+ if (row.Values.TryGetValue(columnKey, out var value))
+ {
+ var len = value.DisplayValue.Length;
+ if (len > columnWidths[i])
+ columnWidths[i] = len;
+ }
+ }
+
+ return columnWidths;
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/RegexTreeViewTextFilter.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/RegexTreeViewTextFilter.cs
new file mode 100644
index 0000000..a7574dd
--- /dev/null
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/RegexTreeViewTextFilter.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Text.RegularExpressions;
+using Terminal.Gui.Views;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Provides regex-based filtering for a TreeView, allowing users to filter tree nodes by matching their display text against a regular expression pattern.
+///
+/// The parent ShowObjectTreeView that owns this filter.
+/// The TreeView to apply filtering to.
+internal sealed class RegexTreeViewTextFilter(ShowObjectTreeWindow parent, TreeView forTree) : ITreeViewFilter
+{
+ #region Fields
+
+ private readonly TreeView _forTree = forTree ?? throw new ArgumentNullException(nameof(forTree));
+ private string _text = string.Empty;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets or sets the regex pattern text used for filtering.
+ ///
+ public string Text
+ {
+ get => _text;
+ set
+ {
+ _text = value;
+ RefreshTreeView();
+ }
+ }
+
+ #endregion
+
+ #region ITreeViewFilter Implementation
+
+ ///
+ /// Determines whether the specified model object matches the current filter criteria.
+ ///
+ /// The model object to test against the filter.
+ /// if the object matches the filter or no filter is set; otherwise, .
+ public bool IsMatch(object model)
+ {
+ if (string.IsNullOrWhiteSpace(Text))
+ return true;
+
+ var modelText = _forTree.AspectGetter(model);
+ try
+ {
+ var isMatch = Regex.IsMatch(modelText ?? string.Empty, Text, RegexOptions.IgnoreCase);
+ parent.SetRegexError(string.Empty);
+ return isMatch;
+ }
+ catch (RegexParseException e)
+ {
+ parent.SetRegexError(e.Message);
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ ///
+ /// Refreshes the tree view to apply the updated filter.
+ ///
+ private void RefreshTreeView()
+ {
+ _forTree.InvalidateLineMap();
+ _forTree.SetNeedsDraw();
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectTreeCmdletCommand.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectTreeCmdletCommand.cs
index 5b2f95d..676f0f9 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectTreeCmdletCommand.cs
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectTreeCmdletCommand.cs
@@ -4,153 +4,160 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Internal;
-
-using OutGridView.Models;
-
-namespace OutGridView.Cmdlet
+using Microsoft.PowerShell.OutGridView.Models;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Displays objects in a hierarchical tree view in a separate console window. This class is invoked by PowerShell when
+/// the
+/// Show-ObjectTree cmdlet is called.
+///
+[Cmdlet("Show", "ObjectTree")]
+[Alias("shot")]
+public class ShowObjectTreeCmdletCommand : PSCmdlet, IDisposable
{
- [Cmdlet("Show", "ObjectTree")]
- [Alias("shot")]
- public class ShowObjectTreeCmdletCommand : PSCmdlet, IDisposable
+ #region Properties
+
+ private const string DATA_NOT_QUALIFIED_FOR_SHOW_OBJECT_TREE = nameof(DATA_NOT_QUALIFIED_FOR_SHOW_OBJECT_TREE);
+
+ private const string ENVIRONMENT_NOT_SUPPORTED_FOR_SHOW_OBJECT_TREE =
+ nameof(ENVIRONMENT_NOT_SUPPORTED_FOR_SHOW_OBJECT_TREE);
+
+ private readonly List _psObjects = [];
+
+ #endregion Properties
+
+ #region Input Parameters
+
+ ///
+ /// Gets or sets the current pipeline object.
+ ///
+ [Parameter(ValueFromPipeline = true, HelpMessage = "Specifies the input pipeline object")]
+ public PSObject InputObject { get; set; } = AutomationNull.Value;
+
+ ///
+ /// Gets or sets the title of the Show-ObjectTree window.
+ ///
+ [Parameter(HelpMessage =
+ "Specifies the text that appears in the title bar of the Show-ObjectTree window. By default, the title bar displays the command that invokes Show-ObjectTree.")]
+ [ValidateNotNullOrEmpty]
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the initial value for the filter in the GUI.
+ ///
+ [Parameter(HelpMessage =
+ "Pre-populates the Filter edit box, allowing filtering to be specified on the command line. The filter uses regular expressions.")]
+ public string? Filter { set; get; }
+
+ ///
+ /// Gets or sets a value indicating whether "minimum UI" mode will be enabled.
+ ///
+ [Parameter(HelpMessage = "If specified no window frame, filter box, or status bar will be displayed in the TUI.")]
+ public SwitchParameter MinUI { set; get; }
+
+ ///
+ /// Gets or sets the Terminal.Gui driver to use.
+ ///
+ [Parameter(HelpMessage =
+ "Forces the Terminal.Gui driver to use. Valid values are 'ansi', 'windows', or 'unix'.")]
+ public string? ForceDriver { set; get; }
+
+ ///
+ /// Gets a value indicating whether the Debug switch is present.
+ ///
+ public bool Debug => MyInvocation.BoundParameters.ContainsKey("Debug");
+
+ #endregion Input Parameters
+
+ ///
+ /// Performs initialization of command execution. Validates that the environment supports object tree view.
+ ///
+ protected override void BeginProcessing()
{
- #region Properties
-
- private const string DataNotQualifiedForShowObjectTree = nameof(DataNotQualifiedForShowObjectTree);
- private const string EnvironmentNotSupportedForShowObjectTree = nameof(EnvironmentNotSupportedForShowObjectTree);
-
- private List _psObjects = new List();
-
- #endregion Properties
-
- #region Input Parameters
-
- ///
- /// This parameter specifies the current pipeline object.
- ///
- [Parameter(ValueFromPipeline = true, HelpMessage = "Specifies the input pipeline object")]
- public PSObject InputObject { get; set; } = AutomationNull.Value;
-
- ///
- /// Gets/sets the title of the Out-GridView window.
- ///
- [Parameter(HelpMessage = "Specifies the text that appears in the title bar of the Out-ConsoleGridView window. y default, the title bar displays the command that invokes Out-ConsoleGridView.")]
- [ValidateNotNullOrEmpty]
- public string Title { get; set; }
-
- ///
- /// gets or sets the initial value for the filter in the GUI
- ///
- [Parameter(HelpMessage = "Pre-populates the Filter edit box, allowing filtering to be specified on the command line. The filter uses regular expressions.")]
- public string Filter { set; get; }
-
- ///
- /// gets or sets the whether "minimum UI" mode will be enabled
- ///
- [Parameter(HelpMessage = "If specified no window frame, filter box, or status bar will be displayed in the GUI.")]
- public SwitchParameter MinUI { set; get; }
- ///
- /// gets or sets the whether the Terminal.Gui System.Net.Console-based ConsoleDriver will be used instead of the
- /// default platform-specific (Windows or Curses) ConsoleDriver.
- ///
- [Parameter(HelpMessage = "If specified the Terminal.Gui System.Net.Console-based ConsoleDriver (NetDriver) will be used.")]
- public SwitchParameter UseNetDriver { set; get; }
-
- ///
- /// For the -Debug switch
- ///
- public bool Debug => MyInvocation.BoundParameters.TryGetValue("Debug", out var o);
-
- #endregion Input Parameters
-
- // This method gets called once for each cmdlet in the pipeline when the pipeline starts executing
- protected override void BeginProcessing()
+ if (Console.IsInputRedirected)
{
- if (Console.IsInputRedirected)
- {
- ErrorRecord error = new ErrorRecord(
- new PSNotSupportedException("Not supported in this environment (when input is redirected)."),
- EnvironmentNotSupportedForShowObjectTree,
- ErrorCategory.NotImplemented,
- null);
-
- ThrowTerminatingError(error);
- }
- }
+ var error = new ErrorRecord(
+ new PSNotSupportedException("Not supported in this environment (when input is redirected)."),
+ ENVIRONMENT_NOT_SUPPORTED_FOR_SHOW_OBJECT_TREE,
+ ErrorCategory.NotImplemented,
+ null);
- // This method will be called for each input received from the pipeline to this cmdlet; if no input is received, this method is not called
- protected override void ProcessRecord()
- {
- if (InputObject == null || InputObject == AutomationNull.Value)
- {
- return;
- }
-
- if (InputObject.BaseObject is IDictionary dictionary)
- {
- // Dictionaries should be enumerated through because the pipeline does not enumerate through them.
- foreach (DictionaryEntry entry in dictionary)
- {
- ProcessObject(PSObject.AsPSObject(entry));
- }
- }
- else
- {
- ProcessObject(InputObject);
- }
+ ThrowTerminatingError(error);
}
+ }
+
+ ///
+ /// Processes each input object received from the pipeline.
+ ///
+ protected override void ProcessRecord()
+ {
+ if (Equals(InputObject, AutomationNull.Value)) return;
+
+ if (InputObject.BaseObject is IDictionary dictionary)
+ // Dictionaries should be enumerated through because the pipeline does not enumerate through them.
+ foreach (DictionaryEntry entry in dictionary)
+ ProcessObject(PSObject.AsPSObject(entry));
+ else
+ ProcessObject(InputObject);
+ }
+
+ private void ProcessObject(PSObject input)
+ {
+ var baseObject = input.BaseObject;
- private void ProcessObject(PSObject input)
+ // Throw a terminating error for types that are not supported.
+ if (baseObject is ScriptBlock ||
+ baseObject is SwitchParameter ||
+ baseObject is PSReference ||
+ baseObject is PSObject)
{
+ var error = new ErrorRecord(
+ new FormatException("Invalid data type for Show-ObjectTree"),
+ DATA_NOT_QUALIFIED_FOR_SHOW_OBJECT_TREE,
+ ErrorCategory.InvalidType,
+ null);
- object baseObject = input.BaseObject;
+ ThrowTerminatingError(error);
+ }
- // Throw a terminating error for types that are not supported.
- if (baseObject is ScriptBlock ||
- baseObject is SwitchParameter ||
- baseObject is PSReference ||
- baseObject is PSObject)
- {
- ErrorRecord error = new ErrorRecord(
- new FormatException("Invalid data type for Show-ObjectTree"),
- DataNotQualifiedForShowObjectTree,
- ErrorCategory.InvalidType,
- null);
+ _psObjects.Add(input);
+ }
- ThrowTerminatingError(error);
- }
+ ///
+ /// Performs final processing after all pipeline objects have been received.
+ /// Displays the object tree view with all collected objects.
+ ///
+ protected override void EndProcessing()
+ {
+ base.EndProcessing();
- _psObjects.Add(input);
- }
+ // Return if no objects
+ if (_psObjects.Count == 0) return;
- // This method will be called once at the end of pipeline execution; if no input is received, this method is not called
- protected override void EndProcessing()
+ var applicationData = new ApplicationData
{
- base.EndProcessing();
-
- //Return if no objects
- if (_psObjects.Count == 0)
- {
- return;
- }
-
- var applicationData = new ApplicationData
- {
- Title = Title ?? "Show-ObjectTree",
- Filter = Filter,
- MinUI = MinUI,
- UseNetDriver = UseNetDriver,
- Debug = Debug,
- ModuleVersion = MyInvocation.MyCommand.Version.ToString()
- };
-
- ShowObjectView.Run(_psObjects, applicationData);
- }
+ PSObjects = _psObjects.Cast().ToList(),
+ Title = Title ?? "Show-ObjectTree",
+ Filter = Filter,
+ MinUI = MinUI,
+ ForceDriver = ForceDriver,
+ Debug = Debug,
+ ModuleVersion = MyInvocation.MyCommand.Version.ToString()
+ };
+
+ ShowObjectView.Run(applicationData);
+ }
- public void Dispose()
- {
- GC.SuppressFinalize(this);
- }
+ ///
+ /// Releases all resources used by the .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectTreeWindow.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectTreeWindow.cs
new file mode 100644
index 0000000..aa423ca
--- /dev/null
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectTreeWindow.cs
@@ -0,0 +1,362 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using Microsoft.PowerShell.OutGridView.Models;
+using Terminal.Gui.App;
+using Terminal.Gui.Configuration;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.Input;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Provides the Terminal.Gui Window implementation for displaying object hierarchies in a tree view with filtering capabilities.
+///
+internal sealed class ShowObjectTreeWindow : Window, ITreeBuilder
+{
+ private const string FILTER_LABEL = "_Filter:";
+
+ #region Fields
+
+ private readonly TreeView _tree;
+ private readonly View _filterErrorView;
+ private Shortcut? _selectedShortcut;
+ private readonly StatusBar _statusBar;
+ private readonly ApplicationData _applicationData;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets a value indicating whether this tree builder supports the CanExpand operation.
+ ///
+ public bool SupportsCanExpand => true;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the class with the specified application data.
+ ///
+ /// The configuration and PSObjects to display.
+ public ShowObjectTreeWindow(ApplicationData applicationData)
+ {
+ _applicationData = applicationData;
+ Title = _applicationData.Title ?? "Show-ObjectView";
+ Width = Dim.Fill();
+ Height = Dim.Fill(1);
+
+ if (_applicationData.MinUI)
+ {
+ BorderStyle = LineStyle.None;
+ Title = string.Empty;
+ X = -1;
+ Height = Dim.Fill();
+ }
+
+ // Extract root objects from PSObjects
+ var rootObjects = _applicationData.PSObjects?.Select(p =>
+ {
+ if (p is PSObject pso)
+ return pso.BaseObject;
+ return p;
+ }).ToList() ?? [];
+
+ var filterLabel = new Label
+ {
+ Text = FILTER_LABEL,
+ X = 1
+ };
+
+ var filterTextField = new TextField
+ {
+ Text = _applicationData.Filter ?? string.Empty,
+ X = Pos.Right(filterLabel) + 1,
+ Width = Dim.Fill(1),
+ InsertionPoint= (_applicationData.Filter ?? string.Empty).Length
+ };
+
+ _filterErrorView = new Label
+ {
+ SchemeName = SchemeManager.SchemesToSchemeName(Schemes.Error),
+ X = Pos.Right(filterLabel) + 1,
+ Y = Pos.Top(filterLabel) + 1,
+ Width = Dim.Width(filterTextField),
+ Height = Dim.Auto(DimAutoStyle.Text)
+ };
+
+ _tree = new TreeView
+ {
+ Y = Pos.Bottom(_filterErrorView),
+ Width = Dim.Fill(),
+ Height = Dim.Fill(),
+ TreeBuilder = this,
+ AspectGetter = AspectGetter
+ };
+ _tree.SelectionChanged += SelectionChanged;
+
+ var regexFilter = new RegexTreeViewTextFilter(this, _tree)
+ {
+ Text = _applicationData.Filter ?? string.Empty
+ };
+ _tree.Filter = regexFilter;
+
+ if (rootObjects.Count > 0)
+ _tree.AddObjects(rootObjects);
+ else
+ _tree.AddObject("No Objects");
+
+ filterTextField.TextChanged += (sender, e) => OnFilterTextChanged(sender, e, regexFilter);
+
+ var shortcuts = CreateShortcuts(rootObjects);
+
+ _statusBar = new StatusBar(shortcuts)
+ {
+ Visible = !_applicationData.MinUI
+ };
+
+ if (!_applicationData.MinUI)
+ {
+ Add(filterLabel);
+ Add(filterTextField);
+ Add(_filterErrorView);
+ }
+
+ Add(_tree);
+ }
+
+ protected override void OnIsRunningChanged(bool newIsRunning)
+ {
+ base.OnIsRunningChanged(newIsRunning);
+ if (!newIsRunning) return;
+
+ // We do this here, because _statusBar requires the Application to be running to
+ // access the driver information.
+ Add(_statusBar);
+ _tree.SetFocus();
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ ///
+ /// Handles filter text changes and applies the regex filter.
+ ///
+ /// The text field that triggered the event.
+ /// The event arguments.
+ /// The regex filter to update.
+ private void OnFilterTextChanged(object? sender, EventArgs e, RegexTreeViewTextFilter regexFilter)
+ {
+ var textField = sender as TextField;
+ if (textField is null) return;
+
+ // Test that the regex is valid before applying it
+ try
+ {
+ _ = new Regex(textField.Text ?? string.Empty, RegexOptions.IgnoreCase);
+ }
+ catch (RegexParseException ex)
+ {
+ _filterErrorView.Text = ex.Message;
+ return;
+ }
+
+ _filterErrorView.Text = string.Empty;
+ regexFilter.Text = textField.Text ?? string.Empty;
+ }
+
+ ///
+ /// Handles selection changes in the tree view and updates the status bar.
+ ///
+ /// The tree view that triggered the event.
+ /// The selection changed event arguments.
+ private void SelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ var selectedValue = e.NewValue;
+
+ if (selectedValue is CachedMemberResult cmr)
+ selectedValue = cmr.Value;
+
+ _selectedShortcut?.Title = selectedValue != null ? selectedValue.GetType().Name : string.Empty;
+
+ _statusBar.SetNeedsDraw();
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Sets the regex error message displayed in the filter error view.
+ ///
+ /// The error message to display.
+ internal void SetRegexError(string error)
+ {
+ if (string.Equals(error, _filterErrorView.Text, StringComparison.Ordinal)) return;
+ _filterErrorView.Text = error;
+ }
+
+ #endregion
+
+ #region ITreeBuilder Implementation
+
+ ///
+ /// Determines whether the specified object can be expanded to show children.
+ ///
+ /// The object to check for expansion capability.
+ /// if the object can be expanded; otherwise, .
+ public bool CanExpand(object toExpand)
+ {
+ if (toExpand is CachedMemberResult p) return IsBasicType(p.Value);
+
+ return IsBasicType(toExpand);
+ }
+
+ ///
+ /// Gets the child objects for the specified parent object.
+ ///
+ /// The parent object to get children for.
+ /// An enumerable collection of child objects.
+ public IEnumerable GetChildren(object? forObject)
+ {
+ while (true)
+ {
+ if (forObject == null || !CanExpand(forObject)) return [];
+
+ switch (forObject)
+ {
+ case CachedMemberResult { IsCollection: true } p:
+ return p.Elements ?? Enumerable.Empty();
+ case CachedMemberResult p:
+ forObject = p.Value;
+ continue;
+ case CachedMemberResultElement e:
+ forObject = e.Value;
+ continue;
+ }
+
+ var children = new List();
+
+ foreach (var member in forObject.GetType().GetMembers(BindingFlags.Instance | BindingFlags.Public)
+ .OrderBy(m => m.Name))
+ {
+ if (member is PropertyInfo prop)
+ children.Add(new CachedMemberResult(forObject, prop));
+
+ if (member is FieldInfo field)
+ children.Add(new CachedMemberResult(forObject, field));
+ }
+
+ try
+ {
+ children.AddRange(GetExtraChildren(forObject));
+ }
+ catch (Exception)
+ {
+ // Extra children unavailable, possibly security or IO exceptions enumerating children etc
+ }
+
+ return children;
+ }
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ ///
+ /// Gets the display text for an object in the tree view.
+ ///
+ /// The object to get display text for.
+ /// The display text for the object.
+ private string? AspectGetter(object? toRender)
+ {
+ return toRender switch
+ {
+ Process p => p.ProcessName,
+ null => "Null",
+ FileSystemInfo fsi when !IsRootObject(fsi) => fsi.Name,
+ _ => toRender.ToString()
+ };
+ }
+
+ ///
+ /// Determines whether the specified object is a root object in the tree.
+ ///
+ /// The object to check.
+ /// if the object is a root object; otherwise, .
+ private bool IsRootObject(object o) => _tree.Objects.Contains(o);
+
+ ///
+ /// Determines whether the specified value is a basic (non-primitive, non-string) type that can be expanded.
+ ///
+ /// The value to check.
+ /// if the value is a basic type; otherwise, .
+ private static bool IsBasicType(object? value) =>
+ value != null && value is not string && !value.GetType().IsValueType;
+
+ ///
+ /// Gets additional child objects for special types like DirectoryInfo.
+ ///
+ /// The object to get extra children for.
+ /// An enumerable collection of additional child objects.
+ private static IEnumerable GetExtraChildren(object forObject)
+ {
+ if (forObject is DirectoryInfo dir)
+ foreach (var c in dir.EnumerateFileSystemInfos())
+ yield return c;
+ }
+
+ ///
+ /// Creates the keyboard shortcuts for the status bar.
+ ///
+ /// The root objects being displayed.
+ /// A list of shortcuts for the status bar.
+ private List CreateShortcuts(List rootObjects)
+ {
+ var shortcuts = new List();
+
+ var elementDescription = "objects";
+ var types = rootObjects.Select(o => o.GetType()).Distinct().ToArray();
+ if (types.Length == 1)
+ elementDescription = types[0].Name;
+
+ shortcuts.Add(new Shortcut(Key.Esc, "Close", () => App?.RequestStop()));
+
+ var countShortcut = new Shortcut(Key.Empty, $"{rootObjects.Count} {elementDescription}", null);
+ _selectedShortcut = new Shortcut(Key.Empty, string.Empty, null);
+ shortcuts.Add(countShortcut);
+ shortcuts.Add(_selectedShortcut);
+
+ if (_applicationData.Verbose || _applicationData.Debug)
+ {
+ shortcuts.Add(new Shortcut(Key.Empty, $" v{_applicationData.ModuleVersion}", null));
+ FileVersionInfo tgFileVersionInfo = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(Application))!.Location);
+ string tgVersion = tgFileVersionInfo?.FileVersion ?? "no version found";
+ //if (tgFileVersionInfo is { IsPreRelease: true })
+ {
+ tgVersion = tgFileVersionInfo?.ProductVersion?[..tgFileVersionInfo.ProductVersion.IndexOf('+')] ?? tgVersion;
+ }
+ shortcuts.Add(new Shortcut(Key.Empty,
+ $"{App?.Driver?.GetName()} v{tgVersion}",
+ null));
+ }
+
+ return shortcuts;
+ }
+
+ #endregion
+}
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs
index 989cab1..f91360e 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/ShowObjectView.cs
@@ -2,461 +2,58 @@
// Licensed under the MIT License.
using System;
-using System.Collections;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
using System.Linq;
using System.Management.Automation;
-using System.Reflection;
-using System.Text.RegularExpressions;
-
-using OutGridView.Models;
-
-using Terminal.Gui;
-using Terminal.Gui.Trees;
-
-namespace OutGridView.Cmdlet
+using Microsoft.PowerShell.OutGridView.Models;
+using Terminal.Gui.App;
+
+namespace Microsoft.PowerShell.ConsoleGuiTools;
+
+///
+/// Provides the main orchestration for the Show-ObjectTree cmdlet, managing the Terminal.Gui application lifecycle
+/// and coordinating between the application data and the tree view window.
+///
+///
+/// This class serves as a facade that initializes the Terminal.Gui framework, creates and runs the tree view window,
+/// and handles cleanup operations. It delegates the actual UI rendering and user interaction to the class.
+///
+internal sealed class ShowObjectView : IDisposable
{
- internal sealed class ShowObjectView : Window, ITreeBuilder
+ ///
+ /// Runs the Show-ObjectView Terminal.Gui Application with the specified configuration.
+ ///
+ /// The application configuration containing the PSObjects and display options.
+ ///
+ ///
+ /// This method initializes the Terminal.Gui framework, creates a instance,
+ /// and runs it until the user closes the window. The method handles the complete application lifecycle
+ /// including initialization, execution, and shutdown.
+ ///
+ ///
+ internal static void Run(ApplicationData applicationData)
{
- private readonly TreeView tree;
- private readonly RegexTreeViewTextFilter filter;
- private readonly Label filterErrorLabel;
-
- public bool SupportsCanExpand => true;
- private StatusItem selectedStatusBarItem;
- private StatusBar statusBar;
-
- public ShowObjectView(List rootObjects, ApplicationData applicationData)
- {
- Title = applicationData.Title;
- Width = Dim.Fill();
- Height = Dim.Fill(1);
- Modal = false;
-
-
- if (applicationData.MinUI)
- {
- Border.BorderStyle = BorderStyle.None;
- Title = string.Empty;
- X = -1;
- Height = Dim.Fill();
- }
-
- tree = new TreeView
- {
- Y = applicationData.MinUI ? 0 : 2,
- Width = Dim.Fill(),
- Height = Dim.Fill(),
- };
- tree.TreeBuilder = this;
- tree.AspectGetter = this.AspectGetter;
- tree.SelectionChanged += this.SelectionChanged;
-
- tree.ClearKeybinding(Command.ExpandAll);
-
- this.filter = new RegexTreeViewTextFilter(this, tree);
- this.filter.Text = applicationData.Filter ?? string.Empty;
- tree.Filter = this.filter;
-
- if (rootObjects.Count > 0)
- {
- tree.AddObjects(rootObjects);
- }
- else
- {
- tree.AddObject("No Objects");
- }
- statusBar = new StatusBar();
-
- string elementDescription = "objects";
-
- var types = rootObjects.Select(o => o.GetType()).Distinct().ToArray();
- if (types.Length == 1)
- {
- elementDescription = types[0].Name;
- }
-
- var lblFilter = new Label()
- {
- Text = "Filter:",
- X = 1,
- };
- var tbFilter = new TextField()
- {
- X = Pos.Right(lblFilter),
- Width = Dim.Fill(1),
- Text = applicationData.Filter ?? string.Empty
- };
- tbFilter.CursorPosition = tbFilter.Text.Length;
-
- tbFilter.TextChanged += (_) =>
- {
- filter.Text = tbFilter.Text.ToString();
- };
-
-
- filterErrorLabel = new Label(string.Empty)
- {
- X = Pos.Right(lblFilter) + 1,
- Y = Pos.Top(lblFilter) + 1,
- ColorScheme = Colors.Base,
- Width = Dim.Fill() - lblFilter.Text.Length
- };
-
- if (!applicationData.MinUI)
- {
- Add(lblFilter);
- Add(tbFilter);
- Add(filterErrorLabel);
- }
-
- int pos = 0;
- statusBar.AddItemAt(pos++, new StatusItem(Key.Esc, "~ESC~ Close", () => Application.RequestStop()));
-
- var siCount = new StatusItem(Key.Null, $"{rootObjects.Count} {elementDescription}", null);
- selectedStatusBarItem = new StatusItem(Key.Null, string.Empty, null);
- statusBar.AddItemAt(pos++, siCount);
- statusBar.AddItemAt(pos++, selectedStatusBarItem);
-
- if (applicationData.Debug)
- {
- statusBar.AddItemAt(pos++, new StatusItem(Key.Null, $" v{applicationData.ModuleVersion}", null));
- statusBar.AddItemAt(pos++, new StatusItem(Key.Null,
- $"{Application.Driver} v{FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(Application)).Location).ProductVersion}", null));
- }
-
- statusBar.Visible = !applicationData.MinUI;
- Application.Top.Add(statusBar);
-
- Add(tree);
- }
- private void SetRegexError(string error)
- {
- if (string.Equals(error, filterErrorLabel.Text.ToString(), StringComparison.Ordinal))
- {
- return;
- }
- filterErrorLabel.Text = error;
- filterErrorLabel.ColorScheme = Colors.Error;
- filterErrorLabel.Redraw(filterErrorLabel.Bounds);
- }
-
- private void SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- var selectedValue = e.NewValue;
-
- if (selectedValue is CachedMemberResult cmr)
- {
- selectedValue = cmr.Value;
- }
-
- if (selectedValue != null && selectedStatusBarItem != null)
- {
- selectedStatusBarItem.Title = selectedValue.GetType().Name;
- }
- else
- {
- selectedStatusBarItem.Title = string.Empty;
- }
-
- statusBar.SetNeedsDisplay();
- }
-
- private string AspectGetter(object toRender)
- {
- if (toRender is Process p)
- {
- return p.ProcessName;
- }
- if (toRender is null)
- {
- return "Null";
- }
- if (toRender is FileSystemInfo fsi && !IsRootObject(fsi))
- {
- return fsi.Name;
- }
-
- return toRender.ToString();
- }
-
- private bool IsRootObject(object o)
- {
- return tree.Objects.Contains(o);
- }
-
- public bool CanExpand(object toExpand)
- {
- if (toExpand is CachedMemberResult p)
- {
- return IsBasicType(p?.Value);
- }
-
- // Any complex object type can be expanded to reveal properties
- return IsBasicType(toExpand);
- }
-
- private static bool IsBasicType(object value)
- {
- return value != null && value is not string && !value.GetType().IsValueType;
- }
-
- public IEnumerable GetChildren(object forObject)
- {
- if (forObject == null || !this.CanExpand(forObject))
- {
- return Enumerable.Empty();
- }
-
- if (forObject is CachedMemberResult p)
- {
- if (p.IsCollection)
- {
- return p.Elements;
- }
-
- return GetChildren(p.Value);
- }
-
- if (forObject is CachedMemberResultElement e)
- {
- return GetChildren(e.Value);
- }
+ Terminal.Gui.Configuration.ConfigurationManager.Enable(Terminal.Gui.Configuration.ConfigLocations.All);
- List children = new List();
+ if (!string.IsNullOrEmpty(applicationData.ForceDriver))
+ Application.ForceDriver = applicationData.ForceDriver;
- foreach (var member in forObject.GetType().GetMembers(BindingFlags.Instance | BindingFlags.Public).OrderBy(m => m.Name))
- {
- if (member is PropertyInfo prop)
- {
- children.Add(new CachedMemberResult(forObject, prop));
- }
- if (member is FieldInfo field)
- {
- children.Add(new CachedMemberResult(forObject, field));
- }
- }
-
- try
- {
- children.AddRange(GetExtraChildren(forObject));
- }
- catch (Exception)
- {
- // Extra children unavailable, possibly security or IO exceptions enumerating children etc
- }
-
- return children;
- }
-
- private static IEnumerable GetExtraChildren(object forObject)
- {
- if (forObject is DirectoryInfo dir)
- {
- foreach (var c in dir.EnumerateFileSystemInfos())
- {
- yield return c;
- }
- }
- }
-
- internal static void Run(List objects, 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();
- Window window = null;
-
- try
- {
- window = new ShowObjectView(objects.Select(p => p.BaseObject).ToList(), applicationData);
- Application.Top.Add(window);
- Application.Run();
- }
- finally
- {
- Application.Shutdown();
- window?.Dispose();
- }
- }
-
- sealed class CachedMemberResultElement
- {
- public int Index;
- public object Value;
-
- private string representation;
-
- public CachedMemberResultElement(object value, int index)
- {
- Index = index;
- Value = value;
-
- try
- {
- representation = Value?.ToString() ?? "Null";
- }
- catch (Exception)
- {
- Value = representation = "Unavailable";
- }
- }
- public override string ToString()
- {
- return $"[{Index}]: {representation}]";
- }
- }
-
- sealed class CachedMemberResult
- {
- public MemberInfo Member;
- public object Value;
- public object Parent;
- private string representation;
- private List valueAsList;
-
-
- public bool IsCollection => valueAsList != null;
- public IReadOnlyCollection Elements => valueAsList?.AsReadOnly();
-
- public CachedMemberResult(object parent, MemberInfo mem)
- {
- Parent = parent;
- Member = mem;
-
- try
- {
- if (mem is PropertyInfo p)
- {
- Value = p.GetValue(parent);
- }
- else if (mem is FieldInfo f)
- {
- Value = f.GetValue(parent);
- }
- else
- {
- throw new NotSupportedException($"Unknown {nameof(MemberInfo)} Type");
- }
-
- representation = ValueToString();
-
- }
- catch (Exception)
- {
- Value = representation = "Unavailable";
- }
- }
-
- private string ValueToString()
- {
- if (Value == null)
- {
- return "Null";
- }
- try
- {
- if (IsCollectionOfKnownTypeAndSize(out Type elementType, out int size))
- {
- return $"{elementType.Name}[{size}]";
- }
- }
- catch (Exception)
- {
- return Value?.ToString();
- }
-
-
- return Value?.ToString();
- }
-
- private bool IsCollectionOfKnownTypeAndSize(out Type elementType, out int size)
- {
- elementType = null;
- size = 0;
-
- if (Value == null || Value is string)
- {
-
- return false;
- }
-
- if (Value is IEnumerable ienumerable)
- {
- var list = ienumerable.Cast().ToList();
-
- var types = list.Where(v => v != null).Select(v => v.GetType()).Distinct().ToArray();
-
- if (types.Length == 1)
- {
- elementType = types[0];
- size = list.Count;
-
- valueAsList = list.Select((e, i) => new CachedMemberResultElement(e, i)).ToList();
- return true;
- }
- }
-
- return false;
- }
-
- public override string ToString()
- {
- return Member.Name + ": " + representation;
- }
- }
- private sealed class RegexTreeViewTextFilter : ITreeViewFilter
- {
- private readonly ShowObjectView parent;
- readonly TreeView _forTree;
-
- public RegexTreeViewTextFilter(ShowObjectView parent, TreeView forTree)
- {
- this.parent = parent;
- _forTree = forTree ?? throw new ArgumentNullException(nameof(forTree));
- }
-
- private string text;
-
- public string Text
- {
- get { return text; }
- set
- {
- text = value;
- RefreshTreeView();
- }
- }
-
- private void RefreshTreeView()
- {
- _forTree.InvalidateLineMap();
- _forTree.SetNeedsDisplay();
- }
-
- public bool IsMatch(object model)
- {
- if (string.IsNullOrWhiteSpace(Text))
- {
- return true;
- }
-
- parent.SetRegexError(string.Empty);
+ using ShowObjectTreeWindow window = new(applicationData);
+ using IApplication app = Application.Create();
+ app.Init();
+ bool accepted = app.Run(window) is true;
+ Application.ForceDriver = string.Empty;
+ }
- var modelText = _forTree.AspectGetter(model);
- try
- {
- return Regex.IsMatch(modelText, text, RegexOptions.IgnoreCase);
- }
- catch (RegexParseException e)
- {
- parent.SetRegexError(e.Message);
- return true;
- }
- }
- }
+ ///
+ /// Releases resources used by the .
+ ///
+ ///
+ /// Currently, there are no resources to dispose. This method is provided for future extensibility
+ /// and to follow the standard IDisposable pattern.
+ ///
+ public void Dispose()
+ {
+ // No resources to dispose currently
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.ConsoleGuiTools/TypeGetter.cs b/src/Microsoft.PowerShell.ConsoleGuiTools/TypeGetter.cs
index 6f3f643..34ad33e 100644
--- a/src/Microsoft.PowerShell.ConsoleGuiTools/TypeGetter.cs
+++ b/src/Microsoft.PowerShell.ConsoleGuiTools/TypeGetter.cs
@@ -1,198 +1,410 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Management.Automation;
-using System.Management.Automation.Internal;
-
+using System.Management.Automation.Runspaces;
+using System.Text.RegularExpressions;
using Microsoft.PowerShell.Commands;
+using Microsoft.PowerShell.OutGridView.Models;
-using OutGridView.Models;
+namespace Microsoft.PowerShell.ConsoleGuiTools;
-namespace OutGridView.Cmdlet
+///
+/// Provides methods to retrieve type information and convert PowerShell objects into data table structures for display
+/// in the grid view using PowerShell's native formatting infrastructure.
+///
+public class TypeGetter
{
- public class TypeGetter
+ private readonly Dictionary _formatCache = new();
+
+ ///
+ /// Regex pattern to match ANSI escape sequences.
+ ///
+ private static readonly Regex AnsiEscapeRegex = new(@"\x1b\[[0-9;]*m", RegexOptions.Compiled);
+
+ ///
+ /// Strips ANSI escape sequences from a string.
+ ///
+ /// The string potentially containing ANSI codes.
+ /// The string with ANSI codes removed.
+ private static string StripAnsiCodes(string value)
{
- private PSCmdlet _cmdlet;
+ if (string.IsNullOrEmpty(value)) return value;
+ return AnsiEscapeRegex.Replace(value, string.Empty);
+ }
- public TypeGetter(PSCmdlet cmdlet)
- {
- _cmdlet = cmdlet;
- }
- public FormatViewDefinition GetFormatViewDefinitionForObject(PSObject obj)
+ ///
+ /// Gets the format view definition for the specified type name, using a cache to avoid redundant lookups.
+ ///
+ /// The full type name to get the format view definition for.
+ /// The format view definition if found; otherwise, .
+ private FormatViewDefinition? GetFormatViewDefinitionForType(string typeName)
+ {
+ if (_formatCache.TryGetValue(typeName, out var cached)) return cached;
+
+ try
{
- var typeName = obj.BaseObject.GetType().FullName;
+ // Use the current runspace to access format data from loaded modules
+ // This is critical for CIM instances (like Get-NetAdapter) which have format data
+ // defined in their respective modules' .ps1xml files
+ using var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
+ ps.AddCommand("Get-FormatData").AddParameter("TypeName", typeName);
- var types = _cmdlet.InvokeCommand.InvokeScript(@"Microsoft.PowerShell.Utility\Get-FormatData " + typeName).ToList();
+ var results = ps.Invoke();
- //No custom type definitions found - try the PowerShell specific format data
- if (types == null || types.Count == 0)
+ FormatViewDefinition? result = null;
+ if (results.Count > 0)
{
- types = _cmdlet.InvokeCommand
- .InvokeScript(@"Microsoft.PowerShell.Utility\Get-FormatData -PowerShellVersion $PSVersionTable.PSVersion " + typeName).ToList();
-
- if (types == null || types.Count == 0)
- {
- return null;
- }
+ var extendedTypeDefinition = results[0].BaseObject as ExtendedTypeDefinition;
+ result = extendedTypeDefinition?.FormatViewDefinition.FirstOrDefault(v => v.Control is TableControl);
}
- var extendedTypeDefinition = types[0].BaseObject as ExtendedTypeDefinition;
-
- return extendedTypeDefinition.FormatViewDefinition[0];
+ _formatCache[typeName] = result;
+ return result;
+ }
+ catch
+ {
+ // If we can't get format data (e.g., not running in a runspace context),
+ // cache null and continue with fallback logic
+ _formatCache[typeName] = null;
+ return null;
}
+ }
- public static DataTableRow CastObjectToDataTableRow(PSObject ps, List dataColumns, int objectIndex)
+ ///
+ /// Gets the format view definition for the specified PowerShell object.
+ ///
+ /// The PowerShell object to get the format view definition for.
+ /// The format view definition if found; otherwise, .
+ private FormatViewDefinition? GetFormatViewDefinitionForObject(PSObject obj)
+ {
+ // PSObject has a TypeNames collection that includes PowerShell-specific type names
+ // These are what the format system uses, not the .NET type name
+ // For example, Get-NetAdapter returns objects with TypeName like "Microsoft.Management.Infrastructure.CimInstance#ROOT/StandardCimv2/MSFT_NetAdapter"
+
+ foreach (var typeName in obj.TypeNames)
{
- Dictionary valuePairs = new Dictionary();
+ if (_formatCache.TryGetValue(typeName, out var cached))
+ return cached;
- foreach (var dataColumn in dataColumns)
+ var fvd = GetFormatViewDefinitionForType(typeName);
+ if (fvd != null)
{
- var expression = new PSPropertyExpression(ScriptBlock.Create(dataColumn.PropertyScriptAccessor));
+ return fvd;
+ }
+ }
- var result = expression.GetValues(ps).FirstOrDefault().Result;
+ // Fallback to base object type name
+ string? baseTypeName = obj.BaseObject.GetType().FullName;
+ if (baseTypeName is not null)
+ {
+ if (_formatCache.TryGetValue(baseTypeName, out var cached))
+ return cached;
- var stringValue = result?.ToString() ?? String.Empty;
+ return GetFormatViewDefinitionForType(baseTypeName);
+ }
- var isDecimal = decimal.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture.NumberFormat, out var decimalValue);
+ return null;
+ }
- if (isDecimal)
- {
- valuePairs[dataColumn.ToString()] = new DecimalValue { DisplayValue = stringValue, SortValue = decimalValue };
- }
- else
+ ///
+ /// Gets the default display property set (TableContent) for a PowerShell object.
+ /// This represents the subset of properties that PowerShell displays by default.
+ ///
+ /// The PowerShell object to examine.
+ /// A list of property names to display, or null if no default display set is defined.
+ private static List? GetDefaultDisplayPropertySet(PSObject obj)
+ {
+ try
+ {
+ // For CIM instances and other objects, PowerShell adds PSStandardMembers
+ // through the Extended Type System (ETS), not always as instance members
+
+ // First check instance members (for objects with runtime-added members)
+ var standardMembers = obj.Members["PSStandardMembers"]?.Value as PSMemberSet;
+ var defaultDisplayProperty = standardMembers?.Members["DefaultDisplayPropertySet"]?.Value as PSPropertySet;
+
+ if (defaultDisplayProperty?.ReferencedPropertyNames != null && defaultDisplayProperty.ReferencedPropertyNames.Count > 0)
+ {
+ return defaultDisplayProperty.ReferencedPropertyNames.ToList();
+ }
+
+ // Second, check PSObject.Properties for DefaultDisplayPropertySet
+ // Some objects have this defined through type adapters
+ var psStandardMembers = obj.Properties["PSStandardMembers"];
+ if (psStandardMembers?.Value is PSMemberSet memberSet)
+ {
+ var displayPropSet = memberSet.Members["DefaultDisplayPropertySet"] as PSPropertySet;
+ if (displayPropSet?.ReferencedPropertyNames != null && displayPropSet.ReferencedPropertyNames.Count > 0)
{
- var stringDecorated = new StringDecorated(stringValue);
- valuePairs[dataColumn.ToString()] = new StringValue { DisplayValue = stringDecorated.ToString(OutputRendering.PlainText) };
+ return displayPropSet.ReferencedPropertyNames.ToList();
}
}
-
- return new DataTableRow(valuePairs, objectIndex);
}
-
- private static void SetTypesOnDataColumns(List dataTableRows, List dataTableColumns)
+ catch
{
- var dataRows = dataTableRows.Select(x => x.Values);
+ // If we can't get the default display property set, return null
+ }
- foreach (var dataColumn in dataTableColumns)
+ return null;
+ }
+
+ ///
+ /// Retrieves the column definitions for the specified PowerShell objects based on their format view definitions or properties.
+ ///
+ /// The list of PowerShell objects to analyze.
+ /// If true, returns all properties instead of just the default display properties.
+ /// A distinct list of data table columns.
+ private List GetDataColumnsForObject(List psObjects, bool allProperties = false)
+ {
+ var dataColumns = new List();
+
+ if (psObjects.Count == 0) return dataColumns;
+
+ var firstObject = psObjects[0];
+
+ List labels;
+ List propertyAccessors;
+
+ // If allProperties is requested, skip format view and default display property logic
+ if (allProperties)
+ {
+ if (PSObjectIsPrimitive(firstObject))
+ {
+ // Handle primitive types
+ labels = [firstObject.BaseObject.GetType().Name];
+ propertyAccessors = ["$_"];
+ }
+ else
{
- dataColumn.StringType = typeof(decimal).FullName;
+ // Return all properties - use simple property access format for performance
+ // Filter out PS* metadata properties which are often expensive to compute
+ var properties = firstObject.Properties
+ .Where(p => p.IsGettable && !p.Name.StartsWith("PS"))
+ .ToList();
+
+ labels = properties.Select(p => p.Name).ToList();
+ propertyAccessors = properties.Select(p => $"$_.\"{p.Name}\"").ToList();
}
+ }
+ else
+ {
+ // Priority order:
+ // 1. Format view definition (from .ps1xml files) - what cmdlets like Get-NetAdapter use
+ // 2. DefaultDisplayPropertySet (TableContent) - used by custom objects
+ // 3. All properties (fallback)
+
+ var fvd = GetFormatViewDefinitionForObject(firstObject);
+ var defaultDisplayProps = GetDefaultDisplayPropertySet(firstObject);
- //If every value in a column could be a decimal, assume that it is supposed to be a decimal
- foreach (var dataRow in dataRows)
+ if (fvd?.Control is TableControl tableControl)
{
- foreach (var dataColumn in dataTableColumns)
+ // Use the table format definition (THIS IS WHAT GET-NETADAPTER USES)
+ var definedColumnLabels = tableControl.Headers.Select(h => h.Label).ToList();
+ var displayEntries = tableControl.Rows[0].Columns.Select(c => c.DisplayEntry).ToArray();
+ var propertyLabels = displayEntries.Select(de => de.Value).ToList();
+
+ // Use the TypeDefinition Label if available otherwise just use the property name as a label
+ labels = definedColumnLabels.Zip(propertyLabels, (definedLabel, propLabel) =>
{
- if (!(dataRow[dataColumn.ToString()] is DecimalValue))
- {
- dataColumn.StringType = typeof(string).FullName;
- }
- }
+ if (string.IsNullOrEmpty(definedLabel)) return propLabel;
+ return definedLabel;
+ }).ToList();
+
+ propertyAccessors = displayEntries.Select(de =>
+ de.ValueType == DisplayEntryValueType.Property
+ ? $"$_.\"{de.Value}\""
+ : de.Value // ScriptBlock
+ ).ToList();
+ }
+ else if (defaultDisplayProps != null && defaultDisplayProps.Count > 0)
+ {
+ // Use the DefaultDisplayPropertySet (for custom objects)
+ labels = defaultDisplayProps;
+ propertyAccessors = defaultDisplayProps.Select(p => $"$_.\"{p}\"").ToList();
+ }
+ else if (PSObjectIsPrimitive(firstObject))
+ {
+ // Handle primitive types
+ labels = [firstObject.BaseObject.GetType().Name];
+ propertyAccessors = ["$_"];
+ }
+ else
+ {
+ // Fallback to all properties
+ labels = firstObject.Properties.Select(p => p.Name).ToList();
+ propertyAccessors = firstObject.Properties.Select(p => $"$_.\"{p.Name}\"").ToList();
}
}
- private List GetDataColumnsForObject(List psObjects)
+
+ for (int i = 0; i < labels.Count; i++)
{
- var dataColumns = new List();
+ var column = new DataTableColumn(labels[i], propertyAccessors[i]);
+ dataColumns.Add(column);
+ }
+ return dataColumns.Distinct().ToList();
+ }
+ ///
+ /// Types that are considered primitives to PowerShell but not to C#.
+ ///
+ private static readonly List ADDITIONAL_PRIMITIVE_TYPES =
+ [
+ "System.String",
+ "System.Decimal",
+ "System.IntPtr",
+ "System.Security.SecureString",
+ "System.Numerics.BigInteger"
+ ];
+
+ ///
+ /// Determines whether the specified PowerShell object represents a primitive type.
+ ///
+ private static bool PSObjectIsPrimitive(PSObject ps)
+ {
+ var psBaseType = ps.BaseObject.GetType();
+ return psBaseType.IsPrimitive || psBaseType.IsEnum ||
+ ADDITIONAL_PRIMITIVE_TYPES.Contains(psBaseType.FullName!);
+ }
- foreach (PSObject obj in psObjects)
- {
- var labels = new List();
+ ///
+ /// Converts a PowerShell object to a data table row using PSPropertyExpression to evaluate property accessors.
+ ///
+ public static DataTableRow CastObjectToDataTableRow(PSObject psObject, List dataTableColumns, int objectIndex)
+ {
+ var valuePairs = new Dictionary();
- FormatViewDefinition fvd = GetFormatViewDefinitionForObject(obj);
+ foreach (var column in dataTableColumns)
+ {
+ object? result = null;
- var propertyAccessors = new List();
+ try
+ {
+ // PSPropertyExpression constructor takes a ScriptBlock for script expressions
+ // For simple properties, we need to extract just the property name
+ var accessor = column.PropertyScriptAccessor;
- if (fvd == null)
+ if (accessor.StartsWith("$_.\"") && accessor.EndsWith("\""))
{
- if (PSObjectIsPrimitive(obj))
- {
- labels = new List { obj.BaseObject.GetType().Name };
- propertyAccessors = new List { "$_" };
- }
- else
+ // Extract property name from "$_."PropertyName"" format
+ var propertyName = accessor.Substring(4, accessor.Length - 5);
+ var property = psObject.Properties[propertyName];
+ result = property?.Value;
+
+ // Unwrap PSObject if needed to get the base value
+ if (result is PSObject psObjResult)
{
- labels = obj.Properties.Select(x => x.Name).ToList();
- propertyAccessors = obj.Properties.Select(x => $"$_.\"{x.Name}\"").ToList();
+ result = psObjResult.BaseObject;
}
}
+ else if (accessor == "$_")
+ {
+ // The whole object
+ result = psObject.BaseObject;
+ }
else
{
- var tableControl = fvd.Control as TableControl;
+ // It's a script block - create and invoke it
+ var scriptBlock = ScriptBlock.Create(accessor);
+ var results = scriptBlock.InvokeWithContext(null, new List
+ {
+ new PSVariable("_", psObject)
+ });
+ result = results.FirstOrDefault();
- var definedColumnLabels = tableControl.Headers.Select(x => x.Label);
+ // Unwrap PSObject if needed
+ if (result is PSObject psScriptResult)
+ {
+ result = psScriptResult.BaseObject;
+ }
+ }
+ }
+ catch (Exception _)
+ {
+ // If evaluation fails, use null
+ result = null;
+ }
- var displayEntries = tableControl.Rows[0].Columns.Select(x => x.DisplayEntry);
+ // Convert to string and strip ANSI codes
+ var displayValue = result?.ToString() ?? string.Empty;
+ displayValue = StripAnsiCodes(displayValue);
- var propertyLabels = displayEntries.Select(x => x.Value);
+ // Determine if this is a numeric value for sorting
+ var isNumeric = result is decimal or int or long or short or byte or double or float or uint or ulong or ushort or sbyte;
- //Use the TypeDefinition Label if availble otherwise just use the property name as a label
- labels = definedColumnLabels.Zip(propertyLabels, (definedColumnLabel, propertyLabel) =>
- {
- if (String.IsNullOrEmpty(definedColumnLabel))
- {
- return propertyLabel;
- }
- return definedColumnLabel;
- }).ToList();
-
-
- propertyAccessors = displayEntries.Select(x =>
- {
- //If it's a propety access directly
- if (x.ValueType == DisplayEntryValueType.Property)
- {
- return $"$_.\"{x.Value}\"";
- }
- //Otherwise return access script
- return x.Value;
- }).ToList();
- }
+ var columnKey = column.ToString();
- for (var i = 0; i < labels.Count; i++)
+ if (isNumeric)
+ {
+ var decimalValue = Convert.ToDecimal(result, CultureInfo.InvariantCulture);
+ valuePairs[columnKey] = new DecimalValue
{
- dataColumns.Add(new DataTableColumn(labels[i], propertyAccessors[i]));
- }
+ DisplayValue = displayValue,
+ SortValue = decimalValue
+ };
+ }
+ else
+ {
+ valuePairs[columnKey] = new StringValue
+ {
+ DisplayValue = displayValue,
+ RawValue = result
+ };
}
- return dataColumns.Distinct().ToList();
}
- public DataTable CastObjectsToTableView(List psObjects)
- {
- List objectFormats = psObjects.Select(GetFormatViewDefinitionForObject).ToList();
+ return new DataTableRow(valuePairs, objectIndex);
+ }
- var dataTableColumns = GetDataColumnsForObject(psObjects);
+ ///
+ /// Sets the data type on each column based on the values in the data rows.
+ ///
+ private static void SetTypesOnDataColumns(List dataTableRows, List dataTableColumns)
+ {
+ var dataRows = dataTableRows.Select(x => x.Values);
+
+ foreach (var dataColumn in dataTableColumns)
+ dataColumn.StringType = typeof(decimal).FullName;
- List dataTableRows = new List();
- for (var i = 0; i < objectFormats.Count; i++)
+ // If every value in a column could be a decimal, assume that it is supposed to be a decimal
+ foreach (var dataRow in dataRows)
+ {
+ foreach (var dataColumn in dataTableColumns)
{
- var dataTableRow = CastObjectToDataTableRow(psObjects[i], dataTableColumns, i);
- dataTableRows.Add(dataTableRow);
+ if (dataRow[dataColumn.ToString()] is not DecimalValue)
+ dataColumn.StringType = typeof(string).FullName;
}
+ }
+ }
- SetTypesOnDataColumns(dataTableRows, dataTableColumns);
-
- return new DataTable(dataTableColumns, dataTableRows);
+ ///
+ /// Converts a list of PowerShell objects into a data table structure suitable for display in the grid view.
+ ///
+ /// The list of PowerShell objects to convert.
+ /// If true, includes all properties instead of just the default display properties.
+ public static DataTable CastObjectsToTableView(List psObjects, bool allProperties = false)
+ {
+ if (psObjects.Count == 0)
+ {
+ return new DataTable([], []);
}
+ // Get the columns using format view definitions
+ var typeGetter = new TypeGetter();
+ List dataTableColumns = typeGetter.GetDataColumnsForObject(psObjects, allProperties).ToList();
- //Types that are condisidered primitives to PowerShell but not C#
- private readonly static List additionalPrimitiveTypes = new List { "System.String",
- "System.Decimal",
- "System.IntPtr",
- "System.Security.SecureString",
- "System.Numerics.BigInteger"
- };
- private static bool PSObjectIsPrimitive(PSObject ps)
+ // Convert each object to a row
+ var dataTableRows = new List();
+ for (var i = 0; i < psObjects.Count; i++)
{
- var psBaseType = ps.BaseObject.GetType();
-
- return psBaseType.IsPrimitive || psBaseType.IsEnum || additionalPrimitiveTypes.Contains(psBaseType.FullName);
+ var dataTableRow = CastObjectToDataTableRow(psObjects[i], dataTableColumns, i);
+ dataTableRows.Add(dataTableRow);
}
+
+ SetTypesOnDataColumns(dataTableRows, dataTableColumns);
+
+ return new DataTable(dataTableColumns, dataTableRows);
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.OutGridView.Models/ApplicationData.cs b/src/Microsoft.PowerShell.OutGridView.Models/ApplicationData.cs
index 84959f5..0db0656 100644
--- a/src/Microsoft.PowerShell.OutGridView.Models/ApplicationData.cs
+++ b/src/Microsoft.PowerShell.OutGridView.Models/ApplicationData.cs
@@ -1,24 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using System;
using System.Collections.Generic;
-namespace OutGridView.Models
+namespace Microsoft.PowerShell.OutGridView.Models;
+
+///
+/// Represents the application configuration and state data for the Out-ConsoleGridView component.
+///
+public class ApplicationData
{
- public class ApplicationData
- {
- public string Title { get; set; }
- public OutputModeOption OutputMode { get; set; }
- public bool PassThru { get; set; }
- public string Filter { get; set; }
- public bool MinUI { get; set; }
- public DataTable DataTable { get; set; }
-
- public bool UseNetDriver { get; set; }
- public bool Verbose { get; set; }
- public bool Debug { get; set; }
-
- public string ModuleVersion { get; set; }
- }
-}
+ ///
+ /// Gets or sets the PowerShell objects to display.
+ ///
+ public List? PSObjects { get; set; }
+
+ ///
+ /// Gets or sets the output mode that determines how items can be selected and returned.
+ ///
+ public OutputModeOption OutputMode { get; set; }
+
+ ///
+ /// Gets or sets the title displayed in the Out-GridView window.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the filter text to apply to the data.
+ ///
+ public string? Filter { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to use minimal UI mode.
+ ///
+ public bool MinUI { get; set; }
+
+ ///
+ /// Gets or sets the driver to use for rendering.
+ ///
+ public string? ForceDriver { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether all properties should be displayed. If false, only default display
+ /// properties are shown.
+ ///
+ public bool AllProperties { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether verbose output is enabled.
+ ///
+ public bool Verbose { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether debug output is enabled.
+ ///
+ public bool Debug { get; set; }
+
+ ///
+ /// Gets or sets the version of the module.
+ ///
+ public string? ModuleVersion { get; set; }
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.OutGridView.Models/DataTable.cs b/src/Microsoft.PowerShell.OutGridView.Models/DataTable.cs
index 950a7e5..3260700 100644
--- a/src/Microsoft.PowerShell.OutGridView.Models/DataTable.cs
+++ b/src/Microsoft.PowerShell.OutGridView.Models/DataTable.cs
@@ -2,18 +2,32 @@
// Licensed under the MIT License.
using System.Collections.Generic;
-using System.Collections.ObjectModel;
-namespace OutGridView.Models
+
+namespace Microsoft.PowerShell.OutGridView.Models;
+
+///
+/// Represents a data table containing rows and columns for display in the grid view.
+///
+public class DataTable
{
- public class DataTable
- {
- public List Data { get; set; }
- public List DataColumns { get; set; }
- public DataTable(List columns, List data)
- {
- DataColumns = columns;
+ ///
+ /// Gets or sets the list of data rows in the table.
+ ///
+ public List Data { get; set; }
- Data = data;
- }
+ ///
+ /// Gets or sets the list of column definitions for the table.
+ ///
+ public List DataColumns { get; set; }
+
+ ///
+ /// Initializes a new instance of the class with the specified columns and data.
+ ///
+ /// The list of column definitions for the table.
+ /// The list of data rows for the table.
+ public DataTable(List columns, List data)
+ {
+ DataColumns = columns;
+ Data = data;
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.OutGridView.Models/DataTableColumn.cs b/src/Microsoft.PowerShell.OutGridView.Models/DataTableColumn.cs
index 0390dc4..a2e70a3 100644
--- a/src/Microsoft.PowerShell.OutGridView.Models/DataTableColumn.cs
+++ b/src/Microsoft.PowerShell.OutGridView.Models/DataTableColumn.cs
@@ -2,39 +2,114 @@
// Licensed under the MIT License.
using System;
-using Newtonsoft.Json;
using System.Text;
+using Newtonsoft.Json;
+
+// TODO: switch to System.Text.Json
-namespace OutGridView.Models
+namespace Microsoft.PowerShell.OutGridView.Models;
+
+///
+/// Represents a column in a data table with metadata about its label, type, and property accessor.
+///
+/// The display label for the column.
+/// The script accessor used to retrieve the property value.
+public class DataTableColumn(string label, string propertyScriptAccessor)
{
- public class DataTableColumn
+ ///
+ /// Gets the runtime type of the column based on the property.
+ ///
+ [JsonIgnore]
+ public Type? Type => System.Type.GetType(StringType!);
+
+ ///
+ /// Gets the display label for the column.
+ ///
+ public string Label { get; } = label;
+
+ ///
+ /// Gets or sets the serializable string representation of the column's type.
+ ///
+ public string? StringType { get; set; }
+
+ ///
+ /// Gets or sets the format string used to format values in this column.
+ /// This follows PowerShell/. NET composite formatting conventions (e.g., "N0" for numbers, "G" for DateTime).
+ ///
+ public string? FormatString { get; set; }
+
+ ///
+ /// Gets the script accessor used to retrieve the property value for this column.
+ ///
+ public string PropertyScriptAccessor { get; } = propertyScriptAccessor;
+
+ ///
+ /// Formats a value according to this column's format specification.
+ ///
+ /// The value to format.
+ /// A formatted string representation of the value.
+ public string FormatValue(object? value)
{
- [JsonIgnore]
- public Type Type => Type.GetType(StringType);
- public string Label { get; set; }
- //Serializable Version of Type
- public string StringType { get; set; }
- public string PropertyScriptAccessor { get; set; }
- public DataTableColumn(string label, string propertyScriptAccessor)
- {
- Label = label;
- PropertyScriptAccessor = propertyScriptAccessor;
- }
+ if (value == null) return string.Empty;
- //Distinct column defined by Label, Prop Accessor
- public override bool Equals(object obj)
+ // If we have a format string, try to use it
+ if (!string.IsNullOrEmpty(FormatString) && value is IFormattable formattable)
{
- DataTableColumn b = obj as DataTableColumn;
- return b.Label == Label && b.PropertyScriptAccessor == PropertyScriptAccessor;
+ try
+ {
+ return formattable.ToString(FormatString, System.Globalization.CultureInfo.CurrentCulture);
+ }
+ catch
+ {
+ // Fall through to default formatting if format string is invalid
+ }
}
- public override int GetHashCode()
+
+ // If FormatString is explicitly null, use simple ToString for most types
+ // (this prevents unwanted formatting of identifier integers like ProcessId)
+ if (FormatString == null)
{
- return Label.GetHashCode() + PropertyScriptAccessor.GetHashCode();
+ return value.ToString() ?? string.Empty;
}
- public override string ToString()
+
+ // Default formatting based on type (only when FormatString is empty but not null)
+ return value switch
{
- //Needs to be encoded to embed safely in xaml
- return Convert.ToBase64String(Encoding.UTF8.GetBytes(Label + PropertyScriptAccessor));
- }
+ DateTime dt => dt.ToString("G", System.Globalization.CultureInfo.CurrentCulture),
+ decimal d => d.ToString("N0", System.Globalization.CultureInfo.CurrentCulture),
+ double db => db.ToString("N2", System.Globalization.CultureInfo.CurrentCulture),
+ float f => f.ToString("N2", System.Globalization.CultureInfo.CurrentCulture),
+ int or long or short or byte => string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:N0}", value),
+ _ => value.ToString() ?? string.Empty
+ };
}
-}
+
+ ///
+ /// Determines whether the specified object is equal to the current column.
+ /// Two columns are considered equal if they have the same label and property script accessor.
+ ///
+ /// The object to compare with the current column.
+ ///
+ /// if the specified object is equal to the current column; otherwise,
+ /// .
+ ///
+ public override bool Equals(object? obj)
+ {
+ var b = obj as DataTableColumn;
+ return b?.Label == Label && b.PropertyScriptAccessor == PropertyScriptAccessor;
+ }
+
+ ///
+ /// Returns the hash code for this column based on its label and property script accessor.
+ ///
+ /// A hash code for the current column.
+ public override int GetHashCode() => Label.GetHashCode() + PropertyScriptAccessor.GetHashCode();
+
+ ///
+ /// Returns a Base64-encoded string representation of the column for safe embedding in XAML.
+ ///
+ /// A Base64-encoded string containing the label and property script accessor.
+ public override string ToString() =>
+ // Needs to be encoded to embed safely in XAML
+ Convert.ToBase64String(Encoding.UTF8.GetBytes(Label + PropertyScriptAccessor));
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.OutGridView.Models/DataTableRow.cs b/src/Microsoft.PowerShell.OutGridView.Models/DataTableRow.cs
index 3888691..5eb958d 100644
--- a/src/Microsoft.PowerShell.OutGridView.Models/DataTableRow.cs
+++ b/src/Microsoft.PowerShell.OutGridView.Models/DataTableRow.cs
@@ -4,44 +4,118 @@
using System;
using System.Collections.Generic;
-namespace OutGridView.Models
+namespace Microsoft.PowerShell.OutGridView.Models;
+
+///
+/// Represents a value that can be displayed and compared in a data table.
+///
+public interface IValue : IComparable
{
- public interface IValue : IComparable
- {
- string DisplayValue { get; set; }
- }
- public class DecimalValue : IValue
- {
- public string DisplayValue { get; set; }
- public decimal SortValue { get; set; }
-
- public int CompareTo(object obj)
- {
- DecimalValue otherDecimalValue = obj as DecimalValue;
- if (otherDecimalValue == null) return 1;
- return Decimal.Compare(SortValue, otherDecimalValue.SortValue);
- }
- }
- public class StringValue : IValue
- {
- public string DisplayValue { get; set; }
- public int CompareTo(object obj)
- {
- StringValue otherStringValue = obj as StringValue;
- if (otherStringValue == null) return 1;
- return DisplayValue.CompareTo(otherStringValue.DisplayValue);
- }
- }
- public class DataTableRow
+ ///
+ /// Gets or sets the string representation of the value for display purposes.
+ ///
+ string DisplayValue { get; set; }
+
+ ///
+ /// Gets the original object value before formatting.
+ ///
+ object? OriginalValue { get; }
+}
+
+///
+/// Represents a decimal value in a data table with support for numeric sorting.
+///
+public class DecimalValue : IValue
+{
+ ///
+ /// Gets or sets the string representation of the decimal value for display purposes.
+ ///
+ public required string DisplayValue { get; set; }
+
+ ///
+ /// Gets or sets the decimal value used for sorting.
+ ///
+ public decimal SortValue { get; set; }
+
+ ///
+ /// Gets the original decimal value.
+ ///
+ public object? OriginalValue => SortValue;
+
+ ///
+ /// Compares the current instance with another object of the same type.
+ ///
+ /// An object to compare with this instance.
+ ///
+ /// A value that indicates the relative order of the objects being compared.
+ /// Less than zero if this instance precedes ,
+ /// zero if they are equal, or greater than zero if this instance follows .
+ /// Returns 1 if is not a .
+ ///
+ public int CompareTo(object? obj) => obj is not DecimalValue otherDecimalValue
+ ? 1
+ : decimal.Compare(SortValue, otherDecimalValue.SortValue);
+}
+
+///
+/// Represents a string value in a data table with support for string sorting.
+///
+public class StringValue : IValue
+{
+ ///
+ /// Gets or sets the string value for display and sorting purposes.
+ ///
+ public required string DisplayValue { get; set; }
+
+ ///
+ /// Gets or sets the original object value before conversion to string.
+ ///
+ public object? RawValue { get; set; }
+
+ ///
+ /// Gets the original object value.
+ ///
+ public object? OriginalValue => RawValue ?? DisplayValue;
+
+ ///
+ /// Compares the current instance with another object of the same type.
+ ///
+ /// An object to compare with this instance.
+ ///
+ /// A value that indicates the relative order of the objects being compared.
+ /// Less than zero if this instance precedes ,
+ /// zero if they are equal, or greater than zero if this instance follows .
+ /// Returns 1 if is not a .
+ ///
+ public int CompareTo(object? obj) => obj is not StringValue otherStringValue
+ ? 1
+ : string.Compare(DisplayValue, otherStringValue.DisplayValue, StringComparison.Ordinal);
+}
+
+///
+/// Represents a single row in a data table with values mapped to column identifiers.
+///
+public class DataTableRow
+{
+ ///
+ /// Gets or sets the dictionary of values for this row, keyed by the column identifier.
+ /// The key is the data column hash code serialized as a string for JSON compatibility.
+ ///
+ public Dictionary Values { get; set; }
+
+ ///
+ /// Gets or sets the original index of the object in the source collection before any transformations.
+ ///
+ public int OriginalObjectIndex { get; set; }
+
+ ///
+ /// Initializes a new instance of the class with the specified values and original index.
+ ///
+ /// The dictionary of values for this row, keyed by column identifier.
+ /// The original index of the object in the source collection.
+ public DataTableRow(Dictionary data, int originalObjectIndex)
{
- //key is datacolumn hash code
- //have to do it this way because JSON can't serialize objects as keys
- public Dictionary Values { get; set; }
- public int OriginalObjectIndex { get; set; }
- public DataTableRow(Dictionary data, int originalObjectIndex)
- {
- Values = data;
- OriginalObjectIndex = originalObjectIndex;
- }
+ Values = data;
+ OriginalObjectIndex = originalObjectIndex;
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.OutGridView.Models/Microsoft.PowerShell.OutGridView.Models.csproj b/src/Microsoft.PowerShell.OutGridView.Models/Microsoft.PowerShell.OutGridView.Models.csproj
index a4f10f5..102f7df 100644
--- a/src/Microsoft.PowerShell.OutGridView.Models/Microsoft.PowerShell.OutGridView.Models.csproj
+++ b/src/Microsoft.PowerShell.OutGridView.Models/Microsoft.PowerShell.OutGridView.Models.csproj
@@ -1,9 +1,12 @@
- net8.0
+ net10.0
+ latest
+ enable
+
diff --git a/src/Microsoft.PowerShell.OutGridView.Models/OutputModeOptions.cs b/src/Microsoft.PowerShell.OutGridView.Models/OutputModeOptions.cs
index 3170c98..bc499b8 100644
--- a/src/Microsoft.PowerShell.OutGridView.Models/OutputModeOptions.cs
+++ b/src/Microsoft.PowerShell.OutGridView.Models/OutputModeOptions.cs
@@ -1,23 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-namespace OutGridView.Models
+namespace Microsoft.PowerShell.OutGridView.Models;
+
+public enum OutputModeOption
{
- public enum OutputModeOption
- {
- ///
- /// None is the default and it means OK and Cancel will not be present
- /// and no objects will be written to the pipeline.
- /// The selectionMode of the actual list will still be multiple.
- ///
- None,
- ///
- /// Allow selection of one single item to be written to the pipeline.
- ///
- Single,
- ///
- ///Allow select of multiple items to be written to the pipeline.
- ///
- Multiple
- }
-}
+ ///
+ /// None is the default, and it means OK and Cancel will not be present
+ /// and no objects will be written to the pipeline.
+ /// The selectionMode of the actual list will still be multiple.
+ ///
+ None,
+
+ ///
+ /// Allow selection of one single item to be written to the pipeline.
+ ///
+ Single,
+
+ ///
+ /// Allow select of multiple items to be written to the pipeline.
+ ///
+ Multiple
+}
\ No newline at end of file
diff --git a/src/Microsoft.PowerShell.OutGridView.Models/Serializers.cs b/src/Microsoft.PowerShell.OutGridView.Models/Serializers.cs
index 6165c23..f5f8a53 100644
--- a/src/Microsoft.PowerShell.OutGridView.Models/Serializers.cs
+++ b/src/Microsoft.PowerShell.OutGridView.Models/Serializers.cs
@@ -1,32 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using Newtonsoft.Json;
using System;
using System.Text;
-using System.Collections.Generic;
-//TODO: swich to JSON.NET
+using Newtonsoft.Json;
+
+// TODO: switch to JSON.NET
+// BUGBUG: This appears to be unused code. Consider removing it.
-namespace OutGridView.Models
+namespace Microsoft.PowerShell.OutGridView.Models
{
public class Serializers
{
- private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings()
+ private static readonly JsonSerializerSettings JSON_SERIALIZER_SETTINGS = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
};
public static string ObjectToJson(T obj)
{
- var jsonString = JsonConvert.SerializeObject(obj, jsonSerializerSettings);
+ var jsonString = JsonConvert.SerializeObject(obj, JSON_SERIALIZER_SETTINGS);
return ToBase64String(jsonString);
}
- public static T ObjectFromJson(string base64Json)
+ public static T? ObjectFromJson(string base64Json)
{
var jsonString = FromBase64String(base64Json);
- return JsonConvert.DeserializeObject(jsonString, jsonSerializerSettings);
+ return JsonConvert.DeserializeObject(jsonString, JSON_SERIALIZER_SETTINGS);
}
diff --git a/tools/ide/.vscode/settings.json b/tools/ide/.vscode/settings.json
new file mode 100644
index 0000000..23830fb
--- /dev/null
+++ b/tools/ide/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "git.ignoreLimitWarning": true
+}
diff --git a/tools/ide/launchSettings.json b/tools/ide/launchSettings.json
new file mode 100644
index 0000000..10a0e5b
--- /dev/null
+++ b/tools/ide/launchSettings.json
@@ -0,0 +1,46 @@
+{
+ "profiles": {
+ "OCGV": {
+ "commandName": "Executable",
+ "executablePath": "pwsh.exe",
+ "commandLineArgs": "-NoProfile -NoLogo -NoExit -Command \"& { Import-Module '$(TargetPath)'; Get-Process | Out-ConsoleGridView -Debug }\"",
+ "workingDirectory": "$(TargetDir)"
+ },
+ "OCGV -Filter": {
+ "commandName": "Executable",
+ "executablePath": "pwsh.exe",
+ "commandLineArgs": "-NoProfile -NoLogo -NoExit -Command \"& { Import-Module '$(TargetPath)'; Get-Process | Select-Object ProcessName, Id, CPU | Out-ConsoleGridView -Debug -Filter com }\"",
+ "workingDirectory": "$(TargetDir)"
+ },
+ "OCGV -MinUi": {
+ "commandName": "Executable",
+ "executablePath": "pwsh.exe",
+ "commandLineArgs": "-NoProfile -NoLogo -NoExit -Command \"& { Import-Module '$(TargetPath)'; Get-Process | Out-ConsoleGridView -MinUi }\"",
+ "workingDirectory": "$(TargetDir)"
+ },
+ "OCGV -AllProperties": {
+ "commandName": "Executable",
+ "executablePath": "pwsh.exe",
+ "commandLineArgs": "-NoProfile -NoLogo -NoExit -Command \"& { Import-Module '$(TargetPath)'; Get-Process | Out-ConsoleGridView -AllProperties }\"",
+ "workingDirectory": "$(TargetDir)"
+ },
+ "OCGV Select-Object": {
+ "commandName": "Executable",
+ "executablePath": "pwsh.exe",
+ "commandLineArgs": "-NoProfile -NoLogo -NoExit -Command \"& { Import-Module '$(TargetPath)'; Get-Process | Select-Object ProcessName, Id, Handles, NPM, PM, WS, CPU | Out-ConsoleGridView }\"",
+ "workingDirectory": "$(TargetDir)"
+ },
+ "gci | OCGV": {
+ "commandName": "Executable",
+ "executablePath": "pwsh.exe",
+ "commandLineArgs": "-NoProfile -NoLogo -NoExit -Command \"& { Import-Module '$(TargetPath)'; gci | Out-ConsoleGridView -OutputMode: Single }\"",
+ "workingDirectory": "$(TargetDir)"
+ },
+ "SHOT": {
+ "commandName": "Executable",
+ "executablePath": "pwsh.exe",
+ "commandLineArgs": "-NoProfile -NoLogo -NoExit -Command \"& { Import-Module '$(TargetPath)'; Get-Process | Show-ObjectTree }\"",
+ "workingDirectory": "$(TargetDir)"
+ }
+ }
+}
diff --git a/tools/initDevEnvironment.ps1 b/tools/initDevEnvironment.ps1
new file mode 100644
index 0000000..488e054
--- /dev/null
+++ b/tools/initDevEnvironment.ps1
@@ -0,0 +1,82 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+.SYNOPSIS
+ Initializes the development environment for GraphicalTools.
+.DESCRIPTION
+ Creates IDE support files (.sln, launchSettings.json, .vscode/settings.json)
+ that are .gitignored per the PowerShell team convention. Run this after cloning
+ the repository to enable Visual Studio and VS Code development/debugging.
+
+ The template files are stored in tools/ide/ and copied to their expected locations.
+#>
+
+param(
+ [switch]$Force
+)
+
+$ErrorActionPreference = 'Stop'
+Push-Location $PSScriptRoot/..
+
+try {
+ $templateDir = "tools/ide"
+
+ # --- Solution file ---
+ $slnPath = "GraphicalTools.sln"
+ if ($Force -or -not (Test-Path $slnPath)) {
+ Write-Host "Creating $slnPath..." -ForegroundColor Cyan
+ Remove-Item -Force GraphicalTools.slnx -ErrorAction SilentlyContinue
+ & dotnet new sln --name GraphicalTools --force --format sln | Out-Null
+ & dotnet sln add src/Microsoft.PowerShell.ConsoleGuiTools/Microsoft.PowerShell.ConsoleGuiTools.csproj --solution-folder src
+ & dotnet sln add src/Microsoft.PowerShell.OutGridView.Models/Microsoft.PowerShell.OutGridView.Models.csproj --solution-folder src
+ Write-Host " Created $slnPath" -ForegroundColor Green
+ } else {
+ Write-Host "Skipping $slnPath (already exists, use -Force to overwrite)" -ForegroundColor Yellow
+ }
+
+ # --- Visual Studio launch profiles ---
+ $launchDest = "src/Microsoft.PowerShell.ConsoleGuiTools/Properties/launchSettings.json"
+ if ($Force -or -not (Test-Path $launchDest)) {
+ Write-Host "Creating $launchDest..." -ForegroundColor Cyan
+ New-Item -ItemType Directory -Force (Split-Path $launchDest) | Out-Null
+
+ # Read template and replace pwsh.exe placeholder with actual path
+ $pwshPreview = "C:\Program Files\PowerShell\7-preview\pwsh.exe"
+ $pwshStable = "C:\Program Files\PowerShell\7\pwsh.exe"
+ if (Test-Path $pwshPreview) {
+ $pwsh = $pwshPreview
+ } elseif (Test-Path $pwshStable) {
+ $pwsh = $pwshStable
+ } else {
+ $pwsh = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
+ if (-not $pwsh) {
+ Write-Warning "Could not find pwsh.exe. Launch profiles will need manual path correction."
+ $pwsh = "pwsh.exe"
+ }
+ }
+
+ $json = Get-Content "$templateDir/launchSettings.json" -Raw
+ $json = $json.Replace('"pwsh.exe"', "`"$($pwsh.Replace('\', '\\'))`"")
+ Set-Content -Path $launchDest -Value $json
+
+ Write-Host " Created $launchDest (using $pwsh)" -ForegroundColor Green
+ } else {
+ Write-Host "Skipping $launchDest (already exists, use -Force to overwrite)" -ForegroundColor Yellow
+ }
+
+ # --- VS Code settings ---
+ $vscodeDest = ".vscode/settings.json"
+ if ($Force -or -not (Test-Path $vscodeDest)) {
+ Write-Host "Creating $vscodeDest..." -ForegroundColor Cyan
+ New-Item -ItemType Directory -Force .vscode | Out-Null
+ Copy-Item "$templateDir/.vscode/settings.json" $vscodeDest
+ Write-Host " Created $vscodeDest" -ForegroundColor Green
+ } else {
+ Write-Host "Skipping $vscodeDest (already exists, use -Force to overwrite)" -ForegroundColor Yellow
+ }
+
+ Write-Host "`nDev environment initialized! Open GraphicalTools.sln in Visual Studio or the root folder in VS Code." -ForegroundColor Green
+} finally {
+ Pop-Location
+}