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);
}