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:
@@ -42,8 +42,7 @@ page.Add(new Box(eventsText, textMeasurer) { Title = "事件" });
|
||||
|
||||
runtime.Add(page);
|
||||
runtime.SetFocus(input);
|
||||
|
||||
terminalOutput.HideCursor();
|
||||
terminalOutput.ShowCursor();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TinyTUI.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// 表示终端中的一基光标位置
|
||||
/// </summary>
|
||||
public readonly record struct CursorPosition(int Row, int Column);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -29,4 +29,9 @@ public interface ITerminalOutput
|
||||
/// 显示硬件光标
|
||||
/// </summary>
|
||||
void ShowCursor();
|
||||
|
||||
/// <summary>
|
||||
/// 移动硬件光标到指定的一基位置
|
||||
/// </summary>
|
||||
void MoveCursorTo(int row, int column);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user