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
This commit is contained in:
chuan
2026-06-03 23:07:28 +08:00
Unverified
parent e2311f6e99
commit 6da88b6a67
8 changed files with 98 additions and 12 deletions
+1 -2
View File
@@ -42,8 +42,7 @@ page.Add(new Box(eventsText, textMeasurer) { Title = "事件" });
runtime.Add(page);
runtime.SetFocus(input);
terminalOutput.HideCursor();
terminalOutput.ShowCursor();
try
{
+2 -2
View File
@@ -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
/// <inheritdoc />
public IReadOnlyList<string> Render(int width)
{
var cursor = "█";
var visible = $"{Prompt}{Value}{cursor}";
var visible = $"{Prompt}{Value}{CursorMarker.Marker}";
return [_textMeasurer.Truncate(visible, width)];
}
+34
View File
@@ -0,0 +1,34 @@
using TinyTUI.Text;
namespace TinyTUI.Rendering;
/// <summary>
/// 提供组件渲染时标记逻辑光标位置的工具
/// </summary>
public static class CursorMarker
{
/// <summary>
/// 表示逻辑光标位置的不可见标记
/// </summary>
public const string Marker = "\e_TinyTUI:cursor\e\\";
/// <summary>
/// 从渲染行中提取光标位置并移除标记
/// </summary>
public static CursorPosition? Extract(IList<string> 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;
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace TinyTUI.Rendering;
/// <summary>
/// 表示终端中的一基光标位置
/// </summary>
public readonly record struct CursorPosition(int Row, int Column);
+25 -6
View File
@@ -26,14 +26,15 @@ public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer?
public void Render(IReadOnlyList<string> 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);
}
/// <inheritdoc />
@@ -45,7 +46,7 @@ public sealed class DifferentialRenderer(ITerminalOutput output, ITextMeasurer?
output.Flush();
}
private void RenderFull(List<string> lines, TerminalSize size)
private void RenderFull(List<string> 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<string> lines, TerminalSize size)
private void RenderDiff(List<string> 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);
}
}
}
+19 -2
View File
@@ -13,14 +13,18 @@ public sealed class FullScreenRenderer(ITerminalOutput output, ITextMeasurer? te
/// <inheritdoc />
public void Render(IReadOnlyList<string> 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<string> NormalizeLines(IReadOnlyList<string> 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);
}
}
}
@@ -52,6 +52,12 @@ public sealed class ConsoleTerminalOutput : ITerminalOutput
Write("\x1b[?25h");
}
/// <inheritdoc />
public void MoveCursorTo(int row, int column)
{
Write($"\e[{row};{column}H");
}
private static TextWriter CreateConsoleWriter()
{
if (!Console.IsOutputRedirected)
+5
View File
@@ -29,4 +29,9 @@ public interface ITerminalOutput
/// 显示硬件光标
/// </summary>
void ShowCursor();
/// <summary>
/// 移动硬件光标到指定的一基位置
/// </summary>
void MoveCursorTo(int row, int column);
}