Skip to content

Commit 2fa7898

Browse files
tigCopilot
andcommitted
Add Out-ConsoleTableView with native multi-selection and streaming pipeline
- Use TableView's native multi-selection instead of custom mark column - Stream objects to UI as they arrive from the pipeline (async loading) - Show spinner and row count during loading, stop on pipeline complete - Determine columns from first object, add rows incrementally - UI starts on background thread, signaled ready via OnIsRunningChanged Co-authored-by: Copilot <[email protected]>
1 parent 724affc commit 2fa7898

5 files changed

Lines changed: 766 additions & 4 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Management.Automation;
7+
using System.Threading;
8+
using Microsoft.PowerShell.OutGridView.Models;
9+
using Terminal.Gui.App;
10+
using Terminal.Gui.Configuration;
11+
12+
namespace Microsoft.PowerShell.ConsoleGuiTools;
13+
14+
/// <summary>
15+
/// Provides the main orchestration for Out-ConsoleTableView, managing the Terminal.Gui
16+
/// application lifecycle. Supports both batch (all objects upfront) and streaming
17+
/// (objects arrive incrementally from the pipeline) modes.
18+
/// </summary>
19+
internal sealed class OutConsoleTableView : IDisposable
20+
{
21+
private readonly List<PSObject> _psObjects = [];
22+
private readonly TypeGetter _typeGetter = new();
23+
private readonly ManualResetEventSlim _uiRunning = new(false);
24+
private IApplication? _app;
25+
private ApplicationData? _applicationData;
26+
private List<DataTableColumn>? _columns;
27+
private OutTableViewDataSource? _dataSource;
28+
private int _objectIndex;
29+
private HashSet<int>? _result;
30+
private Thread? _uiThread;
31+
private OutTableViewWindow? _window;
32+
33+
public void Dispose()
34+
{
35+
_uiRunning.Dispose();
36+
}
37+
38+
/// <summary>
39+
/// Initializes the streaming session. Call this once before feeding objects.
40+
/// </summary>
41+
public void Initialize(ApplicationData applicationData)
42+
{
43+
_applicationData = applicationData;
44+
}
45+
46+
/// <summary>
47+
/// Adds an object from the pipeline. On the first object, starts the UI on a background thread.
48+
/// </summary>
49+
public void AddObject(PSObject psObject)
50+
{
51+
_psObjects.Add(psObject);
52+
53+
if (_columns == null)
54+
{
55+
// First object: determine columns, add first row, then start UI
56+
_columns = _typeGetter.GetDataColumnsForObject(psObject);
57+
_dataSource = new OutTableViewDataSource(_columns);
58+
var row = TypeGetter.CastObjectToDataTableRow(psObject, _columns, _objectIndex++);
59+
_dataSource.AddRow(row);
60+
StartUi();
61+
_uiRunning.Wait();
62+
}
63+
else
64+
{
65+
var row = TypeGetter.CastObjectToDataTableRow(psObject, _columns, _objectIndex++);
66+
_dataSource!.AddRow(row);
67+
}
68+
69+
// Notify UI thread of new data
70+
_app?.Invoke(() => _window?.OnDataChanged());
71+
}
72+
73+
/// <summary>
74+
/// Signals that the pipeline is complete and waits for the UI to finish.
75+
/// Returns the selected indexes.
76+
/// </summary>
77+
public HashSet<int> Complete()
78+
{
79+
if (_window == null)
80+
return [];
81+
82+
_app?.Invoke(() => _window.OnPipelineComplete());
83+
_uiThread?.Join();
84+
return _result ?? [];
85+
}
86+
87+
/// <summary>
88+
/// Gets the PSObject at the given original index (for output).
89+
/// </summary>
90+
public PSObject GetObject(int index)
91+
{
92+
return _psObjects[index];
93+
}
94+
95+
private void StartUi()
96+
{
97+
_uiThread = new Thread(() =>
98+
{
99+
ConfigurationManager.Enable(ConfigLocations.All);
100+
101+
_window = new OutTableViewWindow(_applicationData!, _dataSource!)
102+
{
103+
OnRunning = () => _uiRunning.Set()
104+
};
105+
_app = Application.Create().Init(_applicationData!.Driver);
106+
_result = _app.Run(_window) as HashSet<int>;
107+
_app.Dispose();
108+
})
109+
{
110+
IsBackground = true,
111+
Name = "OutConsoleTableView-UI"
112+
};
113+
_uiThread.Start();
114+
}
115+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.Management.Automation;
8+
using System.Management.Automation.Internal;
9+
using Microsoft.PowerShell.OutGridView.Models;
10+
11+
namespace Microsoft.PowerShell.ConsoleGuiTools;
12+
13+
/// <summary>
14+
/// Sends output to an interactive table using Terminal.Gui's TableView.
15+
/// This cmdlet uses the same pipeline model as Out-ConsoleGridView (collecting objects
16+
/// in ProcessRecord, displaying in EndProcessing) but renders with TableView for
17+
/// native column headers, sizing, and horizontal scrolling.
18+
/// </summary>
19+
[Cmdlet(VerbsData.Out, "ConsoleTableView")]
20+
[Alias("octv")]
21+
public class OutConsoleTableViewCmdletCommand : PSCmdlet, IDisposable
22+
{
23+
#region Properties
24+
25+
private const string DATA_NOT_QUALIFIED_FOR_TABLE_VIEW = nameof(DATA_NOT_QUALIFIED_FOR_TABLE_VIEW);
26+
private const string ENVIRONMENT_NOT_SUPPORTED_FOR_TABLE_VIEW = nameof(ENVIRONMENT_NOT_SUPPORTED_FOR_TABLE_VIEW);
27+
28+
private readonly OutConsoleTableView _outConsoleTableView = new();
29+
private bool _initialized;
30+
31+
#endregion Properties
32+
33+
#region Input Parameters
34+
35+
/// <summary>
36+
/// Gets or sets the current pipeline object.
37+
/// </summary>
38+
[Parameter(ValueFromPipeline = true, HelpMessage = "Specifies the input pipeline object")]
39+
public PSObject InputObject { get; set; } = AutomationNull.Value;
40+
41+
/// <summary>
42+
/// Gets or sets the title of the Out-ConsoleTableView window.
43+
/// </summary>
44+
[Parameter(HelpMessage =
45+
"Specifies the text that appears in the title bar of the Out-ConsoleTableView window. By default, the title bar displays the command that invokes Out-ConsoleTableView.")]
46+
[ValidateNotNullOrEmpty]
47+
public string? Title { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets a value indicating whether the selected items should be written to the pipeline
51+
/// and if it should be possible to select multiple or single list items.
52+
/// </summary>
53+
[Parameter(HelpMessage =
54+
"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.")]
55+
public OutputModeOption OutputMode { set; get; } = OutputModeOption.Multiple;
56+
57+
/// <summary>
58+
/// Gets or sets the initial value for the filter in the TUI.
59+
/// </summary>
60+
[Parameter(HelpMessage =
61+
"Pre-populates the Filter edit box, allowing filtering to be specified on the command line. The filter uses regular expressions.")]
62+
public string? Filter { set; get; }
63+
64+
/// <summary>
65+
/// Gets or sets a value indicating whether "minimum UI" mode will be enabled.
66+
/// </summary>
67+
[Parameter(HelpMessage = "If specified no window frame, filter box, or status bar will be displayed in the TUI.")]
68+
public SwitchParameter MinUI { set; get; }
69+
70+
/// <summary>
71+
/// Gets or sets the Terminal.Gui driver to use.
72+
/// </summary>
73+
[Parameter(HelpMessage =
74+
"Forces the Terminal.Gui driver to use. Valid values are 'ansi', 'windows', or 'unix'.")]
75+
public string? ForceDriver { set; get; }
76+
77+
/// <summary>
78+
/// Gets a value indicating whether the Verbose switch is present.
79+
/// </summary>
80+
public bool Verbose => MyInvocation.BoundParameters.ContainsKey("Verbose");
81+
82+
/// <summary>
83+
/// Gets a value indicating whether the Debug switch is present.
84+
/// </summary>
85+
public bool Debug => MyInvocation.BoundParameters.ContainsKey("Debug");
86+
87+
#endregion Input Parameters
88+
89+
/// <summary>
90+
/// Validates that the environment supports the table view.
91+
/// </summary>
92+
protected override void BeginProcessing()
93+
{
94+
if (Console.IsInputRedirected)
95+
{
96+
var error = new ErrorRecord(
97+
new PSNotSupportedException("Not supported in this environment (when input is redirected)."),
98+
ENVIRONMENT_NOT_SUPPORTED_FOR_TABLE_VIEW,
99+
ErrorCategory.NotImplemented,
100+
null);
101+
102+
ThrowTerminatingError(error);
103+
}
104+
}
105+
106+
/// <summary>
107+
/// Processes each input object received from the pipeline.
108+
/// </summary>
109+
protected override void ProcessRecord()
110+
{
111+
if (Equals(InputObject, AutomationNull.Value)) return;
112+
113+
if (InputObject.BaseObject is IDictionary dictionary)
114+
// Dictionaries should be enumerated through because the pipeline does not enumerate through them.
115+
foreach (DictionaryEntry entry in dictionary)
116+
ProcessObject(PSObject.AsPSObject(entry));
117+
else
118+
ProcessObject(InputObject);
119+
}
120+
121+
private void ProcessObject(PSObject input)
122+
{
123+
var baseObject = input.BaseObject;
124+
125+
// Throw a terminating error for types that are not supported.
126+
if (baseObject is ScriptBlock ||
127+
baseObject is SwitchParameter ||
128+
baseObject is PSReference ||
129+
baseObject is PSObject)
130+
{
131+
var error = new ErrorRecord(
132+
new FormatException("Invalid data type for Out-ConsoleTableView"),
133+
DATA_NOT_QUALIFIED_FOR_TABLE_VIEW,
134+
ErrorCategory.InvalidType,
135+
null);
136+
137+
ThrowTerminatingError(error);
138+
}
139+
140+
if (!_initialized)
141+
{
142+
_initialized = true;
143+
var applicationData = new ApplicationData
144+
{
145+
Title = Title ?? "Out-ConsoleTableView",
146+
OutputMode = OutputMode,
147+
Filter = Filter,
148+
MinUI = MinUI,
149+
Driver = ForceDriver,
150+
Verbose = Verbose,
151+
Debug = Debug,
152+
ModuleVersion = MyInvocation.MyCommand.Version.ToString()
153+
};
154+
_outConsoleTableView.Initialize(applicationData);
155+
}
156+
157+
_outConsoleTableView.AddObject(input);
158+
}
159+
160+
/// <summary>
161+
/// Performs final processing after all pipeline objects have been received.
162+
/// Signals the UI that loading is complete and writes selected objects to the pipeline.
163+
/// </summary>
164+
protected override void EndProcessing()
165+
{
166+
base.EndProcessing();
167+
168+
if (!_initialized) return;
169+
170+
HashSet<int> selectedIndexes = _outConsoleTableView.Complete();
171+
foreach (var idx in selectedIndexes)
172+
{
173+
WriteObject(_outConsoleTableView.GetObject(idx), false);
174+
}
175+
}
176+
177+
/// <summary>
178+
/// Releases all resources.
179+
/// </summary>
180+
public void Dispose()
181+
{
182+
_outConsoleTableView.Dispose();
183+
GC.SuppressFinalize(this);
184+
}
185+
}

0 commit comments

Comments
 (0)