feat: add default input parser
- define standard key names for normalized input events - parse common terminal key sequences and bracketed paste - update example to display parsed input events
This commit is contained in:
+29
-11
@@ -1,31 +1,34 @@
|
||||
using TinyTUI;
|
||||
using TinyTUI.Input;
|
||||
using TinyTUI.Stdio;
|
||||
using TinyTUI.Stdout;
|
||||
|
||||
using var input = new ConsoleTerminalInput();
|
||||
var output = new ConsoleTerminalOutput();
|
||||
var parser = new DefaultInputParser();
|
||||
var done = new ManualResetEventSlim();
|
||||
var latestSize = input.CurrentSize;
|
||||
|
||||
output.ClearScreen();
|
||||
output.HideCursor();
|
||||
Render(output, latestSize, "等待输入");
|
||||
Render(output, latestSize, "等待输入", []);
|
||||
|
||||
input.DataReceived += (_, data) =>
|
||||
{
|
||||
if (data == "\x1b")
|
||||
var events = parser.Parse(data);
|
||||
if (events.Any(static e => e is { Kind: TuiInputEventKind.Key, Value: KeyNames.Escape }))
|
||||
{
|
||||
done.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
Render(output, latestSize, $"按键: {FormatInput(data)}");
|
||||
Render(output, latestSize, $"原始输入: {FormatInput(data)}", events);
|
||||
};
|
||||
|
||||
input.Resized += (_, size) =>
|
||||
{
|
||||
latestSize = size;
|
||||
Render(output, latestSize, "窗口尺寸已变化");
|
||||
Render(output, latestSize, "窗口尺寸已变化", [new TuiInputEvent(TuiInputEventKind.Resize, $"{size.Columns}x{size.Rows}")]);
|
||||
};
|
||||
|
||||
try
|
||||
@@ -41,18 +44,33 @@ finally
|
||||
output.Flush();
|
||||
}
|
||||
|
||||
static void Render(ConsoleTerminalOutput output, TerminalSize size, string message)
|
||||
static void Render(ConsoleTerminalOutput output, TerminalSize size, string message, IReadOnlyList<TuiInputEvent> events)
|
||||
{
|
||||
output.ClearScreen();
|
||||
output.Write("TinyTUI IO 示例\r\n");
|
||||
output.Write("================\r\n");
|
||||
output.Write("TinyTUI Input 示例\r\n");
|
||||
output.Write("===================\r\n");
|
||||
output.Write($"尺寸: {size.Columns} x {size.Rows}\r\n");
|
||||
output.Write($"{message}\r\n");
|
||||
output.Write("\r\n");
|
||||
output.Write("解析事件:\r\n");
|
||||
|
||||
if (events.Count == 0)
|
||||
{
|
||||
output.Write("- 无\r\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var inputEvent in events)
|
||||
{
|
||||
output.Write($"- {inputEvent.Kind}: {FormatInput(inputEvent.Value)}\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
output.Write("\r\n");
|
||||
output.Write("测试方式:\r\n");
|
||||
output.Write("- 输入普通字符 观察按键内容\r\n");
|
||||
output.Write("- 按方向键 Home End Delete F1-F12 观察原始序列\r\n");
|
||||
output.Write("- 改变终端窗口大小 观察尺寸刷新\r\n");
|
||||
output.Write("- 输入普通字符 应解析为 Text\r\n");
|
||||
output.Write("- 按方向键 Home End Delete F1-F12 应解析为 Key\r\n");
|
||||
output.Write("- 改变终端窗口大小 应解析为 Resize\r\n");
|
||||
output.Write("- 按 Esc 退出\r\n");
|
||||
output.Flush();
|
||||
}
|
||||
@@ -65,7 +83,7 @@ static string FormatInput(string data)
|
||||
'\n' => "\\n",
|
||||
'\t' => "\\t",
|
||||
'\b' => "\\b",
|
||||
'\x1b' => "\\x1b",
|
||||
'\e' => "\\e",
|
||||
_ when char.IsControl(c) => $"\\x{(int)c:x2}",
|
||||
_ => c.ToString(),
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
namespace TinyTUI.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 将常见终端输入序列解析为标准化 TUI 输入事件
|
||||
/// </summary>
|
||||
public sealed class DefaultInputParser : IInputParser
|
||||
{
|
||||
private const string PasteStart = "\e[200~";
|
||||
private const string PasteEnd = "\e[201~";
|
||||
|
||||
private readonly Dictionary<string, string> _keyMap = new()
|
||||
{
|
||||
["\r"] = KeyNames.Enter,
|
||||
["\n"] = KeyNames.Enter,
|
||||
["\b"] = KeyNames.Backspace,
|
||||
["\x7f"] = KeyNames.Backspace,
|
||||
["\t"] = KeyNames.Tab,
|
||||
["\e"] = KeyNames.Escape,
|
||||
["\e[A"] = KeyNames.Up,
|
||||
["\e[B"] = KeyNames.Down,
|
||||
["\e[C"] = KeyNames.Right,
|
||||
["\e[D"] = KeyNames.Left,
|
||||
["\e[H"] = KeyNames.Home,
|
||||
["\e[F"] = KeyNames.End,
|
||||
["\e[2~"] = KeyNames.Insert,
|
||||
["\e[3~"] = KeyNames.Delete,
|
||||
["\e[5~"] = KeyNames.PageUp,
|
||||
["\e[6~"] = KeyNames.PageDown,
|
||||
["\eOP"] = "f1",
|
||||
["\eOQ"] = "f2",
|
||||
["\eOR"] = "f3",
|
||||
["\eOS"] = "f4",
|
||||
["\e[15~"] = "f5",
|
||||
["\e[17~"] = "f6",
|
||||
["\e[18~"] = "f7",
|
||||
["\e[19~"] = "f8",
|
||||
["\e[20~"] = "f9",
|
||||
["\e[21~"] = "f10",
|
||||
["\e[23~"] = "f11",
|
||||
["\e[24~"] = "f12",
|
||||
};
|
||||
|
||||
private string? _pasteBuffer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<TuiInputEvent> Parse(string data)
|
||||
{
|
||||
if (data.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var events = new List<TuiInputEvent>();
|
||||
ParseInto(data, events);
|
||||
return events;
|
||||
}
|
||||
|
||||
private void ParseInto(string data, List<TuiInputEvent> events)
|
||||
{
|
||||
if (_pasteBuffer is not null)
|
||||
{
|
||||
AppendPaste(data, events);
|
||||
return;
|
||||
}
|
||||
|
||||
var pasteStart = data.IndexOf(PasteStart, StringComparison.Ordinal);
|
||||
if (pasteStart >= 0)
|
||||
{
|
||||
ParseRegular(data[..pasteStart], events);
|
||||
_pasteBuffer = string.Empty;
|
||||
AppendPaste(data[(pasteStart + PasteStart.Length)..], events);
|
||||
return;
|
||||
}
|
||||
|
||||
ParseRegular(data, events);
|
||||
}
|
||||
|
||||
private void AppendPaste(string data, List<TuiInputEvent> events)
|
||||
{
|
||||
var pasteEnd = data.IndexOf(PasteEnd, StringComparison.Ordinal);
|
||||
if (pasteEnd < 0)
|
||||
{
|
||||
_pasteBuffer += data;
|
||||
return;
|
||||
}
|
||||
|
||||
events.Add(new TuiInputEvent(TuiInputEventKind.Paste, _pasteBuffer + data[..pasteEnd]));
|
||||
_pasteBuffer = null;
|
||||
|
||||
ParseInto(data[(pasteEnd + PasteEnd.Length)..], events);
|
||||
}
|
||||
|
||||
private void ParseRegular(string data, List<TuiInputEvent> events)
|
||||
{
|
||||
if (data.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_keyMap.TryGetValue(data, out var keyName))
|
||||
{
|
||||
events.Add(new TuiInputEvent(TuiInputEventKind.Key, keyName));
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.StartsWith('\e'))
|
||||
{
|
||||
events.Add(new TuiInputEvent(TuiInputEventKind.Key, data));
|
||||
return;
|
||||
}
|
||||
|
||||
events.Add(new TuiInputEvent(TuiInputEventKind.Text, data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace TinyTUI.Input;
|
||||
|
||||
/// <summary>
|
||||
/// 定义 TUI 内部使用的标准按键名称
|
||||
/// </summary>
|
||||
public static class KeyNames
|
||||
{
|
||||
/// <summary>
|
||||
/// 回车键
|
||||
/// </summary>
|
||||
public const string Enter = "enter";
|
||||
|
||||
/// <summary>
|
||||
/// 退格键
|
||||
/// </summary>
|
||||
public const string Backspace = "backspace";
|
||||
|
||||
/// <summary>
|
||||
/// 制表键
|
||||
/// </summary>
|
||||
public const string Tab = "tab";
|
||||
|
||||
/// <summary>
|
||||
/// 退出键
|
||||
/// </summary>
|
||||
public const string Escape = "escape";
|
||||
|
||||
/// <summary>
|
||||
/// 上方向键
|
||||
/// </summary>
|
||||
public const string Up = "up";
|
||||
|
||||
/// <summary>
|
||||
/// 下方向键
|
||||
/// </summary>
|
||||
public const string Down = "down";
|
||||
|
||||
/// <summary>
|
||||
/// 左方向键
|
||||
/// </summary>
|
||||
public const string Left = "left";
|
||||
|
||||
/// <summary>
|
||||
/// 右方向键
|
||||
/// </summary>
|
||||
public const string Right = "right";
|
||||
|
||||
/// <summary>
|
||||
/// 行首键
|
||||
/// </summary>
|
||||
public const string Home = "home";
|
||||
|
||||
/// <summary>
|
||||
/// 行尾键
|
||||
/// </summary>
|
||||
public const string End = "end";
|
||||
|
||||
/// <summary>
|
||||
/// 插入键
|
||||
/// </summary>
|
||||
public const string Insert = "insert";
|
||||
|
||||
/// <summary>
|
||||
/// 删除键
|
||||
/// </summary>
|
||||
public const string Delete = "delete";
|
||||
|
||||
/// <summary>
|
||||
/// 上翻页键
|
||||
/// </summary>
|
||||
public const string PageUp = "page-up";
|
||||
|
||||
/// <summary>
|
||||
/// 下翻页键
|
||||
/// </summary>
|
||||
public const string PageDown = "page-down";
|
||||
}
|
||||
Reference in New Issue
Block a user