using TinyTUI; using TinyTUI.Components; using TinyTUI.Input; using TinyTUI.Overlay; using TinyTUI.Rendering; using TinyTUI.Runtime; using TinyTUI.Terminal; using TinyTUI.Text; using var terminal = new ConsoleTerminalSession(); var parser = new DefaultInputParser(); var textMeasurer = new TerminalTextMeasurer(); var renderer = new DifferentialRenderer(terminal, textMeasurer); using var runtime = new TuiRuntime(terminal, parser, renderer); var done = new ManualResetEventSlim(); var eventsText = new Text("最近事件:\n- 无"); var statusText = new Text(); var editorHistory = new List { "history: first command", "history: second command" }; var loader = new Loader(textMeasurer) { Message = "运行中" }; using var loaderCancellation = new CancellationTokenSource(); var input = new TinyTUI.Components.Input(textMeasurer) { Prompt = "请输入: " }; IOverlayHandle? tipHandle = null; input.OnChanged = value => UpdateStatus(statusText, value, textMeasurer, renderer); input.OnSubmitted = value => { switch (value.Trim()) { case "/help": AddEvent(eventsText, "Overlay: help opened"); runtime.ShowOverlay( new HelpOverlay(runtime, textMeasurer), new OverlayOptions { Width = OverlaySize.Percent(45), MinWidth = 42, Anchor = OverlayAnchor.TopRight, Margin = new OverlayMargin(1), }); break; case "/tip": tipHandle?.Hide(); tipHandle = runtime.ShowOverlay( new TipOverlay(textMeasurer), OverlayOptions.NonCapturingTooltip()); AddEvent(eventsText, "Overlay: non-capturing tip opened"); break; case "/select": AddEvent(eventsText, "Overlay: select opened"); runtime.ShowOverlay( new SelectOverlay(runtime, message => AddEvent(eventsText, message), textMeasurer), new OverlayOptions { Width = OverlaySize.Percent(62), MinWidth = 56, Anchor = OverlayAnchor.LeftCenter, Margin = new OverlayMargin(1), MaxHeight = 12, }); break; case "/edit": AddEvent(eventsText, "Overlay: editor opened"); runtime.ShowOverlay( new EditorOverlay(runtime, editorHistory, message => AddEvent(eventsText, message), textMeasurer), new OverlayOptions { Width = OverlaySize.Percent(70), MinWidth = 52, Anchor = OverlayAnchor.BottomCenter, Margin = new OverlayMargin(1), }); break; case "": AddEvent(eventsText, "Submitted empty input"); break; default: AddEvent(eventsText, $"Submitted: {value}"); break; } input.Clear(); }; input.OnCanceled = () => done.Set(); UpdateStatus(statusText, input.Value, textMeasurer, renderer); var page = new Container(); AddAll( page, new Text("TinyTUI 基础组件示例"), new Text("======================"), new Text( "操作说明\n" + "- 输入文本后 Enter 提交\n" + "- /help 打开帮助弹层\n" + "- /tip 打开不抢焦点的右上角提示\n" + "- /select 打开可交互选择列表\n" + "- /edit 打开需要焦点的多行编辑器\n" + "- Esc 退出"), new Text(), new Box(statusText, textMeasurer) { Title = "状态" }, new Text(), new Box(new Container { Children = { loader, input } }, textMeasurer) { Title = "Input" }, new Text(), new Box(eventsText, textMeasurer) { Title = "事件" }); runtime.Add(page); runtime.SetFocus(input); var loaderTask = RunLoaderAsync(loader, runtime, loaderCancellation.Token); try { runtime.Start(); done.Wait(); } finally { StopLoader(loaderCancellation, loaderTask); runtime.Stop(); terminal.Write("\r\n"); terminal.Flush(); } static async Task RunLoaderAsync(Loader loader, ITuiRuntime runtime, CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { await Task.Delay(180, cancellationToken).ConfigureAwait(false); loader.Tick(); runtime.RequestRender(); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } } static void StopLoader(CancellationTokenSource cancellation, Task loaderTask) { cancellation.Cancel(); loaderTask.Wait(TimeSpan.FromSeconds(1)); } static void UpdateStatus( Text statusText, string inputValue, ITextMeasurer textMeasurer, DifferentialRenderer renderer) { statusText.Value = string.Join( '\n', $"当前输入 = \"{inputValue}\"", $"当前输入宽度 = {textMeasurer.GetWidth(inputValue)}", $"字符串 \"abc中文😀xyz\" 宽度 = {textMeasurer.GetWidth("abc中文😀xyz")}", $"截断到 8 列 = \"{textMeasurer.Truncate("abc中文😀xyz", 8)}\"", $"全量重绘次数 = {renderer.FullRedrawCount}", $"差分重绘次数 = {renderer.DifferentialRedrawCount}"); } static void AddEvent(Text eventsText, string message) { var lines = eventsText.Value.Split('\n').Skip(1).Append($"- {message}").TakeLast(8); eventsText.Value = "最近事件:\n" + string.Join('\n', lines); } static void AddAll(Container container, params IComponent[] components) { foreach (var component in components) container.Add(component); } file sealed class HelpOverlay(ITuiRuntime runtime, ITextMeasurer textMeasurer) : IInputComponent { private readonly Box _box = new( new Text( "Overlay 示例\n" + "\n" + "- 这个弹层由 Runtime 显示\n" + "- OverlayManager 会把它合成到基础页面上\n" + "- 显示时焦点会切到弹层\n" + "- 关闭后焦点会恢复到输入框\n" + "\n" + "按 Enter 或 Esc 关闭"), textMeasurer) { Title = "Help", }; public IReadOnlyList Render(int width) => _box.Render(width); public void HandleInput(TuiInputEvent input) { if (input is { Kind: TuiInputEventKind.Key, Value: KeyNames.Enter or KeyNames.Escape }) { runtime.HideOverlay(); } } } file sealed class TipOverlay(ITextMeasurer textMeasurer) : IComponent { private readonly Box _box = new( new Text( "这是 non-capturing overlay\n" + "打开后输入框仍然接收键盘输入\n" + "再次输入 /tip 会替换当前提示"), textMeasurer) { Title = "Tip", }; public IReadOnlyList Render(int width) => _box.Render(width); } file sealed class SelectOverlay : IInputComponent { private readonly ITuiRuntime _runtime; private readonly Action _addEvent; private readonly SelectList _selectList; private readonly Box _box; public SelectOverlay(ITuiRuntime runtime, Action addEvent, ITextMeasurer textMeasurer) { _runtime = runtime; _addEvent = addEvent; _selectList = new SelectList(textMeasurer) { Height = 6, EmptyText = "没有匹配项", }; _selectList.SetItems( [ new SelectItem("text", "Text", "渲染纯文本和多行内容"), new SelectItem("input", "Input", "单行输入 提交 取消和光标定位"), new SelectItem("editor", "Editor", "多行编辑 历史记录 词级移动和撤销"), new SelectItem("select-list", "SelectList", "过滤 描述列 循环选择和提交"), new SelectItem("markdown", "Markdown", "轻量 Markdown 文本展示"), new SelectItem("loader", "Loader", "自动 tick 的加载动画"), new SelectItem("overlay", "Overlay", "定位 尺寸 焦点和 non-capturing 提示"), ]); _selectList.OnSelectionChanged = (item, _) => _addEvent($"Selection: {item.Label}"); _selectList.OnItemSubmitted = (item, _) => { _addEvent($"Selected: {item.Label} ({item.Value})"); _runtime.HideOverlay(); }; _selectList.OnCanceled = _runtime.HideOverlay; _box = new Box( new Container { Children = { new Text("输入文本过滤 Backspace 删除过滤 Up/Down 循环选择 Enter 提交"), new Text(), _selectList, }, }, textMeasurer) { Title = "SelectList", }; } public IReadOnlyList Render(int width) => _box.Render(width); public void HandleInput(TuiInputEvent input) => _selectList.HandleInput(input); } file sealed class EditorOverlay : IInputComponent { private readonly ITuiRuntime _runtime; private readonly List _history; private readonly Action _addEvent; private readonly Editor _editor; private readonly Box _box; public EditorOverlay(ITuiRuntime runtime, List history, Action addEvent, ITextMeasurer textMeasurer) { _runtime = runtime; _history = history; _addEvent = addEvent; _editor = new Editor(textMeasurer) { Height = 6, Placeholder = "在这里输入内容", OnSubmitted = SubmitAndClose, OnCanceled = CancelAndClose, }; _editor.SetHistory(_history); _box = new Box( new Container { Children = { new Text( "Home/End 行首行尾 Ctrl+Left/Right 按词移动\n" + "Ctrl+Backspace/Delete 按词删除 Ctrl+Z/Y 撤销重做\n" + "Ctrl+C 清空当前内容但保留历史记录\n" + "Enter 换行 Ctrl+Enter 或 Esc 提交并关闭\n" + "长行会按弹层宽度软换行 Up/Down 按视觉行移动"), new Text(), _editor, }, }, textMeasurer) { Title = "Editor", }; } public IReadOnlyList Render(int width) => _box.Render(width); public void HandleInput(TuiInputEvent input) { if (input is { Kind: TuiInputEventKind.Key, Value: KeyNames.Escape }) { _editor.Submit(); return; } _editor.HandleInput(input); } private void SubmitAndClose(string text) { var value = text.Trim(); if (value.Length > 0) { _history.Remove(value); _history.Insert(0, value); } _addEvent($"Editor submitted {Math.Max(1, text.Split('\n').Length)} line(s)"); _runtime.HideOverlay(); } private void CancelAndClose() { _addEvent("Editor canceled"); _runtime.HideOverlay(); } }