Skip to content

Commit f678e8d

Browse files
Fix Sixel rendering in Windows local sessions (#41)
## Summary - Bundle Microsoft's pinned ConPTY runtime with the matching `OpenConsole.exe` hosts so local Windows sessions preserve Sixel DCS payloads rather than silently dropping them through the system console host. - Default DECSDM to scrolling/cursor-anchored mode and convert Sixel image geometry from physical pixels to DIPs on scaled displays. - Deploy the hosts for app publishes and Control NuGet consumers; update third-party notice and protocol/release docs. ## Validation - Core advanced-protocol tests: 50 passed. - Renderer tests: 40 passed. - Connection contract tests, including the live ConPTY Sixel transport regression: 49 passed. - Visually confirmed `Get-XKCD -Show` renders the comic at the correct size and position. - Verified a win-x64 non-AOT publish and both RID-less and RID-specific NuGet-consumer host layouts. NativeAOT publishing was not verified locally because the required C++ linker workload is unavailable; CI should cover it. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 8efd7eb commit f678e8d

18 files changed

Lines changed: 213 additions & 38 deletions

File tree

‎Directory.Packages.props‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@
2121
<PackageVersion Include="xunit.v3" Version="3.2.2" />
2222
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
2323
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
24+
<PackageVersion Include="Microsoft.Windows.Console.ConPTY" Version="1.24.260710001" />
2425
</ItemGroup>
2526
</Project>

‎docs/advanced-vt-protocols.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Advanced VT protocols
22

33
> [!NOTE]
4-
> The public out-of-process Windows ConPTY may filter DCS payloads before they
5-
> reach a terminal client on some Windows builds, and APC sequences (kitty
6-
> graphics) can be affected the same way. The Core parser and renderer
7-
> support Sixel and kitty graphics when a connection transports those bytes
8-
> unchanged (for example, remote/Azure transports); local ConPTY support is
9-
> limited by the installed Windows pseudoconsole implementation.
4+
> Windows local sessions use the bundled Microsoft ConPTY runtime, including
5+
> `OpenConsole.exe`, rather than the OS copy that filters Sixel on some Windows
6+
> builds. With the built-in engine, Sixel producers such as `Get-XKCD -Show`
7+
> can render inline images. Other transports must likewise preserve graphics
8+
> payloads; the optional Ghostty engine does not expose image resources.
109
1110
`Devolutions.Terminal.Core` parses advanced string protocols without depending on Avalonia,
1211
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`
3635
followed by anything other than `\` aborts the DCS and starts the new escape
3736
sequence.
3837

38+
DECSDM defaults to reset (scrolling mode): Sixel images anchor at the cursor
39+
unless the application explicitly selects display mode with `CSI ? 80 h`.
40+
3941
## Limits
4042

4143
Limits are public constants on `TerminalImageLimits` and `VtResourceLimits`.

‎docs/release.md‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,17 @@ Commands are relative to the repository root.
1212
- WinApp CLI 0.6.0 for MSIX creation and validation
1313
- A trusted code-signing certificate for distributable MSIX artifacts
1414

15-
Windows local sessions use ConPTY. Linux and macOS local sessions use the
15+
Windows local sessions use the pinned `Microsoft.Windows.Console.ConPTY` NuGet
16+
runtime. Ship `conpty.dll` and the architecture-specific `OpenConsole.exe`
17+
subdirectories together: without its host, ConPTY can fall back to the OS copy
18+
and silently lose Sixel output. `ConPtyHost.targets` supplies these files for
19+
RID-less builds, RID-specific publishes, and Control NuGet consumers, including
20+
the ARM64 host required by x64 processes running under emulation. Because
21+
`conpty.dll` lands under `runtimes\<rid>\native` for RID-less builds but at the
22+
output root once a RID is applied, the hosts are emitted in both layouts; a RID
23+
does not reliably flow to referenced projects, so the placement cannot depend on
24+
it.
25+
Linux and macOS local sessions use the
1626
bundled `forkpty` relay. The Avalonia shell, settings, renderer, and terminal
1727
engines are shared.
1828

@@ -400,9 +410,9 @@ that `Control` needs, or a project that needs direct access to
400410

401411
## Platform constraints
402412

403-
- Public out-of-process ConPTY can filter or alter DCS/APC payloads on some
404-
Windows builds. Sixel works when the selected connection passes DCS bytes
405-
through unchanged.
413+
- The OS-provided ConPTY can filter graphics payloads on some Windows builds.
414+
Local sessions use the bundled runtime to preserve Sixel; remote connections
415+
must also transport image sequences unchanged.
406416
- Avalonia 12 exposes the terminal as a readable UIA Document/Value provider,
407417
but does not provide a public bridge for native UIA TextPattern/TextPattern2
408418
or LiveSetting events. Managed ranges and visible notification text remain

‎src/Devolutions.Terminal.Connection/ConPtyConnection.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public void Resize(int columns, int rows)
158158
if (pseudoConsole is not null && !pseudoConsole.IsInvalid && !pseudoConsole.IsClosed)
159159
{
160160
var size = new Kernel32.Coord { X = (short)validatedColumns, Y = (short)validatedRows };
161-
var hr = Kernel32.ResizePseudoConsole(pseudoConsole, size);
161+
var hr = ConPty.ResizePseudoConsole(pseudoConsole, size);
162162
if (hr != 0)
163163
{
164164
Marshal.ThrowExceptionForHR(hr);
@@ -225,7 +225,7 @@ private void StartCore(TerminalLaunchOptions options, CancellationToken cancella
225225
X = (short)options.Columns,
226226
Y = (short)options.Rows,
227227
};
228-
var hr = Kernel32.CreatePseudoConsole(size, inputRead, outputWrite, 0, out var pseudoConsoleValue);
228+
var hr = ConPty.CreatePseudoConsole(size, inputRead, outputWrite, 0, out var pseudoConsoleValue);
229229
if (hr != 0)
230230
{
231231
Marshal.ThrowExceptionForHR(hr);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<Project>
2+
<PropertyGroup>
3+
<ConPtyHostSourceRoot Condition="'$(ConPtyHostSourceRoot)' == ''">$(MSBuildThisFileDirectory)..\tools\conpty</ConPtyHostSourceRoot>
4+
</PropertyGroup>
5+
<!-- ConPTY resolves the host for the native OS architecture relative to conpty.dll,
6+
and silently falls back to the OS conhost.exe (which strips DCS/sixel) when it is
7+
missing. conpty.dll lands under runtimes\<rid>\native for RID-less builds and at the
8+
output root for RID/self-contained publishes, so both layouts are populated: a RID
9+
does not necessarily flow to this project when a referencing app is published.
10+
ARM64 hosts are included for x64/x86 processes running under Windows emulation. -->
11+
<Target Name="IncludeConPtyHosts" BeforeTargets="AssignTargetPaths">
12+
<ItemGroup>
13+
<_ConPtyArch Include="x86;x64;arm64" />
14+
<_ConPtyRuntime Include="win-x86;win-x64;win-arm64" Condition="'$(RuntimeIdentifier)' == ''" />
15+
<None Include="@(_ConPtyArch->'$(ConPtyHostSourceRoot)\%(Identity)\OpenConsole.exe')"
16+
Link="%(_ConPtyArch.Identity)\OpenConsole.exe"
17+
CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
18+
<None Include="@(_ConPtyRuntime->'$(ConPtyHostSourceRoot)\x86\OpenConsole.exe')"
19+
Condition="'%(_ConPtyRuntime.Identity)' == 'win-x86'"
20+
Link="runtimes\%(_ConPtyRuntime.Identity)\native\x86\OpenConsole.exe"
21+
CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
22+
<None Include="@(_ConPtyRuntime->'$(ConPtyHostSourceRoot)\x64\OpenConsole.exe')"
23+
Condition="'%(_ConPtyRuntime.Identity)' == 'win-x86' or '%(_ConPtyRuntime.Identity)' == 'win-x64'"
24+
Link="runtimes\%(_ConPtyRuntime.Identity)\native\x64\OpenConsole.exe"
25+
CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
26+
<None Include="@(_ConPtyRuntime->'$(ConPtyHostSourceRoot)\arm64\OpenConsole.exe')"
27+
Link="runtimes\%(_ConPtyRuntime.Identity)\native\arm64\OpenConsole.exe"
28+
CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
29+
<None Include="$(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES-CONPTY.txt"
30+
Link="THIRD-PARTY-NOTICES-CONPTY.txt"
31+
CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
32+
</ItemGroup>
33+
</Target>
34+
</Project>

‎src/Devolutions.Terminal.Connection/Devolutions.Terminal.Connection.csproj‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
<IsPackable>false</IsPackable>
88
</PropertyGroup>
99
<ItemGroup>
10+
<PackageReference Include="Microsoft.Windows.Console.ConPTY" GeneratePathProperty="true" ExcludeAssets="build" />
1011
<ProjectReference Include="..\Devolutions.Terminal.Core\Devolutions.Terminal.Core.csproj" />
1112
</ItemGroup>
13+
<PropertyGroup>
14+
<ConPtyHostSourceRoot>$(PkgMicrosoft_Windows_Console_ConPTY)\build\native\runtimes</ConPtyHostSourceRoot>
15+
</PropertyGroup>
16+
<Import Project="ConPtyHost.targets" />
1217
</Project>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Runtime.InteropServices;
2+
using System.Runtime.Versioning;
3+
using Microsoft.Win32.SafeHandles;
4+
5+
namespace Devolutions.Terminal.Connection.Native;
6+
7+
[SupportedOSPlatform("windows")]
8+
internal static partial class ConPty
9+
{
10+
[LibraryImport("conpty.dll", EntryPoint = "ConptyCreatePseudoConsole")]
11+
internal static partial int CreatePseudoConsole(Kernel32.Coord size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out nint phPC);
12+
13+
[LibraryImport("conpty.dll", EntryPoint = "ConptyResizePseudoConsole")]
14+
internal static partial int ResizePseudoConsole(SafePseudoConsoleHandle hPC, Kernel32.Coord size);
15+
16+
[LibraryImport("conpty.dll", EntryPoint = "ConptyClosePseudoConsole")]
17+
internal static partial void ClosePseudoConsole(nint hPC);
18+
}

‎src/Devolutions.Terminal.Connection/Native/Kernel32.cs‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,6 @@ internal struct ProcessInformation
6868
public int dwThreadId;
6969
}
7070

71-
[LibraryImport("kernel32.dll", SetLastError = true)]
72-
internal static partial int CreatePseudoConsole(Coord size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out nint phPC);
73-
74-
[LibraryImport("kernel32.dll", SetLastError = true)]
75-
internal static partial int ResizePseudoConsole(SafePseudoConsoleHandle hPC, Coord size);
76-
77-
[LibraryImport("kernel32.dll", SetLastError = true)]
78-
internal static partial void ClosePseudoConsole(nint hPC);
79-
8071
[LibraryImport("kernel32.dll", SetLastError = true)]
8172
[return: MarshalAs(UnmanagedType.Bool)]
8273
internal static partial bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, nint lpPipeAttributes, uint nSize);

‎src/Devolutions.Terminal.Connection/Native/SafeHandles.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal SafePseudoConsoleHandle(nint handle)
2121

2222
protected override bool ReleaseHandle()
2323
{
24-
Kernel32.ClosePseudoConsole(handle);
24+
ConPty.ClosePseudoConsole(handle);
2525
return true;
2626
}
2727
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Microsoft Windows Console ConPTY
2+
https://github.com/microsoft/terminal
3+
4+
Copyright (c) Microsoft Corporation. All rights reserved.
5+
6+
MIT License
7+
8+
Permission is hereby granted, free of charge, to any person obtaining a copy
9+
of this software and associated documentation files (the "Software"), to deal
10+
in the Software without restriction, including without limitation the rights
11+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
copies of the Software, and to permit persons to whom the Software is
13+
furnished to do so, subject to the following conditions:
14+
15+
The above copyright notice and this permission notice shall be included in all
16+
copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.

0 commit comments

Comments
 (0)