From f87ed9afae92f56435f3eb49bcd6152835da9af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 23 Sep 2026 18:38:54 -0400 Subject: [PATCH 1/3] Fix Sixel rendering through ConPTY Bundle the Microsoft ConPTY runtime and hosts, correct DECSDM defaults and high-DPI image geometry, and cover the complete rendering path with regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Directory.Packages.props | 1 + docs/advanced-vt-protocols.md | 14 +++--- docs/release.md | 18 ++++++-- .../ConPtyConnection.cs | 4 +- .../ConPtyHost.targets | 34 +++++++++++++++ .../Devolutions.Terminal.Connection.csproj | 5 +++ .../Native/ConPty.cs | 18 ++++++++ .../Native/Kernel32.cs | 9 ---- .../Native/SafeHandles.cs | 2 +- .../THIRD-PARTY-NOTICES-CONPTY.txt | 24 +++++++++++ .../Devolutions.Terminal.Control.csproj | 10 ++++- .../TerminalEngine.cs | 6 +-- .../SkiaTerminalRenderer.cs | 5 ++- .../ConnectionContractTests.cs | 43 +++++++++++++++++++ .../AdvancedProtocolEngineTests.cs | 13 +++++- .../SkiaTerminalRendererTests.cs | 26 +++++++++++ 16 files changed, 203 insertions(+), 29 deletions(-) create mode 100644 src/Devolutions.Terminal.Connection/ConPtyHost.targets create mode 100644 src/Devolutions.Terminal.Connection/Native/ConPty.cs create mode 100644 src/Devolutions.Terminal.Connection/THIRD-PARTY-NOTICES-CONPTY.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index b3d4691..2911b07 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,5 +21,6 @@ + diff --git a/docs/advanced-vt-protocols.md b/docs/advanced-vt-protocols.md index ffa2a65..8b5f98e 100644 --- a/docs/advanced-vt-protocols.md +++ b/docs/advanced-vt-protocols.md @@ -1,12 +1,11 @@ # Advanced VT protocols > [!NOTE] -> The public out-of-process Windows ConPTY may filter DCS payloads before they -> reach a terminal client on some Windows builds, and APC sequences (kitty -> graphics) can be affected the same way. The Core parser and renderer -> support Sixel and kitty graphics when a connection transports those bytes -> unchanged (for example, remote/Azure transports); local ConPTY support is -> limited by the installed Windows pseudoconsole implementation. +> Windows local sessions use the bundled Microsoft ConPTY runtime, including +> `OpenConsole.exe`, rather than the OS copy that filters Sixel on some Windows +> builds. With the built-in engine, Sixel producers such as `Get-XKCD -Show` +> can render inline images. Other transports must likewise preserve graphics +> payloads; the optional Ghostty engine does not expose image resources. `Devolutions.Terminal.Core` parses advanced string protocols without depending on Avalonia, Skia, Win32, or an image codec. It exposes decoded Sixel pixels and bounded @@ -36,6 +35,9 @@ terminator split across input chunks. BEL does not terminate DCS. An `ESC` followed by anything other than `\` aborts the DCS and starts the new escape sequence. +DECSDM defaults to reset (scrolling mode): Sixel images anchor at the cursor +unless the application explicitly selects display mode with `CSI ? 80 h`. + ## Limits Limits are public constants on `TerminalImageLimits` and `VtResourceLimits`. diff --git a/docs/release.md b/docs/release.md index c7b96d2..16acc80 100644 --- a/docs/release.md +++ b/docs/release.md @@ -12,7 +12,17 @@ Commands are relative to the repository root. - WinApp CLI 0.6.0 for MSIX creation and validation - A trusted code-signing certificate for distributable MSIX artifacts -Windows local sessions use ConPTY. Linux and macOS local sessions use the +Windows local sessions use the pinned `Microsoft.Windows.Console.ConPTY` NuGet +runtime. Ship `conpty.dll` and the architecture-specific `OpenConsole.exe` +subdirectories together: without its host, ConPTY can fall back to the OS copy +and silently lose Sixel output. `ConPtyHost.targets` supplies these files for +RID-less builds, RID-specific publishes, and Control NuGet consumers, including +the ARM64 host required by x64 processes running under emulation. Because +`conpty.dll` lands under `runtimes\\native` for RID-less builds but at the +output root once a RID is applied, the hosts are emitted in both layouts; a RID +does not reliably flow to referenced projects, so the placement cannot depend on +it. +Linux and macOS local sessions use the bundled `forkpty` relay. The Avalonia shell, settings, renderer, and terminal engines are shared. @@ -400,9 +410,9 @@ that `Control` needs, or a project that needs direct access to ## Platform constraints -- Public out-of-process ConPTY can filter or alter DCS/APC payloads on some - Windows builds. Sixel works when the selected connection passes DCS bytes - through unchanged. +- The OS-provided ConPTY can filter graphics payloads on some Windows builds. + Local sessions use the bundled runtime to preserve Sixel; remote connections + must also transport image sequences unchanged. - Avalonia 12 exposes the terminal as a readable UIA Document/Value provider, but does not provide a public bridge for native UIA TextPattern/TextPattern2 or LiveSetting events. Managed ranges and visible notification text remain diff --git a/src/Devolutions.Terminal.Connection/ConPtyConnection.cs b/src/Devolutions.Terminal.Connection/ConPtyConnection.cs index ff097e9..d4d98da 100644 --- a/src/Devolutions.Terminal.Connection/ConPtyConnection.cs +++ b/src/Devolutions.Terminal.Connection/ConPtyConnection.cs @@ -158,7 +158,7 @@ public void Resize(int columns, int rows) if (pseudoConsole is not null && !pseudoConsole.IsInvalid && !pseudoConsole.IsClosed) { var size = new Kernel32.Coord { X = (short)validatedColumns, Y = (short)validatedRows }; - var hr = Kernel32.ResizePseudoConsole(pseudoConsole, size); + var hr = ConPty.ResizePseudoConsole(pseudoConsole, size); if (hr != 0) { Marshal.ThrowExceptionForHR(hr); @@ -225,7 +225,7 @@ private void StartCore(TerminalLaunchOptions options, CancellationToken cancella X = (short)options.Columns, Y = (short)options.Rows, }; - var hr = Kernel32.CreatePseudoConsole(size, inputRead, outputWrite, 0, out var pseudoConsoleValue); + var hr = ConPty.CreatePseudoConsole(size, inputRead, outputWrite, 0, out var pseudoConsoleValue); if (hr != 0) { Marshal.ThrowExceptionForHR(hr); diff --git a/src/Devolutions.Terminal.Connection/ConPtyHost.targets b/src/Devolutions.Terminal.Connection/ConPtyHost.targets new file mode 100644 index 0000000..dc8c81d --- /dev/null +++ b/src/Devolutions.Terminal.Connection/ConPtyHost.targets @@ -0,0 +1,34 @@ + + + $(MSBuildThisFileDirectory)..\tools\conpty + + + + + <_ConPtyArch Include="x86;x64;arm64" /> + <_ConPtyRuntime Include="win-x86;win-x64;win-arm64" Condition="'$(RuntimeIdentifier)' == ''" /> + + + + + + + + diff --git a/src/Devolutions.Terminal.Connection/Devolutions.Terminal.Connection.csproj b/src/Devolutions.Terminal.Connection/Devolutions.Terminal.Connection.csproj index d22e93a..7aa09bb 100644 --- a/src/Devolutions.Terminal.Connection/Devolutions.Terminal.Connection.csproj +++ b/src/Devolutions.Terminal.Connection/Devolutions.Terminal.Connection.csproj @@ -7,6 +7,11 @@ false + + + $(PkgMicrosoft_Windows_Console_ConPTY)\build\native\runtimes + + diff --git a/src/Devolutions.Terminal.Connection/Native/ConPty.cs b/src/Devolutions.Terminal.Connection/Native/ConPty.cs new file mode 100644 index 0000000..46a987f --- /dev/null +++ b/src/Devolutions.Terminal.Connection/Native/ConPty.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Microsoft.Win32.SafeHandles; + +namespace Devolutions.Terminal.Connection.Native; + +[SupportedOSPlatform("windows")] +internal static partial class ConPty +{ + [LibraryImport("conpty.dll", EntryPoint = "ConptyCreatePseudoConsole")] + internal static partial int CreatePseudoConsole(Kernel32.Coord size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out nint phPC); + + [LibraryImport("conpty.dll", EntryPoint = "ConptyResizePseudoConsole")] + internal static partial int ResizePseudoConsole(SafePseudoConsoleHandle hPC, Kernel32.Coord size); + + [LibraryImport("conpty.dll", EntryPoint = "ConptyClosePseudoConsole")] + internal static partial void ClosePseudoConsole(nint hPC); +} diff --git a/src/Devolutions.Terminal.Connection/Native/Kernel32.cs b/src/Devolutions.Terminal.Connection/Native/Kernel32.cs index e325e59..e66fccb 100644 --- a/src/Devolutions.Terminal.Connection/Native/Kernel32.cs +++ b/src/Devolutions.Terminal.Connection/Native/Kernel32.cs @@ -68,15 +68,6 @@ internal struct ProcessInformation public int dwThreadId; } - [LibraryImport("kernel32.dll", SetLastError = true)] - internal static partial int CreatePseudoConsole(Coord size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out nint phPC); - - [LibraryImport("kernel32.dll", SetLastError = true)] - internal static partial int ResizePseudoConsole(SafePseudoConsoleHandle hPC, Coord size); - - [LibraryImport("kernel32.dll", SetLastError = true)] - internal static partial void ClosePseudoConsole(nint hPC); - [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, nint lpPipeAttributes, uint nSize); diff --git a/src/Devolutions.Terminal.Connection/Native/SafeHandles.cs b/src/Devolutions.Terminal.Connection/Native/SafeHandles.cs index 11320c3..75d6dd4 100644 --- a/src/Devolutions.Terminal.Connection/Native/SafeHandles.cs +++ b/src/Devolutions.Terminal.Connection/Native/SafeHandles.cs @@ -21,7 +21,7 @@ internal SafePseudoConsoleHandle(nint handle) protected override bool ReleaseHandle() { - Kernel32.ClosePseudoConsole(handle); + ConPty.ClosePseudoConsole(handle); return true; } } diff --git a/src/Devolutions.Terminal.Connection/THIRD-PARTY-NOTICES-CONPTY.txt b/src/Devolutions.Terminal.Connection/THIRD-PARTY-NOTICES-CONPTY.txt new file mode 100644 index 0000000..cdec909 --- /dev/null +++ b/src/Devolutions.Terminal.Connection/THIRD-PARTY-NOTICES-CONPTY.txt @@ -0,0 +1,24 @@ +Microsoft Windows Console ConPTY +https://github.com/microsoft/terminal + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Devolutions.Terminal.Control/Devolutions.Terminal.Control.csproj b/src/Devolutions.Terminal.Control/Devolutions.Terminal.Control.csproj index 3e58d76..1e449fb 100644 --- a/src/Devolutions.Terminal.Control/Devolutions.Terminal.Control.csproj +++ b/src/Devolutions.Terminal.Control/Devolutions.Terminal.Control.csproj @@ -36,6 +36,7 @@ +