diff --git a/src/Example/Program.cs b/src/Example/Program.cs index f8a1e76..bb879a6 100644 --- a/src/Example/Program.cs +++ b/src/Example/Program.cs @@ -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 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(), })); diff --git a/src/TinyTUI/Input/DefaultInputParser.cs b/src/TinyTUI/Input/DefaultInputParser.cs new file mode 100644 index 0000000..207fb13 --- /dev/null +++ b/src/TinyTUI/Input/DefaultInputParser.cs @@ -0,0 +1,114 @@ +namespace TinyTUI.Input; + +/// +/// 将常见终端输入序列解析为标准化 TUI 输入事件 +/// +public sealed class DefaultInputParser : IInputParser +{ + private const string PasteStart = "\e[200~"; + private const string PasteEnd = "\e[201~"; + + private readonly Dictionary _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; + + /// + public IReadOnlyList Parse(string data) + { + if (data.Length == 0) + { + return []; + } + + var events = new List(); + ParseInto(data, events); + return events; + } + + private void ParseInto(string data, List 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 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 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)); + } +} diff --git a/src/TinyTUI/Input/KeyNames.cs b/src/TinyTUI/Input/KeyNames.cs new file mode 100644 index 0000000..b01e50d --- /dev/null +++ b/src/TinyTUI/Input/KeyNames.cs @@ -0,0 +1,77 @@ +namespace TinyTUI.Input; + +/// +/// 定义 TUI 内部使用的标准按键名称 +/// +public static class KeyNames +{ + /// + /// 回车键 + /// + public const string Enter = "enter"; + + /// + /// 退格键 + /// + public const string Backspace = "backspace"; + + /// + /// 制表键 + /// + public const string Tab = "tab"; + + /// + /// 退出键 + /// + public const string Escape = "escape"; + + /// + /// 上方向键 + /// + public const string Up = "up"; + + /// + /// 下方向键 + /// + public const string Down = "down"; + + /// + /// 左方向键 + /// + public const string Left = "left"; + + /// + /// 右方向键 + /// + public const string Right = "right"; + + /// + /// 行首键 + /// + public const string Home = "home"; + + /// + /// 行尾键 + /// + public const string End = "end"; + + /// + /// 插入键 + /// + public const string Insert = "insert"; + + /// + /// 删除键 + /// + public const string Delete = "delete"; + + /// + /// 上翻页键 + /// + public const string PageUp = "page-up"; + + /// + /// 下翻页键 + /// + public const string PageDown = "page-down"; +}