e2d534170f
- add terminal image service and image component - protect image lines and clean up kitty image ids
161 lines
7.0 KiB
C#
161 lines
7.0 KiB
C#
using TinyTUI;
|
|
using TinyTUI.Components;
|
|
using TinyTUI.Autocomplete;
|
|
using TinyTUI.Input;
|
|
using TinyTUI.Rendering;
|
|
using TinyTUI.Stdout;
|
|
using TinyTUI.Terminal.Images;
|
|
using TinyTUI.Text;
|
|
using System.Text;
|
|
|
|
var measurer = new TerminalTextMeasurer();
|
|
|
|
var input = new Input(measurer) { Prompt = "> " };
|
|
AssertFalse(input.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "unfocused input marker");
|
|
input.Focused = true;
|
|
AssertTrue(input.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "focused input marker");
|
|
|
|
var editor = new Editor(measurer) { Placeholder = "type here" };
|
|
AssertFalse(editor.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "unfocused editor marker");
|
|
editor.Focused = true;
|
|
AssertTrue(editor.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "focused editor marker");
|
|
|
|
var list = new SelectList(measurer);
|
|
list.SetItems(["alpha", "beta"]);
|
|
AssertFalse(list.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "unfocused select marker");
|
|
list.Focused = true;
|
|
AssertTrue(list.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "focused select marker");
|
|
|
|
AssertEqual(3, new Spacer(3).Render(10).Count, "spacer line count");
|
|
AssertEqual(0, new Spacer(-1).Render(10).Count, "spacer clamps negative lines");
|
|
|
|
var truncated = new TruncatedText("abcdef\nignored", measurer)
|
|
{
|
|
PaddingX = 1,
|
|
PaddingY = 1,
|
|
};
|
|
var truncatedLines = truncated.Render(6);
|
|
AssertEqual(3, truncatedLines.Count, "truncated text vertical padding");
|
|
AssertEqual(6, measurer.GetWidth(truncatedLines[1]), "truncated text padded width");
|
|
AssertTrue(truncatedLines[1].Contains("abc", StringComparison.Ordinal), "truncated text first line only");
|
|
|
|
var loader = new CancellableLoader(keybindings: KeybindingRegistry.CreateDefault());
|
|
var canceled = false;
|
|
loader.OnCanceled = () => canceled = true;
|
|
loader.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape));
|
|
AssertTrue(loader.IsCanceled, "cancellable loader token");
|
|
AssertTrue(canceled, "cancellable loader callback");
|
|
|
|
var autocompleteEditor = new Editor(measurer)
|
|
{
|
|
AutocompleteProvider = new SlashCommandAutocompleteProvider(
|
|
[
|
|
new SlashCommand("help", "show help"),
|
|
new SlashCommand("history", "show history"),
|
|
]),
|
|
};
|
|
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Text, "/h"));
|
|
AssertTrue(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete active");
|
|
AssertTrue(autocompleteEditor.RenderAutocomplete(40).Any(line => line.Contains("help", StringComparison.Ordinal)), "slash command autocomplete render");
|
|
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Down));
|
|
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab));
|
|
AssertEqual("/history ", autocompleteEditor.Value, "slash command autocomplete confirm");
|
|
AssertFalse(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete closes after confirm");
|
|
|
|
autocompleteEditor.Value = "/h";
|
|
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab));
|
|
AssertTrue(autocompleteEditor.IsAutocompleteActive, "forced autocomplete active");
|
|
autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape));
|
|
AssertFalse(autocompleteEditor.IsAutocompleteActive, "autocomplete cancel");
|
|
AssertEqual("/h", autocompleteEditor.Value, "autocomplete cancel keeps text");
|
|
|
|
var imageService = new TerminalImageService();
|
|
imageService.SetCapabilities(new TerminalCapabilities(ImageProtocol.Kitty, true, true));
|
|
imageService.SetCellDimensions(new CellDimensions(10, 10));
|
|
var cellSize = imageService.CalculateCellSize(new ImageDimensions(20, 20), 2);
|
|
AssertEqual(new ImageCellSize(2, 2), cellSize, "image cell size");
|
|
|
|
var pngData = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAFElEQVR4nGNgYGBgYGBgYGBgAAAABQABJzQnCgAAAABJRU5ErkJggg==");
|
|
AssertEqual(new ImageDimensions(2, 3), imageService.TryGetDimensions(pngData, "image/png"), "png dimensions");
|
|
|
|
var image = new Image([1, 2, 3, 4], "image/png", imageService: imageService, dimensions: new ImageDimensions(20, 20))
|
|
{
|
|
MaxWidthCells = 2,
|
|
};
|
|
var imageLines = image.Render(4);
|
|
AssertEqual(2, imageLines.Count, "kitty image line count");
|
|
AssertTrue(image.ImageId is > 0, "kitty image id allocated");
|
|
AssertTrue(imageLines[0].StartsWith("\e_G", StringComparison.Ordinal), "kitty image sequence");
|
|
AssertTrue(imageLines[0].Contains(",C=1,", StringComparison.Ordinal), "kitty image no cursor movement");
|
|
AssertTrue(TerminalImageService.IsImageLine($"prefix {imageLines[0]} suffix"), "image line detection anywhere");
|
|
|
|
imageService.SetCapabilities(TerminalCapabilities.Conservative);
|
|
var fallback = new Image([1, 2, 3], "image/jpeg", imageService: imageService, dimensions: new ImageDimensions(8, 9))
|
|
{
|
|
FileName = "photo.jpg",
|
|
}.Render(80)[0];
|
|
AssertTrue(fallback.Contains("[Image: photo.jpg [image/jpeg] 8x9]", StringComparison.Ordinal), "image fallback");
|
|
|
|
var fakeOutput = new FakeTerminalOutput();
|
|
var renderer = new DifferentialRenderer(
|
|
fakeOutput,
|
|
measurer,
|
|
new RenderPipelineOptions { ThrowOnWidthOverflow = true, UseSynchronizedOutput = false });
|
|
var longImageLine = $"Read image file \e]1337;File=inline=1:{new string('A', 200)}\a";
|
|
renderer.Render([longImageLine], new TerminalSize(20, 5));
|
|
renderer.Render(["plain"], new TerminalSize(20, 5));
|
|
|
|
fakeOutput.Buffer.Clear();
|
|
var kittyLine = "\e_Ga=T,f=100,q=2,C=1,c=1,r=1,i=42;AAAA\e\\";
|
|
renderer.Render([kittyLine], new TerminalSize(20, 5));
|
|
fakeOutput.Buffer.Clear();
|
|
renderer.Render(["changed"], new TerminalSize(20, 5));
|
|
AssertTrue(fakeOutput.Buffer.ToString().Contains(TerminalImageService.DeleteKittyImage(42), StringComparison.Ordinal), "kitty image cleanup on diff");
|
|
|
|
Console.WriteLine("TinyTUI component checks passed");
|
|
|
|
static void AssertEqual<T>(T expected, T actual, string name)
|
|
{
|
|
if (!EqualityComparer<T>.Default.Equals(expected, actual))
|
|
throw new InvalidOperationException($"{name}: expected '{expected}', actual '{actual}'");
|
|
}
|
|
|
|
static void AssertTrue(bool condition, string name)
|
|
{
|
|
if (!condition)
|
|
throw new InvalidOperationException($"{name}: assertion failed");
|
|
}
|
|
|
|
static void AssertFalse(bool condition, string name) => AssertTrue(!condition, name);
|
|
|
|
/// <summary>
|
|
/// 捕获渲染器写出的终端序列
|
|
/// </summary>
|
|
internal sealed class FakeTerminalOutput : ITerminalOutput
|
|
{
|
|
/// <summary>
|
|
/// 获取已写出的终端缓冲
|
|
/// </summary>
|
|
public StringBuilder Buffer { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public void Write(string value) => Buffer.Append(value);
|
|
|
|
/// <inheritdoc />
|
|
public void Flush()
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ClearScreen() => Buffer.Append("\e[2J\e[H");
|
|
|
|
/// <inheritdoc />
|
|
public void HideCursor() => Buffer.Append("\e[?25l");
|
|
|
|
/// <inheritdoc />
|
|
public void ShowCursor() => Buffer.Append("\e[?25h");
|
|
|
|
/// <inheritdoc />
|
|
public void MoveCursorTo(int row, int column) => Buffer.Append($"\e[{row};{column}H");
|
|
}
|