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:
chuan
2026-06-03 21:53:18 +08:00
Unverified
parent 0d9a36b6a6
commit 0fd4465aa8
3 changed files with 220 additions and 11 deletions
+29 -11
View File
@@ -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(),
}));
+114
View File
@@ -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));
}
}
+77
View File
@@ -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";
}