Skip to content

Commit 59e1cf7

Browse files
committed
merge
1 parent 3efc919 commit 59e1cf7

File tree

2 files changed

+375
-0
lines changed

2 files changed

+375
-0
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using NStack;
9+
using OutGridView.Models;
10+
using Terminal.Gui;
11+
12+
namespace OutGridView.Cmdlet
13+
{
14+
internal class ConsoleGui : IDisposable
15+
{
16+
private const string FILTER_LABEL = "Filter";
17+
// This adjusts the left margin of all controls
18+
<<<<<<< HEAD
19+
private const int MARGIN_LEFT = 2;
20+
// Width of Terminal.Gui ListView selection/check UI elements (old == 4, new == 2)
21+
private const int CHECK_WIDTH = 4;
22+
=======
23+
private const int FILTER_LABEL_X = 2;
24+
>>>>>>> 474826e (made column spacing tigher)
25+
private bool _cancelled;
26+
private GridViewDataSource _itemSource;
27+
private Label _filterLabel;
28+
private TextField _filterField;
29+
private ListView _listView;
30+
private ApplicationData _applicationData;
31+
private GridViewDetails _gridViewDetails;
32+
33+
public HashSet<int> Start(ApplicationData applicationData)
34+
{
35+
Application.Init();
36+
_applicationData = applicationData;
37+
_gridViewDetails = new GridViewDetails
38+
{
39+
// If OutputMode is Single or Multiple, then we make items selectable. If we make them selectable,
40+
// 2 columns are required for the check/selection indicator and space.
41+
<<<<<<< HEAD
42+
ListViewOffset = _applicationData.OutputMode != OutputModeOption.None ? MARGIN_LEFT + CHECK_WIDTH : MARGIN_LEFT
43+
=======
44+
ListViewOffset = _applicationData.OutputMode != OutputModeOption.None ? FILTER_LABEL_X + 2 : FILTER_LABEL_X
45+
>>>>>>> 474826e (made column spacing tigher)
46+
};
47+
48+
Window win = AddTopLevelWindow();
49+
AddStatusBar();
50+
51+
// GridView header logic
52+
List<string> gridHeaders = _applicationData.DataTable.DataColumns.Select((c) => c.Label).ToList();
53+
CalculateColumnWidths(gridHeaders);
54+
55+
AddFilter(win);
56+
AddHeaders(win, gridHeaders);
57+
58+
// GridView row logic
59+
LoadData();
60+
AddRows(win);
61+
62+
_filterField.Text = _applicationData.Filter ?? string.Empty;
63+
_filterField.CursorPosition = _filterField.Text.Length;
64+
// Run the GUI.
65+
Application.Run();
66+
Application.Shutdown();
67+
68+
// Return results of selection if required.
69+
HashSet<int> selectedIndexes = new HashSet<int>();
70+
if (_cancelled)
71+
{
72+
return selectedIndexes;
73+
}
74+
75+
foreach (GridViewRow gvr in _itemSource.GridViewRowList)
76+
{
77+
if (gvr.IsMarked)
78+
{
79+
selectedIndexes.Add(gvr.OriginalIndex);
80+
}
81+
}
82+
83+
return selectedIndexes;
84+
}
85+
86+
private void Accept()
87+
{
88+
Application.RequestStop();
89+
}
90+
91+
private void Close()
92+
{
93+
_cancelled = true;
94+
Application.RequestStop();
95+
}
96+
97+
private Window AddTopLevelWindow()
98+
{
99+
// Creates the top-level window to show
100+
var win = new Window(_applicationData.Title)
101+
{
102+
X = 0,
103+
Y = 0,
104+
// By using Dim.Fill(), it will automatically resize without manual intervention
105+
Width = Dim.Fill(),
106+
Height = Dim.Fill(1)
107+
};
108+
109+
Application.Top.Add(win);
110+
return win;
111+
}
112+
113+
private void AddStatusBar()
114+
{
115+
var statusBar = new StatusBar(
116+
_applicationData.OutputMode != OutputModeOption.None
117+
? new StatusItem[]
118+
{
119+
// Use Key.Unknown for SPACE with no delegate because ListView already
120+
// handles SPACE
121+
new StatusItem(Key.Unknown, "~SPACE~ Mark Item", null),
122+
new StatusItem(Key.Enter, "~ENTER~ Accept", () =>
123+
{
124+
if (Application.Top.MostFocused == _listView)
125+
{
126+
// If nothing was explicitly marked, we return the item that was selected
127+
// when ENTER is pressed in Single mode. If something was previously selected
128+
// (using SPACE) then honor that as the single item to return
129+
if (_applicationData.OutputMode == OutputModeOption.Single &&
130+
_itemSource.GridViewRowList.Find(i => i.IsMarked) == null)
131+
{
132+
_listView.MarkUnmarkRow();
133+
}
134+
Accept();
135+
}
136+
else if (Application.Top.MostFocused == _filterField)
137+
{
138+
_listView.SetFocus();
139+
}
140+
}),
141+
new StatusItem(Key.Esc, "~ESC~ Close", () => Close())
142+
}
143+
: new StatusItem[]
144+
{
145+
new StatusItem(Key.Esc, "~ESC~ Close", () => Close())
146+
}
147+
);
148+
149+
Application.Top.Add(statusBar);
150+
}
151+
152+
private void CalculateColumnWidths(List<string> gridHeaders)
153+
{
154+
_gridViewDetails.ListViewColumnWidths = new int[gridHeaders.Count];
155+
var listViewColumnWidths = _gridViewDetails.ListViewColumnWidths;
156+
157+
for (int i = 0; i < gridHeaders.Count; i++)
158+
{
159+
listViewColumnWidths[i] = gridHeaders[i].Length;
160+
}
161+
162+
// calculate the width of each column based on longest string in each column for each row
163+
foreach (var row in _applicationData.DataTable.Data)
164+
{
165+
int index = 0;
166+
167+
// use half of the visible buffer height for the number of objects to inspect to calculate widths
168+
foreach (var col in row.Values.Take(Application.Top.Frame.Height / 2))
169+
{
170+
var len = col.Value.DisplayValue.Length;
171+
if (len > listViewColumnWidths[index])
172+
{
173+
listViewColumnWidths[index] = len;
174+
}
175+
176+
index++;
177+
}
178+
}
179+
180+
// if the total width is wider than the usable width, remove 1 from widest column until it fits
181+
_gridViewDetails.UsableWidth = Application.Top.Frame.Width - MARGIN_LEFT - listViewColumnWidths.Length - _gridViewDetails.ListViewOffset;
182+
int columnWidthsSum = listViewColumnWidths.Sum();
183+
while (columnWidthsSum >= _gridViewDetails.UsableWidth)
184+
{
185+
int maxWidth = 0;
186+
int maxIndex = 0;
187+
for (int i = 0; i < listViewColumnWidths.Length; i++)
188+
{
189+
if (listViewColumnWidths[i] > maxWidth)
190+
{
191+
maxWidth = listViewColumnWidths[i];
192+
maxIndex = i;
193+
}
194+
}
195+
196+
listViewColumnWidths[maxIndex]--;
197+
columnWidthsSum--;
198+
}
199+
}
200+
201+
private void AddFilter(Window win)
202+
{
203+
_filterLabel = new Label(FILTER_LABEL)
204+
{
205+
<<<<<<< HEAD
206+
X = MARGIN_LEFT
207+
=======
208+
X = FILTER_LABEL_X
209+
>>>>>>> 474826e (made column spacing tigher)
210+
};
211+
212+
_filterField = new TextField(string.Empty)
213+
{
214+
X = Pos.Right(_filterLabel) + 1,
215+
Y = Pos.Top(_filterLabel),
216+
CanFocus = true,
217+
Width = Dim.Fill() - _filterLabel.Text.Length
218+
};
219+
220+
var filterErrorLabel = new Label(string.Empty)
221+
{
222+
X = Pos.Right(_filterLabel) + 1,
223+
Y = Pos.Top(_filterLabel) + 1,
224+
ColorScheme = Colors.Base,
225+
Width = Dim.Fill() - _filterLabel.Text.Length
226+
};
227+
228+
_filterField.TextChanged += (str) =>
229+
{
230+
// str is the OLD value
231+
string filterText = _filterField.Text?.ToString();
232+
try
233+
{
234+
filterErrorLabel.Text = " ";
235+
filterErrorLabel.ColorScheme = Colors.Base;
236+
filterErrorLabel.Redraw(filterErrorLabel.Bounds);
237+
238+
List<GridViewRow> itemList = GridViewHelpers.FilterData(_itemSource.GridViewRowList, filterText);
239+
_listView.Source = new GridViewDataSource(itemList);
240+
}
241+
catch (Exception ex)
242+
{
243+
filterErrorLabel.Text = ex.Message;
244+
filterErrorLabel.ColorScheme = Colors.Error;
245+
filterErrorLabel.Redraw(filterErrorLabel.Bounds);
246+
_listView.Source = _itemSource;
247+
}
248+
};
249+
250+
win.Add(_filterLabel, _filterField, filterErrorLabel);
251+
}
252+
253+
private void AddHeaders(Window win, List<string> gridHeaders)
254+
{
255+
var header = new Label(GridViewHelpers.GetPaddedString(
256+
gridHeaders,
257+
_gridViewDetails.ListViewOffset,
258+
_gridViewDetails.ListViewColumnWidths))
259+
{
260+
X = 0,
261+
Y = 2
262+
};
263+
264+
win.Add(header);
265+
266+
// This renders dashes under the header to make it more clear what is header and what is data
267+
var headerLineText = new StringBuilder();
268+
foreach (char c in header.Text)
269+
{
270+
if (c.Equals(' '))
271+
{
272+
headerLineText.Append(' ');
273+
}
274+
else
275+
{
276+
// When gui.cs supports text decorations, should replace this with just underlining the header
277+
headerLineText.Append('-');
278+
}
279+
}
280+
281+
var headerLine = new Label(headerLineText.ToString())
282+
{
283+
X = 0,
284+
Y = 3
285+
};
286+
287+
win.Add(headerLine);
288+
}
289+
290+
private void LoadData()
291+
{
292+
var items = new List<GridViewRow>();
293+
int newIndex = 0;
294+
for (int i = 0; i < _applicationData.DataTable.Data.Count; i++)
295+
{
296+
var dataTableRow = _applicationData.DataTable.Data[i];
297+
var valueList = new List<string>();
298+
foreach (var dataTableColumn in _applicationData.DataTable.DataColumns)
299+
{
300+
string dataValue = dataTableRow.Values[dataTableColumn.ToString()].DisplayValue;
301+
valueList.Add(dataValue);
302+
}
303+
304+
string displayString = GridViewHelpers.GetPaddedString(valueList, 0, _gridViewDetails.ListViewColumnWidths);
305+
306+
items.Add(new GridViewRow
307+
{
308+
DisplayString = displayString,
309+
OriginalIndex = i
310+
});
311+
312+
newIndex++;
313+
}
314+
315+
_itemSource = new GridViewDataSource(items);
316+
}
317+
318+
private void AddRows(Window win)
319+
{
320+
_listView = new ListView(_itemSource)
321+
{
322+
X = Pos.Left(_filterLabel),
323+
Y = Pos.Bottom(_filterLabel) + 3, // 1 for space, 1 for header, 1 for header underline
324+
Width = Dim.Fill(2),
325+
Height = Dim.Fill(),
326+
AllowsMarking = _applicationData.OutputMode != OutputModeOption.None,
327+
AllowsMultipleSelection = _applicationData.OutputMode == OutputModeOption.Multiple,
328+
};
329+
330+
win.Add(_listView);
331+
}
332+
333+
public void Dispose()
334+
{
335+
if (!Console.IsInputRedirected)
336+
{
337+
// By emitting this, we fix two issues:
338+
// 1. An issue where arrow keys don't work in the console because .NET
339+
// requires application mode to support Arrow key escape sequences.
340+
// Esc[?1h sets the cursor key to application mode
341+
// See http://ascii-table.com/ansi-escape-sequences-vt-100.php
342+
// 2. An issue where moving the mouse causes characters to show up because
343+
// mouse tracking is still on. Esc[?1003l turns it off.
344+
// See https://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
345+
Console.Write("\u001b[?1h\u001b[?1003l");
346+
}
347+
}
348+
}
349+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="..\..\GraphicalTools.Common.props" />
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<<<<<<< HEAD
9+
<PackageReference Include="Nstack.Core" Version="0.17.*" />
10+
<PackageReference Include="Terminal.Gui" Version="1.7.*" />
11+
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.*" />
12+
=======
13+
<PackageReference Include="Nstack.Core" Version="0.14.0" />
14+
<PackageReference Include="Terminal.Gui" Version="1.00-pre.99.1" />
15+
<PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.1" />
16+
>>>>>>> 474826e (made column spacing tigher)
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include ="../Microsoft.PowerShell.OutGridView.Models/Microsoft.PowerShell.OutGridView.Models.csproj" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<None Update="Microsoft.PowerShell.ConsoleGuiTools.psd1" CopyToOutputDirectory="PreserveNewest" />
25+
</ItemGroup>
26+
</Project>

0 commit comments

Comments
 (0)