-
Notifications
You must be signed in to change notification settings - Fork 71
Expand file tree
/
Copy pathConsoleGui.cs
More file actions
341 lines (290 loc) · 13.1 KB
/
ConsoleGui.cs
File metadata and controls
341 lines (290 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
// 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 Microsoft.PowerShell.ConsoleGuiTools.Models;
using Terminal.Gui;
namespace Microsoft.PowerShell.ConsoleGuiTools
{
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 propagated back to _inputSource.
private GridViewDataSource _listViewSource;
private ApplicationData _applicationData;
public IEnumerable<int> Start(ApplicationData applicationData)
{
_applicationData = applicationData;
// Note, in Terminal.Gui v2, this property is renamed to Application.UseNetDriver, hence
// using that terminology here.
Application.UseSystemConsole = _applicationData.UseNetDriver;
Application.Init();
var (gridViewHeader, gridViewDataSource) = GridViewHelpers.CreateGridViewInputs(
// If OutputMode is Single or Multiple, then we make items selectable. If we make them selectable,
// 2 columns are required for the check/selection indicator and space.
listViewOffset: _applicationData.OutputMode != OutputModeOption.None ? MARGIN_LEFT + CHECK_WIDTH : MARGIN_LEFT,
applicationData: _applicationData,
properties: _applicationData.Properties,
leftMargin: MARGIN_LEFT
);
Window win = CreateTopLevelWindow();
// Copy the input DataTable into our master ListView source list; upon exit any items
// that are IsMarked are returned (if Outputmode is set)
_inputSource = gridViewDataSource;
if (!_applicationData.MinUI)
{
// Add Filter UI
AddFilter(win);
// Add Header UI
AddHeaders(win, gridViewHeader);
}
// 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 filtered version of _inputSource even if there is no
// actual filter.
ApplyFilter();
_listView.SetFocus();
// Run the GUI.
Application.Run();
Application.Shutdown();
// Return results of selection if required.
if (_cancelled)
{
return Enumerable.Empty<int>();
}
// Return any items that were selected.
return _inputSource.GridViewRowList
.Where(gvr => gvr.IsMarked)
.Select(gvr => gvr.OriginalIndex);
}
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<StatusItem>();
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())
{
Visible = visible
};
Application.Top.Add(statusBar);
}
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, GridViewHeader gridViewHeader)
{
var header = new Label(gridViewHeader.HeaderText)
{
X = 0,
Y = _applicationData.MinUI ? 0 : 2
};
win.Add(header);
if (_applicationData.MinUI)
{
return;
}
var headerLine = new Label(gridViewHeader.HeaderUnderLine)
{
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");
}
}
}
}