using TinyTUI; using TinyTUI.Components; using TinyTUI.Input; using TinyTUI.Overlay; using TinyTUI.Rendering; using TinyTUI.Runtime; using TinyTUI.Stdio; using TinyTUI.Stdout; using TinyTUI.Text; using var terminalInput = new ConsoleTerminalInput(); var terminalOutput = new ConsoleTerminalOutput(); var parser = new DefaultInputParser(); var textMeasurer = new TerminalTextMeasurer(); var renderer = new DifferentialRenderer(terminalOutput, textMeasurer); using var runtime = new TuiRuntime(terminalInput, 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 = 34, Anchor = OverlayAnchor.LeftCenter, Margin = new OverlayMargin(1), MaxHeight = 9, }); 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); terminalOutput.ShowCursor(); var loaderTask = RunLoaderAsync(loader, runtime, loaderCancellation.Token); try { runtime.Start(); done.Wait(); } finally { StopLoader(loaderCancellation, loaderTask); runtime.Stop(); terminalOutput.ShowCursor(); terminalOutput.Write("\r\n"); terminalOutput.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 = 5 }; _selectList.SetItems(["Text", "Input", "Editor", "SelectList", "Markdown", "Loader", "Overlay"]); _selectList.OnSubmitted = (item, _) => { _addEvent($"Selected: {item}"); _runtime.HideOverlay(); }; _selectList.OnCanceled = _runtime.HideOverlay; _box = new Box(_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 = "在这里输入内容", OnCanceled = SubmitAndClose, }; _editor.SetHistory(_history); _box = new Box( new Container { Children = { new Text( "Ctrl+A/E 行首行尾 Ctrl+Left/Right 按词移动\n" + "Ctrl+Backspace/Delete 按词删除 Ctrl+Z/Y 撤销重做\n" + "空内容时 Up/Down 浏览历史 Esc 提交并关闭"), 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 }) { SubmitAndClose(); return; } _editor.HandleInput(input); } private void SubmitAndClose() { var value = _editor.Value.Trim(); if (value.Length > 0) { _history.Remove(value); _history.Insert(0, value); } _addEvent($"Edited {Math.Max(1, _editor.Value.Split('\n').Length)} line(s)"); _runtime.HideOverlay(); } }