LANCommander/LANCommander.SDK/Helpers/DisplayHelper.cs
Pat Hartl da94ff8438
Some checks failed
LANCommander Release / prep (push) Failing after 49s
LANCommander Release / build_server_linux_arm64 (push) Has been skipped
LANCommander Release / build_server_linux_x64 (push) Has been skipped
LANCommander Release / build_server_osx_arm64 (push) Has been skipped
LANCommander Release / build_server_osx_x64 (push) Has been skipped
LANCommander Release / build_server_win_arm64 (push) Has been skipped
LANCommander Release / build_server_win_x64 (push) Has been skipped
LANCommander Release / build_launcher_linux_arm64 (push) Has been skipped
LANCommander Release / build_launcher_linux_x64 (push) Has been skipped
LANCommander Release / build_launcher_osx_arm64 (push) Has been skipped
LANCommander Release / build_launcher_osx_x64 (push) Has been skipped
LANCommander Release / build_launcher_win_arm64 (push) Has been skipped
LANCommander Release / build_launcher_win_x64 (push) Has been skipped
LANCommander Release / build_packager (push) Has been skipped
LANCommander Release / build_release (push) Has been skipped
Fix detection of primary display's resolution on some Linux multi-display configurations
2026-07-27 00:26:12 -05:00

294 lines
10 KiB
C#

