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 expected, T actual, string name) { if (!EqualityComparer.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); /// /// 捕获渲染器写出的终端序列 /// internal sealed class FakeTerminalOutput : ITerminalOutput { /// /// 获取已写出的终端缓冲 /// public StringBuilder Buffer { get; } = new(); /// public void Write(string value) => Buffer.Append(value); /// public void Flush() { } /// public void ClearScreen() => Buffer.Append("\e[2J\e[H"); /// public void HideCursor() => Buffer.Append("\e[?25l"); /// public void ShowCursor() => Buffer.Append("\e[?25h"); /// public void MoveCursorTo(int row, int column) => Buffer.Append($"\e[{row};{column}H"); }