Skip to content

Commit fc2bb08

Browse files
Detect SSH X11 forwarding for Avalonia decorations (#4769)
Use native Linux window decorations for WSL and SSH-forwarded X11 sessions to avoid frameless rendering issues. Add an override environment variable and log the selected decoration mode.
1 parent b48fef9 commit fc2bb08

1 file changed

Lines changed: 99 additions & 6 deletions

File tree

src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public enum PageType
4040

4141
public partial class MainWindow : Window
4242
{
43+
private const string FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE = "UNIGETUI_FORCE_NATIVE_LINUX_DECORATIONS";
44+
4345
// Workaround for Avalonia 12 issue #21160 / #21212: BorderOnly + ExtendClientArea
4446
// strips WS_CAPTION / WS_THICKFRAME, which makes DWM disable Aero Snap drag-to-top,
4547
// Win+Up, and the maximize/minimize/restore animations. Re-add those bits on every
@@ -242,14 +244,15 @@ private void SetupTitleBar()
242244
}
243245
else if (OperatingSystem.IsLinux())
244246
{
245-
// WSLg can report incorrect maximize/input bounds with frameless windows.
246-
// Keep native decorations there and use the in-app toolbar only.
247-
bool isWsl = IsRunningUnderWsl();
248-
WindowDecorations = isWsl ? WindowDecorations.Full : WindowDecorations.None;
247+
// WSLg and SSH-forwarded X11 can report incorrect maximize/input bounds
248+
// with frameless windows. Keep native decorations for those environments.
249+
bool useNativeDecorations = ShouldUseNativeLinuxWindowDecorations(out string decorationReason);
250+
Logger.Info($"Linux window decorations: {(useNativeDecorations ? "native" : "custom")} ({decorationReason})");
251+
WindowDecorations = useNativeDecorations ? WindowDecorations.Full : WindowDecorations.None;
249252
TitleBarGrid.ClearValue(HeightProperty);
250253
TitleBarGrid.Height = 44;
251254
HamburgerPanel.Margin = new Thickness(10, 0, 8, 0);
252-
LinuxWindowButtons.IsVisible = !isWsl;
255+
LinuxWindowButtons.IsVisible = !useNativeDecorations;
253256
MainContentGrid.Margin = new Thickness(0, 44, 0, 0);
254257
// Keep maximize icon in sync with window state
255258
this.GetObservable(WindowStateProperty).Subscribe(state =>
@@ -259,21 +262,111 @@ private void SetupTitleBar()
259262

260263
// Avalonia's X11 backend treats BorderOnly as None (no decorations at all).
261264
// Add invisible resize grips so the user can still resize by dragging edges.
262-
if (!isWsl)
265+
if (!useNativeDecorations)
263266
{
264267
CreateResizeGrips();
265268
}
266269

267270
}
268271
}
269272

273+
private static bool ShouldUseNativeLinuxWindowDecorations(out string reason)
274+
{
275+
if (TryGetNativeLinuxDecorationsOverride(out bool forceNativeDecorations))
276+
{
277+
reason = $"{FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE}={(forceNativeDecorations ? "true" : "false")}";
278+
return forceNativeDecorations;
279+
}
280+
281+
if (IsRunningUnderWsl())
282+
{
283+
reason = "WSL environment";
284+
return true;
285+
}
286+
287+
if (IsRunningUnderSshX11Forwarding())
288+
{
289+
reason = "SSH X11 forwarding";
290+
return true;
291+
}
292+
293+
reason = "default Linux desktop";
294+
return false;
295+
}
296+
297+
private static bool TryGetNativeLinuxDecorationsOverride(out bool forceNativeDecorations)
298+
{
299+
forceNativeDecorations = false;
300+
301+
string? overrideValue = Environment.GetEnvironmentVariable(FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE);
302+
if (string.IsNullOrWhiteSpace(overrideValue))
303+
{
304+
return false;
305+
}
306+
307+
switch (overrideValue.Trim().ToLowerInvariant())
308+
{
309+
case "1":
310+
case "true":
311+
case "on":
312+
case "yes":
313+
case "enabled":
314+
forceNativeDecorations = true;
315+
return true;
316+
317+
case "0":
318+
case "false":
319+
case "off":
320+
case "no":
321+
case "disabled":
322+
forceNativeDecorations = false;
323+
return true;
324+
325+
default:
326+
Logger.Warn($"Ignoring invalid {FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE} value '{overrideValue}'. Use true/false.");
327+
return false;
328+
}
329+
}
330+
270331
private static bool IsRunningUnderWsl()
271332
{
272333
string? wslDistro = Environment.GetEnvironmentVariable("WSL_DISTRO_NAME");
273334
string? wslInterop = Environment.GetEnvironmentVariable("WSL_INTEROP");
274335
return !string.IsNullOrWhiteSpace(wslDistro) || !string.IsNullOrWhiteSpace(wslInterop);
275336
}
276337

338+
private static bool IsRunningUnderSshX11Forwarding()
339+
{
340+
if (!OperatingSystem.IsLinux())
341+
{
342+
return false;
343+
}
344+
345+
string? display = Environment.GetEnvironmentVariable("DISPLAY");
346+
if (string.IsNullOrWhiteSpace(display))
347+
{
348+
return false;
349+
}
350+
351+
bool hasSshSession =
352+
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SSH_CONNECTION")) ||
353+
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SSH_CLIENT"));
354+
if (!hasSshSession)
355+
{
356+
return false;
357+
}
358+
359+
string normalizedDisplay = display.Trim();
360+
if (normalizedDisplay.StartsWith(":", StringComparison.Ordinal) ||
361+
normalizedDisplay.StartsWith("unix/", StringComparison.OrdinalIgnoreCase) ||
362+
normalizedDisplay.StartsWith("unix:", StringComparison.OrdinalIgnoreCase))
363+
{
364+
return false;
365+
}
366+
367+
return normalizedDisplay.Contains(':');
368+
}
369+
277370
/// <summary>
278371
/// Creates invisible resize-grip borders at the edges and corners of the window,
279372
/// enabling mouse-driven resize on platforms where native decorations are absent

0 commit comments

Comments
 (0)