using LANCommander.SDK.PowerShell.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace LANCommander.SDK.Helpers
{
public class DisplayHelper
{
public static DEVMODE GetDeviceMode()
{
DEVMODE devMode = new DEVMODE();
devMode.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref devMode);
return devMode;
}
[DllImport("user32.dll")]
private static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);
private const int ENUM_CURRENT_SETTINGS = -1;
[StructLayout(LayoutKind.Sequential)]
public struct DEVMODE
{
private const int CCHDEVICENAME = 32;
private const int CCHFORMNAME = 32;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public int dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHFORMNAME)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmNup;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
public static Screen GetScreen()
{
var screen = new Screen();
var bounds = new Bounds();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var devMode = GetDeviceMode();
bounds.Width = devMode.dmPelsWidth;
bounds.Height = devMode.dmPelsHeight;
screen.RefreshRate = devMode.dmDisplayFrequency;
screen.BitsPerPixel = devMode.dmBitsPerPel;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Try each source in order of reliability.
// xrandr works on X11 and XWayland; xdpyinfo is a lighter X11 fallback;
// /sys/class/drm works without a display server and covers Wayland-only setups.
if (!TryGetScreenFromXrandr(out bounds, out var refreshRate, out var bitsPerPixel) &&
!TryGetScreenFromXdpyinfo(out bounds) &&
!TryGetScreenFromDrm(out bounds))
{
// Nothing worked — leave bounds at zero so callers can detect the failure.
}
screen.RefreshRate = refreshRate;
screen.BitsPerPixel = bitsPerPixel;
}
screen.Primary = true;
screen.Bounds = bounds;
screen.Width = bounds.Width;
screen.Height = bounds.Height;
return screen;
}
// ── Linux helpers ─────────────────────────────────────────────────────────
/// <summary>
/// Parses <c>xrandr</c> output to find the primary display's active resolution
/// and refresh rate. Works on X11 and XWayland.
///
/// We deliberately parse the primary output's connected line rather than the
/// "Screen 0: ... current W x H" summary, because that summary reports the
/// combined bounding box of all displays in a multi-monitor setup.
///
/// Example xrandr output:
/// <code>
/// Screen 0: minimum 16 x 16, current 4480 x 1440, maximum 32767 x 32767
/// DP-1 connected 1920x1200+2560+0 ...
/// 1920x1200 59.88*+
/// DP-3 connected primary 2560x1440+0+0 ...
/// 2560x1440 164.85*+
/// </code>
/// </summary>
private static bool TryGetScreenFromXrandr(out Bounds bounds, out int refreshRate, out int bitsPerPixel)
{
bounds = new Bounds();
refreshRate = 0;
bitsPerPixel = 24; // xrandr does not report bit depth; 24 is the near-universal default
try
{
var output = RunProcess("xrandr", "");
if (string.IsNullOrWhiteSpace(output))
return false;
var lines = output.Split('\n');
var connectedLine = @"^\S+\s+connected(\s+primary)?\s+(\d+)x(\d+)\+\d+\+\d+";
var primaryIndex = -1;
var fallbackIndex = -1;
for (var i = 0; i < lines.Length; i++)
{
var match = Regex.Match(lines[i], connectedLine);
if (!match.Success)
continue;
if (match.Groups[1].Success && primaryIndex == -1)
primaryIndex = i;
if (fallbackIndex == -1)
fallbackIndex = i;
}
var displayIndex = primaryIndex != -1 ? primaryIndex : fallbackIndex;
if (displayIndex == -1)
return false;
var displayMatch = Regex.Match(lines[displayIndex], connectedLine);
bounds.Width = int.Parse(displayMatch.Groups[2].Value);
bounds.Height = int.Parse(displayMatch.Groups[3].Value);
for (var i = displayIndex + 1; i < lines.Length; i++)
{
// A non-indented, non-empty line starts the next output's block.
if (lines[i].Length > 0 && !char.IsWhiteSpace(lines[i][0]))
break;
var refreshMatch = Regex.Match(lines[i], @"(\d+\.\d+)\*");
if (refreshMatch.Success &&
float.TryParse(refreshMatch.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var rate))
{
refreshRate = (int)Math.Round(rate);
break;
}
}
return bounds.Width > 0 && bounds.Height > 0;
}
catch
{
return false;
}
}
/// <summary>
/// Parses <c>xdpyinfo</c> output to find the screen dimensions.
/// Lighter than xrandr; does not provide refresh rate.
///
/// Example relevant line:
/// <code>
/// dimensions: 1920x1080 pixels (508x286 millimeters)
/// </code>
/// </summary>
private static bool TryGetScreenFromXdpyinfo(out Bounds bounds)
{
bounds = new Bounds();
try
{
var output = RunProcess("xdpyinfo", "");
if (string.IsNullOrWhiteSpace(output))
return false;
var match = Regex.Match(output, @"dimensions:\s+(\d+)x(\d+)\s+pixels");
if (!match.Success)
return false;
bounds.Width = int.Parse(match.Groups[1].Value);
bounds.Height = int.Parse(match.Groups[2].Value);
return bounds.Width > 0 && bounds.Height > 0;
}
catch
{
return false;
}
}
/// <summary>
/// Reads the preferred/current mode from the kernel DRM subsystem.
/// Works without any display server — covers pure Wayland and headless setups.
///
/// Each connected output exposes its mode list at
/// <c>/sys/class/drm/card*-*/modes</c>; the first line is the preferred mode.
/// </summary>
private static bool TryGetScreenFromDrm(out Bounds bounds)
{
bounds = new Bounds();
try
{
var modeFiles = Directory.GetFiles("/sys/class/drm", "modes", SearchOption.AllDirectories);
foreach (var file in modeFiles)
{
var firstLine = File.ReadLines(file).FirstOrDefault();
if (string.IsNullOrWhiteSpace(firstLine))
continue;
// Format: "1920x1080"
var match = Regex.Match(firstLine.Trim(), @"^(\d+)x(\d+)$");
if (!match.Success)
continue;
bounds.Width = int.Parse(match.Groups[1].Value);
bounds.Height = int.Parse(match.Groups[2].Value);
if (bounds.Width > 0 && bounds.Height > 0)
return true;
}
}
catch
{
// /sys may not be accessible in all environments
}
return false;
}
private static string RunProcess(string fileName, string arguments)
{
var psi = new ProcessStartInfo(fileName, arguments)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
using var process = Process.Start(psi);
if (process == null)
return string.Empty;
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return process.ExitCode == 0 ? output : string.Empty;
}
}
}