From 6da88b6a6712ad4ef8a53cee78684df426af9fd2 Mon Sep 17 00:00:00 2001 From: chuan Date: Wed, 3 Jun 2026 23:07:28 +0800 Subject: [PATCH] feat: position hardware cursor - add cursor marker extraction for rendered lines - move the terminal cursor after full and differential renders - update input to use the real cursor instead of a block glyph --- src/Example/Program.cs | 3 +- src/TinyTUI/Components/Input.cs | 4 +-- src/TinyTUI/Rendering/CursorMarker.cs | 34 +++++++++++++++++++ src/TinyTUI/Rendering/CursorPosition.cs | 6 ++++ src/TinyTUI/Rendering/DifferentialRenderer.cs | 31 +++++++++++++---- src/TinyTUI/Rendering/FullScreenRenderer.cs | 21 ++++++++++-- src/TinyTUI/Stdout/ConsoleTerminalOutput.cs | 6 ++++ src/TinyTUI/Stdout/ITerminalOutput.cs | 5 +++ 8 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 src/TinyTUI/Rendering/CursorMarker.cs create mode 100644 src/TinyTUI/Rendering/CursorPosition.cs diff --git a/src/Example/Program.cs b/src/Example/Program.cs index d409639..ff453df 100644 --- a/src/Example/Program.cs +++ b/src/Example/Program.cs @@ -42,8 +42,7 @@ page.Add(new Box(eventsText, textMeasurer) { Title = "事件" }); runtime.Add(page); runtime.SetFocus(input); - -terminalOutput.HideCursor(); +terminalOutput.ShowCursor(); try { diff --git a/src/TinyTUI/Components/Input.cs b/src/TinyTUI/Components/Input.cs index a490725..d7e28dd 100644 --- a/src/TinyTUI/Components/Input.cs +++ b/src/TinyTUI/Components/Input.cs @@ -1,5 +1,6 @@ using System.Text; using TinyTUI.Input; +using TinyTUI.Rendering; using TinyTUI.Text; namespace TinyTUI.Components; @@ -39,8 +40,7 @@ public sealed class Input(ITextMeasurer? textMeasurer = null) : IInputComponent /// public IReadOnlyList Render(int width) { - var cursor = "█"; - var visible = $"{Prompt}{Value}{cursor}"; + var visible = $"{Prompt}{Value}{CursorMarker.Marker}"; return [_textMeasurer.Truncate(visible, width)]; } diff --git a/src/TinyTUI/Rendering/CursorMarker.cs b/src/TinyTUI/Rendering/CursorMarker.cs new file mode 100644 index 0000000..4bbdc02 --- /dev/null +++ b/src/TinyTUI/Rendering/CursorMarker.cs @@ -0,0 +1,34 @@ +using TinyTUI.Text; + +namespace TinyTUI.Rendering; + +/// +/// 提供组件渲染时标记逻辑光标位置的工具 +/// +public static class CursorMarker +{ + /// + /// 表示逻辑光标位置的不可见标记 + /// + public const string Marker = "\e_TinyTUI:cursor\e\\"; + + /// + /// 从渲染行中提取光标位置并移除标记 + /// + public static CursorPosition? Extract(IList lines, ITextMeasurer textMeasurer) + { + for (var row = 0; row < lines.Count; row++) + { + var markerIndex = lines[row].IndexOf(Marker, StringComparison.Ordinal); + if (markerIndex < 0) + continue; + + var before = lines[row][..markerIndex]; + var column = textMeasurer.GetWidth(before); + lines[row] = lines[row].Remove(markerIndex, Marker.Length); + return new CursorPosition(row + 1, column + 1); + } + + return null; + } +} diff --git a/src/TinyTUI/Rendering/CursorPosition.cs b/src/TinyTUI/Rendering/CursorPosition.cs new file mode 100644 index 0000000..4aa5cdd --- /dev/null +++ b/src/TinyTUI/Rendering/CursorPosition.cs @@ -0,0 +1,6 @@ +namespace TinyTUI.Rendering; + +/// +/// 表示终端中的一基光标位置 +/// +public readonly record struct CursorPosition(int Row, int Column); diff --git a/src/TinyTUI/Rendering/DifferentialRenderer.cs b/src/TinyTUI/Rendering/DifferentialRenderer.cs index 3ab1c7e..296dde1 100644 --- a/src/TinyTUI/Rendering/DifferentialRenderer.cs +++ b/src/TinyTUI/Rendering/DifferentialRenderer.cs @@ -26,14 +26,15 @@ public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer? public void Render(IReadOnlyList lines, TerminalSize size) { var nextLines = NormalizeLines(lines, size.Columns); + var cursor = CursorMarker.Extract(nextLines, _textMeasurer); if (_previousSize != size) { - RenderFull(nextLines, size); + RenderFull(nextLines, size, cursor); return; } - RenderDiff(nextLines, size); + RenderDiff(nextLines, size, cursor); } /// @@ -45,7 +46,7 @@ public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer? output.Flush(); } - private void RenderFull(List lines, TerminalSize size) + private void RenderFull(List lines, TerminalSize size, CursorPosition? cursor) { FullRedrawCount++; output.ClearScreen(); @@ -56,12 +57,13 @@ public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer? output.Write("\r\n"); } + MoveCursor(cursor); output.Flush(); _previousLines = lines; _previousSize = size; } - private void RenderDiff(List lines, TerminalSize size) + private void RenderDiff(List lines, TerminalSize size, CursorPosition? cursor) { var maxLineCount = Math.Max(lines.Count, _previousLines.Count); var changed = false; @@ -86,9 +88,18 @@ public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer? } } - if (changed) + if (cursor is not null) { - DifferentialRedrawCount++; + MoveCursor(cursor); + } + + if (changed || cursor is not null) + { + if (changed) + { + DifferentialRedrawCount++; + } + output.Flush(); } @@ -100,4 +111,12 @@ public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer? { return [.. lines.Select(line => _textMeasurer.Truncate(line, width))]; } + + private void MoveCursor(CursorPosition? cursor) + { + if (cursor is { } position) + { + output.MoveCursorTo(position.Row, position.Column); + } + } } diff --git a/src/TinyTUI/Rendering/FullScreenRenderer.cs b/src/TinyTUI/Rendering/FullScreenRenderer.cs index 58d006a..2431a62 100644 --- a/src/TinyTUI/Rendering/FullScreenRenderer.cs +++ b/src/TinyTUI/Rendering/FullScreenRenderer.cs @@ -13,14 +13,18 @@ public sealed class FullScreenRenderer(ITerminalOutput output, ITextMeasurer? te /// public void Render(IReadOnlyList lines, TerminalSize size) { + var nextLines = NormalizeLines(lines, size.Columns); + var cursor = CursorMarker.Extract(nextLines, _textMeasurer); + output.ClearScreen(); - foreach (var line in lines) + foreach (var line in nextLines) { - output.Write(_textMeasurer.Truncate(line, size.Columns)); + output.Write(line); output.Write("\r\n"); } + MoveCursor(cursor); output.Flush(); } @@ -30,4 +34,17 @@ public sealed class FullScreenRenderer(ITerminalOutput output, ITextMeasurer? te output.ClearScreen(); output.Flush(); } + + private List NormalizeLines(IReadOnlyList lines, int width) + { + return [.. lines.Select(line => _textMeasurer.Truncate(line, width))]; + } + + private void MoveCursor(CursorPosition? cursor) + { + if (cursor is { } position) + { + output.MoveCursorTo(position.Row, position.Column); + } + } } diff --git a/src/TinyTUI/Stdout/ConsoleTerminalOutput.cs b/src/TinyTUI/Stdout/ConsoleTerminalOutput.cs index 905123d..13d4262 100644 --- a/src/TinyTUI/Stdout/ConsoleTerminalOutput.cs +++ b/src/TinyTUI/Stdout/ConsoleTerminalOutput.cs @@ -52,6 +52,12 @@ public sealed class ConsoleTerminalOutput : ITerminalOutput Write("\x1b[?25h"); } + /// + public void MoveCursorTo(int row, int column) + { + Write($"\e[{row};{column}H"); + } + private static TextWriter CreateConsoleWriter() { if (!Console.IsOutputRedirected) diff --git a/src/TinyTUI/Stdout/ITerminalOutput.cs b/src/TinyTUI/Stdout/ITerminalOutput.cs index 569b4e4..31c2a85 100644 --- a/src/TinyTUI/Stdout/ITerminalOutput.cs +++ b/src/TinyTUI/Stdout/ITerminalOutput.cs @@ -29,4 +29,9 @@ public interface ITerminalOutput /// 显示硬件光标 /// void ShowCursor(); + + /// + /// 移动硬件光标到指定的一基位置 + /// + void MoveCursorTo(int row, int column); }