-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKeyHandler.cs
More file actions
494 lines (443 loc) · 18.5 KB
/
KeyHandler.cs
File metadata and controls
494 lines (443 loc) · 18.5 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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
/*
* File Name : KeyHandler.cs
* From : https://github.com/tonerdo/readline/blob/master/src/ReadLine/KeyHandler.cs
* Last Updated : Chen Jaofeng @ 2023/05/22
*/
using CJF.CommandLine.Abstractions;
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Text;
namespace CJF.CommandLine;
#region Internal Class : KeyHandler
[UnsupportedOSPlatform("browser")]
internal class KeyHandler
{
/// <summary>文字緩衝區。</summary>
private readonly StringBuilder _TextBuffer;
/// <summary>輸入列開始的水平游標位置。</summary>
private readonly int _CursorStartPos;
/// <summary>游標處於文字的位置。</summary>
/// <remarks>
/// <para>以字元(<see cref="char"/>)為單位,即使為雙寬度字元(如中文)亦為 1。</para>
/// <para>如果位置在最後一位,其值等於 <see cref="_TextBuffer"/> 的長度。</para>
/// </remarks>
private int _CursorCharPos;
/// <summary>游標在輸入區的位置,從 0 開始。</summary>
/// <remarks>
/// <para>以寬度為單位,英數字為 1,中文全形字為 2。</para>
/// <para>當 <see cref="_TextBuffer"/> 長度為 0、或在行首,<see cref="_CursorPos"/> 為 0;</para>
/// <para>如果在行尾,則 <see cref="_CursorPos"/> = <see cref="TextMaxWidth"/>。</para>
/// </remarks>
private int _CursorPos;
/// <summary>歷史指令儲存區索引值。</summary>
private int _HistoryIndex;
/// <summary>歷史指令儲存區。</summary>
private readonly List<string> _History;
/// <summary><see cref="Console.ReadKey()"/> 讀到的按鍵值。</summary>
private ConsoleKeyInfo _PressKey;
/// <summary>控制類按鍵行為函示集合字典。</summary>
private readonly Dictionary<string, Action> _KeyActions;
/// <summary>自動完成的字串陣列。</summary>
private string[]? _Completions;
private int _CompletionStart;
private int _CompletionsIndex;
private readonly IConsole Console2;
/// <summary>輸入的文字總寬度,英數字為 1,中文字為 2 。</summary>
private int TextMaxWidth => _TextBuffer.ToString().GetWidth();
/// <summary>是否在輸入列的首位。</summary>
/// <returns>如果在行首則為 <see langword="true"/>,否則為 <see langword="false"/>。</returns>
private bool IsStartOfLine() => _CursorPos == 0;
/// <summary>是否在輸入列的末位。</summary>
/// <returns>如果在行尾則為 <see langword="true"/>,否則為 <see langword="false"/>。</returns>
private bool IsEndOfLine() => _CursorPos == TextMaxWidth;
/// <summary>是否在該列的首位。</summary>
private bool IsStartOfBuffer() => Console2.CursorLeft == 0;
/// <summary>是否在該列的末位。</summary>
private bool IsEndOfBuffer() => Console2.CursorLeft == Console2.BufferWidth - 1;
/// <summary>是否在自動完成模式。</summary>
private bool IsInAutoCompleteMode() => _Completions is not null;
public static Action<string[]>? PrintCommandsHandler { private get; set; }
public bool IsMultiAutoCompleteResult() => _Completions is not null && _Completions.Length > 1;
/// <summary>取得輸入列的文字內容。</summary>
public string Text => _TextBuffer.ToString();
#region Private Method : void MoveCursorLeft(bool cursorVisible = true)
private void MoveCursorLeft(bool cursorVisible = true)
{
if (IsStartOfLine()) return;
if (cursorVisible) Console2.CursorVisible = false;
if (IsStartOfBuffer() && _TextBuffer.Length > 0)
{
Console2.SetCursorPosition(Console2.BufferWidth - 1, Console2.CursorTop - 1);
_CursorPos = Console2.CursorLeft;
}
else if (_CursorCharPos > 0 && _TextBuffer.Length > 0)
{
int charLen = _TextBuffer[_CursorCharPos - 1].IsDoubleWord() ? 2 : 1;
Console2.SetCursorPosition(Console2.CursorLeft - charLen, Console2.CursorTop);
_CursorPos -= charLen;
}
else
return;
if (cursorVisible) Console2.CursorVisible = true;
_CursorCharPos--;
Debug.Print($"CursorPos:{_CursorPos}, CursorLimit:{TextMaxWidth}, TextPos:{_CursorCharPos}");
}
#endregion
#region Private Method : void MoveCursorHome(bool cursorVisible = true)
private void MoveCursorHome(bool cursorVisible = true)
{
if (cursorVisible) Console2.CursorVisible = false;
//Console2.SetCursorPosition(_CursorStartPos, Console2.CursorTop);
// 避免游標定位在指令行時,被新訊息給更新而造成定位錯誤,所以使用相對位置定位
Console2.SetCursorPosition(Console2.CursorLeft - _CursorCharPos, Console2.CursorTop);
_CursorPos = 0;
_CursorCharPos = 0;
if (cursorVisible) Console2.CursorVisible = true;
}
#endregion
#region Private Method : void MoveCursorRight(bool cursorVisible = true)
private void MoveCursorRight(bool cursorVisible = true)
{
if (IsEndOfLine()) return;
if (cursorVisible) Console2.CursorVisible = false;
if (IsEndOfBuffer())
{
Console2.SetCursorPosition(0, Console2.CursorTop + 1);
_CursorPos = Console2.CursorLeft;
}
else if (_TextBuffer.Length > _CursorCharPos)
{
int charLen = _TextBuffer[_CursorCharPos].IsDoubleWord() ? 2 : 1;
Console2.SetCursorPosition(Console2.CursorLeft + charLen, Console2.CursorTop);
_CursorPos += charLen;
}
if (cursorVisible) Console2.CursorVisible = true;
_CursorCharPos++;
Debug.Print($"CursorPos:{_CursorPos}, CursorLimit:{TextMaxWidth}, TextPos:{_CursorCharPos}");
}
#endregion
#region Private Method : void MoveCursorEnd(bool cursorVisible = false)
private void MoveCursorEnd(bool cursorVisible = false)
{
if (cursorVisible) Console2.CursorVisible = false;
//Console2.SetCursorPosition(_CursorStartPos + TextMaxWidth, Console2.CursorTop);
// 避免游標定位在指令行時,被新訊息給更新而造成定位錯誤,所以使用相對位置定位
Console2.SetCursorPosition(Console2.CursorLeft + (TextMaxWidth - _CursorCharPos), Console2.CursorTop);
_CursorPos = TextMaxWidth;
_CursorCharPos = _TextBuffer.Length;
if (cursorVisible) Console2.CursorVisible = true;
}
#endregion
#region Private Method : void ClearLine(bool cursorVisible = true)
private void ClearLine(bool cursorVisible = true)
{
if (cursorVisible)
Console2.CursorVisible = false;
// 避免游標定位在指令行時,被新訊息給更新而造成定位錯誤,所以使用相對位置定位
//Console2.SetCursorPosition(_CursorStartPos, Console2.CursorTop);
Console2.SetCursorPosition(Console2.CursorLeft - _CursorCharPos, Console2.CursorTop);
Console2.Write("".PadLeft(TextMaxWidth));
//Console2.SetCursorPosition(_CursorStartPos, Console2.CursorTop);
Console2.SetCursorPosition(Console2.CursorLeft - TextMaxWidth, Console2.CursorTop);
_TextBuffer.Clear();
_CursorPos = 0;
_CursorCharPos = 0;
if (cursorVisible)
Console2.CursorVisible = true;
}
#endregion
#region Private Method : void Backspace(bool cursorVisible = true)
private void Backspace(bool cursorVisible = true)
{
if (IsStartOfLine() || _TextBuffer.Length == 0 || _CursorCharPos == 0) return;
_CursorCharPos--;
int index = _CursorCharPos;
var c = _TextBuffer[index];
var cLen = c.IsDoubleWord() ? 2 : 1;
_TextBuffer.Remove(index, 1);
string lastChars = _TextBuffer.ToString()[index..];
int left = Console2.CursorLeft - cLen;
int top = Console2.CursorTop;
if (cursorVisible) Console2.CursorVisible = false;
Console2.SetCursorPosition(left, top);
Console2.Write($"{lastChars} ");
Console2.SetCursorPosition(left, top);
if (cursorVisible) Console2.CursorVisible = true;
_CursorPos -= cLen;
Debug.Print($"CursorPos:{_CursorPos}, CursorLimit:{TextMaxWidth}, TextPos:{_CursorCharPos}");
}
#endregion
#region Private Method : void Delete(bool cursorVisible = true)
private void Delete(bool cursorVisible = true)
{
if (IsEndOfLine() || _TextBuffer.Length == 0) return;
int index = _CursorCharPos;
var c = _TextBuffer[index];
_TextBuffer.Remove(index, 1);
string lastChars = _TextBuffer.ToString()[index..];
int left = Console2.CursorLeft;
int top = Console2.CursorTop;
if (cursorVisible) Console2.CursorVisible = false;
Console2.Write($"{lastChars} ");
Console2.SetCursorPosition(left, top);
if (cursorVisible) Console2.CursorVisible = true;
Debug.Print($"CursorPos:{_CursorPos}, CursorLimit:{TextMaxWidth}, TextPos:{_CursorCharPos}");
}
#endregion
#region Private Method : void WriteNewString(string str)
private void WriteNewString(string str)
{
Console2.CursorVisible = false;
ClearLine(false);
foreach (char character in str)
WriteChar(character, false);
Console2.CursorVisible = true;
}
#endregion
#region Private Method : void WriteString(string str)
private void WriteString(string str)
{
Console2.CursorVisible = false;
foreach (char character in str)
WriteChar(character, false);
Console2.CursorVisible = true;
}
#endregion
#region Private Method : string BuildKeyInput()
private string BuildKeyInput()
{
return (_PressKey.Modifiers != ConsoleModifiers.Control && _PressKey.Modifiers != ConsoleModifiers.Shift) ?
_PressKey.Key.ToString() : _PressKey.Modifiers.ToString() + _PressKey.Key.ToString();
}
#endregion
#region Private Method : void TransposeChars()
private void TransposeChars()
{
// local helper functions
bool almostEndOfLine() => (TextMaxWidth - _CursorPos) == 1;
int incrementIf(Func<bool> expression, int index) => expression() ? index + 1 : index;
int decrementIf(Func<bool> expression, int index) => expression() ? index - 1 : index;
if (IsStartOfLine()) { return; }
var firstIdx = decrementIf(IsEndOfLine, _CursorPos - 1);
var secondIdx = decrementIf(IsEndOfLine, _CursorPos);
//char secondChar = _TextBuffer[secondIdx];
//_TextBuffer[secondIdx] = _TextBuffer[firstIdx];
//_TextBuffer[firstIdx] = secondChar;
(_TextBuffer[firstIdx], _TextBuffer[secondIdx]) = (_TextBuffer[secondIdx], _TextBuffer[firstIdx]);
var left = incrementIf(almostEndOfLine, Console2.CursorLeft);
var cursorPosition = incrementIf(almostEndOfLine, _CursorPos);
WriteNewString(_TextBuffer.ToString());
Console2.SetCursorPosition(left, Console2.CursorTop);
_CursorPos = cursorPosition;
MoveCursorRight();
}
#endregion
#region Private Method : void StartAutoComplete()
private void StartAutoComplete()
{
if (_Completions is null) return;
Console2.CursorVisible = false;
while (_CursorPos > _CompletionStart)
Backspace(false);
_CompletionsIndex = 0;
WriteString(_Completions[_CompletionsIndex] + ' ');
}
#endregion
#region Private Method : void NextAutoComplete()
private void NextAutoComplete()
{
if (_Completions is null) return;
while (_CursorPos > _CompletionStart)
Backspace();
_CompletionsIndex++;
if (_CompletionsIndex == _Completions.Length)
_CompletionsIndex = 0;
WriteString(_Completions[_CompletionsIndex]);
}
#endregion
#region Private Method : void PreviousAutoComplete()
private void PreviousAutoComplete()
{
if (_Completions is null) return;
while (_CursorPos > _CompletionStart)
Backspace();
_CompletionsIndex--;
if (_CompletionsIndex == -1)
_CompletionsIndex = _Completions.Length - 1;
WriteString(_Completions[_CompletionsIndex]);
}
#endregion
#region Private Method : void PrevHistory()
private void PrevHistory()
{
if (_HistoryIndex > 0)
{
_HistoryIndex--;
WriteNewString(_History[_HistoryIndex]);
}
}
#endregion
#region Private Method : void NextHistory()
private void NextHistory()
{
if (_HistoryIndex < _History.Count)
{
_HistoryIndex++;
if (_HistoryIndex == _History.Count)
ClearLine();
else
WriteNewString(_History[_HistoryIndex]);
}
}
#endregion
#region Private Method : void ResetAutoComplete()
private void ResetAutoComplete()
{
_Completions = null;
_CompletionsIndex = 0;
}
#endregion
#region Public Construct Method : KeyHandler(IConsole console, List<string>? history = null, Func<string, int, string[]?>? handler = null)
public KeyHandler(IConsole console, List<string>? history = null, Func<string, int, string[]?>? handler = null)
{
Console2 = console;
_CursorStartPos = Console2.CursorLeft;
char[] separators = new char[] { ' ' };
_History = history ?? new List<string>();
_HistoryIndex = _History.Count;
_TextBuffer = new StringBuilder();
_KeyActions = new Dictionary<string, Action>
{
["Escape"] = () => ClearLine(),
["Backspace"] = () => Backspace(),
["Delete"] = () => Delete(),
["LeftArrow"] = () => MoveCursorLeft(),
["RightArrow"] = () => MoveCursorRight(),
["Home"] = () => MoveCursorHome(),
["End"] = () => MoveCursorEnd(),
["UpArrow"] = () => PrevHistory(),
["ControlA"] = () => MoveCursorHome(),
["ControlB"] = () => MoveCursorLeft(),
["ControlD"] = () => Delete(),
["ControlE"] = () => MoveCursorEnd(),
["ControlF"] = () => MoveCursorRight(),
["ControlH"] = () => Backspace(),
["ControlL"] = () => ClearLine(),
["ControlP"] = () => PrevHistory(),
["DownArrow"] = () => NextHistory(),
["ControlN"] = () => NextHistory(),
["ControlU"] = () => // 刪除到行首的字元
{
while (!IsStartOfLine())
Backspace();
},
["ControlK"] = () => // 刪除到行尾的字元
{
int pos = _CursorPos;
MoveCursorEnd();
while (_CursorPos > pos)
Backspace();
},
["ControlW"] = () => // 刪除游標前的單字
{
while (!IsStartOfLine() && _TextBuffer[_CursorCharPos - 1] != ' ')
Backspace();
},
["ControlT"] = TransposeChars,
["Tab"] = () =>
{
if (handler == null || !IsEndOfLine() || _TextBuffer.Length == 0) return;
string text = _TextBuffer.ToString();
_CompletionStart = text.LastIndexOfAny(separators);
_CompletionStart = _CompletionStart == -1 ? 0 : _CompletionStart + 1;
if (text.EndsWith(" ")) return;
_Completions = handler.Invoke(text, _CompletionStart);
_Completions = _Completions?.Length == 0 ? null : _Completions;
if (_Completions == null) return;
if (_Completions.Length >= 2)
{
Console.Beep();
Console.WriteLine();
if (PrintCommandsHandler is not null)
PrintCommandsHandler.Invoke(_Completions);
else
CliCenter.PrintCommands(_Completions);
}
else
StartAutoComplete();
},
};
}
#endregion
#region Public Method : void Handle(ConsoleKeyInfo keyInfo)
public void Handle(ConsoleKeyInfo keyInfo)
{
_PressKey = keyInfo;
if (IsInAutoCompleteMode() && _PressKey.Key != ConsoleKey.Tab)
ResetAutoComplete();
if (!_KeyActions.TryGetValue(BuildKeyInput(), out Action? action))
action = () => WriteChar(_PressKey.KeyChar);
action.Invoke();
}
#endregion
#region Public Method : void WriteChar(char c, bool cursorVisible = true)
public void WriteChar(char c, bool cursorVisible = true)
{
int charLen = c.IsDoubleWord() ? 2 : 1;
string last = "";
if (cursorVisible) Console2.CursorVisible = false;
if (IsEndOfLine())
{
_TextBuffer.Append(c);
Console2.Write(c.ToString());
}
else
{
int left = Console2.CursorLeft;
int top = Console2.CursorTop;
if (_CursorCharPos >= _TextBuffer.Length)
{
_TextBuffer.Append(c);
_CursorCharPos = _TextBuffer.Length - 1;
}
else
{
_TextBuffer.Insert(_CursorCharPos, c);
last = _TextBuffer.ToString()[_CursorCharPos..];
Console2.CursorVisible = false;
Console2.Write(last);
Console2.SetCursorPosition(left + charLen, top);
}
}
if (cursorVisible) Console2.CursorVisible = true;
_CursorCharPos++;
_CursorPos += charLen;
Debug.Print($"CursorPos:{_CursorPos}, CursorLimit:{TextMaxWidth}, TextPos:{_CursorCharPos}, Last:{last}");
}
#endregion
}
#endregion
#region Static Class : StringExtensions
static class StringExtensions
{
/// <summary>取得字串寬度。</summary>
/// <param name="source">欲取得寬度的原始字串。</param>
/// <returns>字串寬度。</returns>
public static int GetWidth(this string source)
{
int width = 0;
foreach (char c in source)
width += c.IsDoubleWord() ? 2 : 1;
return width;
}
}
#endregion
#region Static Class : CharExtensions
static class CharExtensions
{
/// <summary>判斷 <paramref name="c"/> 是否為雙位元組字元。</summary>
/// <param name="c"></param>
/// <returns>如果 <paramref name="c"/> 為雙位元組字元,則傳回 <see langword="true"/>,否則為 <see langword="false"/>。</returns>
public static bool IsDoubleWord(this char c) => Encoding.UTF8.GetByteCount(c.ToString()) != 1;
}
#endregion