From 623597a326ca776d464c2cd0aebf30debceee37d Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Sun, 8 Nov 2026 19:30:45 -0600 Subject: [PATCH 1/6] Report measured terminal cell pixels through CSI queries --- CHANGELOG.md | 5 + Directory.Packages.props | 2 +- src/SharpMUTerm.Avalonia/TerminalView.cs | 29 ++- src/SharpMUTerm.Avalonia/WindowPointer.cs | 17 ++ src/SharpMUTerm.Core/Session/WorldSession.cs | 7 + src/SharpMUTerm.Core/SharpMUTerm.Core.csproj | 4 + src/SharpMUTerm.Core/Telnet/ITelnetSession.cs | 6 + src/SharpMUTerm.Core/Telnet/TelnetSession.cs | 11 + .../Telnet/TerminalCellQueryResponder.cs | 135 ++++++++++ .../Telnet/TerminalCellSize.cs | 7 + src/SharpMUTerm.Tui/Pointer/PointerTarget.cs | 6 + src/SharpMUTerm.Tui/SharpMUTermApp.cs | 36 +++ .../TerminalCellPixelTests.cs | 99 +++++++ .../Telnet/TerminalCellQueryTests.cs | 241 ++++++++++++++++++ .../RecordingTelnetSession.cs | 9 + 15 files changed, 611 insertions(+), 3 deletions(-) create mode 100644 src/SharpMUTerm.Core/Telnet/TerminalCellQueryResponder.cs create mode 100644 src/SharpMUTerm.Core/Telnet/TerminalCellSize.cs create mode 100644 tests/SharpMUTerm.Avalonia.Tests/TerminalCellPixelTests.cs create mode 100644 tests/SharpMUTerm.Core.Tests/Telnet/TerminalCellQueryTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b9f2b5..20e30aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,11 @@ and a release's notes are its section. The procedure is in [`docs/RELEASING.md`] ### Fixed +- The desktop window answers terminal cell-pixel queries with its actual measured pixels, allowing + servers to size images correctly. Servers that asked receive refreshed cell dimensions after zoom + or display-scaling changes. Replies do not add a command newline or forward server escapes to + the local terminal. + - Desktop MXP images use the measured font cell dimensions when sizing pictures, instead of assuming an 8×16 cell. This avoids stretching pictures when the selected font has a different shape. Explicit server-specified cell boxes stay unchanged, and terminal hosts without measured cells keep their existing sizing. diff --git a/Directory.Packages.props b/Directory.Packages.props index d32e792e..01490984 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -49,7 +49,7 @@ - + - + From 69ef0ebc684a6c2cff3a713917b3ce12f640fc63 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Sun, 8 Nov 2026 22:41:56 -0600 Subject: [PATCH 6/6] Synchronize native overlays with asynchronous modal notifications --- CHANGELOG.md | 2 ++ src/SharpMUTerm.Tui/SharpMUTermApp.cs | 6 +++-- .../NativePanelTests.cs | 26 ++++++++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8a5c1d..03c94538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ and a release's notes are its section. The procedure is in [`docs/RELEASING.md`] ### Fixed +- Desktop overlays follow modal visibility changes on the client UI thread, avoiding background-thread reads of changing pane layouts. + - The desktop window answers terminal cell-pixel queries with its actual measured pixels, allowing servers to size images correctly. Unix terminal mode also reports cells when the terminal publishes an exact pixel extent; unavailable dimensions stay unknown. Servers that asked receive refreshed diff --git a/src/SharpMUTerm.Tui/SharpMUTermApp.cs b/src/SharpMUTerm.Tui/SharpMUTermApp.cs index 66df2507..03bef7c3 100644 --- a/src/SharpMUTerm.Tui/SharpMUTermApp.cs +++ b/src/SharpMUTerm.Tui/SharpMUTermApp.cs @@ -1113,13 +1113,15 @@ public SharpMUTermApp( // A modal that covers the main window entirely leaves it unpainted, so no PostBufferPaint says the native // panels are now under it; the modal stack changing says it instead. - _system.ModalStateService.StateChanged += (_, _) => + // The framework delivers this notification on the thread pool. Native overlay models read + // UI-owned layout state, so publish on the same thread as frame-driven updates. + _system.ModalStateService.StateChanged += (_, _) => OnUiThread(() => { PublishPanels(); PublishInputBand(); PublishDividers(); PublishPaneChrome(); - }; + }); if (_sixel is not null) { // Another window over this one rewrites the cells a picture sits on, and the frame that puts diff --git a/tests/SharpMUTerm.Avalonia.Tests/NativePanelTests.cs b/tests/SharpMUTerm.Avalonia.Tests/NativePanelTests.cs index fe3d639f..f5b7c74b 100644 --- a/tests/SharpMUTerm.Avalonia.Tests/NativePanelTests.cs +++ b/tests/SharpMUTerm.Avalonia.Tests/NativePanelTests.cs @@ -154,10 +154,28 @@ public async Task WithNoSettingsWindowTheEditLinkOpensF4AndNoNativePanelCoversIt app.RenderSnapshot("layout-combat"); var panel = driver.Pointer.Panels.Single(p => p.PanelId == PanelIds.QuickBar); - panel.Dispatch!(QuickBarModel.EditCommand); - app.RenderNextFrame(); - await Assert.That(app.OpenSettingsKey).IsEqualTo(ConsoleKey.F4); - await Assert.That(driver.Pointer.Panels).IsEmpty().Because("a native view would sit over the modal's cells"); + var hidden = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + void ObservePanels() + { + if (driver.Pointer.Panels.Count == 0) { hidden.TrySetResult(); } + } + + driver.Pointer.PanelsChanged += ObservePanels; + try + { + panel.Dispatch!(QuickBarModel.EditCommand); + app.RenderNextFrame(); + await Assert.That(app.OpenSettingsKey).IsEqualTo(ConsoleKey.F4); + // A fullscreen modal occludes the main pane, so rendering cannot publish its overlays. + // Modal state notifications arrive asynchronously; await the actual host update rather + // than assuming a thread-pool callback has run before this assertion. + await hidden.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await Assert.That(driver.Pointer.Panels).IsEmpty().Because("a native view would sit over the modal's cells"); + } + finally + { + driver.Pointer.PanelsChanged -= ObservePanels; + } } private sealed class RecordingSettings : INativeSettings